maplibre-gl 2.0.3 → 2.1.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 (58) hide show
  1. package/build/rollup_plugins.js +1 -0
  2. package/dist/maplibre-gl-dev.js +54589 -0
  3. package/dist/maplibre-gl.css +1 -1
  4. package/dist/maplibre-gl.d.ts +49 -15
  5. package/dist/maplibre-gl.js +3 -3
  6. package/dist/maplibre-gl.js.map +1 -1
  7. package/dist/package.json +1 -0
  8. package/package.json +78 -100
  9. package/src/css/maplibre-gl.css +36 -36
  10. package/src/data/bucket/symbol_bucket.test.ts +1 -1
  11. package/src/data/bucket/symbol_bucket.ts +3 -3
  12. package/src/data/program_configuration.ts +1 -1
  13. package/src/render/draw_debug.ts +1 -1
  14. package/src/render/glyph_manager.ts +1 -1
  15. package/src/render/painter.ts +5 -3
  16. package/src/render/program/circle_program.ts +1 -1
  17. package/src/render/program/line_program.ts +3 -3
  18. package/src/render/program/symbol_program.ts +1 -1
  19. package/src/source/geojson_source.test.ts +2 -1
  20. package/src/source/geojson_source.ts +1 -1
  21. package/src/source/image_source.test.ts +153 -0
  22. package/src/source/raster_dem_tile_source.test.ts +2 -1
  23. package/src/source/raster_dem_tile_source.ts +1 -1
  24. package/src/source/raster_tile_source.test.ts +2 -1
  25. package/src/source/raster_tile_source.ts +1 -1
  26. package/src/source/source_cache.test.ts +1 -1
  27. package/src/source/tile.test.ts +1 -1
  28. package/src/source/tile_cache.test.ts +6 -4
  29. package/src/source/tile_id.test.ts +10 -12
  30. package/src/source/tile_id.ts +2 -2
  31. package/src/source/vector_tile_source.test.ts +14 -2
  32. package/src/source/vector_tile_source.ts +3 -4
  33. package/src/style/load_glyph_range.test.ts +0 -2
  34. package/src/style/load_sprite.ts +2 -1
  35. package/src/style/properties.ts +1 -1
  36. package/src/style/style.test.ts +24 -13
  37. package/src/style/style.ts +1 -1
  38. package/src/style/style_layer/custom_style_layer.ts +2 -2
  39. package/src/style/style_layer/fill_extrusion_style_layer.ts +1 -1
  40. package/src/style/style_layer/symbol_style_layer.ts +19 -0
  41. package/src/style/style_layer/symbol_style_layer_properties.ts +6 -0
  42. package/src/style-spec/CHANGELOG.md +6 -0
  43. package/src/style-spec/package.json +1 -1
  44. package/src/style-spec/reference/v8.json +68 -2
  45. package/src/style-spec/types.ts +2 -0
  46. package/src/symbol/collision_index.ts +19 -19
  47. package/src/symbol/grid_index.test.ts +42 -19
  48. package/src/symbol/grid_index.ts +62 -33
  49. package/src/symbol/placement.ts +82 -53
  50. package/src/symbol/symbol_style_layer.test.ts +48 -1
  51. package/src/ui/camera.test.ts +4 -4
  52. package/src/ui/camera.ts +8 -0
  53. package/src/ui/control/logo_control.test.ts +1 -0
  54. package/src/ui/handler/scroll_zoom.test.ts +2 -1
  55. package/src/ui/map.test.ts +77 -10
  56. package/src/ui/map.ts +33 -8
  57. package/src/util/ajax.test.ts +206 -0
  58. package/src/util/test/util.ts +14 -0
@@ -185,4 +185,23 @@ class SymbolStyleLayer extends StyleLayer {
185
185
  }
186
186
  }
187
187
 
