maplibre-gl 6.2.0 → 6.4.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 (150) hide show
  1. package/dist/maplibre-gl-dev.mjs +1108 -427
  2. package/dist/maplibre-gl-dev.mjs.map +1 -1
  3. package/dist/maplibre-gl-shared-dev.mjs +359 -291
  4. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  5. package/dist/maplibre-gl-shared.mjs +2 -2
  6. package/dist/maplibre-gl-shared.mjs.map +1 -1
  7. package/dist/maplibre-gl-worker-dev.mjs +23 -49
  8. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  9. package/dist/maplibre-gl-worker.mjs +2 -2
  10. package/dist/maplibre-gl-worker.mjs.map +1 -1
  11. package/dist/maplibre-gl.css +1 -1
  12. package/dist/maplibre-gl.d.ts +467 -151
  13. package/dist/maplibre-gl.mjs +49 -49
  14. package/dist/maplibre-gl.mjs.map +1 -1
  15. package/package.json +12 -22
  16. package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
  17. package/src/data/bucket/round_polygon_corners.test.ts +84 -36
  18. package/src/data/bucket/round_polygon_corners.ts +90 -39
  19. package/src/data/extent_bounds.ts +28 -1
  20. package/src/data/feature_index.bench.ts +21 -0
  21. package/src/data/segment.test.ts +2 -2
  22. package/src/geo/edge_insets.test.ts +1 -1
  23. package/src/geo/lng_lat.test.ts +7 -7
  24. package/src/geo/lng_lat_bounds.test.ts +4 -4
  25. package/src/geo/mercator_coordinate.test.ts +2 -2
  26. package/src/geo/projection/camera_helper.ts +5 -0
  27. package/src/geo/projection/globe_transform.test.ts +154 -1
  28. package/src/geo/projection/globe_transform.ts +7 -3
  29. package/src/geo/projection/globe_utils.ts +189 -2
  30. package/src/geo/projection/mercator_camera_helper.ts +1 -1
  31. package/src/geo/projection/mercator_transform.ts +21 -5
  32. package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
  33. package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
  34. package/src/geo/projection/vertical_perspective_transform.ts +7 -1
  35. package/src/geo/transform_interface.ts +13 -1
  36. package/src/index.ts +7 -2
  37. package/src/render/image_atlas.test.ts +180 -0
  38. package/src/render/image_atlas.ts +52 -6
  39. package/src/render/image_manager.test.ts +111 -0
  40. package/src/render/image_manager.ts +143 -144
  41. package/src/render/painter.ts +6 -14
  42. package/src/render/pattern_atlas.test.ts +131 -0
  43. package/src/render/pattern_atlas.ts +144 -0
  44. package/src/render/update_pattern_positions_in_program.test.ts +1 -1
  45. package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
  46. package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
  47. package/src/shaders/glsl/circle.vertex.glsl +4 -4
  48. package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
  49. package/src/shaders/glsl/color_relief.fragment.glsl +1 -2
  50. package/src/shaders/glsl/color_relief.fragment.glsl.g.ts +1 -1
  51. package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
  52. package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
  53. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
  54. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
  55. package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
  56. package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
  57. package/src/shaders/glsl/hillshade_prepare.fragment.glsl +12 -12
  58. package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
  59. package/src/shaders/glsl/line.vertex.glsl +7 -7
  60. package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
  61. package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
  62. package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
  63. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
  64. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
  66. package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
  67. package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
  68. package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
  69. package/src/shaders/glsl/raster.fragment.glsl +4 -4
  70. package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
  71. package/src/shaders/glsl/raster.vertex.glsl +11 -6
  72. package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
  73. package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
  74. package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
  75. package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
  76. package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
  77. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
  78. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
  79. package/src/source/canvas_source.test.ts +31 -0
  80. package/src/source/canvas_source.ts +2 -1
  81. package/src/source/geojson_worker_source.test.ts +88 -0
  82. package/src/source/geojson_worker_source.ts +22 -39
  83. package/src/source/image_source.test.ts +246 -38
  84. package/src/source/image_source.ts +132 -19
  85. package/src/source/raster_dem_tile_source.test.ts +37 -3
  86. package/src/source/raster_dem_tile_source.ts +1 -2
  87. package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
  88. package/src/source/raster_tile_source.test.ts +5 -26
  89. package/src/source/raster_tile_source.ts +4 -3
  90. package/src/source/vector_tile_worker_source.test.ts +94 -2
  91. package/src/source/vector_tile_worker_source.ts +18 -27
  92. package/src/source/video_source.test.ts +12 -8
  93. package/src/source/worker_tile.bench.ts +10 -0
  94. package/src/source/worker_tile.ts +0 -3
  95. package/src/style/create_style_layer.bench.ts +15 -0
  96. package/src/style/properties.bench.ts +81 -0
  97. package/src/style/style.test.ts +75 -10
  98. package/src/style/style.ts +37 -64
  99. package/src/style/style_image.ts +67 -9
  100. package/src/style/style_layer.bench.ts +65 -0
  101. package/src/style/style_layer.test.ts +1 -1
  102. package/src/style/style_layer.ts +2 -2
  103. package/src/style/validate_style.bench.ts +11 -0
  104. package/src/style/validate_style.test.ts +2 -2
  105. package/src/symbol/anchor.test.ts +2 -2
  106. package/src/symbol/collision_index.bench.ts +116 -0
  107. package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
  108. package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
  109. package/src/symbol/cross_tile_symbol_index.ts +67 -37
  110. package/src/symbol/shaping.test.ts +2 -1
  111. package/src/symbol/symbol_layout.bench.ts +30 -0
  112. package/src/tile/tile_id.test.ts +1 -1
  113. package/src/tile/tile_manager.test.ts +69 -1
  114. package/src/tile/tile_manager.ts +16 -11
  115. package/src/ui/control/fullscreen_control.test.ts +10 -10
  116. package/src/ui/control/geolocate_control.test.ts +3 -5
  117. package/src/ui/control/globe_control.test.ts +6 -6
  118. package/src/ui/events.ts +1 -1
  119. package/src/ui/handler/scroll_zoom.test.ts +50 -0
  120. package/src/ui/handler/two_fingers_touch.test.ts +17 -6
  121. package/src/ui/handler_inertia.test.ts +91 -0
  122. package/src/ui/handler_inertia.ts +65 -14
  123. package/src/ui/handler_manager.test.ts +243 -3
  124. package/src/ui/handler_manager.ts +79 -16
  125. package/src/ui/map.ts +17 -3
  126. package/src/ui/map_tests/map_cross_window.test.ts +2 -2
  127. package/src/ui/map_tests/map_events.test.ts +3 -3
  128. package/src/ui/map_tests/map_images.test.ts +55 -0
  129. package/src/ui/map_tests/map_style.test.ts +9 -8
  130. package/src/ui/map_tests/map_webgl.test.ts +30 -0
  131. package/src/ui/marker.test.ts +177 -42
  132. package/src/ui/marker.ts +120 -10
  133. package/src/ui/popup.test.ts +14 -14
  134. package/src/util/evented.test.ts +33 -28
  135. package/src/util/evented.ts +69 -43
  136. package/src/util/image_request.ts +31 -1
  137. package/src/util/test/null_gl.ts +10 -0
  138. package/src/util/test/util.ts +2 -1
  139. package/src/util/util.test.ts +1 -1
  140. package/src/util/web_worker_transfer.test.ts +5 -5
  141. package/src/webgl/context.ts +20 -0
  142. package/src/webgl/draw/draw_background.ts +1 -1
  143. package/src/webgl/draw/draw_raster.ts +11 -6
  144. package/src/webgl/program/pattern.ts +3 -3
  145. package/src/webgl/program/raster_program.ts +11 -0
  146. package/src/webgl/program.ts +31 -16
  147. package/src/webgl/texture.test.ts +16 -1
  148. package/src/webgl/texture.ts +3 -0
  149. package/src/webgl/vertex_buffer.test.ts +17 -2
  150. package/src/webgl/vertex_buffer.ts +24 -13
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maplibre-gl",
3
3
  "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
