8000 [GLJS-1274] Add support for `extra_bounds` in TileJSON (internal-2447) · mapbox/mapbox-gl-js@76d1c14 · GitHub
[go: up one dir, main page]

Skip to content

Commit 76d1c14

Browse files
committed
[GLJS-1274] Add support for extra_bounds in TileJSON (internal-2447)
1 parent 163f00d commit 76d1c14

15 files changed

+364
-39
lines changed

src/source/load_tilejson.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export default function (
100100
const result: TileJSON = pick(
101101
// explicit source options take precedence over TileJSON
102102
extend({}, tileJSON, options),
103-
['tilejson', 'tiles', 'minzoom', 'maxzoom', 'attribution', 'mapbox_logo', 'bounds', 'scheme', 'tileSize', 'encoding', 'vector_layers', 'raster_layers', 'worldview_options', 'worldview_default', 'worldview']
103+
['tilejson', 'tiles', 'minzoom', 'maxzoom', 'attribution', 'mapbox_logo', 'bounds', 'extra_bounds', 'scheme', 'tileSize', 'encoding', 'vector_layers', 'raster_layers', 'worldview_options', 'worldview_default', 'worldview']
104104
);
105105

106106
result.tiles = requestManager.canonicalizeTileset(result, options.url);

src/source/raster_tile_source.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class RasterTileSource<T = 'raster'> extends Evented<SourceEvents> implements IS
6363
rasterLayerIds?: Array<string>;
6464

6565
bounds: [number, number, number, number] | null | undefined;
66-
tileBounds: TileBounds;
66+
tileBounds?: TileBounds;
6767
roundZoom: boolean | undefined;
6868
reparseOverscaled: boolean | undefined;
6969
dispatcher: Dispatcher;
@@ -112,8 +112,7 @@ class RasterTileSource<T = 'raster'> extends Evented<SourceEvents> implements IS
112112
this.rasterLayerIds = this.rasterLayers.map(layer => layer.id);
113113
}
114114

115-
if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom);
116-
115+
this.tileBounds = TileBounds.fromTileJSON(tileJSON);
117116
postTurnstileEvent(tileJSON.tiles);
118117

119118
// `content` is included here to prevent a race condition where `Style#updateSources` is called

src/source/tile_bounds.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,78 @@
11
import {LngLatBounds} from '../geo/lng_lat';
22
import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate';
33

4+
import type {TileJSON} from '../types/tilejson';
45
import type {CanonicalTileID} from './tile_id';
56

