@maptrip/map-tiles 1.1.1 → 1.2.0

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