188
+ export type OverlapMode = 'never' | 'always' | 'cooperative';
189
+
190
+ export function getOverlapMode(layout: PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>, overlapProp: 'icon-overlap', allowOverlapProp: 'icon-allow-overlap'): OverlapMode;
191
+ export function getOverlapMode(layout: PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>, overlapProp: 'text-overlap', allowOverlapProp: 'text-allow-overlap'): OverlapMode;
192
+ export function getOverlapMode(layout: PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>, overlapProp: 'icon-overlap' | 'text-overlap', allowOverlapProp: 'icon-allow-overlap' | 'text-allow-overlap'): OverlapMode {
193
+ let result: OverlapMode = 'never';
194
+ const overlap = layout.get(overlapProp);
195
+
196
+ if (overlap) {
197
+ // if -overlap is set, use it
198
+ result = overlap;
199
+ } else if (layout.get(allowOverlapProp)) {
200
+ // fall back to -allow-overlap, with false='never', true='always'
201
+ result = 'always';
202
+ }
203
+
204
+ return result;
205
+ }
206
+
188
207
  export default SymbolStyleLayer;
@@ -32,6 +32,7 @@ export type SymbolLayoutProps = {
32
32
  "symbol-sort-key": DataDrivenProperty<number>,
33
33
  "symbol-z-order": DataConstantProperty<"auto" | "viewport-y" | "source">,
34
34
  "icon-allow-overlap": DataConstantProperty<boolean>,
35
+ "icon-overlap": DataConstantProperty<"never" | "always" | "cooperative">,
35
36
  "icon-ignore-placement": DataConstantProperty<boolean>,
36
37
  "icon-optional": DataConstantProperty<boolean>,
37
38
  "icon-rotation-alignment": DataConstantProperty<"map" | "viewport" | "auto">,
@@ -65,6 +66,7 @@ export type SymbolLayoutProps = {
65
66
  "text-transform": DataDrivenProperty<"none" | "uppercase" | "lowercase">,
66
67
  "text-offset": DataDrivenProperty<[number, number]>,
67
68
  "text-allow-overlap": DataConstantProperty<boolean>,
69
+ "text-overlap": DataConstantProperty<"never" | "always" | "cooperative">,
68
70
  "text-ignore-placement": DataConstantProperty<boolean>,
69
71
  "text-optional": DataConstantProperty<boolean>,
70
72
  };
@@ -76,6 +78,7 @@ export type SymbolLayoutPropsPossiblyEvaluated = {
76
78
  "symbol-sort-key": PossiblyEvaluatedPropertyValue<number>,
77
79
  "symbol-z-order": "auto" | "viewport-y" | "source",
78
80
  "icon-allow-overlap": boolean,
81
+ "icon-overlap": "never" | "always" | "cooperative",
79
82
  "icon-ignore-placement": boolean,
80
83
  "icon-optional": boolean,
81
84
  "icon-rotation-alignment": "map" | "viewport" | "auto",
@@ -109,6 +112,7 @@ export type SymbolLayoutPropsPossiblyEvaluated = {
109
112
  "text-transform": PossiblyEvaluatedPropertyValue<"none" | "uppercase" | "lowercase">,
110
113
  "text-offset": PossiblyEvaluatedPropertyValue<[number, number]>,
111
114
  "text-allow-overlap": boolean,
115
+ "text-overlap": "never" | "always" | "cooperative",
112
116
  "text-ignore-placement": boolean,
113
117
  "text-optional": boolean,
114
118
  };
@@ -120,6 +124,7 @@ const layout: Properties<SymbolLayoutProps> = new Properties({
120
124
  "symbol-sort-key": new DataDrivenProperty(styleSpec["layout_symbol"]["symbol-sort-key"] as any as StylePropertySpecification),
121
125
  "symbol-z-order": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-z-order"] as any as StylePropertySpecification),
122
126
  "icon-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["icon-allow-overlap"] as any as StylePropertySpecification),
127
+ "icon-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["icon-overlap"] as any as StylePropertySpecification),
123
128
  "icon-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["icon-ignore-placement"] as any as StylePropertySpecification),
124
129
  "icon-optional": new DataConstantProperty(styleSpec["layout_symbol"]["icon-optional"] as any as StylePropertySpecification),
125
130
  "icon-rotation-alignment": new DataConstantProperty(styleSpec["layout_symbol"]["icon-rotation-alignment"] as any as StylePropertySpecification),
@@ -153,6 +158,7 @@ const layout: Properties<SymbolLayoutProps> = new Properties({
153
158
  "text-transform": new DataDrivenProperty(styleSpec["layout_symbol"]["text-transform"] as any as StylePropertySpecification),
154
159
  "text-offset": new DataDrivenProperty(styleSpec["layout_symbol"]["text-offset"] as any as StylePropertySpecification),
155
160
  "text-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["text-allow-overlap"] as any as StylePropertySpecification),
161
+ "text-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["text-overlap"] as any as StylePropertySpecification),
156
162
  "text-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["text-ignore-placement"] as any as StylePropertySpecification),
157
163
  "text-optional": new DataConstantProperty(styleSpec["layout_symbol"]["text-optional"] as any as StylePropertySpecification),
158
164
  });
@@ -1,3 +1,9 @@
1
+ ## 15.1.0
2
+
3
+ ### ✨ Features and improvements
4
+ * Add `icon-overlap` and `text-overlap` symbol layout properties [#347](https://github.com/maplibre/maplibre-gl-js/pull/347)
5
+ * Deprecate `icon-allow-overlap` and `text-allow-overlap` symbol layout properties. `icon-overlap` and `text-overlap` are their replacements.
6
+
1
7
  ## 15.0.0
2
8
 
3
9
  ### Breaking changes
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maplibre/maplibre-gl-style-spec",
3
3
  "description": "a specification for maplibre gl styles",
4
- "version": "15.0.0",
4
+ "version": "15.1.0",
5
5
  "author": "MapLibre",
6
6
  "keywords": [
7
7
  "mapbox",
@@ -1144,7 +1144,10 @@
1144
1144
  "default": false,
1145
1145
  "doc": "If true, the icon will be visible even if it collides with other previously drawn symbols.",
1146
1146
  "requires": [
1147
- "icon-image"
1147
+ "icon-image",
1148
+ {
1149
+ "!": "icon-overlap"
1150
+ }
1148
1151
  ],
1149
1152
  "sdk-support": {
1150
1153
  "basic functionality": {
@@ -1162,6 +1165,36 @@
1162
1165
  },
1163
1166
  "property-type": "data-constant"
1164
1167
  },
1168
+ "icon-overlap": {
1169
+ "type": "enum",
1170
+ "values": {
1171
+ "never": {
1172
+ "doc": "The icon will be hidden if it collides with any other previously drawn symbol."
1173
+ },
1174
+ "always": {
1175
+ "doc": "The icon will be visible even if it collides with any other previously drawn symbol."
1176
+ },
1177
+ "cooperative": {
1178
+ "doc": "If the icon collides with another previously drawn symbol, the overlap mode for that symbol is checked. If the previous symbol was placed using `never` overlap mode, the new icon is hidden. If the previous symbol was placed using `always` or `cooperative` overlap mode, the new icon is visible."
1179
+ }
1180
+ },
1181
+ "doc": "Allows for control over whether to show an icon when it overlaps other symbols on the map. If `icon-overlap` is not set, `icon-allow-overlap` is used instead.",
1182
+ "requires": [
1183
+ "icon-image"
1184
+ ],
1185
+ "sdk-support": {
1186
+ "basic functionality": {
1187
+ "js": "2.1.0"
1188
+ }
1189
+ },
1190
+ "expression": {
1191
+ "interpolated": false,
1192
+ "parameters": [
1193
+ "zoom"
1194
+ ]
1195
+ },
1196
+ "property-type": "data-constant"
1197
+ },
1165
1198
  "icon-ignore-placement": {
1166
1199
  "type": "boolean",
1167
1200
  "default": false,
@@ -2309,7 +2342,10 @@
2309
2342
  "default": false,
2310
2343
  "doc": "If true, the text will be visible even if it collides with other previously drawn symbols.",
2311
2344
  "requires": [
2312
- "text-field"
2345
+ "text-field",
2346
+ {
2347
+ "!": "text-overlap"
2348
+ }
2313
2349
  ],
2314
2350
  "sdk-support": {
2315
2351
  "basic functionality": {
@@ -2327,6 +2363,36 @@
2327
2363
  },
2328
2364
  "property-type": "data-constant"
2329
2365
  },
2366
+ "text-overlap": {
2367
+ "type": "enum",
2368
+ "values": {
2369
+ "never": {
2370
+ "doc": "The text will be hidden if it collides with any other previously drawn symbol."
2371
+ },
2372
+ "always": {
2373
+ "doc": "The text will be visible even if it collides with any other previously drawn symbol."
2374
+ },
2375
+ "cooperative": {
2376
+ "doc": "If the text collides with another previously drawn symbol, the overlap mode for that symbol is checked. If the previous symbol was placed using `never` overlap mode, the new text is hidden. If the previous symbol was placed using `always` or `cooperative` overlap mode, the new text is visible."
2377
+ }
2378
+ },
2379
+ "doc": "Allows for control over whether to show symbol text when it overlaps other symbols on the map. If `text-overlap` is not set, `text-allow-overlap` is used instead",
2380
+ "requires": [
2381
+ "text-field"
2382
+ ],
2383
+ "sdk-support": {
2384
+ "basic functionality": {
2385
+ "js": "2.1.0"
2386
+ }
2387
+ },
2388
+ "expression": {
2389
+ "interpolated": false,
2390
+ "parameters": [
2391
+ "zoom"
2392
+ ]
2393
+ },
2394
+ "property-type": "data-constant"
2395
+ },
2330
2396
  "text-ignore-placement": {
2331
2397
  "type": "boolean",
2332
2398
  "default": false,
@@ -248,6 +248,7 @@ export type SymbolLayerSpecification = {
248
248
  "symbol-sort-key"?: DataDrivenPropertyValueSpecification<number>,
249
249
  "symbol-z-order"?: PropertyValueSpecification<"auto" | "viewport-y" | "source">,
250
250
  "icon-allow-overlap"?: PropertyValueSpecification<boolean>,
251
+ "icon-overlap"?: PropertyValueSpecification<"never" | "always" | "cooperative">,
251
252
  "icon-ignore-placement"?: PropertyValueSpecification<boolean>,
252
253
  "icon-optional"?: PropertyValueSpecification<boolean>,
253
254
  "icon-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">,
@@ -281,6 +282,7 @@ export type SymbolLayerSpecification = {
281
282
  "text-transform"?: DataDrivenPropertyValueSpecification<"none" | "uppercase" | "lowercase">,
282
283
  "text-offset"?: DataDrivenPropertyValueSpecification<[number, number]>,
283
284
  "text-allow-overlap"?: PropertyValueSpecification<boolean>,
285
+ "text-overlap"?: PropertyValueSpecification<"never" | "always" | "cooperative">,
284
286
  "text-ignore-placement"?: PropertyValueSpecification<boolean>,
285
287
  "text-optional"?: PropertyValueSpecification<boolean>,
286
288
  "visibility"?: "visible" | "none"
@@ -16,6 +16,7 @@ import type {
16
16
  GlyphOffsetArray,
17
17
  SymbolLineVertexArray
18
18
  } from '../data/array_types';
19
+ import type {OverlapMode} from '../style/style_layer/symbol_style_layer';
19
20
 
20
21
  // When a symbol crosses the edge that causes it to be included in
21
22
  // collision detection, it will cause changes in the symbols around
@@ -29,6 +30,7 @@ export type FeatureKey = {
29
30
  bucketInstanceId: number;
30
31
  featureIndex: number;
31
32
  collisionGroupID: number;
33
+ overlapMode: OverlapMode;
32
34
  };
33
35
 
34
36
  /**
@@ -72,7 +74,7 @@ class CollisionIndex {
72
74
 
73
75
  placeCollisionBox(
74
76
  collisionBox: SingleCollisionBox,
75
- allowOverlap: boolean,
77
+ overlapMode: OverlapMode,
76
78
  textPixelRatio: number,
77
79
  posMatrix: mat4,
78
80
  collisionGroupPredicate?: (key: FeatureKey) => boolean
@@ -88,7 +90,7 @@ class CollisionIndex {
88
90
  const brY = collisionBox.y2 * tileToViewport + projectedPoint.point.y;
89
91
 
90
92
  if (!this.isInsideGrid(tlX, tlY, brX, brY) ||
91
- (!allowOverlap && this.grid.hitTest(tlX, tlY, brX, brY, collisionGroupPredicate))) {
93
+ (overlapMode !== 'always' && this.grid.hitTest(tlX, tlY, brX, brY, overlapMode, collisionGroupPredicate))) {
92
94
  return {
93
95
  box: [],
94
96
  offscreen: false
@@ -102,7 +104,7 @@ class CollisionIndex {
102
104
  }
103
105
 
104
106
  placeCollisionCircles(
105
- allowOverlap: boolean,
107
+ overlapMode: OverlapMode,
106
108
  symbol: any,
107
109
  lineVertexArray: SymbolLineVertexArray,
108
110
  glyphOffsetArray: GlyphOffsetArray,
@@ -245,18 +247,16 @@ class CollisionIndex {
245
247
  entirelyOffscreen = entirelyOffscreen && this.isOffscreen(x1, y1, x2, y2);
246
248
  inGrid = inGrid || this.isInsideGrid(x1, y1, x2, y2);
247
249
 
248
- if (!allowOverlap) {
249
- if (this.grid.hitTestCircle(centerX, centerY, radius, collisionGroupPredicate)) {
250
- // Don't early exit if we're showing the debug circles because we still want to calculate
251
- // which circles are in use
252
- collisionDetected = true;
253
- if (!showCollisionCircles) {
254
- return {
255
- circles: [],
256
- offscreen: false,
257
- collisionDetected
258
- };
259
- }
250
+ if (overlapMode !== 'always' && this.grid.hitTestCircle(centerX, centerY, radius, overlapMode, collisionGroupPredicate)) {
251
+ // Don't early exit if we're showing the debug circles because we still want to calculate
252
+ // which circles are in use
253
+ collisionDetected = true;
254
+ if (!showCollisionCircles) {
255
+ return {
256
+ circles: [],
257
+ offscreen: false,
258
+ collisionDetected
259
+ };
260
260
  }
261
261
  }
262
262
  }
@@ -337,17 +337,17 @@ class CollisionIndex {
337
337
  return result;
338
338
  }
339
339
 
340
- insertCollisionBox(collisionBox: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
340
+ insertCollisionBox(collisionBox: Array<number>, overlapMode: OverlapMode, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
341
341
  const grid = ignorePlacement ? this.ignoredGrid : this.grid;
342
342
 
343
- const key = {bucketInstanceId, featureIndex, collisionGroupID};
343
+ const key = {bucketInstanceId, featureIndex, collisionGroupID, overlapMode};
344
344
  grid.insert(key, collisionBox[0], collisionBox[1], collisionBox[2], collisionBox[3]);
345
345
  }
346
346
 
347
- insertCollisionCircles(collisionCircles: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
347
+ insertCollisionCircles(collisionCircles: Array<number>, overlapMode: OverlapMode, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
348
348
  const grid = ignorePlacement ? this.ignoredGrid : this.grid;
349
349
 
350
- const key = {bucketInstanceId, featureIndex, collisionGroupID};
350
+ const key = {bucketInstanceId, featureIndex, collisionGroupID, overlapMode};
351
351
  for (let k = 0; k < collisionCircles.length; k += 4) {
352
352
  grid.insertCircle(key, collisionCircles[k], collisionCircles[k + 1], collisionCircles[k + 2]);
353
353
  }
@@ -1,12 +1,13 @@
1
1
  import GridIndex from './grid_index';
2
+ import type {GridKey} from './grid_index';
2
3
 
3
4
  describe('GridIndex', () => {
4
5
 
5
6
  test('indexes features', () => {
6
- const grid = new GridIndex<number>(100, 100, 10);
7
- grid.insert(0, 4, 10, 6, 30);
8
- grid.insert(1, 4, 10, 30, 12);
9
- grid.insert(2, -10, 30, 5, 35);
7
+ const grid = new GridIndex(100, 100, 10);
8
+ grid.insert(0 as GridKey, 4, 10, 6, 30);
9
+ grid.insert(1 as GridKey, 4, 10, 30, 12);
10
+ grid.insert(2 as GridKey, -10, 30, 5, 35);
10
11
 
11
12
  expect(grid.query(4, 10, 5, 11).map(x => x.key).sort()).toEqual([0, 1]);
12
13
  expect(grid.query(24, 10, 25, 11).map(x => x.key).sort()).toEqual([1]);
@@ -18,8 +19,8 @@ describe('GridIndex', () => {
18
19
  });
19
20
 
20
21
  test('returns multiple copies of a key if multiple boxes were inserted with the same key', () => {
21
- const grid = new GridIndex<number>(100, 100, 10);
22
- const key = 123;
22
+ const grid = new GridIndex(100, 100, 10);
23
+ const key = 123 as GridKey;
23
24
  grid.insert(key, 3, 3, 4, 4);
24
25
  grid.insert(key, 13, 13, 14, 14);
25
26
  grid.insert(key, 23, 23, 24, 24);
@@ -27,26 +28,48 @@ describe('GridIndex', () => {
27
28
  });
28
29
 
29
30
  test('circle-circle intersection', () => {
30
- const grid = new GridIndex<number>(100, 100, 10);
31
- grid.insertCircle(0, 50, 50, 10);
32
- grid.insertCircle(1, 60, 60, 15);
33
- grid.insertCircle(2, -10, 110, 20);
34
-
35
- expect(grid.hitTestCircle(55, 55, 2)).toBeTruthy();
36
- expect(grid.hitTestCircle(10, 10, 10)).toBeFalsy();
37
- expect(grid.hitTestCircle(0, 100, 10)).toBeTruthy();
38
- expect(grid.hitTestCircle(80, 60, 10)).toBeTruthy();
31
+ const grid = new GridIndex(100, 100, 10);
32
+ grid.insertCircle(0 as GridKey, 50, 50, 10);
33
+ grid.insertCircle(1 as GridKey, 60, 60, 15);
34
+ grid.insertCircle(2 as GridKey, -10, 110, 20);
35
+
36
+ expect(grid.hitTestCircle(55, 55, 2, 'never')).toBeTruthy();
37
+ expect(grid.hitTestCircle(10, 10, 10, 'never')).toBeFalsy();
38
+ expect(grid.hitTestCircle(0, 100, 10, 'never')).toBeTruthy();
39
+ expect(grid.hitTestCircle(80, 60, 10, 'never')).toBeTruthy();
39
40
  });
40
41
 
41
42
  test('circle-rectangle intersection', () => {
42
- const grid = new GridIndex<number>(100, 100, 10);
43
- grid.insertCircle(0, 50, 50, 10);
44
- grid.insertCircle(1, 60, 60, 15);
45
- grid.insertCircle(2, -10, 110, 20);
43
+ const grid = new GridIndex(100, 100, 10);
44
+ grid.insertCircle(0 as GridKey, 50, 50, 10);
45
+ grid.insertCircle(1 as GridKey, 60, 60, 15);
46
+ grid.insertCircle(2 as GridKey, -10, 110, 20);
46
47
 
47
48
  expect(grid.query(45, 45, 55, 55).map(x => x.key)).toEqual([0, 1]);
48
49
  expect(grid.query(0, 0, 30, 30).map(x => x.key)).toEqual([]);
49
50
  expect(grid.query(0, 80, 20, 100).map(x => x.key)).toEqual([2]);
50
51
  });
51
52
 
53
+ test('overlap mode', () => {
54
+ const grid = new GridIndex(100, 100, 10);
55
+ grid.insert({overlapMode: 'never'}, 10, 10, 20, 20);
56
+ grid.insert({overlapMode: 'always'}, 30, 10, 40, 20);
57
+ grid.insert({overlapMode: 'cooperative'}, 50, 10, 60, 20);
58
+
59
+ // 'never' can't overlap anything
60
+ expect(grid.hitTest(15, 15, 25, 25, 'never')).toBeTruthy();
61
+ expect(grid.hitTest(35, 15, 45, 25, 'never')).toBeTruthy();
62
+ expect(grid.hitTest(55, 15, 65, 25, 'never')).toBeTruthy();
63
+
64
+ // 'always' can overlap everything
65
+ expect(grid.hitTest(15, 15, 25, 25, 'always')).toBeFalsy();
66
+ expect(grid.hitTest(35, 15, 45, 25, 'always')).toBeFalsy();
67
+ expect(grid.hitTest(55, 15, 65, 25, 'always')).toBeFalsy();
68
+
69
+ // 'cooperative' can overlap 'always' and 'cooperative'
70
+ expect(grid.hitTest(15, 15, 25, 25, 'cooperative')).toBeTruthy();
71
+ expect(grid.hitTest(35, 15, 45, 25, 'cooperative')).toBeFalsy();
72
+ expect(grid.hitTest(55, 15, 65, 25, 'cooperative')).toBeFalsy();
73
+ });
74
+
52
75
  });
@@ -1,5 +1,8 @@
1
+ import type {OverlapMode} from '../style/style_layer/symbol_style_layer';
2
+
1
3
  type QueryArgs = {
2
4
  hitTest: boolean;
5
+ overlapMode?: OverlapMode;
3
6
  circle?: {
4
7
  x: number;
5
8
  y: number;
@@ -23,6 +26,24 @@ type QueryResult<T> = {
23
26
  y2: number;
24
27
  };
25
28
 
29
+ export type GridKey = {
30
+ overlapMode?: OverlapMode;
31
+ }
32
+
33
+ function overlapAllowed(overlapA: OverlapMode, overlapB: OverlapMode): boolean {
34
+ let allowed = true;
35
+
36
+ if (overlapA === 'always') {
37
+ // symbol A using 'always' overlap - allowed to overlap anything.
38
+ } else if (overlapA === 'never' || overlapB === 'never') {
39
+ // symbol A using 'never' overlap - can't overlap anything
40
+ // symbol A using 'cooperative' overlap - can overlap 'always' or 'cooperative' symbol; can't overlap 'never'
41
+ allowed = false;
42
+ }
43
+
44
+ return allowed;
45
+ }
46
+
26
47
  /**
27
48
  * GridIndex is a data structure for testing the intersection of
28
49
  * circles and rectangles in a 2d plane.
@@ -36,7 +57,7 @@ type QueryResult<T> = {
36
57
  *
37
58
  * @private
38
59
  */
39
- class GridIndex<T> {
60
+ class GridIndex<T extends GridKey> {
40
61
  circleKeys: Array<T>;
41
62
  boxKeys: Array<T>;
42
63
  boxCells: Array<Array<number>>;
@@ -110,7 +131,7 @@ class GridIndex<T> {
110
131
  this.circleCells[cellIndex].push(uid);
111
132
  }
112
133
 
113
- private _query(x1: number, y1: number, x2: number, y2: number, hitTest: boolean, predicate?: (key: T) => boolean): Array<QueryResult<T>> {
134
+ private _query(x1: number, y1: number, x2: number, y2: number, hitTest: boolean, overlapMode: OverlapMode, predicate?: (key: T) => boolean): Array<QueryResult<T>> {
114
135
  if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) {
115
136
  return [];
116
137
  }
@@ -150,6 +171,7 @@ class GridIndex<T> {
150
171
  } else {
151
172
  const queryArgs: QueryArgs = {
152
173
  hitTest,
174
+ overlapMode,
153
175
  seenUids: {box: {}, circle: {}}
154
176
  };
155
177
  this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate);
@@ -162,11 +184,11 @@ class GridIndex<T> {
162
184
  return this._query(x1, y1, x2, y2, false, null);
163
185
  }
164
186
 
165
- hitTest(x1: number, y1: number, x2: number, y2: number, predicate?: (key: T) => boolean): boolean {
166
- return this._query(x1, y1, x2, y2, true, predicate).length > 0;
187
+ hitTest(x1: number, y1: number, x2: number, y2: number, overlapMode: OverlapMode, predicate?: (key: T) => boolean): boolean {
188
+ return this._query(x1, y1, x2, y2, true, overlapMode, predicate).length > 0;
167
189
  }
168
190
 
169
- hitTestCircle(x: number, y: number, radius: number, predicate?: (key: T) => boolean): boolean {
191
+ hitTestCircle(x: number, y: number, radius: number, overlapMode: OverlapMode, predicate?: (key: T) => boolean): boolean {
170
192
  // Insert circle into grid for all cells in the circumscribing square
171
193
  // It's more than necessary (by a factor of 4/PI), but fast to insert
172
194
  const x1 = x - radius;
@@ -183,6 +205,7 @@ class GridIndex<T> {
183
205
  const result: boolean[] = [];
184
206
  const queryArgs: QueryArgs = {
185
207
  hitTest: true,
208
+ overlapMode,
186
209
  circle: {x, y, radius},
187
210
  seenUids: {box: {}, circle: {}}
188
211
  };
@@ -191,7 +214,7 @@ class GridIndex<T> {
191
214
  }
192
215
 
193
216
  private _queryCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: Array<QueryResult<T>>, queryArgs: QueryArgs, predicate?: (key: T) => boolean): boolean {
194
- const {seenUids, hitTest} = queryArgs;
217
+ const {seenUids, hitTest, overlapMode} = queryArgs;
195
218
  const boxCell = this.boxCells[cellIndex];
196
219
 
197
220
  if (boxCell !== null) {
@@ -207,16 +230,18 @@ class GridIndex<T> {
207
230
  (x2 >= bboxes[offset + 0]) &&
208
231
  (y2 >= bboxes[offset + 1]) &&
209
232
  (!predicate || predicate(key))) {
210
- result.push({
211
- key,
212
- x1: bboxes[offset],
213
- y1: bboxes[offset + 1],
214
- x2: bboxes[offset + 2],
215
- y2: bboxes[offset + 3]
216
- });
217
- if (hitTest) {
218
- // true return value stops the query after first match
219
- return true;
233
+ if (!hitTest || !overlapAllowed(overlapMode, key.overlapMode)) {
234
+ result.push({
235
+ key,
236
+ x1: bboxes[offset],
237
+ y1: bboxes[offset + 1],
238
+ x2: bboxes[offset + 2],
239
+ y2: bboxes[offset + 3]
240
+ });
241
+ if (hitTest) {
242
+ // true return value stops the query after first match
243
+ return true;
244
+ }
220
245
  }
221
246
  }
222
247
  }
@@ -240,19 +265,21 @@ class GridIndex<T> {
240
265
  x2,
241
266
  y2) &&
242
267
  (!predicate || predicate(key))) {
243
- const x = circles[offset];
244
- const y = circles[offset + 1];
245
- const radius = circles[offset + 2];
246
- result.push({
247
- key,
248
- x1: x - radius,
249
- y1: y - radius,
250
- x2: x + radius,
251
- y2: y + radius
252
- });
253
- if (hitTest) {
254
- // true return value stops the query after first match
255
- return true;
268
+ if (!hitTest || !overlapAllowed(overlapMode, key.overlapMode)) {
269
+ const x = circles[offset];
270
+ const y = circles[offset + 1];
271
+ const radius = circles[offset + 2];
272
+ result.push({
273
+ key,
274
+ x1: x - radius,
275
+ y1: y - radius,
276
+ x2: x + radius,
277
+ y2: y + radius
278
+ });
279
+ if (hitTest) {
280
+ // true return value stops the query after first match
281
+ return true;
282
+ }
256
283
  }
257
284
  }
258
285
  }
@@ -263,8 +290,8 @@ class GridIndex<T> {
263
290
  return false;
264
291
  }
265
292
 
266
- private _queryCellCircle(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: Array<boolean>, queryArgs: QueryArgs, predicate?: (key:T) => boolean): boolean {
267
- const {circle, seenUids} = queryArgs;
293
+ private _queryCellCircle(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: Array<boolean>, queryArgs: QueryArgs, predicate?: (key: T) => boolean): boolean {
294
+ const {circle, seenUids, overlapMode} = queryArgs;
268
295
  const boxCell = this.boxCells[cellIndex];
269
296
 
270
297
  if (boxCell !== null) {
@@ -282,7 +309,8 @@ class GridIndex<T> {
282
309
  bboxes[offset + 1],
283
310
  bboxes[offset + 2],
284
311
  bboxes[offset + 3]) &&
285
- (!predicate || predicate(key))) {
312
+ (!predicate || predicate(key)) &&
313
+ !overlapAllowed(overlapMode, key.overlapMode)) {
286
314
  result.push(true);
287
315
  return true;
288
316
  }
@@ -305,7 +333,8 @@ class GridIndex<T> {
305
333
  circle.x,
306
334
  circle.y,
307
335
  circle.radius) &&
308
- (!predicate || predicate(key))) {
336
+ (!predicate || predicate(key)) &&
337
+ !overlapAllowed(overlapMode, key.overlapMode)) {
309
338
  result.push(true);
310
339
  return true;
311
340
  }