7+
function contains(bounds: LngLatBounds, tileID: CanonicalTileID): boolean {
8+
const worldSize = Math.pow(2, tileID.z);
9+
10+
const minX = Math.floor(mercatorXfromLng(bounds.getWest()) * worldSize);
11+
const minY = Math.floor(mercatorYfromLat(bounds.getNorth()) * worldSize);
12+
const maxX = Math.ceil(mercatorXfromLng(bounds.getEast()) * worldSize);
13+
const maxY = Math.ceil(mercatorYfromLat(bounds.getSouth()) * worldSize);
14+
15+
const hit = tileID.x >= minX && tileID.x < maxX && tileID.y >= minY && tileID.y < maxY;
16+
return hit;
17+
}
18+
619
class TileBounds {
7-
bounds: LngLatBounds;
20+
bounds?: LngLatBounds;
21+
extraBounds?: LngLatBounds[];
822
minzoom: number;
923
maxzoom: number;
1024

11-
constructor(bounds: [number, number, number, number], minzoom?: number | null, maxzoom?: number | null) {
12-
this.bounds = LngLatBounds.convert(this.validateBounds(bounds));
25+
constructor(bounds?: [number, number, number, number] | null, minzoom?: number | null, maxzoom?: number | null) {
26+
this.bounds = bounds ? LngLatBounds.convert(this.validateBounds(bounds)) : null;
1327
this.minzoom = minzoom || 0;
1428
this.maxzoom = maxzoom || 24;
1529
}
1630

31+
// left, bottom, right, top
1732
validateBounds(bounds: [number, number, number, number]): [number, number, number, number] {
1833
// make sure the bounds property contains valid longitude and latitudes
1934
if (!Array.isArray(bounds) || bounds.length !== 4) return [-180, -90, 180, 90];
2035
return [Math.max(-180, bounds[0]), Math.max(-90, bounds[1]), Math.min(180, bounds[2]), Math.min(90, bounds[3])];
2136
}
2237

38+
addExtraBounds(extraBounds?: [number, number, number, number][] | null) {
39+
if (!extraBounds) return;
40+
if (!this.extraBounds) this.extraBounds = [];
41+
42+
for (const bounds of extraBounds) {
43+
this.extraBounds.push(LngLatBounds.convert(this.validateBounds(bounds)));
44+
}
45+
}
46+
2347
contains(tileID: CanonicalTileID): boolean {
24-
const worldSize = Math.pow(2, tileID.z);
25-
const level = {
26-
minX: Math.floor(mercatorXfromLng(this.bounds.getWest()) * worldSize),
27-
minY: Math.floor(mercatorYfromLat(this.bounds.getNorth()) * worldSize),
28-
maxX: Math.ceil(mercatorXfromLng(this.bounds.getEast()) * worldSize),
29-
maxY: Math.ceil(mercatorYfromLat(this.bounds.getSouth()) * worldSize)
30-
};
31-
const hit = tileID.x >= level.minX && tileID.x < level.maxX && tileID.y >= level.minY && tileID.y < level.maxY;
32-
return hit;
48+
if (tileID.z > this.maxzoom || tileID.z < this.minzoom) {
49+
return false;
50+
}
51+
52+
if (this.bounds && !contains(this.bounds, tileID)) {
53+
return false;
54+
}
55+
56+
if (!this.extraBounds) {
57+
return true;
58+
}
59+
60+
for (const bounds of this.extraBounds) {
61+
if (contains(bounds, tileID)) {
62+
return true;
63+
}
64+
}
65+
66+
return false;
67+
}
68+
69+
static fromTileJSON(tileJSON: Partial<TileJSON>): TileBounds | null {
70+
if (!tileJSON.bounds && !tileJSON.extra_bounds) return null;
71+
72+
const tileBounds = new TileBounds(tileJSON.bounds, tileJSON.minzoom, tileJSON.maxzoom);
73+
tileBounds.addExtraBounds(tileJSON.extra_bounds);
74+
75+
return tileBounds;
3376
}
3477
}
3578

src/source/vector_tile_source.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class VectorTileSource extends Evented<SourceEvents> implements ISource<'vector'
7272
map: Map;
7373
bounds?: [number, number, number, number] | null;
7474
tiles: Array<string>;
75-
tileBounds: TileBounds;
75+
tileBounds?: TileBounds;
7676
reparseOverscaled?: boolean;
7777
isTileClipped?: boolean;
7878
_tileJSONRequest?: Cancelable | null;
@@ -153,7 +153,7 @@ class VectorTileSource extends Evented<SourceEvents> implements ISource<'vector'
153153
}
154154
}
155155

156-
if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom);
156+
this.tileBounds = TileBounds.fromTileJSON(tileJSON);
157157
postTurnstileEvent(tileJSON.tiles, this.map._requestManager._customAccessToken);
158158

159159
// `content` is included here to prevent a race condition where `Style#updateSources` is called

