diff --git a/package-lock.json b/package-lock.json index fb4736f..98ab76a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2316,8 +2316,13 @@ "optional": true, "requires": { "esri-leaflet": "^2.0.0", - "leaflet": "^1.0.0-rc.3", - "leaflet.markercluster": "git+https://github.com/Leaflet/Leaflet.markercluster.git#336fec3242091c2a01ff3867b39a5f836f9b1a64" + "leaflet": "^1.0.0-rc.3" + }, + "dependencies": { + "leaflet.markercluster": { + "version": "git+https://github.com/Leaflet/Leaflet.markercluster.git#336fec3242091c2a01ff3867b39a5f836f9b1a64", + "from": "git+https://github.com/Leaflet/Leaflet.markercluster.git#336fec3242091c2a01ff3867b39a5f836f9b1a64" + } } }, "estraverse": { @@ -2707,7 +2712,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2731,13 +2737,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2754,19 +2762,22 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2897,7 +2908,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2911,6 +2923,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2927,6 +2940,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2935,13 +2949,15 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2962,6 +2978,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3050,7 +3067,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3064,6 +3082,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3159,7 +3178,8 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3201,6 +3221,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3222,6 +3243,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3270,13 +3292,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true + "dev": true, + "optional": true } } }, @@ -4708,11 +4732,6 @@ "leaflet": "^1.0.0" } }, - "leaflet.markercluster": { - "version": "git+https://github.com/Leaflet/Leaflet.markercluster.git#336fec3242091c2a01ff3867b39a5f836f9b1a64", - "from": "git+https://github.com/Leaflet/Leaflet.markercluster.git#v1.0.0-rc.1", - "optional": true - }, "leven": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/leven/-/leven-1.0.2.tgz", diff --git a/package.json b/package.json index 5397f63..76493a6 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "pretest": "npm run build", "release": "./scripts/release.sh", "start": "watch 'npm run build' src & http-server -p 5000 -c-1 -o", + "start-with-double-quotes": "watch \"npm run build\" src & http-server -p 5000 -c-1 -o", "test": "npm run lint && karma start", "test:ci": "npm run lint && karma start --browsers Chrome_travis_ci" } diff --git a/src/EsriLeafletRenderers.js b/src/EsriLeafletRenderers.js index 3a5015f..7f7665a 100644 --- a/src/EsriLeafletRenderers.js +++ b/src/EsriLeafletRenderers.js @@ -5,6 +5,10 @@ export { SimpleRenderer, simpleRenderer } from './Renderers/SimpleRenderer'; export { ClassBreaksRenderer, classBreaksRenderer } from './Renderers/ClassBreaksRenderer'; export { UniqueValueRenderer, uniqueValueRenderer } from './Renderers/UniqueValueRenderer'; +export { LabelRenderer } from './Labels/LabelRenderer'; +export { SimpleLabelRenderer } from './Labels/SimpleLabelRenderer'; +export { scalingToZoom } from './Scaling/ScalingToZoom'; + export { Symbol } from './Symbols/Symbol'; export { PointSymbol, pointSymbol } from './Symbols/PointSymbol'; export { LineSymbol, lineSymbol } from './Symbols/LineSymbol'; diff --git a/src/FeatureLayerHook.js b/src/FeatureLayerHook.js index a3b4584..99d6087 100644 --- a/src/FeatureLayerHook.js +++ b/src/FeatureLayerHook.js @@ -3,11 +3,82 @@ import Esri from 'esri-leaflet'; import classBreaksRenderer from './Renderers/ClassBreaksRenderer'; import uniqueValueRenderer from './Renderers/UniqueValueRenderer'; import simpleRenderer from './Renderers/SimpleRenderer'; +import simpleLabelRenderer from './Labels/SimpleLabelRenderer'; +import scalingToZoom from './Scaling/ScalingToZoom'; function wireUpRenderers () { if (this.options.ignoreRenderer) { return; } + + this.on('createfeature', function (e) { + if (this._hasLabelRenderer) { + var feature = e.feature; + + var labelLayer = L.GeoJSON.geometryToLayer(feature, this._labelLayer.options); + + if (!labelLayer) { + console.warn('invalid GeoJSON encountered'); + } else { + labelLayer.feature = feature; + + var featureId = feature.id.toString(); + // only add the feature if it does not already exist on the map + if (!this._labelLayerIds[featureId]) { + this._labelLayerIds[featureId] = labelLayer; + } + labelLayer.addEventParent(this); + this._labelLayer.bringToFront(); + + if (this._labelsShouldBeVisible()) { + this._map.addLayer(labelLayer); + } + } + } + }); + + this.on('removefeature', function (e) { + if (this._hasLabelRenderer) { + var layer = this._labelLayerIds[e.feature.id]; + if (layer) { + this._map.removeLayer(layer); + } + if (layer && e.permanent) { + delete this._labelLayerIds[e.feature.id]; + } + } + }); + + this.on('addfeature ', function (e) { + if (this._hasLabelRenderer) { + var layer = this._labelLayerIds[e.feature.id]; + if (layer) { + if (this._labelsShouldBeVisible()) { + this._map.addLayer(layer); + } + } + } + }); + + this._labelsShouldBeVisible = function () { + var currentZoom = this._map.getZoom(); + if (this._labelLayerMaxZoom) { + if (this._labelLayerMinZoom) { + if (currentZoom >= this._labelLayerMinZoom && currentZoom <= this._labelLayerMaxZoom) { + return true; + } + return false; + } + + return currentZoom <= this._labelLayerMaxZoom; + } + + if (this._labelLayerMinZoom) { + return currentZoom >= this._labelLayerMinZoom; + } + return true; + }; + var oldOnAdd = L.Util.bind(this.onAdd, this); var oldUnbindPopup = L.Util.bind(this.unbindPopup, this); var oldOnRemove = L.Util.bind(this.onRemove, this); @@ -29,8 +100,15 @@ function wireUpRenderers () { } this._setRenderers(response); + if (response.hasLabels) { + this._setLabelRenderers(response); + } + oldOnAdd(map); this._addPointLayer(map); + this._addLabelLayer(map); + + map.on('zoomend', this._handleZoomChangeForLabels, this); } }, this); }; @@ -43,6 +121,8 @@ function wireUpRenderers () { map.removeLayer(pointLayers[i]); } } + + map.off('zoomend', this._handleZoomChangeForLabels, this); }; this.unbindPopup = function () { @@ -55,6 +135,18 @@ function wireUpRenderers () { } }; + this._handleZoomChangeForLabels = function () { + if (this._labelsShouldBeVisible()) { + for (var i in this._labelLayerIds) { + this._map.addLayer(this._labelLayerIds[i]); + } + } else { + for (var j in this._labelLayerIds) { + this._map.removeLayer(this._labelLayerIds[j]); + } + } + }; + this._addPointLayer = function (map) { if (this._pointLayer) { this._pointLayer.addTo(map); @@ -62,6 +154,13 @@ function wireUpRenderers () { } }; + this._addLabelLayer = function (map) { + if (this._labelLayer) { + this._labelLayer.addTo(map); + this._labelLayer.bringToFront(); + } + }; + this._createPointLayer = function () { if (!this._pointLayer) { this._pointLayer = L.geoJson(); @@ -77,6 +176,14 @@ function wireUpRenderers () { } }; + this._createLabelLayer = function () { + if (!this._labelLayer) { + this._labelLayer = L.geoJson(); + // store the feature ids that have already been added to the map + this._labelLayerIds = {}; + } + }; + this.createNewLayer = function (geojson) { var fLayer = L.GeoJSON.geometryToLayer(geojson, this.options); @@ -119,7 +226,7 @@ function wireUpRenderers () { p1 = pts[i]; p2 = pts[j]; twicearea += p1[0] * p2[1]; twicearea -= p1[1] * p2[0]; - f = p1[0] * p2[1] - p2[0] * p1[1]; + f = (p1[0] * p2[1]) - (p2[0] * p1[1]); x += (p1[0] + p2[0]) * f; y += (p1[1] + p2[1]) * f; } @@ -198,14 +305,48 @@ function wireUpRenderers () { } rend.attachStylesToLayer(this); }; + + this._setLabelRenderers = function (serviceInfo) { + this._firstLabelingInfo = serviceInfo.drawingInfo.labelingInfo[0]; + + if (this._firstLabelingInfo) { + this._hasLabelRenderer = true; + var options = { + url: this.options.url + }; + + if (this.options.token) { + options.token = this.options.token; + } + + if (this.options.pane) { + options.pane = this.options.pane; + } + + if (serviceInfo.drawingInfo.transparency) { + options.layerTransparency = serviceInfo.drawingInfo.transparency; + } + + if (this._firstLabelingInfo.minScale) { + this._labelLayerMinZoom = scalingToZoom().scaleToZoom(this._firstLabelingInfo.minScale); + } + if (this._firstLabelingInfo.maxScale) { + this._labelLayerMaxZoom = scalingToZoom().scaleToZoom(this._firstLabelingInfo.maxScale); + } + + var labelRenderer = simpleLabelRenderer(this._firstLabelingInfo, options); + this._createLabelLayer(); + labelRenderer.attachStylesToLayer(this._labelLayer); + } + }; } Esri.FeatureLayer.addInitHook(function () { // the only method not shared with the clustered implementation L.Util.bind(this.createNewLayer, this); - wireUpRenderers(); + wireUpRenderers.bind(this)(); }); if (L.esri.Cluster) { - L.esri.Cluster.FeatureLayer.addInitHook(wireUpRenderers) + L.esri.Cluster.FeatureLayer.addInitHook(wireUpRenderers); } diff --git a/src/Labels/LabelRenderer.js b/src/Labels/LabelRenderer.js new file mode 100644 index 0000000..fd4c96d --- /dev/null +++ b/src/Labels/LabelRenderer.js @@ -0,0 +1,92 @@ +import L from 'leaflet'; + +export var LabelRenderer = L.Class.extend({ + options: { + clickable: true + }, + + initialize: function (rendererJson, options) { + this._labelSymbolVariables = rendererJson.symbol; + + // in the symbol property are the color, font (family, size, style, ... ) and so on . + // we start with the font properties and the color. + this._labelExpression = rendererJson.labelExpression; + this._labelPlacement = rendererJson.labelPlacement; + + this._layerTransparency = 1; + if (options && options.layerTransparency) { + this._layerTransparency = 1 - (options.layerTransparency / 100.0); + } + + L.Util.setOptions(this, options); + }, + + // color is an array [r,g,b,a] + textColorValue: function (color) { + return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + this.alphaValue(color) + ')'; + }, + + alphaValue: function (color) { + return Math.round(color[3] * this._layerTransparency); + }, + + _getLabelHtml: function (geojson, latlng) { + // override + }, + + _getLabelPositionStyle: function (geojson, latlng) { + if (this._labelPlacement === 'esriServerPointLabelPlacementAboveRight') { + return 'position: relative; top: -10px; left: 10px'; + } + return ''; + }, + getLabelExpressionValue: function (geojson, latlng) { + if (this._labelExpression) { + var expressionAttributes = this._labelExpression.match(/\[(.*?)\]/g); + // In arcgis: labels can use values from the geojson object. + // Therefor the field is surrounded by square brackets. + // To start, only this values are taken and filled with the other content from the expression. + // (@see: http://desktop.arcgis.com/en/arcmap/10.3/map/working-with-text/about-building-label-expressions.htm) + var labelExpressionValue = ''; + var currentCharacterIndex = 0; + for (var i = 0; i < expressionAttributes.length; i++) { + var anExpressionAttribute = expressionAttributes[i]; + var indexOfExpressionAttribute = this._labelExpression.indexOf(anExpressionAttribute, currentCharacterIndex); + if (indexOfExpressionAttribute !== currentCharacterIndex) { + labelExpressionValue += this._labelExpression.substr(currentCharacterIndex, indexOfExpressionAttribute); + } + currentCharacterIndex = indexOfExpressionAttribute + anExpressionAttribute.length; + + var geoJsonPropertyValue = geojson.properties[anExpressionAttribute.substr(1, anExpressionAttribute.length - 2)]; + if (geoJsonPropertyValue !== undefined && geoJsonPropertyValue !== null) { + labelExpressionValue += geoJsonPropertyValue; + } else { + labelExpressionValue += ''; + } + } + + if (currentCharacterIndex !== this._labelExpression.length - 1) { + labelExpressionValue += this._labelExpression.substr(currentCharacterIndex); + } + return labelExpressionValue; + } + + return ''; + }, + + _getDivIcon: function (geojson, latlng) { + return L.divIcon({html: this._getLabelHtml(geojson, latlng), className: 'leaflet-div-icon-for-label-renderer'}); + }, + + attachStylesToLayer: function (layer) { + layer.options.pointToLayer = L.Util.bind(this.pointToLayer, this); + }, + + pointToLayer: function (geojson, latlng) { + var _myIcon = this._getDivIcon(geojson, latlng); + return L.marker(latlng, {icon: _myIcon}); + } + +}); + +export default LabelRenderer; diff --git a/src/Labels/SimpleLabelRenderer.js b/src/Labels/SimpleLabelRenderer.js new file mode 100644 index 0000000..a986fcc --- /dev/null +++ b/src/Labels/SimpleLabelRenderer.js @@ -0,0 +1,33 @@ +import LabelRenderer from './LabelRenderer'; + +export var SimpleLabelRenderer = LabelRenderer.extend({ + initialize: function (rendererJson, options) { + LabelRenderer.prototype.initialize.call(this, rendererJson, options); + }, + + _getLabelHtml: function (geojson, latlng) { + var _inlineLabelStyling = ''; + if (this._labelSymbolVariables.color) { + _inlineLabelStyling = 'color: ' + this.textColorValue(this._labelSymbolVariables.color) + ';'; + } + if (this._labelSymbolVariables.font) { + _inlineLabelStyling += 'font-family: ' + this._labelSymbolVariables.font.family + ';'; + _inlineLabelStyling += 'font-size: ' + this._labelSymbolVariables.font.size + 'px;'; + _inlineLabelStyling += 'font-style: ' + this._labelSymbolVariables.font.style + ';'; + _inlineLabelStyling += 'font-weight: ' + this._labelSymbolVariables.font.weight + ';'; + _inlineLabelStyling += 'text-decoration: ' + this._labelSymbolVariables.font.decoration + ';'; + } + + _inlineLabelStyling += this._getLabelPositionStyle(geojson, latlng); + + var _labelValue = this.getLabelExpressionValue(geojson, latlng); + return ''; + } + +}); + +export function simpleLabelRenderer (rendererJson, options) { + return new SimpleLabelRenderer(rendererJson, options); +} + +export default simpleLabelRenderer; diff --git a/src/Scaling/ScalingToZoom.js b/src/Scaling/ScalingToZoom.js new file mode 100644 index 0000000..0ab9f64 --- /dev/null +++ b/src/Scaling/ScalingToZoom.js @@ -0,0 +1,78 @@ +import L from 'leaflet'; + +// values taken from https://wiki.openstreetmap.org/wiki/Zoom_levels + +export var ScalingToZoom = L.Class.extend({ + + // the geojson values returned are in points + scaleToZoom: function (scaleValue) { + if (scaleValue >= 500000000) { + return 0; + } + if (scaleValue >= 250000000) { + return 1; + } + if (scaleValue >= 150000000) { + return 2; + } + if (scaleValue >= 70000000) { + return 3; + } + if (scaleValue >= 35000000) { + return 4; + } + if (scaleValue >= 15000000) { + return 5; + } + if (scaleValue >= 10000000) { + return 6; + } + if (scaleValue >= 4000000) { + return 7; + } + if (scaleValue >= 2000000) { + return 8; + } + if (scaleValue >= 1000000) { + return 9; + } + if (scaleValue >= 500000) { + return 10; + } + if (scaleValue >= 250000) { + return 11; + } + if (scaleValue >= 150000) { + return 12; + } + if (scaleValue >= 70000) { + return 13; + } + if (scaleValue >= 35000) { + return 14; + } + if (scaleValue >= 15000) { + return 15; + } + if (scaleValue >= 8000) { + return 16; + } + if (scaleValue >= 4000) { + return 17; + } + if (scaleValue >= 2000) { + return 18; + } + if (scaleValue >= 1000) { + return 19; + } + return 20; + } + +}); + +export function scalingToZoom () { + return new ScalingToZoom(); +} + +export default scalingToZoom;