diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index 10e1699d..14c40403 100644 --- a/frontend/src/app/providers/models-provider.tsx +++ b/frontend/src/app/providers/models-provider.tsx @@ -223,8 +223,8 @@ const ModelsContext = createContext<{ validateEditMode: boolean; }>({ formData: initialFormState, - setFormData: () => { }, - handleChange: () => { }, + setFormData: () => {}, + handleChange: () => {}, createNewTrainingDatasetMutation: {} as UseMutationResult< TTrainingDataset, Error, @@ -239,13 +239,13 @@ const ModelsContext = createContext<{ >, hasLabeledTrainingAreas: false, hasAOIsWithGeometry: false, - resetState: () => { }, + resetState: () => {}, isEditMode: false, modelId: "", getFullPath: () => "", - handleModelCreationAndUpdate: () => { }, + handleModelCreationAndUpdate: () => {}, trainingDatasetCreationInProgress: false, - handleTrainingDatasetCreation: () => { }, + handleTrainingDatasetCreation: () => {}, validateEditMode: false, }); diff --git a/frontend/src/app/routes/account/models.tsx b/frontend/src/app/routes/account/models.tsx index 2ab974db..e6e1edc1 100644 --- a/frontend/src/app/routes/account/models.tsx +++ b/frontend/src/app/routes/account/models.tsx @@ -4,9 +4,20 @@ import { APPLICATION_CONTENTS } from "@/contents"; import { LayoutView } from "@/enums/models"; import { LayoutToggle, PageHeader } from "@/features/models/components"; import { MobileModelFiltersDialog } from "@/features/models/components/dialogs"; -import { CategoryFilter, ClearFilters, DateRangeFilter, MobileFilter, OrderingFilter, SearchFilter } from "@/features/models/components/filters"; +import { + CategoryFilter, + ClearFilters, + DateRangeFilter, + MobileFilter, + OrderingFilter, + SearchFilter, + StatusFilter, +} from "@/features/models/components/filters"; import { useModelsListFilters } from "@/features/models/hooks/use-models"; -import { ModelListGridLayout, ModelListTableLayout } from "@/features/models/layouts"; +import { + ModelListGridLayout, + ModelListTableLayout, +} from "@/features/models/layouts"; import { useDialog } from "@/hooks/use-dialog"; import { APP_CONTENT } from "@/utils"; import { useMemo } from "react"; @@ -14,13 +25,19 @@ import ModelNotFound from "@/features/models/components/model-not-found"; import { SEARCH_PARAMS } from "@/app/routes/models/models-list"; import { useAuth } from "@/app/providers/auth-provider"; - export const UserModelsPage = () => { - const { isOpened, openDialog, closeDialog } = useDialog(); const { user } = useAuth(); - const { clearAllFilters, data, isError, isPending, isPlaceholderData, query, updateQuery } = useModelsListFilters(user.osm_id) + const { + clearAllFilters, + data, + isError, + isPending, + isPlaceholderData, + query, + updateQuery, + } = useModelsListFilters(user?.osm_id); // Since it's just a static filter, it's better to memoize it. const memoizedCategoryFilter = useMemo( @@ -28,7 +45,6 @@ export const UserModelsPage = () => { [isPending], ); - const renderContent = () => { if (data?.count === 0) { return ( @@ -69,7 +85,10 @@ export const UserModelsPage = () => { />
- + {/* Filters */}
@@ -77,6 +96,11 @@ export const UserModelsPage = () => {
{memoizedCategoryFilter} + {/* Mobile filters */}
@@ -96,10 +120,7 @@ export const UserModelsPage = () => {
{/* Desktop */} - +
{/* Mobile */} @@ -149,7 +170,9 @@ export const UserModelsPage = () => {
{renderContent()}
@@ -169,5 +192,5 @@ export const UserModelsPage = () => {
- ) + ); }; diff --git a/frontend/src/app/routes/models/models-list.tsx b/frontend/src/app/routes/models/models-list.tsx index c427c872..fb0bb625 100644 --- a/frontend/src/app/routes/models/models-list.tsx +++ b/frontend/src/app/routes/models/models-list.tsx @@ -2,12 +2,16 @@ import { useModelsListFilters, useModelsMapData, } from "@/features/models/hooks/use-models"; -import { useMemo, } from "react"; +import { useMemo } from "react"; import { ModelListGridLayout, ModelListTableLayout, } from "@/features/models/layouts"; -import { LayoutToggle, ModelMapToggle, ModelsMap } from "@/features/models/components"; +import { + LayoutToggle, + ModelMapToggle, + ModelsMap, +} from "@/features/models/components"; import { CategoryFilter, ClearFilters, @@ -17,16 +21,15 @@ import { SearchFilter, } from "@/features/models/components/filters"; import Pagination, { PAGE_LIMIT } from "@/components/pagination"; -import { APP_CONTENT, } from "@/utils"; +import { APP_CONTENT } from "@/utils"; import { PageHeader } from "@/features/models/components/"; -import { FeatureCollection, } from "@/types"; +import { FeatureCollection } from "@/types"; import ModelNotFound from "@/features/models/components/model-not-found"; import { useDialog } from "@/hooks/use-dialog"; import { MobileModelFiltersDialog } from "@/features/models/components/dialogs"; import { Head } from "@/components/seo"; import { LayoutView } from "@/enums/models"; - export const SEARCH_PARAMS = { startDate: "start_date", endDate: "end_date", @@ -37,16 +40,22 @@ export const SEARCH_PARAMS = { dateFilter: "dateFilter", layout: "layout", id: "id", + status: "status", }; - - - export const ModelsPage = () => { - const { isOpened, openDialog, closeDialog } = useDialog(); - const { clearAllFilters, data, isError, isPending, isPlaceholderData, query, updateQuery, mapViewIsActive } = useModelsListFilters() + const { + clearAllFilters, + data, + isError, + isPending, + isPlaceholderData, + query, + updateQuery, + mapViewIsActive, + } = useModelsListFilters(); const { data: mapData, @@ -60,8 +69,6 @@ export const ModelsPage = () => { [isPending], ); - - const renderContent = () => { if (data?.count === 0) { return ( diff --git a/frontend/src/components/ui/dropdown/dropdown.tsx b/frontend/src/components/ui/dropdown/dropdown.tsx index 6daf7f32..1ff5ea64 100644 --- a/frontend/src/components/ui/dropdown/dropdown.tsx +++ b/frontend/src/components/ui/dropdown/dropdown.tsx @@ -13,7 +13,7 @@ export type DropdownMenuItem = { className?: string; name?: string; disabled?: boolean; - apiValue?: string; + apiValue?: string | number; }; type DropDownProps = { diff --git a/frontend/src/components/ui/form/checkbox-group/checkbox-group.tsx b/frontend/src/components/ui/form/checkbox-group/checkbox-group.tsx index c2d12640..3007a59d 100644 --- a/frontend/src/components/ui/form/checkbox-group/checkbox-group.tsx +++ b/frontend/src/components/ui/form/checkbox-group/checkbox-group.tsx @@ -2,11 +2,12 @@ import { cn } from "@/utils"; import { SlCheckbox } from "@shoelace-style/shoelace/dist/react/index.js"; import { useEffect, useState } from "react"; import "./checkbox-group.css"; +import { SHOELACE_SIZES } from "@/enums"; type CheckboxGroupProps = { options: { value: string; - apiValue?: string; + apiValue?: string | number; }[]; disabled?: boolean; defaultSelectedOption?: string | string[] | number[]; @@ -68,7 +69,8 @@ const CheckboxGroup: React.FC = ({
  • ; status: number; id: number; - userId?: number + userId?: number; }; export const getModelsQueryOptions = ({ @@ -35,7 +35,7 @@ export const getModelsQueryOptions = ({ orderBy, dateFilters, id, - userId + userId, }: TModelQueryOptions) => { return queryOptions({ queryKey: [ @@ -43,7 +43,16 @@ export const getModelsQueryOptions = ({ { status, searchQuery, offset, orderBy, dateFilters, id, userId }, ], queryFn: () => - getModels(limit, offset, orderBy, status, searchQuery, dateFilters, id, userId), + getModels( + limit, + offset, + orderBy, + status, + searchQuery, + dateFilters, + id, + userId, + ), placeholderData: keepPreviousData, }); }; diff --git a/frontend/src/features/models/api/get-models.ts b/frontend/src/features/models/api/get-models.ts index e556cc54..9aa9124a 100644 --- a/frontend/src/features/models/api/get-models.ts +++ b/frontend/src/features/models/api/get-models.ts @@ -9,7 +9,7 @@ export const getModels = async ( searchQuery: string, dateFilters: Record, id: number, - userId?: number + userId?: number, ): Promise => { const res = await apiClient.get(API_ENDPOINTS.GET_MODELS, { params: { diff --git a/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx b/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx index 8bf65595..c0b659a8 100644 --- a/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx +++ b/frontend/src/features/models/components/dialogs/mobile-filters-dialog.tsx @@ -4,9 +4,12 @@ import { CategoryFilter, DateRangeFilter, OrderingFilter, + StatusFilter, } from "@/features/models/components/filters"; import { DialogProps, TQueryParams } from "@/types"; import { Button } from "@/components/ui/button"; +import { useLocation } from "react-router-dom"; +import { APPLICATION_ROUTES } from "@/utils"; type TrainingAreaDialogProps = DialogProps & { updateQuery: (updatedParams: TQueryParams) => void; @@ -36,6 +39,11 @@ const MobileModelFiltersDialog: React.FC = ({ updateQuery, disabled, }) => { + const currentRoute = useLocation(); + const userIsInAccountModelsPage = currentRoute.pathname.includes( + APPLICATION_ROUTES.ACCOUNT_MODELS, + ); + return (
    @@ -49,6 +57,18 @@ const MobileModelFiltersDialog: React.FC = ({ + + {userIsInAccountModelsPage && ( + + + + )} + ) => void; - query: TQueryParams; - isMobile?: boolean; + clearAllFilters: (event: React.ChangeEvent) => void; + query: TQueryParams; + isMobile?: boolean; }) => { - const canClearAllFilters = Boolean( - query[SEARCH_PARAMS.searchQuery] || - query[SEARCH_PARAMS.startDate] || - query[SEARCH_PARAMS.endDate] || - query[SEARCH_PARAMS.id], - ); + const canClearAllFilters = Boolean( + query[SEARCH_PARAMS.searchQuery] || + query[SEARCH_PARAMS.startDate] || + query[SEARCH_PARAMS.endDate] || + query[SEARCH_PARAMS.id], + ); - return ( -
    - {canClearAllFilters ? ( - // @ts-expect-error bad type definition - - ) : null} -
    - ); + return ( +
    + {canClearAllFilters ? ( + // @ts-expect-error bad type definition + + ) : null} +
    + ); }; -export default ClearFilters \ No newline at end of file +export default ClearFilters; diff --git a/frontend/src/features/models/components/filters/index.ts b/frontend/src/features/models/components/filters/index.ts index ae7cf1b0..94d47bcd 100644 --- a/frontend/src/features/models/components/filters/index.ts +++ b/frontend/src/features/models/components/filters/index.ts @@ -2,5 +2,6 @@ export { default as OrderingFilter } from "./ordering-filter"; export { default as DateRangeFilter } from "./date-range-filter"; export { default as CategoryFilter } from "./category-filter"; export { default as SearchFilter } from "./search-filter"; -export { default as ClearFilters } from './clear-filters' -export { default as MobileFilter } from './mobile-filter' \ No newline at end of file +export { default as ClearFilters } from "./clear-filters"; +export { default as MobileFilter } from "./mobile-filter"; +export { default as StatusFilter } from "./status-filter"; diff --git a/frontend/src/features/models/components/filters/mobile-filter.tsx b/frontend/src/features/models/components/filters/mobile-filter.tsx index 28554b48..fcd5c50a 100644 --- a/frontend/src/features/models/components/filters/mobile-filter.tsx +++ b/frontend/src/features/models/components/filters/mobile-filter.tsx @@ -1,22 +1,22 @@ import { FilterIcon } from "@/components/ui/icons"; const MobileFilter = ({ - openMobileFilterModal, + openMobileFilterModal, }: { - openMobileFilterModal: () => void; - isMobile?: boolean; + openMobileFilterModal: () => void; + isMobile?: boolean; }) => { - return ( -
    - {} -
    - ); + return ( +
    + {} +
    + ); }; -export default MobileFilter +export default MobileFilter; diff --git a/frontend/src/features/models/components/filters/status-filter.tsx b/frontend/src/features/models/components/filters/status-filter.tsx new file mode 100644 index 00000000..caeee790 --- /dev/null +++ b/frontend/src/features/models/components/filters/status-filter.tsx @@ -0,0 +1,97 @@ +import { SEARCH_PARAMS } from "@/app/routes/models/models-list"; +import { DropDown } from "@/components/ui/dropdown"; +import { DropdownMenuItem } from "@/components/ui/dropdown/dropdown"; +import { CheckboxGroup } from "@/components/ui/form"; +import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; +import { useMemo } from "react"; + +type StatusFilterProps = { + disabled: boolean; + isMobileFilterModal?: boolean; + updateQuery: (param: any) => void; + query: Record; +}; + +const StatusFilter: React.FC = ({ + disabled, + isMobileFilterModal = false, + updateQuery, + query, +}) => { + const statusCategories: DropdownMenuItem[] = [ + { + value: "Published", + apiValue: 0, + onClick() { + updateQuery({ + [SEARCH_PARAMS.status]: 0, + }); + }, + }, + { + value: "Draft", + apiValue: -1, + onClick() { + updateQuery({ + [SEARCH_PARAMS.status]: -1, + }); + }, + }, + { + value: "Archived", + apiValue: 1, + onClick() { + updateQuery({ + [SEARCH_PARAMS.status]: 1, + }); + }, + }, + ]; + const categoryLabel = useMemo( + () => + statusCategories.filter( + (status) => status.apiValue === query[SEARCH_PARAMS.status], + ), + [query], + ); + + const { dropdownIsOpened, onDropdownHide, onDropdownShow } = + useDropdownMenu(); + + if (!isMobileFilterModal) { + return ( +
    + null} + disabled={disabled} + defaultSelectedItem={categoryLabel[0]?.value} + withCheckbox + menuItemTextSize="small" + triggerComponent={ +

    + {categoryLabel[0]?.value} +

    + } + >
    +
    + ); + } + return ( + { + updateQuery({ + [SEARCH_PARAMS.status]: status[0], + }); + }} + defaultSelectedOption={categoryLabel[0]?.apiValue as string} + > + ); +}; + +export default StatusFilter; diff --git a/frontend/src/features/models/components/header.tsx b/frontend/src/features/models/components/header.tsx index ec96d7d2..e304101d 100644 --- a/frontend/src/features/models/components/header.tsx +++ b/frontend/src/features/models/components/header.tsx @@ -3,7 +3,13 @@ import { AddIcon } from "@/components/ui/icons"; import { APP_CONTENT, APPLICATION_ROUTES } from "@/utils"; import { useNavigate } from "react-router-dom"; -const PageHeader = ({ title, description }: { title?: string, description?: string }) => { +const PageHeader = ({ + title, + description, +}: { + title?: string; + description?: string; +}) => { const navigate = useNavigate(); const handleClick = () => { navigate(APPLICATION_ROUTES.CREATE_NEW_MODEL); diff --git a/frontend/src/features/models/components/index.ts b/frontend/src/features/models/components/index.ts index 08f521df..7555d1ba 100644 --- a/frontend/src/features/models/components/index.ts +++ b/frontend/src/features/models/components/index.ts @@ -9,5 +9,5 @@ export { default as ModelDetailItem } from "./model-detail-item"; export { default as ModelDetailsProperties } from "./model-details-properties"; export { default as TrainingHistoryTable } from "./training-history-table"; export { default as ModelDetailsPopUp } from "./model-details-popup"; -export { default as LayoutToggle } from './layout-toggle' -export { default as ModelMapToggle } from './map-toggle' \ No newline at end of file +export { default as LayoutToggle } from "./layout-toggle"; +export { default as ModelMapToggle } from "./map-toggle"; diff --git a/frontend/src/features/models/components/layout-toggle.tsx b/frontend/src/features/models/components/layout-toggle.tsx index cb8f137a..a4f06d5d 100644 --- a/frontend/src/features/models/components/layout-toggle.tsx +++ b/frontend/src/features/models/components/layout-toggle.tsx @@ -4,39 +4,38 @@ import { LayoutView } from "@/enums/models"; import { TQueryParams } from "@/types"; const LayoutToggle = ({ - query, - updateQuery, - isMobile, - disabled = false, + query, + updateQuery, + isMobile, + disabled = false, }: { - updateQuery: (params: TQueryParams) => void; - query: TQueryParams; - isMobile?: boolean; - disabled?: boolean; + updateQuery: (params: TQueryParams) => void; + query: TQueryParams; + isMobile?: boolean; + disabled?: boolean; }) => { - const activeLayout = query[SEARCH_PARAMS.layout]; - return ( - - ); + const activeLayout = query[SEARCH_PARAMS.layout]; + return ( + + ); }; - -export default LayoutToggle \ No newline at end of file +export default LayoutToggle; diff --git a/frontend/src/features/models/components/map-toggle.tsx b/frontend/src/features/models/components/map-toggle.tsx index 287a1afd..1eb3c02a 100644 --- a/frontend/src/features/models/components/map-toggle.tsx +++ b/frontend/src/features/models/components/map-toggle.tsx @@ -5,34 +5,33 @@ import { TQueryParams } from "@/types"; import { APP_CONTENT } from "@/utils"; const ModelMapToggle = ({ - query, - updateQuery, - isMobile, + query, + updateQuery, + isMobile, }: { - updateQuery: (params: TQueryParams) => void; - query: TQueryParams; - isMobile?: boolean; + updateQuery: (params: TQueryParams) => void; + query: TQueryParams; + isMobile?: boolean; }) => { - return ( -
    -

    - {APP_CONTENT.models.modelsList.filtersSection.mapViewToggleText} -

    - { - updateQuery({ - [SEARCH_PARAMS.mapIsActive]: !query[SEARCH_PARAMS.mapIsActive], - }); - }} - /> -
    - ); + return ( +
    +

    + {APP_CONTENT.models.modelsList.filtersSection.mapViewToggleText} +

    + { + updateQuery({ + [SEARCH_PARAMS.mapIsActive]: !query[SEARCH_PARAMS.mapIsActive], + }); + }} + /> +
    + ); }; - -export default ModelMapToggle \ No newline at end of file +export default ModelMapToggle; diff --git a/frontend/src/features/models/components/model-card.tsx b/frontend/src/features/models/components/model-card.tsx index 4338333b..154f3335 100644 --- a/frontend/src/features/models/components/model-card.tsx +++ b/frontend/src/features/models/components/model-card.tsx @@ -49,7 +49,7 @@ const ModelCard: React.FC = ({ model }) => { {APP_CONTENT.models.modelsList.modelCard.accuracy}

    - {roundNumber(model.accuracy)} % + {roundNumber(model.accuracy ?? 0)} %

    {/* Name, date and base model */} diff --git a/frontend/src/features/models/components/training-history-table.tsx b/frontend/src/features/models/components/training-history-table.tsx index e288dc1d..0b7cfc30 100644 --- a/frontend/src/features/models/components/training-history-table.tsx +++ b/frontend/src/features/models/components/training-history-table.tsx @@ -107,7 +107,7 @@ const columnDefinitions = ( return ( {Number(row.getValue("accuracy")) > 0 - ? roundNumber(row.getValue("accuracy")) + ? roundNumber(row.getValue("accuracy") ?? 0) : "-"} ); diff --git a/frontend/src/features/models/hooks/use-models.ts b/frontend/src/features/models/hooks/use-models.ts index e48fb63a..6449d487 100644 --- a/frontend/src/features/models/hooks/use-models.ts +++ b/frontend/src/features/models/hooks/use-models.ts @@ -23,7 +23,7 @@ type UseModelsOptions = { dateFilters: Record; status?: number; id: number; - userId?: number + userId?: number; }; export const useModels = ({ @@ -34,7 +34,7 @@ export const useModels = ({ searchQuery, dateFilters, id, - userId + userId, }: UseModelsOptions) => { return useQuery({ ...getModelsQueryOptions({ @@ -45,7 +45,7 @@ export const useModels = ({ searchQuery, dateFilters, id, - userId + userId, }), //@ts-expect-error bad type definition throwOnError: (error) => error.response?.status >= 500, @@ -78,8 +78,6 @@ export const useModelsMapData = () => { }); }; - - export const useModelsListFilters = (userId?: number) => { const [searchParams, setSearchParams] = useSearchParams(); @@ -99,10 +97,10 @@ export const useModelsListFilters = (userId?: number) => { [SEARCH_PARAMS.layout]: searchParams.get(SEARCH_PARAMS.layout) || LayoutView.GRID, [SEARCH_PARAMS.id]: searchParams.get(SEARCH_PARAMS.id) || "", + [SEARCH_PARAMS.status]: searchParams.get(SEARCH_PARAMS.status) || 0, }; const [query, setQuery] = useState(defaultQueries); - const debouncedSearchText = useDebounce( query[SEARCH_PARAMS.searchQuery] as string, 300, @@ -121,7 +119,8 @@ export const useModelsListFilters = (userId?: number) => { query[SEARCH_PARAMS.startDate] as string, query[SEARCH_PARAMS.endDate] as string, ), - userId: userId + userId: userId, + status: query[SEARCH_PARAMS.status] as number, }); const updateQuery = useCallback( @@ -156,8 +155,6 @@ export const useModelsListFilters = (userId?: number) => { } }, [query]); - - useEffect(() => { const newQuery = { [SEARCH_PARAMS.offset]: defaultQueries[SEARCH_PARAMS.offset], @@ -169,6 +166,7 @@ export const useModelsListFilters = (userId?: number) => { [SEARCH_PARAMS.layout]: defaultQueries[SEARCH_PARAMS.layout], [SEARCH_PARAMS.searchQuery]: defaultQueries[SEARCH_PARAMS.searchQuery], [SEARCH_PARAMS.id]: defaultQueries[SEARCH_PARAMS.id], + [SEARCH_PARAMS.status]: defaultQueries[SEARCH_PARAMS.status], }; setQuery(newQuery); }, []); @@ -192,7 +190,14 @@ export const useModelsListFilters = (userId?: number) => { })); }, []); - - return { query, data, isPending, isPlaceholderData, isError, updateQuery, mapViewIsActive, clearAllFilters } - -} \ No newline at end of file + return { + query, + data, + isPending, + isPlaceholderData, + isError, + updateQuery, + mapViewIsActive, + clearAllFilters, + }; +}; diff --git a/frontend/src/features/models/layouts/table.tsx b/frontend/src/features/models/layouts/table.tsx index fa9af710..180becf5 100644 --- a/frontend/src/features/models/layouts/table.tsx +++ b/frontend/src/features/models/layouts/table.tsx @@ -55,7 +55,7 @@ const columnDefinitions: ColumnDef[] = [ ), cell: ({ row }) => { - return {roundNumber(row.getValue("accuracy"))}; + return {roundNumber(row.getValue("accuracy") ?? 0)}; }, }, ]; diff --git a/frontend/src/lib/geojson2xml.ts b/frontend/src/lib/geojson2xml.ts index d6030ea0..52de6057 100644 --- a/frontend/src/lib/geojson2xml.ts +++ b/frontend/src/lib/geojson2xml.ts @@ -1,35 +1,35 @@ -import { create } from 'xmlbuilder2'; -import { FeatureCollection } from '@/types'; +import { create } from "xmlbuilder2"; +import { FeatureCollection } from "@/types"; class Node { lat: number; lon: number; tags: Record; - id: number + id: number; constructor(coordinates: [number, number]) { this.lat = coordinates[1]; this.lon = coordinates[0]; this.tags = {}; - this.id = 0 + this.id = 0; } } class Way { tags: Record; nodes: Node[]; - id: number + id: number; constructor(properties: Record) { this.tags = properties; this.nodes = []; - this.id = 0 + this.id = 0; } } class Relation { tags: Record; - members: { elem: Way; type: 'way'; role: 'outer' | 'inner' }[]; + members: { elem: Way; type: "way"; role: "outer" | "inner" }[]; constructor(properties: Record) { this.tags = properties; @@ -38,8 +38,8 @@ class Relation { } function geojson2osm(geojson: FeatureCollection): string { - if (!geojson || geojson.type !== 'FeatureCollection') { - throw new Error('Invalid GeoJSON FeatureCollection'); + if (!geojson || geojson.type !== "FeatureCollection") { + throw new Error("Invalid GeoJSON FeatureCollection"); } const nodes: Node[] = []; @@ -48,7 +48,7 @@ function geojson2osm(geojson: FeatureCollection): string { const relations: Relation[] = []; geojson.features.forEach((feature) => { - if (feature.geometry.type === 'Polygon') { + if (feature.geometry.type === "Polygon") { const properties = feature.properties || {}; processPolygon( feature.geometry.coordinates as [[[number, number]]], @@ -56,39 +56,49 @@ function geojson2osm(geojson: FeatureCollection): string { relations, ways, nodes, - nodesIndex + nodesIndex, ); } }); - const doc = create({ declaration: false }) - .ele('osm', { version: '0.6', generator: 'geojson2osm' }); + const doc = create({ declaration: false }).ele("osm", { + version: "0.6", + generator: "geojson2osm", + }); let lastNodeId = -1; nodes.forEach((node) => { - const nodeEl = doc.ele('node', { id: lastNodeId--, lat: node.lat, lon: node.lon }); + const nodeEl = doc.ele("node", { + id: lastNodeId--, + lat: node.lat, + lon: node.lon, + }); Object.entries(node.tags).forEach(([k, v]) => { - nodeEl.ele('tag', { k, v }); + nodeEl.ele("tag", { k, v }); }); }); let lastWayId = -1; ways.forEach((way) => { - const wayEl = doc.ele('way', { id: lastWayId-- }); - way.nodes.forEach((node) => wayEl.ele('nd', { ref: node.id })); + const wayEl = doc.ele("way", { id: lastWayId-- }); + way.nodes.forEach((node) => wayEl.ele("nd", { ref: node.id })); Object.entries(way.tags).forEach(([k, v]) => { - wayEl.ele('tag', { k, v }); + wayEl.ele("tag", { k, v }); }); }); let lastRelationId = -1; relations.forEach((relation) => { - const relationEl = doc.ele('relation', { id: lastRelationId-- }); + const relationEl = doc.ele("relation", { id: lastRelationId-- }); relation.members.forEach((member) => { - relationEl.ele('member', { type: member.type, ref: member.elem.id, role: member.role }); + relationEl.ele("member", { + type: member.type, + ref: member.elem.id, + role: member.role, + }); }); Object.entries(relation.tags).forEach(([k, v]) => { - relationEl.ele('tag', { k, v }); + relationEl.ele("tag", { k, v }); }); }); @@ -101,16 +111,20 @@ function processPolygon( relations: Relation[], ways: Way[], nodes: Node[], - nodesIndex: Record + nodesIndex: Record, ): void { const relation = new Relation(properties); - relation.tags.type = 'multipolygon'; + relation.tags.type = "multipolygon"; relations.push(relation); coordinates.forEach((polygon, index) => { const way = new Way({}); ways.push(way); - relation.members.push({ elem: way, type: 'way', role: index === 0 ? 'outer' : 'inner' }); + relation.members.push({ + elem: way, + type: "way", + role: index === 0 ? "outer" : "inner", + }); polygon.forEach((point) => { const nodeHash = JSON.stringify(point); diff --git a/frontend/src/utils/geometry-utils.ts b/frontend/src/utils/geometry-utils.ts index ee1b2437..5212caba 100644 --- a/frontend/src/utils/geometry-utils.ts +++ b/frontend/src/utils/geometry-utils.ts @@ -322,7 +322,6 @@ export const handleConflation = ( existingPredictions: TModelPredictions, newFeatures: Feature[], ): TModelPredictions => { - let updatedAll = [...existingPredictions.all]; newFeatures.forEach((newFeature) => { @@ -333,13 +332,11 @@ export const handleConflation = ( (rejectedFeature) => booleanIntersects(newFeature, rejectedFeature), ); - const intersectingIndex = updatedAll.findIndex((existingFeature) => booleanIntersects(newFeature, existingFeature), ); if (intersectingIndex !== -1) { - updatedAll[intersectingIndex] = { ...newFeature, properties: { @@ -347,11 +344,7 @@ export const handleConflation = ( id: updatedAll[intersectingIndex].properties?.id || uuid4(), }, }; - } else if ( - !intersectsWithAccepted && - !intersectsWithRejected - ) { - + } else if (!intersectsWithAccepted && !intersectsWithRejected) { updatedAll.push({ ...newFeature, properties: { diff --git a/frontend/src/utils/number-utils.ts b/frontend/src/utils/number-utils.ts index 4aabbfe8..df32f4d8 100644 --- a/frontend/src/utils/number-utils.ts +++ b/frontend/src/utils/number-utils.ts @@ -10,5 +10,5 @@ * @returns {string} - The rounded number as a string. */ export const roundNumber = (num: number, round: number = 2): number => { - return Number(num.toFixed(round) ?? 0); + return Number(num.toFixed(round)); };