From db66689705ddc295ee9855e396a9c91c2636e593 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 4 Jan 2021 18:55:10 +0100 Subject: [PATCH] Add working clustering! --- Customizations/JSON/LayoutConfig.ts | 38 ++++++++---- Customizations/JSON/LayoutConfigJson.ts | 7 +++ InitUiElements.ts | 3 +- Logic/FeatureSource/FilteringFeatureSource.ts | 3 - UI/BigComponents/Attribution.ts | 5 +- UI/ShowDataLayer.ts | 30 ++++++---- UI/SpecialVisualizations.ts | 1 - index.html | 2 + package-lock.json | 8 +++ package.json | 1 + vendor/MarkerCluster.Default.css | 60 +++++++++++++++++++ vendor/MarkerCluster.css | 14 +++++ 12 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 vendor/MarkerCluster.Default.css create mode 100644 vendor/MarkerCluster.css diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index b914a79..e849206 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -24,6 +24,8 @@ export default class LayoutConfig { public readonly roamingRenderings: TagRenderingConfig[]; public readonly defaultBackgroundId?: string; public readonly layers: LayerConfig[]; + public readonly clustering: { }; + public readonly hideFromOverview: boolean; public readonly enableUserBadge: boolean; public readonly enableShareScreen: boolean; @@ -32,12 +34,12 @@ export default class LayoutConfig { public readonly enableLayers: boolean; public readonly enableSearch: boolean; public readonly enableGeolocation: boolean; - public readonly enableBackgroundLayerSelection: boolean; + public readonly enableBackgroundLayerSelection: boolean; public readonly customCss?: string; - constructor(json: LayoutConfigJson, context?:string) { + constructor(json: LayoutConfigJson, context?: string) { this.id = json.id; - context = (context ?? "")+"."+this.id; + context = (context ?? "") + "." + this.id; this.maintainer = json.maintainer; this.changesetmessage = json.changesetmessage; this.version = json.version; @@ -47,16 +49,16 @@ export default class LayoutConfig { } else { this.language = json.language; } - if(json.title === undefined){ - throw "Title not defined in "+this.id; + if (json.title === undefined) { + throw "Title not defined in " + this.id; } - if(json.description === undefined){ - throw "Description not defined in "+this.id; + if (json.description === undefined) { + throw "Description not defined in " + this.id; } - this.title = new Translation(json.title, context+".title"); - this.description = new Translation(json.description, context+".description"); - this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context+".shortdescription"); - this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*":""}, context) : new Translation(json.descriptionTail, context+".descriptionTail"); + this.title = new Translation(json.title, context + ".title"); + this.description = new Translation(json.description, context + ".description"); + this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription"); + this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context) : new Translation(json.descriptionTail, context + ".descriptionTail"); this.icon = json.icon; this.socialImage = json.socialImage; this.startZoom = json.startZoom; @@ -80,8 +82,20 @@ export default class LayoutConfig { } else { throw "Unkown fixed layer " + layer; } - return new LayerConfig(layer, this.roamingRenderings,`${this.id}.layers[${i}]`); + return new LayerConfig(layer, this.roamingRenderings, `${this.id}.layers[${i}]`); }); + + + this.clustering = json.clustering ?? false; + + if(this.clustering){ + for (const layer of this.layers) { + if(layer.wayHandling !== LayerConfig.WAYHANDLING_CENTER_ONLY){ + throw "In order to allow clustering, every layer must be set to CENTER_ONLY"; + } + } + } + this.hideFromOverview = json.hideFromOverview ?? false; this.enableUserBadge = json.enableUserBadge ?? true; diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index db50372..2e2a89f 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -123,6 +123,13 @@ export interface LayoutConfigJson { */ layers: (LayerConfigJson | string)[], + /** + * If defined, data will be clustered. + */ + clustering: { + maxZoom?: number + }, + /** * The URL of a custom CSS stylesheet to modify the layout */ diff --git a/InitUiElements.ts b/InitUiElements.ts index 9193142..0be4f41 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -358,7 +358,8 @@ export class InitUiElements { State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId)); - const attr = new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.leafletMap); + const attr = new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, + State.state.leafletMap); const bm = new Basemap("leafletDiv", State.state.locationControl, State.state.backgroundLayer, diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 843ca28..9ab1097 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -40,14 +40,11 @@ export default class FilteringFeatureSource implements FeatureSource { for (const layer of layers) { layerDict[layer.layerDef.id] = layer; layer.isDisplayed.addCallback(() => { - console.log("Updating due to layer change") update()}) } upstream.features.addCallback(() => { - console.log("Updating due to upstream change") update()}); location.map(l => l.zoom).addCallback(() => { - console.log("UPdating due to zoom level change") update();}); diff --git a/UI/BigComponents/Attribution.ts b/UI/BigComponents/Attribution.ts index a419a1d..6c98d17 100644 --- a/UI/BigComponents/Attribution.ts +++ b/UI/BigComponents/Attribution.ts @@ -8,18 +8,19 @@ import Constants from "../../Models/Constants"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import Loc from "../../Models/Loc"; import LeafletMap from "../../Models/LeafletMap"; +import * as L from "leaflet" export default class Attribution extends UIElement { private readonly _location: UIEventSource; private readonly _layoutToUse: UIEventSource; private readonly _userDetails: UIEventSource; - private readonly _leafletMap: UIEventSource; + private readonly _leafletMap: UIEventSource; constructor(location: UIEventSource, userDetails: UIEventSource, layoutToUse: UIEventSource, - leafletMap: UIEventSource) { + leafletMap: UIEventSource) { super(location); this._layoutToUse = layoutToUse; this.ListenTo(layoutToUse); diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 55c9957..6ad8e2d 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -3,12 +3,15 @@ */ import {UIEventSource} from "../Logic/UIEventSource"; import * as L from "leaflet" +import "leaflet.markercluster" import LayerConfig from "../Customizations/JSON/LayerConfig"; import State from "../State"; import LazyElement from "./Base/LazyElement"; import Hash from "../Logic/Web/Hash"; import {GeoOperations} from "../Logic/GeoOperations"; import FeatureInfoBox from "./Popup/FeatureInfoBox"; +import LayoutConfig from "../Customizations/JSON/LayoutConfig"; + export default class ShowDataLayer { @@ -17,15 +20,14 @@ export default class ShowDataLayer { constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, - layers: { layerDef: LayerConfig, isDisplayed: UIEventSource }[]) { + layoutToUse: LayoutConfig) { this._leafletMap = leafletMap; const self = this; - let oldGeoLayer: L.Layer = undefined; this._layerDict = {}; - for (const layer of layers) { - this._layerDict[layer.layerDef.id] = layer.layerDef; + for (const layer of layoutToUse.layers) { + this._layerDict[layer.id] = layer; } function update() { @@ -38,7 +40,13 @@ export default class ShowDataLayer { const mp = leafletMap.data; const feats = features.data.map(ff => ff.feature); - const geoLayer = self.CreateGeojsonLayer(feats); + let geoLayer = self.CreateGeojsonLayer(feats) + const cl = window["L"]; + const cluster = cl.markerClusterGroup(); + cluster.addLayer(geoLayer); + geoLayer = cluster; + + if (oldGeoLayer) { mp.removeLayer(oldGeoLayer); } @@ -65,7 +73,7 @@ export default class ShowDataLayer { // Click handling is done in the next step const tagSource = State.state.allElements.getEventSourceFor(feature); - const layer : LayerConfig = this._layerDict[feature._matching_layer_id]; + const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)); return L.marker(latLng, { @@ -79,14 +87,14 @@ export default class ShowDataLayer { }) }); } - - private postProcessFeature(feature, leafletLayer: L.Layer){ - const layer : LayerConfig = this._layerDict[feature._matching_layer_id]; + + private postProcessFeature(feature, leafletLayer: L.Layer) { + const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) { // No popup action defined -> Don't do anything return; } - + const popup = L.popup({ autoPan: true, closeOnEscapeKey: true, @@ -106,7 +114,7 @@ export default class ShowDataLayer { uiElement.Activate(); State.state.selectedElement.setData(feature); }); - + if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) { // This element is in the URL, so this is a share link diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 8814242..41d3dd2 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -171,7 +171,6 @@ export default class SpecialVisualizations { }], constr: (state: State,tags, args) => { const tgs = tags.data; - console.log("Args[0]", args[0]) let subject = tgs.name ?? ""; if (args[0] !== undefined && args[0] !== "") { subject = args[0]; diff --git a/index.html b/index.html index c1aa020..20b3ee6 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,8 @@ + + diff --git a/package-lock.json b/package-lock.json index 7016245..f92e274 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3581,6 +3581,14 @@ "@types/geojson": "*" } }, + "@types/leaflet-markercluster": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/leaflet-markercluster/-/leaflet-markercluster-1.0.3.tgz", + "integrity": "sha1-ZBUb5FP2SQ6HUVAEgt65YQZOeCw=", + "requires": { + "@types/leaflet": "*" + } + }, "@types/leaflet-providers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/leaflet-providers/-/leaflet-providers-1.2.0.tgz", diff --git a/package.json b/package.json index 5e604d7..c1f9dd1 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "author": "pietervdvn", "license": "GPL", "dependencies": { + "@types/leaflet-markercluster": "^1.0.3", "@types/leaflet-providers": "^1.2.0", "@types/leaflet.markercluster": "^1.4.3", "country-language": "^0.1.7", diff --git a/vendor/MarkerCluster.Default.css b/vendor/MarkerCluster.Default.css new file mode 100644 index 0000000..bbc8c9f --- /dev/null +++ b/vendor/MarkerCluster.Default.css @@ -0,0 +1,60 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); + } +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); + } + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); + } +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); + } + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); + } +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); + } + + /* IE 6-8 fallback colors */ +.leaflet-oldie .marker-cluster-small { + background-color: rgb(181, 226, 140); + } +.leaflet-oldie .marker-cluster-small div { + background-color: rgb(110, 204, 57); + } + +.leaflet-oldie .marker-cluster-medium { + background-color: rgb(241, 211, 87); + } +.leaflet-oldie .marker-cluster-medium div { + background-color: rgb(240, 194, 12); + } + +.leaflet-oldie .marker-cluster-large { + background-color: rgb(253, 156, 115); + } +.leaflet-oldie .marker-cluster-large div { + background-color: rgb(241, 128, 23); +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; + } +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.marker-cluster span { + line-height: 30px; + } \ No newline at end of file diff --git a/vendor/MarkerCluster.css b/vendor/MarkerCluster.css new file mode 100644 index 0000000..c60d71b --- /dev/null +++ b/vendor/MarkerCluster.css @@ -0,0 +1,14 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; + -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; + -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; + transition: transform 0.3s ease-out, opacity 0.3s ease-in; +} + +.leaflet-cluster-spider-leg { + /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ + -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; + -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; + -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; + transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; +}