src/style-spec/reference/v8.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,16 @@
834834
],
835835
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL."
836836
},
837+
"extra_bounds": {
838+
"type": "array",
839+
"value": {
840+
"type": "array",
841+
"value": "number",
842+
"length": 4,
843+
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`."
844+
},
845+
"doc": "An array of additional discrete geographic regions where tiles are available. When used alongside the `bounds` property, these regions act as an additional filter - Mapbox GL will only request tiles that are both within the `bounds` and any of the regions defined in `extra_bounds`. When used independently (without `bounds`), Mapbox GL will request tiles that fall within any of the regions in `extra_bounds`. This allows for more fine-grained control over tile requests, particularly when dealing with sparse data coverage."
846+
},
837847
"scheme": {
838848
"type": "enum",
839849
"values": {
@@ -913,6 +923,16 @@
913923
],
914924
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL."
915925
},
926+
"extra_bounds": {
927+
"type": "array",
928+
"value": {
929+
"type": "array",
930+
"value": "number",
931+
"length": 4,
932+
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`."
933+
},
934+
"doc": "An array of additional discrete geographic regions where tiles are available. When used alongside the `bounds` property, these regions act as an additional filter - Mapbox GL will only request tiles that are both within the `bounds` and any of the regions defined in `extra_bounds`. When used independently (without `bounds`), Mapbox GL will request tiles that fall within any of the regions in `extra_bounds`. This allows for more fine-grained control over tile requests, particularly when dealing with sparse data coverage."
935+
},
916936
"minzoom": {
917937
"type": "number",
918938
"default": 0,
@@ -994,6 +1014,16 @@
9941014
],
9951015
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL."
9961016
},
1017+
"extra_bounds": {
1018+
"type": "array",
1019+
"value": {
1020+
"type": "array",
1021+
"value": "number",
1022+
"length": 4,
1023+
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`."
1024+
},
1025+
"doc": "An array of additional discrete geographic regions where tiles are available. When used alongside the `bounds` property, these regions act as an additional filter - Mapbox GL will only request tiles that are both within the `bounds` and any of the regions defined in `extra_bounds`. When used independently (without `bounds`), Mapbox GL will request tiles that fall within any of the regions in `extra_bounds`. This allows for more fine-grained control over tile requests, particularly when dealing with sparse data coverage."
1026+
},
9971027
"minzoom": {
9981028
"type": "number",
9991029
"default": 0,
@@ -1076,6 +1106,16 @@
10761106
],
10771107
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL."
10781108
},
1109+
"extra_bounds": {
1110+
"type": "array",
1111+
"value": {
1112+
"type": "array",
1113+
"value": "number",
1114+
"length": 4,
1115+
"doc": "An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`."
1116+
},
1117+
"doc": "An array of additional discrete geographic regions where tiles are available. When used alongside the `bounds` property, these regions act as an additional filter - Mapbox GL will only request tiles that are both within the `bounds` and any of the regions defined in `extra_bounds`. When used independently (without `bounds`), Mapbox GL will request tiles that fall within any of the regions in `extra_bounds`. This allows for more fine-grained control over tile requests, particularly when dealing with sparse data coverage."
1118+
},
10791119
"minzoom": {
10801120
"type": "number",
10811121
"default": 0,

src/style-spec/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ export type VectorSourceSpecification = {
387387
"url"?: string,
388388
"tiles"?: Array<string>,
389389
"bounds"?: [number, number, number, number],
390+
"extra_bounds"?: Array<[number, number, number, number]>,
390391
"scheme"?: "xyz" | "tms",
391392
"minzoom"?: number,
392393
"maxzoom"?: number,
@@ -401,6 +402,7 @@ export type RasterSourceSpecification = {
401402
"url"?: string,
402403
"tiles"?: Array<string>,
403404
"bounds"?: [number, number, number, number],
405+
"extra_bounds"?: Array<[number, number, number, number]>,
404406
"minzoom"?: number,
405407
"maxzoom"?: number,
406408
"tileSize"?: number,
@@ -415,6 +417,7 @@ export type RasterDEMSourceSpecification = {
415417
"url"?: string,
416418
"tiles"?: Array<string>,
417419
"bounds"?: [number, number, number, number],
420+
"extra_bounds"?: Array<[number, number, number, number]>,
418421
"minzoom"?: number,
419422
"maxzoom"?: number,
420423
"tileSize"?: number,
@@ -432,6 +435,7 @@ export type RasterArraySourceSpecification = {
432435
"url"?: string,
433436
"tiles"?: Array<string>,
434437
"bounds"?: [number, number, number, number],
438+
"extra_bounds"?: Array<[number, number, number, number]>,
435439
"minzoom"?: number,
436440
"maxzoom"?: number,
437441
"tileSize"?: number,

src/style-spec/validate/validate_object.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import type {LayerSpecification} from '../types';
77

88
type Options = ValidationOptions & {
99
layer?: LayerSpecification;
10-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11-
objectElementValidators?: any;
10+
objectElementValidators?: object;
1211
};
1312

1413
export default function validateObject(options: Options): Array<ValidationError> {
@@ -18,7 +17,7 @@ export default function validateObject(options: Options): Array<ValidationError>
1817
const elementValidators = options.objectElementValidators || {};
1918
const style = options.style;
2019
const styleSpec = options.styleSpec;
21-
let errors = [];
20+
let errors: ValidationError[] = [];
2221

2322
const type = getType(object);
2423
if (type !== 'object') {
@@ -67,6 +66,5 @@ export default function validateObject(options: Options): Array<ValidationError>
6766
}
6867
}
6968

70-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
7169
return errors;
7270
}

src/style-spec/validate/validate_source.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function validateSource(options: ValidationOptions): Array<Valida
2626
}
2727

2828
const type = unbundle(value.type) as string;
29-
let errors = [];
29+
let errors: ValidationError[] = [];
3030

3131
if (['vector', 'raster', 'raster-dem', 'raster-array'].includes(type)) {
3232
if (!value.url && !value.tiles) {
@@ -47,9 +47,7 @@ export default function validateSource(options: ValidationOptions): Array<Valida
4747
styleSpec,
4848
objectElementValidators
4949
}));
50-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
5150
return errors;
52-
5351
case 'geojson':
5452
errors = validateObject({
5553
key,
@@ -59,6 +57,7 @@ export default function validateSource(options: ValidationOptions): Array<Valida
5957
styleSpec,
6058
objectElementValidators
6159
});
60+
6261
if (value.cluster) {
6362
for (const prop in value.clusterProperties) {
6463
const [operator, mapExpr] = value.clusterProperties[prop];
@@ -76,9 +75,8 @@ export default function validateSource(options: ValidationOptions): Array<Valida
7675
}));
7776
}
7877
}
79-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
80-
return errors;
8178

