@maptrip/map-tiles 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +2 -0
  2. package/dist/api-extensions/google/MapTripGoogleMapType.d.ts +28 -0
  3. package/dist/api-extensions/leaflet/MapTripLeafletGridLayer.d.ts +25 -0
  4. package/dist/api-extensions/openlayers/MapTripOpenLayersSource.d.ts +19 -0
  5. package/dist/core/CollisionDetector.d.ts +14 -0
  6. package/dist/core/VectorTileFactory.d.ts +64 -0
  7. package/dist/core/VectorTileLoader.d.ts +40 -0
  8. package/dist/core/VectorTileRenderer.d.ts +123 -0
  9. package/dist/core/constants.d.ts +10 -0
  10. package/dist/core/rendering_config_default.d.ts +658 -0
  11. package/dist/core/rendering_config_grayscale.d.ts +672 -0
  12. package/dist/core/rendering_config_traffic.d.ts +39 -0
  13. package/dist/map-tiles.d.ts +9 -0
  14. package/dist/map-tiles.js +22856 -9398
  15. package/dist/map-tiles.js.map +1 -0
  16. package/dist/types/interfaces.d.ts +134 -0
  17. package/lib/api-extensions/google/MapTripGoogleMapType.ts +43 -0
  18. package/lib/api-extensions/leaflet/MapTripLeafletGridLayer.ts +48 -0
  19. package/lib/api-extensions/openlayers/MapTripOpenLayersSource.ts +62 -0
  20. package/lib/core/CollisionDetector.ts +95 -0
  21. package/lib/core/VectorTileFactory.ts +331 -0
  22. package/lib/core/VectorTileLoader.ts +165 -0
  23. package/lib/core/VectorTileRenderer.ts +1220 -0
  24. package/lib/core/constants.ts +21 -0
  25. package/lib/core/rendering_config_grayscale.ts +1129 -0
  26. package/lib/types/interfaces.ts +147 -0
  27. package/package.json +22 -17
  28. package/lib/api-extensions/google/MapTripGoogleMapType.js +0 -25
  29. package/lib/api-extensions/leaflet/MapTripLeafletGridLayer.js +0 -24
  30. package/lib/api-extensions/openlayers/MapTripOpenLayersSource.js +0 -43
  31. package/lib/core/CollisionDetector.js +0 -65
  32. package/lib/core/VectorTileFactory.js +0 -245
  33. package/lib/core/VectorTileLoader.js +0 -132
  34. package/lib/core/VectorTileRenderer.js +0 -826
  35. package/lib/core/constants.js +0 -18
  36. package/lib/core/rendering_config_grayscale.js +0 -1107
  37. /package/lib/core/{rendering_config_default.js → rendering_config_default.ts} +0 -0
  38. /package/lib/core/{rendering_config_traffic.js → rendering_config_traffic.ts} +0 -0
  39. /package/lib/{main.js → main.ts} +0 -0
