From e4ed537da9eeab9e84dafc340bb5cd4b0255afcd Mon Sep 17 00:00:00 2001 From: Mikael Moilanen Date: Tue, 5 Nov 2024 16:15:05 +0200 Subject: [PATCH 1/2] Fix map extent setting and refactor --- backend/src/components/projectObject/index.ts | 10 +- frontend/src/components/Map/DrawMap.tsx | 434 +++++++++++++++++ frontend/src/components/Map/MapControls.tsx | 8 +- frontend/src/components/Map/MapWrapper.tsx | 438 ++---------------- .../src/components/Map/SearchResultsMap.tsx | 90 ++++ frontend/src/stores/navigationBlocker.tsx | 8 +- .../DetailplanProject/DetailplanProject.tsx | 33 +- .../MaintenanceProject/MaintenanceProject.tsx | 76 ++- .../src/views/Project/InvestmentProject.tsx | 77 ++- frontend/src/views/Project/ResultsMap.tsx | 5 +- .../src/views/ProjectObject/ProjectObject.tsx | 86 ++-- .../ProjectObject/ProjectObjectResultsMap.tsx | 5 +- 12 files changed, 701 insertions(+), 569 deletions(-) create mode 100644 frontend/src/components/Map/DrawMap.tsx create mode 100644 frontend/src/components/Map/SearchResultsMap.tsx diff --git a/backend/src/components/projectObject/index.ts b/backend/src/components/projectObject/index.ts index 76812ee6..5c374dc5 100644 --- a/backend/src/components/projectObject/index.ts +++ b/backend/src/components/projectObject/index.ts @@ -108,11 +108,13 @@ export async function getProjectObjectsByProjectId(projectId: string) { export async function getGeometriesByProjectId(projectId: string) { return getPool().any(sql.type(dbProjectObjectGeometrySchema)` + WITH dump as (${getProjectObjectGeometryDumpFragment()}) SELECT - ST_AsGeoJSON(ST_CollectionExtract(geom)) AS geom, - id "projectObjectId", - object_name "objectName" - FROM app.project_object + dump.geom, + po.id "projectObjectId", + po.object_name "objectName" + FROM app.project_object po + LEFT JOIN dump ON dump.id = po.id WHERE project_id = ${projectId} AND deleted = false; `); } diff --git a/frontend/src/components/Map/DrawMap.tsx b/frontend/src/components/Map/DrawMap.tsx new file mode 100644 index 00000000..b479cdd5 --- /dev/null +++ b/frontend/src/components/Map/DrawMap.tsx @@ -0,0 +1,434 @@ +import { Box, Skeleton, css } from '@mui/material'; +import { useAtom, useAtomValue } from 'jotai'; +import { RESET } from 'jotai/utils'; +import { Feature } from 'ol'; +import { Extent, createEmpty, extend, isEmpty } from 'ol/extent'; +import { Geometry } from 'ol/geom'; +import VectorSource from 'ol/source/Vector'; +import Style from 'ol/style/Style'; +import * as olUtil from 'ol/util'; +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import { useLocation } from 'react-router'; + +import { + ALL_VECTOR_ITEM_LAYERS, + VectorItemLayerKey, + featureSelectorAtom, + mapProjectionAtom, + selectedItemLayersAtom, + selectionSourceAtom, +} from '@frontend/stores/map'; +import { useNavigationBlocker } from '@frontend/stores/navigationBlocker'; +import { dirtyAndValidFieldsAtom, projectEditingAtom } from '@frontend/stores/projectView'; +import { useMapInfoBox } from '@frontend/stores/useMapInfoBox'; + +import { MapInteraction } from './Map'; +import { MapToolbar, ToolType } from './MapToolbar'; +import { BaseMapWrapperProps, MapWrapper } from './MapWrapper'; +import { + addFeaturesFromGeoJson, + createDrawInteraction, + createDrawLayer, + createModifyInteraction, + createSelectInteraction, + createSelectionLayer, + deleteSelectedFeatures, + getGeoJSONFeaturesString, + getSelectedDrawLayerFeatures, +} from './mapInteractions'; +import { mapOptions } from './mapOptions'; + +export interface DrawOptions { + drawGeom: { isLoading: boolean; isFetching: boolean; geoJson: string | object | null }; + onFeaturesSaved?: (features: string) => void; + drawStyle: Style | Style[]; + toolsHidden?: ToolType[]; + editable: boolean; + coversMunicipality?: boolean; + drawItemType: 'project' | 'projectObject'; +} + +interface Props extends BaseMapWrapperProps { + drawOptions: DrawOptions; + interactiveLayers?: VectorItemLayerKey[]; + drawSource?: VectorSource>; + onGeometrySave?: ( + geometry: string, + ) => Promise<{ projectId: string; geom: string } | { projectObjectId: string; geom: string }>; + fitExtent: 'geoJson' | 'all'; +} + +export const DrawMap = forwardRef(function DrawMap( + props: Props, + ref: React.Ref<{ handleUndoDraw: () => void }>, +) { + const { + drawOptions, + interactiveLayers, + vectorLayers: propVectorLayers, + drawSource: propDrawSource, + onGeometrySave, + fitExtent, + ...wrapperProps + } = props; + + const [selectedTool, setSelectedTool] = useState(null); + const [extent, setExtent] = useState(null); + const [interactions, setInteractions] = useState(null); + + const { pathname } = useLocation(); + const { setInfoBox, resetInfoBox, isVisible: infoBoxVisible } = useMapInfoBox(); + + const [featureSelector, setFeatureSelector] = useAtom(featureSelectorAtom); + const projection = useAtomValue(mapProjectionAtom); + const [dirtyAndValidViews, setDirtyAndValidViews] = useAtom(dirtyAndValidFieldsAtom); + const [editing, setEditing] = useAtom(projectEditingAtom); + const selectedItemLayers = useAtomValue(selectedItemLayersAtom); + + useNavigationBlocker(dirtyAndValidViews.map.isDirtyAndValid, 'map'); + + const selectionSource = useAtomValue(selectionSourceAtom); + const drawSource = useMemo(() => propDrawSource ?? new VectorSource({ wrapX: false }), []); + + useImperativeHandle( + ref, + () => ({ + handleUndoDraw, + handleSave: async () => { + selectionSource.clear(); + setFeatureSelector(RESET); + setSelectedTool(null); + const result = await onGeometrySave?.( + getGeoJSONFeaturesString( + drawSource.getFeatures(), + projection?.getCode() ?? mapOptions.projection.code, + ), + ); + if (result) { + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); + } + }, + getGeometry: getGeometryForSave, + }), + [drawSource, selectionSource, handleUndoDraw, getGeometryForSave], + ); + + /** Layers */ + + const selectionLayer = useMemo(() => createSelectionLayer(selectionSource), []); + + const drawLayer = useMemo( + () => createDrawLayer(drawSource, drawOptions?.drawStyle, drawOptions?.drawItemType), + [], + ); + + const vectorLayers = useMemo(() => { + if (!selectedItemLayers || !propVectorLayers) return []; + return propVectorLayers.filter( + (layer) => selectedItemLayers.findIndex((l) => l.id === layer.getProperties().id) !== -1, + ); + }, [selectedItemLayers, propVectorLayers]); + + /** Interactions */ + + const registerProjectSelectInteraction = useMemo(() => { + return createSelectInteraction({ + source: selectionSource, + onSelectionChanged(features, event) { + setInfoBox(features, event.mapBrowserEvent.pixel); + }, + multi: true, + delegateFeatureAdding: true, + filterLayers(layer) { + if ((interactiveLayers ?? ALL_VECTOR_ITEM_LAYERS).includes(layer.getProperties().id)) + return true; + return false; + }, + drawLayerHooverDisabled: true, + }); + }, []); + + const registerSelectInteraction = useMemo( + () => + createSelectInteraction({ + source: selectionSource, + onSelectionChanged(features) { + setFeatureSelector({ features: features, pos: [0, 0] }); + }, + drawLayerHooverDisabled: false, + }), + [], + ); + + const registerModifyInteraction = useMemo( + () => + createModifyInteraction({ + source: selectionSource, + onModifyEnd: () => { + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: true } })); + }, + }), + [], + ); + + const registerDrawInteraction = useMemo( + () => + createDrawInteraction({ + source: drawSource, + drawStyle: drawOptions?.drawStyle, + trace: selectedTool === 'tracedFeature', + traceSource: selectionSource, + onDrawEnd: () => { + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: true } })); + }, + drawType: selectedTool === 'newPointFeature' ? 'Point' : 'Polygon', + }), + [selectedTool], + ); + + /** Effects */ + + useEffect(() => { + return () => { + resetInfoBox(); + setEditing(false); + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); + }; + }, []); + + useEffect(() => { + setExtent(null); + }, [pathname]); + + useEffect(() => { + if (props.drawOptions?.coversMunicipality === undefined) return; + + drawSource.clear(); + + if (props.drawOptions?.coversMunicipality === false) { + addFeaturesFromGeoJson(drawSource, props.drawOptions.drawGeom.geoJson, { editing }); + } + drawFinished(); + }, [props.drawOptions?.coversMunicipality]); + + useEffect(() => { + if (drawOptions.drawGeom.isFetching) { + return; + } + + if (drawOptions?.drawGeom.geoJson) { + addFeaturesFromGeoJson(drawSource, drawOptions.drawGeom.geoJson, { editing }); + if (editing) { + drawLayer.setZIndex(101); + } else { + drawLayer.setZIndex(0); + } + } + + if (!extent) { + let newExtent = createEmpty(); + switch (fitExtent) { + case 'geoJson': + if (drawSource && drawSource.getFeatures().length > 0) { + setExtent(drawSource.getExtent()); + } + break; + case 'all': + if (drawSource.getFeatures().length > 0) { + setExtent(drawSource.getExtent()); + } else { + newExtent = vectorLayers?.reduce((extent, layer) => { + const layerExtent = layer.getSource()?.getExtent(); + if (!layerExtent) return extent; + return extend(extent, layerExtent); + }, createEmpty()) as Extent; + + if (!isEmpty(newExtent)) { + setExtent(newExtent); + } + } + } + } + // GeoJSON can change without fetching if editing status is changed + }, [drawOptions.drawGeom.geoJson, drawOptions.drawGeom.isFetching]); + + useEffect(() => { + switch (selectedTool) { + case 'selectFeature': + setInteractions([registerSelectInteraction]); + break; + case 'copyFromSelection': + copySelectionToDrawSource(); + break; + case 'newFeature': + case 'newPointFeature': + setInteractions([registerDrawInteraction]); + break; + case 'tracedFeature': + setInteractions([registerDrawInteraction]); + break; + case 'editFeature': + setInteractions([registerModifyInteraction]); + break; + case 'clearSelectedFeature': + setFeatureSelector(RESET); + selectionSource.clear(); + drawFinished(); + break; + case 'deleteFeature': + handleDeleteFeatures(); + break; + case 'deleteAllFeatures': + handleDeleteFeatures(false); + break; + default: + drawFinished(); + break; + } + }, [selectedTool]); + + /** Helper functions */ + + function resetSelectInteractions() { + resetInfoBox(); + setInteractions([registerProjectSelectInteraction]); + } + + function handleUndoDraw() { + setFeatureSelector((prev) => ({ + features: deleteSelectedFeatures(drawSource, selectionSource), + pos: prev.pos, + })); + setSelectedTool(null); + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); + selectionSource.clear(); + addFeaturesFromGeoJson(drawSource, drawOptions.drawGeom.geoJson, { editing }); + } + + function getGeometryForSave() { + selectionSource.clear(); + setFeatureSelector(RESET); + setSelectedTool(null); + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); + return getGeoJSONFeaturesString( + drawSource.getFeatures(), + projection?.getCode() ?? mapOptions.projection.code, + ); + } + + function drawSourceHasGeometryOfType( + geometryType: 'Point' | 'MultiPoint' | 'Polygon' | 'MultiPolygon', + ) { + return drawSource + .getFeatures() + ?.some((feature) => feature.getGeometry()?.getType() === geometryType); + } + + function copySelectionToDrawSource() { + const drawFeatureIds = drawSource.getFeatures().map((feature) => olUtil.getUid(feature)); + const selectionFeatures = selectionSource.getFeatures(); + const featuresToCopy = selectionFeatures + .filter((feature) => !drawFeatureIds.includes(olUtil.getUid(feature))) + .map((feature) => { + feature.setProperties({ editing: true }); + return feature; + }); + drawSource.addFeatures(featuresToCopy); + selectionSource.clear(); + drawFinished(); + setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: true } })); + } + + function drawFinished() { + setInteractions([registerProjectSelectInteraction]); + setSelectedTool(null); + } + + function handleDeleteFeatures(selectedOnly = true) { + if (selectedOnly) { + setFeatureSelector((prev) => ({ + features: deleteSelectedFeatures(drawSource, selectionSource), + pos: prev.pos, + })); + } else { + drawSource.clear(); + selectionSource.clear(); + setFeatureSelector((prev) => ({ + features: [], + pos: prev.pos, + })); + } + + setDirtyAndValidViews((prev) => ({ + ...prev, + map: { + isDirtyAndValid: + !drawOptions?.drawGeom.geoJson && drawSource.getFeatures().length === 0 ? false : true, + }, + })); + drawFinished(); + } + + function handleFitScreen() { + setExtent(drawSource.getExtent()); + } + + if (drawOptions.drawGeom.isLoading || (!editing && !extent && drawOptions.drawGeom.geoJson)) { + return ; + } + + return ( + + + + {drawOptions.editable && ( + 0} + toolsHidden={drawOptions?.toolsHidden} + toolsDisabled={{ + selectFeature: infoBoxVisible, + copyFromSelection: + featureSelector.features.every( + (feature) => feature.getProperties().layer === 'drawLayer', + ) || infoBoxVisible, + newFeature: + drawSourceHasGeometryOfType('Point') || + drawSourceHasGeometryOfType('MultiPoint') || + infoBoxVisible, + newPointFeature: + drawSourceHasGeometryOfType('Polygon') || + drawSourceHasGeometryOfType('MultiPolygon') || + infoBoxVisible, + tracedFeature: + featureSelector.features.length === 0 || + drawSourceHasGeometryOfType('Point') || + drawSourceHasGeometryOfType('MultiPoint') || + infoBoxVisible, + editFeature: getSelectedDrawLayerFeatures(featureSelector.features).length === 0, + clearSelectedFeature: featureSelector.features.length === 0 || infoBoxVisible, + deleteFeature: getSelectedDrawLayerFeatures(featureSelector.features).length === 0, + deleteAllFeatures: drawSource.getFeatures().length === 0, + }} + onToolChange={(tool) => setSelectedTool(tool)} + /> + )} + + ); +}); diff --git a/frontend/src/components/Map/MapControls.tsx b/frontend/src/components/Map/MapControls.tsx index a38f1e1a..cc1e5360 100644 --- a/frontend/src/components/Map/MapControls.tsx +++ b/frontend/src/components/Map/MapControls.tsx @@ -1,7 +1,6 @@ import { css } from '@emotion/react'; import { Add, Remove, ZoomInMap, ZoomOutMap } from '@mui/icons-material'; import { Box, Tooltip } from '@mui/material'; -import { useLocation } from 'react-router-dom'; import { useTranslations } from '@frontend/stores/lang'; @@ -22,13 +21,10 @@ interface Props { defaultZoom: number; zoomStep: number; onZoomChanged: (zoom: number) => void; - onFitScreen: () => void; + onFitScreen?: () => void; } -const searchViewPaths = ['/kartta/hankkeet', '/kartta/kohteet']; - export function MapControls(props: Props) { - const { pathname } = useLocation(); const tr = useTranslations(); const { zoom, zoomStep, onZoomChanged, onFitScreen } = props; const toolTipOpts = { enterDelay: 1000, enterNextDelay: 1000, placement: 'right' as const }; @@ -45,7 +41,7 @@ export function MapControls(props: Props) { gap: '6px', }} > - {!searchViewPaths.includes(pathname) && ( + {onFitScreen && ( diff --git a/frontend/src/components/Map/MapWrapper.tsx b/frontend/src/components/Map/MapWrapper.tsx index 37427c68..701e9763 100644 --- a/frontend/src/components/Map/MapWrapper.tsx +++ b/frontend/src/components/Map/MapWrapper.tsx @@ -1,28 +1,15 @@ import { GlobalStyles } from '@mui/material'; import { useAtom, useAtomValue } from 'jotai'; -import { RESET } from 'jotai/utils'; import Feature from 'ol/Feature'; -import { Extent, createEmpty, extend, isEmpty } from 'ol/extent'; import { Geometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import { Projection } from 'ol/proj'; import VectorSource from 'ol/source/Vector'; -import Style from 'ol/style/Style'; -import * as olUtil from 'ol/util'; -import React, { - forwardRef, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react'; -import { useLocation, useParams } from 'react-router'; +import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'; +import { useLocation } from 'react-router'; import { - ALL_VECTOR_ITEM_LAYERS, ItemLayerState, - VectorItemLayerKey, baseLayerIdAtom, featureSelectorAtom, freezeMapHeightAtom, @@ -31,8 +18,6 @@ import { selectedWFSLayersAtom, selectionSourceAtom, } from '@frontend/stores/map'; -import { useNavigationBlocker } from '@frontend/stores/navigationBlocker'; -import { dirtyAndValidFieldsAtom, projectEditingAtom } from '@frontend/stores/projectView'; import { useMapInfoBox } from '@frontend/stores/useMapInfoBox'; import { ColorPatternSelect } from './ColorPatternSelect'; @@ -40,31 +25,16 @@ import { DrawerContainer } from './DrawerContainer'; import { Map, MapInteraction } from './Map'; import { MapControls } from './MapControls'; import { MapInfoBoxButton } from './MapInfoBoxButton'; -import { MapToolbar, ToolType } from './MapToolbar'; import { SelectionInfoBox } from './SelectionInfoBox'; import { createWFSLayer, createWMTSLayer, getFeatureItemIds } from './mapFunctions'; -import { - addFeaturesFromGeoJson, - createDrawInteraction, - createDrawLayer, - createModifyInteraction, - createSelectInteraction, - createSelectionLayer, - deleteSelectedFeatures, - getGeoJSONFeaturesString, - getSelectedDrawLayerFeatures, -} from './mapInteractions'; import { mapOptions } from './mapOptions'; -export interface DrawOptions { - geoJson: string | object | null; - onFeaturesSaved?: (features: string) => void; - drawStyle: Style | Style[]; - toolsHidden?: ToolType[]; - editable: boolean; - coversMunicipality?: boolean; - drawItemType: 'project' | 'projectObject'; -} +export type BaseMapWrapperProps = Omit< + ComponentProps, + 'resetSelectInteractions' +> & { + drawLayer?: VectorLayer>, Feature>; +}; export interface ProjectData { projectId: string; @@ -90,25 +60,22 @@ export interface ProjectObjectData { } interface Props { - drawOptions?: DrawOptions; + resetSelectInteractions: () => void; onMoveEnd?: (zoom: number, extent: number[]) => void; - loading?: boolean; - vectorLayers?: VectorLayer>, Feature>[]; - fitExtent?: 'geoJson' | 'vectorLayers' | 'all'; projects?: TProject[]; projectObjects?: TProjectObject[]; - /** Layers which contain features users can interact with by clicking a feature and opening a map info box. */ - interactiveLayers?: VectorItemLayerKey[]; - drawSource?: VectorSource>; + extent?: number[] | null; + handleFitScreen?: () => void; withColorPatternSelect?: boolean; - onGeometrySave?: (geometry: string) => Promise; + interactions?: MapInteraction[] | null; + vectorLayers?: VectorLayer>, Feature>[]; + activeVectorLayers?: VectorLayer>, Feature>[]; + interactionLayers?: VectorLayer>, Feature>[]; } -export const MapWrapper = forwardRef(function MapWrapper< - TProject extends ProjectData, - TProjectObject extends ProjectObjectData, ->(props: Props, ref: React.Ref<{ handleUndoDraw: () => void }>) { - const { projectObjectId } = useParams() as { projectObjectId?: string }; +export function MapWrapper( + props: Props, +) { const [zoom, setZoom] = useState(mapOptions.tre.defaultZoom); const [viewExtent, setViewExtent] = useState(mapOptions.tre.extent); const [mapGeometryInitialized, setMapGeometryInitialized] = useState(false); @@ -120,7 +87,7 @@ export const MapWrapper = forwardRef(function MapWrapper< const mapWrapperRef = useRef(null); - const [featureSelector, setFeatureSelector] = useAtom(featureSelectorAtom); + const featureSelector = useAtomValue(featureSelectorAtom); const selectionSource = useAtomValue(selectionSourceAtom); useEffect(() => { @@ -129,8 +96,6 @@ export const MapWrapper = forwardRef(function MapWrapper< } }, [zoom, viewExtent]); - const [extent, setExtent] = useState(null); - const projection = useAtomValue(mapProjectionAtom); const [baseLayerId] = useAtom(baseLayerIdAtom); @@ -152,312 +117,22 @@ export const MapWrapper = forwardRef(function MapWrapper< .map((layer) => createWFSLayer(layer)); }, [selectedWFSLayers]); - const vectorLayers = useMemo(() => { - if (!selectedItemLayers || !props.vectorLayers) return []; - return props.vectorLayers.filter( - (layer) => selectedItemLayers.findIndex((l) => l.id === layer.getProperties().id) !== -1, - ); - }, [selectedItemLayers, props.vectorLayers]); - - /** - * Interactions - */ - - const { setInfoBox, resetInfoBox, isVisible: infoBoxVisible } = useMapInfoBox(); + const { resetInfoBox, isVisible: infoBoxVisible } = useMapInfoBox(); const [wholeMunicipalityInfoBoxVisible, setWholeMunicipalityInfoBoxVisible] = useState(false); - const [selectedTool, setSelectedTool] = useState(null); - const [interactions, setInteractions] = useState(null); - const selectionLayer = useMemo(() => createSelectionLayer(selectionSource), []); - const freezeMapHeight = useAtomValue(freezeMapHeightAtom); - const [dirtyAndValidViews, setDirtyAndValidViews] = useAtom(dirtyAndValidFieldsAtom); - const [editing, setEditing] = useAtom(projectEditingAtom); - useNavigationBlocker(dirtyAndValidViews.map.isDirtyAndValid, 'map'); - - useEffect(() => { - return () => { - resetInfoBox(); - setEditing(false); - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); - }; - }, []); - - const drawSource = useMemo(() => props.drawSource ?? new VectorSource({ wrapX: false }), []); - - useImperativeHandle( - ref, - () => ({ - handleUndoDraw, - handleSave: async () => { - selectionSource.clear(); - setFeatureSelector(RESET); - setSelectedTool(null); - await props.onGeometrySave?.( - getGeoJSONFeaturesString( - drawSource.getFeatures(), - projection?.getCode() ?? mapOptions.projection.code, - ), - ); - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); - }, - getGeometry: getGeometryForSave, - }), - [drawSource, selectionSource, handleUndoDraw, getGeometryForSave], - ); - - function resetSelectInteractions() { - resetInfoBox(); - setInteractions([registerProjectSelectInteraction]); - } - - const registerSelectInteraction = useMemo( - () => - createSelectInteraction({ - source: selectionSource, - onSelectionChanged(features) { - setFeatureSelector({ features: features, pos: [0, 0] }); - }, - drawLayerHooverDisabled: false, - }), - [], - ); - const registerProjectSelectInteraction = useMemo(() => { - return createSelectInteraction({ - source: selectionSource, - onSelectionChanged(features, event) { - setInfoBox(features, event.mapBrowserEvent.pixel); - }, - multi: true, - delegateFeatureAdding: true, - filterLayers(layer) { - if ((props.interactiveLayers ?? ALL_VECTOR_ITEM_LAYERS).includes(layer.getProperties().id)) - return true; - return false; - }, - drawLayerHooverDisabled: true, - }); - }, []); - - const registerModifyInteraction = useMemo( - () => - createModifyInteraction({ - source: selectionSource, - onModifyEnd: () => { - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: true } })); - }, - }), - [], - ); - useEffect(() => { - if (props.drawOptions?.geoJson) { - addFeaturesFromGeoJson(drawSource, props.drawOptions.geoJson, { editing }); - if (!mapGeometryInitialized) setMapGeometryInitialized(true); - } - }, [props.drawOptions?.geoJson]); + const freezeMapHeight = useAtomValue(freezeMapHeightAtom); - function drawSourceHasGeometryOfType(geometryType: 'Point' | 'Polygon') { - return drawSource - .getFeatures() - ?.some((feature) => feature.getGeometry()?.getType() === geometryType); + function handleMapClickEvent() { + // This is just used to clear the whole municipality info box, use interactions for other events + setWholeMunicipalityInfoBoxVisible(false); + props.resetSelectInteractions(); } - const drawLayer = useMemo( - () => - createDrawLayer(drawSource, props.drawOptions?.drawStyle, props.drawOptions?.drawItemType), - [], - ); - - const registerDrawInteraction = useMemo( - () => - createDrawInteraction({ - source: drawSource, - drawStyle: props.drawOptions?.drawStyle, - trace: selectedTool === 'tracedFeature', - traceSource: selectionSource, - onDrawEnd: () => { - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: true } })); - }, - drawType: selectedTool === 'newPointFeature' ? 'Point' : 'Polygon', - }), - [selectedTool], - ); - const projectsForWholeMunicipality = useMemo(() => { if (props.projects) return props.projects.filter((project) => project.coversMunicipality); return []; }, [props.projects]); - useEffect(() => { - if (props.drawOptions?.geoJson) - // GeoJson is not loading - drawSource.getFeatures().forEach((feature) => { - feature.setProperties({ editing }); - }); - - if (projectObjectId) { - return; - } - - if (editing) { - drawLayer.setZIndex(101); - } else { - drawLayer.setZIndex(0); - } - }, [editing, drawLayer, projectObjectId]); - - useEffect(() => { - if (!mapGeometryInitialized) { - return; - } - let extent = createEmpty(); - switch (props.fitExtent) { - case 'geoJson': - if (drawSource && drawSource.getFeatures().length > 0) { - if ( - Object.values(dirtyAndValidViews).every( - (status) => !status.isValid || !status.isDirtyAndValid, - ) - ) - setExtent(drawSource.getExtent()); - } - break; - case 'vectorLayers': - extent = vectorLayers?.reduce((extent, layer) => { - const layerExtent = layer.getSource()?.getExtent(); - if (!layerExtent) return extent; - return extend(extent, layerExtent); - }, createEmpty()) as Extent; - if (!isEmpty(extent)) { - setExtent(extent); - } - break; - case 'all': - if (drawSource.getFeatures().length > 0) { - if ( - Object.values(dirtyAndValidViews).every( - (status) => !status.isValid || !status.isDirtyAndValid, - ) - ) - setExtent(drawSource.getExtent()); - } else { - extent = vectorLayers?.reduce((extent, layer) => { - const layerExtent = layer.getSource()?.getExtent(); - if (!layerExtent) return extent; - return extend(extent, layerExtent); - }, createEmpty()) as Extent; - - if (!isEmpty(extent)) { - setExtent(extent); - } - } - } - }, [mapGeometryInitialized]); - - function drawFinished() { - if (props.projects || props.projectObjects) { - setInteractions([registerProjectSelectInteraction]); - } else { - setInteractions(null); - } - setSelectedTool(null); - } - - function copySelectionToDrawSource() { - const drawFeatureIds = drawSource.getFeatures().map((feature) => olUtil.getUid(feature)); - const selectionFeatures = selectionSource.getFeatures(); - const featuresToCopy = selectionFeatures - .filter((feature) => !drawFeatureIds.includes(olUtil.getUid(feature))) - .map((feature) => { - feature.setProperties({ editing: true }); - return feature; - }); - drawSource.addFeatures(featuresToCopy); - selectionSource.clear(); - drawFinished(); - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: true } })); - } - - function handleDeleteFeatures(selectedOnly = true) { - if (selectedOnly) { - setFeatureSelector((prev) => ({ - features: deleteSelectedFeatures(drawSource, selectionSource), - pos: prev.pos, - })); - } else { - drawSource.clear(); - selectionSource.clear(); - setFeatureSelector((prev) => ({ - features: [], - pos: prev.pos, - })); - } - - setDirtyAndValidViews((prev) => ({ - ...prev, - map: { - isDirtyAndValid: - !props.drawOptions?.geoJson && drawSource.getFeatures().length === 0 ? false : true, - }, - })); - drawFinished(); - } - - useEffect(() => { - switch (selectedTool) { - case 'selectFeature': - setInteractions([registerSelectInteraction]); - break; - case 'copyFromSelection': - copySelectionToDrawSource(); - break; - case 'newFeature': - case 'newPointFeature': - setInteractions([registerDrawInteraction]); - break; - case 'tracedFeature': - setInteractions([registerDrawInteraction]); - break; - case 'editFeature': - setInteractions([registerModifyInteraction]); - break; - case 'clearSelectedFeature': - setFeatureSelector(RESET); - selectionSource.clear(); - drawFinished(); - break; - case 'deleteFeature': - handleDeleteFeatures(); - break; - case 'deleteAllFeatures': - handleDeleteFeatures(false); - break; - default: - drawFinished(); - break; - } - }, [selectedTool]); - - useEffect(() => { - if (props.drawOptions?.coversMunicipality === undefined) return; - - drawSource.clear(); - - if (props.drawOptions?.coversMunicipality === false) { - addFeaturesFromGeoJson(drawSource, props.drawOptions.geoJson, { editing }); - } - drawFinished(); - }, [props.drawOptions?.coversMunicipality]); - - useEffect(() => { - if (props.projects || props.projectObjects) setInteractions([registerProjectSelectInteraction]); - }, []); - - function handleMapClickEvent() { - // This is just used to clear the whole municipality info box, use interactions for other events - setWholeMunicipalityInfoBoxVisible(false); - resetSelectInteractions(); - } - const wholeMunicipalityInfoBoxButtonVisible = useMemo(() => { return selectedItemLayers.some( (layer: ItemLayerState) => @@ -465,28 +140,6 @@ export const MapWrapper = forwardRef(function MapWrapper< ); }, [selectedItemLayers]); - function handleUndoDraw() { - setFeatureSelector((prev) => ({ - features: deleteSelectedFeatures(drawSource, selectionSource), - pos: prev.pos, - })); - setSelectedTool(null); - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); - selectionSource.clear(); - addFeaturesFromGeoJson(drawSource, props.drawOptions?.geoJson, { editing }); - } - - function getGeometryForSave() { - selectionSource.clear(); - setFeatureSelector(RESET); - setSelectedTool(null); - setDirtyAndValidViews((prev) => ({ ...prev, map: { isDirtyAndValid: false } })); - return getGeoJSONFeaturesString( - drawSource.getFeatures(), - projection?.getCode() ?? mapOptions.projection.code, - ); - } - return (
@@ -556,7 +209,7 @@ export const MapWrapper = forwardRef(function MapWrapper< if (changedZoom <= mapOptions.tre.maxZoom && changedZoom >= mapOptions.tre.minZoom) setZoom(changedZoom); }} - onFitScreen={() => setExtent(drawSource?.getExtent())} + {...(props.handleFitScreen && { onFitScreen: props.handleFitScreen })} /> {wholeMunicipalityInfoBoxButtonVisible && ( )} @@ -586,30 +239,7 @@ export const MapWrapper = forwardRef(function MapWrapper< colorPatternSelectorVisible={props.withColorPatternSelect} /> - {props.drawOptions?.editable && ( - 0} - toolsHidden={props.drawOptions?.toolsHidden} - toolsDisabled={{ - selectFeature: infoBoxVisible, - copyFromSelection: - featureSelector.features.every( - (feature) => feature.getProperties().layer === 'drawLayer', - ) || infoBoxVisible, - newFeature: drawSourceHasGeometryOfType('Point') || infoBoxVisible, - newPointFeature: drawSourceHasGeometryOfType('Polygon') || infoBoxVisible, - tracedFeature: - featureSelector.features.length === 0 || - drawSourceHasGeometryOfType('Point') || - infoBoxVisible, - editFeature: getSelectedDrawLayerFeatures(featureSelector.features).length === 0, - clearSelectedFeature: featureSelector.features.length === 0 || infoBoxVisible, - deleteFeature: getSelectedDrawLayerFeatures(featureSelector.features).length === 0, - deleteAllFeatures: drawSource.getFeatures().length === 0, - }} - onToolChange={(tool) => setSelectedTool(tool)} - /> - )} + {infoBoxVisible && ( )}
); -}); +} diff --git a/frontend/src/components/Map/SearchResultsMap.tsx b/frontend/src/components/Map/SearchResultsMap.tsx new file mode 100644 index 00000000..1fc50e8c --- /dev/null +++ b/frontend/src/components/Map/SearchResultsMap.tsx @@ -0,0 +1,90 @@ +import { useAtomValue } from 'jotai'; +import Feature from 'ol/Feature'; +import { Geometry } from 'ol/geom'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import { useEffect, useMemo, useState } from 'react'; + +import { + ALL_VECTOR_ITEM_LAYERS, + VectorItemLayerKey, + selectedItemLayersAtom, + selectionSourceAtom, +} from '@frontend/stores/map'; +import { useMapInfoBox } from '@frontend/stores/useMapInfoBox'; + +import { MapInteraction } from './Map'; +import { BaseMapWrapperProps, MapWrapper } from './MapWrapper'; +import { createSelectInteraction, createSelectionLayer } from './mapInteractions'; + +interface Props extends BaseMapWrapperProps { + interactiveLayers?: VectorItemLayerKey[]; + vectorLayers?: VectorLayer>, Feature>[]; +} + +export function SearchResultsMap(props: Props) { + const { vectorLayers: propVectorLayers, interactiveLayers, ...wrapperProps } = props; + const [interactions, setInteractions] = useState(null); + + const { setInfoBox, resetInfoBox } = useMapInfoBox(); + + const selectedItemLayers = useAtomValue(selectedItemLayersAtom); + const selectionSource = useAtomValue(selectionSourceAtom); + + /** Layers */ + + const selectionLayer = useMemo(() => createSelectionLayer(selectionSource), []); + + const vectorLayers = useMemo(() => { + if (!selectedItemLayers || !propVectorLayers) return []; + return propVectorLayers.filter( + (layer) => selectedItemLayers.findIndex((l) => l.id === layer.getProperties().id) !== -1, + ); + }, [selectedItemLayers, props.vectorLayers]); + + /** Interactions */ + + const registerProjectSelectInteraction = useMemo(() => { + return createSelectInteraction({ + source: selectionSource, + onSelectionChanged(features, event) { + setInfoBox(features, event.mapBrowserEvent.pixel); + }, + multi: true, + delegateFeatureAdding: true, + filterLayers(layer) { + if ((interactiveLayers ?? ALL_VECTOR_ITEM_LAYERS).includes(layer.getProperties().id)) + return true; + return false; + }, + drawLayerHooverDisabled: true, + }); + }, []); + + /** Effects */ + + useEffect(() => { + setInteractions([registerProjectSelectInteraction]); + return () => { + resetInfoBox(); + }; + }, []); + + /** Helper functions */ + + function resetSelectInteractions() { + resetInfoBox(); + setInteractions([registerProjectSelectInteraction]); + } + + return ( + + ); +} diff --git a/frontend/src/stores/navigationBlocker.tsx b/frontend/src/stores/navigationBlocker.tsx index 26a003fd..fe55d482 100644 --- a/frontend/src/stores/navigationBlocker.tsx +++ b/frontend/src/stores/navigationBlocker.tsx @@ -15,11 +15,15 @@ export interface BlockerStatus { export const blockerStatusAtom = atom(defaultStatus); -export function useNavigationBlocker(isDirty: boolean, identifier: string, callBack?: () => void) { +export function useNavigationBlocker( + isDirty: boolean, + identifier: string, + unmountCallBack?: () => void, +) { const [blockerStatus, setBlockerStatus] = useAtom(blockerStatusAtom); useEffect(() => { - return () => callBack?.(); + return () => unmountCallBack?.(); }, []); useEffect(() => { diff --git a/frontend/src/views/DetailplanProject/DetailplanProject.tsx b/frontend/src/views/DetailplanProject/DetailplanProject.tsx index 3d54aebf..9fe0ec60 100644 --- a/frontend/src/views/DetailplanProject/DetailplanProject.tsx +++ b/frontend/src/views/DetailplanProject/DetailplanProject.tsx @@ -6,7 +6,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom'; import { trpc } from '@frontend/client'; import { ErrorPage } from '@frontend/components/ErrorPage'; -import { MapWrapper } from '@frontend/components/Map/MapWrapper'; +import { DrawMap } from '@frontend/components/Map/DrawMap'; import { projectAreaStyle } from '@frontend/components/Map/styles'; import { asyncUserAtom } from '@frontend/stores/auth'; import { useTranslations } from '@frontend/stores/lang'; @@ -44,12 +44,6 @@ const pageContentStyle = css` overflow: hidden; `; -const mapContainerStyle = css` - min-height: 320px; - flex: 1; - position: relative; -`; - function getTabs(projectId: string) { return [ { @@ -200,18 +194,19 @@ export function DetailplanProject() { } {tabView === 'default' && ( - - - + )} {tabView !== 'default' && ( diff --git a/frontend/src/views/MaintenanceProject/MaintenanceProject.tsx b/frontend/src/views/MaintenanceProject/MaintenanceProject.tsx index ada9570e..ecd4ba16 100644 --- a/frontend/src/views/MaintenanceProject/MaintenanceProject.tsx +++ b/frontend/src/views/MaintenanceProject/MaintenanceProject.tsx @@ -11,7 +11,7 @@ import { useSearchParams } from 'react-router-dom'; import { trpc } from '@frontend/client'; import { ErrorPage } from '@frontend/components/ErrorPage'; -import { MapWrapper } from '@frontend/components/Map/MapWrapper'; +import { DrawMap } from '@frontend/components/Map/DrawMap'; import { DRAW_LAYER_Z_INDEX, addFeaturesFromGeoJson, @@ -55,14 +55,6 @@ const pageContentStyle = css` overflow: hidden; `; -const mapContainerStyle = css` - display: flex; - flex-direction: column; - min-height: 320px; - flex: 1; - position: relative; -`; - function getTabs(projectId: string) { return [ { @@ -352,39 +344,39 @@ export function MaintenanceProject() {
{tabView === 'default' && ( - - { - await geometryUpdate.mutateAsync({ projectId, features }); - }} - drawOptions={{ - coversMunicipality: coversMunicipality, - toolsHidden: ['newPointFeature'], - geoJson: project.isFetching - ? null - : (editing ? project?.data?.geometryDump : project?.data?.geom) ?? null, - drawStyle: projectAreaStyle(undefined, undefined, false), - editable: editing && mapIsEditable(), - drawItemType: 'project', - }} - drawSource={drawSource} - fitExtent="geoJson" - vectorLayers={vectorLayers} - projectObjects={ - projectObjects.data?.map((obj) => ({ - ...obj, - project: { - projectId: projectId, - projectName: project.data?.projectName ?? '', - projectType: 'maintenanceProject', - coversMunicipality: project.data?.coversMunicipality ?? false, - }, - })) ?? [] - } - interactiveLayers={['projectObjects']} - /> - + { + return geometryUpdate.mutateAsync({ projectId, features }); + }} + drawOptions={{ + coversMunicipality: coversMunicipality, + toolsHidden: ['newPointFeature'], + drawGeom: { + isLoading: Boolean(projectId) && project.isLoading, + isFetching: project.isFetching, + geoJson: (editing ? project?.data?.geometryDump : project?.data?.geom) ?? null, + }, + drawStyle: projectAreaStyle(undefined, undefined, false), + editable: editing && mapIsEditable(), + drawItemType: 'project', + }} + drawSource={drawSource} + fitExtent="geoJson" + vectorLayers={vectorLayers} + projectObjects={ + projectObjects.data?.map((obj) => ({ + ...obj, + project: { + projectId: projectId, + projectName: project.data?.projectName ?? '', + projectType: 'maintenanceProject', + coversMunicipality: project.data?.coversMunicipality ?? false, + }, + })) ?? [] + } + interactiveLayers={['projectObjects']} + /> )} {tabView !== 'default' && ( diff --git a/frontend/src/views/Project/InvestmentProject.tsx b/frontend/src/views/Project/InvestmentProject.tsx index 6b9f024a..7a89ac6a 100644 --- a/frontend/src/views/Project/InvestmentProject.tsx +++ b/frontend/src/views/Project/InvestmentProject.tsx @@ -11,7 +11,7 @@ import { useSearchParams } from 'react-router-dom'; import { trpc } from '@frontend/client'; import { ErrorPage } from '@frontend/components/ErrorPage'; -import { MapWrapper } from '@frontend/components/Map/MapWrapper'; +import { DrawMap } from '@frontend/components/Map/DrawMap'; import { DRAW_LAYER_Z_INDEX, addFeaturesFromGeoJson, @@ -56,14 +56,6 @@ const pageContentStyle = css` padding: 0 16px; `; -const mapContainerStyle = css` - display: flex; - flex-direction: column; - min-height: 320px; - flex: 1; - position: relative; -`; - function getTabs(projectId: string) { return [ { @@ -359,40 +351,39 @@ export function InvestmentProject() { {tabView === 'default' && ( - - { - await geometryUpdate.mutateAsync({ projectId, features }); - }} - drawOptions={{ - coversMunicipality: coversMunicipality, - toolsHidden: ['newPointFeature'], - geoJson: project.isFetching - ? null - : (editing ? project?.data?.geometryDump : project?.data?.geom) ?? null, - drawStyle: projectAreaStyle(undefined, undefined, false), - editable: editing && mapIsEditable(), - drawItemType: 'project', - }} - drawSource={drawSource} - fitExtent="geoJson" - vectorLayers={vectorLayers} - projectObjects={ - projectObjects.data?.map((obj) => ({ - ...obj, - objectStage: obj.objectStage ?? '', - project: { - projectId: projectId, - projectName: project.data?.projectName ?? '', - projectType: 'investmentProject', - coversMunicipality: project.data?.coversMunicipality ?? false, - }, - })) ?? [] - } - interactiveLayers={['projectObjects']} - /> - + { + return geometryUpdate.mutateAsync({ projectId, features }); + }} + fitExtent="geoJson" + vectorLayers={vectorLayers} + drawOptions={{ + coversMunicipality: coversMunicipality, + toolsHidden: ['newPointFeature'], + drawGeom: { + isLoading: Boolean(projectId) && project.isLoading, + isFetching: project.isFetching, + geoJson: (editing ? project?.data?.geometryDump : project?.data?.geom) ?? null, + }, + drawStyle: projectAreaStyle(undefined, undefined, false), + editable: editing && mapIsEditable(), + drawItemType: 'project', + }} + projectObjects={ + projectObjects.data?.map((obj) => ({ + ...obj, + objectStage: obj.objectStage ?? '', + project: { + projectId: projectId, + projectName: project.data?.projectName ?? '', + projectType: 'investmentProject', + coversMunicipality: project.data?.coversMunicipality ?? false, + }, + })) ?? [] + } + drawSource={drawSource} + /> )} {tabView !== 'default' && ( diff --git a/frontend/src/views/Project/ResultsMap.tsx b/frontend/src/views/Project/ResultsMap.tsx index bbe0d09d..5e13a65a 100644 --- a/frontend/src/views/Project/ResultsMap.tsx +++ b/frontend/src/views/Project/ResultsMap.tsx @@ -7,7 +7,7 @@ import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { useEffect, useMemo } from 'react'; -import { MapWrapper } from '@frontend/components/Map/MapWrapper'; +import { SearchResultsMap } from '@frontend/components/Map/SearchResultsMap'; import { getProjectObjectGeoJSON } from '@frontend/components/Map/mapFunctions'; import { addFeaturesFromGeoJson, @@ -166,8 +166,7 @@ export function ResultsMap(props: Props) { return ( - { setMap({ zoom: Math.floor(zoom), extent }); diff --git a/frontend/src/views/ProjectObject/ProjectObject.tsx b/frontend/src/views/ProjectObject/ProjectObject.tsx index 4d4bb03d..dfbb3e1e 100644 --- a/frontend/src/views/ProjectObject/ProjectObject.tsx +++ b/frontend/src/views/ProjectObject/ProjectObject.tsx @@ -4,13 +4,13 @@ import { Box, Breadcrumbs, Chip, Paper, Tab, Tabs, Typography } from '@mui/mater import dayjs from 'dayjs'; import { useAtomValue } from 'jotai'; import VectorSource from 'ol/source/Vector'; -import { ReactElement, useMemo, useState } from 'react'; -import { useParams } from 'react-router'; +import { ReactElement, useEffect, useMemo, useState } from 'react'; +import { useLocation, useParams } from 'react-router'; import { Link, useSearchParams } from 'react-router-dom'; import { trpc } from '@frontend/client'; import { ErrorPage } from '@frontend/components/ErrorPage'; -import { MapWrapper } from '@frontend/components/Map/MapWrapper'; +import { DrawMap } from '@frontend/components/Map/DrawMap'; import { getProjectObjectGeoJSON } from '@frontend/components/Map/mapFunctions'; import { featuresFromGeoJSON } from '@frontend/components/Map/mapInteractions'; import { PROJ_OBJ_DRAW_STYLE } from '@frontend/components/Map/styles'; @@ -88,12 +88,6 @@ function projectObjectTabs( ]; } -const mapContainerStyle = css` - min-height: 320px; - flex: 1; - position: relative; -`; - interface Props { projectType: Exclude; } @@ -107,6 +101,7 @@ export function ProjectObject(props: Props) { const projectObjectId = routeParams?.projectObjectId; const [searchParams] = useSearchParams(); + const { pathname } = useLocation(); const tabView = searchParams.get('tab') || 'default'; const tabs = projectObjectTabs(routeParams.projectId, props.projectType, projectObjectId); @@ -164,6 +159,10 @@ export function ProjectObject(props: Props) { { enabled: Boolean(projectId) }, ); + useEffect(() => { + projectObjectGeometries.refetch(); + }, [pathname]); + // Create vectorlayer of the project geometry const projectSource = useMemo(() => { const source = new VectorSource(); @@ -370,40 +369,41 @@ export function ProjectObject(props: Props) { {!searchParams.get('tab') && ( - - { - await geometryUpdate.mutateAsync({ projectObjectId, features }); - }} - drawOptions={{ - geoJson: projectObject.isFetching - ? null - : (editing ? projectObject.data?.geometryDump : projectObject.data?.geom) ?? - null, - drawStyle: PROJ_OBJ_DRAW_STYLE, - editable: editing && (!projectObjectId || isOwner || canWrite), - drawItemType: 'projectObject', - }} - vectorLayers={vectorLayers} - fitExtent="all" - projectObjects={ - projectObjects.data - ?.filter((obj) => obj.projectObjectId !== projectObjectId) - .map((obj) => ({ - ...obj, - project: { - projectId: projectId, - projectName: project.data?.projectName ?? '', - projectType: project.data?.projectType, - coversMunicipality: project.data?.coversMunicipality ?? false, - }, - })) ?? [] - } - interactiveLayers={['projectObjects', 'projects']} - projects={project.data ? [project.data] : []} - /> - + { + return geometryUpdate.mutateAsync({ projectObjectId, features }); + }} + drawOptions={{ + drawGeom: { + isLoading: Boolean(projectObjectId) && projectObject.isLoading, + isFetching: projectObject.isFetching, + geoJson: + (editing ? projectObject.data?.geometryDump : projectObject.data?.geom) ?? + null, + }, + drawStyle: PROJ_OBJ_DRAW_STYLE, + editable: editing && (!projectObjectId || isOwner || canWrite), + drawItemType: 'projectObject', + }} + vectorLayers={vectorLayers} + fitExtent="all" + projectObjects={ + projectObjects.data + ?.filter((obj) => obj.projectObjectId !== projectObjectId) + .map((obj) => ({ + ...obj, + project: { + projectId: projectId, + projectName: project.data?.projectName ?? '', + projectType: project.data?.projectType, + coversMunicipality: project.data?.coversMunicipality ?? false, + }, + })) ?? [] + } + interactiveLayers={['projectObjects', 'projects']} + projects={project.data ? [project.data] : []} + /> )} {searchParams.get('tab') && ( diff --git a/frontend/src/views/ProjectObject/ProjectObjectResultsMap.tsx b/frontend/src/views/ProjectObject/ProjectObjectResultsMap.tsx index 5c653f88..40c5b9da 100644 --- a/frontend/src/views/ProjectObject/ProjectObjectResultsMap.tsx +++ b/frontend/src/views/ProjectObject/ProjectObjectResultsMap.tsx @@ -7,7 +7,7 @@ import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { useEffect, useMemo } from 'react'; -import { MapWrapper } from '@frontend/components/Map/MapWrapper'; +import { SearchResultsMap } from '@frontend/components/Map/SearchResultsMap'; import { addFeaturesFromGeoJson, featuresFromGeoJSON, @@ -167,8 +167,7 @@ export function ProjectObjectResultsMap(props: Props) { return ( - { setMap({ zoom: Math.floor(zoom), extent }); From d85704b8c66b619283517e0f3ca2ea0e9c4cb7ee Mon Sep 17 00:00:00 2001 From: Mikael Moilanen Date: Wed, 6 Nov 2024 09:18:12 +0200 Subject: [PATCH 2/2] Fix project object search role filters --- .../src/components/projectObject/search.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/backend/src/components/projectObject/search.ts b/backend/src/components/projectObject/search.ts index de305f5e..61240be9 100644 --- a/backend/src/components/projectObject/search.ts +++ b/backend/src/components/projectObject/search.ts @@ -7,6 +7,7 @@ import { timePeriodFragment, } from '@backend/components/projectObject/index.js'; import { getPool } from '@backend/db.js'; +import { logger } from '@backend/logging.js'; import { ObjectsByProjectSearch, @@ -117,31 +118,30 @@ function getObjectRoleFilterFragment( // Object participants includes all roles except suunnitteluttaja and rakennuttaja if (objectParticipantUser) { return sql.fragment` - ((pour.role).code_list_id = 'KohdeKayttajaRooli' AND pour.user_id = ${objectParticipantUser})`; + ((pour_participant.role).code_list_id = 'KohdeKayttajaRooli' AND pour_participant.user_id = ${objectParticipantUser})`; } - return sql.fragment`true`; } if (rakennuttajaUsers.length > 0 && suunnitteluttajaUsers.length > 0) { - return sql.fragment`((pour.role = ('InvestointiKohdeKayttajaRooli', '01')::app.code_id AND pour.user_id = ANY(${sql.array( + return sql.fragment`((pour_rakennuttaja.role = ('InvestointiKohdeKayttajaRooli', '01')::app.code_id AND pour_rakennuttaja.user_id = ANY(${sql.array( rakennuttajaUsers, 'text', - )})) OR (pour.role = ('InvestointiKohdeKayttajaRooli', '02')::app.code_id AND pour.user_id = ANY(${sql.array( + )})) AND (pour_suunnitteluttaja.role = ('InvestointiKohdeKayttajaRooli', '02')::app.code_id AND pour_suunnitteluttaja.user_id = ANY(${sql.array( suunnitteluttajaUsers, 'text', - )})) OR ${objectParticipantFragment(objectParticipantUser)})`; + )})) AND ${objectParticipantFragment(objectParticipantUser) ?? sql.fragment`true`})`; } else if (rakennuttajaUsers.length > 0) { - return sql.fragment`((pour.role = ('InvestointiKohdeKayttajaRooli', '01')::app.code_id AND pour.user_id = ANY(${sql.array( + return sql.fragment`((pour_rakennuttaja.role = ('InvestointiKohdeKayttajaRooli', '01')::app.code_id AND pour_rakennuttaja.user_id = ANY(${sql.array( rakennuttajaUsers, 'text', - )})) OR ${objectParticipantFragment(objectParticipantUser)})`; + )})) AND ${objectParticipantFragment(objectParticipantUser) ?? sql.fragment`true`})`; } else if (suunnitteluttajaUsers.length > 0) { - return sql.fragment`((pour.role = ('InvestointiKohdeKayttajaRooli', '02')::app.code_id AND pour.user_id = ANY(${sql.array( + return sql.fragment`((pour_suunnitteluttaja.role = ('InvestointiKohdeKayttajaRooli', '02')::app.code_id AND pour_suunnitteluttaja.user_id = ANY(${sql.array( suunnitteluttajaUsers, 'text', - )})) OR ${objectParticipantFragment(objectParticipantUser)})`; + )})) AND ${objectParticipantFragment(objectParticipantUser) ?? sql.fragment`true`})`; } else { - return objectParticipantFragment(objectParticipantUser); + return objectParticipantFragment(objectParticipantUser) ?? sql.fragment`true`; } } @@ -203,7 +203,9 @@ export async function projectObjectSearch(input: ProjectObjectSearch) { withGeoHash: true, withGeometries, })} - LEFT JOIN app.project_object_user_role pour ON po.id = pour.project_object_id + LEFT JOIN app.project_object_user_role pour_rakennuttaja ON po.id = pour_rakennuttaja.project_object_id + LEFT JOIN app.project_object_user_role pour_suunnitteluttaja ON po.id = pour_suunnitteluttaja.project_object_id + LEFT JOIN app.project_object_user_role pour_participant ON po.id = pour_participant.project_object_id WHERE po.deleted = false -- search date range intersection AND ${timePeriodFragment(input)} @@ -238,10 +240,9 @@ export async function projectObjectSearch(input: ProjectObjectSearch) { rakennuttajaUsers, suunnitteluttajaUsers, )} - - GROUP BY po.id, poi.project_object_id, project.id, poi.object_stage - - + GROUP BY po.id, poi.project_object_id, project.id, poi.object_stage ${ + withGeometries ? sql.fragment`, project_dump.geom, object_dump.geom` : sql.fragment`` + } ), search_results AS (select * from total_results LIMIT ${limit}), project_object_results AS (