4
- "version": "6.2.0",
4
+ "version": "6.4.0",
5
5
  "module": "dist/maplibre-gl.mjs",
6
6
  "style": "dist/maplibre-gl.css",
7
7
  "license": "BSD-3-Clause",
@@ -51,7 +51,6 @@
51
51
  "@mapbox/mapbox-gl-rtl-text": "^0.4.0",
52
52
  "@mapbox/mvt-fixtures": "^4.0.0",
53
53
  "@stylistic/eslint-plugin": "^5.10.0",
54
- "@types/benchmark": "^2.1.5",
55
54
  "@types/d3": "^7.4.3",
56
55
  "@types/earcut": "^3.0.0",
57
56
  "@types/eslint": "^9.6.1",
@@ -60,39 +59,36 @@
60
59
  "@types/minimist": "^1.2.5",
61
60
  "@types/murmurhash-js": "^1.0.7",
62
61
  "@types/nise": "^1.4.5",
63
- "@types/node": "^26.1.2",
62
+ "@types/node": "^26.2.0",
64
63
  "@types/offscreencanvas": "^2019.7.3",
65
64
  "@types/pixelmatch": "^5.2.6",
66
65
  "@types/pngjs": "^6.0.5",
67
- "@types/react": "^19.2.18",
68
- "@types/react-dom": "^19.2.4",
69
66
  "@types/request": "^2.48.13",
70
67
  "@types/shuffle-seed": "^1.1.3",
71
68
  "@types/window-or-global": "^1.0.6",
72
- "@typescript-eslint/eslint-plugin": "^8.65.0",
69
+ "@typescript-eslint/eslint-plugin": "^8.67.0",
73
70
  "@typescript-eslint/parser": "^8.59.3",
74
71
  "@typescript/native-preview": "beta",
75
72
  "@unicode/unicode-17.0.0": "^1.6.17",
76
73
  "@vitest/coverage-v8": "4.1.10",
77
- "@vitest/eslint-plugin": "^1.6.24",
74
+ "@vitest/eslint-plugin": "^1.6.27",
78
75
  "@vitest/ui": "4.1.10",
79
76
  "address": "^2.0.3",
80
77
  "autoprefixer": "^10.5.4",
81
- "benchmark": "^2.1.4",
82
78
  "canvas": "^3.2.3",
83
79
  "cross-env": "^10.1.0",
84
80
  "cspell": "^10.0.1",
85
- "cssnano": "^8.0.2",
81
+ "cssnano": "^8.0.5",
86
82
  "d3": "^7.9.0",
87
83
  "d3-queue": "^3.0.7",
88
- "devtools-protocol": "^0.0.1672245",
84
+ "devtools-protocol": "^0.0.1676914",
89
85
  "diff": "^9.0.0",
