From 677479252e21c479176d72d9a02f6b5733f77ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 21 Dec 2018 19:19:37 +0100 Subject: [PATCH 01/10] repeat: specify distance in pixels --- README.md | 2 +- index.html | 2 +- leaflet.textpath.js | 21 +++++++++++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b7ba40f..83ce85c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ With a GeoJSON containing lines, it becomes: ### Options -* `repeat` Specifies if the text should be repeated along the polyline (Default: `false`) +* `repeat` Specifies if the text should be repeated along the polyline (Default: `false`). Specify `repeat` as float to set the distance between each repetition in pixels (will be approximated by spaces). * `center` Centers the text according to the polyline's bounding box (Default: `false`) * `below` Show text below the path (Default: false) * `offset` Set an offset to position text relative to the polyline (Default: 0) diff --git a/index.html b/index.html index ad8145e..2260a5b 100644 --- a/index.html +++ b/index.html @@ -188,7 +188,7 @@ L.geoJson(flightsWE, { onEachFeature: function (feature, layer) { - layer.setText(feature.properties.flight, {offset: -5}); + layer.setText(feature.properties.flight, {offset: -5, repeat: 20, center: true}); }, style: { weight: 3, diff --git a/leaflet.textpath.js b/leaflet.textpath.js index 0f22be2..28d95eb 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -78,7 +78,7 @@ var PolylineTextPath = { var svg = this._map._renderer._container; this._path.setAttribute('id', id); - if (options.repeat) { + if (options.repeat !== false) { /* Compute single pattern length */ var pattern = L.SVG.create('text'); for (var attr in options.attributes) @@ -88,8 +88,25 @@ var PolylineTextPath = { var alength = pattern.getComputedTextLength(); svg.removeChild(pattern); + /* Compute length of a space */ + var pattern = L.SVG.create('text'); + for (var attr in options.attributes) + pattern.setAttribute(attr, options.attributes[attr]); + pattern.appendChild(document.createTextNode('\u00A0')); + svg.appendChild(pattern); + var slength = pattern.getComputedTextLength(); + svg.removeChild(pattern); + /* Create string as long as path */ - text = new Array(Math.ceil(this._path.getTotalLength() / alength)).join(text); + var repeatDistance = parseFloat(options.repeat) || 0 + var spacingBalance = 0 + var singleText = text + for (var i = 1; i < Math.floor((this._path.getTotalLength() + repeatDistance) / (alength + repeatDistance)); i++) { + var spacesCount = Math.round((repeatDistance + spacingBalance) / slength) + spacingBalance = repeatDistance - (spacesCount * slength) + + text += '\u00A0'.repeat(spacesCount) + singleText + } } /* Put it along the path using textPath */ From 903c39009153717a1dd40621784a36595007a4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 21 Dec 2018 21:58:15 +0100 Subject: [PATCH 02/10] Function _getLength() calculates length of text --- leaflet.textpath.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/leaflet.textpath.js b/leaflet.textpath.js index 28d95eb..b359949 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -44,6 +44,20 @@ var PolylineTextPath = { } }, + _getLength: function (text, options) { + var svg = this._map._renderer._container; + + var pattern = L.SVG.create('text'); + for (var attr in options.attributes) + pattern.setAttribute(attr, options.attributes[attr]); + pattern.appendChild(document.createTextNode(text)); + svg.appendChild(pattern); + var length = pattern.getComputedTextLength(); + svg.removeChild(pattern); + + return length; + }, + setText: function (text, options) { this._text = text; this._textOptions = options; @@ -80,22 +94,10 @@ var PolylineTextPath = { if (options.repeat !== false) { /* Compute single pattern length */ - var pattern = L.SVG.create('text'); - for (var attr in options.attributes) - pattern.setAttribute(attr, options.attributes[attr]); - pattern.appendChild(document.createTextNode(text)); - svg.appendChild(pattern); - var alength = pattern.getComputedTextLength(); - svg.removeChild(pattern); + var alength = this._getLength(text, options); /* Compute length of a space */ - var pattern = L.SVG.create('text'); - for (var attr in options.attributes) - pattern.setAttribute(attr, options.attributes[attr]); - pattern.appendChild(document.createTextNode('\u00A0')); - svg.appendChild(pattern); - var slength = pattern.getComputedTextLength(); - svg.removeChild(pattern); + var slength = this._getLength('\u00A0', options); /* Create string as long as path */ var repeatDistance = parseFloat(options.repeat) || 0 From 825d7a972169f9a920a9cf9212e40362e391e9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 21 Dec 2018 22:02:25 +0100 Subject: [PATCH 03/10] Cache lengths of svg texts for performance reasons --- leaflet.textpath.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/leaflet.textpath.js b/leaflet.textpath.js index b359949..c5954ea 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -11,6 +11,7 @@ var __onAdd = L.Polyline.prototype.onAdd, __updatePath = L.Polyline.prototype._updatePath, __bringToFront = L.Polyline.prototype.bringToFront; +var _getLengthCache = {} var PolylineTextPath = { @@ -45,6 +46,12 @@ var PolylineTextPath = { }, _getLength: function (text, options) { + var cacheId = text + '|' + JSON.stringify(options.attributes) + + if (cacheId in _getLengthCache) { + return _getLengthCache[cacheId] + } + var svg = this._map._renderer._container; var pattern = L.SVG.create('text'); @@ -55,6 +62,8 @@ var PolylineTextPath = { var length = pattern.getComputedTextLength(); svg.removeChild(pattern); + _getLengthCache[cacheId] = length + return length; }, From 50580357e14b1841a48a0b4e0e3e561869c35a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 21 Dec 2018 22:31:56 +0100 Subject: [PATCH 04/10] Parameter 'allowCrop': whether a label may be cropped or not --- README.md | 1 + index.html | 2 +- leaflet.textpath.js | 17 +++++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 83ce85c..01cf6cb 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ With a GeoJSON containing lines, it becomes: - {orientation: angle} - rotate to a specified angle (e.g. {orientation: 15}) - {orientation: flip} - filps the text 180deg correction for upside down text placement on west -> east lines - {orientation: perpendicular} - places text at right angles to the line. +* `allowCrop` If the line is too short to display the whole text, crop the text. If false, don't show the text at all. (Default: true). * `attributes` Object containing the attributes applied to the `text` tag. Check valid attributes [here](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text#Attributes) (Default: `{}`) diff --git a/index.html b/index.html index 2260a5b..89d9acb 100644 --- a/index.html +++ b/index.html @@ -199,7 +199,7 @@ L.geoJson(flightsEW, { onEachFeature: function (feature, layer) { - layer.setText(feature.properties.flight, {offset: -5, orientation: 'flip'}); + layer.setText(feature.properties.flight, {offset: -5, orientation: 'flip', allowCrop: false}); }, style: { weight: 3, diff --git a/leaflet.textpath.js b/leaflet.textpath.js index c5954ea..2acb485 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -82,6 +82,7 @@ var PolylineTextPath = { fillColor: 'black', attributes: {}, below: false, + allowCrop: true }; options = L.Util.extend(defaults, options); @@ -101,9 +102,21 @@ var PolylineTextPath = { var svg = this._map._renderer._container; this._path.setAttribute('id', id); + var textLength = null + + if (!options.allowCrop) { + textLength = this._getLength(text, options); + + if (textLength > this._path.getTotalLength()) { + return this; + } + } + if (options.repeat !== false) { /* Compute single pattern length */ - var alength = this._getLength(text, options); + if (textLength === null) { + textLength = this._getLength(text, options); + } /* Compute length of a space */ var slength = this._getLength('\u00A0', options); @@ -112,7 +125,7 @@ var PolylineTextPath = { var repeatDistance = parseFloat(options.repeat) || 0 var spacingBalance = 0 var singleText = text - for (var i = 1; i < Math.floor((this._path.getTotalLength() + repeatDistance) / (alength + repeatDistance)); i++) { + for (var i = 1; i < Math.floor((this._path.getTotalLength() + repeatDistance) / (textLength + repeatDistance)); i++) { var spacesCount = Math.round((repeatDistance + spacingBalance) / slength) spacingBalance = repeatDistance - (spacesCount * slength) From 1047f3d2359c364b42ddd3011850af3f3b640fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 22 Dec 2018 08:43:49 +0100 Subject: [PATCH 05/10] Support labels which consist of several tspans --- README.md | 16 ++++++++++ index.html | 73 +++++++++++++++++++++++++++++++++++++++++++++ leaflet.textpath.js | 35 ++++++++++++++++++---- package.json | 3 ++ 4 files changed, 122 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 01cf6cb..7f09008 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,22 @@ With a GeoJSON containing lines, it becomes: ``` +You can also apply attributes only to parts of the text, e.g. to create multi colored labels: + +```javascript +layer.setText([ + { fill: 'red', text: 'Red' }, + ' ' + { style: 'fill: blue', text: 'Blue' } +]) +``` + +### `text` parameter +The `text` parameter of `setText()` can either be: +* A string: use this string as label +* An object: use value of key 'text' as content (which can be either string, object or array), other key/value pairs as attributes for a `tspan` SVG node. +* An array: The label consists of several parts, where each part can either be a string, an object or an array. + ### Options * `repeat` Specifies if the text should be repeated along the polyline (Default: `false`). Specify `repeat` as float to set the distance between each repetition in pixels (will be approximated by spaces). diff --git a/index.html b/index.html index 89d9acb..8bddd4a 100644 --- a/index.html +++ b/index.html @@ -186,6 +186,53 @@ ] }; + var rainbowGeom = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -4.04296875, + 17.476432197195518 + ], + [ + -2.8125, + 21.12549763660628 + ], + [ + 0.3515625, + 24.287026865376436 + ], + [ + 5.09765625, + 25.64152637306577 + ], + [ + 11.25, + 25.64152637306577 + ], + [ + 15.908203125, + 23.805449612314625 + ], + [ + 18.45703125, + 21.289374355860424 + ], + [ + 19.86328125, + 17.644022027872726 + ] + ] + } + } + ] + }; + L.geoJson(flightsWE, { onEachFeature: function (feature, layer) { layer.setText(feature.properties.flight, {offset: -5, repeat: 20, center: true}); @@ -208,6 +255,32 @@ } }).addTo(map); + L.geoJson(rainbowGeom, { + onEachFeature: function (feature, layer) { + layer.setText( + [ + { fill: '#ff0000', text: 'R' }, + { fill: '#ff7f00', text: 'a' }, + { fill: '#ffff00', text: 'i' }, + { fill: '#00ff00', text: 'n' }, + { fill: '#008f8f', text: 'b' }, + { fill: '#0000ff', text: 'o' }, + { fill: '#8b00ff', text: 'w' }, + ], + { + offset: -5, + repeat: 20, + center: true, + attributes: { 'font-size': '16pt', 'font-weight': 'bold', 'stroke-width': 0.5, 'stroke': '#000000' } + } + ); + }, + style: { + weight: 2, + color: 'black' + } + }).addTo(map); + var pos1 = [40.418075, -3.704643], pos2 = [40.413119, -3.702369]; var line = L.polyline([pos1, pos2]); diff --git a/leaflet.textpath.js b/leaflet.textpath.js index 2acb485..26c50e2 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -46,7 +46,7 @@ var PolylineTextPath = { }, _getLength: function (text, options) { - var cacheId = text + '|' + JSON.stringify(options.attributes) + var cacheId = JSON.stringify(text) + '|' + JSON.stringify(options.attributes) if (cacheId in _getLengthCache) { return _getLengthCache[cacheId] @@ -57,7 +57,7 @@ var PolylineTextPath = { var pattern = L.SVG.create('text'); for (var attr in options.attributes) pattern.setAttribute(attr, options.attributes[attr]); - pattern.appendChild(document.createTextNode(text)); + this._applyText(pattern, text); svg.appendChild(pattern); var length = pattern.getComputedTextLength(); svg.removeChild(pattern); @@ -97,7 +97,6 @@ var PolylineTextPath = { return this; } - text = text.replace(/ /g, '\u00A0'); // Non breakable spaces var id = 'pathdef-' + L.Util.stamp(this); var svg = this._map._renderer._container; this._path.setAttribute('id', id); @@ -129,7 +128,11 @@ var PolylineTextPath = { var spacesCount = Math.round((repeatDistance + spacingBalance) / slength) spacingBalance = repeatDistance - (spacesCount * slength) - text += '\u00A0'.repeat(spacesCount) + singleText + if (Array.isArray(singleText)) { + text = text.concat([{ text: '\u00A0'.repeat(spacesCount) }], singleText) + } else { + text += '\u00A0'.repeat(spacesCount) + singleText + } } } @@ -143,7 +146,7 @@ var PolylineTextPath = { textNode.setAttribute('dy', dy); for (var attr in options.attributes) textNode.setAttribute(attr, options.attributes[attr]); - textPath.appendChild(document.createTextNode(text)); + this._applyText(textPath, text); textNode.appendChild(textPath); this._textNode = textNode; @@ -197,6 +200,28 @@ var PolylineTextPath = { } return this; + }, + + _applyText: function(parentNode, text) { + if (Array.isArray(text)) { + text.forEach(function (part) { + this._applyText(parentNode, part); + }.bind(this)); + } else if (typeof text === 'object' && text !== null) { + var tspan = L.SVG.create('tspan'); + parentNode.appendChild(tspan); + + for (var attr in text) { + if (attr === 'text') { + this._applyText(tspan, text[attr]); + } else { + tspan.setAttribute(attr, text[attr]); + } + } + } else { + text = text.replace(/ /g, '\u00A0'); // Non breakable spaces + parentNode.appendChild(document.createTextNode(text)); + } } }; diff --git a/package.json b/package.json index f24b1ea..6eb8905 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,8 @@ }, "peerDependencies": { "leaflet": "^1.3.1" + }, + "dependencies": { + "innersvg-polyfill": "0.0.2" } } From 239b1bd214e73b51a2503d7fc56e16c2d0ffcd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 23 Dec 2018 20:37:16 +0100 Subject: [PATCH 06/10] Prepare next step: separate rendering of single and repeated labels --- leaflet.textpath.js | 75 +++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/leaflet.textpath.js b/leaflet.textpath.js index 26c50e2..56a7c80 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -101,21 +101,48 @@ var PolylineTextPath = { var svg = this._map._renderer._container; this._path.setAttribute('id', id); - var textLength = null + var textLength = null; + var pathLength = null; + var finalText = []; + var dx = 0 if (!options.allowCrop) { - textLength = this._getLength(text, options); + if (textLength === null) { + textLength = this._getLength(text, options); + } + if (pathLength === null) { + pathLength = this._path.getTotalLength(); + } - if (textLength > this._path.getTotalLength()) { - return this; - } + if (textLength > pathLength) { + return this; + } } - if (options.repeat !== false) { + if (options.repeat === false) { + finalText = [ text ]; + + /* Center text according to the path's bounding box */ + if (options.center) { + if (textLength === null) { + textLength = this._getLength(text, options); + } + + /* Set the position for the left side of the textNode */ + dx = Math.max(0, (pathLength / 2) - (textLength / 2)); + } + } else { + if (!Array.isArray(text)) { + text = [ text ] + } + /* Compute single pattern length */ if (textLength === null) { textLength = this._getLength(text, options); } + if (pathLength === null) { + pathLength = this._path.getTotalLength(); + } /* Compute length of a space */ var slength = this._getLength('\u00A0', options); @@ -123,16 +150,22 @@ var PolylineTextPath = { /* Create string as long as path */ var repeatDistance = parseFloat(options.repeat) || 0 var spacingBalance = 0 - var singleText = text - for (var i = 1; i < Math.floor((this._path.getTotalLength() + repeatDistance) / (textLength + repeatDistance)); i++) { - var spacesCount = Math.round((repeatDistance + spacingBalance) / slength) - spacingBalance = repeatDistance - (spacesCount * slength) + var repeatCount = Math.floor((pathLength + repeatDistance) / (textLength + repeatDistance)) || 1; - if (Array.isArray(singleText)) { - text = text.concat([{ text: '\u00A0'.repeat(spacesCount) }], singleText) - } else { - text += '\u00A0'.repeat(spacesCount) + singleText + /* Calculate the position for the left side of the textNode */ + if (options.center) { + dx = Math.max(0, (pathLength - textLength * repeatCount - repeatDistance * (repeatCount - 1)) / 2); + } + + for (var i = 0; i < repeatCount; i++) { + var spacesCount = 0 + if (i > 0) { + spacesCount = Math.round((repeatDistance + spacingBalance) / slength); + spacingBalance = repeatDistance - (spacesCount * slength); + finalText.push('\u00A0'.repeat(spacesCount)); } + + finalText.push(text); } } @@ -146,10 +179,14 @@ var PolylineTextPath = { textNode.setAttribute('dy', dy); for (var attr in options.attributes) textNode.setAttribute(attr, options.attributes[attr]); - this._applyText(textPath, text); + this._applyText(textPath, finalText); textNode.appendChild(textPath); this._textNode = textNode; + if (dx !== 0) { + textNode.setAttribute('dx', dx); + } + if (options.below) { svg.insertBefore(textNode, svg.firstChild); } @@ -157,14 +194,6 @@ var PolylineTextPath = { svg.appendChild(textNode); } - /* Center text according to the path's bounding box */ - if (options.center) { - var textLength = textNode.getComputedTextLength(); - var pathLength = this._path.getTotalLength(); - /* Set the position for the left side of the textNode */ - textNode.setAttribute('dx', ((pathLength / 2) - (textLength / 2))); - } - /* Change label rotation (if required) */ if (options.orientation) { var rotateAngle = 0; From d691920fb55f7d9cf176f69b4caab2e2947cf6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 23 Dec 2018 20:55:16 +0100 Subject: [PATCH 07/10] New option orientation=auto: auto-flip labels, even for part of lines --- README.md | 1 + index.html | 146 ++++++++++++++++++++++++++++++++++++++++++++ leaflet.textpath.js | 61 ++++++++++++++++-- 3 files changed, 202 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7f09008..207ce42 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ The `text` parameter of `setText()` can either be: - {orientation: angle} - rotate to a specified angle (e.g. {orientation: 15}) - {orientation: flip} - filps the text 180deg correction for upside down text placement on west -> east lines - {orientation: perpendicular} - places text at right angles to the line. + - {orientation: auto} - flips the text on (part of) ways running west to east, so that they are readable upside down. * `allowCrop` If the line is too short to display the whole text, crop the text. If false, don't show the text at all. (Default: true). * `attributes` Object containing the attributes applied to the `text` tag. Check valid attributes [here](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text#Attributes) (Default: `{}`) diff --git a/index.html b/index.html index 8bddd4a..2361b8a 100644 --- a/index.html +++ b/index.html @@ -281,6 +281,152 @@ } }).addTo(map); + var road = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 33.3984375, + 29.53522956294847 + ], + [ + 26.894531249999996, + 28.613459424004414 + ], + [ + 24.960937499999996, + 27.994401411046148 + ], + [ + 23.5546875, + 25.3241665257384 + ], + [ + 23.291015625, + 22.836945920943855 + ], + [ + 23.642578125, + 20.3034175184893 + ], + [ + 25.13671875, + 18.729501999072138 + ], + [ + 28.4765625, + 18.145851771694467 + ], + [ + 33.75, + 17.811456088564483 + ], + [ + 40.95703125, + 16.636191878397664 + ], + [ + 41.748046875, + 14.774882506516272 + ], + [ + 41.572265625, + 11.695272733029402 + ], + [ + 36.73828124999999, + 10.574222078332806 + ], + [ + 31.113281249999996, + 9.362352822055605 + ], + [ + 24.521484375, + 8.494104537551882 + ], + [ + 22.67578125, + 11.609193407938953 + ], + [ + 19.599609375, + 13.496472765758952 + ], + [ + 15.8203125, + 11.26461221250444 + ], + [ + 15.908203125, + 5.7908968128719565 + ], + [ + 18.720703125, + 1.7575368113083254 + ], + [ + 22.8515625, + -0.5273363048115043 + ], + [ + 27.421875, + -0.17578097424708533 + ], + [ + 30.05859375, + 4.039617826768437 + ], + [ + 31.201171875, + 6.664607562172573 + ], + [ + 36.650390625, + 7.536764322084078 + ], + [ + 39.7265625, + 4.653079918274051 + ] + ] + } + } + ] + }; + + // casing + L.geoJson(road, { + style: { + weight: 13, + color: 'black' + } + }).addTo(map); + // road + label + L.geoJson(road, { + onEachFeature: function (feature, layer) { + layer.setText( + 'Road', + { + repeat: 20, + center: true, + offset: 4, + orientation: 'auto', + attributes: { 'font-size': '8pt', 'font-weight': 'bold' } + } + ); + }, + style: { + weight: 10, + color: 'white' + } + }).addTo(map); + var pos1 = [40.418075, -3.704643], pos2 = [40.413119, -3.702369]; var line = L.polyline([pos1, pos2]); diff --git a/leaflet.textpath.js b/leaflet.textpath.js index 56a7c80..cc97263 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -120,8 +120,6 @@ var PolylineTextPath = { } if (options.repeat === false) { - finalText = [ text ]; - /* Center text according to the path's bounding box */ if (options.center) { if (textLength === null) { @@ -131,9 +129,23 @@ var PolylineTextPath = { /* Set the position for the left side of the textNode */ dx = Math.max(0, (pathLength / 2) - (textLength / 2)); } + + if (options.orientation === 'auto') { + var poiBegin = this._path.getPointAtLength(dx) + var poiEnd = this._path.getPointAtLength(dx + textLength) + var leftToRight = poiEnd.x >= poiBegin.x + + if (leftToRight) { + finalText.push(text); + } else { + finalText.push({ text: turnText(text), rotate: 180 }); + } + } else { + finalText = [ text ]; + } } else { - if (!Array.isArray(text)) { - text = [ text ] + if (options.orientation === 'auto') { + var textTurned = turnText(text) } /* Compute single pattern length */ @@ -149,8 +161,10 @@ var PolylineTextPath = { /* Create string as long as path */ var repeatDistance = parseFloat(options.repeat) || 0 + var pos = 0 var spacingBalance = 0 var repeatCount = Math.floor((pathLength + repeatDistance) / (textLength + repeatDistance)) || 1; + var finalText = [] /* Calculate the position for the left side of the textNode */ if (options.center) { @@ -162,10 +176,25 @@ var PolylineTextPath = { if (i > 0) { spacesCount = Math.round((repeatDistance + spacingBalance) / slength); spacingBalance = repeatDistance - (spacesCount * slength); + pos += spacesCount * slength finalText.push('\u00A0'.repeat(spacesCount)); } - finalText.push(text); + if (options.orientation === 'auto') { + var poiBegin = this._path.getPointAtLength(pos) + var poiEnd = this._path.getPointAtLength(pos + textLength) + var leftToRight = poiEnd.x >= poiBegin.x + + if (leftToRight) { + finalText.push(text); + } else { + finalText.push({ text: textTurned, rotate: 180 }); + } + } else { + finalText.push(text); + } + + pos += textLength } } @@ -204,6 +233,9 @@ var PolylineTextPath = { case 'perpendicular': rotateAngle = 90; break; + case 'auto': + rotateAngle = 0; + break; default: rotateAngle = options.orientation; } @@ -267,6 +299,23 @@ L.LayerGroup.include({ } }); - +function turnText (text) { + if (Array.isArray(text)) { + return text + .slice().reverse() + .map(function (part) { + return turnText(part) + }) + } else if (typeof text === 'object' && text !== null) { + var ret = {} + for (var attr in text) { + ret[attr] = text[attr] + } + ret.text = turnText(ret.text) + return ret + } else { + return text.split('').reverse().join('') + } +} })(); From 8e6a1711d016cc3d2c7ed0eba46ddb81e7d3212f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 23 Dec 2018 22:09:21 +0100 Subject: [PATCH 08/10] Use same code as orientation=auto for orientation=flip fixes #71 --- README.md | 2 +- leaflet.textpath.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 207ce42..9ed4f5b 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The `text` parameter of `setText()` can either be: * `offset` Set an offset to position text relative to the polyline (Default: 0) * `orientation` Rotate text. (Default: 0) - {orientation: angle} - rotate to a specified angle (e.g. {orientation: 15}) - - {orientation: flip} - filps the text 180deg correction for upside down text placement on west -> east lines + - {orientation: flip} - flips the text 180deg correction for upside down text placement on west -> east lines - {orientation: perpendicular} - places text at right angles to the line. - {orientation: auto} - flips the text on (part of) ways running west to east, so that they are readable upside down. * `allowCrop` If the line is too short to display the whole text, crop the text. If false, don't show the text at all. (Default: true). diff --git a/leaflet.textpath.js b/leaflet.textpath.js index cc97263..c4a81c5 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -140,11 +140,13 @@ var PolylineTextPath = { } else { finalText.push({ text: turnText(text), rotate: 180 }); } + } else if (options.orientation === 'flip') { + finalText.push({ text: turnText(text), rotate: 180 }); } else { finalText = [ text ]; } } else { - if (options.orientation === 'auto') { + if (options.orientation === 'auto' || options.orientation === 'flip') { var textTurned = turnText(text) } @@ -190,6 +192,8 @@ var PolylineTextPath = { } else { finalText.push({ text: textTurned, rotate: 180 }); } + } else if (options.orientation === 'flip') { + finalText.push({ text: textTurned, rotate: 180 }); } else { finalText.push(text); } @@ -227,13 +231,11 @@ var PolylineTextPath = { if (options.orientation) { var rotateAngle = 0; switch (options.orientation) { - case 'flip': - rotateAngle = 180; - break; case 'perpendicular': rotateAngle = 90; break; case 'auto': + case 'flip': rotateAngle = 0; break; default: From 7244e438e1b94924cfa56ec56f74a5e4eb631492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 24 Dec 2018 07:03:50 +0100 Subject: [PATCH 09/10] Enable option 'offset: 0' (would be replaced by the value of the path's stroke-width) --- leaflet.textpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leaflet.textpath.js b/leaflet.textpath.js index c4a81c5..3469d2e 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -206,7 +206,7 @@ var PolylineTextPath = { var textNode = L.SVG.create('text'), textPath = L.SVG.create('textPath'); - var dy = options.offset || this._path.getAttribute('stroke-width'); + var dy = 'offset' in options ? options.offset : this._path.getAttribute('stroke-width'); textPath.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", '#'+id); textNode.setAttribute('dy', dy); From 7494e3d7f27d0dc78f9e381916fc07b6f0b4c9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 23 Dec 2018 23:18:45 +0100 Subject: [PATCH 10/10] Option 'turnedText': alternative text for east-west lines --- README.md | 1 + index.html | 11 ++++++++--- leaflet.textpath.js | 21 ++++++++++++++------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9ed4f5b..7574138 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The `text` parameter of `setText()` can either be: - {orientation: perpendicular} - places text at right angles to the line. - {orientation: auto} - flips the text on (part of) ways running west to east, so that they are readable upside down. * `allowCrop` If the line is too short to display the whole text, crop the text. If false, don't show the text at all. (Default: true). +* `turnedText` When orientation=auto is used, use this text for east -> west lines. * `attributes` Object containing the attributes applied to the `text` tag. Check valid attributes [here](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text#Attributes) (Default: `{}`) diff --git a/index.html b/index.html index 2361b8a..690d487 100644 --- a/index.html +++ b/index.html @@ -411,13 +411,18 @@ L.geoJson(road, { onEachFeature: function (feature, layer) { layer.setText( - 'Road', + [ 'Road', { text: ' ⯈', fill: 'red' } ], { repeat: 20, center: true, - offset: 4, + offset: 0, orientation: 'auto', - attributes: { 'font-size': '8pt', 'font-weight': 'bold' } + attributes: { + 'font-size': '8pt', + 'font-weight': 'bold', + 'dominant-baseline': 'middle', + }, + turnedText: [ { text: '⯇ ', fill: 'blue' }, 'Road' ], } ); }, diff --git a/leaflet.textpath.js b/leaflet.textpath.js index 3469d2e..ff79d7a 100755 --- a/leaflet.textpath.js +++ b/leaflet.textpath.js @@ -146,14 +146,16 @@ var PolylineTextPath = { finalText = [ text ]; } } else { - if (options.orientation === 'auto' || options.orientation === 'flip') { - var textTurned = turnText(text) - } - /* Compute single pattern length */ if (textLength === null) { textLength = this._getLength(text, options); } + + if (options.orientation === 'auto' || options.orientation === 'flip') { + var textTurned = turnText(options.turnedText || text) + var textLengthTurned = this._getLength(options.turnedText || text, options) + } + if (pathLength === null) { pathLength = this._path.getTotalLength(); } @@ -173,7 +175,8 @@ var PolylineTextPath = { dx = Math.max(0, (pathLength - textLength * repeatCount - repeatDistance * (repeatCount - 1)) / 2); } - for (var i = 0; i < repeatCount; i++) { + var i = 0; + do { var spacesCount = 0 if (i > 0) { spacesCount = Math.round((repeatDistance + spacingBalance) / slength); @@ -189,17 +192,21 @@ var PolylineTextPath = { if (leftToRight) { finalText.push(text); + pos += textLength } else { finalText.push({ text: textTurned, rotate: 180 }); + pos += textLengthTurned } } else if (options.orientation === 'flip') { finalText.push({ text: textTurned, rotate: 180 }); + pos += textLengthTurned } else { finalText.push(text); + pos += textLength } - pos += textLength - } + i++ + } while (pos + repeatDistance + textLength < pathLength); } /* Put it along the path using textPath */