├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── example.html
├── logo
│ ├── github.svg
│ ├── makina.svg
│ └── npm.svg
├── meta.js
├── meta.json
└── style.css
├── index.html
├── leaflet.textpath.js
├── package.json
└── screenshot.png
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Next version / Unreleased
2 | ==================
3 |
4 | * Update changelog and demo page
5 | * Fix this._map._renderer unedefined if using panes (#87)
6 |
7 | 1.2.3 / 2020-02-07
8 | ==================
9 |
10 | * Fix mouse events
11 |
12 | 1.2.2 / 2020-02-07
13 | ==================
14 |
15 | * Fix mouse events
16 | * Fix repeated text on 0 length path
17 |
18 | 1.2.1 / 2018-06-22
19 | ==================
20 |
21 | * Update logo and turn links to https
22 | * Add example html page charset (utf8)
23 |
24 | 1.2.0 / 2018-03-01
25 | ==================
26 |
27 | * Add npm script to help releasing package versions
28 | * Set Leaflet as peerDependency
29 | * Upgrade Leaflet version from demo to `1.3.1`
30 | * Don't try to remove from container if container doesn't exist
31 | * Change way of dealing with the SVG to comply with leaflet >= 0.8
32 | * Remove useless console.log
33 |
34 | 1.1.0 / 2016-05-19
35 | ==================
36 |
37 | * Add the orientation option (#27, thanks @kirkau)
38 |
39 | 1.0.2 / 2016-03-14
40 | ==================
41 |
42 | * Allow HTTP and HTTPS to access the demo (#39, thanks @sonny89 and @leplatrem)
43 |
44 | 1.0.1 / 2016-02-05
45 | ==================
46 |
47 | * Fix text centering for vertical lines (#33, #34, #38, thanks @msgoloborodov)
48 |
49 | 1.0.0 / 2016-01-16
50 | ==================
51 |
52 | **Breaking changes**
53 |
54 | * Text is now shown on top by default. Set option ``below`` to true to put the text below the layer.
55 |
56 | 0.2.2 / 2014-08-14
57 | ==================
58 |
59 | * Fix bug when removing layer whose text was removed (fixes #18) (thanks Victor Gomes)
60 | * Fix path width when using options.center (fixes #17) (thanks Brent Miller).
61 |
62 | 0.2.1 / 2014-06-01
63 | ==================
64 |
65 | * Fix layer order (fixes #5) (thanks Albin Larsson)
66 |
67 | 0.2.0 / 2014-02-04
68 | ==================
69 |
70 | * Stay on top after bringToFront
71 | * Clean-up and fix `onAdd` and `onRemove`
72 | * Fire mouse events from underlying text layer (thanks Lewis Christie)
73 |
74 | 0.1.0 / 2013-04-11
75 | ===================
76 |
77 | * Initial working version
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2012 Makina Corpus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Leaflet.TextPath
2 | ================
3 |
4 | Shows a text along a Polyline.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Check out the demo !
14 |
15 |
16 | Install
17 | -----
18 | install it via your favorite package manager:
19 |
20 | `npm i leaflet-textpath`
21 |
22 | Leaflet versions
23 | -----
24 |
25 | The version on the github page (demo) currently targets Leaflet `1.3.1`.
26 |
27 | Usage
28 | -----
29 |
30 | For example, show path orientation on mouse over :
31 |
32 | ```javascript
33 | var layer = L.polyLine(...);
34 |
35 | layer.on('mouseover', function () {
36 | this.setText(' ► ', {repeat: true, attributes: {fill: 'red'}});
37 | });
38 |
39 | layer.on('mouseout', function () {
40 | this.setText(null);
41 | });
42 | ```
43 |
44 | With a GeoJSON containing lines, it becomes:
45 |
46 | ```javascript
47 | L.geoJson(data, {
48 | onEachFeature: function (feature, layer) {
49 | layer.setText(feature.properties.label);
50 | }
51 | }).addTo(map);
52 |
53 | ```
54 |
55 | ### Options
56 |
57 | * `repeat` Specifies if the text should be repeated along the polyline (Default: `false`)
58 | * `center` Centers the text according to the polyline's bounding box (Default: `false`)
59 | * `below` Show text below the path (Default: false)
60 | * `offset` Set an offset to position text relative to the polyline (Default: 0)
61 | * `orientation` Rotate text. (Default: 0)
62 | - {orientation: angle} - rotate to a specified angle (e.g. {orientation: 15})
63 | - {orientation: flip} - filps the text 180deg correction for upside down text placement on west -> east lines
64 | - {orientation: perpendicular} - places text at right angles to the line.
65 |
66 | * `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: `{}`)
67 |
68 | Credits
69 | -------
70 |
71 | The main idea comes from Tom Mac Wright's *[Getting serious about SVG](https://web.archive.org/web/20130312131812/http://mapbox.com/osmdev/2012/11/20/getting-serious-about-svg/)*
72 |
73 | Authors
74 | -------
75 |
76 | Many thanks to [all contributors](https://github.com/makinacorpus/Leaflet.TextPath/graphs/contributors) !
77 |
78 | [](http://makinacorpus.com)
79 |
--------------------------------------------------------------------------------
/docs/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 |
23 |
24 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/docs/logo/github.svg:
--------------------------------------------------------------------------------
1 | GitHub
--------------------------------------------------------------------------------
/docs/logo/makina.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
22 |
25 |
27 |
31 |
35 |
45 |
53 |
61 |
68 |
74 |
90 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/docs/logo/npm.svg:
--------------------------------------------------------------------------------
1 | npm
--------------------------------------------------------------------------------
/docs/meta.js:
--------------------------------------------------------------------------------
1 | const logJson = async () => {
2 | const reponse = await fetch('docs/meta.json');
3 | return await reponse.json();
4 | }
5 |
6 | (async () => {
7 | const meta = await logJson()
8 | document.querySelector("title").innerText= meta.name
9 | document.querySelector(".demo-name").innerText= meta.name
10 | if (meta.npm){
11 | document.querySelector(".npm").href="https://www.npmjs.com/package/" + meta.npm
12 | document.querySelector(".npm .title").innerText= meta.npm
13 | }else{
14 | document.querySelector(".npm").remove()
15 | }
16 | document.querySelector(".sources").href= meta.github
17 | document.querySelector(".readme-md").src= meta.github.replace("github.com","raw.githubusercontent.com") + "HEAD/README.md"
18 | if (meta.moreInfo){
19 | document.querySelector(".moreInfo").innerText= meta.moreInfo
20 | }
21 | })()
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "Leaflet.TextPath",
3 | "github" : "https://github.com/makinacorpus/Leaflet.TextPath/",
4 | "npm" : "leaflet-textpath"
5 | }
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | font-family: "Helvetica", arial, sans-serif;
4 | }
5 |
6 | html,
7 | body {
8 | height: 100%;
9 | width: 100%;
10 | overflow: hidden;
11 | }
12 |
13 | * {
14 | box-sizing: inherit;
15 | }
16 |
17 | body {
18 | display: flex;
19 | flex-direction: column;
20 | }
21 |
22 | header {
23 | padding: 0.5rem 1rem;
24 | background: #20273c;
25 | color: white;
26 | display: flex;
27 | z-index: 2;
28 | justify-content: space-between;
29 | }
30 |
31 | header a {
32 | color: white;
33 | }
34 |
35 | main {
36 | z-index: 1;
37 | display: flex;
38 | position: relative;
39 | overflow: hidden;
40 | height: 100%;
41 | flex-shrink: 1;
42 | }
43 |
44 | header,
45 | main {
46 | width: 100%;
47 | }
48 |
49 | h1 {
50 | margin: 0;
51 | font-size: 1.5rem;
52 | padding: 0.7rem;
53 | }
54 |
55 | h2 {
56 | margin: 0;
57 | font-size: 1.2rem;
58 | }
59 |
60 | /* ---- MENU ---- */
61 |
62 | .menu {
63 | display: flex;
64 | justify-content: space-around;
65 | align-items: center;
66 | }
67 |
68 | .menu .title {
69 | padding-top: 5px;
70 | }
71 |
72 | .menu > a {
73 | display: flex;
74 | align-items: center;
75 | font-weight: 700;
76 | text-decoration: none;
77 | padding: 0rem;
78 | text-align: right;
79 | margin: 0 0.4rem;
80 | }
81 |
82 | .menu a:hover,
83 | .menu a:focus {
84 | text-decoration: underline;
85 | }
86 |
87 | .menu-toggle {
88 | background: none;
89 | border: none;
90 | outline: none;
91 | cursor: pointer;
92 | display: none;
93 | }
94 |
95 | .menu-toggle.close .bar:nth-child(1) {
96 | transform: translateY(7px) rotateZ(45deg);
97 | }
98 |
99 | .menu-toggle.close .bar:nth-child(2) {
100 | transform: translateX(300px);
101 | }
102 |
103 | .menu-toggle.close .bar:nth-child(3) {
104 | transform: translateY(-7px) rotateZ(-45deg);
105 | }
106 |
107 | .bar {
108 | display: block;
109 | height: 2px;
110 | width: 20px;
111 | margin: 5px 0;
112 | background: white;
113 | border-radius: 2px;
114 | transition: transform 0.2s ease-out;
115 | }
116 |
117 | .icon {
118 | height: 24px;
119 | margin: 4px;
120 | }
121 |
122 | .logo {
123 | height: 50px;
124 | }
125 |
126 | #map {
127 | height: 50%;
128 | width: 100%;
129 | }
130 |
131 | /* ---- SIDEBAR ---- */
132 |
133 | .side {
134 | background-color: #ffffff;
135 | width: 360px;
136 | height: 100%;
137 | flex-shrink: 0;
138 | overflow-y: scroll;
139 | color: rgb(0, 0, 0);
140 | border-left: 1px solid rgba(0, 0, 0, 0.3);
141 | }
142 | .side h1 {
143 | margin-top: 10px;
144 | margin-bottom: 16px;
145 | padding: 0.7px;
146 | padding-bottom: 5px;
147 | margin-left: 5px;
148 | line-height: 1.2;
149 | border-bottom: 1px solid rgba(0,0,0,0.12);
150 | }
151 |
152 | #demoIframe{
153 | border: none;
154 | }
155 |
156 | @media screen and (max-width: 850px) {
157 | .side {
158 | position: absolute;
159 | transform: translateX(0px);
160 | right: 0;
161 | transition:
162 | transform 0.2s ease-out,
163 | box-shadow 0.2s ease-out;
164 |
165 | max-width: 100vw;
166 | -webkit-box-shadow: -5px 0px 15px 0px rgba(0,0,0,0.5);
167 | -moz-box-shadow: -5px 0px 15px 0px rgba(0,0,0,0.5);
168 | box-shadow: -5px 0px 15px 0px rgba(0,0,0,0.5);
169 | }
170 |
171 | .side.closed {
172 | transform: translateX(360px);
173 | -webkit-box-shadow: none;
174 | -moz-box-shadow: none;
175 | box-shadow: none;
176 | }
177 |
178 | .menu a .title {
179 | display: none;
180 | }
181 |
182 | .menu-toggle {
183 | z-index: 3;
184 | display: block;
185 | }
186 |
187 | header {
188 | flex-wrap: wrap;
189 | justify-content: flex-end;
190 | }
191 |
192 | header h1 {
193 | font-size: 1em;
194 | flex-grow: 1;
195 | }
196 | .icon {
197 | height: 18px;
198 | }
199 | .logo {
200 | height: 30px;
201 | }
202 | }
203 |
204 | @media (prefers-color-scheme: dark) {
205 | .side {
206 | background-color: #0d1117;
207 | color: #ffffff;
208 | }
209 | .side h1{
210 | border-bottom-color: rgba(255, 255, 255, 0.15);
211 | }
212 | }
213 |
214 |
215 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
32 |
33 |
35 |
36 |
37 |
38 |
40 |
41 |
43 |
44 |
45 | Description
46 |
47 |
48 |
68 |
70 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/leaflet.textpath.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Leaflet.TextPath - Shows text along a polyline
3 | * Inspired by Tom Mac Wright article :
4 | * https://web.archive.org/web/20130312131812/http://mapbox.com/osmdev/2012/11/20/getting-serious-about-svg/
5 | */
6 |
7 | (function () {
8 |
9 | var __onAdd = L.Polyline.prototype.onAdd,
10 | __onRemove = L.Polyline.prototype.onRemove,
11 | __updatePath = L.Polyline.prototype._updatePath,
12 | __bringToFront = L.Polyline.prototype.bringToFront;
13 |
14 |
15 | var PolylineTextPath = {
16 |
17 | onAdd: function (map) {
18 | __onAdd.call(this, map);
19 | this._textRedraw();
20 | },
21 |
22 | onRemove: function (map) {
23 | map = map || this._map;
24 | if (map && this._textNode && this._renderer._container)
25 | this._renderer._container.removeChild(this._textNode);
26 | __onRemove.call(this, map);
27 | },
28 |
29 | bringToFront: function () {
30 | __bringToFront.call(this);
31 | this._textRedraw();
32 | },
33 |
34 | _updatePath: function () {
35 | __updatePath.call(this);
36 | this._textRedraw();
37 | },
38 |
39 | _textRedraw: function () {
40 | var text = this._text,
41 | options = this._textOptions;
42 | if (text) {
43 | this.setText(null).setText(text, options);
44 | }
45 | },
46 |
47 | setText: function (text, options) {
48 | this._text = text;
49 | this._textOptions = options;
50 |
51 | /* If not in SVG mode or Polyline not added to map yet return */
52 | /* setText will be called by onAdd, using value stored in this._text */
53 | if (!L.Browser.svg || typeof this._map === 'undefined') {
54 | return this;
55 | }
56 |
57 | var defaults = {
58 | repeat: false,
59 | fillColor: 'black',
60 | attributes: {},
61 | below: false,
62 | };
63 | options = L.Util.extend(defaults, options);
64 |
65 | /* If empty text, hide */
66 | if (!text) {
67 | if (this._textNode && this._textNode.parentNode) {
68 | this._renderer._container.removeChild(this._textNode);
69 |
70 | /* delete the node, so it will not be removed a 2nd time if the layer is later removed from the map */
71 | delete this._textNode;
72 | }
73 | return this;
74 | }
75 |
76 | text = text.replace(/ /g, '\u00A0'); // Non breakable spaces
77 | var id = 'pathdef-' + L.Util.stamp(this);
78 | var svg = this._renderer._container;
79 | this._path.setAttribute('id', id);
80 |
81 | if (options.repeat) {
82 | /* Compute single pattern length */
83 | var pattern = L.SVG.create('text');
84 | for (var attr in options.attributes)
85 | pattern.setAttribute(attr, options.attributes[attr]);
86 | pattern.appendChild(document.createTextNode(text));
87 | svg.appendChild(pattern);
88 | var alength = pattern.getComputedTextLength();
89 | svg.removeChild(pattern);
90 |
91 | /* Create string as long as path */
92 | text = new Array(Math.ceil(isNaN(this._path.getTotalLength() / alength) ? 0 : this._path.getTotalLength() / alength)).join(text);
93 | }
94 |
95 | /* Put it along the path using textPath */
96 | var textNode = L.SVG.create('text'),
97 | textPath = L.SVG.create('textPath');
98 |
99 | var dy = options.offset || this._path.getAttribute('stroke-width');
100 |
101 | textPath.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", '#'+id);
102 | textNode.setAttribute('dy', dy);
103 | for (var attr in options.attributes)
104 | textNode.setAttribute(attr, options.attributes[attr]);
105 | textPath.appendChild(document.createTextNode(text));
106 | textNode.appendChild(textPath);
107 | this._textNode = textNode;
108 |
109 | if (options.below) {
110 | svg.insertBefore(textNode, svg.firstChild);
111 | }
112 | else {
113 | svg.appendChild(textNode);
114 | }
115 |
116 | /* Center text according to the path's bounding box */
117 | if (options.center) {
118 | var textLength = textNode.getComputedTextLength();
119 | var pathLength = this._path.getTotalLength();
120 | /* Set the position for the left side of the textNode */
121 | textNode.setAttribute('dx', ((pathLength / 2) - (textLength / 2)));
122 | }
123 |
124 | /* Change label rotation (if required) */
125 | if (options.orientation) {
126 | var rotateAngle = 0;
127 | switch (options.orientation) {
128 | case 'flip':
129 | rotateAngle = 180;
130 | break;
131 | case 'perpendicular':
132 | rotateAngle = 90;
133 | break;
134 | default:
135 | rotateAngle = options.orientation;
136 | }
137 |
138 | var rotatecenterX = (textNode.getBBox().x + textNode.getBBox().width / 2);
139 | var rotatecenterY = (textNode.getBBox().y + textNode.getBBox().height / 2);
140 | textNode.setAttribute('transform','rotate(' + rotateAngle + ' ' + rotatecenterX + ' ' + rotatecenterY + ')');
141 | }
142 |
143 | /* Initialize mouse events for the additional nodes */
144 | if (this.options.interactive) {
145 | if (L.Browser.svg || !L.Browser.vml) {
146 | textPath.setAttribute('class', 'leaflet-interactive');
147 | }
148 |
149 | var events = ['click', 'dblclick', 'mousedown', 'mouseover',
150 | 'mouseout', 'mousemove', 'contextmenu'];
151 | for (var i = 0; i < events.length; i++) {
152 | L.DomEvent.on(textNode, events[i], this.fire, this);
153 | }
154 | }
155 |
156 | return this;
157 | }
158 | };
159 |
160 | L.Polyline.include(PolylineTextPath);
161 |
162 | L.LayerGroup.include({
163 | setText: function(text, options) {
164 | for (var layer in this._layers) {
165 | if (typeof this._layers[layer].setText === 'function') {
166 | this._layers[layer].setText(text, options);
167 | }
168 | }
169 | return this;
170 | }
171 | });
172 |
173 |
174 |
175 | })();
176 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "leaflet-textpath",
3 | "version": "1.2.3",
4 | "description": "Shows a text (or a pattern) along a Polyline",
5 | "keywords": [
6 | "Leaflet",
7 | "GIS",
8 | "SVG"
9 | ],
10 | "main": "leaflet.textpath.js",
11 | "bugs": {
12 | "url": "https://github.com/makinacorpus/Leaflet.TextPath/issues"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git://github.com/makinacorpus/Leaflet.TextPath.git"
17 | },
18 | "license": "MIT",
19 | "scripts": {
20 | "version": "git changelog -t $npm_package_version && git add CHANGELOG.md"
21 | },
22 | "peerDependencies": {
23 | "leaflet": "^1.3.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makinacorpus/Leaflet.TextPath/86bd037d411a706a02779032d1386fa4d0d945a0/screenshot.png
--------------------------------------------------------------------------------