From 42ef6d8dc589d206168bf4213c6aa8952409c70b Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Tue, 21 May 2024 11:19:15 +0200 Subject: [PATCH 1/2] BaseLayer example setup for React-leaflet (#4) (#9) * BaseLayer example setup for React-leaflet * Updated react-leaflet baselayer docs --- package-lock.json | 148 ++++++++++-------- package.json | 3 +- .../ReactLeaflet/BaseLayer/index.test.tsx | 25 +++ src/pages/ReactLeaflet/BaseLayer/index.tsx | 27 ++++ .../ReactLeaflet/BaseLayer/styles.module.css | 8 + .../pages/ReactLeaflet/BaseLayer/index.mdx | 67 ++++++++ .../ReactLeaflet/BaseLayer/index.stories.ts | 19 +++ 7 files changed, 234 insertions(+), 63 deletions(-) create mode 100644 src/pages/ReactLeaflet/BaseLayer/index.test.tsx create mode 100644 src/pages/ReactLeaflet/BaseLayer/index.tsx create mode 100644 src/pages/ReactLeaflet/BaseLayer/styles.module.css create mode 100644 src/stories/pages/ReactLeaflet/BaseLayer/index.mdx create mode 100644 src/stories/pages/ReactLeaflet/BaseLayer/index.stories.ts diff --git a/package-lock.json b/package-lock.json index 1355bdb..e1e6b68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "leaflet": "^1.9.4", "proj4": "^2.11.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1" }, "devDependencies": { "@storybook/addon-essentials": "^8.1.0", @@ -3370,6 +3371,16 @@ } } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", @@ -6004,9 +6015,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz", - "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==", + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", "dev": true }, "node_modules/@types/mdx": { @@ -6143,16 +6154,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", - "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", + "integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/type-utils": "7.9.0", - "@typescript-eslint/utils": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/type-utils": "7.10.0", + "@typescript-eslint/utils": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6176,15 +6187,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", - "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz", + "integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "debug": "^4.3.4" }, "engines": { @@ -6204,13 +6215,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", - "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0" + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6221,13 +6232,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", - "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", + "integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/utils": "7.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6248,9 +6259,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", - "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6261,13 +6272,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", - "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -6301,15 +6312,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", - "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0" + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6323,12 +6334,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", - "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/types": "7.10.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7326,12 +7337,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -7473,9 +7484,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001618", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz", - "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==", + "version": "1.0.30001620", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", + "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", "dev": true, "funding": [ { @@ -8759,9 +8770,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.769", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.769.tgz", - "integrity": "sha512-bZu7p623NEA2rHTc9K1vykl57ektSPQYFFqQir8BOYf6EKOB+yIsbFB9Kpm7Cgt6tsLr9sRkqfqSZUw7LP1XxQ==", + "version": "1.4.776", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.776.tgz", + "integrity": "sha512-s694bi3+gUzlliqxjPHpa9NRTlhzTgB34aan+pVKZmOTGy2xoZXl+8E1B8i5p5rtev3PKMK/H4asgNejC+YHNg==", "dev": true }, "node_modules/emoji-regex": { @@ -8957,9 +8968,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz", - "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", "dev": true }, "node_modules/es-object-atoms": { @@ -9931,9 +9942,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -14457,6 +14468,19 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", diff --git a/package.json b/package.json index f01b92a..99b9905 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "leaflet": "^1.9.4", "proj4": "^2.11.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1" } } diff --git a/src/pages/ReactLeaflet/BaseLayer/index.test.tsx b/src/pages/ReactLeaflet/BaseLayer/index.test.tsx new file mode 100644 index 0000000..4230141 --- /dev/null +++ b/src/pages/ReactLeaflet/BaseLayer/index.test.tsx @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import BaseMap from './'; + +describe('BaseMap', () => { + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('uses the amsterdam base tile', () => { + const { container } = render(); + + // Only test on the less dynamic part of the URL + const imgSrc = ( + container.querySelector('.leaflet-tile-container img') as HTMLImageElement + )?.src.substring(0, 38); + + expect( + imgSrc.match( + /https:\/\/(t1)|(t2)|(t3)|(t4)\.data.amsterdam.nl\/topo_rd\//g + ) + ).not.toEqual(null); + }); +}); diff --git a/src/pages/ReactLeaflet/BaseLayer/index.tsx b/src/pages/ReactLeaflet/BaseLayer/index.tsx new file mode 100644 index 0000000..989250c --- /dev/null +++ b/src/pages/ReactLeaflet/BaseLayer/index.tsx @@ -0,0 +1,27 @@ +import L from 'leaflet'; +import { MapContainer, TileLayer } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; +import getCrsRd from '@/utils/getCrsRd'; +import styles from './styles.module.css'; + +const BaseLayer = (): JSX.Element => ( +
+ + + +
+); + +export default BaseLayer; diff --git a/src/pages/ReactLeaflet/BaseLayer/styles.module.css b/src/pages/ReactLeaflet/BaseLayer/styles.module.css new file mode 100644 index 0000000..7a173cb --- /dev/null +++ b/src/pages/ReactLeaflet/BaseLayer/styles.module.css @@ -0,0 +1,8 @@ +.container { + height: 100%; + min-height: 100%; + + > div { + height: 100%; + } +} diff --git a/src/stories/pages/ReactLeaflet/BaseLayer/index.mdx b/src/stories/pages/ReactLeaflet/BaseLayer/index.mdx new file mode 100644 index 0000000..5155cb4 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/BaseLayer/index.mdx @@ -0,0 +1,67 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as BaseLayerStories from './index.stories'; +import BaseLayer from '@/pages/ReactLeaflet/BaseLayer/index?raw'; +import styles from '@/pages/ReactLeaflet/BaseLayer/styles.module.css?raw'; +import getCrsRd from '@/utils/getCrsRd?raw'; + + + +# Amsterdam BaseLayer +## Requirements + +- [See global requirements list](../?path=/docs/global-requirements--docs) +- CRS handling ([utils/getCrsRd.ts](#1-getcrsrdts)) + +## Description + +This is the Amsterdam base/tile layer on a React-leaflet map. + +## Background + +### Amsterdam base/tile layer + +A [TileLayer](https://leafletjs.com/reference.html#tilelayer) is composed of images, such as satellite imagery, that are composed of square tiles mosaicked together in columns and rows, giving the layer the appearance that it is one continuous image. These layers have several levels of detail (LOD) that permit users to zoom in to any region of the map and load additional tiles that depict features in higher resolution at larger map scales. + +The datateam Geo makes various reference maps based on reference data from team BenK (Basis- en Kernregistraties). They are available in [various reference systems (Rijksdriehoek and Web Mercator)](../?path=/docs/coordinate-reference-systems-crs--docs) and the following visualizations: + +- Standard (standaard) +- Black and white (zwart-wit) +- Light (light) + +These tiles are hosted in a [Azure Blob Store](https://azure.microsoft.com/en-us/products/storage/blobs) and accessed via [Azure Front Door](https://azure.microsoft.com/nl-nl/products/frontdoor). The maptitles are made with [MapProxy](https://github.com/Amsterdam/mapproxy) based on [MapServer](https://github.com/Amsterdam/mapserver). + +### 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 + * [BaseLayer.tsx](#1-baselayertsx) +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. BaseLayer.tsx + + + +### CSS Styling + +#### 1. styles.module.css + + + +### Utils + +#### 1. getCrsRd.ts + + diff --git a/src/stories/pages/ReactLeaflet/BaseLayer/index.stories.ts b/src/stories/pages/ReactLeaflet/BaseLayer/index.stories.ts new file mode 100644 index 0000000..f6c6595 --- /dev/null +++ b/src/stories/pages/ReactLeaflet/BaseLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import BaseLayer from '@/pages/ReactLeaflet/BaseLayer'; + +const meta = { + title: 'React-Leaflet/BaseLayer', + component: BaseLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; From 1d4da4f69b74b7a303019a17f48ea88cead95a05 Mon Sep 17 00:00:00 2001 From: Thomas Mills Date: Thu, 23 May 2024 15:59:44 +0200 Subject: [PATCH 2/2] Feature/scts 2 pointer marker (#11) (#20) * 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 = {};