diff --git a/package.json b/package.json index b2995e06..aae95f12 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "homepage": "http://wiki.openstreetmap.org/wiki/UMap", "dependencies": { "@tmcw/togeojson": "^5.8.0", + "chroma-js": "^2.4.2", "csv2geojson": "5.1.1", "dompurify": "^3.0.3", "georsstogeojson": "^0.1.0", diff --git a/scripts/vendorsjs.sh b/scripts/vendorsjs.sh index 751132fc..b12bd27b 100755 --- a/scripts/vendorsjs.sh +++ b/scripts/vendorsjs.sh @@ -26,5 +26,6 @@ mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js uma mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/dist/L.Control.Locate.css umap/static/umap/vendors/locatecontrol/ mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/src/L.Control.Locate.js umap/static/umap/vendors/locatecontrol/ mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.js umap/static/umap/vendors/dompurify/ +mkdir -p umap/static/umap/vendors/chroma/ && cp -r node_modules/chroma-js/chroma.min.js umap/static/umap/vendors/chroma/ echo 'Done!' diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 7de50be8..7e2428d9 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -283,7 +283,7 @@ L.U.FeatureMixin = { } else if (L.Util.usableOption(this.properties._umap_options, option)) { value = this.properties._umap_options[option] } else if (this.datalayer) { - value = this.datalayer.getOption(option) + value = this.datalayer.getOption(option, this) } else { value = this.map.getOption(option) } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 0d983e67..f3f022b2 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -379,6 +379,7 @@ L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({ ['Default', L._('Default')], ['Cluster', L._('Clustered')], ['Heat', L._('Heatmap')], + ['Choropleth', L._('Choropleth')], ], }) diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 21d41b52..16a3d75b 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -106,6 +106,94 @@ L.U.Layer.Cluster = L.MarkerClusterGroup.extend({ }, }) +L.U.Layer.Choropleth = L.FeatureGroup.extend({ + _type: 'Choropleth', + includes: [L.U.Layer], + canBrowse: true, + + initialize: function (datalayer) { + this.datalayer = datalayer + if (!L.Util.isObject(this.datalayer.options.choropleth)) { + this.datalayer.options.choropleth = {} + } + L.FeatureGroup.prototype.initialize.call( + this, + [], + this.datalayer.options.choropleth + ) + }, + + computeLimits: function () { + const values = [] + this.datalayer.eachLayer((layer) => values.push(layer.properties.density)) + this.options.limits = chroma.limits( + values, + this.datalayer.options.choropleth.mode || 'q', + this.datalayer.options.choropleth.steps || 5 + ) + const color = this.datalayer.getOption('color') + this.options.colors = chroma + .scale(['white', color]) + .colors(this.options.limits.length) + }, + + getColor: function (feature) { + if (!feature) return // FIXME shold not happen + const featureValue = feature.properties.density + // Find the bucket/step/limit that this value is less than and give it that color + for (let i = 0; i < this.options.limits.length; i++) { + if (featureValue <= this.options.limits[i]) { + return this.options.colors[i] + } + } + }, + + getOption: function (option, feature) { + if (option === 'fillColor' || option === 'color') return this.getColor(feature) + }, + + addLayer: function (layer) { + this.computeLimits() + L.FeatureGroup.prototype.addLayer.call(this, layer) + }, + + removeLayer: function (layer) { + this.computeLimits() + L.FeatureGroup.prototype.removeLayer.call(this, layer) + }, + + onAdd: function (map) { + this.computeLimits() + L.FeatureGroup.prototype.onAdd.call(this, map) + }, + + getEditableOptions: function () { + return [ + [ + 'options.choropleth.steps', + { + handler: 'IntInput', + placeholder: L._('Choropleth steps'), + helpText: L._('Choropleth steps (default 5)'), + }, + ], + [ + 'options.choropleth.mode', + { + handler: 'Select', + selectOptions: [ + ['q', L._('quantile')], + ['e', L._('equidistant')], + ['l', L._('logarithmic')], + ['k', L._('k-mean')], + ], + helpText: L._('Choropleth mode'), + }, + ], + ] + }, +}) + L.U.Layer.Heat = L.HeatLayer.extend({ _type: 'Heat', includes: [L.U.Layer], @@ -897,8 +985,6 @@ L.U.DataLayer = L.Evented.extend({ 'options.fillOpacity', ] - shapeOptions = shapeOptions.concat(this.layer.getEditableOptions()) - const redrawCallback = function (field) { this.hide() this.layer.postUpdate(field) @@ -1050,7 +1136,11 @@ L.U.DataLayer = L.Evented.extend({ this.map.ui.openPanel({ data: { html: container }, className: 'dark' }) }, - getOption: function (option) { + getOption: function (option, feature) { + if (this.layer && this.layer.getOption) { + const value = this.layer.getOption(option, feature) + if (value) return value + } if (L.Util.usableOption(this.options, option)) return this.options[option] else return this.map.getOption(option) }, diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index e2da8669..609eff2d 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -24,6 +24,7 @@ + {% endcompress %} {% if locale %}{% endif %} {% compress js %}