90
- "eslint": "^10.8.0",
86
+ "eslint": "^10.8.1",
91
87
  "eslint-plugin-html": "^8.1.4",
92
88
  "eslint-plugin-tsdoc": "0.5.2",
93
89
  "expect": "^30.4.1",
94
90
  "glob": "^13.0.6",
95
- "globals": "^17.8.0",
91
+ "globals": "^17.9.0",
96
92
  "is-builtin-module": "^5.0.0",
97
93
  "jsdom": "^30.0.1",
98
94
  "minimist": "^1.2.8",
@@ -108,11 +104,9 @@
108
104
  "postcss-cli": "^11.0.1",
109
105
  "postcss-inline-svg": "^6.0.0",
110
106
  "pretty-bytes": "^7.1.1",
111
- "puppeteer": "^25.4.0",
112
- "react": "^19.2.8",
113
- "react-dom": "^19.2.8",
107
+ "puppeteer": "^25.5.0",
114
108
  "regenerate": "^1.4.2",
115
- "rolldown": "^1.2.1",
109
+ "rolldown": "^1.2.3",
116
110
  "rolldown-plugin-dts": "^0.27.8",
117
111
  "rw": "^1.3.3",
118
112
  "semver": "^7.8.5",
@@ -125,7 +119,7 @@
125
119
  "typedoc": "^0.28.20",
126
120
  "typedoc-plugin-markdown": "^4.11.0",
127
121
  "typescript": "^6.0.3",
128
- "vite": "^8.2.0",
122
+ "vite": "^8.2.1",
129
123
  "vitest": "^4.1.5",
130
124
  "vitest-webgl-canvas-mock": "^1.1.0"
131
125
  },
@@ -143,14 +137,11 @@
143
137
  "build-prod": "cross-env BUILD=production rolldown -c rolldown.config.ts",
144
138
  "build-css": "postcss -o dist/maplibre-gl.css src/css/maplibre-gl.css",
145
139
  "watch-css": "postcss --watch -o dist/maplibre-gl.css src/css/maplibre-gl.css",
146
- "build-benchmarks": "npm run build-dev && rolldown -c test/bench/rolldown_config_benchmarks.ts",
147
- "watch-benchmarks": "rolldown -c test/bench/rolldown_config_benchmarks.ts --watch",
148
140
  "spellcheck": "cspell",
149
141
  "docs": "npm run generate-docs && docker compose run --rm docs",
150
142
  "start-server": "st --no-cache -H localhost --port 9966 .",
151
143
  "start-docs": "docker compose run --rm -it --service-ports docs-serve",
152
144
  "start": "run-p watch-css watch-dev start-server",
153
- "start-bench": "run-p watch-css watch-benchmarks start-server",
154
145
  "lint": "eslint",
155
146
  "lint-css": "stylelint **/*.css verbose",
156
147
  "test": "run-p lint lint-css test-render test-unit test-integration test-build",
@@ -164,8 +155,7 @@
164
155
  "test-render": "vitest test/integration/render/render.test.ts",
165
156
  "codegen": "run-p --print-label generate-style-code generate-unicode-data generate-struct-arrays generate-shaders && npm run generate-typings",
166
157
  "bench": "vitest bench --run --config vitest.config.bench.ts",
167
- "benchmark": "node test/bench/run-benchmarks.ts",
168
- "gl-stats": "node test/bench/gl-stats.ts",
158
+ "bench-e2e": "node test/bench/e2e/run.ts",
169
159
  "prepare": "npm run codegen",
170
160
  "typecheck": "tsgo --noEmit && tsgo --project tsconfig.dist.json"
171
161
  },
@@ -4,7 +4,7 @@ import {members as layoutAttributes, centroidAttributes} from './fill_extrusion_
4
4
  import {type Segment, SegmentVector} from '../segment.ts';
5
5
  import {ProgramConfigurationSet} from '../program_configuration.ts';
6
6
  import {TriangleIndexArray} from '../index_array_type.ts';
7
- import {EXTENT} from '../extent.ts';
7
+ import {isBoundaryEdge, isEntirelyOutside} from '../extent_bounds.ts';
8
8
  import {VectorTileFeature} from '@mapbox/vector-tile';
9
9
  import {classifyRings} from '@maplibre/maplibre-gl-style-spec';
10
10
  const EARCUT_MAX_RINGS = 500;
@@ -335,15 +335,3 @@ function accumulatePointsToCentroid(centroid: CentroidAccumulator, geometry: Poi
335
335
  }
336
336
 
337
337
  register('FillExtrusionBucket', FillExtrusionBucket, {omit: ['layers', 'features']});
338
-
339
- function isBoundaryEdge(p1, p2) {
340
- return (p1.x === p2.x && (p1.x < 0 || p1.x > EXTENT)) ||
341
- (p1.y === p2.y && (p1.y < 0 || p1.y > EXTENT));
342
- }
343
-
344
- function isEntirelyOutside(ring) {
345
- return ring.every(p => p.x < 0) ||
346
- ring.every(p => p.x > EXTENT) ||
347
- ring.every(p => p.y < 0) ||
348
- ring.every(p => p.y > EXTENT);
349
- }
@@ -10,6 +10,14 @@ function round(p: Point):GeoJSON.Position {
10
10
  return [x, y];
11
11
  }
12
12
 
