Skip to content

Commit

Permalink
Merge pull request #195 from thedirtyfew/1.0.5
Browse files Browse the repository at this point in the history
1.0.5
  • Loading branch information
emilhe authored Aug 27, 2023
2 parents 6829203 + 3fe0881 commit 0ec6337
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 16 deletions.
24 changes: 20 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,38 @@

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)
- 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

### 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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
39 changes: 33 additions & 6 deletions src/ts/components/MapContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -34,14 +52,23 @@ type Props = Modify<MapContainerProps, {
/**
* Change the value to force map size invalidation. [DL]
*/
invalidateSize?: string | number | object
invalidateSize?: string | number | object;

/**
* If true (default), zoom, center, and bounds properties are updated on whenReady/moveend. [DL]
*/
trackViewport?: boolean;
} & DashComponent & ClickEvents & LoadEvents & KeyboardEvents>;

/**
* 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 (
Expand Down
29 changes: 29 additions & 0 deletions src/ts/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,31 @@ 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 = {
/**
* 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;
Expand All @@ -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
Expand Down
54 changes: 51 additions & 3 deletions src/ts/react-leaflet/GeoJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down

0 comments on commit 0ec6337

Please sign in to comment.