From 7006d1c52cbfcd965da5984029f12fab80da4d91 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sat, 26 Aug 2023 22:11:14 +0200 Subject: [PATCH 1/5] GeoJSON cluster rendering delta update --- src/ts/react-leaflet/GeoJSON.ts | 54 +++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/ts/react-leaflet/GeoJSON.ts b/src/ts/react-leaflet/GeoJSON.ts index 1d77abc..23d4277 100644 --- a/src/ts/react-leaflet/GeoJSON.ts +++ b/src/ts/react-leaflet/GeoJSON.ts @@ -186,14 +186,18 @@ async function _fetchGeoJSON(props) { features: [geojson] } } - // Add cluster properties if they are missing. - geojson.features = geojson.features.map(feature => { + geojson.features = geojson.features.map((feature, index) => { if (!feature.properties) { feature["properties"] = {} } + // Add cluster property if missing. if (!feature.properties.cluster) { feature["properties"]["cluster"] = false } + // Add id property if missing. + if (!feature.properties.id) { + feature["properties"]["id"] = index + } return feature }); return geojson @@ -233,6 +237,10 @@ function _redrawClusters(instance, props, map, index, toSpiderfyRef) { } catch (err) { return } + // Reduce clusters to delta, i.e. the ones that need to be added. + if(Object.keys(instance._layers).length > 0){ + clusters = deltaClusters(instance, clusters) + } // Update the data. if (props.spiderfyOnMaxZoom && toSpiderfyRef.current) { // If zoom level has changes, drop the spiderfy state. @@ -245,10 +253,50 @@ function _redrawClusters(instance, props, map, index, toSpiderfyRef) { toSpiderfyRef.current.zoom = zoom; } } - instance.clearLayers(); + // If the data hasn't changed, just return. + if(clusters.length === 0){return;} + // Add clusters to the map. instance.addData(clusters); } +function deltaClusters(instance, clusters){ + const layers = instance._layers; + // If there is no data on the map, delta = all. + if(Object.keys(layers).length == 0){ + return clusters; + } + // Allocate new/unchanged arrays. + const newIds = clusters.filter(c => !c.properties.cluster).map(c => c.properties.id); + const newClusterIds = clusters.filter(c => c.properties.cluster).map(c => c.properties.cluster_id); + const unchangedIds = []; + const unchangedClusterIds = []; + // Loop layers, removing any layer not to be shown. + for (const l of Object.values(layers)) { + const props = (l as any).feature.properties; + if (props.cluster) { + // If the feature is still there, don't re-add it. + if (newClusterIds.includes(props.cluster_id)) { + unchangedClusterIds.push(props.cluster_id) + } + // Otherwise, remove it. + else { + instance.removeLayer(l); + } + } else { + // If the feature is still there, don't re-add it. + if (newIds.includes(props.id)) { + unchangedIds.push(props.id) + } + // Otherwise, remove it. + else { + instance.removeLayer(l); + } + } + } + // Return delta clusters. + return clusters.filter(c => c.properties.cluster? !unchangedClusterIds.includes(c.properties.cluster_id) : !unchangedIds.includes(c.properties.id)) +} + function _redrawGeoJSON(instance, props, map, geojson) { instance.clearLayers(); instance.addData(geojson); From 3ea5d5aa8505ecee4fe71164c0dc30d2dcd1dce4 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sat, 26 Aug 2023 22:11:31 +0200 Subject: [PATCH 2/5] Add data props to event defs --- src/ts/props.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/ts/props.ts b/src/ts/props.ts index c45a4bf..b78c1c4 100644 --- a/src/ts/props.ts +++ b/src/ts/props.ts @@ -75,6 +75,15 @@ export type SingleClickEvent = { * An integer that represents the number of times that this element has been clicked on. */ 'n_clicks'?: number; + + /** + * An object holding data related to the click event. Typing is indicative. + */ + 'clickData'?: { + 'latlng': number[], + 'layerPoint': number[], + 'containerPoint': number[] + }; }; export type DoubleClickEvent = { @@ -82,6 +91,15 @@ export type DoubleClickEvent = { * An integer that represents the number of times that this element has been double-clicked on. */ 'n_dblclicks'?: number; + + /** + * An object holding data related to the double click event. Typing is indicative. + */ + 'dblclickData'?: { + 'latlng': number[], + 'layerPoint': number[], + 'containerPoint': number[] + }; }; export type ClickEvents = SingleClickEvent & DoubleClickEvent; @@ -91,6 +109,17 @@ export type KeyboardEvents = { * An integer that represents the number of times that the keyboard has been pressed. */ 'n_keydowns'?: number; + + /** + * An object holding data related to the keydown event. Typing is indicative. + */ + 'keydownData'?: { + 'key': string, + 'ctrlKey': boolean, + 'metaKey': boolean, + 'shiftKey': boolean, + 'repeat': boolean + }; }; //#endregion From 2c3353148a8cd657c35d704fdadec5981b188d27 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sun, 27 Aug 2023 14:30:16 +0200 Subject: [PATCH 3/5] Getting ready for 1.0.5 --- CHANGELOG.md | 23 +++++++++++++++++++---- package.json | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0eb920..9a6d8d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,22 +2,37 @@ All notable changes to this project will be documented in this file. -## [1.0.4] - 2023-08-25 +## [1.0.5] - 2023-08-27 + +### Changed + +- When clustering is enabled, the `GeoJSON` component now performs _delta_ updates, i.e. features that remain within the viewport are no longer redraw on pan/zoom. Fixes [#180](https://github.com/thedirtyfew/dash-leaflet/issues/180) + +## [1.0.4] - 2023-08-26 + +### Added + +- Add option to specify custom units in the `MeasureControl`. Fixes [#130](https://github.com/thedirtyfew/dash-leaflet/issues/130) +- Add `invalidateSize` option to the map component. Fixes [#73](https://github.com/thedirtyfew/dash-leaflet/issues/73) + +### Changed + +- The `GeoJSON` component now supports single features (in addition to feature collections). Fixes [#160](https://github.com/thedirtyfew/dash-leaflet/issues/160) + +## [1.0.0] - 2023-08-25 ### Added - New event handling system, allowing much greater flexibility - Basic unit tests for all components (rendering or better) - Added (separate) `Attribution` component -- Add option to specify custom units in the `MeasureControl` -- Add `invalidateSize` property to Map component ### Changed - Library completely rewritten in TypeScript based on React Leaflet v4 - Dependencies updated (incl. React version bump), npm now reports *0 vulnerabilities* +- Various fixes, incl. but not limited to [#193](https://github.com/thedirtyfew/dash-leaflet/issues/193), [#192](https://github.com/thedirtyfew/dash-leaflet/issues/192), [#189](https://github.com/thedirtyfew/dash-leaflet/issues/189), [#184](https://github.com/thedirtyfew/dash-leaflet/issues/184), [#178](https://github.com/thedirtyfew/dash-leaflet/issues/178) - The `GeoJSON` component is now loaded async, bringing the main asset < 300 kB -- The `GeoJSON` component now supports single features ### Removed diff --git a/package.json b/package.json index c8c25a5..74b9065 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dash-leaflet", - "version": "1.0.4", + "version": "1.0.5", "description": "Dash Leaflet is a light wrapper around React-Leaflet. The syntax is similar to other Dash components, with naming conventions following the React-Leaflet API.", "main": "index.ts", "repository": { From a32040af2828176b6d21b9181ff4f6b17f05b25f Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sun, 27 Aug 2023 16:34:11 +0200 Subject: [PATCH 4/5] Add map viewport tracking --- CHANGELOG.md | 1 + src/ts/components/MapContainer.tsx | 39 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6d8d8..8853b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Changed - When clustering is enabled, the `GeoJSON` component now performs _delta_ updates, i.e. features that remain within the viewport are no longer redraw on pan/zoom. Fixes [#180](https://github.com/thedirtyfew/dash-leaflet/issues/180) +- The map viewport is now tracked (through the `zoom`, `center`, and `bounds` properties) unless `trackViewport` it set to `False` ## [1.0.4] - 2023-08-26 diff --git a/src/ts/components/MapContainer.tsx b/src/ts/components/MapContainer.tsx index 5d17779..52ef923 100644 --- a/src/ts/components/MapContainer.tsx +++ b/src/ts/components/MapContainer.tsx @@ -5,12 +5,30 @@ import {resolveCRS, resolveEventHandlers, resolveRenderer} from '../utils'; import '../../../node_modules/leaflet/dist/leaflet.css'; import {ClickEvents, KeyboardEvents, LoadEvents, MapContainerProps, DashComponent, Modify} from "../props"; +const trackViewport = (map, props) => { + const bounds = map.getBounds() + props.setProps({ + zoom: map.zoom, center: map.center, + bounds: [[bounds.getSouth(), bounds.getWest()], [bounds.getNorth(), bounds.getEast()]] + }) +} + function EventSubscriber(props) { - const map = useMapEvents(resolveEventHandlers(props, ["click", "dblclick", "keydown", "load"])) + const eventHandlers = resolveEventHandlers(props, ["click", "dblclick", "keydown", "load"]) + const map = useMapEvents(Object.assign(eventHandlers, !props.trackViewport? {} : { + moveend: (e) => { + trackViewport(map, props); + } + })); + if(props.trackViewport) { + map.whenReady(() => { + trackViewport(map, props); + }) + } useEffect(function invalidateSize(){ if(props.invalidateSize !== undefined){ - map.invalidateSize() + map.invalidateSize(); } }, [props.invalidateSize]) @@ -34,14 +52,23 @@ type Props = Modify; /** - * Component description + * The MapContainer component is responsible for creating the Leaflet Map instance and providing it to its child components, using a React Context. */ -const MapContainer = ({crs="EPSG3857", renderer=undefined, ...props}: Props) => { - const target = {crs: resolveCRS(crs), renderer: resolveRenderer(renderer)} // map from string repr of CRS to actual object +const MapContainer = ({crs="EPSG3857", renderer=undefined, trackViewport=true, ...props}: Props) => { + const target = { + crs: resolveCRS(crs), // map from string repr of CRS to actual object + renderer: resolveRenderer(renderer), // map from string repr of Renderer to actual object + trackViewport: trackViewport + } const nProps = Object.assign(target, props); // Add a custom event subscriber that exposes events to Dash. return ( From 3fe08817707b63366ee1f0c81707843d436a6340 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sun, 27 Aug 2023 16:35:01 +0200 Subject: [PATCH 5/5] Update lock file --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6155933..276b368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dash-leaflet", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dash-leaflet", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "dependencies": { "base64-js": "^1.5.1",