@@ -0,0 +1,1220 @@
1
+ import {
2
+ Point,
3
+ LineString,
4
+ MultiLineString,
5
+ Polygon,
6
+ MultiPolygon,
7
+ GeoJsonProperties,
8
+ } from "geojson";
9
+ import { CollisionDetector } from "./CollisionDetector.js";
10
+ import {
11
+ Boundaries,
12
+ ImageStyle,
13
+ PointXY,
14
+ PolygonStyle,
15
+ PolylineStyle,
16
+ PolylineTextStyle,
17
+ PositionXY,
18
+ Renderer,
19
+ Vertex,
20
+ } from "../types/interfaces.js";
21
+
22
+ import { RENDERING_CONFIG} from "../types/interfaces.js"
23
+
24
+ class VectorTileRenderer {
25
+ private _styles: RENDERING_CONFIG;
26
+ private _images: { [key: string]: HTMLImageElement };
27
+ constructor(styles: RENDERING_CONFIG) {
28
+ this._styles = styles;
29
+ this._images = {};
30
+
31
+ this.preloadImages();
32
+ }
33
+
34
+ drawTile(
35
+ layer: GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>[],
36
+ z: number,
37
+ boundaries: Boundaries,
38
+ context: CanvasRenderingContext2D
39
+ ): void {
40
+ context.fillStyle = this.getBackgroundColor(z);
41
+ context.fillRect(0, 0, 256, 256);
42
+
43
+ var collisions = new CollisionDetector();
44
+ let polygons = layer
45
+ .filter((it) => !!it.properties && !!it.geometry.type)
46
+ .filter(
47
+ (it) =>
48
+ it.geometry.type === "Polygon" || it.geometry.type === "MultiPolygon"
49
+ )
50
+ .sort(
51
+ (a, b) =>
52
+ parseInt(a.properties!.sort_rank) - parseInt(b.properties!.sort_rank)
53
+ );
54
+
55
+ let lines = layer
56
+ .filter((it) => !!it.properties)
57
+ .filter(
58
+ (it) =>
59
+ it.geometry.type === "LineString" ||
60
+ it.geometry.type === "MultiLineString"
61
+ )
62
+ .sort(
63
+ (a, b) =>
64
+ parseInt(a.properties!.sort_rank) - parseInt(b.properties!.sort_rank)
65
+ );
66
+
67
+ let points = layer
68
+ .filter((it) => !!it.properties)
69
+ .filter((it) => it.geometry.type === "Point")
70
+ .sort(
71
+ (a, b) =>
72
+ parseInt(a.properties!.sort_rank) - parseInt(b.properties!.sort_rank)
73
+ );
74
+
75
+ this.drawTilePhase(polygons, z, boundaries, context, "areas", collisions);
76
+ this.drawPolylineLayers(lines, z, boundaries, context, "", collisions);
77
+ this.drawTilePhase(points, z, boundaries, context, "texts", collisions);
78
+ this.drawTilePhase(lines, z, boundaries, context, "lineTexts", collisions);
79
+ this.drawTilePhase(
80
+ polygons,
81
+ z,
82
+ boundaries,
83
+ context,
84
+ "areaTexts",
85
+ collisions
86
+ ); // for rivers etc (i.e. less important than the other texts)
87
+ }
88
+
89
+ drawPolylineLayers(
90
+ lines: GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>[],
91
+ z: number,
92
+ boundaries: Boundaries,
93
+ context: CanvasRenderingContext2D,
94
+ phase: string,
95
+ collisions: CollisionDetector
96
+ ): void {
97
+ let rank = lines
98
+ .map((it) => parseInt(it.properties!.sort_rank))
99
+ .filter((value, index, self) => self.indexOf(value) === index)
100
+ .sort((a, b) => a - b);
101
+
102
+ for (let i = 0; i < rank.length; i++) {
103
+ // select all lines with this sort rank
104
+ let currentRank = lines.filter(
105
+ (it) => parseInt(it.properties!.sort_rank) === rank[i]
106
+ );
107
+
108
+ this.drawTilePhase(
109
+ currentRank,
110
+ z,
111
+ boundaries,
112
+ context,
113
+ "linesOutline",
114
+ collisions
115
+ );
116
+ this.drawTilePhase(
117
+ currentRank,
118
+ z,
119
+ boundaries,
120
+ context,
121
+ "lines",
122
+ collisions
123
+ );
124
+ }
125
+ }
126
+
127
+ drawErroredTile(canvas: HTMLCanvasElement): void {
128
+ var context = canvas.getContext("2d");
129
+ if (context != null) {
130
+ context.fillStyle = "#C8C8C8";
131
+ context.fillRect(0, 0, 256, 256);
132
+ context.fillStyle = "#AAA";
133
+ context.font = "15px Arial";
134
+ context.textAlign = "center";
135
+ context.fillText(
136
+ "Could not display map tile",
137
+ canvas.width / 2,
138
+ canvas.height / 2 - 10
139
+ );
140
+ context.fillText(
141
+ "Please refresh",
142
+ canvas.width / 2,
143
+ canvas.height / 2 + 10
144
+ );
145
+ }
146
+ }
147
+
148
+ drawTilePhase(
149
+ layer: GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>[],
150
+ z: number,
151
+ boundaries: Boundaries,
152
+ context: CanvasRenderingContext2D,
153
+ phase: string,
154
+ collisions: CollisionDetector
155
+ ): void {
156
+ const texts: { [key: string]: any } = {};
157
+
158
+ for (let i = 0; i < layer.length; i++) {
159
+ const kind = layer[i].properties!.kind;
160
+ const geometry = layer[i].geometry;
161
+
162
+ // Look up the renderer for this kind of data
163
+ //@ts-ignore
164
+ const rendererName = this._styles.layers[kind];
165
+ const renderer = rendererName
166
+ ? //@ts-ignore
167
+ this._styles?.renderers[rendererName]
168
+ : undefined;
169
+
170
+ if (renderer) {
171
+ for (let j = 0; j < renderer.length; j++) {
172
+ if (
173
+ renderer[j].from <= z &&
174
+ renderer[j].to >= z &&
175
+ this.accept(renderer[j], layer[i])
176
+ ) {
177
+ // Apply filters
178
+
179
+ if (geometry.type === "Polygon") {
180
+ const polygonGeometry = geometry as Polygon;
181
+ const coordinates = polygonGeometry.coordinates;
182
+ if (
183
+ (phase === "areasOutline" || phase === "areas") &&
184
+ renderer[j].polygon
185
+ ) {
186
+ this.drawPolygon(
187
+ coordinates,
188
+ layer[i].properties,
189
+ z,
190
+ boundaries,
191
+ renderer[j].polygon as PolygonStyle,
192
+ context,
193
+ phase
194
+ );
195
+ } else if (
196
+ phase === "areaTexts" &&
197
+ renderer[j].text !== undefined
198
+ ) {
199
+ this.drawPolygon(
200
+ coordinates,
201
+ layer[i].properties,
202
+ z,
203
+ boundaries,
204
+ renderer[j].text as PolygonStyle,
205
+ context,
206
+ phase,
207
+ collisions,
208
+ texts
209
+ );
210
+ }
211
+ } else if (geometry.type === "MultiPolygon") {
212
+ const multiPolygonGeometry = geometry as MultiPolygon;
213
+ const coordinates = multiPolygonGeometry.coordinates;
214
+ if (
215
+ (phase === "areasOutline" || phase === "areas") &&
216
+ renderer[j].polygon
217
+ ) {
218
+ for (let k = 0; k < coordinates.length; k++) {
219
+ this.drawPolygon(
220
+ coordinates[k],
221
+ layer[i].properties,
222
+ z,
223
+ boundaries,
224
+ renderer[j].polygon as PolygonStyle,
225
+ context,
226
+ phase
227
+ );
228
+ }
229
+ } else if (phase === "areaTexts" && renderer[j].text) {
230
+ for (let k = 0; k < coordinates.length; k++) {
231
+ this.drawPolygon(
232
+ coordinates[k],
233
+ layer[i].properties,
234
+ z,
235
+ boundaries,
236
+ renderer[j].text as PolygonStyle,
237
+ context,
238
+ phase,
239
+ collisions,
240
+ texts
241
+ );
242
+ }
243
+ }
244
+ } else if (geometry.type === "LineString") {
245
+ const lineStringGeometry = geometry as LineString;
246
+ const coordinates = lineStringGeometry.coordinates;
247
+ if (
248
+ (phase === "linesOutline" || phase === "lines") &&
249
+ renderer[j].line
250
+ ) {
251
+ this.drawPolyline(
252
+ coordinates,
253
+ layer[i].properties,
254
+ boundaries,
255
+ renderer[j].line as PolylineStyle,
256
+ context,
257
+ phase
258
+ );
259
+ } else if (phase === "lineTexts" && renderer[j].text) {
260
+ this.drawPolylineText(
261
+ coordinates,
262
+ layer[i].properties,
263
+ boundaries,
264
+ renderer[j].text,
265
+ context,
266
+ collisions,
267
+ texts
268
+ );
269
+ }
270
+ } else if (geometry.type === "MultiLineString") {
271
+ const multiLineStringGeometry = geometry as MultiLineString;
272
+ const coordinates = multiLineStringGeometry.coordinates;
273
+ if (
274
+ (phase === "linesOutline" || phase === "lines") &&
275
+ renderer[j].line
276
+ ) {
277
+ for (let k = 0; k < coordinates.length; k++) {
278
+ this.drawPolyline(
279
+ coordinates[k],
280
+ layer[i].properties,
281
+ boundaries,
282
+ renderer[j].line as PolylineStyle,
283
+ context,
284
+ phase
285
+ );
286
+ }
287
+ } else if (phase === "lineTexts" && renderer[j].text) {
288
+ for (let k = 0; k < coordinates.length; k++) {
289
+ this.drawPolylineText(
290
+ coordinates[k],
291
+ layer[i].properties,
292
+ boundaries,
293
+ renderer[j].text,
294
+ context,
295
+ collisions,
296
+ texts
297
+ );
298
+ }
299
+ }
300
+ } else if (geometry.type === "Point") {
301
+ const pointGeometry = geometry as Point;
302
+ const coordinates = pointGeometry.coordinates;
303
+ if (phase === "texts" && renderer[j].text !== undefined) {
304
+ let text = layer[i].properties!["name"]
305
+ ? layer[i].properties!["name"]
306
+ : layer[i].properties!["TEXT"];
307
+ if (text) {
308
+ if (
309
+ !this.isTextCollision(
310
+ coordinates,
311
+ text,
312
+ boundaries,
313
+ renderer[j].text as PolylineTextStyle,
314
+ context,
315
+ collisions
316
+ )
317
+ ) {
318
+ const match = text.match(/([A-Z])\s?([0-9]+[a-z]*).*/);
319
+ if (match) {
320
+ const letter = match[1];
321
+ const number = match[2];
322
+ if (letter === "A" || letter === "B") {
323
+ text = number;
324
+ this.drawImage(
325
+ coordinates,
326
+ boundaries,
327
+ renderer[j].image as ImageStyle,
328
+ context
329
+ );
330
+ this.drawText(
331
+ coordinates,
332
+ text,
333
+ boundaries,
334
+ renderer[j].text as PolylineTextStyle,
335
+ context,
336
+ collisions
337
+ );
338
+ }
339
+ } else {
340
+ this.drawText(
341
+ coordinates,
342
+ text,
343
+ boundaries,
344
+ renderer[j].text as PolylineTextStyle,
345
+ context,
346
+ collisions
347
+ );
348
+ }
349
+ }
350
+ } else {
351
+ this.drawImage(
352
+ coordinates,
353
+ boundaries,
354
+ renderer[j].image as ImageStyle,
355
+ context
356
+ );
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ /**
367
+ * @private
368
+ */
369
+ getBackgroundColor(z: number): string {
370
+ if (this._styles?.background) {
371
+ for (var i = 0; i < this._styles.background.length; i++) {
372
+ if (
373
+ this._styles.background[i].from <= z &&
374
+ this._styles.background[i].to >= z
375
+ ) {
376
+ return this._styles.background[i].color;
377
+ }
378
+ }
379
+ }
380
+
381
+ return "#ffffff";
382
+ }
383
+
384
+ /**
385
+ * @private
386
+ */
387
+ accept(
388
+ style: Renderer,
389
+ layer: GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>
390
+ ): boolean {
391
+ if (style.filters) {
392
+ for (var i = 0; i < style.filters.length; i++) {
393
+ var filter = style.filters[i];
394
+
395
+ if (filter.type == "minimum") {
396
+ if (layer.properties![filter.key]) {
397
+ let value = layer.properties![filter.key];
398
+ if (value < filter.value) {
399
+ return false;
400
+ }
401
+ }
402
+ } else if (filter.type == "equals not") {
403
+ if (layer.properties![filter.key]) {
404
+ let value = layer.properties![filter.key];
405
+ if (value == filter.value) {
406
+ return false;
407
+ }
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ return true;
414
+ }
415
+
416
+ /**
417
+ * @private
418
+ */
419
+ drawPolygon(
420
+ //Collisions and texts are optional properties that are only needed when the phase === "areaTexts"
421
+ coordinates: GeoJSON.Position[][],
422
+ geoJsonProperties: GeoJsonProperties,
423
+ z: number,
424
+ boundaries: Boundaries,
425
+ style: PolygonStyle,
426
+ context: CanvasRenderingContext2D,
427
+ phase: string,
428
+ collisions?: CollisionDetector,
429
+ texts?: { [key: string]: any }
430
+ ): void {
431
+ if (style) {
432
+ let merged = { ...VectorTileRenderer.DEFAULT_STYLE.POLYGON, ...style };
433
+
434
+ for (var j = 0; j < coordinates.length; j++) {
435
+ var part = coordinates[j];
436
+
437
+ context.globalAlpha = merged.opacity;
438
+ context.beginPath();
439
+
440
+ var points: { x: number; y: number }[] = [];
441
+
442
+ for (let k = 0; k < part.length; k++) {
443
+ let x =
444
+ (256 * (part[k][0] - boundaries.min.lon!)) /
445
+ (boundaries.max.lon! - boundaries.min.lon!);
446
+ let y =
447
+ (256 * (part[k][1] - boundaries.min.lat!)) /
448
+ (boundaries.max.lat! - boundaries.min.lat!);
449
+
450
+ points.push({ x: x, y: y });
451
+
452
+ if (k == 0) {
453
+ context.moveTo(x, y);
454
+ } else {
455
+ context.lineTo(x, y);
456
+ }
457
+ }
458
+
459
+ context.closePath();
460
+
461
+ if ("areasOutline" === phase) {
462
+ if (merged.outlineColor && merged.outlineWidth) {
463
+ context.strokeStyle = merged.outlineColor;
464
+ context.lineWidth = merged.outlineWidth;
465
+ context.stroke();
466
+ }
467
+
468
+ return;
469
+ } else if ("areas" === phase) {
470
+ if (merged.url) {
471
+ if (this._images[merged.url]) {
472
+ var pattern = context.createPattern(
473
+ this._images[merged.url],
474
+ "repeat"
475
+ );
476
+ context.fillStyle = pattern ?? merged.color;
477
+ }
478
+ } else {
479
+ if (z <= 7) {
480
+ // draw an outline to prevent raster lines (see INWA-997)
481
+ context.strokeStyle = merged.color;
482
+ context.lineWidth = 1;
483
+ context.stroke();
484
+ }
485
+
486
+ context.fillStyle = merged.color;
487
+ }
488
+
489
+ context.fill();
490
+ } else if ("areaTexts" === phase) {
491
+ if (geoJsonProperties && geoJsonProperties.NAME) {
492
+ // skip texts which already are in this tile
493
+ if (texts && texts[geoJsonProperties.NAME]) {
494
+ return;
495
+ }
496
+
497
+ let merged = { ...VectorTileRenderer.DEFAULT_STYLE.TEXT, ...style };
498
+
499
+ context.globalAlpha = merged.opacity;
500
+ context.font =
501
+ merged.fontStyle +
502
+ " " +
503
+ merged.fontWeight +
504
+ " " +
505
+ merged.size +
506
+ "px " +
507
+ merged.font;
508
+ context.textAlign = "left";
509
+ context.textBaseline = "top";
510
+
511
+ var metrics = context.measureText(geoJsonProperties.NAME);
512
+
513
+ var centroid = this.getCentroid(points);
514
+ let x = centroid.x - 0.5 * metrics.width;
515
+ let y = centroid.y;
516
+
517
+ let w = metrics.width;
518
+ let h = merged.size!;
519
+
520
+ // skip texts outside a tile
521
+ if (x < 0 || x + w > 256 || y < 0 || y + h > 256) {
522
+ return;
523
+ }
524
+
525
+ // skip texts colliding with other texts
526
+ if (collisions && collisions.isCollision(x, y, w, h)) {
527
+ return;
528
+ }
529
+
530
+ if (merged.outlineColor && merged.outlineWidth) {
531
+ context.lineWidth = merged.outlineWidth;
532
+ context.strokeStyle = merged.outlineColor;
533
+ context.strokeText(geoJsonProperties.NAME, x, y);
534
+ }
535
+
536
+ context.fillStyle = merged.color;
537
+ context.fillText(geoJsonProperties.NAME, x, y);
538
+
539
+ if (collisions) {
540
+ collisions.insert(x, y, w, h);
541
+ }
542
+ if (texts) {
543
+ texts[geoJsonProperties.NAME] = true;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+
551
+ getCentroid(vertices: Vertex[]): Vertex {
552
+ var centroid = { x: 0, y: 0 };
553
+ var signedArea = 0.0;
554
+ var x0 = 0.0; // Current vertex X
555
+ var y0 = 0.0; // Current vertex Y
556
+ var x1 = 0.0; // Next vertex X
557
+ var y1 = 0.0; // Next vertex Y
558
+ var a = 0.0; // Partial signed area
559
+
560
+ // For all vertices except last
561
+ for (var i = 0; i < vertices.length - 1; i++) {
562
+ x0 = vertices[i].x;
563
+ y0 = vertices[i].y;
564
+ x1 = vertices[i + 1].x;
565
+ y1 = vertices[i + 1].y;
566
+ a = x0 * y1 - x1 * y0;
567
+ signedArea += a;
568
+ centroid.x += (x0 + x1) * a;
569
+ centroid.y += (y0 + y1) * a;
570
+ }
571
+
572
+ // Do last vertex separately to avoid performing an expensive
573
+ // modulus operation in each iteration.
574
+ x0 = vertices[i].x;
575
+ y0 = vertices[i].y;
576
+ x1 = vertices[0].x;
577
+ y1 = vertices[0].y;
578
+ a = x0 * y1 - x1 * y0;
579
+ signedArea += a;
580
+ centroid.x += (x0 + x1) * a;
581
+ centroid.y += (y0 + y1) * a;
582
+
583
+ signedArea *= 0.5;
584
+ centroid.x /= 6.0 * signedArea;
585
+ centroid.y /= 6.0 * signedArea;
586
+
587
+ return centroid;
588
+ }
589
+
590
+ getBBox(coordinates: PointXY[]): {
591
+ min: { x: number; y: number };
592
+ max: { x: number; y: number };
593
+ } {
594
+ var bbox = {
595
+ min: {
596
+ x: coordinates[0].x,
597
+ y: coordinates[0].y,
598
+ },
599
+ max: {
600
+ x: coordinates[0].x,
601
+ y: coordinates[0].y,
602
+ },
603
+ };
604
+
605
+ for (var i = 1; i < coordinates.length; i++) {
606
+ bbox.min.x = Math.min(bbox.min.x, coordinates[i].x);
607
+ bbox.min.y = Math.min(bbox.min.y, coordinates[i].y);
608
+ bbox.max.x = Math.max(bbox.max.x, coordinates[i].x);
609
+ bbox.max.y = Math.max(bbox.max.y, coordinates[i].y);
610
+ }
611
+
612
+ return bbox;
613
+ }
614
+
615
+ /**
616
+ * @private
617
+ */
618
+ projectCoordinates(
619
+ coordinates: GeoJSON.Position[],
620
+ boundaries: Boundaries
621
+ ): { x: number; y: number }[] {
622
+ var lastX, lastY;
623
+ var points = [];
624
+
625
+ for (var j = 0; j < coordinates.length; j++) {
626
+ var x = Math.round(
627
+ (256 * (coordinates[j][0] - boundaries.min.lon!)) /
628
+ (boundaries.max.lon! - boundaries.min.lon!)
629
+ );
630
+ var y = Math.round(
631
+ (256 * (coordinates[j][1] - boundaries.min.lat!)) /
632
+ (boundaries.max.lat! - boundaries.min.lat!)
633
+ );
634
+
635
+ if (x != lastX || y != lastY) {
636
+ points.push({ x: x, y: y });
637
+ }
638
+
639
+ lastX = x;
640
+ lastY = y;
641
+ }
642
+
643
+ return points;
644
+ }
645
+
646
+ /**
647
+ * @private
648
+ */
649
+ drawPolyline(
650
+ coordinates: GeoJSON.Position[],
651
+ geoJsonProperties: GeoJsonProperties,
652
+ boundaries: Boundaries,
653
+ style: PolylineStyle,
654
+ context: CanvasRenderingContext2D,
655
+ phase: "linesOutline" | "lines"
656
+ ) {
657
+ var merged = { ...VectorTileRenderer.DEFAULT_STYLE.LINE, ...style };
658
+ context.globalAlpha = merged.opacity;
659
+
660
+ if ("linesOutline" === phase) {
661
+ if (merged.outlineColor && merged.outlineWidth) {
662
+ let points = this.projectCoordinates(coordinates, boundaries);
663
+ this.drawPath(
664
+ points,
665
+ merged.outlineColor,
666
+ merged.width + merged.outlineWidth * 2,
667
+ context
668
+ );
669
+ }
670
+
671
+ return;
672
+ }
673
+
674
+ let points = this.projectCoordinates(coordinates, boundaries);
675
+ this.drawPath(points, merged.color, merged.width, context);
676
+
677
+ if (merged.dashColor && merged.dashLength) {
678
+ context.setLineDash([merged.dashLength, merged.dashLength]);
679
+ this.drawPath(points, merged.dashColor, merged.width, context);
680
+ context.setLineDash([]);
681
+ }
682
+
683
+ // draw images on the line (e.g. arrows for one-way roads)
684
+ if (merged.url && this._images[merged.url]) {
685
+ var desired = 40; // XXX config
686
+ var distance = Math.round(desired / 2);
687
+
688
+ for (var i = 0; i < points.length - 1; i++) {
689
+ // calculate angle of this section of the line
690
+ var dx = points[i + 1].x - points[i].x;
691
+ var dy = points[i + 1].y - points[i].y;
692
+ var angle = Math.atan2(dy, dx);
693
+
694
+ if (merged.direction != "reverse") {
695
+ angle = angle + Math.PI;
696
+ }
697
+
698
+ // now calculate equidistant points on the polyline
699
+ var distanceToNext = Math.sqrt(dx * dx + dy * dy);
700
+ while (distance < distanceToNext) {
701
+ // there are more points on this section of the line
702
+ var f = distance / distanceToNext;
703
+
704
+ var x = points[i].x + f * dx;
705
+ var y = points[i].y + f * dy;
706
+
707
+ context.save();
708
+
709
+ context.translate(x, y);
710
+ context.rotate(angle);
711
+ context.translate(-x, -y);
712
+
713
+ context.drawImage(
714
+ this._images[merged.url],
715
+ x + merged.dx,
716
+ y + merged.dy,
717
+ Number(merged.width),
718
+ Number(merged.height)
719
+ );
720
+
721
+ context.restore();
722
+
723
+ distance += desired;
724
+ }
725
+
726
+ distance = (desired - (distanceToNext - distance)) % desired;
727
+ }
728
+ }
729
+ }
730
+
731
+ drawPolylineText(
732
+ coordinates: GeoJSON.Position[],
733
+ geoJsonProperties: GeoJsonProperties,
734
+ boundaries: Boundaries,
735
+ style: PolylineTextStyle | undefined,
736
+ context: CanvasRenderingContext2D,
737
+ collisions: CollisionDetector,
738
+ texts: { [key: string]: any }
739
+ ) {
740
+ if (
741
+ !geoJsonProperties ||
742
+ !geoJsonProperties.NAME ||
743
+ coordinates.length < 2
744
+ ) {
745
+ return;
746
+ }
747
+
748
+ var text = geoJsonProperties.NAME;
749
+
750
+ if (texts[text]) {
751
+ // there already is a label for this street on this tile
752
+ return;
753
+ }
754
+
755
+ var merged = { ...VectorTileRenderer.DEFAULT_STYLE.TEXT, ...style };
756
+
757
+ context.globalAlpha = merged.opacity;
758
+ context.font =
759
+ merged.fontStyle +
760
+ " " +
761
+ merged.fontWeight +
762
+ " " +
763
+ merged.size +
764
+ "px " +
765
+ merged.font;
766
+
767
+ var points = this.projectCoordinates(coordinates, boundaries);
768
+ if (points.length < 2) {
769
+ return;
770
+ }
771
+
772
+ var last = points[points.length - 1];
773
+ var first = points[0];
774
+
775
+ var dx = last.x - first.x;
776
+ if (dx < 0) {
777
+ // if road segment goes to the left, the text should be flipped (to prevent the text being upside down)
778
+ points.reverse();
779
+ }
780
+
781
+ if (collisions) {
782
+ var positions = this.getLetterPositions(
783
+ points,
784
+ text,
785
+ context,
786
+ collisions
787
+ );
788
+
789
+ if (positions) {
790
+ context.fillStyle = merged.color;
791
+ context.textAlign = "center";
792
+ context.textBaseline = "middle";
793
+
794
+ if (merged.outlineColor && merged.outlineWidth) {
795
+ context.lineWidth = merged.outlineWidth;
796
+ context.strokeStyle = merged.outlineColor;
797
+
798
+ for (let i = 0; i < text.length; i++) {
799
+ this.drawLetter(
800
+ text.charAt(i),
801
+ positions[i],
802
+ merged.outlineWidth,
803
+ context
804
+ );
805
+ }
806
+ }
807
+
808
+ var avgLetterWidth = context.measureText("a").width;
809
+
810
+ for (let i = 0; i < text.length; i++) {
811
+ this.drawLetter(text.charAt(i), positions[i], 0, context);
812
+ collisions.insertPolylineText(
813
+ context,
814
+ text.charAt(i),
815
+ positions[i]
816
+ // avgLetterWidth removed as is not used in method
817
+ );
818
+ }
819
+ texts[text] = true;
820
+ }
821
+ }
822
+ }
823
+
824
+ getPolylineLength(points: PointXY[]): number {
825
+ var length = 0;
826
+
827
+ for (var i = 1; i < points.length; i++) {
828
+ var current = points[i];
829
+ var last = points[i - 1];
830
+ var dx = last.x - current.x;
831
+ var dy = last.y - current.y;
832
+
833
+ length += Math.sqrt(dx * dx + dy * dy);
834
+ }
835
+
836
+ return length;
837
+ }
838
+
839
+ getAngleAndCoordsAtLength(points: PointXY[], dist: number, width: number) {
840
+ var x,
841
+ y,
842
+ len = 0;
843
+
844
+ var gotxy = false;
845
+
846
+ for (var i = 1; i < points.length; i++) {
847
+ var current = points[i];
848
+ var last = points[i - 1];
849
+
850
+ var dx = current.x - last.x;
851
+ var dy = current.y - last.y;
852
+
853
+ var segLen = Math.sqrt(dx * dx + dy * dy);
854
+
855
+ if (!gotxy && len + segLen >= dist) {
856
+ let partLen = dist - len;
857
+ x = last.x + (dx * partLen) / segLen;
858
+ y = last.y + (dy * partLen) / segLen;
859
+
860
+ gotxy = true;
861
+ }
862
+
863
+ if (gotxy && len + segLen >= dist + width && y && x) {
864
+ let partLen = dist + width - len;
865
+ dx = last.x + (dx * partLen) / segLen;
866
+ dy = last.y + (dy * partLen) / segLen;
867
+ var angle = Math.atan2(dy - y, dx - x);
868
+
869
+ return {
870
+ angle: angle,
871
+ x: x,
872
+ y: y,
873
+ };
874
+ }
875
+
876
+ len += segLen;
877
+ }
878
+ }
879
+
880
+ getLetterPositions(
881
+ points: PointXY[],
882
+ text: string,
883
+ context: CanvasRenderingContext2D,
884
+ collisions: CollisionDetector
885
+ ) {
886
+ var pathlength = this.getPolylineLength(points);
887
+ var widthUsed = 0;
888
+
889
+ do {
890
+ var positions = [];
891
+
892
+ for (let i = 0; i < text.length; i++) {
893
+ var letter = text.charAt(i);
894
+ var letterWidth = context.measureText(letter).width;
895
+
896
+ var position = this.getAngleAndCoordsAtLength(
897
+ points,
898
+ widthUsed,
899
+ letterWidth
900
+ );
901
+
902
+ if (widthUsed < pathlength && !!position) {
903
+ var collision = collisions.isPolylineTextCollision(
904
+ context,
905
+ letter,
906
+ position
907
+ );
908
+ var deltaAngle =
909
+ positions.length == 0
910
+ ? 0
911
+ : Math.abs(
912
+ positions[positions.length - 1].angle - position.angle
913
+ );
914
+
915
+ if (!collision && deltaAngle < Math.PI / 6) {
916
+ widthUsed += letterWidth;
917
+ positions.push(position);
918
+ } else {
919
+ // there is a collision, so skip 2 letters and try again to place them
920
+ widthUsed += 2 * letterWidth;
921
+ break;
922
+ }
923
+ } else {
924
+ widthUsed += 2 * letterWidth;
925
+ break;
926
+ }
927
+ }
928
+
929
+ if (positions.length == text.length) {
930
+ // if all letters have been projected
931
+ // count the number of letters which are upside down
932
+ var halfPi = Math.PI / 2;
933
+
934
+ var count = 0;
935
+ for (let i = 0; i < positions.length; i++) {
936
+ if (Math.abs(positions[i].angle) > halfPi) {
937
+ count++;
938
+ }
939
+ }
940
+
941
+ if (count < text.length / 2) {
942
+ // most of the text is upside down, so discard it
943
+ return positions;
944
+ }
945
+ }
946
+ } while (widthUsed < pathlength); // try as long as there is space left
947
+
948
+ return null;
949
+ }
950
+
951
+ /**
952
+ * @private
953
+ */
954
+ drawLetter(
955
+ letter: string,
956
+ position: PositionXY,
957
+ outline: number,
958
+ context: CanvasRenderingContext2D
959
+ ): void {
960
+ var width = context.measureText(letter).width + outline; // XXX
961
+
962
+ var x = position.x + 0.5 * Math.cos(position.angle) * width;
963
+ var y = position.y + 0.5 * Math.sin(position.angle) * width;
964
+
965
+ context.translate(x, y);
966
+ context.rotate(position.angle);
967
+
968
+ if (outline) {
969
+ context.strokeText(letter, 0, 0);
970
+ } else {
971
+ context.fillText(letter, 0, 0);
972
+ }
973
+
974
+ context.rotate(-position.angle);
975
+ context.translate(-x, -y);
976
+ }
977
+
978
+ /**
979
+ * @private
980
+ */
981
+ drawPath(
982
+ points: PointXY[],
983
+ color: string,
984
+ width: number,
985
+ context: CanvasRenderingContext2D
986
+ ): void {
987
+ context.beginPath();
988
+
989
+ context.strokeStyle = color;
990
+ context.lineWidth = width;
991
+
992
+ context.lineCap = "square";
993
+ context.lineJoin = "miter";
994
+
995
+ context.moveTo(points[0].x, points[0].y);
996
+ for (var j = 1; j < points.length; j++) {
997
+ context.lineTo(points[j].x, points[j].y);
998
+ }
999
+ context.stroke();
1000
+ }
1001
+
1002
+ drawImage(
1003
+ coordinate: number[],
1004
+ boundaries: Boundaries,
1005
+ style: PolylineStyle,
1006
+ context: CanvasRenderingContext2D
1007
+ ): void {
1008
+ var merged = { ...VectorTileRenderer.DEFAULT_STYLE.IMAGE, ...style };
1009
+
1010
+ if (merged.url) {
1011
+ if (this._images[merged.url]) {
1012
+ var x = Math.round(
1013
+ (256 * (coordinate[0] - boundaries.min.lon!)) /
1014
+ (boundaries.max.lon! - boundaries.min.lon!)
1015
+ );
1016
+ var y = Math.round(
1017
+ (256 * (coordinate[1] - boundaries.min.lat!)) /
1018
+ (boundaries.max.lat! - boundaries.min.lat!)
1019
+ );
1020
+ if (merged.width && merged.height) {
1021
+ merged.width = Number(merged.width);
1022
+ merged.height = Number(merged.height);
1023
+ context.drawImage(
1024
+ this._images[merged.url],
1025
+ x + merged.dx,
1026
+ y + merged.dy,
1027
+ merged.width,
1028
+ merged.height
1029
+ );
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ /**
1036
+ * @private
1037
+ */
1038
+ isTextCollision(
1039
+ coordinate: GeoJSON.Position,
1040
+ name: string,
1041
+ boundaries: Boundaries,
1042
+ style: PolylineTextStyle,
1043
+ context: CanvasRenderingContext2D,
1044
+ collisions: CollisionDetector
1045
+ ): boolean {
1046
+ var merged = { ...VectorTileRenderer.DEFAULT_STYLE.TEXT, ...style };
1047
+
1048
+ var x = Math.round(
1049
+ (256 * (coordinate[0] - boundaries.min.lon)) /
1050
+ (boundaries.max.lon - boundaries.min.lon)
1051
+ );
1052
+ var y = Math.round(
1053
+ (256 * (coordinate[1] - boundaries.min.lat)) /
1054
+ (boundaries.max.lat - boundaries.min.lat)
1055
+ );
1056
+
1057
+ context.font =
1058
+ merged.fontStyle +
1059
+ " " +
1060
+ merged.fontWeight +
1061
+ " " +
1062
+ merged.size +
1063
+ "px " +
1064
+ merged.font;
1065
+
1066
+ var w = context.measureText(name).width;
1067
+ var h = merged.size; // XXX ???
1068
+
1069
+ x = x - w / 2; // center on x
1070
+
1071
+ return collisions.isCollision(x, y, w, h);
1072
+ }
1073
+
1074
+ /**
1075
+ * @private
1076
+ */
1077
+ drawText(
1078
+ coordinate: GeoJSON.Position,
1079
+ name: string,
1080
+ boundaries: Boundaries,
1081
+ style: PolylineTextStyle,
1082
+ context: CanvasRenderingContext2D,
1083
+ collisions: CollisionDetector
1084
+ ): void {
1085
+ var merged = { ...VectorTileRenderer.DEFAULT_STYLE.TEXT, ...style };
1086
+
1087
+ var x = Math.round(
1088
+ (256 * (coordinate[0] - boundaries.min.lon)) /
1089
+ (boundaries.max.lon - boundaries.min.lon)
1090
+ );
1091
+ var y = Math.round(
1092
+ (256 * (coordinate[1] - boundaries.min.lat)) /
1093
+ (boundaries.max.lat - boundaries.min.lat)
1094
+ );
1095
+
1096
+ context.globalAlpha = merged.opacity;
1097
+ context.font =
1098
+ merged.fontStyle +
1099
+ " " +
1100
+ merged.fontWeight +
1101
+ " " +
1102
+ merged.size +
1103
+ "px " +
1104
+ merged.font;
1105
+
1106
+ var w = context.measureText(name).width;
1107
+ var h = merged.size; // XXX ???
1108
+
1109
+ x = x - w / 2; // center on x
1110
+
1111
+ if (collisions.isCollision(x, y, w, h)) {
1112
+ return;
1113
+ }
1114
+
1115
+ if (merged.outlineColor && merged.outlineWidth) {
1116
+ context.lineWidth = merged.outlineWidth;
1117
+ context.strokeStyle = merged.outlineColor;
1118
+ context.strokeText(name, x, y);
1119
+ }
1120
+
1121
+ context.fillStyle = merged.color;
1122
+ context.fillText(name, x, y);
1123
+
1124
+ collisions.insert(x, y, w, h);
1125
+ }
1126
+
1127
+ preloadImages(): void {
1128
+ for (const name in this._styles?.renderers) {
1129
+ //@ts-ignore not allowing for bracket notation
1130
+ for (let i = 0; i < this._styles.renderers[name].length; i++) {
1131
+ //@ts-ignore not allowing for bracket notation
1132
+ for (const renderer in this._styles.renderers[name][i]) {
1133
+ //@ts-ignore not allowing for bracket notation
1134
+ const config = this._styles.renderers[name][i][renderer];
1135
+
1136
+ if (config.url) {
1137
+ this._images[config.url] = new Image();
1138
+ this._images[config.url].src = config.url; // TODO wait
1139
+
1140
+ this._images[config.url].onerror = ((url: string) => {
1141
+ return () => {
1142
+ delete this._images[url];
1143
+ };
1144
+ })(config.url);
1145
+ }
1146
+ }
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ findConfiguredFonts(): string[] {
1152
+ let result = [];
1153
+
1154
+ for (let renderer in this._styles?.renderers) {
1155
+ //@ts-ignore not allowing for bracket notation
1156
+ for (let i = 0; i < this._styles.renderers[renderer].length; i++) {
1157
+ //@ts-ignore not allowing for bracket notation
1158
+ let text = this._styles.renderers[renderer][i].text;
1159
+ if (text) {
1160
+ let merged = { ...VectorTileRenderer.DEFAULT_STYLE.TEXT, ...text };
1161
+ let font = (
1162
+ merged.fontStyle +
1163
+ " " +
1164
+ merged.fontWeight +
1165
+ " " +
1166
+ merged.size +
1167
+ "px " +
1168
+ merged.font
1169
+ ).trim();
1170
+ result.push(font);
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ return [...new Set(result)];
1176
+ }
1177
+
1178
+ static DEFAULT_STYLE = {
1179
+ POLYGON: {
1180
+ color: "#000000",
1181
+ url: undefined,
1182
+ outlineColor: undefined,
1183
+ outlineWidth: 0,
1184
+ opacity: 1.0,
1185
+ },
1186
+ LINE: {
1187
+ color: "#000000",
1188
+ url: undefined,
1189
+ height: 6,
1190
+ dx: -5,
1191
+ dy: -3,
1192
+ direction: "forward",
1193
+ width: 1,
1194
+ outlineColor: undefined,
1195
+ outlineWidth: 0,
1196
+ dashColor: undefined,
1197
+ dashLength: 0,
1198
+ opacity: 1.0,
1199
+ },
1200
+ IMAGE: {
1201
+ url: undefined,
1202
+ width: 33,
1203
+ height: 20,
1204
+ dx: 10,
1205
+ dy: 10,
1206
+ },
1207
+ TEXT: {
1208
+ color: "black",
1209
+ outlineColor: undefined,
1210
+ outlineWidth: 0,
1211
+ font: "sans-serif",
1212
+ fontWeight: "normal",
1213
+ fontStyle: "normal",
1214
+ size: 12,
1215
+ opacity: 1.0,
1216
+ },
1217
+ };
1218
+ }
1219
+
1220
+ export { VectorTileRenderer };