79+
return errors;
8280
case 'video':
8381
return validateObject({
8482
key,
@@ -113,12 +111,11 @@ export default function validateSource(options: ValidationOptions): Array<Valida
113111

114112
function getSourceTypeValues(styleSpec: StyleReference) {
115113
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
116-
return styleSpec.source.reduce((memo, source) => {
114+
return styleSpec.source.reduce((memo: string[], source: string) => {
117115
const sourceType = styleSpec[source];
118116
if (sourceType.type.type === 'enum') {
119117
memo = memo.concat(Object.keys(sourceType.type.values));
120118
}
121-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
122119
return memo;
123120
}, []);
124121
}
@@ -130,7 +127,7 @@ function validatePromoteId({
130127
if (getType(value) === 'string') {
131128
return validateString({key, value});
132129
} else if (Array.isArray(value)) {
133-
const errors = [];
130+
const errors: ValidationError[] = [];
134131
const unbundledValue = deepUnbundle(value);
135132
const expression = createExpression(unbundledValue);
136133
if (expression.result === 'error') {
@@ -146,14 +143,13 @@ function validatePromoteId({
146143
errors.push(new ValidationError(`${key}`, null, 'promoteId expression should be only feature dependent'));
147144
}
148145

149-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
150146
return errors;
151147
} else {
152-
const errors = [];
148+
const errors: ValidationError[] = [];
153149
for (const prop in value) {
154150
errors.push(...validatePromoteId({key: `${key}.${prop}`, value: value[prop]}));
155151
}
156-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
152+
157153
return errors;
158154
}
159155
}

src/style-spec/validate_mapbox_api_supported.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,19 @@ function getSourceCount(source: SourceSpecification): number {
3636

3737
function getAllowedKeyErrors(obj: object, keys: string[], path?: string | null): Array<ValidationError> {
3838
const allowed = new Set(keys);
39-
const errors = [];
39+
const errors: ValidationError[] = [];
4040
Object.keys(obj).forEach(k => {
4141
if (!allowed.has(k)) {
4242
const prop = path ? `${path}.${k}` : null;
4343
errors.push(new ValidationError(prop, obj[k], `Unsupported property "${k}"`));
4444
}
4545
});
46-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
4746
return errors;
4847
}
4948

5049
const acceptedSourceTypes = new Set<SourceSpecification['type']>(['vector', 'raster', 'raster-dem', 'raster-array', 'model', 'batched-model']);
5150
function getSourceErrors(source: SourceSpecification, i: number): Array<ValidationError> {
52-
const errors = [];
51+
const errors: ValidationError[] = [];
5352

5453
/*
5554
* Inlined sources are not supported by the Mapbox Styles API, so only
@@ -76,16 +75,14 @@ function getSourceErrors(source: SourceSpecification, i: number): Array<Validati
7675
errors.push(new ValidationError(`sources[${i}].url`, (source as {url?: string}).url, 'Expected a valid Mapbox tileset url'));
7776
}
7877

79-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
8078
return errors;
8179
}
8280

8381
function getMaxSourcesErrors(sourcesCount: number): Array<ValidationError> {
84-
const errors = [];
82+
const errors: ValidationError[] = [];
8583
if (sourcesCount > MAX_SOURCES_IN_STYLE) {
8684
errors.push(new ValidationError('sources', null, `Styles must contain ${MAX_SOURCES_IN_STYLE} or fewer sources`));
8785
}
88-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
8986
return errors;
9087
}
9188

src/types/tilejson.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type TileJSON = {
1717
minzoom?: number;
1818
maxzoom?: number;
1919
bounds?: [number, number, number, number];
20+
extra_bounds?: Array<[number, number, number, number]>;
2021
center?: [number, number, number];
2122
vector_layers?: Array<SourceVectorLayer>;
2223
raster_layers?: Array<SourceRasterLayer>;

0 commit comments

Comments
 (0)
0