From 30475e4b596e6afba68a274e2dd386a05dd9aea5 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Thu, 23 May 2024 15:58:35 +0200 Subject: [PATCH 01/34] Feature/scts 2 pointer marker (#11) * Leaflet marker example setup * Marker docs and docs for Leaflet icons added --- src/assets/icons/map-marker.svg | 13 ++++ src/pages/Marker/Marker.test.tsx | 18 +++++ src/pages/Marker/Marker.tsx | 67 ++++++++++++++++ src/pages/Marker/icons/customMarker.tsx | 11 +++ src/pages/Marker/styles.module.css | 4 + src/stories/Icons.mdx | 50 ++++++++++++ src/stories/pages/Marker/index.mdx | 93 +++++++++++++++++++++++ src/stories/pages/Marker/index.stories.ts | 19 +++++ 8 files changed, 275 insertions(+) create mode 100644 src/assets/icons/map-marker.svg create mode 100644 src/pages/Marker/Marker.test.tsx create mode 100644 src/pages/Marker/Marker.tsx create mode 100644 src/pages/Marker/icons/customMarker.tsx create mode 100644 src/pages/Marker/styles.module.css create mode 100644 src/stories/Icons.mdx create mode 100644 src/stories/pages/Marker/index.mdx create mode 100644 src/stories/pages/Marker/index.stories.ts diff --git a/src/assets/icons/map-marker.svg b/src/assets/icons/map-marker.svg new file mode 100644 index 0000000..2d89c1a --- /dev/null +++ b/src/assets/icons/map-marker.svg @@ -0,0 +1,13 @@ + + + Name=Location + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/Marker/Marker.test.tsx b/src/pages/Marker/Marker.test.tsx new file mode 100644 index 0000000..0cbdb24 --- /dev/null +++ b/src/pages/Marker/Marker.test.tsx @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import Marker from './Marker'; + +describe('Marker', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('renders a leaflet marker icon', () => { + const { container } = render(); + const icon = container.querySelector('.c-marker'); + + expect(icon).toBeInTheDocument(); + expect((icon as HTMLImageElement)?.alt).toEqual('Marker'); + }); +}); diff --git a/src/pages/Marker/Marker.tsx b/src/pages/Marker/Marker.tsx new file mode 100644 index 0000000..e3d62fc --- /dev/null +++ b/src/pages/Marker/Marker.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import L from 'leaflet'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; +import customMarker from './icons/customMarker'; + +const Marker: FunctionComponent = () => { + const containerRef = useRef(null); + const [mapInstance, setMapInstance] = useState(null); + const [, setMarkerInstance] = useState(null); + const createdMapInstance = useRef(false); + + // Set the Leaflet map and Amsterdam base layer + useEffect(() => { + if (containerRef.current === null || createdMapInstance.current !== false) { + return; + } + + const map = new L.Map(containerRef.current, { + center: L.latLng([52.370216, 4.895168]), + zoom: 12, + layers: [ + L.tileLayer('https://{s}.data.amsterdam.nl/topo_rd/{z}/{x}/{y}.png', { + attribution: '', + subdomains: ['t1', 't2', 't3', 't4'], + tms: true, + }), + ], + zoomControl: false, + maxZoom: 16, + minZoom: 3, + crs: getCrsRd(), + maxBounds: [ + [52.25168, 4.64034], + [52.50536, 5.10737], + ], + }); + + map.attributionControl.setPrefix(false); + + createdMapInstance.current = true; + setMapInstance(map); + + return () => { + if (mapInstance) mapInstance.remove(); + }; + }, []); + + // Create the marker and add it to the map + useEffect(() => { + if (mapInstance) { + const marker = L.marker([52.370216, 4.895168], { + // There are many more options to choose from @see https://leafletjs.com/reference.html#marker + icon: customMarker, + }) + .addTo(mapInstance) + // Marker click event listener example + .on('click', () => alert('Marker click!')); + setMarkerInstance(marker); + } + }, [mapInstance]); + + return
; +}; + +export default Marker; diff --git a/src/pages/Marker/icons/customMarker.tsx b/src/pages/Marker/icons/customMarker.tsx new file mode 100644 index 0000000..95f5661 --- /dev/null +++ b/src/pages/Marker/icons/customMarker.tsx @@ -0,0 +1,11 @@ +import L from 'leaflet'; +import MapMarkerIcon from '../../../assets/icons/map-marker.svg'; + +const customMarker = L.icon({ + iconUrl: MapMarkerIcon, + iconSize: [24, 32], + iconAnchor: [12, 32], + className: 'c-marker', +}); + +export default customMarker; diff --git a/src/pages/Marker/styles.module.css b/src/pages/Marker/styles.module.css new file mode 100644 index 0000000..d171d7c --- /dev/null +++ b/src/pages/Marker/styles.module.css @@ -0,0 +1,4 @@ +.container { + height: 100%; + min-height: 100%; +} diff --git a/src/stories/Icons.mdx b/src/stories/Icons.mdx new file mode 100644 index 0000000..b4f9380 --- /dev/null +++ b/src/stories/Icons.mdx @@ -0,0 +1,50 @@ +import { Meta } from "@storybook/blocks"; + + + +# Icons + +## In short + +- Use `L.icon` if you need a simple, image-based marker. +- Use `L.divIcon` if you need a highly customizable marker with HTML content. + +Your choice will depend on whether you prioritize performance and simplicity (`L.icon`) or customization and flexibility (`L.divIcon`). If you need to add dynamic content, animations, or specific styling to your markers, `L.divIcon` is the better option. For static images or less complex markers, `L.icon` is more straightforward and efficient. + +## Background + +When creating a marker, it is common to replace the default Leaflet marker image. There are two possible replacements: + +1. `L.icon` - [docs](https://leafletjs.com/reference.html#icon) + +```js +var myIcon = L.icon({ + iconUrl: 'my-icon.png', + iconSize: [38, 95], + iconAnchor: [22, 94], + popupAnchor: [-3, -76], + shadowUrl: 'my-icon-shadow.png', + shadowSize: [68, 95], + shadowAnchor: [22, 94] +}); +``` + +In Leaflet, the default image marker has a shadow image, which is aligned according to the sizing options. This shadow can be disabled via setting `shadowUrl` to `null`. + +If the marker is created with the `draggable: true` option, the `iconAnchor` option is quite important, as it corresponds with the 'tip of the icon' - where the cursor 'grabs' the marker. + +2. `L.divIcon` - [docs](https://leafletjs.com/reference.html#divicon) + +```js +var myIcon = L.divIcon({ + className: 'my-div-icon', + html: '', + iconSize: [24, 32], + iconAnchor: [12, 32], + popupAnchor: [0, -30], +}); +``` + +The main advantage with the `L.divIcon` is that you can pass it any HTML element. These days icons are often SVG files, which work with CSS styling. Therefore, dynamic styling, such a different background-color on hover, is achievable - and works faster than having to write JS to listen for the `mouseover` event on the marker and then run the associated side-effect. + +One disadvantage of this is passing complex HTML elements could lead to a less performant map. diff --git a/src/stories/pages/Marker/index.mdx b/src/stories/pages/Marker/index.mdx new file mode 100644 index 0000000..7ebec01 --- /dev/null +++ b/src/stories/pages/Marker/index.mdx @@ -0,0 +1,93 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as MarkerStories from './index.stories'; +import Marker from '@/pages/Marker/Marker?raw'; +import styles from '@/pages/Marker/styles.module.css?raw'; +import customMarker from '@/pages/Marker/icons/customMarker?raw'; +import icon from '@/assets/icons/map-marker.svg?raw'; + + + +# Marker +## Requirements + +- This example is built upon the [BaseMap component example](../?path=/docs/react-baselayer--docs). +- [See global requirements list](../?path=/docs/global-requirements--docs) + +## Description + +A marker is used to display a location on a map. By default, a marker is a HTML image element rendered inside the parent map DOM element. This marker element can be configured, extended and (like in this example) replaced with another icon. + +In this code example, the default Leaflet marker ([example](https://leafletjs.com/examples/layers-control/)) is replaced with the `L.icon` ([docs](https://leafletjs.com/reference.html#icon)); another alternative to this is the `L.divIcon` ([docs](https://leafletjs.com/reference.html#divicon)). [Read more Leaflet icons here](../?path=/docs/icons--docs). + +The primary code in regards to creating a Leaflet marker, is lines 50-59: + +```js +useEffect(() => { + if (mapInstance) { + const marker = L.marker(L.latLng([52.370216, 4.895168]), { + // There are many more options to choose from @see https://leafletjs.com/reference.html#marker + icon: customMarker + }).addTo(mapInstance) + .on('click', () => alert('Marker click!')); + setMarkerInstance(marker); + } +}, [mapInstance]); +``` + +This creates a marker at the coordinates (52.370216, 4.895168), which is added to the map (via the `addTo` method) and includes an example event listener that will be triggered on marker clicks. Then in the rest of the code, this marker element, can be referred to via the `markerInstance` state variable and interacted with using Leaflet methods. + +A Leaflet marker element consists of [events](https://leafletjs.com/reference.html#marker-move), [methods](https://leafletjs.com/reference.html#marker-l-marker) and [options](https://leafletjs.com/reference.html#marker-icon). + +### Large numbers of markers can lead to degraded performance + +A standard Leaflet marker is a HTML image element. Therefore, if there are 100 markers, then there are 100 HTML image elements - each one with its own events, listeners and side-effects - another element to add to the DOM tree. Modern browsers and devices are quite efficient so negative performance often won't be noticed until you are handling tens of thousands of markers. + +The real solution to this is to ideally never render so many markers simultaneously. However, with some APIs that isn't always an easy option. This is where clustering ideally should be implemented or the `preferCanvas` option is set to `true` when creating your Leaflet map. + +The `preferCanvas` instructs Leaflet to use the HTML Canvas element, which performs a lot quicker than the traditional HTML DOM tree. [See docs](https://leafletjs.com/reference.html#map-prefercanvas). + +## Usage Scenarios + +- **Location Pins**: Highlighting specific locations such as restaurants, shops, landmarks, etc. +- **Data Visualization**: Displaying data points like weather stations, earthquake epicenters, etc. +- **Interactive Maps**: Providing interactive elements for user interaction, such as selecting meeting points or identifying places of interest. + +## How to implement + +To implement a Leaflet marker, there are four files: + +1. The React component + * [Marker.tsx](#1-markertsx) + * *This is based on the [BaseMap component example](../?path=/docs/react-baselayer--docs) so includes a dependency on [`utils/getCrsRd`](../?path=/docs/react-baselayer--docs#1-getcrsrdts).* +2. The custom icon + * [icons/customMarker.tsx](#1-iconscustommarkertsx) +3. The CSS styles (1 file) + * [styles.module.css](#1-stylesmodulecss) +4. Image + * [assets/icons/map-marker.svg](#1-map-markersvg) + +## Usage + +### React Components + +#### 1. Marker.tsx + + + +### Custom icon + +#### 1. icons/customMarker.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + +### Assets + +#### 1. map-marker.svg + + diff --git a/src/stories/pages/Marker/index.stories.ts b/src/stories/pages/Marker/index.stories.ts new file mode 100644 index 0000000..30f6c13 --- /dev/null +++ b/src/stories/pages/Marker/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Marker from '@/pages/Marker/Marker'; + +const meta = { + title: 'React/Marker', + component: Marker, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; From 89fa847a97c7546606da7998751ce40058257168 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Wed, 29 May 2024 11:20:30 +0200 Subject: [PATCH 02/34] Polyline layer example with docs and test (#22) --- .../PolylineLayer/PolylineLayer.test.tsx | 31 ++++++ src/pages/PolylineLayer/PolylineLayer.tsx | 80 +++++++++++++++ src/pages/PolylineLayer/data.json | 18 ++++ src/pages/PolylineLayer/layerStyles.ts | 10 ++ src/pages/PolylineLayer/styles.module.css | 4 + src/stories/pages/PolylineLayer/index.mdx | 98 +++++++++++++++++++ .../pages/PolylineLayer/index.stories.ts | 19 ++++ 7 files changed, 260 insertions(+) create mode 100644 src/pages/PolylineLayer/PolylineLayer.test.tsx create mode 100644 src/pages/PolylineLayer/PolylineLayer.tsx create mode 100644 src/pages/PolylineLayer/data.json create mode 100644 src/pages/PolylineLayer/layerStyles.ts create mode 100644 src/pages/PolylineLayer/styles.module.css create mode 100644 src/stories/pages/PolylineLayer/index.mdx create mode 100644 src/stories/pages/PolylineLayer/index.stories.ts diff --git a/src/pages/PolylineLayer/PolylineLayer.test.tsx b/src/pages/PolylineLayer/PolylineLayer.test.tsx new file mode 100644 index 0000000..929c56f --- /dev/null +++ b/src/pages/PolylineLayer/PolylineLayer.test.tsx @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import PolylineLayer from './PolylineLayer'; +import { lineStyles, lineHoverStyles } from './layerStyles'; + +describe('PolylineLayer', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('renders a polygon layer', () => { + const { container } = render(); + const layer = container.querySelector('.c-layer'); + + expect(layer).toBeInTheDocument(); + }); + + it('changes background on mouseover', async () => { + const { container } = render(); + const layer = container.querySelector('.c-layer'); + + expect(layer?.getAttribute('stroke')).toEqual(lineStyles.color); + + // @ts-expect-error Type 'null' is not assignable to type + fireEvent.mouseOver(container.querySelector('.c-layer')); + await waitFor(() => + expect(layer?.getAttribute('stroke')).toEqual(lineHoverStyles.color) + ); + }); +}); diff --git a/src/pages/PolylineLayer/PolylineLayer.tsx b/src/pages/PolylineLayer/PolylineLayer.tsx new file mode 100644 index 0000000..c5ecceb --- /dev/null +++ b/src/pages/PolylineLayer/PolylineLayer.tsx @@ -0,0 +1,80 @@ +import { useEffect, useRef, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import L, { LatLngTuple } from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; +import data from './data.json'; +import { lineHoverStyles, lineStyles } from './layerStyles'; + +const PolylineLayer: FunctionComponent = () => { + const containerRef = useRef(null); + const [mapInstance, setMapInstance] = useState(null); + const createdMapInstance = useRef(false); + + const polylineRef = useRef(null); + + // Set the Leaflet map and Amsterdam base layer + useEffect(() => { + if (containerRef.current === null || createdMapInstance.current !== false) { + return; + } + + const map = new L.Map(containerRef.current, { + center: [52.37079908397672, 4.89500238214001], + zoom: 15, + layers: [ + L.tileLayer('https://{s}.data.amsterdam.nl/topo_rd/{z}/{x}/{y}.png', { + attribution: '', + subdomains: ['t1', 't2', 't3', 't4'], + tms: true, + }), + ], + zoomControl: false, + maxZoom: 16, + minZoom: 3, + crs: getCrsRd(), + maxBounds: [ + [52.25168, 4.64034], + [52.50536, 5.10737], + ], + }); + + map.attributionControl.setPrefix(false); + + createdMapInstance.current = true; + setMapInstance(map); + + return () => { + if (mapInstance) mapInstance.remove(); + }; + }, []); + + // Create the polygon layer and add it to the map + useEffect(() => { + if (mapInstance) { + // TypeScript will often throw errors with Leaflet coordinate sets if you don't explicitly cast the type + polylineRef.current = L.polyline( + data.geometry.coordinates[0] as LatLngTuple[], + { ...lineStyles, className: 'c-layer' } + ) + .addTo(mapInstance) + .on('mouseover', () => { + polylineRef.current?.setStyle(lineHoverStyles); + }) + .on('mouseout', () => { + polylineRef.current?.setStyle(lineStyles); + }); + } + + return () => { + if (polylineRef.current && mapInstance) { + mapInstance.removeLayer(polylineRef.current); + } + }; + }, [data, mapInstance]); + + return
; +}; + +export default PolylineLayer; diff --git a/src/pages/PolylineLayer/data.json b/src/pages/PolylineLayer/data.json new file mode 100644 index 0000000..c5f0526 --- /dev/null +++ b/src/pages/PolylineLayer/data.json @@ -0,0 +1,18 @@ +{ + "geometry": { + "type": "MultiLineString", + "coordinates": [ + [ + [52.37048257777301, 4.894874428904198], + [52.370554861935126, 4.894898864319151], + [52.37055767285022, 4.894899406706797], + [52.37058186436687, 4.894908315567703], + [52.37079908397672, 4.89500238214001], + [52.37085087918953, 4.895030257356288], + [52.37089819409757, 4.895061394436971], + [52.370944121895796, 4.895096676809393], + [52.370972589599155, 4.895120795880029] + ] + ] + } +} diff --git a/src/pages/PolylineLayer/layerStyles.ts b/src/pages/PolylineLayer/layerStyles.ts new file mode 100644 index 0000000..926d909 --- /dev/null +++ b/src/pages/PolylineLayer/layerStyles.ts @@ -0,0 +1,10 @@ +export const lineStyles = { + color: '#0000ff', + opacity: 0.6, + weight: 2, +}; + +export const lineHoverStyles = { + color: '#ff0000', + weight: 2, +}; diff --git a/src/pages/PolylineLayer/styles.module.css b/src/pages/PolylineLayer/styles.module.css new file mode 100644 index 0000000..d171d7c --- /dev/null +++ b/src/pages/PolylineLayer/styles.module.css @@ -0,0 +1,4 @@ +.container { + height: 100%; + min-height: 100%; +} diff --git a/src/stories/pages/PolylineLayer/index.mdx b/src/stories/pages/PolylineLayer/index.mdx new file mode 100644 index 0000000..62bcc6c --- /dev/null +++ b/src/stories/pages/PolylineLayer/index.mdx @@ -0,0 +1,98 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as PolylineLayerStories from './index.stories'; +import PolylineLayer from '@/pages/PolylineLayer/PolylineLayer?raw'; +import styles from '@/pages/PolylineLayer/styles.module.css?raw'; +import layerStyles from '@/pages/PolylineLayer/layerStyles?raw'; +import data from '@/pages/PolylineLayer/data.json?raw'; + + + +# PolylineLayer + +## Requirements + +- This example is built upon the [BaseMap component example](../?path=/docs/react-baselayer--docs). +- [See global requirements list](../?path=/docs/global-requirements--docs) + +## Description + +A polyline layer is used to display lines on a map. By default, a polyline layer is a HTML SVG element rendered inside the parent map DOM element. This polyline layer can be configured, extended and restyled ([see docs](https://leafletjs.com/reference.html#polyline)). + +The primary code in regards to creating a Leaflet polyline layer, is lines 64-87: + +```js +useEffect(() => { + if (mapInstance) { + // TypeScript will often throw errors with Leaflet coordinate sets if you don't explicitly cast the type + polylineRef.current = L.polyline( + data.geometry.coordinates[0] as LatLngTuple[], + lineStyles + ) + .addTo(mapInstance) + .on('mouseover', () => { + polylineRef.current?.setStyle(lineHoverStyles); + }) + .on('mouseout', () => { + polylineRef.current?.setStyle(lineStyles); + }); + } + + return () => { + if (polylineRef.current && mapInstance) { + mapInstance.removeLayer(polylineRef.current); + } + }; +}, [data, mapInstance]); +``` + +## Usage Scenarios + +- **Routing and Navigation**: Displaying the route for a navigation system. +- **Hiking and Biking Trails**: Visualizing outdoor recreational trails. +- **Geographical Boundaries and Borders**: Defining administrative or geographical boundaries. +- **Public Transportation Routes**: Mapping bus, train, or tram routes. +- **Historical Paths and Routes**: Visualizing historical routes or trade paths. +- **Event Tracking and Routes**: Showing routes for events like marathons or parades. +- **Data Visualization**: Representing data flows or connections. + +## How to implement + +To implement a Leaflet polygon layer there are three files required, this example also uses an extra file for demo data: + +1. The React components + - [PolylineLayer.tsx](#1-polylinelayertsx) +2. The CSS styles (1 file) + - [styles.module.css](#1-stylesmodulecss) +3. The layer styles + - [layerStyles.ts](#1-layerstylests) +4. Demo data (1 file) + - [data.json](#1-datajson) + - This data represents the coordinates for Vondelpark. + +## Usage + +The following files are required: + +### React Components + +#### 1. PolylineLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + +### Layer styles + +#### 1. layerStyles.ts + + + +### Utils + +#### 1. data.json + + \ No newline at end of file diff --git a/src/stories/pages/PolylineLayer/index.stories.ts b/src/stories/pages/PolylineLayer/index.stories.ts new file mode 100644 index 0000000..0f668c4 --- /dev/null +++ b/src/stories/pages/PolylineLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import PolylineLayer from '@/pages/PolylineLayer/PolylineLayer'; + +const meta = { + title: 'React/PolylineLayer', + component: PolylineLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; From 077a86c4f675f54ff8c0ef7a41a48d51f7b46511 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Wed, 29 May 2024 11:20:43 +0200 Subject: [PATCH 03/34] WMSLayer example, test and docs (#23) --- package-lock.json | 131 ++++++++++++++++++++ package.json | 2 + src/pages/WMSLayer/WMSLayer.test.tsx | 44 +++++++ src/pages/WMSLayer/WMSLayer.tsx | 73 +++++++++++ src/pages/WMSLayer/styles.module.css | 4 + src/stories/pages/WMSLayer/index.mdx | 65 ++++++++++ src/stories/pages/WMSLayer/index.stories.ts | 19 +++ 7 files changed, 338 insertions(+) create mode 100644 src/pages/WMSLayer/WMSLayer.test.tsx create mode 100644 src/pages/WMSLayer/WMSLayer.tsx create mode 100644 src/pages/WMSLayer/styles.module.css create mode 100644 src/stories/pages/WMSLayer/index.mdx create mode 100644 src/stories/pages/WMSLayer/index.stories.ts diff --git a/package-lock.json b/package-lock.json index 8568b3c..7e21f96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@types/proj4": "^2.5.5", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "@types/sinon": "^17.0.3", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "@vitejs/plugin-react-swc": "^3.6.0", @@ -57,6 +58,7 @@ "postcss-scss": "^4.0.9", "prettier": "^3.2.5", "rimraf": "^5.0.5", + "sinon": "^18.0.0", "storybook": "^8.1.0", "stylelint": "^16.4.0", "stylelint-config-recommended": "^14.0.0", @@ -3639,6 +3641,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@storybook/addon-a11y": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.1.1.tgz", @@ -6140,6 +6186,21 @@ "@types/send": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -12086,6 +12147,12 @@ "node": ">=4.0" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -12487,6 +12554,12 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -13104,6 +13177,25 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -15390,6 +15482,45 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 398369a..47a949c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/proj4": "^2.5.5", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "@types/sinon": "^17.0.3", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "@vitejs/plugin-react-swc": "^3.6.0", @@ -54,6 +55,7 @@ "postcss-scss": "^4.0.9", "prettier": "^3.2.5", "rimraf": "^5.0.5", + "sinon": "^18.0.0", "storybook": "^8.1.0", "stylelint": "^16.4.0", "stylelint-config-recommended": "^14.0.0", diff --git a/src/pages/WMSLayer/WMSLayer.test.tsx b/src/pages/WMSLayer/WMSLayer.test.tsx new file mode 100644 index 0000000..54aad56 --- /dev/null +++ b/src/pages/WMSLayer/WMSLayer.test.tsx @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import sinon from 'sinon'; +import { render } from '@testing-library/react'; +import L from 'leaflet'; +import WMSLayer from './WMSLayer'; + +describe('WMSLayer', () => { + let tileLayerWmsStub: sinon.SinonStub; + + beforeEach(() => { + // Create a stub for L.tileLayer.wms + tileLayerWmsStub = sinon + .stub(L.tileLayer, 'wms') + // @ts-expect-error ts(2345) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .callsFake((url, options) => { + return { + addTo: sinon.stub().returnsThis(), + remove: sinon.stub().returnsThis(), + setUrl: sinon.stub().returnsThis(), + setParams: sinon.stub().returnsThis(), + }; + }); + }); + + afterEach(() => { + // Restore the original method + tileLayerWmsStub.restore(); + }); + + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('renders a leaflet WMSLayer icon', () => { + render(); + expect( + tileLayerWmsStub.calledWith( + 'https://map.data.amsterdam.nl/maps/adresseerbare_objecten?REQUEST=GetCapabilities&VERSION=1.1.0&SERVICE=wms' + ) + ).toBe(true); + }); +}); diff --git a/src/pages/WMSLayer/WMSLayer.tsx b/src/pages/WMSLayer/WMSLayer.tsx new file mode 100644 index 0000000..774bd7c --- /dev/null +++ b/src/pages/WMSLayer/WMSLayer.tsx @@ -0,0 +1,73 @@ +import { useEffect, useRef, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; + +const WMSLayer: FunctionComponent = () => { + const containerRef = useRef(null); + const [mapInstance, setMapInstance] = useState(null); + const createdMapInstance = useRef(false); + + // Set the Leaflet map and Amsterdam base layer + useEffect(() => { + if (containerRef.current === null || createdMapInstance.current !== false) { + return; + } + + const map = new L.Map(containerRef.current, { + center: L.latLng([52.370216, 4.895168]), + zoom: 12, + layers: [ + L.tileLayer('https://{s}.data.amsterdam.nl/topo_rd/{z}/{x}/{y}.png', { + attribution: '', + subdomains: ['t1', 't2', 't3', 't4'], + tms: true, + }), + ], + zoomControl: false, + maxZoom: 16, + minZoom: 3, + crs: getCrsRd(), + maxBounds: [ + [52.25168, 4.64034], + [52.50536, 5.10737], + ], + }); + + map.attributionControl.setPrefix(false); + + createdMapInstance.current = true; + setMapInstance(map); + + return () => { + if (mapInstance) mapInstance.remove(); + }; + }, []); + + // Create the WMS layer and add it to the map + useEffect(() => { + if (mapInstance === null) { + return; + } + + L.tileLayer + .wms( + 'https://map.data.amsterdam.nl/maps/adresseerbare_objecten?REQUEST=GetCapabilities&VERSION=1.1.0&SERVICE=wms', + { + layers: 'verblijfsobjecten_woonfunctie', + format: 'image/svg+xml', + // Ensure transparent is true otherwise the Amsterdam base layer won't be visible through the WMS layer + transparent: true, + // Default for Amsterdam is Rijksdriehoek whereas Leaflet works in WGS84 so be sure to handle the CRS properly + crs: getCrsRd(), + } + ) + .addTo(mapInstance); + }, [mapInstance]); + + return
; +}; + +export default WMSLayer; diff --git a/src/pages/WMSLayer/styles.module.css b/src/pages/WMSLayer/styles.module.css new file mode 100644 index 0000000..d171d7c --- /dev/null +++ b/src/pages/WMSLayer/styles.module.css @@ -0,0 +1,4 @@ +.container { + height: 100%; + min-height: 100%; +} diff --git a/src/stories/pages/WMSLayer/index.mdx b/src/stories/pages/WMSLayer/index.mdx new file mode 100644 index 0000000..612b326 --- /dev/null +++ b/src/stories/pages/WMSLayer/index.mdx @@ -0,0 +1,65 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as WMSLayerStories from './index.stories'; +import WMSLayer from '@/pages/WMSLayer/WMSLayer?raw'; +import styles from '@/pages/WMSLayer/styles.module.css?raw'; + + + +# Amsterdam WMSLayer + +## Requirements + +- This example is built upon the [BaseMap component example](../?path=/docs/react-baselayer--docs). +- [See global requirements list](../?path=/docs/global-requirements--docs) + +## Description + +The Web Map Service is a standard protocol developed by the Open Geospatial Consortium (OGC) for serving georeferenced map images generated by a map server from spatial data. These images are generated by a map server using data from a GIS database. + +Leaflet's `TileLayer.WMS` allows you to display raster maps from WMS servers. WMS servers generate map images in various formats (like PNG, JPEG) based on parameters sent in the request URL. These parameters can include layers, styles, geographic bounding boxes, coordinate reference systems (CRS), and output image format. + +The primary code in regards to creating a Leaflet WMS layer, is lines 50-68: + +```js +useEffect(() => { + if (mapInstance === null) { + return; + } + + L.tileLayer + .wms( + 'https://map.data.amsterdam.nl/maps/adresseerbare_objecten?REQUEST=GetCapabilities&VERSION=1.1.0&SERVICE=wms', + { + layers: 'verblijfsobjecten_woonfunctie', + format: 'image/svg+xml', + transparent: true, + crs: getCrsRd(), + } + ) + .addTo(mapInstance); +}, [mapInstance]); +``` + +## Usage Scenarios + +- **Environmental Monitoring**: Displaying real-time data on air quality, water quality, or weather patterns. +- **Urban Planning**: Showing zoning maps, land use plans, or transportation networks. +- **Historical Maps**: Overlaying historical maps on current maps to show changes over time. +- **Disaster Management**: Displaying flood zones, earthquake impacts, or wildfire extents. +- **Biodiversity and Conservation**: Showing protected areas, wildlife habitats, or vegetation types. + +## Usage + +The following files are required: + +### React Components + +#### 1. WMSLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + diff --git a/src/stories/pages/WMSLayer/index.stories.ts b/src/stories/pages/WMSLayer/index.stories.ts new file mode 100644 index 0000000..11add93 --- /dev/null +++ b/src/stories/pages/WMSLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import BaseLayer from '@/pages/WMSLayer/WMSLayer'; + +const meta = { + title: 'React/WMSLayer', + component: BaseLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; From 974953e17b15771efbde0793f5ac83643c4dd673 Mon Sep 17 00:00:00 2001 From: Siree Koolen-Wijkstra Date: Wed, 22 May 2024 11:13:13 +0200 Subject: [PATCH 04/34] PolygonLayer in React - Documentation is added. - Tests are added - PolygonLayer is added with data --- .../PolygonLayer/PolygonLayer.test.tsx | 17 + .../PolygonLayer/PolygonLayer.tsx | 40 ++ src/pages/ReactLeaflet/PolygonLayer/data.json | 647 ++++++++++++++++++ .../PolygonLayer/styles.module.css | 8 + .../pages/ReactLeaflet/PolygonLayer/index.mdx | 63 ++ .../PolygonLayer/index.stories.ts | 19 + 6 files changed, 794 insertions(+) create mode 100644 src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx create mode 100644 src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx create mode 100644 src/pages/ReactLeaflet/PolygonLayer/data.json create mode 100644 src/pages/ReactLeaflet/PolygonLayer/styles.module.css create mode 100644 src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx create mode 100644 src/stories/pages/ReactLeaflet/PolygonLayer/index.stories.ts diff --git a/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx new file mode 100644 index 0000000..6e24d8a --- /dev/null +++ b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import PolygonLayer from './PolygonLayer'; + +describe('PolygonLayer', () => { + it('renders the PolygonLayer', () => { + const { container } = render(); + const svgPolygon = container.querySelector('svg path'); + const strokeColor = svgPolygon && svgPolygon.getAttribute('stroke'); + const fillOpacity = svgPolygon && svgPolygon.getAttribute('fill-opacity'); + + expect(container).toBeDefined(); + expect(svgPolygon).toBeInTheDocument(); + expect(strokeColor).toEqual('#3388ff'); + expect(fillOpacity && parseFloat(fillOpacity)).toBeCloseTo(0.2); + }); +}); diff --git a/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx new file mode 100644 index 0000000..4147911 --- /dev/null +++ b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx @@ -0,0 +1,40 @@ +import styles from '@/pages/ReactLeaflet/PolygonLayer/styles.module.css'; +import { MapContainer, TileLayer, Polygon } from 'react-leaflet'; +import L from 'leaflet'; +import getCrsRd from '@/utils/getCrsRd'; +import type { MultiPolygon } from 'geojson'; +import type { LatLngExpression, Polygon as PolygonType } from 'leaflet'; + +import data from './data.json'; + +const PolygonLayer = (): JSX.Element => { + const parsedGeoJson = L.geoJSON(data.geometry as MultiPolygon); + return ( +
+ + + (layer as PolygonType).getLatLngs() as LatLngExpression[] + )} + /> + +
+ ); +}; + +export default PolygonLayer; diff --git a/src/pages/ReactLeaflet/PolygonLayer/data.json b/src/pages/ReactLeaflet/PolygonLayer/data.json new file mode 100644 index 0000000..8c3d514 --- /dev/null +++ b/src/pages/ReactLeaflet/PolygonLayer/data.json @@ -0,0 +1,647 @@ +{ + "id": 28, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [4.870868238740244, 52.36021543554047], + [4.870965249389386, 52.36021427287773], + [4.87100579371883, 52.360224250226004], + [4.871016055154826, 52.36022678457574], + [4.871041848153867, 52.36023312554778], + [4.871062546876857, 52.360238221969915], + [4.871241194471373, 52.36028219583987], + [4.871250551275278, 52.360288042703985], + [4.871263303875251, 52.36029601648722], + [4.871271588709332, 52.360300627356686], + [4.871279611088381, 52.36030508429007], + [4.871284818274916, 52.36030798306195], + [4.871289996207376, 52.36031087271836], + [4.871297097009476, 52.360317572570295], + [4.871303540881365, 52.360329006100194], + [4.871304713732071, 52.36033292986794], + [4.871304869042979, 52.36033346082168], + [4.871306167009657, 52.36033798731353], + [4.871307511992059, 52.36034352063762], + [4.871308540134098, 52.36034978957594], + [4.871309150143921, 52.36035414230255], + [4.871309063523301, 52.36035906720889], + [4.871308978261051, 52.3603638752805], + [4.87130811231167, 52.360370171915484], + [4.871307974662543, 52.36037100741604], + [4.871306801321267, 52.36037812934366], + [4.871304708496526, 52.36038514863919], + [4.871295451509399, 52.360413752385746], + [4.871285811198658, 52.360443540547685], + [4.871267163355848, 52.36050067525568], + [4.871261958847114, 52.36051660142254], + [4.871256756323818, 52.360532536963184], + [4.871255733730962, 52.36053566920535], + [4.871385166089871, 52.36056748308236], + [4.871535767884912, 52.36035442862256], + [4.871840546556333, 52.36034943746215], + [4.871859898628045, 52.36035167876694], + [4.871885549378248, 52.36035511589381], + [4.871911003661145, 52.36035903749812], + [4.871936246797878, 52.36036344351606], + [4.871961264109563, 52.36036833388391], + [4.871991843204704, 52.36037466851577], + [4.87202457969532, 52.36038105746711], + [4.872114165785238, 52.36039695115793], + [4.872175645513233, 52.36040722200569], + [4.872449757475709, 52.360474330347], + [4.872509547858224, 52.36049230515856], + [4.872733938410635, 52.36055685983708], + [4.872925594153183, 52.36061744310562], + [4.872984555670326, 52.36063603422896], + [4.873011396232058, 52.36064450042724], + [4.873027458967179, 52.36065063692111], + [4.873285193367326, 52.36074916522985], + [4.873310926906261, 52.360749285928975], + [4.873318663146951, 52.360749319511484], + [4.873343295807144, 52.36074942643704], + [4.873399965745847, 52.360744082034955], + [4.873464439473817, 52.36073800751383], + [4.873467657985415, 52.36073770691011], + [4.873488688980308, 52.36073570403145], + [4.873494126737997, 52.36073518836381], + [4.87350338273293, 52.360735872846476], + [4.873515043233613, 52.36073673514174], + [4.87355158267884, 52.36074183694806], + [4.873558661876566, 52.360742829352496], + [4.873580336026829, 52.360747408275344], + [4.873582329355056, 52.36074768655603], + [4.873594699526397, 52.360749429921114], + [4.873602320941713, 52.36075050556504], + [4.873630254119269, 52.36075717881411], + [4.873654701007094, 52.36076318981541], + [4.873669212900652, 52.360767611823], + [4.873680023813862, 52.36077089430678], + [4.873702298538768, 52.36077812719631], + [4.873722415519887, 52.360782798203694], + [4.873748763158841, 52.360793284337724], + [4.873783907991641, 52.36080600162832], + [4.873809703915876, 52.36083499111634], + [4.873823005716479, 52.36085137952911], + [4.873823418134163, 52.360853043472254], + [4.873829013382997, 52.360875618088045], + [4.873830554401974, 52.36088183548597], + [4.873849585829229, 52.360958619571676], + [4.873850482346277, 52.36096223664752], + [4.873851220195173, 52.36096951991783], + [4.873851746561815, 52.360974780029174], + [4.873850246154462, 52.36097884496974], + [4.873843657802452, 52.36098491008862], + [4.873840481324743, 52.36098825007799], + [4.873837460605654, 52.36099142629045], + [4.873829393762087, 52.360998392758894], + [4.873828153791216, 52.36099898057265], + [4.8738198578542, 52.361002908190336], + [4.873812147654297, 52.36100696417595], + [4.873803341662809, 52.361010566021854], + [4.873792285976887, 52.361011938138276], + [4.873796934924837, 52.36101248356315], + [4.873805172729064, 52.361013450040105], + [4.873813382637197, 52.36101442036945], + [4.87382053705137, 52.36101526029261], + [4.873827384852428, 52.36101595508178], + [4.87383590480633, 52.36101677396253], + [4.873851101290971, 52.36101786446389], + [4.873862229412977, 52.36102165161973], + [4.873874026845114, 52.3610234194349], + [4.873885509886563, 52.36102698343394], + [4.873893738137903, 52.361028906537875], + [4.873905011900939, 52.36103152591473], + [4.873912229946146, 52.36103321095593], + [4.873921883103724, 52.361036326617935], + [4.873931130424396, 52.36103899113253], + [4.873943004736983, 52.36104299722172], + [4.873948564102911, 52.361045119188525], + [4.87395412757132, 52.361047242720716], + [4.873964874136732, 52.361051019224846], + [4.873974164589507, 52.36105503208418], + [4.873982320304786, 52.361058150237064], + [4.873986220704934, 52.36105982089073], + [4.873990121105373, 52.36106149154423], + [4.874007222934505, 52.36107173980622], + [4.874003051502605, 52.361063039564606], + [4.874001998858686, 52.36105377762771], + [4.874000665854225, 52.36105098564919], + [4.874000586102135, 52.36104518821024], + [4.874001278585311, 52.36104116470411], + [4.874001971068359, 52.361037141198004], + [4.874005908748491, 52.36102797279653], + [4.874013871804571, 52.36101728494129], + [4.874014815142515, 52.36101639854807], + [4.874015196294959, 52.3610157358037], + [4.874023459667638, 52.36100827584885], + [4.874030095492257, 52.36100318160192], + [4.874035574541144, 52.36099909795499], + [4.874039138475856, 52.36099685748089], + [4.874041874101878, 52.36099515268063], + [4.874046190866146, 52.36099255596003], + [4.874049959382318, 52.3609903972621], + [4.874055098009594, 52.36098782207918], + [4.874060029767164, 52.360985362839976], + [4.874064691674346, 52.36098338776893], + [4.874071242060121, 52.36098060299887], + [4.874078229930494, 52.3609780717815], + [4.874084213646195, 52.36097604851256], + [4.874089915222385, 52.36097430264044], + [4.874098987629212, 52.36097178045696], + [4.874110600999727, 52.360969152445286], + [4.874122825726281, 52.36096697646917], + [4.874152835096323, 52.36096297216784], + [4.874159050656617, 52.36097009942066], + [4.874190804157952, 52.3609587237327], + [4.874207454900479, 52.360961006872266], + [4.874222899611479, 52.36096348251379], + [4.874238344324212, 52.360965958153294], + [4.874244487210065, 52.360967944098604], + [4.874250644776316, 52.36096993010723], + [4.874258986731547, 52.36097217723712], + [4.87426732858373, 52.36097443335373], + [4.874279857477133, 52.360978963534365], + [4.874296369631498, 52.360984346831664], + [4.87431874953718, 52.36099265858888], + [4.874327895204008, 52.36098506761928], + [4.874339975449332, 52.36098014973373], + [4.874351689385386, 52.360977710876654], + [4.87437977311866, 52.36097734685954], + [4.874381392956569, 52.36097730028913], + [4.874398982638044, 52.360977097864804], + [4.87441658360677, 52.36097845935371], + [4.874434107684498, 52.3609802291694], + [4.874436760544916, 52.3609804970905], + [4.874443160440356, 52.36098161974591], + [4.874461965053916, 52.36098449736919], + [4.874466643433172, 52.36098489569983], + [4.874476000456304, 52.360985691196774], + [4.874493246518126, 52.360987275830624], + [4.874503336304008, 52.36098829411652], + [4.874503397979635, 52.36098830034098], + [4.87453547862922, 52.36098888777971], + [4.874606434364365, 52.36098850298392], + [4.874620425679004, 52.36098717046575], + [4.874618601060795, 52.360982452989006], + [4.874627082819177, 52.360980224803164], + [4.874631114814962, 52.36098448447474], + [4.874645510352803, 52.36097865983327], + [4.874661357407467, 52.360975537796826], + [4.874681654445398, 52.360969720726104], + [4.87469084785382, 52.36097197151178], + [4.874697497349858, 52.36097331250943], + [4.874697991192651, 52.36097344133647], + [4.874714808345848, 52.36097690165837], + [4.874742113685081, 52.360979491491385], + [4.874772386135716, 52.36097939783926], + [4.874799722362178, 52.36097804217437], + [4.874829746543374, 52.360973808278956], + [4.874829834240688, 52.36097379007354], + [4.874836297572837, 52.36097244832579], + [4.874846984097826, 52.36096998699755], + [4.87484961459284, 52.36096975571326], + [4.874842210334912, 52.36095876761148], + [4.874931978937924, 52.36092979314786], + [4.87493827589989, 52.36093878117406], + [4.874961913508191, 52.360932259491484], + [4.874980881885884, 52.36092326394791], + [4.8750013184987, 52.36091043698852], + [4.875012691059704, 52.36089941329645], + [4.875025858389413, 52.36088297776055], + [4.87503786864638, 52.36086249273374], + [4.875043728068513, 52.36085978581033], + [4.875052172224788, 52.36085572393227], + [4.875058795446706, 52.360854023140796], + [4.875065423992472, 52.36085232098172], + [4.87507681552166, 52.36084982673181], + [4.875072724879711, 52.36084555783391], + [4.875085564773717, 52.3608396814705], + [4.875087791856292, 52.360842603132895], + [4.875096516052046, 52.3608333565365], + [4.875144653053066, 52.36075198303553], + [4.875155520345176, 52.36073766763082], + [4.875158776644303, 52.36073281934756], + [4.875170049369759, 52.360721534563936], + [4.875175518577284, 52.36071701941024], + [4.875189625405595, 52.36070432697651], + [4.875189740697685, 52.36070421782887], + [4.875190769084626, 52.360703244249564], + [4.875205278934592, 52.36069640441985], + [4.875216049908607, 52.36069202381027], + [4.875216134777059, 52.36069198446311], + [4.875239735153022, 52.36068104059553], + [4.875258032230589, 52.360678045915556], + [4.875281825236022, 52.36067458067333], + [4.875288260583581, 52.36067401012723], + [4.875311113398473, 52.360671984035626], + [4.875339310713427, 52.36067232166536], + [4.875363727246217, 52.36067334398556], + [4.875369313496562, 52.36067453971072], + [4.875370817525256, 52.36067465090035], + [4.875382004140518, 52.36067718887026], + [4.875387597500463, 52.36067845336115], + [4.875393190860731, 52.360679717851816], + [4.875400751250775, 52.36068249121093], + [4.875432083354253, 52.36069210932042], + [4.875446763889228, 52.36069719693152], + [4.875463662030847, 52.36070475676808], + [4.875483037373985, 52.36071648607218], + [4.875486456213255, 52.36071788752473], + [4.875495662311543, 52.36072412885987], + [4.875503370010693, 52.36072791006424], + [4.875514069333854, 52.36073706089264], + [4.875522190757853, 52.36074442999268], + [4.875526620623274, 52.3607485655251], + [4.875531050489522, 52.360752701057365], + [4.875539880005143, 52.36075850036202], + [4.875544062804551, 52.360762419120114], + [4.87559589686002, 52.36081741443343], + [4.875669438249383, 52.360800287026635], + [4.875673203163666, 52.3607995968002], + [4.875710092197398, 52.36079109455642], + [4.875711340554844, 52.36079080235995], + [4.87574687770298, 52.36078248435938], + [4.875820830160744, 52.36076789317758], + [4.875864194451652, 52.360714558496824], + [4.876452145080829, 52.360888685436564], + [4.876668188469124, 52.360597008612096], + [4.876845889012206, 52.3606396846658], + [4.876962277821802, 52.36065865858288], + [4.876733728937213, 52.36097126824653], + [4.877207130474766, 52.36110099208028], + [4.877440757474827, 52.36078143185254], + [4.881254233257216, 52.36182632110397], + [4.88208506790752, 52.361546413794], + [4.882106740130919, 52.36152934867539], + [4.882111028877661, 52.36149564498983], + [4.882067628717989, 52.36145729788694], + [4.880346696694701, 52.36099953082082], + [4.880345540189635, 52.36096711608749], + [4.880299046646569, 52.36094785402403], + [4.878790383425321, 52.360546795746714], + [4.878594757508464, 52.36050019885188], + [4.877980121158842, 52.360337422004505], + [4.877688222570613, 52.360266591930824], + [4.877301043222332, 52.36018226461887], + [4.877098739433883, 52.360454498748446], + [4.876823246753967, 52.360382303988416], + [4.877025984528478, 52.36009232559679], + [4.876697630434126, 52.359999181168554], + [4.875755054038905, 52.35969103934038], + [4.875816229461869, 52.359617311743825], + [4.87586533607182, 52.3595479545352], + [4.875875558392619, 52.359533510448216], + [4.875881795966668, 52.3595247204147], + [4.875886899782848, 52.35951749833888], + [4.875970095055625, 52.35940000138013], + [4.875875392798265, 52.359374624410954], + [4.875745493905367, 52.35933981096497], + [4.875690832851949, 52.359325158449], + [4.875684832570106, 52.35932355067902], + [4.875649289817392, 52.35931611702085], + [4.875648573606591, 52.35931712055314], + [4.875454801300457, 52.359265843825604], + [4.875444659027987, 52.359263157583875], + [4.875246935612433, 52.35921083883027], + [4.875236890972208, 52.35922513982885], + [4.875074478286337, 52.35918129614326], + [4.875101897703445, 52.35914337862846], + [4.875153902024207, 52.359071458962894], + [4.875160928356276, 52.359061746640485], + [4.875180366695218, 52.35903484951043], + [4.875191778804139, 52.35901908044073], + [4.875278303087995, 52.35889940534282], + [4.875297524699081, 52.358891282653985], + [4.87532091018964, 52.35888106584726], + [4.875322826731707, 52.35888163137474], + [4.875327132767607, 52.35887612253157], + [4.875318135047767, 52.35887348616638], + [4.875317673255046, 52.358874068372884], + [4.875314409341233, 52.358873227386695], + [4.875379007602379, 52.358778461304944], + [4.875285283979371, 52.35875365431454], + [4.875099739730671, 52.358704533680225], + [4.874996035260677, 52.35867708582352], + [4.87501363906076, 52.35865268836199], + [4.875026573706918, 52.3586347328849], + [4.875075581111547, 52.35856678185244], + [4.875144817638096, 52.3584707956497], + [4.87504851533494, 52.35844562679134], + [4.874864165523122, 52.35839744567318], + [4.874628167213479, 52.35833577472619], + [4.874618185841979, 52.35833316101102], + [4.873869464852632, 52.358137479970495], + [4.873859963192123, 52.358135281707575], + [4.873838888257563, 52.35812977070268], + [4.873736240594897, 52.35810124778582], + [4.873721125131302, 52.35809704785705], + [4.873684894898821, 52.35808697721121], + [4.873672449443204, 52.35808292367483], + [4.873636838749975, 52.35807132778478], + [4.873632087249923, 52.35806965342537], + [4.873627423407493, 52.35806801539699], + [4.873606151338839, 52.358060517203064], + [4.873563756070518, 52.35804280714995], + [4.873555089674388, 52.358039471042765], + [4.873495696485481, 52.358016600143344], + [4.873487258822958, 52.35801379530081], + [4.873478835736326, 52.358010999508714], + [4.873455116615721, 52.358003122170174], + [4.873391831658801, 52.357981672367565], + [4.873339514593451, 52.35796637282255], + [4.873321051390579, 52.3579609809151], + [4.873312370561844, 52.35795889402421], + [4.873246188495978, 52.35794297700645], + [4.873178110929426, 52.357927222487916], + [4.873137488966892, 52.35791866052014], + [4.873129962056815, 52.35791707295292], + [4.873122420468444, 52.35791548532154], + [4.873071946844142, 52.35790706929005], + [4.87299833629466, 52.35789502055194], + [4.872921187876169, 52.357882983361065], + [4.872857035853266, 52.357874319068785], + [4.872804953781794, 52.35786790020211], + [4.872750189497988, 52.357857326304014], + [4.872732203298425, 52.357853850767505], + [4.872663279862207, 52.35784017743286], + [4.872603160433002, 52.357825562666065], + [4.872571890426909, 52.35781692431669], + [4.872526646063839, 52.35780444136008], + [4.872477531464466, 52.35778898458838], + [4.872464991374538, 52.35778424744384], + [4.872452466070336, 52.35777950137451], + [4.872444909871898, 52.35777664636371], + [4.872407885019053, 52.35776172747282], + [4.872371928011331, 52.35774586950088], + [4.872335056405482, 52.35772786845581], + [4.872326626195222, 52.357723167147505], + [4.872300197931232, 52.35770842912819], + [4.872260842834286, 52.35768180699408], + [4.872226506301883, 52.35765281593809], + [4.872223355640739, 52.357649818299066], + [4.872220204980019, 52.35764682065994], + [4.87220855698935, 52.35764870235181], + [4.87209380891253, 52.35766754471933], + [4.872079697666194, 52.35767044028309], + [4.872066087654797, 52.35767441655623], + [4.872053276417229, 52.3576791332993], + [4.872041803415388, 52.3576849074315], + [4.872031625969075, 52.35769162192669], + [4.872022838515464, 52.35769872894353], + [4.872005398425424, 52.35771650271049], + [4.871999324683982, 52.35771493038334], + [4.871958842801662, 52.35770445422855], + [4.871952755636998, 52.3577027739876], + [4.871916569017863, 52.35769280185241], + [4.871916232449995, 52.35769271050986], + [4.871864160765294, 52.35767784278921], + [4.871861189954072, 52.357677056909644], + [4.871860268010588, 52.357676810226785], + [4.871835843068565, 52.35767034056552], + [4.871832696635216, 52.357669508981864], + [4.871780607468711, 52.3576561510899], + [4.871776524520484, 52.35765506377007], + [4.871747563493892, 52.35764734301798], + [4.871704099798233, 52.3576357752616], + [4.871686596750248, 52.35763116022543], + [4.871697095482098, 52.35760942863036], + [4.871698567824744, 52.35760652301149], + [4.87169980572514, 52.35760358042064], + [4.871700823862057, 52.35760060092177], + [4.871701023664244, 52.35759982884547], + [4.871701607452404, 52.35759759343822], + [4.871702171070551, 52.35759456702127], + [4.871702499933412, 52.35759153059422], + [4.871702593936635, 52.35758849314446], + [4.87170243840154, 52.35758545460796], + [4.871702062685626, 52.35758241511264], + [4.871701466475731, 52.35757940162037], + [4.871700620623258, 52.35757639602865], + [4.87169955417249, 52.357573425427304], + [4.871698267123468, 52.35757048981627], + [4.871696744797514, 52.35756758913159], + [4.871695001664579, 52.35756474141184], + [4.871693052403415, 52.35756194672091], + [4.871690882335337, 52.35755920499479], + [4.871688505930284, 52.3575565342721], + [4.871633799380239, 52.357503142469184], + [4.871619281788512, 52.357486685584234], + [4.871575464610665, 52.35749039541387], + [4.871566795201396, 52.357491130597175], + [4.871548920836156, 52.35749068423848], + [4.871531031897031, 52.35749022882587], + [4.871498645737637, 52.35748925187886], + [4.871460401890958, 52.35748706301926], + [4.871422143473864, 52.357484865096005], + [4.871377447435672, 52.357480832570786], + [4.871364711296081, 52.35747908737497], + [4.871352006081983, 52.35747720749632], + [4.871339375829547, 52.35747519312679], + [4.871326790972548, 52.357473062112916], + [4.871314266398726, 52.357470796544206], + [4.871301802003702, 52.35746840540795], + [4.871289412466258, 52.35746588876811], + [4.871280568170651, 52.35746398975772], + [4.871277083107798, 52.357463246560826], + [4.871264828607101, 52.357460478849944], + [4.871252663642938, 52.357457585699585], + [4.871240558858034, 52.35745456698179], + [4.871228558288515, 52.35745142288849], + [4.871216632472604, 52.35744816227907], + [4.871204796089054, 52.35744478521752], + [4.871123121183267, 52.35742068353387], + [4.871075828803378, 52.35740513525549], + [4.871029953700519, 52.35739015936632], + [4.87098320000048, 52.357374999872775], + [4.870909744324451, 52.357347239918326], + [4.87081190904815, 52.357310412781736], + [4.870612622532607, 52.35723847711935], + [4.870606877694462, 52.357236402841586], + [4.87058822186973, 52.35722995809761], + [4.870548642646542, 52.3572189281559], + [4.870475508056328, 52.35719767646215], + [4.870336712275719, 52.35716135319136], + [4.870248526219513, 52.35725018950682], + [4.869959843386207, 52.35714805892244], + [4.870020760825543, 52.357083972791735], + [4.869846536088861, 52.357049642219884], + [4.869740915791143, 52.35702848176761], + [4.869733567078332, 52.357026741970486], + [4.869537068646451, 52.356980314922325], + [4.869353120135428, 52.3569289632344], + [4.869314448828934, 52.35691817053085], + [4.869267250621581, 52.35690219951223], + [4.869253108233303, 52.35689770667097], + [4.869229445083364, 52.3568901792468], + [4.869173905914015, 52.356870711408106], + [4.869163146300764, 52.35686691642884], + [4.869131349763381, 52.35685571335098], + [4.869110400600512, 52.35684833259696], + [4.869098573771632, 52.356844164443004], + [4.869090183584429, 52.35684110783554], + [4.869043453060314, 52.35682406923144], + [4.868992609183292, 52.356803534341495], + [4.868981453508246, 52.356798461350955], + [4.868953943924924, 52.356785955792894], + [4.868879010477358, 52.356751689953114], + [4.868873783308043, 52.35674929429822], + [4.868865327950189, 52.35674553634459], + [4.868821590792892, 52.35672610204171], + [4.868748910024494, 52.35669378733792], + [4.868736891423127, 52.356688449899735], + [4.868712342877139, 52.356678824346915], + [4.868711086203541, 52.3566783335039], + [4.868561632100086, 52.356619770691346], + [4.868386387893525, 52.356551100274785], + [4.868362228015656, 52.35654716559971], + [4.86833928819437, 52.35654057589268], + [4.868303244657549, 52.356531070635214], + [4.868245362255234, 52.35651372218235], + [4.868231421913024, 52.35650954467511], + [4.86819845576642, 52.356499234991816], + [4.86812391488411, 52.356481696598586], + [4.867917624729569, 52.35643317469779], + [4.867895866465756, 52.356428486503546], + [4.86787410810138, 52.35642380729257], + [4.867867310186851, 52.35642268096252], + [4.86786048165387, 52.35642166235079], + [4.867853593040549, 52.356420760315864], + [4.867848547060446, 52.35642045056895], + [4.867843484719184, 52.35642028455408], + [4.867838420905404, 52.35642024436111], + [4.867833355408695, 52.35642034796457], + [4.867828302907342, 52.35642059542891], + [4.867823263611666, 52.35642096877952], + [4.867818266667913, 52.35642148611981], + [4.867813312181225, 52.35642213846244], + [4.867810856271602, 52.356422523147685], + [4.867808414935078, 52.35642291688459], + [4.86780356014591, 52.356423830309126], + [4.867798791848679, 52.3564248789293], + [4.867794095470206, 52.35642605369341], + [4.86778950036712, 52.35642735473028], + [4.86778499196625, 52.356428772988174], + [4.867780599519048, 52.35643031758328], + [4.867776323130679, 52.35643197952828], + [4.867772162801122, 52.35643375882319], + [4.867768133313895, 52.35643564654517], + [4.867764249347315, 52.35643764275861], + [4.867762387568757, 52.35643868615599], + [4.867760511006577, 52.35643973847624], + [4.867756932970011, 52.35644193376247], + [4.867753515237617, 52.356444228617356], + [4.867750272698138, 52.3564466051307], + [4.867747190462842, 52.35644908121273], + [4.867744298204019, 52.35645163003035], + [4.867741595921693, 52.35645425158365], + [4.867739083615877, 52.35645694587257], + [4.867736776070147, 52.35645970397428], + [4.867734658606186, 52.3564625258244], + [4.867732760685911, 52.35646540256443], + [4.867731053057873, 52.356468325078296], + [4.867565283773862, 52.35643181724912], + [4.867560700189634, 52.35643088037703], + [4.867561067628969, 52.356427080170235], + [4.867561346787777, 52.35642329755141], + [4.86756140587682, 52.35641950497878], + [4.867561230007355, 52.35641572036246], + [4.867560833857765, 52.35641194376684], + [4.867560202749782, 52.356408175127555], + [4.867559364145811, 52.356404576344744], + [4.867558235237913, 52.35640069766712], + [4.86755691340723, 52.35639699789764], + [4.867555370980985, 52.356393333110766], + [4.867553593280911, 52.356389703242066], + [4.867551609558457, 52.356386117407645], + [4.867549390351794, 52.356382584465976], + [4.86754708076027, 52.35637924885803], + [4.867544333661159, 52.35637566866006], + [4.867541495861541, 52.35637231275766], + [4.867538538846711, 52.35636910912363], + [4.867535201564358, 52.356365786979346], + [4.867531759429436, 52.356362644129725], + [4.867528183716776, 52.3563596265221], + [4.867524314543667, 52.35635659863829], + [4.867520326365917, 52.35635370504816], + [4.86751614621297, 52.356350909480426], + [4.867511876411985, 52.35634824833523], + [4.867507312729654, 52.35634561286292], + [4.867502659294031, 52.35634312080043], + [4.867497857812878, 52.35634073594081], + [4.867492908286182, 52.356338458284085], + [4.867488555518242, 52.35633661465941], + [4.86748260871, 52.35633426072164], + [4.86747734641451, 52.35633236816442], + [4.867471833641051, 52.356330555397015], + [4.867397166068642, 52.35631260255071], + [4.86718842359381, 52.35626429329727], + [4.867137559060911, 52.356123181933725], + [4.867233383961543, 52.355992566291675], + [4.867041280960356, 52.35587857544605], + [4.867388196176178, 52.35522039364998], + [4.867242354728078, 52.355179321690045], + [4.867085830132322, 52.35495313122182], + [4.866853667380711, 52.354738319100704], + [4.86680401703846, 52.35480996114231], + [4.86670351536868, 52.355202411611934], + [4.864662381046981, 52.35547982627098], + [4.864191949037235, 52.35572743726165], + [4.862902525002823, 52.35579149900464], + [4.862106041772348, 52.35573289010199], + [4.861621699923393, 52.35563527137289], + [4.861144572359165, 52.35543486973168], + [4.859972544183071, 52.354698948569855], + [4.858866441702216, 52.35447003118795], + [4.8578930287119, 52.35467498031823], + [4.85762302359612, 52.3548530315845], + [4.857622660895996, 52.35485636442177], + [4.857616102836207, 52.354906522747825], + [4.857568109539648, 52.354903584723225], + [4.857496455034481, 52.35489690075307], + [4.857423673083452, 52.35488752435231], + [4.857399075293866, 52.35488354053305], + [4.857336231696658, 52.35487162907259], + [4.857246903396953, 52.35484903838445], + [4.857168261660299, 52.35482763692723], + [4.857150645963791, 52.35482283947063], + [4.857128921165447, 52.35481673836021], + [4.857068679534972, 52.354797144883676], + [4.856978302511412, 52.354767754545605], + [4.856977514818597, 52.35476856890369], + [4.856954797221194, 52.354570376832896], + [4.856721497902141, 52.354955855859345], + [4.856627397380694, 52.355001005466896], + [4.856624966459253, 52.35507419091262], + [4.8565054413279, 52.355072702392306], + [4.856158818158288, 52.35500904239318], + [4.856468338185143, 52.35455068851282], + [4.85623548985104, 52.35452400216093], + [4.855857496929533, 52.35454916143023], + [4.855617518795705, 52.35462021075809], + [4.855649563330507, 52.35487009766624], + [4.855618611887341, 52.35499951728627], + [4.8555929926328, 52.3551065453607], + [4.854950328694452, 52.3563881704337], + [4.855110155604327, 52.356441952721504], + [4.861818778980194, 52.3581905749727], + [4.865165800724702, 52.358975686817665], + [4.865332084015194, 52.35887801311724], + [4.865553274601393, 52.358873541458586], + [4.865778185095015, 52.358853572914775], + [4.865861569263299, 52.35888085848488], + [4.86592015721833, 52.358958294305666], + [4.866075984879038, 52.35909952184341], + [4.866277230060384, 52.35921728429396], + [4.866335922680389, 52.3592433823367], + [4.868506665683955, 52.35975503222791], + [4.869971597385317, 52.360140760787026], + [4.870058521860415, 52.36000830166947], + [4.870868238740244, 52.36021543554047] + ] + ] + ] + }, + "name": "Vondelpark" +} diff --git a/src/pages/ReactLeaflet/PolygonLayer/styles.module.css b/src/pages/ReactLeaflet/PolygonLayer/styles.module.css new file mode 100644 index 0000000..7a173cb --- /dev/null +++ b/src/pages/ReactLeaflet/PolygonLayer/styles.module.css @@ -0,0 +1,8 @@ +.container { + height: 100%; + min-height: 100%; + + > div { + height: 100%; + } +} diff --git a/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx b/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx new file mode 100644 index 0000000..62e76aa --- /dev/null +++ b/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx @@ -0,0 +1,63 @@ +import { Meta, Source } from '@storybook/blocks'; +import * as PolygonLayerStories from './index.stories'; +import PolygonLayer from '@/pages/ReactLeaflet/PolygonLayer/PolygonLayer?raw'; +import styles from '@/pages/ReactLeaflet/PolygonLayer/styles.module.css?raw'; +import getCrsRd from '@/utils/getCrsRd?raw'; + + + + +# Amsterdam PolygonLayer +## Requirements + +- [See global requirements list](../?path=/docs/global-requirements--docs) +- CRS handling ([utils/getCrsRd.ts](#1-getcrsrdts)) + +## Description + +This is the Amsterdam polygon layer on a React-leaflet map. The polygon layer can be used to display various geometric shapes on the map, such as boundaries, regions, and other areas of interest. + +## Background + +### Amsterdam polygon layer +A PolygonLayer allows users to display and interact with polygon shapes on a map. +The polygons are defined by an array of latitude and longitude points, +and they can be styled with various options such as colour, opacity, and fill patterns. +The datateam Geo provides various reference maps based on reference data from team BenK (Basis- en Kernregistraties), +available in different reference systems and visualisations. + +### Coordinate Reference System handling + +Leaflet by default uses EPSG:3857 (Web Mercator / WGS 84), however, the base layer by default uses Rijksdriehoekscoördinaten. Therefore, we include the `utils/getCrsRd` file to appropriately handle coordinates. [Read more about CRS](#1-getcrsrdts). + +## How to implement + +To accomplish the Amstedam base/tile layer there are three files: +1. The React components + * [PolygonLayer.tsx](#1-polygonlayertsx) +2. The CSS styles (1 file) + * [styles.module.css](#1-stylesmodulecss) +3. Utils (1 file) + * [getCrsRd.ts](#1-getcrsrdts) + +## Usage + +The following files are required: + +### React Components + +#### 1. PolygonLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + +### Utils + +#### 1. getCrsRd.ts + + diff --git a/src/stories/pages/ReactLeaflet/PolygonLayer/index.stories.ts b/src/stories/pages/ReactLeaflet/PolygonLayer/index.stories.ts new file mode 100644 index 0000000..4fc66b3 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/PolygonLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import PolygonLayer from '@/pages/ReactLeaflet/PolygonLayer/PolygonLayer'; + +const meta = { + title: 'React-Leaflet/PolygonLayer', + component: PolygonLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; From 0a32724559c621765db78a56a3b943cf8c51a376 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Tue, 4 Jun 2024 14:02:50 +0200 Subject: [PATCH 05/34] Resolve random storybook Failed to fetch dynamically imported module error (#26) --- src/stories/pages/ZoomControl/index.stories.tsx | 2 +- vite.config.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stories/pages/ZoomControl/index.stories.tsx b/src/stories/pages/ZoomControl/index.stories.tsx index 05071c3..d4cd889 100644 --- a/src/stories/pages/ZoomControl/index.stories.tsx +++ b/src/stories/pages/ZoomControl/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import ZoomControl from '@/components/ZoomControl/ZoomControl'; import { Story } from '@storybook/blocks'; -import Map from '../../../components/Map/Map'; +import Map from '@/components/Map/Map'; const meta = { title: 'Components/ZoomControl', diff --git a/vite.config.ts b/vite.config.ts index 34ae010..897b271 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,9 @@ module.exports = defineConfig({ setupFiles: ['./test/vitest-setup.ts'], }, resolve: { + // Resolve random 'TypeError: Failed to fetch dynamically imported module' error + // @see https://github.com/storybookjs/storybook/issues/21610#issuecomment-1882417258 + extensions: ['.mdx', '.mjs', '.js', '.ts', '.tsx'], alias: [ { find: '@', replacement: path.resolve(__dirname, 'src') }, { find: '@@', replacement: path.resolve(__dirname) }, From 19236e7dd297870ad2df2bea4c419d58133b74e4 Mon Sep 17 00:00:00 2001 From: Siree Koolen-Wijkstra Date: Tue, 4 Jun 2024 15:22:24 +0200 Subject: [PATCH 06/34] pass data directly to Polygon component since it already has the right form and shape. - removed obsolete reference to base layer - removed info about CRS since it is already provided for in the baselayer - fixed typos - split tests into a 'renders the component' and a 'polygon is there' test --- .../ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx | 7 +++++-- src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx | 10 ++-------- src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx | 10 +++------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx index 6e24d8a..e70df80 100644 --- a/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx +++ b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.test.tsx @@ -3,13 +3,16 @@ import { render } from '@testing-library/react'; import PolygonLayer from './PolygonLayer'; describe('PolygonLayer', () => { - it('renders the PolygonLayer', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + it('renders the Polygo nLayer', () => { const { container } = render(); const svgPolygon = container.querySelector('svg path'); const strokeColor = svgPolygon && svgPolygon.getAttribute('stroke'); const fillOpacity = svgPolygon && svgPolygon.getAttribute('fill-opacity'); - expect(container).toBeDefined(); expect(svgPolygon).toBeInTheDocument(); expect(strokeColor).toEqual('#3388ff'); expect(fillOpacity && parseFloat(fillOpacity)).toBeCloseTo(0.2); diff --git a/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx index 4147911..2b10863 100644 --- a/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx +++ b/src/pages/ReactLeaflet/PolygonLayer/PolygonLayer.tsx @@ -2,13 +2,11 @@ import styles from '@/pages/ReactLeaflet/PolygonLayer/styles.module.css'; import { MapContainer, TileLayer, Polygon } from 'react-leaflet'; import L from 'leaflet'; import getCrsRd from '@/utils/getCrsRd'; -import type { MultiPolygon } from 'geojson'; -import type { LatLngExpression, Polygon as PolygonType } from 'leaflet'; +import type { LatLngExpression } from 'leaflet'; import data from './data.json'; const PolygonLayer = (): JSX.Element => { - const parsedGeoJson = L.geoJSON(data.geometry as MultiPolygon); return (
{ tms /> (layer as PolygonType).getLatLngs() as LatLngExpression[] - )} + positions={data.geometry.coordinates as LatLngExpression[][][]} />
diff --git a/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx b/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx index 62e76aa..85f8e7e 100644 --- a/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx +++ b/src/stories/pages/ReactLeaflet/PolygonLayer/index.mdx @@ -19,20 +19,16 @@ This is the Amsterdam polygon layer on a React-leaflet map. The polygon layer ca ## Background -### Amsterdam polygon layer -A PolygonLayer allows users to display and interact with polygon shapes on a map. +### Amsterdam Polygon layer +A Polygon layer allows users to display and interact with polygon shapes on a map. The polygons are defined by an array of latitude and longitude points, and they can be styled with various options such as colour, opacity, and fill patterns. The datateam Geo provides various reference maps based on reference data from team BenK (Basis- en Kernregistraties), available in different reference systems and visualisations. -### Coordinate Reference System handling - -Leaflet by default uses EPSG:3857 (Web Mercator / WGS 84), however, the base layer by default uses Rijksdriehoekscoördinaten. Therefore, we include the `utils/getCrsRd` file to appropriately handle coordinates. [Read more about CRS](#1-getcrsrdts). - ## How to implement -To accomplish the Amstedam base/tile layer there are three files: +To accomplish the Amstedam Polyygon layer there are three files: 1. The React components * [PolygonLayer.tsx](#1-polygonlayertsx) 2. The CSS styles (1 file) From 056bd68e130c6589248009bd12735de1bf611620 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Wed, 5 Jun 2024 13:17:07 +0200 Subject: [PATCH 07/34] [3] Polygon marker (#21) * Working polygonlayer and test * Polygon layer docs and styles separated --- src/pages/PolygonLayer/PolygonLayer.test.tsx | 36 + src/pages/PolygonLayer/PolygonLayer.tsx | 82 +++ src/pages/PolygonLayer/data.json | 645 ++++++++++++++++++ src/pages/PolygonLayer/layerStyles.ts | 10 + src/pages/PolygonLayer/styles.module.css | 4 + src/stories/pages/PolygonLayer/index.mdx | 96 +++ .../pages/PolygonLayer/index.stories.ts | 19 + src/utils/reverseLatLng.ts | 11 + 8 files changed, 903 insertions(+) create mode 100644 src/pages/PolygonLayer/PolygonLayer.test.tsx create mode 100644 src/pages/PolygonLayer/PolygonLayer.tsx create mode 100644 src/pages/PolygonLayer/data.json create mode 100644 src/pages/PolygonLayer/layerStyles.ts create mode 100644 src/pages/PolygonLayer/styles.module.css create mode 100644 src/stories/pages/PolygonLayer/index.mdx create mode 100644 src/stories/pages/PolygonLayer/index.stories.ts create mode 100644 src/utils/reverseLatLng.ts diff --git a/src/pages/PolygonLayer/PolygonLayer.test.tsx b/src/pages/PolygonLayer/PolygonLayer.test.tsx new file mode 100644 index 0000000..2c149ec --- /dev/null +++ b/src/pages/PolygonLayer/PolygonLayer.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import PolygonLayer from './PolygonLayer'; +import { polygonStyles, polygonHoverStyles } from './layerStyles'; + +describe('PolygonLayer', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('renders a polygon layer', () => { + const { container } = render(); + const layer = container.querySelector('.c-layer'); + + expect(layer).toBeInTheDocument(); + }); + + it('changes background on mouseover', async () => { + const { container } = render(); + const layer = container.querySelector('.c-layer'); + + // getAttribute will return as a string + expect(layer?.getAttribute('fill-opacity')).toEqual( + `${polygonStyles.fillOpacity}` + ); + + // @ts-expect-error Type 'null' is not assignable to type + fireEvent.mouseOver(container.querySelector('.c-layer')); + await waitFor(() => + expect(layer?.getAttribute('fill-opacity')).toEqual( + `${polygonHoverStyles.fillOpacity}` + ) + ); + }); +}); diff --git a/src/pages/PolygonLayer/PolygonLayer.tsx b/src/pages/PolygonLayer/PolygonLayer.tsx new file mode 100644 index 0000000..1a7c478 --- /dev/null +++ b/src/pages/PolygonLayer/PolygonLayer.tsx @@ -0,0 +1,82 @@ +import { useEffect, useRef, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import L, { LatLngTuple } from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; +import data from './data.json'; +import { polygonHoverStyles, polygonStyles } from './layerStyles'; + +const PolygonLayer: FunctionComponent = () => { + const containerRef = useRef(null); + const [mapInstance, setMapInstance] = useState(null); + const createdMapInstance = useRef(false); + + const polygonRef = useRef(null); + + // Set the Leaflet map and Amsterdam base layer + useEffect(() => { + if (containerRef.current === null || createdMapInstance.current !== false) { + return; + } + + const map = new L.Map(containerRef.current, { + center: [52.356423, 4.867811], + zoom: 10, + layers: [ + L.tileLayer('https://{s}.data.amsterdam.nl/topo_rd/{z}/{x}/{y}.png', { + attribution: '', + subdomains: ['t1', 't2', 't3', 't4'], + tms: true, + }), + ], + zoomControl: false, + maxZoom: 16, + minZoom: 3, + crs: getCrsRd(), + maxBounds: [ + [52.25168, 4.64034], + [52.50536, 5.10737], + ], + }); + + map.attributionControl.setPrefix(false); + + createdMapInstance.current = true; + setMapInstance(map); + + return () => { + if (mapInstance) mapInstance.remove(); + }; + }, []); + + // Create the polygon layer and add it to the map + useEffect(() => { + if (mapInstance) { + // TypeScript will often throw errors with Leaflet coordinate sets if you don't explicitly cast the type + polygonRef.current = L.polygon( + data.geometry.coordinates as LatLngTuple[][][], + { + className: 'c-layer', + } + ) + .addTo(mapInstance) + .on('mouseover', () => { + polygonRef.current?.setStyle(polygonHoverStyles); + }) + .on('mouseout', () => { + polygonRef.current?.setStyle(polygonStyles); + }); + } + + return () => { + if (polygonRef.current && mapInstance) { + mapInstance.removeLayer(polygonRef.current); + } + }; + }, [data, mapInstance]); + + return
; +}; + +export default PolygonLayer; diff --git a/src/pages/PolygonLayer/data.json b/src/pages/PolygonLayer/data.json new file mode 100644 index 0000000..952e077 --- /dev/null +++ b/src/pages/PolygonLayer/data.json @@ -0,0 +1,645 @@ +{ + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [52.360215, 4.870868], + [52.360214, 4.870965], + [52.360224, 4.871006], + [52.360227, 4.871016], + [52.360233, 4.871042], + [52.360238, 4.871063], + [52.360282, 4.871241], + [52.360288, 4.871251], + [52.360296, 4.871263], + [52.360301, 4.871272], + [52.360305, 4.87128], + [52.360308, 4.871285], + [52.360311, 4.87129], + [52.360318, 4.871297], + [52.360329, 4.871304], + [52.360333, 4.871305], + [52.360333, 4.871305], + [52.360338, 4.871306], + [52.360344, 4.871308], + [52.36035, 4.871309], + [52.360354, 4.871309], + [52.360359, 4.871309], + [52.360364, 4.871309], + [52.36037, 4.871308], + [52.360371, 4.871308], + [52.360378, 4.871307], + [52.360385, 4.871305], + [52.360414, 4.871295], + [52.360444, 4.871286], + [52.360501, 4.871267], + [52.360517, 4.871262], + [52.360533, 4.871257], + [52.360536, 4.871256], + [52.360567, 4.871385], + [52.360354, 4.871536], + [52.360349, 4.871841], + [52.360352, 4.87186], + [52.360355, 4.871886], + [52.360359, 4.871911], + [52.360363, 4.871936], + [52.360368, 4.871961], + [52.360375, 4.871992], + [52.360381, 4.872025], + [52.360397, 4.872114], + [52.360407, 4.872176], + [52.360474, 4.87245], + [52.360492, 4.87251], + [52.360557, 4.872734], + [52.360617, 4.872926], + [52.360636, 4.872985], + [52.360645, 4.873011], + [52.360651, 4.873027], + [52.360749, 4.873285], + [52.360749, 4.873311], + [52.360749, 4.873319], + [52.360749, 4.873343], + [52.360744, 4.8734], + [52.360738, 4.873464], + [52.360738, 4.873468], + [52.360736, 4.873489], + [52.360735, 4.873494], + [52.360736, 4.873503], + [52.360737, 4.873515], + [52.360742, 4.873552], + [52.360743, 4.873559], + [52.360747, 4.87358], + [52.360748, 4.873582], + [52.360749, 4.873595], + [52.360751, 4.873602], + [52.360757, 4.87363], + [52.360763, 4.873655], + [52.360768, 4.873669], + [52.360771, 4.87368], + [52.360778, 4.873702], + [52.360783, 4.873722], + [52.360793, 4.873749], + [52.360806, 4.873784], + [52.360835, 4.87381], + [52.360851, 4.873823], + [52.360853, 4.873823], + [52.360876, 4.873829], + [52.360882, 4.873831], + [52.360959, 4.87385], + [52.360962, 4.87385], + [52.36097, 4.873851], + [52.360975, 4.873852], + [52.360979, 4.87385], + [52.360985, 4.873844], + [52.360988, 4.87384], + [52.360991, 4.873837], + [52.360998, 4.873829], + [52.360999, 4.873828], + [52.361003, 4.87382], + [52.361007, 4.873812], + [52.361011, 4.873803], + [52.361012, 4.873792], + [52.361012, 4.873797], + [52.361013, 4.873805], + [52.361014, 4.873813], + [52.361015, 4.873821], + [52.361016, 4.873827], + [52.361017, 4.873836], + [52.361018, 4.873851], + [52.361022, 4.873862], + [52.361023, 4.873874], + [52.361027, 4.873886], + [52.361029, 4.873894], + [52.361032, 4.873905], + [52.361033, 4.873912], + [52.361036, 4.873922], + [52.361039, 4.873931], + [52.361043, 4.873943], + [52.361045, 4.873949], + [52.361047, 4.873954], + [52.361051, 4.873965], + [52.361055, 4.873974], + [52.361058, 4.873982], + [52.36106, 4.873986], + [52.361061, 4.87399], + [52.361072, 4.874007], + [52.361063, 4.874003], + [52.361054, 4.874002], + [52.361051, 4.874001], + [52.361045, 4.874001], + [52.361041, 4.874001], + [52.361037, 4.874002], + [52.361028, 4.874006], + [52.361017, 4.874014], + [52.361016, 4.874015], + [52.361016, 4.874015], + [52.361008, 4.874023], + [52.361003, 4.87403], + [52.360999, 4.874036], + [52.360997, 4.874039], + [52.360995, 4.874042], + [52.360993, 4.874046], + [52.36099, 4.87405], + [52.360988, 4.874055], + [52.360985, 4.87406], + [52.360983, 4.874065], + [52.360981, 4.874071], + [52.360978, 4.874078], + [52.360976, 4.874084], + [52.360974, 4.87409], + [52.360972, 4.874099], + [52.360969, 4.874111], + [52.360967, 4.874123], + [52.360963, 4.874153], + [52.36097, 4.874159], + [52.360959, 4.874191], + [52.360961, 4.874207], + [52.360963, 4.874223], + [52.360966, 4.874238], + [52.360968, 4.874244], + [52.36097, 4.874251], + [52.360972, 4.874259], + [52.360974, 4.874267], + [52.360979, 4.87428], + [52.360984, 4.874296], + [52.360993, 4.874319], + [52.360985, 4.874328], + [52.36098, 4.87434], + [52.360978, 4.874352], + [52.360977, 4.87438], + [52.360977, 4.874381], + [52.360977, 4.874399], + [52.360978, 4.874417], + [52.36098, 4.874434], + [52.36098, 4.874437], + [52.360982, 4.874443], + [52.360984, 4.874462], + [52.360985, 4.874467], + [52.360986, 4.874476], + [52.360987, 4.874493], + [52.360988, 4.874503], + [52.360988, 4.874503], + [52.360989, 4.874535], + [52.360989, 4.874606], + [52.360987, 4.87462], + [52.360982, 4.874619], + [52.36098, 4.874627], + [52.360984, 4.874631], + [52.360979, 4.874646], + [52.360976, 4.874661], + [52.36097, 4.874682], + [52.360972, 4.874691], + [52.360973, 4.874697], + [52.360973, 4.874698], + [52.360977, 4.874715], + [52.360979, 4.874742], + [52.360979, 4.874772], + [52.360978, 4.8748], + [52.360974, 4.87483], + [52.360974, 4.87483], + [52.360972, 4.874836], + [52.36097, 4.874847], + [52.36097, 4.87485], + [52.360959, 4.874842], + [52.36093, 4.874932], + [52.360939, 4.874938], + [52.360932, 4.874962], + [52.360923, 4.874981], + [52.36091, 4.875001], + [52.360899, 4.875013], + [52.360883, 4.875026], + [52.360862, 4.875038], + [52.36086, 4.875044], + [52.360856, 4.875052], + [52.360854, 4.875059], + [52.360852, 4.875065], + [52.36085, 4.875077], + [52.360846, 4.875073], + [52.36084, 4.875086], + [52.360843, 4.875088], + [52.360833, 4.875097], + [52.360752, 4.875145], + [52.360738, 4.875156], + [52.360733, 4.875159], + [52.360722, 4.87517], + [52.360717, 4.875176], + [52.360704, 4.87519], + [52.360704, 4.87519], + [52.360703, 4.875191], + [52.360696, 4.875205], + [52.360692, 4.875216], + [52.360692, 4.875216], + [52.360681, 4.87524], + [52.360678, 4.875258], + [52.360675, 4.875282], + [52.360674, 4.875288], + [52.360672, 4.875311], + [52.360672, 4.875339], + [52.360673, 4.875364], + [52.360675, 4.875369], + [52.360675, 4.875371], + [52.360677, 4.875382], + [52.360678, 4.875388], + [52.36068, 4.875393], + [52.360682, 4.875401], + [52.360692, 4.875432], + [52.360697, 4.875447], + [52.360705, 4.875464], + [52.360716, 4.875483], + [52.360718, 4.875486], + [52.360724, 4.875496], + [52.360728, 4.875503], + [52.360737, 4.875514], + [52.360744, 4.875522], + [52.360749, 4.875527], + [52.360753, 4.875531], + [52.360759, 4.87554], + [52.360762, 4.875544], + [52.360817, 4.875596], + [52.3608, 4.875669], + [52.3608, 4.875673], + [52.360791, 4.87571], + [52.360791, 4.875711], + [52.360782, 4.875747], + [52.360768, 4.875821], + [52.360715, 4.875864], + [52.360889, 4.876452], + [52.360597, 4.876668], + [52.36064, 4.876846], + [52.360659, 4.876962], + [52.360971, 4.876734], + [52.361101, 4.877207], + [52.360781, 4.877441], + [52.361826, 4.881254], + [52.361546, 4.882085], + [52.361529, 4.882107], + [52.361496, 4.882111], + [52.361457, 4.882068], + [52.361, 4.880347], + [52.360967, 4.880346], + [52.360948, 4.880299], + [52.360547, 4.87879], + [52.3605, 4.878595], + [52.360337, 4.87798], + [52.360267, 4.877688], + [52.360182, 4.877301], + [52.360454, 4.877099], + [52.360382, 4.876823], + [52.360092, 4.877026], + [52.359999, 4.876698], + [52.359691, 4.875755], + [52.359617, 4.875816], + [52.359548, 4.875865], + [52.359534, 4.875876], + [52.359525, 4.875882], + [52.359517, 4.875887], + [52.3594, 4.87597], + [52.359375, 4.875875], + [52.35934, 4.875745], + [52.359325, 4.875691], + [52.359324, 4.875685], + [52.359316, 4.875649], + [52.359317, 4.875649], + [52.359266, 4.875455], + [52.359263, 4.875445], + [52.359211, 4.875247], + [52.359225, 4.875237], + [52.359181, 4.875074], + [52.359143, 4.875102], + [52.359071, 4.875154], + [52.359062, 4.875161], + [52.359035, 4.87518], + [52.359019, 4.875192], + [52.358899, 4.875278], + [52.358891, 4.875298], + [52.358881, 4.875321], + [52.358882, 4.875323], + [52.358876, 4.875327], + [52.358873, 4.875318], + [52.358874, 4.875318], + [52.358873, 4.875314], + [52.358778, 4.875379], + [52.358754, 4.875285], + [52.358705, 4.8751], + [52.358677, 4.874996], + [52.358653, 4.875014], + [52.358635, 4.875027], + [52.358567, 4.875076], + [52.358471, 4.875145], + [52.358446, 4.875049], + [52.358397, 4.874864], + [52.358336, 4.874628], + [52.358333, 4.874618], + [52.358137, 4.873869], + [52.358135, 4.87386], + [52.35813, 4.873839], + [52.358101, 4.873736], + [52.358097, 4.873721], + [52.358087, 4.873685], + [52.358083, 4.873672], + [52.358071, 4.873637], + [52.35807, 4.873632], + [52.358068, 4.873627], + [52.358061, 4.873606], + [52.358043, 4.873564], + [52.358039, 4.873555], + [52.358017, 4.873496], + [52.358014, 4.873487], + [52.358011, 4.873479], + [52.358003, 4.873455], + [52.357982, 4.873392], + [52.357966, 4.87334], + [52.357961, 4.873321], + [52.357959, 4.873312], + [52.357943, 4.873246], + [52.357927, 4.873178], + [52.357919, 4.873137], + [52.357917, 4.87313], + [52.357915, 4.873122], + [52.357907, 4.873072], + [52.357895, 4.872998], + [52.357883, 4.872921], + [52.357874, 4.872857], + [52.357868, 4.872805], + [52.357857, 4.87275], + [52.357854, 4.872732], + [52.35784, 4.872663], + [52.357826, 4.872603], + [52.357817, 4.872572], + [52.357804, 4.872527], + [52.357789, 4.872478], + [52.357784, 4.872465], + [52.35778, 4.872452], + [52.357777, 4.872445], + [52.357762, 4.872408], + [52.357746, 4.872372], + [52.357728, 4.872335], + [52.357723, 4.872327], + [52.357708, 4.8723], + [52.357682, 4.872261], + [52.357653, 4.872227], + [52.35765, 4.872223], + [52.357647, 4.87222], + [52.357649, 4.872209], + [52.357668, 4.872094], + [52.35767, 4.87208], + [52.357674, 4.872066], + [52.357679, 4.872053], + [52.357685, 4.872042], + [52.357692, 4.872032], + [52.357699, 4.872023], + [52.357717, 4.872005], + [52.357715, 4.871999], + [52.357704, 4.871959], + [52.357703, 4.871953], + [52.357693, 4.871917], + [52.357693, 4.871916], + [52.357678, 4.871864], + [52.357677, 4.871861], + [52.357677, 4.87186], + [52.35767, 4.871836], + [52.35767, 4.871833], + [52.357656, 4.871781], + [52.357655, 4.871777], + [52.357647, 4.871748], + [52.357636, 4.871704], + [52.357631, 4.871687], + [52.357609, 4.871697], + [52.357607, 4.871699], + [52.357604, 4.8717], + [52.357601, 4.871701], + [52.3576, 4.871701], + [52.357598, 4.871702], + [52.357595, 4.871702], + [52.357592, 4.871702], + [52.357588, 4.871703], + [52.357585, 4.871702], + [52.357582, 4.871702], + [52.357579, 4.871701], + [52.357576, 4.871701], + [52.357573, 4.8717], + [52.35757, 4.871698], + [52.357568, 4.871697], + [52.357565, 4.871695], + [52.357562, 4.871693], + [52.357559, 4.871691], + [52.357557, 4.871689], + [52.357503, 4.871634], + [52.357487, 4.871619], + [52.35749, 4.871575], + [52.357491, 4.871567], + [52.357491, 4.871549], + [52.35749, 4.871531], + [52.357489, 4.871499], + [52.357487, 4.87146], + [52.357485, 4.871422], + [52.357481, 4.871377], + [52.357479, 4.871365], + [52.357477, 4.871352], + [52.357475, 4.871339], + [52.357473, 4.871327], + [52.357471, 4.871314], + [52.357468, 4.871302], + [52.357466, 4.871289], + [52.357464, 4.871281], + [52.357463, 4.871277], + [52.35746, 4.871265], + [52.357458, 4.871253], + [52.357455, 4.871241], + [52.357451, 4.871229], + [52.357448, 4.871217], + [52.357445, 4.871205], + [52.357421, 4.871123], + [52.357405, 4.871076], + [52.35739, 4.87103], + [52.357375, 4.870983], + [52.357347, 4.87091], + [52.35731, 4.870812], + [52.357238, 4.870613], + [52.357236, 4.870607], + [52.35723, 4.870588], + [52.357219, 4.870549], + [52.357198, 4.870476], + [52.357161, 4.870337], + [52.35725, 4.870249], + [52.357148, 4.86996], + [52.357084, 4.870021], + [52.35705, 4.869847], + [52.357028, 4.869741], + [52.357027, 4.869734], + [52.35698, 4.869537], + [52.356929, 4.869353], + [52.356918, 4.869314], + [52.356902, 4.869267], + [52.356898, 4.869253], + [52.35689, 4.869229], + [52.356871, 4.869174], + [52.356867, 4.869163], + [52.356856, 4.869131], + [52.356848, 4.86911], + [52.356844, 4.869099], + [52.356841, 4.86909], + [52.356824, 4.869043], + [52.356804, 4.868993], + [52.356798, 4.868981], + [52.356786, 4.868954], + [52.356752, 4.868879], + [52.356749, 4.868874], + [52.356746, 4.868865], + [52.356726, 4.868822], + [52.356694, 4.868749], + [52.356688, 4.868737], + [52.356679, 4.868712], + [52.356678, 4.868711], + [52.35662, 4.868562], + [52.356551, 4.868386], + [52.356547, 4.868362], + [52.356541, 4.868339], + [52.356531, 4.868303], + [52.356514, 4.868245], + [52.35651, 4.868231], + [52.356499, 4.868198], + [52.356482, 4.868124], + [52.356433, 4.867918], + [52.356428, 4.867896], + [52.356424, 4.867874], + [52.356423, 4.867867], + [52.356422, 4.86786], + [52.356421, 4.867854], + [52.35642, 4.867849], + [52.35642, 4.867843], + [52.35642, 4.867838], + [52.35642, 4.867833], + [52.356421, 4.867828], + [52.356421, 4.867823], + [52.356421, 4.867818], + [52.356422, 4.867813], + [52.356423, 4.867811], + [52.356423, 4.867808], + [52.356424, 4.867804], + [52.356425, 4.867799], + [52.356426, 4.867794], + [52.356427, 4.86779], + [52.356429, 4.867785], + [52.35643, 4.867781], + [52.356432, 4.867776], + [52.356434, 4.867772], + [52.356436, 4.867768], + [52.356438, 4.867764], + [52.356439, 4.867762], + [52.35644, 4.867761], + [52.356442, 4.867757], + [52.356444, 4.867754], + [52.356447, 4.86775], + [52.356449, 4.867747], + [52.356452, 4.867744], + [52.356454, 4.867742], + [52.356457, 4.867739], + [52.35646, 4.867737], + [52.356463, 4.867735], + [52.356465, 4.867733], + [52.356468, 4.867731], + [52.356432, 4.867565], + [52.356431, 4.867561], + [52.356427, 4.867561], + [52.356423, 4.867561], + [52.35642, 4.867561], + [52.356416, 4.867561], + [52.356412, 4.867561], + [52.356408, 4.86756], + [52.356405, 4.867559], + [52.356401, 4.867558], + [52.356397, 4.867557], + [52.356393, 4.867555], + [52.35639, 4.867554], + [52.356386, 4.867552], + [52.356383, 4.867549], + [52.356379, 4.867547], + [52.356376, 4.867544], + [52.356372, 4.867541], + [52.356369, 4.867539], + [52.356366, 4.867535], + [52.356363, 4.867532], + [52.35636, 4.867528], + [52.356357, 4.867524], + [52.356354, 4.86752], + [52.356351, 4.867516], + [52.356348, 4.867512], + [52.356346, 4.867507], + [52.356343, 4.867503], + [52.356341, 4.867498], + [52.356338, 4.867493], + [52.356337, 4.867489], + [52.356334, 4.867483], + [52.356332, 4.867477], + [52.356331, 4.867472], + [52.356313, 4.867397], + [52.356264, 4.867188], + [52.356123, 4.867138], + [52.355993, 4.867233], + [52.355879, 4.867041], + [52.35522, 4.867388], + [52.355179, 4.867242], + [52.354953, 4.867086], + [52.354738, 4.866854], + [52.35481, 4.866804], + [52.355202, 4.866704], + [52.35548, 4.864662], + [52.355727, 4.864192], + [52.355791, 4.862903], + [52.355733, 4.862106], + [52.355635, 4.861622], + [52.355435, 4.861145], + [52.354699, 4.859973], + [52.35447, 4.858866], + [52.354675, 4.857893], + [52.354853, 4.857623], + [52.354856, 4.857623], + [52.354907, 4.857616], + [52.354904, 4.857568], + [52.354897, 4.857496], + [52.354888, 4.857424], + [52.354884, 4.857399], + [52.354872, 4.857336], + [52.354849, 4.857247], + [52.354828, 4.857168], + [52.354823, 4.857151], + [52.354817, 4.857129], + [52.354797, 4.857069], + [52.354768, 4.856978], + [52.354769, 4.856978], + [52.35457, 4.856955], + [52.354956, 4.856721], + [52.355001, 4.856627], + [52.355074, 4.856625], + [52.355073, 4.856505], + [52.355009, 4.856159], + [52.354551, 4.856468], + [52.354524, 4.856235], + [52.354549, 4.855857], + [52.35462, 4.855618], + [52.35487, 4.85565], + [52.355, 4.855619], + [52.355107, 4.855593], + [52.356388, 4.85495], + [52.356442, 4.85511], + [52.358191, 4.861819], + [52.358976, 4.865166], + [52.358878, 4.865332], + [52.358874, 4.865553], + [52.358854, 4.865778], + [52.358881, 4.865862], + [52.358958, 4.86592], + [52.3591, 4.866076], + [52.359217, 4.866277], + [52.359243, 4.866336], + [52.359755, 4.868507], + [52.360141, 4.869972], + [52.360008, 4.870059], + [52.360215, 4.870868] + ] + ] + ] + } +} diff --git a/src/pages/PolygonLayer/layerStyles.ts b/src/pages/PolygonLayer/layerStyles.ts new file mode 100644 index 0000000..454770e --- /dev/null +++ b/src/pages/PolygonLayer/layerStyles.ts @@ -0,0 +1,10 @@ +export const polygonStyles = { + fillOpacity: 0.2, + color: '#0000ff', +}; + +export const polygonHoverStyles = { + ...polygonStyles, + fillOpacity: 0.5, + color: '#ff0000', +}; diff --git a/src/pages/PolygonLayer/styles.module.css b/src/pages/PolygonLayer/styles.module.css new file mode 100644 index 0000000..d171d7c --- /dev/null +++ b/src/pages/PolygonLayer/styles.module.css @@ -0,0 +1,4 @@ +.container { + height: 100%; + min-height: 100%; +} diff --git a/src/stories/pages/PolygonLayer/index.mdx b/src/stories/pages/PolygonLayer/index.mdx new file mode 100644 index 0000000..560caf9 --- /dev/null +++ b/src/stories/pages/PolygonLayer/index.mdx @@ -0,0 +1,96 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as PolygonLayerStories from './index.stories'; +import PolygonLayer from '@/pages/PolygonLayer/PolygonLayer?raw'; +import styles from '@/pages/PolygonLayer/styles.module.css?raw'; +import layerStyles from '@/pages/PolygonLayer/layerStyles?raw'; +import data from '@/pages/PolygonLayer/data.json?raw'; + + + +# PolygonLayer + +## Requirements + +- This example is built upon the [BaseMap component example](../?path=/docs/react-baselayer--docs). +- [See global requirements list](../?path=/docs/global-requirements--docs) + +## Description + +A polygon layer is used to display a location or area on a map. By default, a polygon layer is a HTML SVG element rendered inside the parent map DOM element. This polygon layer can be configured, extended and restyled ([see docs](https://leafletjs.com/reference.html#polygon)). + +The primary code in regards to creating a Leaflet polygon layer, is in `PolygonLayer.tsx` lines 64-87, as displayed here: + +```js +useEffect(() => { + if (mapInstance) { + // TypeScript will often throw errors with Leaflet coordinate sets if you don't explicitly cast the type + polygonRef.current = L.polygon( + data.geometry.coordinates as LatLngTuple[][][], + { + className: 'c-layer', + } + ) + .addTo(mapInstance) + .on('mouseover', () => { + polygonRef.current?.setStyle(polygonHoverStyles); + }) + .on('mouseout', () => { + polygonRef.current?.setStyle(polygonStyles); + }); + } + + return () => { + if (polygonRef.current && mapInstance) { + mapInstance.removeLayer(polygonRef.current); + } + }; +}, [data, mapInstance]); +``` + +## Usage Scenarios + +- **Mapping Regions**: Representing administrative boundaries such as countries, states, or municipalities. +- **Highlighting Zones**: Indicating specific zones like school districts, voting districts, or postal codes. +- **Spatial Analysis**: Visualizing areas affected by events such as weather phenomena, natural disasters, or market areas. +- **Interactive Applications**: Enabling users to draw and interact with polygons to create custom regions for analysis or reporting. + +## How to implement + +To implement a Leaflet polygon layer there are three files required, this example also uses an extra file for demo data: + +1. The React components + - [PolygonLayer.tsx](#1-polygonlayertsx) +2. The CSS styles (1 file) + - [styles.module.css](#1-stylesmodulecss) +3. The layer styles + - [layerStyles.ts](#1-layerstylests) +4. Demo data (1 file) + - [data.json](#1-datajson) *- This data represents the coordinates for Vondelpark.* + +## Usage + +The following files are required: + +### React Components + +#### 1. PolygonLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + +### Layer styles + +#### 1. layerStyles.ts + + + +### Utils + +#### 1. data.json + + diff --git a/src/stories/pages/PolygonLayer/index.stories.ts b/src/stories/pages/PolygonLayer/index.stories.ts new file mode 100644 index 0000000..0f0f502 --- /dev/null +++ b/src/stories/pages/PolygonLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import PolygonLayer from '@/pages/PolygonLayer/PolygonLayer'; + +const meta = { + title: 'React/PolygonLayer', + component: PolygonLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; diff --git a/src/utils/reverseLatLng.ts b/src/utils/reverseLatLng.ts new file mode 100644 index 0000000..a8b7aed --- /dev/null +++ b/src/utils/reverseLatLng.ts @@ -0,0 +1,11 @@ +import L from 'leaflet'; + +// Leaflet uses lat-lng (or north-east) whereas GeoJSON uses lng-lat (or east-north). +// @see https://macwright.com/lonlat/ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const reverseLatLng = (input: any) => + L.geoJSON(input, { + coordsToLatLng: coords => new L.LatLng(coords[0], coords[1], coords[2]), + }); + +export default reverseLatLng; From a7307605e61291835076c0c617cb8a9649828d76 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Tue, 14 May 2024 15:45:51 +0200 Subject: [PATCH 08/34] Further favicon cleanup --- favicon.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 favicon.svg diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..62e1a0b --- /dev/null +++ b/favicon.svg @@ -0,0 +1 @@ + From 424d02320ade95f1e9708e0c353559824f09fa8e Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Tue, 14 May 2024 15:46:46 +0200 Subject: [PATCH 09/34] storybook logo changed to gemeente --- .storybook/static/amsterdam.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .storybook/static/amsterdam.svg diff --git a/.storybook/static/amsterdam.svg b/.storybook/static/amsterdam.svg new file mode 100644 index 0000000..14cf892 --- /dev/null +++ b/.storybook/static/amsterdam.svg @@ -0,0 +1,8 @@ + + + + + + + + From cb60a82f0d68f5a4ba366815bb9174e5d053142e Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Wed, 15 May 2024 16:42:17 +0200 Subject: [PATCH 10/34] Make storybook primary script, remove vite build and non-map components --- favicon.svg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 favicon.svg diff --git a/favicon.svg b/favicon.svg deleted file mode 100644 index 62e1a0b..0000000 --- a/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - From 07e9f4c1d6195e26f97e2df7253cea33337046ca Mon Sep 17 00:00:00 2001 From: Siree Koolen-Wijkstra Date: Tue, 21 May 2024 11:46:45 +0200 Subject: [PATCH 11/34] Marker for react leaflet - tests are added - documentation in line with Marker leaflet --- src/pages/ReactLeaflet/Marker/Marker.test.tsx | 15 ++++ src/pages/ReactLeaflet/Marker/Marker.tsx | 34 +++++++++ .../ReactLeaflet/Marker/styles.module.css | 8 +++ src/shared/assets/Location.svg | 1 + .../pages/ReactLeaflet/Marker/index.mdx | 72 +++++++++++++++++++ .../ReactLeaflet/Marker/index.stories.ts | 19 +++++ src/utils/icons/defaultMarker.ts | 13 ++++ 7 files changed, 162 insertions(+) create mode 100644 src/pages/ReactLeaflet/Marker/Marker.test.tsx create mode 100644 src/pages/ReactLeaflet/Marker/Marker.tsx create mode 100644 src/pages/ReactLeaflet/Marker/styles.module.css create mode 100644 src/shared/assets/Location.svg create mode 100644 src/stories/pages/ReactLeaflet/Marker/index.mdx create mode 100644 src/stories/pages/ReactLeaflet/Marker/index.stories.ts create mode 100644 src/utils/icons/defaultMarker.ts diff --git a/src/pages/ReactLeaflet/Marker/Marker.test.tsx b/src/pages/ReactLeaflet/Marker/Marker.test.tsx new file mode 100644 index 0000000..a1f2905 --- /dev/null +++ b/src/pages/ReactLeaflet/Marker/Marker.test.tsx @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import Marker from './Marker'; + +describe('Marker', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + expect(container.querySelector('.map-marker')).toBeInTheDocument(); + }); + it('shows the marker', () => { + const { container } = render(); + expect(container.querySelector('.map-marker')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/ReactLeaflet/Marker/Marker.tsx b/src/pages/ReactLeaflet/Marker/Marker.tsx new file mode 100644 index 0000000..ca222f8 --- /dev/null +++ b/src/pages/ReactLeaflet/Marker/Marker.tsx @@ -0,0 +1,34 @@ +import L from 'leaflet'; +import { + MapContainer, + Marker as MarkerLeaflet, + TileLayer, +} from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; +import defaultMarker from '@/utils/icons/defaultMarker'; + +const position = L.latLng([52.370216, 4.895168]); +const Marker = (): JSX.Element => ( +
+ + + + +
+); + +export default Marker; diff --git a/src/pages/ReactLeaflet/Marker/styles.module.css b/src/pages/ReactLeaflet/Marker/styles.module.css new file mode 100644 index 0000000..7a173cb --- /dev/null +++ b/src/pages/ReactLeaflet/Marker/styles.module.css @@ -0,0 +1,8 @@ +.container { + height: 100%; + min-height: 100%; + + > div { + height: 100%; + } +} diff --git a/src/shared/assets/Location.svg b/src/shared/assets/Location.svg new file mode 100644 index 0000000..a3e80e2 --- /dev/null +++ b/src/shared/assets/Location.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/stories/pages/ReactLeaflet/Marker/index.mdx b/src/stories/pages/ReactLeaflet/Marker/index.mdx new file mode 100644 index 0000000..23e1de6 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/Marker/index.mdx @@ -0,0 +1,72 @@ +import { Meta, Source } from '@storybook/blocks'; +import * as MarkerStories from './index.stories'; +import Marker from '@/pages/ReactLeaflet/Marker/Marker?raw'; +import styles from '@/pages/ReactLeaflet/Marker/styles.module.css?raw'; +import getCrsRd from '@/utils/getCrsRd?raw'; +import defaultMarker from '@/utils/icons/defaultMarker?raw'; + + + +# Amsterdam Marker +## Requirements + +- [See global requirements list](../?path=/docs/global-requirements--docs) +- CRS handling ([utils/getCrsRd.ts](#1-getcrsrdts)) + +## Description + +The Marker component renders a map centered on Amsterdam with a marker. +This component uses the React Leaflet library to create and manage the map. + +### Large numbers of markers can lead to degraded performance +Every Marker is an HTML image element and showing one will have a ver slight negative impact on performance. +Showing tens of thousands of Markers wil have a significant impact. +Please don't do this without either clustering or setting the 'preferCanvas' option of the Leaflet map to 'true'. + +### Create Custom Marker Icon: +Define a custom marker icon if needed. Make sure to replace the placeholder path with the correct path to your marker icon. +```js +import markerIconUrl from '@/path/to/marker-icon.png'; // Correct path to your marker icon + +const defaultMarker = L.icon({ + iconUrl: markerIconUrl, + ... +}); + +export default defaultMarker; +``` + +## How to implement + +To accomplish the Amstedam Marker there are the following files: +1. The React components + * [Marker.tsx](#1-markertsx) +2. The CSS styles (1 file) + * [styles.module.css](#1-stylesmodulecss) +3. Utils (2 files) + * [defaultMarker.ts](#1-defaultmarkerts) + * [getCrsRd.ts](#2-getcrsrdts) + +## Usage + +The following files are required: + +### React Components + +#### 1. Marker.tsx + + +### CSS Styling + +#### 1. styles.module.css + + + +### Utils +#### 1. defaultMarker.ts + + + +#### 2. getCrsRd.ts + + \ No newline at end of file diff --git a/src/stories/pages/ReactLeaflet/Marker/index.stories.ts b/src/stories/pages/ReactLeaflet/Marker/index.stories.ts new file mode 100644 index 0000000..28adc27 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/Marker/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Marker from '@/pages/ReactLeaflet/Marker/Marker'; + +const meta = { + title: 'React-Leaflet/Marker', + component: Marker, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; diff --git a/src/utils/icons/defaultMarker.ts b/src/utils/icons/defaultMarker.ts new file mode 100644 index 0000000..44a378a --- /dev/null +++ b/src/utils/icons/defaultMarker.ts @@ -0,0 +1,13 @@ +import L from 'leaflet'; +import type { PointExpression } from 'leaflet'; +import LocationIcon from '../../shared/assets/Location.svg?raw'; + +const defaultMarker = L.divIcon({ + html: LocationIcon, + iconSize: [24, 32] as PointExpression, + iconAnchor: [12, 32] as PointExpression, + popupAnchor: [0, -30] as PointExpression, + className: 'map-marker', +}); + +export default defaultMarker; From 075ff6dcb7a72f36c49882af7325b4a402ac1b4e Mon Sep 17 00:00:00 2001 From: Siree Koolen-Wijkstra Date: Wed, 5 Jun 2024 13:59:40 +0200 Subject: [PATCH 12/34] removed duplicated test, removed var that was used only once --- src/pages/ReactLeaflet/Marker/Marker.test.tsx | 1 - src/pages/ReactLeaflet/Marker/Marker.tsx | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/ReactLeaflet/Marker/Marker.test.tsx b/src/pages/ReactLeaflet/Marker/Marker.test.tsx index a1f2905..ec9f52f 100644 --- a/src/pages/ReactLeaflet/Marker/Marker.test.tsx +++ b/src/pages/ReactLeaflet/Marker/Marker.test.tsx @@ -6,7 +6,6 @@ describe('Marker', () => { it('renders the component', () => { const { container } = render(); expect(container.firstChild).toBeDefined(); - expect(container.querySelector('.map-marker')).toBeInTheDocument(); }); it('shows the marker', () => { const { container } = render(); diff --git a/src/pages/ReactLeaflet/Marker/Marker.tsx b/src/pages/ReactLeaflet/Marker/Marker.tsx index ca222f8..b58de1b 100644 --- a/src/pages/ReactLeaflet/Marker/Marker.tsx +++ b/src/pages/ReactLeaflet/Marker/Marker.tsx @@ -9,7 +9,6 @@ import getCrsRd from '@/utils/getCrsRd'; import styles from './styles.module.css'; import defaultMarker from '@/utils/icons/defaultMarker'; -const position = L.latLng([52.370216, 4.895168]); const Marker = (): JSX.Element => (
( subdomains={['t1', 't2', 't3', 't4']} tms /> - +
); From 34d89ed36e48182224eb4f539c67acb73ce0ed66 Mon Sep 17 00:00:00 2001 From: Siree Koolen-Wijkstra Date: Tue, 4 Jun 2024 13:25:20 +0200 Subject: [PATCH 13/34] Polyline Layer component is added - includes test and storybook --- .../PolylineLayer/PolylineLayer.test.tsx | 20 +++++++ .../PolylineLayer/PolylineLayer.tsx | 30 ++++++++++ .../ReactLeaflet/PolylineLayer/data.json | 18 ++++++ .../PolylineLayer/styles.module.css | 8 +++ .../ReactLeaflet/PolylineLayer/index.mdx | 59 +++++++++++++++++++ .../PolylineLayer/index.stories.ts | 19 ++++++ .../pages/ReactLeaflet/WFSLayer/index.mdx | 2 +- 7 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.test.tsx create mode 100644 src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.tsx create mode 100644 src/pages/ReactLeaflet/PolylineLayer/data.json create mode 100644 src/pages/ReactLeaflet/PolylineLayer/styles.module.css create mode 100644 src/stories/pages/ReactLeaflet/PolylineLayer/index.mdx create mode 100644 src/stories/pages/ReactLeaflet/PolylineLayer/index.stories.ts diff --git a/src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.test.tsx b/src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.test.tsx new file mode 100644 index 0000000..4052ad0 --- /dev/null +++ b/src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.test.tsx @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import PolylineLayer from './PolylineLayer'; + +describe('PolylineLayer', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + it('shows the polyline svg', () => { + const { container } = render(); + const svgPolyline = container.querySelector('svg path'); + const stroke = svgPolyline && svgPolyline.getAttribute('stroke'); + const fill = svgPolyline && svgPolyline.getAttribute('fill'); + + expect(svgPolyline).toBeInTheDocument(); + expect(stroke).toEqual('#3388ff'); + expect(fill).toEqual('none'); + }); +}); diff --git a/src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.tsx b/src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.tsx new file mode 100644 index 0000000..579e7fe --- /dev/null +++ b/src/pages/ReactLeaflet/PolylineLayer/PolylineLayer.tsx @@ -0,0 +1,30 @@ +import { MapContainer, TileLayer, Polyline } from 'react-leaflet'; +import styles from '@/pages/ReactLeaflet/PolylineLayer/styles.module.css'; +import L, { LatLngTuple } from 'leaflet'; +import getCrsRd from '@/utils/getCrsRd'; +import data from './data.json'; + +const PolylineLayer = (): JSX.Element => { + return ( +
+ + + + +
+ ); +}; + +export default PolylineLayer; diff --git a/src/pages/ReactLeaflet/PolylineLayer/data.json b/src/pages/ReactLeaflet/PolylineLayer/data.json new file mode 100644 index 0000000..c5f0526 --- /dev/null +++ b/src/pages/ReactLeaflet/PolylineLayer/data.json @@ -0,0 +1,18 @@ +{ + "geometry": { + "type": "MultiLineString", + "coordinates": [ + [ + [52.37048257777301, 4.894874428904198], + [52.370554861935126, 4.894898864319151], + [52.37055767285022, 4.894899406706797], + [52.37058186436687, 4.894908315567703], + [52.37079908397672, 4.89500238214001], + [52.37085087918953, 4.895030257356288], + [52.37089819409757, 4.895061394436971], + [52.370944121895796, 4.895096676809393], + [52.370972589599155, 4.895120795880029] + ] + ] + } +} diff --git a/src/pages/ReactLeaflet/PolylineLayer/styles.module.css b/src/pages/ReactLeaflet/PolylineLayer/styles.module.css new file mode 100644 index 0000000..7a173cb --- /dev/null +++ b/src/pages/ReactLeaflet/PolylineLayer/styles.module.css @@ -0,0 +1,8 @@ +.container { + height: 100%; + min-height: 100%; + + > div { + height: 100%; + } +} diff --git a/src/stories/pages/ReactLeaflet/PolylineLayer/index.mdx b/src/stories/pages/ReactLeaflet/PolylineLayer/index.mdx new file mode 100644 index 0000000..7cfeee0 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/PolylineLayer/index.mdx @@ -0,0 +1,59 @@ +import { Meta, Source } from '@storybook/blocks'; +import * as PolylineLayerStories from './index.stories'; +import PolylineLayer from '@/pages/ReactLeaflet/PolylineLayer/PolylineLayer?raw'; +import styles from '@/pages/ReactLeaflet/PolylineLayer/styles.module.css?raw'; +import getCrsRd from '@/utils/getCrsRd?raw'; + + + +# Amsterdam PolylineLayer +## Requirements + +- [See global requirements list](../?path=/docs/global-requirements--docs) +- CRS handling ([utils/getCrsRd.ts](#1-getcrsrdts)) + +## Description + +This is the Amsterdam polyline layer on a React-leaflet map. + +## Background + +### Amsterdam polyline layer + +A polyline layer is used to display lines on a map. +It can be configured, extended and restyled ([see docs](https://leafletjs.com/reference.html#polyline)). +You can create a Polyline object with multiple separate lines +(MultiPolyline) by passing an array of arrays of geographic points. + + +## How to implement + +To accomplish the Amstedam base/tile layer there are three files: +1. The React components + * [PolylineLayer.tsx](#1-polylinelayertsx) +2. The CSS styles (1 file) + * [styles.module.css](#1-stylesmodulecss) +3. Utils (1 file) + * [getCrsRd.ts](#1-getcrsrdts) + +## Usage + +The following files are required: + +### React Components + +#### 1. PolylineLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + +### Utils + +#### 1. getCrsRd.ts + + diff --git a/src/stories/pages/ReactLeaflet/PolylineLayer/index.stories.ts b/src/stories/pages/ReactLeaflet/PolylineLayer/index.stories.ts new file mode 100644 index 0000000..91139a7 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/PolylineLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import PolylineLayer from '@/pages/ReactLeaflet/PolylineLayer/PolylineLayer'; + +const meta = { + title: 'React-Leaflet/PolylineLayer', + component: PolylineLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; diff --git a/src/stories/pages/ReactLeaflet/WFSLayer/index.mdx b/src/stories/pages/ReactLeaflet/WFSLayer/index.mdx index 3f563e6..e9663ca 100644 --- a/src/stories/pages/ReactLeaflet/WFSLayer/index.mdx +++ b/src/stories/pages/ReactLeaflet/WFSLayer/index.mdx @@ -1,4 +1,4 @@ -import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import { Meta, Source } from '@storybook/blocks'; import * as WFSLayerStories from './index.stories'; import Baselayer from '@/pages/ReactLeaflet/WFSLayer/BaseLayer?raw'; From 1be08bdd087e4f3148c3e5a82393fa86c893526e Mon Sep 17 00:00:00 2001 From: reno1979 Date: Tue, 18 Jun 2024 09:38:52 +0200 Subject: [PATCH 14/34] [chore] layer descriptions (#29) --- src/stories/Layers.mdx | 133 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/stories/Layers.mdx diff --git a/src/stories/Layers.mdx b/src/stories/Layers.mdx new file mode 100644 index 0000000..1012940 --- /dev/null +++ b/src/stories/Layers.mdx @@ -0,0 +1,133 @@ +import { Meta } from '@storybook/blocks'; + + + +# Layers inside Leaflet + +Leaflet has a number of layers that can be added to a map. These layers can be used to display different types of data on the map. The most common layers are: + +- **TileLayer:** Used to display tile layers on the map. This is the most common layer used in Leaflet. +- **Marker:** Used to display markers on the map. Markers are used to display points of interest on the map. +- **Popup:** Used to display popups on the map. Popups are used to display additional information about a point of interest on the map. +- **Tooltip:** Used to display tooltips on the map. Tooltips are used to display additional information about a point of interest on the map when the user hovers over it. +- **ImageOverlay:** Used to display images on the map. Images can be used to display additional information about a point of interest on the map. +- **VideoOverlay:** Used to display videos on the map. Videos can be used to display additional information about a point of interest on the map. +- **Canvas:** Used to display custom canvas elements on the map. Canvas elements can be used to display custom graphics on the map. + +In addition to these layers, Leaflet also supports various types of data sources that can be displayed as layers on the map: + +- **WFS (Web Feature Service):** This is a standard protocol for requesting geospatial data from a server. In Leaflet, you can use a plugin like `leaflet-wfs` to add WFS layers to your map. WFS layers are used to display vector data on the map. +- **WMS (Web Map Service):** This is another standard protocol for requesting geospatial data from a server. Leaflet has built-in support for WMS layers. WMS layers are used to display raster data on the map. +- **MVT (Mapbox Vector Tiles):** This is a format for encoding vector tile data. In Leaflet, you can use a plugin like `leaflet-vector-tiles` to add MVT layers to your map. MVT layers are used to display vector data on the map. +- **GeoJSON:** This is a format for encoding geographic data structures. Leaflet has built-in support for GeoJSON layers. GeoJSON layers are used to display vector data on the map, and they can be styled to represent different types of features. + +In this guide, we'll take a closer look at some of the most common layers in Leaflet and how you can use them to create interactive maps. + +## TileLayer + +The `TileLayer` in Leaflet is used to display tile layers on the map. Tile layers are images that are loaded and displayed on the map as the user pans and zooms. +Leaflet has built-in support for a number of tile layer providers, including OpenStreetMap, Mapbox, and Stamen. +You can also create custom tile layers using your own tile server or by using a third-party tile provider. + +The `TileLayer` is used in the BaseLayer examples in this guide. The BaseLayer examples show how to create a simple map with a tile layer as the base layer. +See: + +- [BaseLayer react](../?path=/docs/react-baselayer--docs) +- [BaseLayer react-leaflet](../?path=/docs/react-leaflet-baselayer--docs) + +## Marker + +The `Marker` in Leaflet is used to display markers on the map. Markers are used to display points of interest on the map, such as restaurants, shops, or tourist attractions. +Markers can be customized with icons, popups, and tooltips to provide additional information about the point of interest. + +The Marker examples show how to create a simple map with markers at specific locations. +See: + +** Coming Soon ** + +## Popup + +The `Popup` in Leaflet is used to display popups on the map. Popups are used to display additional information about a point of interest on the map. +Popups can contain text, images, videos, and other HTML elements. + +The Popup examples show how to create a simple map with popups at specific locations. + +** Coming Soon ** + +## Tooltip + +The `Tooltip` in Leaflet is used to display tooltips on the map. Tooltips are used to display additional information about a point of interest on the map when the user hovers over it. +Tooltips can contain text, images, videos, and other HTML elements. + +The Tooltip examples show how to create a simple map with tooltips at specific locations. + +** Coming Soon ** + +## ImageOverlay + +The `ImageOverlay` in Leaflet is used to display images on the map. Images can be used to display additional information about a point of interest on the map. +Image overlays can be used to display floor plans, historical maps, or other images that provide context for the map. + +The ImageOverlay examples show how to create a simple map with an image overlay. + +** Coming Soon ** + +## VideoOverlay + +The `VideoOverlay` in Leaflet is used to display videos on the map. Videos can be used to display additional information about a point of interest on the map. +VideoOverlay can be used to display videos of events, attractions, or other points of interest on the map. + +The VideoOverlay examples show how to create a simple map with a video overlay. + +** Coming Soon ** + +## Canvas + +The `Canvas` in Leaflet is used to display custom canvas elements on the map. Canvas elements can be used to display custom graphics on the map. +Canvas elements can be used to create custom visualizations, animations, or interactive elements on the map. + +The Canvas examples show how to create a simple map with a custom canvas element. + +** Coming Soon ** + +## WFS (Web Feature Service) + +The WFS layer in Leaflet is used to display vector data on the map, it can be used to display points, lines, polygons, and other vector features. +`WFS` is a standard protocol for requesting geospatial data from a server. + +You can use a plugin like `leaflet-wfs` to add WFS layers to your map. However, it's not always necessary to use a plugin if you're comfortable with handling the WFS request and response yourself. +The leaflet-wfs plugin and similar plugins provide a more convenient way to work with WFS services by handling the request and response for you. They can also provide additional features like automatic refreshing of the layer when the data changes. + +The WFS examples show how to create a simple map with a WFS layer. (without the use of a plugin) + +See: + +- [WFSLayer react](../?path=/docs/react-wfslayer--docs) +- [WFSLayer react-leaflet](../?path=/docs/react-leaflet-wfslayer--docs) + +## WMS (Web Map Service) + +The WMS layer in Leaflet is used to display raster data on the map, it can be used to display satellite imagery, aerial photography, and other raster data. +`WMS` is a standard protocol for requesting geospatial data from a server. Leaflet has built-in support for WMS layers. + +The WMS examples show how to create a simple map with a WMS layer. + +** Coming Soon ** + +## MVT (Mapbox Vector Tiles) + +The `MVT` in Leaflet is used to display vector data on the map, it can be used to display points, lines, polygons, and other vector features on the map. + +You can use a plugin like `leaflet-vector-tiles` to add MVT layers to your map. +The MVT examples show how to create a simple map with a MVT layer. + +** Coming Soon ** + +## GeoJSON + +The `GeoJSON` in Leaflet is used to display vector data on the map. GeoJSON layers are used to display vector data on the map. +GeoJSON layers can be used to display points, lines, polygons, and other vector features on the map. + +The GeoJSON examples show how to create a simple map with a GeoJSON layer. + +** Coming Soon ** From 5418bc796188c9edd8ffdff7713e674966c93476 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jun 2024 09:50:46 +0200 Subject: [PATCH 15/34] Bump ws from 8.17.0 to 8.17.1 (#31) Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.17.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.0...8.17.1) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e21f96..71e03cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18167,9 +18167,9 @@ } }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" From 06c39416b8ec2181a896fb2d4c44551ef781195b Mon Sep 17 00:00:00 2001 From: Siree Koolen-Wijkstra Date: Wed, 5 Jun 2024 13:46:37 +0200 Subject: [PATCH 16/34] WMSLayer added - test is only if component is rendered since otherwise it is testing an react leaflet component. --- .../ReactLeaflet/BaseLayer/BaseLayer.tsx | 1 + src/pages/ReactLeaflet/Marker/Marker.tsx | 1 + .../PolygonLayer/PolygonLayer.tsx | 1 + .../PolylineLayer/PolylineLayer.tsx | 1 + src/pages/ReactLeaflet/WFSLayer/BaseLayer.tsx | 1 + .../ReactLeaflet/WMSLayer/WMSLayer.test.tsx | 10 ++++ src/pages/ReactLeaflet/WMSLayer/WMSLayer.tsx | 35 ++++++++++++ .../ReactLeaflet/WMSLayer/styles.module.css | 8 +++ .../pages/ReactLeaflet/WMSLayer/index.mdx | 54 +++++++++++++++++++ .../ReactLeaflet/WMSLayer/index.stories.ts | 19 +++++++ src/stories/pages/WMSLayer/index.mdx | 4 +- 11 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/pages/ReactLeaflet/WMSLayer/WMSLayer.test.tsx create mode 100644 src/pages/ReactLeaflet/WMSLayer/WMSLayer.tsx create mode 100644 src/pages/ReactLeaflet/WMSLayer/styles.module.css create mode 100644 src/stories/pages/ReactLeaflet/WMSLayer/index.mdx create mode 100644 src/stories/pages/ReactLeaflet/WMSLayer/index.stories.ts diff --git a/src/pages/ReactLeaflet/BaseLayer/BaseLayer.tsx b/src/pages/ReactLeaflet/BaseLayer/BaseLayer.tsx index 989250c..eb1fd5e 100644 --- a/src/pages/ReactLeaflet/BaseLayer/BaseLayer.tsx +++ b/src/pages/ReactLeaflet/BaseLayer/BaseLayer.tsx @@ -14,6 +14,7 @@ const BaseLayer = (): JSX.Element => ( [52.50536, 5.10737], ]} crs={getCrsRd()} + attributionControl={false} > ( [52.50536, 5.10737], ]} crs={getCrsRd()} + attributionControl={false} > { [52.50536, 5.10737], ]} crs={getCrsRd()} + attributionControl={false} > { [52.50536, 5.10737], ]} crs={getCrsRd()} + attributionControl={false} > = ({ children }) => ( [52.37253554766886, 4.892099548064893], ]} crs={getCrsRd()} + attributionControl={false} > { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); +}); diff --git a/src/pages/ReactLeaflet/WMSLayer/WMSLayer.tsx b/src/pages/ReactLeaflet/WMSLayer/WMSLayer.tsx new file mode 100644 index 0000000..31e1f79 --- /dev/null +++ b/src/pages/ReactLeaflet/WMSLayer/WMSLayer.tsx @@ -0,0 +1,35 @@ +import L from 'leaflet'; +import { MapContainer, TileLayer, WMSTileLayer } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; + +const WMSLayer = (): JSX.Element => ( +
+ + + + +
+); + +export default WMSLayer; diff --git a/src/pages/ReactLeaflet/WMSLayer/styles.module.css b/src/pages/ReactLeaflet/WMSLayer/styles.module.css new file mode 100644 index 0000000..7a173cb --- /dev/null +++ b/src/pages/ReactLeaflet/WMSLayer/styles.module.css @@ -0,0 +1,8 @@ +.container { + height: 100%; + min-height: 100%; + + > div { + height: 100%; + } +} diff --git a/src/stories/pages/ReactLeaflet/WMSLayer/index.mdx b/src/stories/pages/ReactLeaflet/WMSLayer/index.mdx new file mode 100644 index 0000000..9ea871b --- /dev/null +++ b/src/stories/pages/ReactLeaflet/WMSLayer/index.mdx @@ -0,0 +1,54 @@ +import { Meta, Source } from '@storybook/blocks'; +import * as WMSLayerStories from './index.stories'; + + +import WMSLayer from '@/pages/ReactLeaflet/WMSLayer/WMSLayer?raw'; +import styles from '@/pages/ReactLeaflet/WMSLayer/styles.module.css?raw'; + + + +# Amsterdam WMSLayer + +## Requirements +- This example is built upon the [BaseMap component example](../?path=/docs/react-baselayer--docs). +- [See global requirements list](../?path=/docs/global-requirements--docs) + +## Description + +This is the Amsterdam WMS layer on a React Leaflet map. + +## Background + +### WMS layer + +The Web Map Service is a standard protocol developed by the Open Geospatial Consortium (OGC) for serving georeferenced map images generated by a map server from spatial data. These images are generated by a map server using data from a GIS database. + +React Leaflet's `WMSTileLayer` allows you to display raster maps from WMS servers. WMS servers generate map images in various formats (like PNG, JPEG) based on parameters sent in the request URL. These parameters can include layers, styles, geographic bounding boxes, coordinate reference systems (CRS), and output image format. + +## How to implement + +To accomplish the Amsterdam WMS layer, there are the following files: + +1. The React components + - [WMSLayer.tsx](#1-wmslayertsx) +2. The CSS styles (1 file) + * [styles.module.css](#1-stylesmodulecss) + + +## Usage + +The following files are required: + +### React Components + +#### 1. WMSLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + + diff --git a/src/stories/pages/ReactLeaflet/WMSLayer/index.stories.ts b/src/stories/pages/ReactLeaflet/WMSLayer/index.stories.ts new file mode 100644 index 0000000..b315d38 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/WMSLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import WMSLayer from '@/pages/ReactLeaflet/WMSLayer/WMSLayer'; + +const meta = { + title: 'React-Leaflet/WMSLayer', + component: WMSLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; diff --git a/src/stories/pages/WMSLayer/index.mdx b/src/stories/pages/WMSLayer/index.mdx index 612b326..2e63c1f 100644 --- a/src/stories/pages/WMSLayer/index.mdx +++ b/src/stories/pages/WMSLayer/index.mdx @@ -1,4 +1,4 @@ -import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import { Meta, Source } from '@storybook/blocks'; import * as WMSLayerStories from './index.stories'; import WMSLayer from '@/pages/WMSLayer/WMSLayer?raw'; import styles from '@/pages/WMSLayer/styles.module.css?raw'; @@ -33,7 +33,7 @@ useEffect(() => { layers: 'verblijfsobjecten_woonfunctie', format: 'image/svg+xml', transparent: true, - crs: getCrsRd(), + crs: getCrsRd() } ) .addTo(mapInstance); From d9b91ea86a711f8ea0c2547478c7c8b5a0e14296 Mon Sep 17 00:00:00 2001 From: Koolen - Wijkstra Date: Tue, 25 Jun 2024 23:41:50 +0200 Subject: [PATCH 17/34] Add test to see if icons are shown --- .../ReactLeaflet/WMSLayer/WMSLayer.test.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pages/ReactLeaflet/WMSLayer/WMSLayer.test.tsx b/src/pages/ReactLeaflet/WMSLayer/WMSLayer.test.tsx index 401ced0..647232b 100644 --- a/src/pages/ReactLeaflet/WMSLayer/WMSLayer.test.tsx +++ b/src/pages/ReactLeaflet/WMSLayer/WMSLayer.test.tsx @@ -1,10 +1,33 @@ import { describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; import { render } from '@testing-library/react'; import WMSLayer from '@/pages/ReactLeaflet/WMSLayer/WMSLayer'; +import { WMSTileLayer } from 'react-leaflet'; + +vi.mock('react-leaflet', async () => { + const actual = await vi.importActual('react-leaflet'); + + return { + ...actual, + WMSTileLayer: vi.fn(() =>
), + }; +}); describe('WMSLayer', () => { it('renders the component', () => { const { container } = render(); expect(container.firstChild).toBeDefined(); }); + it('renders a leaflet WMSLayer icon', () => { + render(); + expect(WMSTileLayer).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'https://map.data.amsterdam.nl/maps/adresseerbare_objecten?REQUEST=GetCapabilities&VERSION=1.1.0&SERVICE=wms', + layers: 'verblijfsobjecten_woonfunctie', + format: 'image/svg+xml', + transparent: true, + }), + {} + ); + }); }); From 8896b3e4d03a0cbd86603e747dcf804d9bc27300 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Wed, 3 Jul 2024 11:50:54 +0200 Subject: [PATCH 18/34] Use utility class to visually hide elements (#36) The `VisuallyHidden` component will soon be removed. --- src/components/ZoomControl/ZoomControl.tsx | 6 +++--- src/pages/Fullscreen/FullscreenPageFooter.tsx | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/ZoomControl/ZoomControl.tsx b/src/components/ZoomControl/ZoomControl.tsx index 509c6c1..7a7eaf3 100644 --- a/src/components/ZoomControl/ZoomControl.tsx +++ b/src/components/ZoomControl/ZoomControl.tsx @@ -1,4 +1,4 @@ -import { Button, Icon, VisuallyHidden } from '@amsterdam/design-system-react'; +import { Button, Icon } from '@amsterdam/design-system-react'; import { FunctionComponent } from 'react'; import { EnlargeIcon, @@ -25,11 +25,11 @@ const ZoomControl: FunctionComponent = () => { return (
diff --git a/src/pages/Fullscreen/FullscreenPageFooter.tsx b/src/pages/Fullscreen/FullscreenPageFooter.tsx index 511e4f3..d63248e 100644 --- a/src/pages/Fullscreen/FullscreenPageFooter.tsx +++ b/src/pages/Fullscreen/FullscreenPageFooter.tsx @@ -7,7 +7,6 @@ import { LinkList, PageMenu, Paragraph, - VisuallyHidden, } from '@amsterdam/design-system-react'; import { ChattingIcon, PhoneIcon } from '@amsterdam/design-system-react-icons'; @@ -16,9 +15,7 @@ export const FullscreenPageFooter = () => { <>