From aaa2fd531ca03e63bd967e62b0189937bf98e032 Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Wed, 22 May 2024 16:24:09 +0200 Subject: [PATCH 01/13] created component --- .../MlGeoJsonLayer/assets/sample_1.json | 2 +- .../MlHighlightFeature.meta_.json | 14 ++++ .../MlHighlightFeature.stories.tsx | 46 +++++++++++++ .../MlHighlightFeature/MlHighlightFeature.tsx | 69 +++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/components/MlHighlightFeature/MlHighlightFeature.meta_.json create mode 100644 src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx create mode 100644 src/components/MlHighlightFeature/MlHighlightFeature.tsx diff --git a/src/components/MlGeoJsonLayer/assets/sample_1.json b/src/components/MlGeoJsonLayer/assets/sample_1.json index 457e16e8d..e89b4bb32 100644 --- a/src/components/MlGeoJsonLayer/assets/sample_1.json +++ b/src/components/MlGeoJsonLayer/assets/sample_1.json @@ -89,7 +89,7 @@ } }, { - "id": "2c3972ac51f4350582ecc9de57d47c75", + "id": "4", "type": "Feature", "properties": { "name": "Keiserplatz" }, "geometry": { diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.meta_.json b/src/components/MlHighlightFeature/MlHighlightFeature.meta_.json new file mode 100644 index 000000000..996ad2a0a --- /dev/null +++ b/src/components/MlHighlightFeature/MlHighlightFeature.meta_.json @@ -0,0 +1,14 @@ +{ + "name": "MlHighlightFeature", + "title": "", + "description": "", + "i18n": { + "de": { + "title": "", + "description": "" + } + }, + "tags": [], + "category": "", + "type": "component" +} diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx new file mode 100644 index 000000000..871e80c22 --- /dev/null +++ b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx @@ -0,0 +1,46 @@ +import React, { useRef, useState } from 'react'; + +import MlHighlightFeature from './MlHighlightFeature'; + +import mapContextDecorator from '../../decorators/MapContextDecorator'; + +import Sample1 from '../MlGeoJsonLayer/assets/sample_1.json'; +import MlGeoJsonLayer from '../MlGeoJsonLayer/MlGeoJsonLayer'; +import { Feature, FeatureCollection } from '@turf/turf'; + +const storyoptions = { + title: 'MapComponents/MlHighlightFeature', + component: MlHighlightFeature, + argTypes: {}, + decorators: mapContextDecorator, +}; +export default storyoptions; + +const Template = () => { + const [selectedFeatures, setSelectedFeatures] = useState(); + const selectedId = useRef(); + return ( + <> + { + if (selectedId.current === event.features[0].id) { + setSelectedFeatures(undefined); + selectedId.current = undefined + } else { + setSelectedFeatures({ + type: event.features[0].type, + geometry: event.features[0].geometry, + } as Feature); + selectedId.current = event.features[0].id; + } + }} + /> + + + ); +}; + +export const ExampleConfig = Template.bind({}); +ExampleConfig.parameters = {}; +ExampleConfig.args = {}; diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx new file mode 100644 index 000000000..a6497e78c --- /dev/null +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from 'react'; +//import useMap from "../../hooks/useMap"; +import { featureCollection as createCollcetion, transformTranslate, Feature, FeatureCollection } from '@turf/turf'; +import { LayerSpecification } from 'maplibre-gl'; +import MlGeoJsonLayer from '../MlGeoJsonLayer/MlGeoJsonLayer'; + +export interface MlHighlightFeatureProps { + /** + * Id of the target MapLibre instance in mapContext + */ + mapId?: string; + /** + * The Featrue or FeatureCollecion to be highlighted by the component. + */ + features: Feature | FeatureCollection | undefined; + /** + * Distance between the original and the highlighted Features. Default value: 1 + */ + offset?: number; + /** + * Paint properties of the config object that is passed to the MapLibreGl.addLayer call. All + * available properties are documented in the MapLibreGl documentation + * https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/#fill-extrusion + */ + + paint?: LayerSpecification['paint']; +} + +/** + * It takes a single Feature or a FeatureCollection and generate a new Layer with a highlight of the given Features. + * + */ +const MlHighlightFeature = (props: MlHighlightFeatureProps) => { + // const mapHook = useMap({ + // mapId: props.mapId, + // }); + const [geojson, setGeojson] = useState(); + + function getHighlightFeature(feature: Feature) { + const newFeature: Feature = transformTranslate(feature, 0.5, 0); + return newFeature; + } + + useEffect(() => { + if (!props.features) { + setGeojson(undefined); + return; + } + + if (props.features?.type === 'Feature') { + setGeojson(getHighlightFeature(props.features)); + } else if (props.features?.type === 'FeatureCollection') { + const highlightedFeatures: Feature[] = []; + props.features.features.forEach((feature: Feature) => + highlightedFeatures.push(getHighlightFeature(feature)) + ); + setGeojson(createCollcetion(highlightedFeatures)) + } + }, [props]); + + return <> + {geojson && } + ; +}; + +MlHighlightFeature.defaultProps = { + mapId: undefined, +}; +export default MlHighlightFeature; From 64f376db655b95b9c87efc75b3204e3ffea2e903 Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Wed, 22 May 2024 16:43:38 +0200 Subject: [PATCH 02/13] polygon highlight --- .../MlHighlightFeature/MlHighlightFeature.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx index a6497e78c..e47e50a41 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; //import useMap from "../../hooks/useMap"; -import { featureCollection as createCollcetion, transformTranslate, Feature, FeatureCollection } from '@turf/turf'; +import { featureCollection as createCollection, transformTranslate, Feature, FeatureCollection } from '@turf/turf'; import { LayerSpecification } from 'maplibre-gl'; -import MlGeoJsonLayer from '../MlGeoJsonLayer/MlGeoJsonLayer'; +import MlGeoJsonLayer, { MlGeoJsonLayerProps } from '../MlGeoJsonLayer/MlGeoJsonLayer'; export interface MlHighlightFeatureProps { /** @@ -35,9 +35,21 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { // mapId: props.mapId, // }); const [geojson, setGeojson] = useState(); + const [paint, setPaint] = useState(); + const [layerType, setLayerType] = useState("circle") function getHighlightFeature(feature: Feature) { - const newFeature: Feature = transformTranslate(feature, 0.5, 0); + var newFeature: Feature = feature + + switch (feature.geometry.type){ + case "Polygon" : + console.log("hier!") + // newFeature = transformTranslate(feature, 0.5, 0) ---> Hier wird den Offset für die Linie definiert + setPaint({"line-color": "red"}) + setLayerType("line") + + } + return newFeature; } @@ -54,12 +66,12 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { props.features.features.forEach((feature: Feature) => highlightedFeatures.push(getHighlightFeature(feature)) ); - setGeojson(createCollcetion(highlightedFeatures)) + setGeojson(createCollection(highlightedFeatures)) } }, [props]); return <> - {geojson && } + {geojson && } ; }; From 42f272013895cac3a326f4cb8839e03972a0a04f Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Wed, 22 May 2024 16:56:59 +0200 Subject: [PATCH 03/13] spread props.paint into geometry specific paint --- .../MlHighlightFeature.stories.tsx | 2 +- .../MlHighlightFeature/MlHighlightFeature.tsx | 51 ++++++++++++------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx index 871e80c22..48c8ec9d1 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx @@ -36,7 +36,7 @@ const Template = () => { } }} /> - + ); }; diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx index e47e50a41..b198d1122 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -1,6 +1,10 @@ import React, { useEffect, useState } from 'react'; //import useMap from "../../hooks/useMap"; -import { featureCollection as createCollection, transformTranslate, Feature, FeatureCollection } from '@turf/turf'; +import { + featureCollection as createCollection, + Feature, + FeatureCollection, +} from '@turf/turf'; import { LayerSpecification } from 'maplibre-gl'; import MlGeoJsonLayer, { MlGeoJsonLayerProps } from '../MlGeoJsonLayer/MlGeoJsonLayer'; @@ -14,12 +18,18 @@ export interface MlHighlightFeatureProps { */ features: Feature | FeatureCollection | undefined; /** - * Distance between the original and the highlighted Features. Default value: 1 + * Distance between the original and the highlighted Features. + * For linear features, a positive value offsets the line to the right, relative to the direction of the line, and a negative value to the left. + * For polygon features, a positive value results in an inset, and a negative value results in an outset. + * Default value: 0 */ offset?: number; /** - * Paint properties of the config object that is passed to the MapLibreGl.addLayer call. All - * available properties are documented in the MapLibreGl documentation + * Paint properties of the config object that is passed to the MapLibreGl.addLayer call. + * The paint properties must be compatible with the output type: + * For polygon and line inputs ---> Line Type + * For circle inputs ----> circle Type + * All available properties are documented in the MapLibreGl documentation * https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/#fill-extrusion */ @@ -36,18 +46,16 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { // }); const [geojson, setGeojson] = useState(); const [paint, setPaint] = useState(); - const [layerType, setLayerType] = useState("circle") + const [layerType, setLayerType] = useState('circle'); function getHighlightFeature(feature: Feature) { - var newFeature: Feature = feature + var newFeature: Feature = feature; - switch (feature.geometry.type){ - case "Polygon" : - console.log("hier!") + switch (feature.geometry.type) { + case 'Polygon': // newFeature = transformTranslate(feature, 0.5, 0) ---> Hier wird den Offset für die Linie definiert - setPaint({"line-color": "red"}) - setLayerType("line") - + setPaint({ 'line-color': 'red', 'line-offset': props.offset || 0 , ...props.paint }); + setLayerType('line'); } return newFeature; @@ -60,19 +68,28 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { } if (props.features?.type === 'Feature') { - setGeojson(getHighlightFeature(props.features)); + setGeojson(getHighlightFeature(props.features)); } else if (props.features?.type === 'FeatureCollection') { const highlightedFeatures: Feature[] = []; props.features.features.forEach((feature: Feature) => highlightedFeatures.push(getHighlightFeature(feature)) ); - setGeojson(createCollection(highlightedFeatures)) + setGeojson(createCollection(highlightedFeatures)); } }, [props]); - return <> - {geojson && } - ; + return ( + <> + {geojson && ( + + )} + + ); }; MlHighlightFeature.defaultProps = { From 59f5041299b5b1219131a02e4d561732fce58510 Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Thu, 23 May 2024 13:54:54 +0200 Subject: [PATCH 04/13] input as Feature Array --- .../MlHighlightFeature.stories.tsx | 36 +++++++++++++------ .../MlHighlightFeature/MlHighlightFeature.tsx | 29 +++++++-------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx index 48c8ec9d1..2855b081c 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx @@ -17,26 +17,40 @@ const storyoptions = { export default storyoptions; const Template = () => { - const [selectedFeatures, setSelectedFeatures] = useState(); - const selectedId = useRef(); + const [selectedFeatures, setSelectedFeatures] = useState(); + const selectedId = useRef([]); + return ( <> { - if (selectedId.current === event.features[0].id) { - setSelectedFeatures(undefined); - selectedId.current = undefined + if (selectedId.current?.includes(event.features[0].id)) { + setSelectedFeatures((current) => { + if (current) { + const newArray: Feature[] = current.filter((feature)=> feature.id !== event.features[0].id ) + return newArray; + } + return undefined; + }); + selectedId.current = selectedId.current.filter((idx)=> idx !== event.features[0].id ) } else { - setSelectedFeatures({ - type: event.features[0].type, - geometry: event.features[0].geometry, - } as Feature); - selectedId.current = event.features[0].id; + setSelectedFeatures((current) => { + const newArray: Feature[] = []; + current && newArray.push(...current); + + newArray.push({ + type: event.features[0].type, + geometry: event.features[0].geometry, + id: event.features[0].id + } as Feature); + return newArray; + }); + selectedId.current.push(event.features[0].id) } }} /> - + ); }; diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx index b198d1122..78f7f28a7 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -16,7 +16,7 @@ export interface MlHighlightFeatureProps { /** * The Featrue or FeatureCollecion to be highlighted by the component. */ - features: Feature | FeatureCollection | undefined; + features: Feature[] | undefined; /** * Distance between the original and the highlighted Features. * For linear features, a positive value offsets the line to the right, relative to the direction of the line, and a negative value to the left. @@ -37,14 +37,12 @@ export interface MlHighlightFeatureProps { } /** - * It takes a single Feature or a FeatureCollection and generate a new Layer with a highlight of the given Features. + * It takes a Feature Array and generate a new layer with a highlight of the given Features. * */ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { - // const mapHook = useMap({ - // mapId: props.mapId, - // }); - const [geojson, setGeojson] = useState(); + + const [geojson, setGeojson] = useState(); const [paint, setPaint] = useState(); const [layerType, setLayerType] = useState('circle'); @@ -52,30 +50,28 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { var newFeature: Feature = feature; switch (feature.geometry.type) { - case 'Polygon': - // newFeature = transformTranslate(feature, 0.5, 0) ---> Hier wird den Offset für die Linie definiert - setPaint({ 'line-color': 'red', 'line-offset': props.offset || 0 , ...props.paint }); + case 'Polygon': + setPaint({ 'line-color': 'red', 'line-offset': props.offset, ...props.paint }); setLayerType('line'); } return newFeature; } + useEffect(() => { if (!props.features) { setGeojson(undefined); return; } - - if (props.features?.type === 'Feature') { - setGeojson(getHighlightFeature(props.features)); - } else if (props.features?.type === 'FeatureCollection') { + const highlightedFeatures: Feature[] = []; - props.features.features.forEach((feature: Feature) => + props.features.forEach((feature: Feature) => highlightedFeatures.push(getHighlightFeature(feature)) ); - setGeojson(createCollection(highlightedFeatures)); - } + setGeojson(createCollection(highlightedFeatures)); + + }, [props]); return ( @@ -94,5 +90,6 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { MlHighlightFeature.defaultProps = { mapId: undefined, + offset: 0 }; export default MlHighlightFeature; From c44d4172eee84c9b395fc5bbe462886925d8e844 Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Thu, 23 May 2024 14:14:13 +0200 Subject: [PATCH 05/13] added fitData hook to story --- .../MlGeoJsonLayer/MlGeoJsonLayer.stories.tsx | 4 +-- .../MlHighlightFeature.stories.tsx | 33 ++++++++++++++----- .../MlHighlightFeature/MlHighlightFeature.tsx | 10 +++--- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/components/MlGeoJsonLayer/MlGeoJsonLayer.stories.tsx b/src/components/MlGeoJsonLayer/MlGeoJsonLayer.stories.tsx index 34300fbfd..1c718138a 100644 --- a/src/components/MlGeoJsonLayer/MlGeoJsonLayer.stories.tsx +++ b/src/components/MlGeoJsonLayer/MlGeoJsonLayer.stories.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect, Dispatch, SetStateAction } from 'react'; import { Feature, FeatureCollection, Geometry, GeometryCollection } from '@turf/turf'; -import { DataDrivenPropertyValueSpecification } from 'maplibre-gl'; +import { DataDrivenPropertyValueSpecification, LayerSpecification } from 'maplibre-gl'; import { Typography, Button } from '@mui/material'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; @@ -322,7 +322,7 @@ const SymbolTemplate = (props: MlGeoJsonLayerProps) => { [26, 0.2], ], }, - }, + } as LayerSpecification['layout'], }} /> diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx index 2855b081c..32a2569e5 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import MlHighlightFeature from './MlHighlightFeature'; @@ -7,6 +7,7 @@ import mapContextDecorator from '../../decorators/MapContextDecorator'; import Sample1 from '../MlGeoJsonLayer/assets/sample_1.json'; import MlGeoJsonLayer from '../MlGeoJsonLayer/MlGeoJsonLayer'; import { Feature, FeatureCollection } from '@turf/turf'; +import useMap from '../../hooks/useMap'; const storyoptions = { title: 'MapComponents/MlHighlightFeature', @@ -19,26 +20,41 @@ export default storyoptions; const Template = () => { const [selectedFeatures, setSelectedFeatures] = useState(); const selectedId = useRef([]); + const mapHook = useMap({ + mapId: undefined, + }); + const initializedRef = useRef(false); + + useEffect(() => { + if (!mapHook.map || initializedRef.current) return; + + initializedRef.current = true; + mapHook.map.map.flyTo({ center: [7.105175528281227, 50.73348799274236], zoom: 15 }); + }, [mapHook.map]); return ( - <> + <> + + { + if (selectedId.current?.includes(event.features[0].id)) { setSelectedFeatures((current) => { + let newArray: Feature[] = []; if (current) { - const newArray: Feature[] = current.filter((feature)=> feature.id !== event.features[0].id ) - return newArray; - } - return undefined; + newArray = current.filter((feature)=> feature.id !== event.features[0].id ) + } + return newArray; }); + selectedId.current = selectedId.current.filter((idx)=> idx !== event.features[0].id ) + } else { setSelectedFeatures((current) => { const newArray: Feature[] = []; current && newArray.push(...current); - newArray.push({ type: event.features[0].type, geometry: event.features[0].geometry, @@ -46,11 +62,12 @@ const Template = () => { } as Feature); return newArray; }); + selectedId.current.push(event.features[0].id) } }} /> - + ); }; diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx index 78f7f28a7..66851727e 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -46,7 +46,7 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { const [paint, setPaint] = useState(); const [layerType, setLayerType] = useState('circle'); - function getHighlightFeature(feature: Feature) { + function getHighlightedFeature(feature: Feature) { var newFeature: Feature = feature; switch (feature.geometry.type) { @@ -63,14 +63,12 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { if (!props.features) { setGeojson(undefined); return; - } - + } const highlightedFeatures: Feature[] = []; props.features.forEach((feature: Feature) => - highlightedFeatures.push(getHighlightFeature(feature)) + highlightedFeatures.push(getHighlightedFeature(feature)) ); - setGeojson(createCollection(highlightedFeatures)); - + setGeojson(createCollection(highlightedFeatures)); }, [props]); From b04e4a164cd4b54173317d34cd34ab1d7e088e1d Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Tue, 11 Jun 2024 10:41:48 +0200 Subject: [PATCH 06/13] circle and line features --- .../MlHighlightFeature.stories.tsx | 29 +++++--- .../MlHighlightFeature/MlHighlightFeature.tsx | 67 +++++++++++++------ .../assets/sample_lines.json | 9 +++ .../assets/sample_points.json | 10 +++ .../utils/lineToPolygonconverter.js | 40 +++++++++++ 5 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 src/components/MlHighlightFeature/assets/sample_lines.json create mode 100644 src/components/MlHighlightFeature/assets/sample_points.json create mode 100644 src/components/MlHighlightFeature/utils/lineToPolygonconverter.js diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx index 32a2569e5..23ab45e08 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx @@ -4,11 +4,14 @@ import MlHighlightFeature from './MlHighlightFeature'; import mapContextDecorator from '../../decorators/MapContextDecorator'; -import Sample1 from '../MlGeoJsonLayer/assets/sample_1.json'; +import examplePolygons from '../MlGeoJsonLayer/assets/sample_1.json'; +import exampleLines from './assets/sample_lines.json'; +import examplePoints from './assets/sample_points.json'; import MlGeoJsonLayer from '../MlGeoJsonLayer/MlGeoJsonLayer'; import { Feature, FeatureCollection } from '@turf/turf'; import useMap from '../../hooks/useMap'; + const storyoptions = { title: 'MapComponents/MlHighlightFeature', component: MlHighlightFeature, @@ -17,7 +20,7 @@ const storyoptions = { }; export default storyoptions; -const Template = () => { +const Template = (props: any) => { const [selectedFeatures, setSelectedFeatures] = useState(); const selectedId = useRef([]); const mapHook = useMap({ @@ -34,12 +37,12 @@ const Template = () => { return ( <> - + - { - if (selectedId.current?.includes(event.features[0].id)) { setSelectedFeatures((current) => { let newArray: Feature[] = []; @@ -72,6 +75,14 @@ const Template = () => { ); }; -export const ExampleConfig = Template.bind({}); -ExampleConfig.parameters = {}; -ExampleConfig.args = {}; +export const Polygon = Template.bind({}); +Polygon.parameters = {}; +Polygon.args = { sample: examplePolygons, type: "fill"}; + +export const Line = Template.bind({}); +Line.parameters = {}; +Line.args = {sample: exampleLines, type: "line"}; + +export const Circle = Template.bind({}); +Circle.parameters = {}; +Circle.args = {sample: examplePoints, type: "circle"}; diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx index 66851727e..0e682e5ac 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -2,11 +2,13 @@ import React, { useEffect, useState } from 'react'; //import useMap from "../../hooks/useMap"; import { featureCollection as createCollection, - Feature, FeatureCollection, + Feature, + Geometry, } from '@turf/turf'; import { LayerSpecification } from 'maplibre-gl'; import MlGeoJsonLayer, { MlGeoJsonLayerProps } from '../MlGeoJsonLayer/MlGeoJsonLayer'; +import createPolygonAroundLine from './utils/lineToPolygonconverter'; export interface MlHighlightFeatureProps { /** @@ -19,15 +21,16 @@ export interface MlHighlightFeatureProps { features: Feature[] | undefined; /** * Distance between the original and the highlighted Features. - * For linear features, a positive value offsets the line to the right, relative to the direction of the line, and a negative value to the left. - * For polygon features, a positive value results in an inset, and a negative value results in an outset. - * Default value: 0 + * + * For polygon features (line and polygon inputs), a positive value results in an inset, while a negative value results in an outset. + * For circle features (point input), negative values are not allowed; therefore, the absolute value will be used. + * Default value: -5 */ offset?: number; /** * Paint properties of the config object that is passed to the MapLibreGl.addLayer call. - * The paint properties must be compatible with the output type: - * For polygon and line inputs ---> Line Type + * The paint properties must be compatible with the output type: + * For polygon and line inputs ---> Line Type * For circle inputs ----> circle Type * All available properties are documented in the MapLibreGl documentation * https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/#fill-extrusion @@ -40,36 +43,60 @@ export interface MlHighlightFeatureProps { * It takes a Feature Array and generate a new layer with a highlight of the given Features. * */ -const MlHighlightFeature = (props: MlHighlightFeatureProps) => { +const defaultColor = 'red'; + +const MlHighlightFeature = (props: MlHighlightFeatureProps) => { const [geojson, setGeojson] = useState(); const [paint, setPaint] = useState(); const [layerType, setLayerType] = useState('circle'); function getHighlightedFeature(feature: Feature) { - var newFeature: Feature = feature; + console.log(feature); + var newFeature: Feature = feature; switch (feature.geometry.type) { - case 'Polygon': - setPaint({ 'line-color': 'red', 'line-offset': props.offset, ...props.paint }); + case 'Polygon': + setPaint({ 'line-color': defaultColor, 'line-offset': props.offset, ...props.paint }); setLayerType('line'); - } + break; + case 'LineString': + setPaint({ 'line-color': defaultColor, 'line-offset': props.offset, ...props.paint }); + setLayerType('line'); + + // transform newFeature into a polygon that surrounds the line + newFeature.geometry = createPolygonAroundLine( + (newFeature.geometry as Geometry).coordinates, + props.offset && props.offset * 1e-5 + ); + break; + + case 'Point': + setLayerType('circle'); + setPaint({ + 'circle-stroke-color': defaultColor, + 'circle-opacity': 0, + 'circle-radius': props.offset && Math.abs(props.offset), + ...props.paint, + }); + + break; + } + console.log(newFeature); return newFeature; } - useEffect(() => { if (!props.features) { setGeojson(undefined); return; - } - const highlightedFeatures: Feature[] = []; - props.features.forEach((feature: Feature) => - highlightedFeatures.push(getHighlightedFeature(feature)) - ); - setGeojson(createCollection(highlightedFeatures)); - + } + const highlightedFeatures: Feature[] = []; + props.features.forEach((feature: Feature) => + highlightedFeatures.push(getHighlightedFeature(feature)) + ); + setGeojson(createCollection(highlightedFeatures)); }, [props]); return ( @@ -88,6 +115,6 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { MlHighlightFeature.defaultProps = { mapId: undefined, - offset: 0 + offset: -5, }; export default MlHighlightFeature; diff --git a/src/components/MlHighlightFeature/assets/sample_lines.json b/src/components/MlHighlightFeature/assets/sample_lines.json new file mode 100644 index 000000000..5e055eeaa --- /dev/null +++ b/src/components/MlHighlightFeature/assets/sample_lines.json @@ -0,0 +1,9 @@ +{ +"type": "FeatureCollection", +"name": "sample_lines", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, +"features": [ +{ "type": "Feature", "properties": { "fid": 1 }, "geometry": { "type": "LineString", "coordinates": [ [ 7.097676922982851, 50.734551394091113 ], [ 7.104988124482984, 50.730979883248132 ] ] } }, +{ "type": "Feature", "properties": { "fid": 2 }, "geometry": { "type": "LineString", "coordinates": [ [ 7.097826896346955, 50.735844664743446 ], [ 7.099064176600823, 50.73571415281139 ], [ 7.099832790091864, 50.735785341183039 ], [ 7.101126310357271, 50.735571775743473 ], [ 7.102607297327812, 50.735073452597419 ], [ 7.103900817593219, 50.734397148418843 ], [ 7.104781911107337, 50.733851351961363 ], [ 7.106337884759929, 50.733080106993825 ] ] } } +] +} diff --git a/src/components/MlHighlightFeature/assets/sample_points.json b/src/components/MlHighlightFeature/assets/sample_points.json new file mode 100644 index 000000000..df417428d --- /dev/null +++ b/src/components/MlHighlightFeature/assets/sample_points.json @@ -0,0 +1,10 @@ +{ +"type": "FeatureCollection", +"name": "sample_points", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, +"features": [ +{ "type": "Feature", "properties": { "fid": 1 }, "geometry": { "type": "Point", "coordinates": [ 7.103957057604736, 50.73287291831204 ] } }, +{ "type": "Feature", "properties": { "fid": 2 }, "geometry": { "type": "Point", "coordinates": [ 7.100868183124798, 50.734072226879221 ] } }, +{ "type": "Feature", "properties": { "fid": 3 }, "geometry": { "type": "Point", "coordinates": [ 7.103567703678694, 50.735123650224132 ] } } +] +} diff --git a/src/components/MlHighlightFeature/utils/lineToPolygonconverter.js b/src/components/MlHighlightFeature/utils/lineToPolygonconverter.js new file mode 100644 index 000000000..857266c2d --- /dev/null +++ b/src/components/MlHighlightFeature/utils/lineToPolygonconverter.js @@ -0,0 +1,40 @@ +function createPolygonAroundLine(line, offsetDistance) { + function offsetPoint(point, angle, distance) { + return [ + point[0] + Math.cos(angle) * distance, + point[1] + Math.sin(angle) * distance + ]; + } + + function angleBetweenPoints(p1, p2) { + return Math.atan2(p2[1] - p1[1], p2[0] - p1[0]); + } + + const leftOffsetPoints = []; + const rightOffsetPoints = []; + + for (let i = 0; i < line.length - 1; i++) { + const p1 = line[i]; + const p2 = line[i + 1]; + const angle = angleBetweenPoints(p1, p2); + const leftAngle = angle + Math.PI / 2; + const rightAngle = angle - Math.PI / 2; + + leftOffsetPoints.push(offsetPoint(p1, leftAngle, offsetDistance)); + rightOffsetPoints.push(offsetPoint(p1, rightAngle, offsetDistance)); + } + + // Add the last point with the same angle as the second last segment + const lastAngle = angleBetweenPoints(line[line.length - 2], line[line.length - 1]); + const lastLeftAngle = lastAngle + Math.PI / 2; + const lastRightAngle = lastAngle - Math.PI / 2; + + leftOffsetPoints.push(offsetPoint(line[line.length - 1], lastLeftAngle, offsetDistance)); + rightOffsetPoints.push(offsetPoint(line[line.length - 1], lastRightAngle, offsetDistance)); + + // Combine points to form the polygon + const polygon = [...leftOffsetPoints, ...rightOffsetPoints.reverse()]; + return { type: 'Polygon', coordinates: [polygon] }; +} + +export default createPolygonAroundLine; From 2d5a00c7457906a667c12e5f0a9af696dcace73a Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Tue, 11 Jun 2024 10:49:16 +0200 Subject: [PATCH 07/13] lineToPolygon in TS --- .../MlHighlightFeature/MlHighlightFeature.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.tsx index 0e682e5ac..70723226d 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; -//import useMap from "../../hooks/useMap"; import { featureCollection as createCollection, FeatureCollection, Feature, Geometry, + Position, } from '@turf/turf'; import { LayerSpecification } from 'maplibre-gl'; import MlGeoJsonLayer, { MlGeoJsonLayerProps } from '../MlGeoJsonLayer/MlGeoJsonLayer'; @@ -52,8 +52,6 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { const [layerType, setLayerType] = useState('circle'); function getHighlightedFeature(feature: Feature) { - console.log(feature); - var newFeature: Feature = feature; switch (feature.geometry.type) { case 'Polygon': @@ -66,9 +64,9 @@ const MlHighlightFeature = (props: MlHighlightFeatureProps) => { setLayerType('line'); // transform newFeature into a polygon that surrounds the line - newFeature.geometry = createPolygonAroundLine( - (newFeature.geometry as Geometry).coordinates, - props.offset && props.offset * 1e-5 + newFeature.geometry = createPolygonAroundLine( + (newFeature.geometry as Geometry).coordinates as Position[], + props.offset ? props.offset * 1e-5 : 1 * 1e-5 ); break; From 1486dbd2a7c2420cc7b6326146791d1ad280d359 Mon Sep 17 00:00:00 2001 From: Martin Alzueta Date: Tue, 11 Jun 2024 13:51:47 +0200 Subject: [PATCH 08/13] catalogue story --- .../MlHighlightFeature.stories.tsx | 136 +++++++++++++++--- .../MlHighlightFeature/MlHighlightFeature.tsx | 7 +- .../assets/sample_lines.json | 4 +- .../assets/sample_points.json | 6 +- ...converter.js => lineToPolygonconverter.ts} | 8 +- 5 files changed, 131 insertions(+), 30 deletions(-) rename src/components/MlHighlightFeature/utils/{lineToPolygonconverter.js => lineToPolygonconverter.ts} (83%) diff --git a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx index 23ab45e08..c3bab466c 100644 --- a/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx +++ b/src/components/MlHighlightFeature/MlHighlightFeature.stories.tsx @@ -1,16 +1,17 @@ import React, { useEffect, useRef, useState } from 'react'; -import MlHighlightFeature from './MlHighlightFeature'; +import MlHighlightFeature, { MlHighlightFeatureProps } from './MlHighlightFeature'; import mapContextDecorator from '../../decorators/MapContextDecorator'; import examplePolygons from '../MlGeoJsonLayer/assets/sample_1.json'; -import exampleLines from './assets/sample_lines.json'; +import exampleLines from './assets/sample_lines.json'; import examplePoints from './assets/sample_points.json'; import MlGeoJsonLayer from '../MlGeoJsonLayer/MlGeoJsonLayer'; import { Feature, FeatureCollection } from '@turf/turf'; import useMap from '../../hooks/useMap'; - +import TopToolbar from '../../ui_components/TopToolbar'; +import { Button, Menu, MenuItem, Typography } from '@mui/material'; const storyoptions = { title: 'MapComponents/MlHighlightFeature', @@ -20,9 +21,29 @@ const storyoptions = { }; export default storyoptions; -const Template = (props: any) => { - const [selectedFeatures, setSelectedFeatures] = useState(); - const selectedId = useRef([]); +interface TemplateProps extends MlHighlightFeatureProps { + type: + | 'symbol' + | 'circle' + | 'fill' + | 'line' + | 'heatmap' + | 'fill-extrusion' + | 'hillshade' + | 'background' + | undefined; + sample: FeatureCollection; +} + +const configTitles = { + circle: 'Highlight point geometries', + line: 'Highlight line geometries', + polygon: 'Highlight polygon geometries', +}; + +const Template = (props: TemplateProps) => { + const [selectedFeatures, setSelectedFeatures] = useState(props.features); + const selectedId = useRef([]); const mapHook = useMap({ mapId: undefined, }); @@ -36,10 +57,15 @@ const Template = (props: any) => { }, [mapHook.map]); return ( - <> - + <> + - { @@ -47,13 +73,12 @@ const Template = (props: any) => { setSelectedFeatures((current) => { let newArray: Feature[] = []; if (current) { - newArray = current.filter((feature)=> feature.id !== event.features[0].id ) - } - return newArray; + newArray = current.filter((feature) => feature.id !== event.features[0].id); + } + return newArray; }); - selectedId.current = selectedId.current.filter((idx)=> idx !== event.features[0].id ) - + selectedId.current = selectedId.current.filter((idx) => idx !== event.features[0].id); } else { setSelectedFeatures((current) => { const newArray: Feature[] = []; @@ -61,28 +86,99 @@ const Template = (props: any) => { newArray.push({ type: event.features[0].type, geometry: event.features[0].geometry, - id: event.features[0].id + id: event.features[0].id, } as Feature); return newArray; }); - selectedId.current.push(event.features[0].id) + selectedId.current.push(event.features[0].id); } }} /> - ); }; export const Polygon = Template.bind({}); Polygon.parameters = {}; -Polygon.args = { sample: examplePolygons, type: "fill"}; +Polygon.args = { + sample: examplePolygons, + type: 'fill', + offset: -5, + paint: { 'line-opacity': 0.5 }, +}; export const Line = Template.bind({}); Line.parameters = {}; -Line.args = {sample: exampleLines, type: "line"}; +Line.args = { sample: exampleLines, type: 'line', offset: -5, paint: { 'line-opacity': 0.5 } }; export const Circle = Template.bind({}); Circle.parameters = {}; -Circle.args = {sample: examplePoints, type: "circle"}; +Circle.args = { sample: examplePoints, type: 'circle', offset: 5 }; + +/// Catalogue Story + +const catalogueTemplate = () => { + const [openSidebar, setOpenSidebar] = useState(true); + const [selectedLayer, setSelectedLayer] = useState('polygon'); + + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLayerSelect = (layer: string) => { + setSelectedLayer(layer); + }; + + return ( + <> + + + {configTitles[selectedLayer]} + + + + + handleLayerSelect('polygon')}> + Polygon Configuration + + handleLayerSelect('circle')}>Circle Configuration + handleLayerSelect('line')}>Line Configuration + + {selectedLayer === 'circle' &&