13
+ const square = [
14
+ new Point(0, 0),
15
+ new Point(10, 0),
16
+ new Point(10, 10),
17
+ new Point(0, 10),
18
+ new Point(0, 0)
19
+ ];
20
+
13
21
  describe('roundPolygonCorners', () => {
14
22
  const canonical = new CanonicalTileID(10, 500, 300);
15
23
 
@@ -50,58 +58,102 @@ describe('roundPolygonCorners', () => {
50
58
  expect(output).toStrictEqual(input);
51
59
  });
52
60
 
53
- test('still rounds very small rounding', () => {
54
- const input = [[
55
- new Point(0, 0),
56
- new Point(10, 0),
57
- new Point(10, 10),
58
- new Point(0, 10),
59
- new Point(0, 0)
60
- ]];
61
+ test('collapses every arc back onto its corner when the distance is below one tile unit', () => {
62
+ const output = roundPolygonCorners([square], 0.1, canonical);
61
63
 
62
- const output = roundPolygonCorners(input, 0.1, canonical);
64
+ expect(output[0].map(round)).toEqual(square.map(round));
65
+ });
66
+
67
+ test('rounds corners of a square polygon into arc vertices', () => {
68
+ const output = roundPolygonCorners([square], 2, canonical);
63
69
  const points = output[0].map(round);
64
70
 
65
71
  expect(points).toEqual([
66
- [0, 0.04], [0.01, 0.02], [0.02, 0.01], [0.04, 0], [9.96, 0], [9.98, 0.01], [9.99, 0.02], [10, 0.04],
67
- [10, 9.96], [9.99, 9.98], [9.98, 9.99], [9.96, 10], [0.04, 10], [0.02, 9.99], [0.01, 9.98], [0, 9.96], [0, 0.04]
72
+ [0, 1], [0, 0], [1, 0], [9, 0], [10, 0], [10, 1],
73
+ [10, 9], [10, 10], [9, 10], [1, 10], [0, 10], [0, 9], [0, 1]
68
74
  ]);
69
75
  });
70
76
 
71
- test('rounds corners of a square polygon into arc vertices', () => {
77
+ test('clamps corner rounding distance to 20% of edge length when requested distance is large', () => {
78
+ const output = roundPolygonCorners([square], 1000, canonical);
79
+ const points = output[0].map(round);
80
+
81
+ expect(points).toEqual([
82
+ [0, 2], [0, 1], [1, 0], [2, 0], [8, 0], [9, 0], [10, 1], [10, 2],
83
+ [10, 8], [10, 9], [9, 10], [8, 10], [2, 10], [1, 10], [0, 9], [0, 8], [0, 2]
84
+ ]);
85
+ });
86
+
87
+ test('emits only integer tile coordinates, so that the triangulator cannot merge arc points itself', () => {
72
88
  const input = [[
73
89
  new Point(0, 0),
74
- new Point(10, 0),
75
- new Point(10, 10),
76
- new Point(0, 10),
90
+ new Point(137, 41),
91
+ new Point(96, 158),
92
+ new Point(13, 111),
77
93
  new Point(0, 0)
78
94
  ]];
79
95
 
80
- const output = roundPolygonCorners(input, 2, canonical);
96
+ for (const distance of [0.5, 1, 2, 5, 50, 1000]) {
97
+ for (const ring of roundPolygonCorners(input, distance, canonical)) {
98
+ for (const point of ring) {
99
+ expect(point.x).toBe(Math.round(point.x));
100
+ expect(point.y).toBe(Math.round(point.y));
101
+ }
102
+ }
103
+ }
104
+ });
105
+
106
+ test('leaves the corners of a cut sharp and rounds the rest of the same ring', () => {
107
+ const input = [[
108
+ new Point(1000, -100),
109
+ new Point(3000, -100),
110
+ new Point(3000, 4000),
111
+ new Point(1000, 4000),
112
+ new Point(1000, -100)
113
+ ]];
114
+
115
+ const output = roundPolygonCorners(input, 1000, canonical);
81
116
  const points = output[0].map(round);
82
117
 
83
118
  expect(points).toEqual([
84
- [0, 0.82], [0.11, 0.41], [0.41, 0.11], [0.82, 0], [9.18, 0], [9.59, 0.11], [9.89, 0.41], [10, 0.82],
85
- [10, 9.18], [9.89, 9.59], [9.59, 9.89], [9.18, 10], [0.82, 10], [0.41, 9.89], [0.11, 9.59], [0, 9.18], [0, 0.82]
119
+ [1000, -100],
120
+ [3000, -100],
121
+ [3000, 3600],
122
+ [2946, 3800],
123
+ [2800, 3946],
124
+ [2600, 4000],
125
+ [1400, 4000],
126
+ [1200, 3946],
127
+ [1054, 3800],
128
+ [1000, 3600],
129
+ [1000, -100]
86
130
  ]);
87
131
  });
88
132
 
89
- test('clamps corner rounding distance to 20% of edge length when requested distance is large', () => {
133
+ test('rounds a corner of the feature that sits in the buffer, since only a cut stays sharp', () => {
90
134
  const input = [[
91
- new Point(0, 0),
92
- new Point(10, 0),
93
- new Point(10, 10),
94
- new Point(0, 10),
95
- new Point(0, 0)
135
+ new Point(1200, 200),
136
+ new Point(2000, -100),
137
+ new Point(2800, 200),
138
+ new Point(2000, 3000),
139
+ new Point(1200, 200)
96
140
  ]];
97
141
 
98
142
  const output = roundPolygonCorners(input, 1000, canonical);
99
143
  const points = output[0].map(round);
100
144
 
101
- expect(points).toEqual([
102
- [0, 2], [0.27, 1], [1, 0.27], [2, 0], [8, 0], [9, 0.27], [9.73, 1], [10, 2],
103
- [10, 8], [9.73, 9], [9, 9.73], [8, 10], [2, 10], [1, 9.73], [0.27, 9], [0, 8], [0, 2]
104
- ]);
145
+ expect(points).not.toContainEqual([2000, -100]);
146
+ expect(output[0].length).toBeGreaterThan(input[0].length);
147
+ });
148
+
149
+ test('drops arc points that collapse onto their neighbour', () => {
150
+ const output = roundPolygonCorners([square], 2, canonical);
151
+ const ring = output[0];
152
+
153
+ for (let i = 1; i < ring.length; i++) {
154
+ expect([ring[i].x, ring[i].y]).not.toEqual([ring[i - 1].x, ring[i - 1].y]);
155
+ }
156
+ expect([ring[0].x, ring[0].y]).toEqual([ring[ring.length - 1].x, ring[ring.length - 1].y]);
105
157
  });
106
158
 
107
159
  test('preserves near-zero-degree spike vertex without adding arc points', () => {
@@ -115,10 +167,7 @@ describe('roundPolygonCorners', () => {
115
167
  const output = roundPolygonCorners(input, 5, canonical);
116
168
  const points = output[0].map(round);
117
169
 
118
- expect(points).toEqual([
119
- [0, 0.08], [0.01, 0.04], [0.04, 0.01], [0.08, 0], [100, 0], [0.08, 0.4],
120
- [0.05, 0.39], [0.02, 0.38], [0.01, 0.35], [0, 0.32], [0, 0.08]
121
- ]);
170
+ expect(points).toEqual([[0, 0], [100, 0], [0, 0.4], [0, 0]]);
122
171
  });
123
172
 
124
173
  test('preserves collinear 180-degree vertices without adding arc points', () => {
@@ -135,8 +184,8 @@ describe('roundPolygonCorners', () => {
135
184
  const points = output[0].map(round);
136
185
 
137
186
  expect(points).toEqual([
138
- [0, 1], [0.13, 0.5], [0.5, 0.13], [1, 0], [5, 0], [9, 0], [9.5, 0.13], [9.87, 0.5], [10, 1],
139
- [10, 8], [9.73, 9], [9, 9.73], [8, 10], [2, 10], [1, 9.73], [0.27, 9], [0, 8], [0, 1]
187
+ [0, 1], [0, 0], [1, 0], [5, 0], [9, 0], [10, 0], [10, 1],
188
+ [10, 8], [10, 9], [9, 10], [8, 10], [2, 10], [1, 10], [0, 9], [0, 8], [0, 1]
140
189
  ]);
141
190
  });
142
191
 
@@ -152,8 +201,7 @@ describe('roundPolygonCorners', () => {
152
201
  const points = output[0].map(round);
153
202
 
154
203
  expect(points).toEqual([
155
- [1.91, 0.77], [1.77, 0.66], [1.68, 0.5], [1.67, 0.32], [1.75, 0.16], [1.88, 0.04], [2.06, 0], [97.94, 0], [98.12, 0.04],
156
- [98.25, 0.16], [98.33, 0.32], [98.32, 0.5], [98.23, 0.66], [98.09, 0.77], [51.91, 19.23], [50, 19.6], [48.09, 19.23], [1.91, 0.77]
204
+ [2, 1], [2, 0], [98, 0], [98, 1], [52, 19], [50, 20], [48, 19], [2, 1]
157
205
  ]);
158
206
  });
159
207
  });
@@ -1,8 +1,8 @@
1
- import Point from '@mapbox/point-geometry';
2
- import {vec2} from 'gl-matrix';
3
1
  import {EXTENT} from '../extent.ts';
2
+ import {isBoundaryEdge} from '../extent_bounds.ts';
4
3
  import {MercatorCoordinate} from '../../geo/mercator_coordinate.ts';
5
4
  import {tileCoordinatesToLocation} from '../../geo/projection/mercator_utils.ts';
5
+ import type Point from '@mapbox/point-geometry';
6
6
  import type {CanonicalTileID} from '../../tile/tile_id.ts';
7
7
 
8
8
  /**
@@ -32,6 +32,19 @@ function getTileUnitsForMeters(distanceInMeters: number, canonical: CanonicalTil
32
32
  return distanceInMeters * meterInMercator * tileUnitsPerMercator;
33
33
  }
34
34
 
35
+ /**
36
+ * Rounds the corners of a single ring.
37
+ *
38
+ * Corners that tile clipping created are left sharp: they belong to the cut rather than to the
39
+ * feature, and the neighbouring tile cuts the same feature elsewhere, so rounding them would leave
40
+ * the two halves out of step. Every vertex ends up on the integer tile grid, because triangulation,
41
+ * subdivision and the vertex buffers snap and deduplicate vertices there - arcs finer than a tile
42
+ * unit would otherwise be merged only after they were triangulated, turning the mesh into spikes.
43
+ *
44
+ * A ring that collapses into fewer than three distinct vertices is returned unchanged.
45
+ * @param ring - Ring to round, closed or open
46
+ * @param distanceInTileUnits - Corner rounding distance, already converted to tile units
47
+ */
35
48
  function roundRing(ring: Point[], distanceInTileUnits: number): Point[] {
36
49
  if (!ring || ring.length < 3) {
37
50
  return ring;
@@ -44,49 +57,94 @@ function roundRing(ring: Point[], distanceInTileUnits: number): Point[] {
44
57
  return ring;
45
58
  }
46
59
 
47
- const vertices: vec2[] = ring.map(p => vec2.fromValues(p.x,p.y));
48
- const newRing: vec2[] = [];
60
+ const newRing: Point[] = [];
49
61
 
50
62
  for (let i = 0; i < vertexCount; i++) {
51
- const prev = vertices[(i - 1 + vertexCount) % vertexCount];
52
- const current = vertices[i];
53
- const next = vertices[(i + 1) % vertexCount];
63
+ const previous = ring[(i - 1 + vertexCount) % vertexCount];
64
+ const current = ring[i];
65
+ const next = ring[(i + 1) % vertexCount];
66
+
67
+ if (isBoundaryEdge(previous, current) || isBoundaryEdge(current, next)) {
68
+ newRing.push(current.clone());
69
+ continue;
70
+ }
54
71
 
55
- appendRoundCorner(newRing, prev, current, next, distanceInTileUnits);
72
+ appendRoundCorner(newRing, previous, current, next, distanceInTileUnits);
56
73
  }
57
74
 
58
- if (isClosed && newRing.length > 0) {
59
- newRing.push(vec2.clone(newRing[0]));
75
+ const snapped = snapToIntegerGrid(newRing);
76
+
77
+ if (snapped.length < 3) {
78
+ return ring;
60
79
  }
61
80
 
62
- return newRing.map(p => new Point(p[0], p[1]));
81
+ if (isClosed) {
82
+ snapped.push(snapped[0].clone());
83
+ }
84
+
85
+ return snapped;
63
86
  }
64
87
 
88
+ /**
89
+ * Rounds every vertex to the integer tile grid, dropping vertices that collapse onto their neighbour.
90
+ * The ring is treated as closed, so the wrap-around duplicate is dropped as well.
91
+ * @param ring - Ring to snap
92
+ */
93
+ function snapToIntegerGrid(ring: Point[]): Point[] {
94
+ const snapped: Point[] = [];
95
+
96
+ for (const p of ring) {
97
+ const point = p.round();
98
+ const previous = snapped[snapped.length - 1];
99
+
100
+ if (previous?.x === point.x && previous?.y === point.y) {
101
+ continue;
102
+ }
103
+
104
+ snapped.push(point);
105
+ }
106
+
107
+ while (snapped.length > 1 && snapped[0].x === snapped[snapped.length - 1].x && snapped[0].y === snapped[snapped.length - 1].y) {
108
+ snapped.pop();
109
+ }
110
+
111
+ return snapped;
112
+ }
113
+
114
+ /**
115
+ * Appends the arc that replaces one corner, or the corner itself when it is too shallow or too sharp
116
+ * to round.
117
+ * @param newRing - Ring being built, the arc points are appended to it
118
+ * @param prev - Vertex before the corner
119
+ * @param current - The corner
120
+ * @param next - Vertex after the corner
121
+ * @param distanceInTileUnits - Corner rounding distance, already converted to tile units
122
+ */
65
123
  function appendRoundCorner(
66
- newRing: vec2[],
67
- prev: vec2,
68
- current: vec2,
69
- next: vec2,
124
+ newRing: Point[],
125
+ prev: Point,
126
+ current: Point,
127
+ next: Point,
70
128
  distanceInTileUnits: number
71
129
  ): void {
72
130
  // Unit edge vectors from the current vertex towards its neighbours
73
- const ua = vec2.sub(vec2.create(), prev, current);
74
- const ub = vec2.sub(vec2.create(), next, current);
75
- const lenA = vec2.length(ua);
76
- const lenB = vec2.length(ub);
131
+ const ua = prev.sub(current);
132
+ const ub = next.sub(current);
133
+ const lenA = ua.mag();
134
+ const lenB = ub.mag();
77
135
 
78
136
  if (lenA < 1e-6 || lenB < 1e-6) {
79
- newRing.push(vec2.clone(current));
137
+ newRing.push(current.clone());
80
138
  return;
81
139
  }
82
140
 
83
- vec2.scale(ua, ua, 1 / lenA);
84
- vec2.scale(ub, ub, 1 / lenB);
141
+ ua._div(lenA);
142
+ ub._div(lenB);
85
143
 
86
144
  // Straight lines or zero-degree turns
87
- const dot = vec2.dot(ua, ub);
145
+ const dot = ua.x * ub.x + ua.y * ub.y;
88
146
  if (Math.abs(dot) > Math.cos(5 * Math.PI / 180)) {
89
- newRing.push(vec2.clone(current));
147
+ newRing.push(current.clone());
90
148
  return;
91
149
  }
92
150
 
@@ -95,27 +153,20 @@ function appendRoundCorner(
95
153
  const r = Math.min(distanceInTileUnits, lenA * maxEdgeLenPercent, lenB * maxEdgeLenPercent);
96
154
 
97
155
  // Tangent points on edges to prevPoint and nextPoint
98
- const tangentA = vec2.scaleAndAdd(vec2.create(), current, ua, r);
99
- const tangentB = vec2.scaleAndAdd(vec2.create(), current, ub, r);
100
-
101
- const bisector = vec2.add(vec2.create(), ua, ub);
102
- vec2.normalize(bisector, bisector);
156
+ const tangentA = current.add(ua.mult(r));
157
+ const tangentB = current.add(ub.mult(r));
103
158
 
104
159
  // Center of the rounding arc, at r / cos(theta/2) along the bisector
105
160
  const cosHalfTheta = Math.sqrt((1 + dot) / 2);
106
- const center = vec2.scaleAndAdd(vec2.create(), current, bisector, r / cosHalfTheta);
161
+ const center = current.add(ua.add(ub)._unit()._mult(r / cosHalfTheta));
107
162
 
108
- // Both tangent points lie on the arc circle.
109
- // Rotating tangent A around the center traces the fillet onto tangent B along the shortest arc.
110
- const radiusA = vec2.sub(vec2.create(), tangentA, center);
111
- const radiusB = vec2.sub(vec2.create(), tangentB, center);
112
- const sweepAngle = vec2.angle(radiusA, radiusB);
113
- const direction = Math.sign(radiusA[0] * radiusB[1] - radiusA[1] * radiusB[0]); // 2D cross product -> winding direction
163
+ // Both tangent points lie on the arc circle, so rotating tangent A around the center by the angle
164
+ // between the two radii traces the fillet onto tangent B along the shortest arc.
165
+ const sweepAngle = tangentA.sub(center).angleWith(tangentB.sub(center));
114
166
 
115
167
  // ~30 deg per segment; epsilon keeps fp noise from adding one at exact multiples.
116
- const numSegments = Math.max(2, Math.ceil(sweepAngle / (Math.PI / 6) - 1e-6));
168
+ const numSegments = Math.max(2, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 6) - 1e-6));
117
169
  for (let s = 0; s <= numSegments; s++) {
118
- const angle = direction * sweepAngle * (s / numSegments);
119
- newRing.push(vec2.rotate(vec2.create(), tangentA, center, angle));
170
+ newRing.push(tangentA.rotateAround(sweepAngle * (s / numSegments), center));
120
171
  }
121
172
  }
@@ -1,8 +1,35 @@
1
1
  import Point from '@mapbox/point-geometry';
2
+ import {type Point2D} from '@maplibre/maplibre-gl-style-spec';
2
3
  import {Bounds, type ReadOnlyBounds} from '../geo/bounds.ts';
3
4
  import {EXTENT} from './extent.ts';
4
5
 
5
6
  /**
6
7
  * The bounding box covering the entire extent of a tile.
7
8
  */
8
- export const EXTENT_BOUNDS = Bounds.fromPoints([new Point(0, 0), new Point(EXTENT, EXTENT)]) as ReadOnlyBounds;
9
+ export const EXTENT_BOUNDS = Bounds.fromPoints([new Point(0, 0), new Point(EXTENT, EXTENT)]) as ReadOnlyBounds;
10
+
11
+ /**
12
+ * Whether an edge runs along one of the lines the tile was cut out of the world at. Clipping happens
13
+ * on the buffer rectangle, outside the tile, so an axis parallel edge out there belongs to the cut
14
+ * rather than to the feature. Such an edge is shared with the neighbouring tile, which cuts the same
15
+ * feature somewhere else, so geometry derived from it has to stay identical in both tiles.
16
+ * @param p1 - First vertex of the edge
17
+ * @param p2 - Second vertex of the edge
18
+ * @returns True when the edge lies on a clip line.
19
+ */
20
+ export function isBoundaryEdge(p1: Point2D, p2: Point2D): boolean {
21
+ return (p1.x === p2.x && (p1.x < 0 || p1.x > EXTENT)) ||
22
+ (p1.y === p2.y && (p1.y < 0 || p1.y > EXTENT));
23
+ }
24
+
25
+ /**
26
+ * Whether a ring lies completely off one side of the tile, and so cannot contribute a visible pixel.
27
+ * @param ring - Ring to test
28
+ * @returns True when every vertex is beyond the same edge of the tile.
29
+ */
30
+ export function isEntirelyOutside(ring: Point2D[]): boolean {
31
+ return ring.every(p => p.x < 0) ||
32
+ ring.every(p => p.x > EXTENT) ||
33
+ ring.every(p => p.y < 0) ||
34
+ ring.every(p => p.y > EXTENT);
35
+ }
@@ -0,0 +1,21 @@
1
+ import {bench} from 'vitest';
2
+ import {FeatureIndex} from './feature_index.ts';
3
+ import {OverscaledTileID} from '../tile/tile_id.ts';
4
+
5
+ import type {FeatureFilter} from '@maplibre/maplibre-gl-style-spec';
6
+ import type {VectorTileFeatureLike, VectorTileLayerLike} from '@maplibre/vt-pbf';
7
+
8
+ const layerCount = 100;
9
+ const filter = {needGeometry: false} as FeatureFilter;
10
+
11
+ const featureIndex = new FeatureIndex(new OverscaledTileID(0, 0, 0, 0, 0));
12
+ featureIndex.bucketLayerIDs = [Array.from({length: layerCount}, (_, i) => `layer-${i}`)];
13
+ featureIndex.vtLayers = {
14
+ '0': {feature: () => ({} as VectorTileFeatureLike)} as unknown as VectorTileLayerLike
15
+ };
16
+
17
+ const layerIdsToTest = new Set(Array.from({length: layerCount}, (_, i) => `non-existing-layer-${i}`));
18
+
19
+ bench('FeatureIndex.loadMatchingFeature', () => {
20
+ featureIndex.loadMatchingFeature({}, 0, 0, 0, filter, layerIdsToTest, [], {}, {});
21
+ });
@@ -4,13 +4,13 @@ import {SegmentVector} from './segment.ts';
4
4
 
5
5
  describe('SegmentVector', () => {
6
6
  test('constructor', () => {
7
- expect(new SegmentVector() instanceof SegmentVector).toBeTruthy();
7
+ expect(new SegmentVector()).toBeInstanceOf(SegmentVector);
8
8
  });
9
9
 
10
10
  test('simpleSegment', () => {
11
11
  SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
12
12
  const segmentVector = SegmentVector.simpleSegment(0, 0, 10, 0);
13
- expect(segmentVector instanceof SegmentVector).toBeTruthy();
13
+ expect(segmentVector).toBeInstanceOf(SegmentVector);
14
14
  expect(segmentVector.segments).toHaveLength(1);
15
15
  expect(segmentVector.segments[0].vertexLength).toBe(10);
16
16
  });
@@ -4,7 +4,7 @@ import {EdgeInsets} from '../geo/edge_insets.ts';
4
4
  describe('EdgeInsets', () => {
5
5
  describe('constructor', () => {
6
6
  test('creates an object with default values', () => {
7
- expect(new EdgeInsets() instanceof EdgeInsets).toBeTruthy();
7
+ expect(new EdgeInsets()).toBeInstanceOf(EdgeInsets);
8
8
  });
9
9
 
10
10
  test('invalid initialization', () => {
@@ -3,7 +3,7 @@ import {LngLat} from '../geo/lng_lat.ts';
3
3
 
4
4
  describe('LngLat', () => {
5
5
  test('constructor', () => {
6
- expect(new LngLat(0, 0) instanceof LngLat).toBeTruthy();
6
+ expect(new LngLat(0, 0)).toBeInstanceOf(LngLat);
7
7
 
8
8
  expect(() => {
9
9
  new LngLat(0, -91);
@@ -15,12 +15,12 @@ describe('LngLat', () => {
15
15
  });
16
16
 
17
17
  test('convert', () => {
18
- expect(LngLat.convert([0, 10]) instanceof LngLat).toBeTruthy();
19
- expect(LngLat.convert({lng: 0, lat: 10}) instanceof LngLat).toBeTruthy();
20
- expect(LngLat.convert({lng: 0, lat: 0}) instanceof LngLat).toBeTruthy();
21
- expect(LngLat.convert({lon: 0, lat: 10}) instanceof LngLat).toBeTruthy();
22
- expect(LngLat.convert({lon: 0, lat: 0}) instanceof LngLat).toBeTruthy();
23
- expect(LngLat.convert(new LngLat(0, 0)) instanceof LngLat).toBeTruthy();
18
+ expect(LngLat.convert([0, 10])).toBeInstanceOf(LngLat);
19
+ expect(LngLat.convert({lng: 0, lat: 10})).toBeInstanceOf(LngLat);
20
+ expect(LngLat.convert({lng: 0, lat: 0})).toBeInstanceOf(LngLat);
21
+ expect(LngLat.convert({lon: 0, lat: 10})).toBeInstanceOf(LngLat);
22
+ expect(LngLat.convert({lon: 0, lat: 0})).toBeInstanceOf(LngLat);
23
+ expect(LngLat.convert(new LngLat(0, 0))).toBeInstanceOf(LngLat);
24
24
  });
25
25
 
26
26
  test('wrap', () => {
@@ -336,25 +336,25 @@ describe('LngLatBounds', () => {
336
336
  test('point is in bounds', () => {
337
337
  const llb = new LngLatBounds([-1, -1], [1, 1]);
338
338
  const ll = {lng: 0, lat: 0};
339
- expect(llb.contains(ll)).toBeTruthy();
339
+ expect(llb.contains(ll)).toBe(true);
340
340
  });
341
341
 
342
342
  test('point is not in bounds', () => {
343
343
  const llb = new LngLatBounds([-1, -1], [1, 1]);
344
344
  const ll = {lng: 3, lat: 3};
345
- expect(llb.contains(ll)).toBeFalsy();
345
+ expect(llb.contains(ll)).toBe(false);
346
346
  });
347
347
 
348
348
  test('point is in bounds that spans dateline', () => {
349
349
  const llb = new LngLatBounds([190, -10], [170, 10]);
350
350
  const ll = {lng: 180, lat: 0};
351
- expect(llb.contains(ll)).toBeTruthy();
351
+ expect(llb.contains(ll)).toBe(true);
352
352
  });
353
353
 
354
354
  test('point is not in bounds that spans dateline', () => {
355
355
  const llb = new LngLatBounds([190, -10], [170, 10]);
356
356
  const ll = {lng: 0, lat: 0};
357
- expect(llb.contains(ll)).toBeFalsy();
357
+ expect(llb.contains(ll)).toBe(false);
358
358
  });
359
359
  });
360
360
  });
@@ -4,8 +4,8 @@ import {MercatorCoordinate, mercatorScale} from './mercator_coordinate.ts';
4
4
 
5
5
  describe('LngLat', () => {
6
6
  test('constructor', () => {
7
- expect(new MercatorCoordinate(0, 0) instanceof MercatorCoordinate).toBeTruthy();
8
- expect(new MercatorCoordinate(0, 0, 0) instanceof MercatorCoordinate).toBeTruthy();
7
+ expect(new MercatorCoordinate(0, 0)).toBeInstanceOf(MercatorCoordinate);
8
+ expect(new MercatorCoordinate(0, 0, 0)).toBeInstanceOf(MercatorCoordinate);
9
9
  });
10
10
 
11
11
  test('fromLngLat', () => {