From 38f668ece32648e03d0430c18b61f52cedffa97f Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Mon, 23 Jan 2023 18:18:05 +0200 Subject: [PATCH 01/72] Fix `class-object-subtyping` --- .flowconfig | 2 +- flow-typed/geojson.js | 12 +++++------ package.json | 2 +- src/data/load_geometry.js | 2 +- src/source/pixels_to_tile_units.js | 2 +- src/style-spec/expression/index.js | 32 +++++++++++++++--------------- src/symbol/symbol_size.js | 2 +- src/util/vectortile_to_geojson.js | 6 +++--- yarn.lock | 8 ++++---- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.flowconfig b/.flowconfig index 12832956862..324b3ca6e5c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.152.0 +0.153.0 [options] diff --git a/flow-typed/geojson.js b/flow-typed/geojson.js index c266403191c..eda0f1a4fc5 100644 --- a/flow-typed/geojson.js +++ b/flow-typed/geojson.js @@ -27,12 +27,12 @@ declare module "@mapbox/geojson-types" { geometries: Array }; - declare export type GeoJSONFeature = { - type: 'Feature', - geometry: ?GeoJSONGeometry, - properties: ?{}, - id?: number | string - }; + declare export interface GeoJSONFeature { + type: 'Feature'; + geometry: ?GeoJSONGeometry; + properties: ?{}; + id?: number | string; + } declare export type GeoJSONFeatureCollection = { type: 'FeatureCollection', diff --git a/package.json b/package.json index 0059d743754..5c75f1a7241 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.152.0", + "flow-bin": "0.153.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/data/load_geometry.js b/src/data/load_geometry.js index 1be0a343b20..5261569417a 100644 --- a/src/data/load_geometry.js +++ b/src/data/load_geometry.js @@ -32,7 +32,7 @@ function preparePoint(point: Point, scale: number) { } // a subset of VectorTileGeometry -type FeatureWithGeometry = { +interface FeatureWithGeometry { extent: number; type: 1 | 2 | 3; loadGeometry(): Array>; diff --git a/src/source/pixels_to_tile_units.js b/src/source/pixels_to_tile_units.js index e30d009e6d9..34214f86c4f 100644 --- a/src/source/pixels_to_tile_units.js +++ b/src/source/pixels_to_tile_units.js @@ -24,7 +24,7 @@ export default function(tile: {tileID: OverscaledTileID, tileSize: number}, pixe return pixelValue * (EXTENT / (tile.tileSize * Math.pow(2, z - tile.tileID.overscaledZ))); } -export function getPixelsToTileUnitsMatrix(tile: {tileID: OverscaledTileID, tileSize: number, +tileTransform: TileTransform}, transform: Transform): Float32Array { +export function getPixelsToTileUnitsMatrix(tile: interface {tileID: OverscaledTileID, tileSize: number, +tileTransform: TileTransform}, transform: Transform): Float32Array { const {scale} = tile.tileTransform; const s = scale * EXTENT / (tile.tileSize * Math.pow(2, transform.zoom - tile.tileID.overscaledZ + tile.tileID.canonical.z)); return mat2.scale(new Float32Array(4), transform.inverseAdjustmentMatrix, [s, s]); diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 1ab34b13643..11242fbe7e9 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -29,25 +29,25 @@ import type Point from '@mapbox/point-geometry'; import type {CanonicalTileID} from '../../source/tile_id.js'; import type {FeatureDistanceData} from '../feature_filter/index.js'; -export type Feature = { - +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon', - +id?: number | null, - +properties: {[_: string]: any}, - +patterns?: {[_: string]: string}, - +geometry?: Array> -}; +export interface Feature { + +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon'; + +id?: number | null; + +properties: {[_: string]: any}; + +patterns?: {[_: string]: string}; + +geometry?: Array>; +} export type FeatureState = {[_: string]: any}; -export type GlobalProperties = $ReadOnly<{ - zoom: number, - pitch?: number, - heatmapDensity?: number, - lineProgress?: number, - skyRadialProgress?: number, - isSupportedScript?: (_: string) => boolean, - accumulated?: Value -}>; +export interface GlobalProperties { + +zoom: number; + +pitch?: number; + +heatmapDensity?: number; + +lineProgress?: number; + +skyRadialProgress?: number; + +isSupportedScript?: (_: string) => boolean; + +accumulated?: Value; +} export class StyleExpression { expression: Expression; diff --git a/src/symbol/symbol_size.js b/src/symbol/symbol_size.js index 6175f32cea6..98bac3ebffc 100644 --- a/src/symbol/symbol_size.js +++ b/src/symbol/symbol_size.js @@ -80,7 +80,7 @@ function getSizeData(tileZoom: number, value: PropertyValue & { +export interface QueryFeature extends GeoJSONFeature { layer?: ?LayerSpecification; [key: string]: mixed; -}; +} const customProps = ['tile', 'layer', 'source', 'sourceLayer', 'state']; class Feature { type: 'Feature'; _geometry: ?GeoJSONGeometry; - properties: {}; + properties: ?{}; id: number | string | void; _vectorTileFeature: IVectorTileFeature; _x: number; diff --git a/yarn.lock b/yarn.lock index b8d766ca45d..685a0b3fe24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.152.0: - version "0.152.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.152.0.tgz#6980d0cd58f59e9aefd580b11109a1d56eba46b1" - integrity sha512-b4ijbZIQovcx5l/T7VnwyBPIikj60A2qk7hKqQKVWiuftQMrUmC5ct2/0SuVvheX6ZbPdZfeyw2EHO1/n3eAmw== +flow-bin@0.153.0: + version "0.153.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.153.0.tgz#44d941acaf5ef977fa26d1b4b5dc3cf56b68eefc" + integrity sha512-sxP9nfXnoyCUT6hjAO+zDyHLO3dZcWg0h+4HttHs/5wg/2oAkTDwmsWbj095IQsEmwTicq2TfqWq5QRuLxynlQ== follow-redirects@^1.0.0: version "1.15.1" From 4616996a907f1c8f6bf58920a71c660e80c488a6 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Mon, 23 Jan 2023 19:11:10 +0200 Subject: [PATCH 02/72] More fixes --- src/data/dem_data.js | 26 +++++++++++++------------- src/source/pixels_to_tile_units.js | 2 +- src/style-spec/expression/index.js | 16 ++++++++-------- src/ui/map.js | 2 +- src/util/vectortile_to_geojson.js | 1 + src/util/web_worker_transfer.js | 3 ++- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/data/dem_data.js b/src/data/dem_data.js index 9186723e7c5..66d549076f8 100644 --- a/src/data/dem_data.js +++ b/src/data/dem_data.js @@ -23,6 +23,18 @@ const unpackVectors = { terrarium: [256.0, 1.0, 1.0 / 256.0, 32768.0] }; +function unpackMapbox(r: number, g: number, b: number): number { + // unpacking formula for mapbox.terrain-rgb: + // https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb + return ((r * 256 * 256 + g * 256.0 + b) / 10.0 - 10000.0); +} + +function unpackTerrarium(r: number, g: number, b: number): number { + // unpacking formula for mapzen terrarium: + // https://aws.amazon.com/public-datasets/terrain/ + return ((r * 256 + g + b / 256) - 32768.0); +} + export default class DEMData { uid: number; pixels: Uint8Array; @@ -86,7 +98,7 @@ export default class DEMData { y = clamp(y, -1, this.dim); } const index = this._idx(x, y) * 4; - const unpack = this.encoding === "terrarium" ? this._unpackTerrarium : this._unpackMapbox; + const unpack = this.encoding === "terrarium" ? unpackTerrarium : unpackMapbox; return unpack(this.pixels[index], this.pixels[index + 1], this.pixels[index + 2]); } @@ -103,18 +115,6 @@ export default class DEMData { return (y + 1) * this.stride + (x + 1); } - _unpackMapbox(r: number, g: number, b: number): number { - // unpacking formula for mapbox.terrain-rgb: - // https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb - return ((r * 256 * 256 + g * 256.0 + b) / 10.0 - 10000.0); - } - - _unpackTerrarium(r: number, g: number, b: number): number { - // unpacking formula for mapzen terrarium: - // https://aws.amazon.com/public-datasets/terrain/ - return ((r * 256 + g + b / 256) - 32768.0); - } - static pack(altitude: number, encoding: DEMEncoding): [number, number, number, number] { const color = [0, 0, 0, 0]; const vector = DEMData.getUnpackVector(encoding); diff --git a/src/source/pixels_to_tile_units.js b/src/source/pixels_to_tile_units.js index 34214f86c4f..ebb2a41e968 100644 --- a/src/source/pixels_to_tile_units.js +++ b/src/source/pixels_to_tile_units.js @@ -20,7 +20,7 @@ import type {TileTransform} from '../geo/projection/tile_transform.js'; * @returns value in tile units * @private */ -export default function(tile: {tileID: OverscaledTileID, tileSize: number}, pixelValue: number, z: number): number { +export default function(tile: interface {tileID: OverscaledTileID, tileSize: number}, pixelValue: number, z: number): number { return pixelValue * (EXTENT / (tile.tileSize * Math.pow(2, z - tile.tileID.overscaledZ))); } diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 11242fbe7e9..853ff20fe1a 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -211,14 +211,14 @@ export type CameraExpression = { interpolationType: ?InterpolationType }; -export type CompositeExpression = { - kind: 'composite', - isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, - +interpolationFactor: (input: number, lower: number, upper: number) => number, - zoomStops: Array, - interpolationType: ?InterpolationType -}; +export interface CompositeExpression { + kind: 'composite'; + isStateDependent: boolean; + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any; + +interpolationFactor: (input: number, lower: number, upper: number) => number; + zoomStops: Array; + interpolationType: ?InterpolationType; +} export type StylePropertyExpression = | ConstantExpression diff --git a/src/ui/map.js b/src/ui/map.js index 26044a48bfc..8707b563db5 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -74,7 +74,7 @@ import type {QueryResult} from '../data/feature_index.js'; export type ControlPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; /* eslint-disable no-use-before-define */ -type IControl = { +interface IControl { onAdd(map: Map): HTMLElement; onRemove(map: Map): void; diff --git a/src/util/vectortile_to_geojson.js b/src/util/vectortile_to_geojson.js index c23089d71f0..5d4a5c5fbb6 100644 --- a/src/util/vectortile_to_geojson.js +++ b/src/util/vectortile_to_geojson.js @@ -6,6 +6,7 @@ import type {IVectorTileFeature} from '@mapbox/vector-tile'; // we augment GeoJSON with custom properties in query*Features results export interface QueryFeature extends GeoJSONFeature { layer?: ?LayerSpecification; + state: ?mixed; [key: string]: mixed; } diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index 62620cc86a4..68a11c99596 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -13,7 +13,7 @@ import {AJAXError} from './ajax.js'; import type {Transferable} from '../types/transferable.js'; -type SerializedObject = {[_: string]: Serialized }; // eslint-disable-line +type SerializedObject = {[_: string]: Serialized }; export type Serialized = | null | void @@ -252,6 +252,7 @@ export function deserialize(input: Serialized): mixed { for (const key of Object.keys(input)) { if (key === '$name') continue; + // $FlowFixMe[class-object-subtyping] const value = (input: SerializedObject)[key]; result[key] = deserialize(value); } From e1809e3b16f74e6e18ac44295f4fe81f8a744930 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Tue, 24 Jan 2023 14:58:24 +0200 Subject: [PATCH 03/72] flow fix --- src/geo/projection/tile_transform.js | 2 +- src/render/uniform_binding.js | 38 +- src/source/custom_source.js | 469 +- src/source/source_cache.js | 1977 +++++---- src/source/worker.js | 570 +-- src/style-spec/expression/index.js | 45 +- src/style-spec/validate_style.min.js | 8 +- src/style/properties.js | 6 +- src/style/style.js | 3702 ++++++++-------- src/style/style_layer/custom_style_layer.js | 64 +- src/symbol/grid_index.js | 709 ++-- src/terrain/terrain.js | 2665 ++++++------ src/types/cancelable.js | 2 +- src/ui/camera.js | 2196 +++++----- src/ui/control/attribution_control.js | 392 +- src/ui/control/fullscreen_control.js | 216 +- src/ui/control/geolocate_control.js | 1189 +++--- src/ui/control/logo_control.js | 142 +- src/ui/control/navigation_control.js | 525 ++- src/ui/control/scale_control.js | 219 +- src/ui/handler/scroll_zoom.js | 703 +-- src/ui/handler_manager.js | 1169 ++--- src/ui/hash.js | 211 +- src/ui/map.js | 4248 +++++++++++-------- src/ui/marker.js | 1266 +++--- src/ui/popup.js | 692 +-- src/util/actor.js | 299 +- src/util/image.js | 6 +- src/util/mapbox.js | 474 ++- src/util/scheduler.js | 187 +- src/util/web_worker_transfer.js | 2 +- 31 files changed, 13410 insertions(+), 10983 deletions(-) diff --git a/src/geo/projection/tile_transform.js b/src/geo/projection/tile_transform.js index cacbe68d751..bf832c97b43 100644 --- a/src/geo/projection/tile_transform.js +++ b/src/geo/projection/tile_transform.js @@ -114,7 +114,7 @@ export function tileAABB(tr: Transform, numTiles: number, z: number, x: number, [(wrap + tx2) * numTiles, numTiles * ty2, max]); } -export function getTilePoint(tileTransform: TileTransform, {x, y}: {x: number, y: number}, wrap: number = 0): Point { +export function getTilePoint(tileTransform: TileTransform, {x, y}: interface { x: number, y: number }, wrap: number = 0): Point { return new Point( ((x - wrap) * tileTransform.scale - tileTransform.x) * EXTENT, (y * tileTransform.scale - tileTransform.y) * EXTENT); diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index b40512d0c26..bcc5fbe54d3 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -7,25 +7,25 @@ export type UniformValues = $Exact<$ObjMap(u: Uniform) => V>>; class Uniform { - gl: WebGLRenderingContext; - location: ?WebGLUniformLocation; - current: T; - initialized: boolean; - - constructor(context: Context) { - this.gl = context.gl; - this.initialized = false; - } - - fetchUniformLocation(program: WebGLProgram, name: string): boolean { - if (!this.location && !this.initialized) { - this.location = this.gl.getUniformLocation(program, name); - this.initialized = true; - } - return !!this.location; - } - - +set: (program: WebGLProgram, name: string, v: T) => void; + gl: WebGLRenderingContext; + location: ?WebGLUniformLocation; + current: T; + initialized: boolean; + + constructor(context: Context) { + this.gl = context.gl; + this.initialized = false; + } + + fetchUniformLocation = (program: WebGLProgram, name: string): boolean => { + if (!this.location && !this.initialized) { + this.location = this.gl.getUniformLocation(program, name); + this.initialized = true; + } + return !!this.location; + }; + + +set: (program: WebGLProgram, name: string, v: T) => void; } class Uniform1i extends Uniform { diff --git a/src/source/custom_source.js b/src/source/custom_source.js index f4847b84690..0ce544a10c6 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -147,212 +147,269 @@ export type CustomSourceInterface = { onRemove: ?(map: Map) => void, } -class CustomSource extends Evented implements Source { - - id: string; - type: 'custom'; - scheme: string; - minzoom: number; - maxzoom: number; - tileSize: number; - attribution: string | void; - - roundZoom: boolean | void; - tileBounds: ?TileBounds; - minTileCacheSize: ?number; - maxTileCacheSize: ?number; - - _map: Map; - _loaded: boolean; - _dispatcher: Dispatcher; - _dataType: ?DataType; - _implementation: CustomSourceInterface; - - constructor(id: string, implementation: CustomSourceInterface, dispatcher: Dispatcher, eventedParent: Evented) { - super(); - this.id = id; - this.type = 'custom'; - this._dataType = 'raster'; - this._dispatcher = dispatcher; - this._implementation = implementation; - this.setEventedParent(eventedParent); - - this.scheme = 'xyz'; - this.minzoom = 0; - this.maxzoom = 22; - this.tileSize = 512; - - this._loaded = false; - this.roundZoom = true; - - if (!this._implementation) { - this.fire(new ErrorEvent(new Error(`Missing implementation for ${this.id} custom source`))); - } - - if (!this._implementation.loadTile) { - this.fire(new ErrorEvent(new Error(`Missing loadTile implementation for ${this.id} custom source`))); - } - - if (this._implementation.bounds) { - this.tileBounds = new TileBounds(this._implementation.bounds, this.minzoom, this.maxzoom); - } - - // $FlowFixMe[prop-missing] - implementation.update = this._update.bind(this); - - // $FlowFixMe[prop-missing] - implementation.clearTiles = this._clearTiles.bind(this); - - // $FlowFixMe[prop-missing] - implementation.coveringTiles = this._coveringTiles.bind(this); - - extend(this, pick(implementation, ['dataType', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution', 'minTileCacheSize', 'maxTileCacheSize'])); - } - - serialize(): Source { - return pick(this, ['type', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution']); - } - - load() { - this._loaded = true; - this.fire(new Event('data', {dataType: 'source', sourceDataType: 'metadata'})); - this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); - } - - loaded(): boolean { - return this._loaded; - } - - onAdd(map: Map): void { - this._map = map; - this._loaded = false; - this.fire(new Event('dataloading', {dataType: 'source'})); - if (this._implementation.onAdd) this._implementation.onAdd(map); - this.load(); - } - - onRemove(map: Map): void { - if (this._implementation.onRemove) { - this._implementation.onRemove(map); - } - } - - hasTile(tileID: OverscaledTileID): boolean { - if (this._implementation.hasTile) { - const {x, y, z} = tileID.canonical; - return this._implementation.hasTile({x, y, z}); - } - - return !this.tileBounds || this.tileBounds.contains(tileID.canonical); - } - - loadTile(tile: Tile, callback: Callback): void { - const {x, y, z} = tile.tileID.canonical; - const controller = new window.AbortController(); - const signal = controller.signal; - - // $FlowFixMe[prop-missing] - tile.request = Promise - .resolve(this._implementation.loadTile({x, y, z}, {signal})) - .then(tileLoaded.bind(this)) - .catch(error => { - // silence AbortError - if (error.code === 20) return; - tile.state = 'errored'; - callback(error); - }); - - // $FlowFixMe[prop-missing] - tile.request.cancel = () => controller.abort(); - - function tileLoaded(data) { - delete tile.request; - - if (tile.aborted) { - tile.state = 'unloaded'; - return callback(null); - } - - // If the implementation returned `undefined` as tile data, - // mark the tile as `errored` to indicate that we have no data for it. - // A map will render an overscaled parent tile in the tile’s space. - if (data === undefined) { - tile.state = 'errored'; - return callback(null); - } - - // If the implementation returned `null` as tile data, - // mark the tile as `loaded` and use an an empty image as tile data. - // A map will render nothing in the tile’s space. - if (data === null) { - const emptyImage = {width: this.tileSize, height: this.tileSize, data: null}; - this.loadTileData(tile, (emptyImage: any)); - tile.state = 'loaded'; - return callback(null); - } - - if (!isRaster(data)) { - tile.state = 'errored'; - return callback(new Error(`Can't infer data type for ${this.id}, only raster data supported at the moment`)); - } - - this.loadTileData(tile, data); - tile.state = 'loaded'; - callback(null); - } - } - - loadTileData(tile: Tile, data: T): void { - // Only raster data supported at the moment - RasterTileSource.loadTileData(tile, (data: any), this._map.painter); - } - - unloadTileData(tile: Tile): void { - // Only raster data supported at the moment - RasterTileSource.unloadTileData(tile, this._map.painter); - } - - unloadTile(tile: Tile, callback: Callback): void { - this.unloadTileData(tile); - if (this._implementation.unloadTile) { - const {x, y, z} = tile.tileID.canonical; - this._implementation.unloadTile({x, y, z}); - } - - callback(); - } - - abortTile(tile: Tile, callback: Callback): void { - if (tile.request && tile.request.cancel) { - tile.request.cancel(); - delete tile.request; - } - - callback(); - } - - hasTransition(): boolean { - return false; - } - - _coveringTiles(): { z: number, x: number, y: number }[] { - const tileIDs = this._map.transform.coveringTiles({ - tileSize: this.tileSize, - minzoom: this.minzoom, - maxzoom: this.maxzoom, - roundZoom: this.roundZoom - }); - - return tileIDs.map(tileID => ({x: tileID.canonical.x, y: tileID.canonical.y, z: tileID.canonical.z})); - } - - _clearTiles() { - this._map.style._clearSource(this.id); - } - - _update() { - this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); - } +class CustomSource + extends Evented + implements Source { + id: string; + type: 'custom'; + scheme: string; + minzoom: number; + maxzoom: number; + tileSize: number; + attribution: string | void; + + roundZoom: boolean | void; + tileBounds: ?TileBounds; + minTileCacheSize: ?number; + maxTileCacheSize: ?number; + + _map: Map; + _loaded: boolean; + _dispatcher: Dispatcher; + _dataType: ?DataType; + _implementation: CustomSourceInterface; + + constructor( + id: string, + implementation: CustomSourceInterface, + dispatcher: Dispatcher, + eventedParent: Evented, + ) { + super(); + this.id = id; + this.type = 'custom'; + this._dataType = 'raster'; + this._dispatcher = dispatcher; + this._implementation = implementation; + this.setEventedParent(eventedParent); + + this.scheme = 'xyz'; + this.minzoom = 0; + this.maxzoom = 22; + this.tileSize = 512; + + this._loaded = false; + this.roundZoom = true; + + if (!this._implementation) { + this.fire( + new ErrorEvent( + new Error(`Missing implementation for ${this.id} custom source`), + ), + ); + } + + if (!this._implementation.loadTile) { + this.fire( + new ErrorEvent( + new Error(`Missing loadTile implementation for ${this.id} custom source`,), + ), + ); + } + + if (this._implementation.bounds) { + this.tileBounds = new TileBounds( + this._implementation.bounds, + this.minzoom, + this.maxzoom, + ); + } + + // $FlowFixMe[prop-missing] + implementation.update = this._update.bind(this); + + // $FlowFixMe[prop-missing] + implementation.clearTiles = this._clearTiles.bind(this); + + // $FlowFixMe[prop-missing] + implementation.coveringTiles = this._coveringTiles.bind(this); + + extend( + this, + pick( + implementation, + [ + 'dataType', + 'scheme', + 'minzoom', + 'maxzoom', + 'tileSize', + 'attribution', + 'minTileCacheSize', + 'maxTileCacheSize', + ], + ), + ); + } + + serialize(): Source { + return pick( + this, + ['type', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution'], + ); + } + + load() { + this._loaded = true; + this.fire( + new Event('data', {dataType: 'source', sourceDataType: 'metadata'}), + ); + this.fire( + new Event('data', {dataType: 'source', sourceDataType: 'content'}), + ); + } + + loaded(): boolean { + return this._loaded; + } + + onAdd(map: Map): void { + this._map = map; + this._loaded = false; + this.fire(new Event('dataloading', {dataType: 'source'})); + if (this._implementation.onAdd) this._implementation.onAdd(map); + this.load(); + } + + onRemove(map: Map): void { + if (this._implementation.onRemove) { + this._implementation.onRemove(map); + } + } + + hasTile(tileID: OverscaledTileID): boolean { + if (this._implementation.hasTile) { + const {x, y, z} = tileID.canonical; + return this._implementation.hasTile({x, y, z}); + } + + return !this.tileBounds || this.tileBounds.contains(tileID.canonical); + } + + loadTile(tile: Tile, callback: Callback): void { + const {x, y, z} = tile.tileID.canonical; + const controller = new window.AbortController(); + const signal = controller.signal; + + // $FlowFixMe[prop-missing] + tile.request = Promise.resolve( + this._implementation.loadTile({x, y, z}, {signal}), + ).then(tileLoaded.bind(this)).catch( + error => { + // silence AbortError + if (error.code === 20) return; + tile.state = 'errored'; + callback(error); + }, + ); + + // $FlowFixMe[prop-missing] + tile.request.cancel = (() => controller.abort()); + + function tileLoaded(data) { + delete tile.request; + + if (tile.aborted) { + tile.state = 'unloaded'; + return callback(null); + } + + // If the implementation returned `undefined` as tile data, + // mark the tile as `errored` to indicate that we have no data for it. + // A map will render an overscaled parent tile in the tile’s space. + if (data === undefined) { + tile.state = 'errored'; + return callback(null); + } + + // If the implementation returned `null` as tile data, + // mark the tile as `loaded` and use an an empty image as tile data. + // A map will render nothing in the tile’s space. + if (data === null) { + const emptyImage = { + width: this.tileSize, + height: this.tileSize, + data: null, + }; + this.loadTileData(tile, (emptyImage: any)); + tile.state = 'loaded'; + return callback(null); + } + + if (!isRaster(data)) { + tile.state = 'errored'; + return callback( + new Error(`Can't infer data type for ${this.id}, only raster data supported at the moment`,), + ); + } + + this.loadTileData(tile, data); + tile.state = 'loaded'; + callback(null); + } + } + + loadTileData(tile: Tile, data: T): void { + // Only raster data supported at the moment + RasterTileSource.loadTileData(tile, (data: any), this._map.painter); + } + + unloadTileData(tile: Tile): void { + // Only raster data supported at the moment + RasterTileSource.unloadTileData(tile, this._map.painter); + } + + unloadTile(tile: Tile, callback: Callback): void { + this.unloadTileData(tile); + if (this._implementation.unloadTile) { + const {x, y, z} = tile.tileID.canonical; + this._implementation.unloadTile({x, y, z}); + } + + callback(); + } + + abortTile(tile: Tile, callback: Callback): void { + if (tile.request && tile.request.cancel) { + tile.request.cancel(); + delete tile.request; + } + + callback(); + } + + hasTransition(): boolean { + return false; + } + + _coveringTiles = (): Array<{ z: number, x: number, y: number }> => { + const tileIDs = this._map.transform.coveringTiles( + { + tileSize: this.tileSize, + minzoom: this.minzoom, + maxzoom: this.maxzoom, + roundZoom: this.roundZoom, + }, + ); + + return tileIDs.map( + tileID => ({ + x: tileID.canonical.x, + y: tileID.canonical.y, + z: tileID.canonical.z, + }), + ); + }; + + _clearTiles = () => { + this._map.style._clearSource(this.id); + }; + + _update = () => { + this.fire( + new Event('data', {dataType: 'source', sourceDataType: 'content'}), + ); + }; } export default CustomSource; diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 6faa5343b36..1e3cf7b7911 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -33,387 +33,435 @@ import type {QueryGeometry, TilespaceQueryGeometry} from '../style/query_geometr * * @private */ -class SourceCache extends Evented { - id: string; - map: MapboxMap; - style: Style; - - _source: Source; - _sourceLoaded: boolean; - _sourceErrored: boolean; - _tiles: {[_: string | number]: Tile}; - _prevLng: number | void; - _cache: TileCache; - _timers: {[_: any]: TimeoutID}; - _cacheTimers: {[_: any]: TimeoutID}; - _minTileCacheSize: ?number; - _maxTileCacheSize: ?number; - _paused: boolean; - _isRaster: boolean; - _shouldReloadOnResume: boolean; - _coveredTiles: {[_: number | string]: boolean}; - transform: Transform; - _isIdRenderable: (id: number, symbolLayer?: boolean) => boolean; - used: boolean; - usedForTerrain: boolean; - _state: SourceFeatureState; - _loadedParentTiles: {[_: number | string]: ?Tile}; - _onlySymbols: ?boolean; - - static maxUnderzooming: number; - static maxOverzooming: number; - - constructor(id: string, source: Source, onlySymbols?: boolean) { - super(); - this.id = id; - this._onlySymbols = onlySymbols; - - source.on('data', (e) => { - // this._sourceLoaded signifies that the TileJSON is loaded if applicable. - // if the source type does not come with a TileJSON, the flag signifies the - // source data has loaded (in other words, GeoJSON has been tiled on the worker and is ready) - if (e.dataType === 'source' && e.sourceDataType === 'metadata') this._sourceLoaded = true; - - // for sources with mutable data, this event fires when the underlying data - // to a source is changed (for example, using [GeoJSONSource#setData](https://docs.mapbox.com/mapbox-gl-js/api/sources/#geojsonsource#setdata) or [ImageSource#setCoordinates](https://docs.mapbox.com/mapbox-gl-js/api/sources/#imagesource#setcoordinates)) - if (this._sourceLoaded && !this._paused && e.dataType === "source" && e.sourceDataType === 'content') { - this.reload(); - if (this.transform) { - this.update(this.transform); - } - } - }); - - source.on('error', () => { - this._sourceErrored = true; - }); - - this._source = source; - this._tiles = {}; - this._cache = new TileCache(0, this._unloadTile.bind(this)); - this._timers = {}; - this._cacheTimers = {}; - this._minTileCacheSize = source.minTileCacheSize; - this._maxTileCacheSize = source.maxTileCacheSize; - this._loadedParentTiles = {}; - - this._coveredTiles = {}; - this._state = new SourceFeatureState(); - this._isRaster = - this._source.type === 'raster' || - this._source.type === 'raster-dem' || - // $FlowFixMe[prop-missing] - (this._source.type === 'custom' && this._source._dataType === 'raster'); - } - - onAdd(map: MapboxMap) { - this.map = map; - this._minTileCacheSize = this._minTileCacheSize === undefined && map ? map._minTileCacheSize : this._minTileCacheSize; - this._maxTileCacheSize = this._maxTileCacheSize === undefined && map ? map._maxTileCacheSize : this._maxTileCacheSize; - } - - /** +class SourceCache + extends Evented { + id: string; + map: MapboxMap; + style: Style; + + _source: Source; + _sourceLoaded: boolean; + _sourceErrored: boolean; + _tiles: { [_: string | number]: Tile }; + _prevLng: number | void; + _cache: TileCache; + _timers: { [_: any]: TimeoutID }; + _cacheTimers: { [_: any]: TimeoutID }; + _minTileCacheSize: ?number; + _maxTileCacheSize: ?number; + _paused: boolean; + _isRaster: boolean; + _shouldReloadOnResume: boolean; + _coveredTiles: { [_: number | string]: boolean }; + transform: Transform; + _isIdRenderable: (id: number, symbolLayer?: boolean) => boolean; + used: boolean; + usedForTerrain: boolean; + _state: SourceFeatureState; + _loadedParentTiles: { [_: number | string]: ?Tile }; + _onlySymbols: ?boolean; + + static maxUnderzooming: number; + static maxOverzooming: number; + + constructor(id: string, source: Source, onlySymbols?: boolean) { + super(); + this.id = id; + this._onlySymbols = onlySymbols; + + source.on( + 'data', + e => { + // this._sourceLoaded signifies that the TileJSON is loaded if applicable. + // if the source type does not come with a TileJSON, the flag signifies the + // source data has loaded (in other words, GeoJSON has been tiled on the worker and is ready) + if (e.dataType === 'source' && e.sourceDataType === 'metadata') + this._sourceLoaded = true; + + // for sources with mutable data, this event fires when the underlying data + // to a source is changed (for example, using [GeoJSONSource#setData](https://docs.mapbox.com/mapbox-gl-js/api/sources/#geojsonsource#setdata) or [ImageSource#setCoordinates](https://docs.mapbox.com/mapbox-gl-js/api/sources/#imagesource#setcoordinates)) + if ( + this._sourceLoaded && !this._paused && e.dataType === "source" && + e.sourceDataType === 'content' + ) { + this.reload(); + if (this.transform) { + this.update(this.transform); + } + } + }, + ); + + source.on( + 'error', + () => { + this._sourceErrored = true; + }, + ); + + this._source = source; + this._tiles = {}; + this._cache = new TileCache(0, this._unloadTile.bind(this)); + this._timers = {}; + this._cacheTimers = {}; + this._minTileCacheSize = source.minTileCacheSize; + this._maxTileCacheSize = source.maxTileCacheSize; + this._loadedParentTiles = {}; + + this._coveredTiles = {}; + this._state = new SourceFeatureState(); + this._isRaster = this._source.type === 'raster' || + this._source.type === 'raster-dem' || + // $FlowFixMe[prop-missing] + this._source.type === 'custom' && this._source._dataType === 'raster'; + } + + onAdd(map: MapboxMap) { + this.map = map; + this._minTileCacheSize = this._minTileCacheSize === undefined && map ? + map._minTileCacheSize : + this._minTileCacheSize; + this._maxTileCacheSize = this._maxTileCacheSize === undefined && map ? + map._maxTileCacheSize : + this._maxTileCacheSize; + } + + /** * Return true if no tile data is pending, tiles will not change unless * an additional API call is received. * @private */ - loaded(): boolean { - if (this._sourceErrored) { return true; } - if (!this._sourceLoaded) { return false; } - if (!this._source.loaded()) { return false; } - for (const t in this._tiles) { - const tile = this._tiles[t]; - if (tile.state !== 'loaded' && tile.state !== 'errored') - return false; - } - return true; - } - - getSource(): Source { - return this._source; - } - - pause() { - this._paused = true; - } - - resume() { - if (!this._paused) return; - const shouldReload = this._shouldReloadOnResume; - this._paused = false; - this._shouldReloadOnResume = false; - if (shouldReload) this.reload(); - if (this.transform) this.update(this.transform); - } - - _loadTile(tile: Tile, callback: Callback): void { - tile.isSymbolTile = this._onlySymbols; - return this._source.loadTile(tile, callback); - } - - _unloadTile(tile: Tile): void { - if (this._source.unloadTile) - return this._source.unloadTile(tile, () => {}); - } - - _abortTile(tile: Tile): void { - if (this._source.abortTile) - return this._source.abortTile(tile, () => {}); - } - - serialize(): SourceSpecification { - return this._source.serialize(); - } - - prepare(context: Context) { - if (this._source.prepare) { - this._source.prepare(); - } - - this._state.coalesceChanges(this._tiles, this.map ? this.map.painter : null); - - for (const i in this._tiles) { - const tile = this._tiles[i]; - tile.upload(context); - tile.prepare(this.map.style.imageManager); - } - } - - /** + loaded(): boolean { + if (this._sourceErrored) { + return true; + } + if (!this._sourceLoaded) { + return false; + } + if (!this._source.loaded()) { + return false; + } + for (const t in this._tiles) { + const tile = this._tiles[t]; + if (tile.state !== 'loaded' && tile.state !== 'errored') return false; + } + return true; + } + + getSource(): Source { + return this._source; + } + + pause() { + this._paused = true; + } + + resume() { + if (!this._paused) return; + const shouldReload = this._shouldReloadOnResume; + this._paused = false; + this._shouldReloadOnResume = false; + if (shouldReload) this.reload(); + if (this.transform) this.update(this.transform); + } + + _loadTile(tile: Tile, callback: Callback): void { + tile.isSymbolTile = this._onlySymbols; + return this._source.loadTile(tile, callback); + } + + _unloadTile = (tile: Tile): void => { + if (this._source.unloadTile) return this._source.unloadTile(tile, () => {}); + }; + + _abortTile(tile: Tile): void { + if (this._source.abortTile) return this._source.abortTile(tile, () => {}); + } + + serialize(): SourceSpecification { + return this._source.serialize(); + } + + prepare(context: Context) { + if (this._source.prepare) { + this._source.prepare(); + } + + this._state.coalesceChanges(this._tiles, this.map ? this.map.painter : null); + + for (const i in this._tiles) { + const tile = this._tiles[i]; + tile.upload(context); + tile.prepare(this.map.style.imageManager); + } + } + + /** * Return all tile ids ordered with z-order, and cast to numbers * @private */ - getIds(): Array { - return values((this._tiles: any)).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); - } - - getRenderableIds(symbolLayer?: boolean): Array { - const renderables: Array = []; - for (const id in this._tiles) { - if (this._isIdRenderable(+id, symbolLayer)) renderables.push(this._tiles[id]); - } - if (symbolLayer) { - return renderables.sort((a_: Tile, b_: Tile) => { - const a = a_.tileID; - const b = b_.tileID; - const rotatedA = (new Point(a.canonical.x, a.canonical.y))._rotate(this.transform.angle); - const rotatedB = (new Point(b.canonical.x, b.canonical.y))._rotate(this.transform.angle); - return a.overscaledZ - b.overscaledZ || rotatedB.y - rotatedA.y || rotatedB.x - rotatedA.x; - }).map(tile => tile.tileID.key); - } - return renderables.map(tile => tile.tileID).sort(compareTileId).map(id => id.key); - } - - hasRenderableParent(tileID: OverscaledTileID): boolean { - const parentTile = this.findLoadedParent(tileID, 0); - if (parentTile) { - return this._isIdRenderable(parentTile.tileID.key); - } - return false; - } - - _isIdRenderable(id: number, symbolLayer?: boolean): boolean { - return this._tiles[id] && this._tiles[id].hasData() && - !this._coveredTiles[id] && (symbolLayer || !this._tiles[id].holdingForFade()); - } - - reload() { - if (this._paused) { - this._shouldReloadOnResume = true; - return; - } - - this._cache.reset(); - - for (const i in this._tiles) { - if (this._tiles[i].state !== "errored") this._reloadTile(+i, 'reloading'); - } - } - - _reloadTile(id: number, state: TileState) { - const tile = this._tiles[id]; - - // this potentially does not address all underlying - // issues https://github.com/mapbox/mapbox-gl-js/issues/4252 - // - hard to tell without repro steps - if (!tile) return; - - // The difference between "loading" tiles and "reloading" or "expired" - // tiles is that "reloading"/"expired" tiles are "renderable". - // Therefore, a "loading" tile cannot become a "reloading" tile without - // first becoming a "loaded" tile. - if (tile.state !== 'loading') { - tile.state = state; - } - - this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); - } - - _tileLoaded(tile: Tile, id: number, previousState: TileState, err: ?Error) { - if (err) { - tile.state = 'errored'; - if ((err: any).status !== 404) this._source.fire(new ErrorEvent(err, {tile})); - else { - // continue to try loading parent/children tiles if a tile doesn't exist (404) - const updateForTerrain = this._source.type === 'raster-dem' && this.usedForTerrain; - if (updateForTerrain && this.map.painter.terrain) { - const terrain = this.map.painter.terrain; - this.update(this.transform, terrain.getScaledDemTileSize(), true); - terrain.resetTileLookupCache(this.id); - } else { - this.update(this.transform); - } - } - return; - } - - tile.timeAdded = browser.now(); - if (previousState === 'expired') tile.refreshedUponExpiration = true; - this._setTileReloadTimer(id, tile); - if (this._source.type === 'raster-dem' && tile.dem) this._backfillDEM(tile); - this._state.initializeTileState(tile, this.map ? this.map.painter : null); - - this._source.fire(new Event('data', {dataType: 'source', tile, coord: tile.tileID, 'sourceCacheId': this.id})); - } - - /** + getIds(): Array { + return values((this._tiles: any)).map((tile: Tile) => tile.tileID).sort( + compareTileId, + ).map(id => id.key); + } + + getRenderableIds(symbolLayer?: boolean): Array { + const renderables: Array = []; + for (const id in this._tiles) { + if (this._isIdRenderable(+id, symbolLayer)) + renderables.push(this._tiles[id]); + } + if (symbolLayer) { + return renderables.sort( + (a_: Tile, b_: Tile) => { + const a = a_.tileID; + const b = b_.tileID; + const rotatedA = new Point(a.canonical.x, a.canonical.y)._rotate( + this.transform.angle, + ); + const rotatedB = new Point(b.canonical.x, b.canonical.y)._rotate( + this.transform.angle, + ); + return ( + a.overscaledZ - b.overscaledZ || rotatedB.y - rotatedA.y || + rotatedB.x - rotatedA.x + ); + }, + ).map(tile => tile.tileID.key); + } + return renderables.map(tile => tile.tileID).sort(compareTileId).map( + id => id.key, + ); + } + + hasRenderableParent(tileID: OverscaledTileID): boolean { + const parentTile = this.findLoadedParent(tileID, 0); + if (parentTile) { + return this._isIdRenderable(parentTile.tileID.key); + } + return false; + } + + _isIdRenderable(id: number, symbolLayer?: boolean): boolean { + return ( + this._tiles[id] && this._tiles[id].hasData() && !this._coveredTiles[id] && + (symbolLayer || !this._tiles[id].holdingForFade()) + ); + } + + reload() { + if (this._paused) { + this._shouldReloadOnResume = true; + return; + } + + this._cache.reset(); + + for (const i in this._tiles) { + if (this._tiles[i].state !== "errored") this._reloadTile(+i, 'reloading'); + } + } + + _reloadTile(id: number, state: TileState) { + const tile = this._tiles[id]; + + // this potentially does not address all underlying + // issues https://github.com/mapbox/mapbox-gl-js/issues/4252 + // - hard to tell without repro steps + if (!tile) return; + + // The difference between "loading" tiles and "reloading" or "expired" + // tiles is that "reloading"/"expired" tiles are "renderable". + // Therefore, a "loading" tile cannot become a "reloading" tile without + // first becoming a "loaded" tile. + if (tile.state !== 'loading') { + tile.state = state; + } + + this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); + } + + _tileLoaded = (tile: Tile, id: number, previousState: TileState, err: ?Error) => { + if (err) { + tile.state = 'errored'; + if ((err: any).status !== 404) + this._source.fire(new ErrorEvent(err, {tile})); else { + // continue to try loading parent/children tiles if a tile doesn't exist (404) + const updateForTerrain = this._source.type === 'raster-dem' && + this.usedForTerrain; + if (updateForTerrain && this.map.painter.terrain) { + const terrain = this.map.painter.terrain; + this.update(this.transform, terrain.getScaledDemTileSize(), true); + terrain.resetTileLookupCache(this.id); + } else { + this.update(this.transform); + } + } + return; + } + + tile.timeAdded = browser.now(); + if (previousState === 'expired') tile.refreshedUponExpiration = true; + this._setTileReloadTimer(id, tile); + if (this._source.type === 'raster-dem' && tile.dem) this._backfillDEM(tile); + this._state.initializeTileState(tile, this.map ? this.map.painter : null); + + this._source.fire( + new Event( + 'data', + { + dataType: 'source', + tile, + coord: tile.tileID, + 'sourceCacheId': this.id, + }, + ), + ); + }; + + /** * For raster terrain source, backfill DEM to eliminate visible tile boundaries * @private */ - _backfillDEM(tile: Tile) { - const renderables = this.getRenderableIds(); - for (let i = 0; i < renderables.length; i++) { - const borderId = renderables[i]; - if (tile.neighboringTiles && tile.neighboringTiles[borderId]) { - const borderTile = this.getTileByID(borderId); - fillBorder(tile, borderTile); - fillBorder(borderTile, tile); - } - } - - function fillBorder(tile, borderTile) { - if (!tile.dem || tile.dem.borderReady) return; - tile.needsHillshadePrepare = true; - tile.needsDEMTextureUpload = true; - let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x; - const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y; - const dim = Math.pow(2, tile.tileID.canonical.z); - const borderId = borderTile.tileID.key; - if (dx === 0 && dy === 0) return; - - if (Math.abs(dy) > 1) { - return; - } - if (Math.abs(dx) > 1) { - // Adjust the delta coordinate for world wraparound. - if (Math.abs(dx + dim) === 1) { - dx += dim; - } else if (Math.abs(dx - dim) === 1) { - dx -= dim; - } - } - if (!borderTile.dem || !tile.dem) return; - tile.dem.backfillBorder(borderTile.dem, dx, dy); - if (tile.neighboringTiles && tile.neighboringTiles[borderId]) - tile.neighboringTiles[borderId].backfilled = true; - } - } - /** + _backfillDEM(tile: Tile) { + const renderables = this.getRenderableIds(); + for (let i = 0; i < renderables.length; i++) { + const borderId = renderables[i]; + if (tile.neighboringTiles && tile.neighboringTiles[borderId]) { + const borderTile = this.getTileByID(borderId); + fillBorder(tile, borderTile); + fillBorder(borderTile, tile); + } + } + + function fillBorder(tile, borderTile) { + if (!tile.dem || tile.dem.borderReady) return; + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x; + const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y; + const dim = Math.pow(2, tile.tileID.canonical.z); + const borderId = borderTile.tileID.key; + if (dx === 0 && dy === 0) return; + + if (Math.abs(dy) > 1) { + return; + } + if (Math.abs(dx) > 1) { + // Adjust the delta coordinate for world wraparound. + if (Math.abs(dx + dim) === 1) { + dx += dim; + } else if (Math.abs(dx - dim) === 1) { + dx -= dim; + } + } + if (!borderTile.dem || !tile.dem) return; + tile.dem.backfillBorder(borderTile.dem, dx, dy); + if (tile.neighboringTiles && tile.neighboringTiles[borderId]) + tile.neighboringTiles[borderId].backfilled = true; + } + } + /** * Get a specific tile by TileID * @private */ - getTile(tileID: OverscaledTileID): Tile { - return this.getTileByID(tileID.key); - } + getTile(tileID: OverscaledTileID): Tile { + return this.getTileByID(tileID.key); + } - /** + /** * Get a specific tile by id * @private */ - getTileByID(id: number): Tile { - return this._tiles[id]; - } + getTileByID(id: number): Tile { + return this._tiles[id]; + } - /** + /** * For a given set of tiles, retain children that are loaded and have a zoom * between `zoom` (exclusive) and `maxCoveringZoom` (inclusive) * @private */ - _retainLoadedChildren( - idealTiles: {[number | string]: OverscaledTileID}, - zoom: number, - maxCoveringZoom: number, - retain: {[number | string]: OverscaledTileID} - ) { - for (const id in this._tiles) { - let tile = this._tiles[id]; - - // only consider renderable tiles up to maxCoveringZoom - if (retain[id] || - !tile.hasData() || - tile.tileID.overscaledZ <= zoom || - tile.tileID.overscaledZ > maxCoveringZoom - ) continue; - - // loop through parents and retain the topmost loaded one if found - let topmostLoadedID = tile.tileID; - while (tile && tile.tileID.overscaledZ > zoom + 1) { - const parentID = tile.tileID.scaledTo(tile.tileID.overscaledZ - 1); - - tile = this._tiles[parentID.key]; - - if (tile && tile.hasData()) { - topmostLoadedID = parentID; - } - } - - // loop through ancestors of the topmost loaded child to see if there's one that needed it - let tileID = topmostLoadedID; - while (tileID.overscaledZ > zoom) { - tileID = tileID.scaledTo(tileID.overscaledZ - 1); - - if (idealTiles[tileID.key]) { - // found a parent that needed a loaded child; retain that child - retain[topmostLoadedID.key] = topmostLoadedID; - break; - } - } - } - } - - /** + _retainLoadedChildren( + idealTiles: { [number | string]: OverscaledTileID }, + zoom: number, + maxCoveringZoom: number, + retain: { [number | string]: OverscaledTileID }, + ) { + for (const id in this._tiles) { + let tile = this._tiles[id]; + + // only consider renderable tiles up to maxCoveringZoom + if ( + retain[id] || !tile.hasData() || tile.tileID.overscaledZ <= zoom || + tile.tileID.overscaledZ > maxCoveringZoom + ) + continue; + + // loop through parents and retain the topmost loaded one if found + let topmostLoadedID = tile.tileID; + while (tile && tile.tileID.overscaledZ > zoom + 1) { + const parentID = tile.tileID.scaledTo(tile.tileID.overscaledZ - 1); + + tile = this._tiles[parentID.key]; + + if (tile && tile.hasData()) { + topmostLoadedID = parentID; + } + } + + // loop through ancestors of the topmost loaded child to see if there's one that needed it + let tileID = topmostLoadedID; + while (tileID.overscaledZ > zoom) { + tileID = tileID.scaledTo(tileID.overscaledZ - 1); + + if (idealTiles[tileID.key]) { + // found a parent that needed a loaded child; retain that child + retain[topmostLoadedID.key] = topmostLoadedID; + break; + } + } + } + } + + /** * Find a loaded parent of the given tile (up to minCoveringZoom) * @private */ - findLoadedParent(tileID: OverscaledTileID, minCoveringZoom: number): ?Tile { - if (tileID.key in this._loadedParentTiles) { - const parent = this._loadedParentTiles[tileID.key]; - if (parent && parent.tileID.overscaledZ >= minCoveringZoom) { - return parent; - } else { - return null; - } - } - for (let z = tileID.overscaledZ - 1; z >= minCoveringZoom; z--) { - const parentTileID = tileID.scaledTo(z); - const tile = this._getLoadedTile(parentTileID); - if (tile) { - return tile; - } - } - } - - _getLoadedTile(tileID: OverscaledTileID): ?Tile { - const tile = this._tiles[tileID.key]; - if (tile && tile.hasData()) { - return tile; - } - // TileCache ignores wrap in lookup. - const cachedTile = this._cache.getByKey(this._source.reparseOverscaled ? tileID.wrapped().key : tileID.canonical.key); - return cachedTile; - } - - /** + findLoadedParent(tileID: OverscaledTileID, minCoveringZoom: number): ?Tile { + if (tileID.key in this._loadedParentTiles) { + const parent = this._loadedParentTiles[tileID.key]; + if (parent && parent.tileID.overscaledZ >= minCoveringZoom) { + return parent; + } else { + return null; + } + } + for (let z = tileID.overscaledZ - 1; z >= minCoveringZoom; z--) { + const parentTileID = tileID.scaledTo(z); + const tile = this._getLoadedTile(parentTileID); + if (tile) { + return tile; + } + } + } + + _getLoadedTile(tileID: OverscaledTileID): ?Tile { + const tile = this._tiles[tileID.key]; + if (tile && tile.hasData()) { + return tile; + } + // TileCache ignores wrap in lookup. + const cachedTile = this._cache.getByKey( + this._source.reparseOverscaled ? + tileID.wrapped().key : + tileID.canonical.key, + ); + return cachedTile; + } + + /** * Resizes the tile cache based on the current viewport's size * or the minTileCacheSize and maxTileCacheSize options passed during map creation * @@ -422,64 +470,68 @@ class SourceCache extends Evented { * the map is more important. * @private */ - updateCacheSize(transform: Transform, tileSize?: number) { - tileSize = tileSize || this._source.tileSize; - const widthInTiles = Math.ceil(transform.width / tileSize) + 1; - const heightInTiles = Math.ceil(transform.height / tileSize) + 1; - const approxTilesInView = widthInTiles * heightInTiles; - const commonZoomRange = 5; - - const viewDependentMaxSize = Math.floor(approxTilesInView * commonZoomRange); - const minSize = typeof this._minTileCacheSize === 'number' ? Math.max(this._minTileCacheSize, viewDependentMaxSize) : viewDependentMaxSize; - const maxSize = typeof this._maxTileCacheSize === 'number' ? Math.min(this._maxTileCacheSize, minSize) : minSize; - - this._cache.setMaxSize(maxSize); - } - - handleWrapJump(lng: number) { - // On top of the regular z/x/y values, TileIDs have a `wrap` value that specify - // which copy of the world the tile belongs to. For example, at `lng: 10` you - // might render z/x/y/0 while at `lng: 370` you would render z/x/y/1. - // - // When lng values get wrapped (going from `lng: 370` to `long: 10`) you expect - // to see the same thing on the screen (370 degrees and 10 degrees is the same - // place in the world) but all the TileIDs will have different wrap values. - // - // In order to make this transition seamless, we calculate the rounded difference of - // "worlds" between the last frame and the current frame. If the map panned by - // a world, then we can assign all the tiles new TileIDs with updated wrap values. - // For example, assign z/x/y/1 a new id: z/x/y/0. It is the same tile, just rendered - // in a different position. - // - // This enables us to reuse the tiles at more ideal locations and prevent flickering. - const prevLng = this._prevLng === undefined ? lng : this._prevLng; - const lngDifference = lng - prevLng; - const worldDifference = lngDifference / 360; - const wrapDelta = Math.round(worldDifference); - this._prevLng = lng; - - if (wrapDelta) { - const tiles: {[_: string | number]: Tile} = {}; - for (const key in this._tiles) { - const tile = this._tiles[key]; - tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); - tiles[tile.tileID.key] = tile; - } - this._tiles = tiles; - - // Reset tile reload timers - for (const id in this._timers) { - clearTimeout(this._timers[id]); - delete this._timers[id]; - } - for (const id in this._tiles) { - const tile = this._tiles[id]; - this._setTileReloadTimer(+id, tile); - } - } - } - - /** + updateCacheSize(transform: Transform, tileSize?: number) { + tileSize = tileSize || this._source.tileSize; + const widthInTiles = Math.ceil(transform.width / tileSize) + 1; + const heightInTiles = Math.ceil(transform.height / tileSize) + 1; + const approxTilesInView = widthInTiles * heightInTiles; + const commonZoomRange = 5; + + const viewDependentMaxSize = Math.floor(approxTilesInView * commonZoomRange); + const minSize = typeof this._minTileCacheSize === 'number' ? + Math.max(this._minTileCacheSize, viewDependentMaxSize) : + viewDependentMaxSize; + const maxSize = typeof this._maxTileCacheSize === 'number' ? + Math.min(this._maxTileCacheSize, minSize) : + minSize; + + this._cache.setMaxSize(maxSize); + } + + handleWrapJump(lng: number) { + // On top of the regular z/x/y values, TileIDs have a `wrap` value that specify + // which copy of the world the tile belongs to. For example, at `lng: 10` you + // might render z/x/y/0 while at `lng: 370` you would render z/x/y/1. + // + // When lng values get wrapped (going from `lng: 370` to `long: 10`) you expect + // to see the same thing on the screen (370 degrees and 10 degrees is the same + // place in the world) but all the TileIDs will have different wrap values. + // + // In order to make this transition seamless, we calculate the rounded difference of + // "worlds" between the last frame and the current frame. If the map panned by + // a world, then we can assign all the tiles new TileIDs with updated wrap values. + // For example, assign z/x/y/1 a new id: z/x/y/0. It is the same tile, just rendered + // in a different position. + // + // This enables us to reuse the tiles at more ideal locations and prevent flickering. + const prevLng = this._prevLng === undefined ? lng : this._prevLng; + const lngDifference = lng - prevLng; + const worldDifference = lngDifference / 360; + const wrapDelta = Math.round(worldDifference); + this._prevLng = lng; + + if (wrapDelta) { + const tiles: { [_: string | number]: Tile } = {}; + for (const key in this._tiles) { + const tile = this._tiles[key]; + tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); + tiles[tile.tileID.key] = tile; + } + this._tiles = tiles; + + // Reset tile reload timers + for (const id in this._timers) { + clearTimeout(this._timers[id]); + delete this._timers[id]; + } + for (const id in this._tiles) { + const tile = this._tiles[id]; + this._setTileReloadTimer(+id, tile); + } + } + } + + /** * Removes tiles that are outside the viewport and adds new tiles that * are inside the viewport. * @private @@ -488,358 +540,418 @@ class SourceCache extends Evented { * @param {tileSize} tileSize If needed to get lower resolution ideal cover, * override source.tileSize used in tile cover calculation. */ - update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { - this.transform = transform; - if (!this._sourceLoaded || this._paused || this.transform.freezeTileCoverage) { return; } - assert(!(updateForTerrain && !this.usedForTerrain)); - if (this.usedForTerrain && !updateForTerrain) { - // If source is used for both terrain and hillshade, don't update it twice. - return; - } - - this.updateCacheSize(transform, tileSize); - if (this.transform.projection.name !== 'globe') { - this.handleWrapJump(this.transform.center.lng); - } - - // Covered is a list of retained tiles who's areas are fully covered by other, - // better, retained tiles. They are not drawn separately. - this._coveredTiles = {}; - - let idealTileIDs; - if (!this.used && !this.usedForTerrain) { - idealTileIDs = []; - } else if (this._source.tileID) { - idealTileIDs = transform.getVisibleUnwrappedCoordinates(this._source.tileID) - .map((unwrapped) => new OverscaledTileID(unwrapped.canonical.z, unwrapped.wrap, unwrapped.canonical.z, unwrapped.canonical.x, unwrapped.canonical.y)); - } else { - idealTileIDs = transform.coveringTiles({ - tileSize: tileSize || this._source.tileSize, - minzoom: this._source.minzoom, - maxzoom: this._source.maxzoom, - roundZoom: this._source.roundZoom && !updateForTerrain, - reparseOverscaled: this._source.reparseOverscaled, - isTerrainDEM: this.usedForTerrain - }); - - if (this._source.hasTile) { - idealTileIDs = idealTileIDs.filter((coord) => (this._source.hasTile: any)(coord)); - } - } - - // Retain is a list of tiles that we shouldn't delete, even if they are not - // the most ideal tile for the current viewport. This may include tiles like - // parent or child tiles that are *already* loaded. - const retain = this._updateRetainedTiles(idealTileIDs); - - if (isRasterType(this._source.type) && idealTileIDs.length !== 0) { - const parentsForFading: {[_: string | number]: OverscaledTileID} = {}; - const fadingTiles = {}; - const ids = Object.keys(retain); - for (const id of ids) { - const tileID = retain[id]; - assert(tileID.key === +id); - - const tile = this._tiles[id]; - if (!tile || (tile.fadeEndTime && tile.fadeEndTime <= browser.now())) continue; - - // if the tile is loaded but still fading in, find parents to cross-fade with it - const parentTile = this.findLoadedParent(tileID, Math.max(tileID.overscaledZ - SourceCache.maxOverzooming, this._source.minzoom)); - if (parentTile) { - this._addTile(parentTile.tileID); - parentsForFading[parentTile.tileID.key] = parentTile.tileID; - } - - fadingTiles[id] = tileID; - } - - // for children tiles with parent tiles still fading in, - // retain the children so the parent can fade on top - const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; - for (const id in this._tiles) { - const childTile = this._tiles[id]; - if (retain[id] || !childTile.hasData()) { - continue; - } - - let parentID = childTile.tileID; - while (parentID.overscaledZ > minZoom) { - parentID = parentID.scaledTo(parentID.overscaledZ - 1); - const tile = this._tiles[parentID.key]; - if (tile && tile.hasData() && fadingTiles[parentID.key]) { - retain[id] = childTile.tileID; - break; - } - } - } - - for (const id in parentsForFading) { - if (!retain[id]) { - // If a tile is only needed for fading, mark it as covered so that it isn't rendered on it's own. - this._coveredTiles[id] = true; - retain[id] = parentsForFading[id]; - } - } - } - - for (const retainedId in retain) { - // Make sure retained tiles always clear any existing fade holds - // so that if they're removed again their fade timer starts fresh. - this._tiles[retainedId].clearFadeHold(); - } - - // Remove the tiles we don't need anymore. - const remove = keysDifference((this._tiles: any), (retain: any)); - for (const tileID of remove) { - const tile = this._tiles[tileID]; - if (tile.hasSymbolBuckets && !tile.holdingForFade()) { - tile.setHoldDuration(this.map._fadeDuration); - } else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) { - this._removeTile(+tileID); - } - } - - // Construct a cache of loaded parents - this._updateLoadedParentTileCache(); - - if (this._onlySymbols && this._source.afterUpdate) { - this._source.afterUpdate(); - } - } - - releaseSymbolFadeTiles() { - for (const id in this._tiles) { - if (this._tiles[id].holdingForFade()) { - this._removeTile(+id); - } - } - } - - _updateRetainedTiles(idealTileIDs: Array): {[_: number | string]: OverscaledTileID} { - const retain: {[_: number | string]: OverscaledTileID} = {}; - if (idealTileIDs.length === 0) { return retain; } - - const checked: {[_: number | string]: boolean } = {}; - const minZoom = idealTileIDs.reduce((min, id) => Math.min(min, id.overscaledZ), Infinity); - const maxZoom = idealTileIDs[0].overscaledZ; - assert(minZoom <= maxZoom); - const minCoveringZoom = Math.max(maxZoom - SourceCache.maxOverzooming, this._source.minzoom); - const maxCoveringZoom = Math.max(maxZoom + SourceCache.maxUnderzooming, this._source.minzoom); - - const missingTiles = {}; - for (const tileID of idealTileIDs) { - const tile = this._addTile(tileID); - - // retain the tile even if it's not loaded because it's an ideal tile. - retain[tileID.key] = tileID; - - if (tile.hasData()) continue; - - if (minZoom < this._source.maxzoom) { - // save missing tiles that potentially have loaded children - missingTiles[tileID.key] = tileID; - } - } - - // retain any loaded children of ideal tiles up to maxCoveringZoom - this._retainLoadedChildren(missingTiles, minZoom, maxCoveringZoom, retain); - - for (const tileID of idealTileIDs) { - let tile = this._tiles[tileID.key]; - - if (tile.hasData()) continue; - - // The tile we require is not yet loaded or does not exist; - // Attempt to find children that fully cover it. - - if (tileID.canonical.z >= this._source.maxzoom) { - // We're looking for an overzoomed child tile. - const childCoord = tileID.children(this._source.maxzoom)[0]; - const childTile = this.getTile(childCoord); - if (!!childTile && childTile.hasData()) { - retain[childCoord.key] = childCoord; - continue; // tile is covered by overzoomed child - } - } else { - // Check if all 4 immediate children are loaded (in other words, the missing ideal tile is covered) - const children = tileID.children(this._source.maxzoom); - - if (retain[children[0].key] && - retain[children[1].key] && - retain[children[2].key] && - retain[children[3].key]) continue; // tile is covered by children - } - - // We couldn't find child tiles that entirely cover the ideal tile; look for parents now. - - // As we ascend up the tile pyramid of the ideal tile, we check whether the parent - // tile has been previously requested (and errored because we only loop over tiles with no data) - // in order to determine if we need to request its parent. - let parentWasRequested = tile.wasRequested(); - - for (let overscaledZ = tileID.overscaledZ - 1; overscaledZ >= minCoveringZoom; --overscaledZ) { - const parentId = tileID.scaledTo(overscaledZ); - - // Break parent tile ascent if this route has been previously checked by another child. - if (checked[parentId.key]) break; - checked[parentId.key] = true; - - tile = this.getTile(parentId); - if (!tile && parentWasRequested) { - tile = this._addTile(parentId); - } - if (tile) { - retain[parentId.key] = parentId; - // Save the current values, since they're the parent of the next iteration - // of the parent tile ascent loop. - parentWasRequested = tile.wasRequested(); - if (tile.hasData()) break; - } - } - } - - return retain; - } - - _updateLoadedParentTileCache() { - this._loadedParentTiles = {}; - - for (const tileKey in this._tiles) { - const path = []; - let parentTile: ?Tile; - let currentId = this._tiles[tileKey].tileID; - - // Find the closest loaded ancestor by traversing the tile tree towards the root and - // caching results along the way - while (currentId.overscaledZ > 0) { - - // Do we have a cached result from previous traversals? - if (currentId.key in this._loadedParentTiles) { - parentTile = this._loadedParentTiles[currentId.key]; - break; - } - - path.push(currentId.key); - - // Is the parent loaded? - const parentId = currentId.scaledTo(currentId.overscaledZ - 1); - parentTile = this._getLoadedTile(parentId); - if (parentTile) { - break; - } - - currentId = parentId; - } - - // Cache the result of this traversal to all newly visited tiles - for (const key of path) { - this._loadedParentTiles[key] = parentTile; - } - } - } - - /** + update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { + this.transform = transform; + if ( + !this._sourceLoaded || this._paused || this.transform.freezeTileCoverage + ) { + return; + } + assert(!(updateForTerrain && !this.usedForTerrain)); + if (this.usedForTerrain && !updateForTerrain) { + // If source is used for both terrain and hillshade, don't update it twice. + return; + } + + this.updateCacheSize(transform, tileSize); + if (this.transform.projection.name !== 'globe') { + this.handleWrapJump(this.transform.center.lng); + } + + // Covered is a list of retained tiles who's areas are fully covered by other, + // better, retained tiles. They are not drawn separately. + this._coveredTiles = {}; + + let idealTileIDs; + if (!this.used && !this.usedForTerrain) { + idealTileIDs = []; + } else if (this._source.tileID) { + idealTileIDs = transform.getVisibleUnwrappedCoordinates( + this._source.tileID, + ).map( + unwrapped => new OverscaledTileID( + unwrapped.canonical.z, + unwrapped.wrap, + unwrapped.canonical.z, + unwrapped.canonical.x, + unwrapped.canonical.y, + ), + ); + } else { + idealTileIDs = transform.coveringTiles( + { + tileSize: tileSize || this._source.tileSize, + minzoom: this._source.minzoom, + maxzoom: this._source.maxzoom, + roundZoom: this._source.roundZoom && !updateForTerrain, + reparseOverscaled: this._source.reparseOverscaled, + isTerrainDEM: this.usedForTerrain, + }, + ); + + if (this._source.hasTile) { + idealTileIDs = idealTileIDs.filter( + coord => (this._source.hasTile: any)(coord), + ); + } + } + + // Retain is a list of tiles that we shouldn't delete, even if they are not + // the most ideal tile for the current viewport. This may include tiles like + // parent or child tiles that are *already* loaded. + const retain = this._updateRetainedTiles(idealTileIDs); + + if (isRasterType(this._source.type) && idealTileIDs.length !== 0) { + const parentsForFading: { [_: string | number]: OverscaledTileID } = {}; + const fadingTiles = {}; + const ids = Object.keys(retain); + for (const id of ids) { + const tileID = retain[id]; + assert(tileID.key === +id); + + const tile = this._tiles[id]; + if (!tile || tile.fadeEndTime && tile.fadeEndTime <= browser.now()) + continue; + + // if the tile is loaded but still fading in, find parents to cross-fade with it + const parentTile = this.findLoadedParent( + tileID, + Math.max( + tileID.overscaledZ - SourceCache.maxOverzooming, + this._source.minzoom, + ), + ); + if (parentTile) { + this._addTile(parentTile.tileID); + parentsForFading[parentTile.tileID.key] = parentTile.tileID; + } + + fadingTiles[id] = tileID; + } + + // for children tiles with parent tiles still fading in, + // retain the children so the parent can fade on top + const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; + for (const id in this._tiles) { + const childTile = this._tiles[id]; + if (retain[id] || !childTile.hasData()) { + continue; + } + + let parentID = childTile.tileID; + while (parentID.overscaledZ > minZoom) { + parentID = parentID.scaledTo(parentID.overscaledZ - 1); + const tile = this._tiles[parentID.key]; + if (tile && tile.hasData() && fadingTiles[parentID.key]) { + retain[id] = childTile.tileID; + break; + } + } + } + + for (const id in parentsForFading) { + if (!retain[id]) { + // If a tile is only needed for fading, mark it as covered so that it isn't rendered on it's own. + this._coveredTiles[id] = true; + retain[id] = parentsForFading[id]; + } + } + } + + for (const retainedId in retain) { + // Make sure retained tiles always clear any existing fade holds + // so that if they're removed again their fade timer starts fresh. + this._tiles[retainedId].clearFadeHold(); + } + + // Remove the tiles we don't need anymore. + const remove = keysDifference((this._tiles: any), (retain: any)); + for (const tileID of remove) { + const tile = this._tiles[tileID]; + if (tile.hasSymbolBuckets && !tile.holdingForFade()) { + tile.setHoldDuration(this.map._fadeDuration); + } else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) { + this._removeTile(+tileID); + } + } + + // Construct a cache of loaded parents + this._updateLoadedParentTileCache(); + + if (this._onlySymbols && this._source.afterUpdate) { + this._source.afterUpdate(); + } + } + + releaseSymbolFadeTiles() { + for (const id in this._tiles) { + if (this._tiles[id].holdingForFade()) { + this._removeTile(+id); + } + } + } + + _updateRetainedTiles( + idealTileIDs: Array, + ): { [_: number | string]: OverscaledTileID } { + const retain: { [_: number | string]: OverscaledTileID } = {}; + if (idealTileIDs.length === 0) { + return retain; + } + + const checked: { [_: number | string]: boolean } = {}; + const minZoom = idealTileIDs.reduce( + (min, id) => Math.min(min, id.overscaledZ), + Infinity, + ); + const maxZoom = idealTileIDs[0].overscaledZ; + assert(minZoom <= maxZoom); + const minCoveringZoom = Math.max( + maxZoom - SourceCache.maxOverzooming, + this._source.minzoom, + ); + const maxCoveringZoom = Math.max( + maxZoom + SourceCache.maxUnderzooming, + this._source.minzoom, + ); + + const missingTiles = {}; + for (const tileID of idealTileIDs) { + const tile = this._addTile(tileID); + + // retain the tile even if it's not loaded because it's an ideal tile. + retain[tileID.key] = tileID; + + if (tile.hasData()) continue; + + if (minZoom < this._source.maxzoom) { + // save missing tiles that potentially have loaded children + missingTiles[tileID.key] = tileID; + } + } + + // retain any loaded children of ideal tiles up to maxCoveringZoom + this._retainLoadedChildren(missingTiles, minZoom, maxCoveringZoom, retain); + + for (const tileID of idealTileIDs) { + let tile = this._tiles[tileID.key]; + + if (tile.hasData()) continue; + + // The tile we require is not yet loaded or does not exist; + // Attempt to find children that fully cover it. + + if (tileID.canonical.z >= this._source.maxzoom) { + // We're looking for an overzoomed child tile. + const childCoord = tileID.children(this._source.maxzoom)[0]; + const childTile = this.getTile(childCoord); + if (!!childTile && childTile.hasData()) { + retain[childCoord.key] = childCoord; + continue; // tile is covered by overzoomed child + + } + } else { + // Check if all 4 immediate children are loaded (in other words, the missing ideal tile is covered) + const children = tileID.children(this._source.maxzoom); + + if ( + retain[children[0].key] && retain[children[1].key] && + retain[children[2].key] && + retain[children[3].key] + ) + continue; // tile is covered by children + + } + + // We couldn't find child tiles that entirely cover the ideal tile; look for parents now. + + // As we ascend up the tile pyramid of the ideal tile, we check whether the parent + // tile has been previously requested (and errored because we only loop over tiles with no data) + // in order to determine if we need to request its parent. + let parentWasRequested = tile.wasRequested(); + + for ( + let overscaledZ = tileID.overscaledZ - 1; + overscaledZ >= minCoveringZoom; + --overscaledZ + ) { + const parentId = tileID.scaledTo(overscaledZ); + + // Break parent tile ascent if this route has been previously checked by another child. + if (checked[parentId.key]) break; + checked[parentId.key] = true; + + tile = this.getTile(parentId); + if (!tile && parentWasRequested) { + tile = this._addTile(parentId); + } + if (tile) { + retain[parentId.key] = parentId; + // Save the current values, since they're the parent of the next iteration + // of the parent tile ascent loop. + parentWasRequested = tile.wasRequested(); + if (tile.hasData()) break; + } + } + } + + return retain; + } + + _updateLoadedParentTileCache() { + this._loadedParentTiles = {}; + + for (const tileKey in this._tiles) { + const path = []; + let parentTile: ?Tile; + let currentId = this._tiles[tileKey].tileID; + + // Find the closest loaded ancestor by traversing the tile tree towards the root and + // caching results along the way + while (currentId.overscaledZ > 0) { + // Do we have a cached result from previous traversals? + if (currentId.key in this._loadedParentTiles) { + parentTile = this._loadedParentTiles[currentId.key]; + break; + } + + path.push(currentId.key); + + // Is the parent loaded? + const parentId = currentId.scaledTo(currentId.overscaledZ - 1); + parentTile = this._getLoadedTile(parentId); + if (parentTile) { + break; + } + + currentId = parentId; + } + + // Cache the result of this traversal to all newly visited tiles + for (const key of path) { + this._loadedParentTiles[key] = parentTile; + } + } + } + + /** * Add a tile, given its coordinate, to the pyramid. * @private */ - _addTile(tileID: OverscaledTileID): Tile { - let tile = this._tiles[tileID.key]; - if (tile) return tile; - - tile = this._cache.getAndRemove(tileID); - if (tile) { - this._setTileReloadTimer(tileID.key, tile); - // set the tileID because the cached tile could have had a different wrap value - tile.tileID = tileID; - this._state.initializeTileState(tile, this.map ? this.map.painter : null); - if (this._cacheTimers[tileID.key]) { - clearTimeout(this._cacheTimers[tileID.key]); - delete this._cacheTimers[tileID.key]; - this._setTileReloadTimer(tileID.key, tile); - } - } - - const cached = Boolean(tile); - if (!cached) { - const painter = this.map ? this.map.painter : null; - tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, painter, this._isRaster); - this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state)); - } - - // Impossible, but silence flow. - if (!tile) return (null: any); - - tile.uses++; - this._tiles[tileID.key] = tile; - if (!cached) this._source.fire(new Event('dataloading', {tile, coord: tile.tileID, dataType: 'source'})); - - return tile; - } - - _setTileReloadTimer(id: number, tile: Tile) { - if (id in this._timers) { - clearTimeout(this._timers[id]); + _addTile(tileID: OverscaledTileID): Tile { + let tile = this._tiles[tileID.key]; + if (tile) return tile; + + tile = this._cache.getAndRemove(tileID); + if (tile) { + this._setTileReloadTimer(tileID.key, tile); + // set the tileID because the cached tile could have had a different wrap value + tile.tileID = tileID; + this._state.initializeTileState(tile, this.map ? this.map.painter : null); + if (this._cacheTimers[tileID.key]) { + clearTimeout(this._cacheTimers[tileID.key]); + delete this._cacheTimers[tileID.key]; + this._setTileReloadTimer(tileID.key, tile); + } + } + + const cached = Boolean(tile); + if (!cached) { + const painter = this.map ? this.map.painter : null; + tile = new Tile( + tileID, + this._source.tileSize * tileID.overscaleFactor(), + this.transform.tileZoom, + painter, + this._isRaster, + ); + this._loadTile( + tile, + this._tileLoaded.bind(this, tile, tileID.key, tile.state), + ); + } + + // Impossible, but silence flow. + if (!tile) return (null: any); + + tile.uses++; + this._tiles[tileID.key] = tile; + if (!cached) + this._source.fire( + new Event( + 'dataloading', + {tile, coord: tile.tileID, dataType: 'source'}, + ), + ); + + return tile; + } + + _setTileReloadTimer(id: number, tile: Tile) { + if (id in this._timers) { + clearTimeout(this._timers[id]); + delete this._timers[id]; + } + + const expiryTimeout = tile.getExpiryTimeout(); + if (expiryTimeout) { + this._timers[id] = setTimeout( + () => { + this._reloadTile(id, 'expired'); delete this._timers[id]; - } - - const expiryTimeout = tile.getExpiryTimeout(); - if (expiryTimeout) { - this._timers[id] = setTimeout(() => { - this._reloadTile(id, 'expired'); - delete this._timers[id]; - }, expiryTimeout); - } - } - - /** + }, + expiryTimeout, + ); + } + } + + /** * Remove a tile, given its id, from the pyramid * @private */ - _removeTile(id: number) { - const tile = this._tiles[id]; - if (!tile) - return; - - tile.uses--; - delete this._tiles[id]; - if (this._timers[id]) { - clearTimeout(this._timers[id]); - delete this._timers[id]; - } - - if (tile.uses > 0) - return; - - if (tile.hasData() && tile.state !== 'reloading') { - this._cache.add(tile.tileID, tile, tile.getExpiryTimeout()); - } else { - tile.aborted = true; - this._abortTile(tile); - this._unloadTile(tile); - } - } - - /** + _removeTile(id: number) { + const tile = this._tiles[id]; + if (!tile) return; + + tile.uses--; + delete this._tiles[id]; + if (this._timers[id]) { + clearTimeout(this._timers[id]); + delete this._timers[id]; + } + + if (tile.uses > 0) return; + + if (tile.hasData() && tile.state !== 'reloading') { + this._cache.add(tile.tileID, tile, tile.getExpiryTimeout()); + } else { + tile.aborted = true; + this._abortTile(tile); + this._unloadTile(tile); + } + } + + /** * Remove all tiles from this pyramid. * @private */ - clearTiles() { - this._shouldReloadOnResume = false; - this._paused = false; + clearTiles() { + this._shouldReloadOnResume = false; + this._paused = false; - for (const id in this._tiles) - this._removeTile(+id); + for (const id in this._tiles) + this._removeTile(+id); - if (this._source._clear) this._source._clear(); + if (this._source._clear) this._source._clear(); - this._cache.reset(); + this._cache.reset(); - if (this.map && this.usedForTerrain && this.map.painter.terrain) { - this.map.painter.terrain.resetTileLookupCache(this.id); - } - } + if (this.map && this.usedForTerrain && this.map.painter.terrain) { + this.map.painter.terrain.resetTileLookupCache(this.id); + } + } - /** + /** * Search through our current tiles and attempt to find the tiles that cover the given `queryGeometry`. * * @param {QueryGeometry} queryGeometry @@ -848,185 +960,232 @@ class SourceCache extends Evented { * @returns * @private */ - tilesIn(queryGeometry: QueryGeometry, use3DQuery: boolean, visualizeQueryGeometry: boolean): TilespaceQueryGeometry[] { - const tileResults = []; - - const transform = this.transform; - if (!transform) return tileResults; - - const isGlobe = transform.projection.name === 'globe'; - const centerX = mercatorXfromLng(transform.center.lng); - - for (const tileID in this._tiles) { - const tile = this._tiles[tileID]; - if (visualizeQueryGeometry) { - tile.clearQueryDebugViz(); - } - if (tile.holdingForFade()) { - // Tiles held for fading are covered by tiles that are closer to ideal - continue; - } - - // An array of wrap values for the tile [-1, 0, 1]. The default value is 0 but -1 or 1 wrapping - // might be required in globe view due to globe's surface being continuous. - let tilesToCheck; - - if (isGlobe) { - // Compare distances to copies of the tile to see if a wrapped one should be used. - const id = tile.tileID.canonical; - assert(tile.tileID.wrap === 0); - - if (id.z === 0) { - // Render the zoom level 0 tile twice as the query polygon might span over the antimeridian - const distances = [ - Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), - Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX) - ]; - - tilesToCheck = [0, distances.indexOf(Math.min(...distances)) * 2 - 1]; - } else { - const distances = [ - Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), - Math.abs(clamp(centerX, ...tileBoundsX(id, 0)) - centerX), - Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX) - ]; - - tilesToCheck = [distances.indexOf(Math.min(...distances)) - 1]; - } - } else { - tilesToCheck = [0]; - } - - for (const wrap of tilesToCheck) { - const tileResult = queryGeometry.containsTile(tile, transform, use3DQuery, wrap); - if (tileResult) { - tileResults.push(tileResult); - } - } - } - return tileResults; - } - - getVisibleCoordinates(symbolLayer?: boolean): Array { - const coords = this.getRenderableIds(symbolLayer).map((id) => this._tiles[id].tileID); - for (const coord of coords) { - coord.projMatrix = this.transform.calculateProjMatrix(coord.toUnwrapped()); - } - return coords; - } - - hasTransition(): boolean { - if (this._source.hasTransition()) { - return true; - } - - if (isRasterType(this._source.type)) { - for (const id in this._tiles) { - const tile = this._tiles[id]; - if (tile.fadeEndTime !== undefined && tile.fadeEndTime >= browser.now()) { - return true; - } - } - } - - return false; - } - - /** + tilesIn( + queryGeometry: QueryGeometry, + use3DQuery: boolean, + visualizeQueryGeometry: boolean, + ): Array { + const tileResults = []; + + const transform = this.transform; + if (!transform) return tileResults; + + const isGlobe = transform.projection.name === 'globe'; + const centerX = mercatorXfromLng(transform.center.lng); + + for (const tileID in this._tiles) { + const tile = this._tiles[tileID]; + if (visualizeQueryGeometry) { + tile.clearQueryDebugViz(); + } + if (tile.holdingForFade()) { + // Tiles held for fading are covered by tiles that are closer to ideal + continue; + } + + // An array of wrap values for the tile [-1, 0, 1]. The default value is 0 but -1 or 1 wrapping + // might be required in globe view due to globe's surface being continuous. + let tilesToCheck; + + if (isGlobe) { + // Compare distances to copies of the tile to see if a wrapped one should be used. + const id = tile.tileID.canonical; + assert(tile.tileID.wrap === 0); + + if (id.z === 0) { + // Render the zoom level 0 tile twice as the query polygon might span over the antimeridian + const distances = [ + Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), + Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX), + ]; + + tilesToCheck = [0, distances.indexOf(Math.min(...distances)) * 2 - 1]; + } else { + const distances = [ + Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), + Math.abs(clamp(centerX, ...tileBoundsX(id, 0)) - centerX), + Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX), + ]; + + tilesToCheck = [distances.indexOf(Math.min(...distances)) - 1]; + } + } else { + tilesToCheck = [0]; + } + + for (const wrap of tilesToCheck) { + const tileResult = queryGeometry.containsTile( + tile, + transform, + use3DQuery, + wrap, + ); + if (tileResult) { + tileResults.push(tileResult); + } + } + } + return tileResults; + } + + getVisibleCoordinates(symbolLayer?: boolean): Array { + const coords = this.getRenderableIds(symbolLayer).map( + id => this._tiles[id].tileID, + ); + for (const coord of coords) { + coord.projMatrix = this.transform.calculateProjMatrix(coord.toUnwrapped()); + } + return coords; + } + + hasTransition(): boolean { + if (this._source.hasTransition()) { + return true; + } + + if (isRasterType(this._source.type)) { + for (const id in this._tiles) { + const tile = this._tiles[id]; + if (tile.fadeEndTime !== undefined && tile.fadeEndTime >= browser.now()) { + return true; + } + } + } + + return false; + } + + /** * Set the value of a particular state for a feature * @private */ - setFeatureState(sourceLayer?: string, featureId: number | string, state: Object) { - sourceLayer = sourceLayer || '_geojsonTileLayer'; - this._state.updateState(sourceLayer, featureId, state); - } - - /** + setFeatureState( + sourceLayer?: string, + featureId: number | string, + state: Object, + ) { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + this._state.updateState(sourceLayer, featureId, state); + } + + /** * Resets the value of a particular state key for a feature * @private */ - removeFeatureState(sourceLayer?: string, featureId?: number | string, key?: string) { - sourceLayer = sourceLayer || '_geojsonTileLayer'; - this._state.removeFeatureState(sourceLayer, featureId, key); - } - - /** + removeFeatureState( + sourceLayer?: string, + featureId?: number | string, + key?: string, + ) { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + this._state.removeFeatureState(sourceLayer, featureId, key); + } + + /** * Get the entire state object for a feature * @private */ - getFeatureState(sourceLayer?: string, featureId: number | string): FeatureStates { - sourceLayer = sourceLayer || '_geojsonTileLayer'; - return this._state.getState(sourceLayer, featureId); - } - - /** + getFeatureState( + sourceLayer?: string, + featureId: number | string, + ): FeatureStates { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + return this._state.getState(sourceLayer, featureId); + } + + /** * Sets the set of keys that the tile depends on. This allows tiles to * be reloaded when their dependencies change. * @private */ - setDependencies(tileKey: number, namespace: string, dependencies: Array) { - const tile = this._tiles[tileKey]; - if (tile) { - tile.setDependencies(namespace, dependencies); - } - } - - /** + setDependencies( + tileKey: number, + namespace: string, + dependencies: Array, + ) { + const tile = this._tiles[tileKey]; + if (tile) { + tile.setDependencies(namespace, dependencies); + } + } + + /** * Reloads all tiles that depend on the given keys. * @private */ - reloadTilesForDependencies(namespaces: Array, keys: Array) { - for (const id in this._tiles) { - const tile = this._tiles[id]; - if (tile.hasDependency(namespaces, keys)) { - this._reloadTile(+id, 'reloading'); - } - } - this._cache.filter(tile => !tile.hasDependency(namespaces, keys)); - } - - /** + reloadTilesForDependencies(namespaces: Array, keys: Array) { + for (const id in this._tiles) { + const tile = this._tiles[id]; + if (tile.hasDependency(namespaces, keys)) { + this._reloadTile(+id, 'reloading'); + } + } + this._cache.filter(tile => !tile.hasDependency(namespaces, keys)); + } + + /** * Preloads all tiles that will be requested for one or a series of transformations * * @private * @returns {Object} Returns `this` | Promise. */ - _preloadTiles(transform: Transform | Array, callback: Callback) { - const coveringTilesIDs: Map = new Map(); - const transforms = Array.isArray(transform) ? transform : [transform]; - - const terrain = this.map.painter.terrain; - const tileSize = this.usedForTerrain && terrain ? terrain.getScaledDemTileSize() : this._source.tileSize; - - for (const tr of transforms) { - const tileIDs = tr.coveringTiles({ - tileSize, - minzoom: this._source.minzoom, - maxzoom: this._source.maxzoom, - roundZoom: this._source.roundZoom && !this.usedForTerrain, - reparseOverscaled: this._source.reparseOverscaled, - isTerrainDEM: this.usedForTerrain - }); - - for (const tileID of tileIDs) { - coveringTilesIDs.set(tileID.key, tileID); - } - - if (this.usedForTerrain) { - tr.updateElevation(false); - } - } - - const tileIDs = Array.from(coveringTilesIDs.values()); - - asyncAll(tileIDs, (tileID, done) => { - const tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, this.map.painter, this._isRaster); - this._loadTile(tile, (err) => { - if (this._source.type === 'raster-dem' && tile.dem) this._backfillDEM(tile); - done(err, tile); - }); - }, callback); - } + _preloadTiles( + transform: Transform | Array, + callback: Callback, + ) { + const coveringTilesIDs: Map = new Map(); + const transforms = Array.isArray(transform) ? transform : [transform]; + + const terrain = this.map.painter.terrain; + const tileSize = this.usedForTerrain && terrain ? + terrain.getScaledDemTileSize() : + this._source.tileSize; + + for (const tr of transforms) { + const tileIDs = tr.coveringTiles( + { + tileSize, + minzoom: this._source.minzoom, + maxzoom: this._source.maxzoom, + roundZoom: this._source.roundZoom && !this.usedForTerrain, + reparseOverscaled: this._source.reparseOverscaled, + isTerrainDEM: this.usedForTerrain, + }, + ); + + for (const tileID of tileIDs) { + coveringTilesIDs.set(tileID.key, tileID); + } + + if (this.usedForTerrain) { + tr.updateElevation(false); + } + } + + const tileIDs = Array.from(coveringTilesIDs.values()); + + asyncAll( + tileIDs, + (tileID, done) => { + const tile = new Tile( + tileID, + this._source.tileSize * tileID.overscaleFactor(), + this.transform.tileZoom, + this.map.painter, + this._isRaster, + ); + this._loadTile( + tile, + err => { + if (this._source.type === 'raster-dem' && tile.dem) + this._backfillDEM(tile); + done(err, tile); + }, + ); + }, + callback, + ); + } } SourceCache.maxOverzooming = 10; diff --git a/src/source/worker.js b/src/source/worker.js index 32f1e9ae48c..91d4ddb2951 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -33,258 +33,334 @@ import type Projection from '../geo/projection/projection.js'; * @private */ export default class Worker { - self: WorkerGlobalScopeInterface; - actor: Actor; - layerIndexes: {[_: string]: StyleLayerIndex }; - availableImages: {[_: string]: Array }; - workerSourceTypes: {[_: string]: Class }; - workerSources: {[_: string]: {[_: string]: {[_: string]: WorkerSource } } }; - demWorkerSources: {[_: string]: {[_: string]: RasterDEMTileWorkerSource } }; - projections: {[_: string]: Projection }; - defaultProjection: Projection; - isSpriteLoaded: {[_: string]: boolean }; - referrer: ?string; - terrain: ?boolean; - - constructor(self: WorkerGlobalScopeInterface) { - PerformanceUtils.measure('workerEvaluateScript'); - this.self = self; - this.actor = new Actor(self, this); - - this.layerIndexes = {}; - this.availableImages = {}; - this.isSpriteLoaded = {}; - - this.projections = {}; - this.defaultProjection = getProjection({name: 'mercator'}); - - this.workerSourceTypes = { - vector: VectorTileWorkerSource, - geojson: GeoJSONWorkerSource - }; - - // [mapId][sourceType][sourceName] => worker source instance - this.workerSources = {}; - this.demWorkerSources = {}; - - this.self.registerWorkerSource = (name: string, WorkerSource: Class) => { - if (this.workerSourceTypes[name]) { - throw new Error(`Worker source with name "${name}" already registered.`); - } - this.workerSourceTypes[name] = WorkerSource; - }; - - // This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed. - this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => { - if (globalRTLTextPlugin.isParsed()) { - throw new Error('RTL text plugin already registered.'); - } - globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping; - globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText; - globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText; - }; - } - - clearCaches(mapId: string, unused: mixed, callback: WorkerTileCallback) { - delete this.layerIndexes[mapId]; - delete this.availableImages[mapId]; - delete this.workerSources[mapId]; - delete this.demWorkerSources[mapId]; - callback(); - } - - checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) { - // noop, used to check if a worker is fully set up and ready to receive messages - callback(); - } - - setReferrer(mapID: string, referrer: string) { - this.referrer = referrer; - } - - spriteLoaded(mapId: string, bool: boolean) { - this.isSpriteLoaded[mapId] = bool; - for (const workerSource in this.workerSources[mapId]) { - const ws = this.workerSources[mapId][workerSource]; - for (const source in ws) { - if (ws[source] instanceof VectorTileWorkerSource) { - ws[source].isSpriteLoaded = bool; - ws[source].fire(new Event('isSpriteLoaded')); - } - } - } - } - - setImages(mapId: string, images: Array, callback: WorkerTileCallback) { - this.availableImages[mapId] = images; - for (const workerSource in this.workerSources[mapId]) { - const ws = this.workerSources[mapId][workerSource]; - for (const source in ws) { - ws[source].availableImages = images; - } - } - callback(); - } - - enableTerrain(mapId: string, enable: boolean, callback: WorkerTileCallback) { - this.terrain = enable; - callback(); - } - - setProjection(mapId: string, config: ProjectionSpecification) { - this.projections[mapId] = getProjection(config); - } - - setLayers(mapId: string, layers: Array, callback: WorkerTileCallback) { - this.getLayerIndex(mapId).replace(layers); - callback(); - } - - updateLayers(mapId: string, params: {layers: Array, removedIds: Array}, callback: WorkerTileCallback) { - this.getLayerIndex(mapId).update(params.layers, params.removedIds); - callback(); - } - - loadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { - assert(params.type); - const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; - p.projection = this.projections[mapId] || this.defaultProjection; - this.getWorkerSource(mapId, params.type, params.source).loadTile(p, callback); - } - - loadDEMTile(mapId: string, params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { - const p = this.enableTerrain ? extend({buildQuadTree: this.terrain}, params) : params; - this.getDEMWorkerSource(mapId, params.source).loadTile(p, callback); - } - - reloadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { - assert(params.type); - const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; - p.projection = this.projections[mapId] || this.defaultProjection; - this.getWorkerSource(mapId, params.type, params.source).reloadTile(p, callback); - } - - abortTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) { - assert(params.type); - this.getWorkerSource(mapId, params.type, params.source).abortTile(params, callback); - } - - removeTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) { - assert(params.type); - this.getWorkerSource(mapId, params.type, params.source).removeTile(params, callback); - } - - removeSource(mapId: string, params: {source: string} & {type: string}, callback: WorkerTileCallback) { - assert(params.type); - assert(params.source); - - if (!this.workerSources[mapId] || - !this.workerSources[mapId][params.type] || - !this.workerSources[mapId][params.type][params.source]) { - return; - } - - const worker = this.workerSources[mapId][params.type][params.source]; - delete this.workerSources[mapId][params.type][params.source]; - - if (worker.removeSource !== undefined) { - worker.removeSource(params, callback); - } else { - callback(); - } - } - - /** + self: WorkerGlobalScopeInterface; + actor: Actor; + layerIndexes: { [_: string]: StyleLayerIndex }; + availableImages: { [_: string]: Array }; + workerSourceTypes: { [_: string]: Class }; + workerSources: { [_: string]: { [_: string]: { [_: string]: WorkerSource } } }; + demWorkerSources: { [_: string]: { [_: string]: RasterDEMTileWorkerSource } }; + projections: { [_: string]: Projection }; + defaultProjection: Projection; + isSpriteLoaded: { [_: string]: boolean }; + referrer: ?string; + terrain: ?boolean; + + constructor(self: WorkerGlobalScopeInterface) { + PerformanceUtils.measure('workerEvaluateScript'); + this.self = self; + this.actor = new Actor(self, this); + + this.layerIndexes = {}; + this.availableImages = {}; + this.isSpriteLoaded = {}; + + this.projections = {}; + this.defaultProjection = getProjection({name: 'mercator'}); + + this.workerSourceTypes = { + vector: VectorTileWorkerSource, + geojson: GeoJSONWorkerSource, + }; + + // [mapId][sourceType][sourceName] => worker source instance + this.workerSources = {}; + this.demWorkerSources = {}; + + this.self.registerWorkerSource = (( + name: string, + WorkerSource: Class, + ) => { + if (this.workerSourceTypes[name]) { + throw new Error(`Worker source with name "${name}" already registered.`); + } + this.workerSourceTypes[name] = WorkerSource; + }); + + // This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed. + this.self.registerRTLTextPlugin = (( + rtlTextPlugin: { + applyArabicShaping: Function, + processBidirectionalText: Function, + processStyledBidirectionalText?: Function, + }, + ) => { + if (globalRTLTextPlugin.isParsed()) { + throw new Error('RTL text plugin already registered.'); + } + globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping; + globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText; + globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText; + }); + } + + clearCaches(mapId: string, unused: mixed, callback: WorkerTileCallback) { + delete this.layerIndexes[mapId]; + delete this.availableImages[mapId]; + delete this.workerSources[mapId]; + delete this.demWorkerSources[mapId]; + callback(); + } + + checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) { + // noop, used to check if a worker is fully set up and ready to receive messages + callback(); + } + + setReferrer(mapID: string, referrer: string) { + this.referrer = referrer; + } + + spriteLoaded(mapId: string, bool: boolean) { + this.isSpriteLoaded[mapId] = bool; + for (const workerSource in this.workerSources[mapId]) { + const ws = this.workerSources[mapId][workerSource]; + for (const source in ws) { + if (ws[source] instanceof VectorTileWorkerSource) { + ws[source].isSpriteLoaded = bool; + ws[source].fire(new Event('isSpriteLoaded')); + } + } + } + } + + setImages(mapId: string, images: Array, callback: WorkerTileCallback) { + this.availableImages[mapId] = images; + for (const workerSource in this.workerSources[mapId]) { + const ws = this.workerSources[mapId][workerSource]; + for (const source in ws) { + ws[source].availableImages = images; + } + } + callback(); + } + + enableTerrain = (mapId: string, enable: boolean, callback: WorkerTileCallback) => { + this.terrain = enable; + callback(); + }; + + setProjection(mapId: string, config: ProjectionSpecification) { + this.projections[mapId] = getProjection(config); + } + + setLayers( + mapId: string, + layers: Array, + callback: WorkerTileCallback, + ) { + this.getLayerIndex(mapId).replace(layers); + callback(); + } + + updateLayers( + mapId: string, + params: { layers: Array, removedIds: Array }, + callback: WorkerTileCallback, + ) { + this.getLayerIndex(mapId).update(params.layers, params.removedIds); + callback(); + } + + loadTile( + mapId: string, + params: WorkerTileParameters & { type: string }, + callback: WorkerTileCallback, + ) { + assert(params.type); + const p = this.enableTerrain ? + extend({enableTerrain: this.terrain}, params) : + params; + p.projection = this.projections[mapId] || this.defaultProjection; + this.getWorkerSource(mapId, params.type, params.source).loadTile( + p, + callback, + ); + } + + loadDEMTile( + mapId: string, + params: WorkerDEMTileParameters, + callback: WorkerDEMTileCallback, + ) { + const p = this.enableTerrain ? + extend({buildQuadTree: this.terrain}, params) : + params; + this.getDEMWorkerSource(mapId, params.source).loadTile(p, callback); + } + + reloadTile( + mapId: string, + params: WorkerTileParameters & { type: string }, + callback: WorkerTileCallback, + ) { + assert(params.type); + const p = this.enableTerrain ? + extend({enableTerrain: this.terrain}, params) : + params; + p.projection = this.projections[mapId] || this.defaultProjection; + this.getWorkerSource(mapId, params.type, params.source).reloadTile( + p, + callback, + ); + } + + abortTile( + mapId: string, + params: TileParameters & { type: string }, + callback: WorkerTileCallback, + ) { + assert(params.type); + this.getWorkerSource(mapId, params.type, params.source).abortTile( + params, + callback, + ); + } + + removeTile( + mapId: string, + params: TileParameters & { type: string }, + callback: WorkerTileCallback, + ) { + assert(params.type); + this.getWorkerSource(mapId, params.type, params.source).removeTile( + params, + callback, + ); + } + + removeSource( + mapId: string, + params: { source: string } & { type: string }, + callback: WorkerTileCallback, + ) { + assert(params.type); + assert(params.source); + + if ( + !this.workerSources[mapId] || !this.workerSources[mapId][params.type] || + !this.workerSources[mapId][params.type][params.source] + ) { + return; + } + + const worker = this.workerSources[mapId][params.type][params.source]; + delete this.workerSources[mapId][params.type][params.source]; + + if (worker.removeSource !== undefined) { + worker.removeSource(params, callback); + } else { + callback(); + } + } + + /** * Load a {@link WorkerSource} script at params.url. The script is run * (using importScripts) with `registerWorkerSource` in scope, which is a * function taking `(name, workerSourceObject)`. * @private */ - loadWorkerSource(map: string, params: { url: string }, callback: Callback) { - try { - this.self.importScripts(params.url); - callback(); - } catch (e) { - callback(e.toString()); - } - } - - syncRTLPluginState(map: string, state: PluginState, callback: Callback) { - try { - globalRTLTextPlugin.setState(state); - const pluginURL = globalRTLTextPlugin.getPluginURL(); - if ( - globalRTLTextPlugin.isLoaded() && - !globalRTLTextPlugin.isParsed() && - pluginURL != null // Not possible when `isLoaded` is true, but keeps flow happy - ) { - this.self.importScripts(pluginURL); - const complete = globalRTLTextPlugin.isParsed(); - const error = complete ? undefined : new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`); - callback(error, complete); - } - } catch (e) { - callback(e.toString()); - } - } - - getAvailableImages(mapId: string): Array { - let availableImages = this.availableImages[mapId]; - - if (!availableImages) { - availableImages = []; - } - - return availableImages; - } - - getLayerIndex(mapId: string): StyleLayerIndex { - let layerIndexes = this.layerIndexes[mapId]; - if (!layerIndexes) { - layerIndexes = this.layerIndexes[mapId] = new StyleLayerIndex(); - } - return layerIndexes; - } - - getWorkerSource(mapId: string, type: string, source: string): WorkerSource { - if (!this.workerSources[mapId]) - this.workerSources[mapId] = {}; - if (!this.workerSources[mapId][type]) - this.workerSources[mapId][type] = {}; - - if (!this.workerSources[mapId][type][source]) { - // use a wrapped actor so that we can attach a target mapId param - // to any messages invoked by the WorkerSource - const actor = { - send: (type, data, callback, _, mustQueue, metadata) => { - this.actor.send(type, data, callback, mapId, mustQueue, metadata); - }, - scheduler: this.actor.scheduler - }; - this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)((actor: any), this.getLayerIndex(mapId), this.getAvailableImages(mapId), this.isSpriteLoaded[mapId]); - } - - return this.workerSources[mapId][type][source]; - } - - getDEMWorkerSource(mapId: string, source: string): RasterDEMTileWorkerSource { - if (!this.demWorkerSources[mapId]) - this.demWorkerSources[mapId] = {}; - - if (!this.demWorkerSources[mapId][source]) { - this.demWorkerSources[mapId][source] = new RasterDEMTileWorkerSource(); - } - - return this.demWorkerSources[mapId][source]; - } - - enforceCacheSizeLimit(mapId: string, limit: number) { - enforceCacheSizeLimit(limit); - } - - getWorkerPerformanceMetrics(mapId: string, params: any, callback: (error: ?Error, result: ?Object) => void) { - callback(undefined, PerformanceUtils.getWorkerPerformanceMetrics()); - } + loadWorkerSource( + map: string, + params: { url: string }, + callback: Callback, + ) { + try { + this.self.importScripts(params.url); + callback(); + } catch (e) { + callback(e.toString()); + } + } + + syncRTLPluginState( + map: string, + state: PluginState, + callback: Callback, + ) { + try { + globalRTLTextPlugin.setState(state); + const pluginURL = globalRTLTextPlugin.getPluginURL(); + if ( + globalRTLTextPlugin.isLoaded() && !globalRTLTextPlugin.isParsed() && + pluginURL != null // Not possible when `isLoaded` is true, but keeps flow happy + + ) { + this.self.importScripts(pluginURL); + const complete = globalRTLTextPlugin.isParsed(); + const error = complete ? + undefined : + new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`,); + callback(error, complete); + } + } catch (e) { + callback(e.toString()); + } + } + + getAvailableImages(mapId: string): Array { + let availableImages = this.availableImages[mapId]; + + if (!availableImages) { + availableImages = []; + } + + return availableImages; + } + + getLayerIndex(mapId: string): StyleLayerIndex { + let layerIndexes = this.layerIndexes[mapId]; + if (!layerIndexes) { + layerIndexes = this.layerIndexes[mapId] = new StyleLayerIndex(); + } + return layerIndexes; + } + + getWorkerSource(mapId: string, type: string, source: string): WorkerSource { + if (!this.workerSources[mapId]) this.workerSources[mapId] = {}; + if (!this.workerSources[mapId][type]) this.workerSources[mapId][type] = {}; + + if (!this.workerSources[mapId][type][source]) { + // use a wrapped actor so that we can attach a target mapId param + // to any messages invoked by the WorkerSource + const actor = { + send: (type, data, callback, _, mustQueue, metadata) => { + this.actor.send(type, data, callback, mapId, mustQueue, metadata); + }, + scheduler: this.actor.scheduler, + }; + this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)( + (actor: any), + this.getLayerIndex(mapId), + this.getAvailableImages(mapId), + this.isSpriteLoaded[mapId], + ); + } + + return this.workerSources[mapId][type][source]; + } + + getDEMWorkerSource(mapId: string, source: string): RasterDEMTileWorkerSource { + if (!this.demWorkerSources[mapId]) this.demWorkerSources[mapId] = {}; + + if (!this.demWorkerSources[mapId][source]) { + this.demWorkerSources[mapId][source] = new RasterDEMTileWorkerSource(); + } + + return this.demWorkerSources[mapId][source]; + } + + enforceCacheSizeLimit(mapId: string, limit: number) { + enforceCacheSizeLimit(limit); + } + + getWorkerPerformanceMetrics( + mapId: string, + params: any, + callback: (error: ?Error, result: ?Object) => void, + ) { + callback(undefined, PerformanceUtils.getWorkerPerformanceMetrics()); + } } /* global self, WorkerGlobalScope */ diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 853ff20fe1a..6af5c44df26 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -192,23 +192,42 @@ export class ZoomDependentExpression { } } -export type ConstantExpression = { - kind: 'constant', - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, +export type ConstantExpression = interface { + kind: 'constant', + +evaluate: ( + globals: GlobalProperties, + feature?: Feature, + featureState?: FeatureState, + canonical?: CanonicalTileID, + availableImages?: Array + ) => any, } -export type SourceExpression = { - kind: 'source', - isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, +export type SourceExpression = interface { + kind: 'source', + isStateDependent: boolean, + +evaluate: ( + globals: GlobalProperties, + feature?: Feature, + featureState?: FeatureState, + canonical?: CanonicalTileID, + availableImages?: Array, + formattedSection?: FormattedSection + ) => any, }; -export type CameraExpression = { - kind: 'camera', - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, - +interpolationFactor: (input: number, lower: number, upper: number) => number, - zoomStops: Array, - interpolationType: ?InterpolationType +export type CameraExpression = interface { + kind: 'camera', + +evaluate: ( + globals: GlobalProperties, + feature?: Feature, + featureState?: FeatureState, + canonical?: CanonicalTileID, + availableImages?: Array + ) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, + zoomStops: Array, + interpolationType: ?InterpolationType, }; export interface CompositeExpression { diff --git a/src/style-spec/validate_style.min.js b/src/style-spec/validate_style.min.js index 25e56306513..28cc1eef01f 100644 --- a/src/style-spec/validate_style.min.js +++ b/src/style-spec/validate_style.min.js @@ -14,10 +14,10 @@ import _validateLayoutProperty from './validate/validate_layout_property.js'; import type {StyleSpecification} from './types.js'; -export type ValidationError = { - message: string; - identifier?: ?string; - line?: ?number; +export type ValidationError = interface { + message: string, + identifier?: ?string, + line?: ?number, }; export type ValidationErrors = $ReadOnlyArray; export type Validator = (Object) => ValidationErrors; diff --git a/src/style/properties.js b/src/style/properties.js index d7f25adc651..284fee825b2 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -106,9 +106,9 @@ export class PropertyValue { // ------- Transitionable ------- -export type TransitionParameters = { - now: TimePoint, - transition: TransitionSpecification +export type TransitionParameters = interface { + now: TimePoint, + transition: TransitionSpecification, }; /** diff --git a/src/style/style.js b/src/style/style.js index a7f81bd1e41..846b7546ff2 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -130,358 +130,401 @@ const drapedLayers = {'fill': true, 'line': true, 'background': true, "hillshade /** * @private */ -class Style extends Evented { - map: Map; - stylesheet: StyleSpecification; - dispatcher: Dispatcher; - imageManager: ImageManager; - glyphManager: GlyphManager; - light: Light; - terrain: ?Terrain; - fog: ?Fog; - - _request: ?Cancelable; - _spriteRequest: ?Cancelable; - _layers: {[_: string]: StyleLayer}; - _num3DLayers: number; - _numSymbolLayers: number; - _numCircleLayers: number; - _serializedLayers: {[_: string]: Object}; - _order: Array; - _drapedFirstOrder: Array; - _sourceCaches: {[_: string]: SourceCache}; - _otherSourceCaches: {[_: string]: SourceCache}; - _symbolSourceCaches: {[_: string]: SourceCache}; - _loaded: boolean; - _rtlTextPluginCallback: Function; - _changed: boolean; - _updatedSources: {[_: string]: 'clear' | 'reload'}; - _updatedLayers: {[_: string]: true}; - _removedLayers: {[_: string]: StyleLayer}; - _changedImages: {[_: string]: true}; - _updatedPaintProps: {[layer: string]: true}; - _layerOrderChanged: boolean; - _availableImages: Array; - _markersNeedUpdate: boolean; - - crossTileSymbolIndex: CrossTileSymbolIndex; - pauseablePlacement: PauseablePlacement; - placement: Placement; - z: number; - - // exposed to allow stubbing by unit tests - static getSourceType: typeof getSourceType; - static setSourceType: typeof setSourceType; - static registerForPluginStateChange: typeof registerForPluginStateChange; - - constructor(map: Map, options: StyleOptions = {}) { - super(); - - this.map = map; - this.dispatcher = new Dispatcher(getWorkerPool(), this); - this.imageManager = new ImageManager(); - this.imageManager.setEventedParent(this); - this.glyphManager = new GlyphManager(map._requestManager, - options.localFontFamily ? - LocalGlyphMode.all : - (options.localIdeographFontFamily ? LocalGlyphMode.ideographs : LocalGlyphMode.none), - options.localFontFamily || options.localIdeographFontFamily); - this.crossTileSymbolIndex = new CrossTileSymbolIndex(); - - this._layers = {}; - this._num3DLayers = 0; - this._numSymbolLayers = 0; - this._numCircleLayers = 0; - this._serializedLayers = {}; - this._sourceCaches = {}; - this._otherSourceCaches = {}; - this._symbolSourceCaches = {}; - this._loaded = false; - this._availableImages = []; - this._order = []; - this._drapedFirstOrder = []; - this._markersNeedUpdate = false; - - this._resetUpdates(); - - this.dispatcher.broadcast('setReferrer', getReferrer()); - - const self = this; - this._rtlTextPluginCallback = Style.registerForPluginStateChange((event) => { - const state = { - pluginStatus: event.pluginStatus, - pluginURL: event.pluginURL - }; - self.dispatcher.broadcast('syncRTLPluginState', state, (err, results) => { - triggerPluginCompletionEvent(err); - if (results) { - const allComplete = results.every((elem) => elem); - if (allComplete) { - for (const id in self._sourceCaches) { - const sourceCache = self._sourceCaches[id]; - const sourceCacheType = sourceCache.getSource().type; - if (sourceCacheType === 'vector' || sourceCacheType === 'geojson') { - sourceCache.reload(); // Should be a no-op if the plugin loads before any tiles load - } - } - } - } - - }); - }); - - this.on('data', (event) => { - if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') { - return; - } - - const source = this.getSource(event.sourceId); - if (!source || !source.vectorLayerIds) { - return; - } - - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.source === source.id) { - this._validateLayer(layer); - } - } - }); - } - - loadURL(url: string, options: { - validate?: boolean, - accessToken?: string - } = {}) { - this.fire(new Event('dataloading', {dataType: 'style'})); - - const validate = typeof options.validate === 'boolean' ? - options.validate : !isMapboxURL(url); - - url = this.map._requestManager.normalizeStyleURL(url, options.accessToken); - const request = this.map._requestManager.transformRequest(url, ResourceType.Style); - this._request = getJSON(request, (error: ?Error, json: ?Object) => { - this._request = null; - if (error) { - this.fire(new ErrorEvent(error)); - } else if (json) { - this._load(json, validate); - } - }); - } - - loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}) { - this.fire(new Event('dataloading', {dataType: 'style'})); - - this._request = browser.frame(() => { - this._request = null; - this._load(json, options.validate !== false); - }); - } - - loadEmpty() { - this.fire(new Event('dataloading', {dataType: 'style'})); - this._load(empty, false); - } - - _updateLayerCount(layer: StyleLayer, add: boolean) { - // Typed layer bookkeeping - const count = add ? 1 : -1; - if (layer.is3D()) { - this._num3DLayers += count; - } - if (layer.type === 'circle') { - this._numCircleLayers += count; - } - if (layer.type === 'symbol') { - this._numSymbolLayers += count; - } - } - - _load(json: StyleSpecification, validate: boolean) { - if (validate && emitValidationErrors(this, validateStyle(json))) { - return; - } - - this._loaded = true; - this.stylesheet = clone(json); - this._updateMapProjection(); - - for (const id in json.sources) { - this.addSource(id, json.sources[id], {validate: false}); - } - this._changed = false; // avoid triggering redundant style update after adding initial sources - if (json.sprite) { - this._loadSprite(json.sprite); - } else { - this.imageManager.setLoaded(true); - this.dispatcher.broadcast('spriteLoaded', true); - } - - this.glyphManager.setURL(json.glyphs); - - const layers = deref(this.stylesheet.layers); - - this._order = layers.map((layer) => layer.id); - - this._layers = {}; - this._serializedLayers = {}; - for (let layer of layers) { - layer = createStyleLayer(layer); - layer.setEventedParent(this, {layer: {id: layer.id}}); - this._layers[layer.id] = layer; - this._serializedLayers[layer.id] = layer.serialize(); - this._updateLayerCount(layer, true); - } - - this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); - - this.light = new Light(this.stylesheet.light); - if (this.stylesheet.terrain && !this.terrainSetForDrapingOnly()) { - this._createTerrain(this.stylesheet.terrain, DrapeRenderMode.elevated); - } - if (this.stylesheet.fog) { - this._createFog(this.stylesheet.fog); - } - this._updateDrapeFirstLayers(); - - this.fire(new Event('data', {dataType: 'style'})); - this.fire(new Event('style.load')); - } - - terrainSetForDrapingOnly(): boolean { - return !!this.terrain && this.terrain.drapeRenderMode === DrapeRenderMode.deferred; - } - - setProjection(projection?: ?ProjectionSpecification) { - if (projection) { - this.stylesheet.projection = projection; - } else { - delete this.stylesheet.projection; - } - this._updateMapProjection(); - } - - applyProjectionUpdate() { - if (!this._loaded) return; - this.dispatcher.broadcast('setProjection', this.map.transform.projectionOptions); - - if (this.map.transform.projection.requiresDraping) { - const hasTerrain = this.getTerrain() || this.stylesheet.terrain; - if (!hasTerrain) { - this.setTerrainForDraping(); - } - } else if (this.terrainSetForDrapingOnly()) { - this.setTerrain(null); - } - } - - _updateMapProjection() { - if (!this.map._useExplicitProjection) { // Update the visible projection if map's is null - this.map._prioritizeAndUpdateProjection(null, this.stylesheet.projection); - } else { // Ensure that style is consistent with current projection on style load - this.applyProjectionUpdate(); - } - } - - _loadSprite(url: string) { - this._spriteRequest = loadSprite(url, this.map._requestManager, (err, images) => { - this._spriteRequest = null; - if (err) { - this.fire(new ErrorEvent(err)); - } else if (images) { - for (const id in images) { - this.imageManager.addImage(id, images[id]); - } - } - - this.imageManager.setLoaded(true); - this._availableImages = this.imageManager.listImages(); - this.dispatcher.broadcast('setImages', this._availableImages); - this.dispatcher.broadcast('spriteLoaded', true); - this.fire(new Event('data', {dataType: 'style'})); - }); - } - - _validateLayer(layer: StyleLayer) { - const source = this.getSource(layer.source); - if (!source) { - return; - } - - const sourceLayer = layer.sourceLayer; - if (!sourceLayer) { - return; - } - - if (source.type === 'geojson' || (source.vectorLayerIds && source.vectorLayerIds.indexOf(sourceLayer) === -1)) { - this.fire(new ErrorEvent(new Error( - `Source layer "${sourceLayer}" ` + - `does not exist on source "${source.id}" ` + - `as specified by style layer "${layer.id}"` - ))); - } - } - - loaded(): boolean { - if (!this._loaded) - return false; - - if (Object.keys(this._updatedSources).length) - return false; - - for (const id in this._sourceCaches) - if (!this._sourceCaches[id].loaded()) - return false; - - if (!this.imageManager.isLoaded()) - return false; - - return true; - } - - _serializeLayers(ids: Array): Array { - const serializedLayers = []; - for (const id of ids) { - const layer = this._layers[id]; - if (layer.type !== 'custom') { - serializedLayers.push(layer.serialize()); - } - } - return serializedLayers; - } - - hasTransitions(): boolean { - if (this.light && this.light.hasTransition()) { - return true; - } - - if (this.fog && this.fog.hasTransition()) { - return true; - } - - for (const id in this._sourceCaches) { - if (this._sourceCaches[id].hasTransition()) { - return true; - } - } - - for (const id in this._layers) { - if (this._layers[id].hasTransition()) { - return true; - } - } - - return false; - } - - get order(): Array { - if (this.map._optimizeForTerrain && this.terrain) { - assert(this._drapedFirstOrder.length === this._order.length); - return this._drapedFirstOrder; - } - return this._order; - } +class Style + extends Evented { + map: Map; + stylesheet: StyleSpecification; + dispatcher: Dispatcher; + imageManager: ImageManager; + glyphManager: GlyphManager; + light: Light; + terrain: ?Terrain; + fog: ?Fog; + + _request: ?Cancelable; + _spriteRequest: ?Cancelable; + _layers: { [_: string]: StyleLayer }; + _num3DLayers: number; + _numSymbolLayers: number; + _numCircleLayers: number; + _serializedLayers: { [_: string]: Object }; + _order: Array; + _drapedFirstOrder: Array; + _sourceCaches: { [_: string]: SourceCache }; + _otherSourceCaches: { [_: string]: SourceCache }; + _symbolSourceCaches: { [_: string]: SourceCache }; + _loaded: boolean; + _rtlTextPluginCallback: Function; + _changed: boolean; + _updatedSources: { [_: string]: 'clear' | 'reload' }; + _updatedLayers: { [_: string]: true }; + _removedLayers: { [_: string]: StyleLayer }; + _changedImages: { [_: string]: true }; + _updatedPaintProps: { [layer: string]: true }; + _layerOrderChanged: boolean; + _availableImages: Array; + _markersNeedUpdate: boolean; + + crossTileSymbolIndex: CrossTileSymbolIndex; + pauseablePlacement: PauseablePlacement; + placement: Placement; + z: number; + + // exposed to allow stubbing by unit tests + static getSourceType: typeof getSourceType; + static setSourceType: typeof setSourceType; + static registerForPluginStateChange: typeof registerForPluginStateChange; + + constructor(map: Map, options: StyleOptions = {}) { + super(); + + this.map = map; + this.dispatcher = new Dispatcher(getWorkerPool(), this); + this.imageManager = new ImageManager(); + this.imageManager.setEventedParent(this); + this.glyphManager = new GlyphManager( + map._requestManager, + options.localFontFamily ? + LocalGlyphMode.all : + options.localIdeographFontFamily ? + LocalGlyphMode.ideographs : + LocalGlyphMode.none, + options.localFontFamily || options.localIdeographFontFamily, + ); + this.crossTileSymbolIndex = new CrossTileSymbolIndex(); + + this._layers = {}; + this._num3DLayers = 0; + this._numSymbolLayers = 0; + this._numCircleLayers = 0; + this._serializedLayers = {}; + this._sourceCaches = {}; + this._otherSourceCaches = {}; + this._symbolSourceCaches = {}; + this._loaded = false; + this._availableImages = []; + this._order = []; + this._drapedFirstOrder = []; + this._markersNeedUpdate = false; + + this._resetUpdates(); + + this.dispatcher.broadcast('setReferrer', getReferrer()); + + const self = this; + this._rtlTextPluginCallback = Style.registerForPluginStateChange( + event => { + const state = { + pluginStatus: event.pluginStatus, + pluginURL: event.pluginURL, + }; + self.dispatcher.broadcast( + 'syncRTLPluginState', + state, + (err, results) => { + triggerPluginCompletionEvent(err); + if (results) { + const allComplete = results.every(elem => elem); + if (allComplete) { + for (const id in self._sourceCaches) { + const sourceCache = self._sourceCaches[id]; + const sourceCacheType = sourceCache.getSource().type; + if ( + sourceCacheType === 'vector' || + sourceCacheType === 'geojson' + ) { + sourceCache.reload(); // Should be a no-op if the plugin loads before any tiles load + + } + } + } + } + }, + ); + }, + ); + + this.on( + 'data', + event => { + if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') { + return; + } + + const source = this.getSource(event.sourceId); + if (!source || !source.vectorLayerIds) { + return; + } + + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.source === source.id) { + this._validateLayer(layer); + } + } + }, + ); + } + + loadURL( + url: string, + options: { + validate?: boolean, + accessToken?: string, + } = {}, + ) { + this.fire(new Event('dataloading', {dataType: 'style'})); + + const validate = typeof options.validate === 'boolean' ? + options.validate : + !isMapboxURL(url); + + url = this.map._requestManager.normalizeStyleURL(url, options.accessToken); + const request = this.map._requestManager.transformRequest( + url, + ResourceType.Style, + ); + this._request = getJSON( + request, + (error: ?Error, json: ?Object) => { + this._request = null; + if (error) { + this.fire(new ErrorEvent(error)); + } else if (json) { + this._load(json, validate); + } + }, + ); + } + + loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}) { + this.fire(new Event('dataloading', {dataType: 'style'})); + + this._request = browser.frame( + () => { + this._request = null; + this._load(json, options.validate !== false); + }, + ); + } + + loadEmpty() { + this.fire(new Event('dataloading', {dataType: 'style'})); + this._load(empty, false); + } + + _updateLayerCount(layer: StyleLayer, add: boolean) { + // Typed layer bookkeeping + const count = add ? 1 : -1; + if (layer.is3D()) { + this._num3DLayers += count; + } + if (layer.type === 'circle') { + this._numCircleLayers += count; + } + if (layer.type === 'symbol') { + this._numSymbolLayers += count; + } + } + + _load(json: StyleSpecification, validate: boolean) { + if (validate && emitValidationErrors(this, validateStyle(json))) { + return; + } + + this._loaded = true; + this.stylesheet = clone(json); + this._updateMapProjection(); + + for (const id in json.sources) { + this.addSource(id, json.sources[id], {validate: false}); + } + this._changed = false; // avoid triggering redundant style update after adding initial sources + if (json.sprite) { + this._loadSprite(json.sprite); + } else { + this.imageManager.setLoaded(true); + this.dispatcher.broadcast('spriteLoaded', true); + } + + this.glyphManager.setURL(json.glyphs); + + const layers = deref(this.stylesheet.layers); + + this._order = layers.map(layer => layer.id); + + this._layers = {}; + this._serializedLayers = {}; + for (let layer of layers) { + layer = createStyleLayer(layer); + layer.setEventedParent(this, {layer: {id: layer.id}}); + this._layers[layer.id] = layer; + this._serializedLayers[layer.id] = layer.serialize(); + this._updateLayerCount(layer, true); + } + + this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); + + this.light = new Light(this.stylesheet.light); + if (this.stylesheet.terrain && !this.terrainSetForDrapingOnly()) { + this._createTerrain(this.stylesheet.terrain, DrapeRenderMode.elevated); + } + if (this.stylesheet.fog) { + this._createFog(this.stylesheet.fog); + } + this._updateDrapeFirstLayers(); + + this.fire(new Event('data', {dataType: 'style'})); + this.fire(new Event('style.load')); + } + + terrainSetForDrapingOnly(): boolean { + return ( + !!this.terrain && + this.terrain.drapeRenderMode === DrapeRenderMode.deferred + ); + } + + setProjection(projection?: ?ProjectionSpecification) { + if (projection) { + this.stylesheet.projection = projection; + } else { + delete this.stylesheet.projection; + } + this._updateMapProjection(); + } + + applyProjectionUpdate() { + if (!this._loaded) return; + this.dispatcher.broadcast( + 'setProjection', + this.map.transform.projectionOptions, + ); + + if (this.map.transform.projection.requiresDraping) { + const hasTerrain = this.getTerrain() || this.stylesheet.terrain; + if (!hasTerrain) { + this.setTerrainForDraping(); + } + } else if (this.terrainSetForDrapingOnly()) { + this.setTerrain(null); + } + } + + _updateMapProjection() { + if (!this.map._useExplicitProjection) { + // Update the visible projection if map's is null + this.map._prioritizeAndUpdateProjection(null, this.stylesheet.projection); + } else { + // Ensure that style is consistent with current projection on style load + this.applyProjectionUpdate(); + } + } + + _loadSprite(url: string) { + this._spriteRequest = loadSprite( + url, + this.map._requestManager, + (err, images) => { + this._spriteRequest = null; + if (err) { + this.fire(new ErrorEvent(err)); + } else if (images) { + for (const id in images) { + this.imageManager.addImage(id, images[id]); + } + } + + this.imageManager.setLoaded(true); + this._availableImages = this.imageManager.listImages(); + this.dispatcher.broadcast('setImages', this._availableImages); + this.dispatcher.broadcast('spriteLoaded', true); + this.fire(new Event('data', {dataType: 'style'})); + }, + ); + } + + _validateLayer(layer: StyleLayer) { + const source = this.getSource(layer.source); + if (!source) { + return; + } + + const sourceLayer = layer.sourceLayer; + if (!sourceLayer) { + return; + } + + if ( + source.type === 'geojson' || + source.vectorLayerIds && + source.vectorLayerIds.indexOf(sourceLayer) === -1 + ) { + this.fire( + new ErrorEvent( + new Error( + `Source layer "${sourceLayer}" ` + `does not exist on source "${source.id}" ` + `as specified by style layer "${layer.id}"`, + ), + ), + ); + } + } + + loaded(): boolean { + if (!this._loaded) return false; + + if (Object.keys(this._updatedSources).length) return false; + + for (const id in this._sourceCaches) + if (!this._sourceCaches[id].loaded()) return false; + + if (!this.imageManager.isLoaded()) return false; + + return true; + } + + _serializeLayers(ids: Array): Array { + const serializedLayers = []; + for (const id of ids) { + const layer = this._layers[id]; + if (layer.type !== 'custom') { + serializedLayers.push(layer.serialize()); + } + } + return serializedLayers; + } + + hasTransitions(): boolean { + if (this.light && this.light.hasTransition()) { + return true; + } + + if (this.fog && this.fog.hasTransition()) { + return true; + } + + for (const id in this._sourceCaches) { + if (this._sourceCaches[id].hasTransition()) { + return true; + } + } + + for (const id in this._layers) { + if (this._layers[id].hasTransition()) { + return true; + } + } + + return false; + } + + get order(): Array { + if (this.map._optimizeForTerrain && this.terrain) { + assert(this._drapedFirstOrder.length === this._order.length); + return this._drapedFirstOrder; + } + return this._order; + } isLayerDraped(layer: StyleLayer): boolean { if (!this.terrain) return false; @@ -491,142 +534,159 @@ class Style extends Evented { return drapedLayers[layer.type]; } - _checkLoaded() { - if (!this._loaded) { - throw new Error('Style is not done loading'); - } - } + _checkLoaded() { + if (!this._loaded) { + throw new Error('Style is not done loading'); + } + } - /** + /** * Apply queued style updates in a batch and recalculate zoom-dependent paint properties. * @private */ - update(parameters: EvaluationParameters) { - if (!this._loaded) { - return; - } - - const changed = this._changed; - if (this._changed) { - const updatedIds = Object.keys(this._updatedLayers); - const removedIds = Object.keys(this._removedLayers); - - if (updatedIds.length || removedIds.length) { - this._updateWorkerLayers(updatedIds, removedIds); - } - for (const id in this._updatedSources) { - const action = this._updatedSources[id]; - assert(action === 'reload' || action === 'clear'); - if (action === 'reload') { - this._reloadSource(id); - } else if (action === 'clear') { - this._clearSource(id); - } - } - - this._updateTilesForChangedImages(); - - for (const id in this._updatedPaintProps) { - this._layers[id].updateTransitions(parameters); - } - - this.light.updateTransitions(parameters); - if (this.fog) { - this.fog.updateTransitions(parameters); - } - - this._resetUpdates(); - } - - const sourcesUsedBefore = {}; - - for (const sourceId in this._sourceCaches) { - const sourceCache = this._sourceCaches[sourceId]; - sourcesUsedBefore[sourceId] = sourceCache.used; - sourceCache.used = false; - } - - for (const layerId of this._order) { - const layer = this._layers[layerId]; - - layer.recalculate(parameters, this._availableImages); - if (!layer.isHidden(parameters.zoom)) { - const sourceCache = this._getLayerSourceCache(layer); - if (sourceCache) sourceCache.used = true; - } - - const painter = this.map.painter; - if (painter) { - const programIds = layer.getProgramIds(); - if (!programIds) continue; - - const programConfiguration = layer.getProgramConfiguration(parameters.zoom); - - for (const programId of programIds) { - painter.useProgram(programId, programConfiguration); - } - } - } - - for (const sourceId in sourcesUsedBefore) { - const sourceCache = this._sourceCaches[sourceId]; - if (sourcesUsedBefore[sourceId] !== sourceCache.used) { - sourceCache.getSource().fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId: sourceCache.getSource().id})); - } - } - - this.light.recalculate(parameters); - if (this.terrain) { - this.terrain.recalculate(parameters); - } - if (this.fog) { - this.fog.recalculate(parameters); - } - this.z = parameters.zoom; - - if (this._markersNeedUpdate) { - this._updateMarkersOpacity(); - this._markersNeedUpdate = false; - } - - if (changed) { - this.fire(new Event('data', {dataType: 'style'})); - } - } - - /* + update(parameters: EvaluationParameters) { + if (!this._loaded) { + return; + } + + const changed = this._changed; + if (this._changed) { + const updatedIds = Object.keys(this._updatedLayers); + const removedIds = Object.keys(this._removedLayers); + + if (updatedIds.length || removedIds.length) { + this._updateWorkerLayers(updatedIds, removedIds); + } + for (const id in this._updatedSources) { + const action = this._updatedSources[id]; + assert(action === 'reload' || action === 'clear'); + if (action === 'reload') { + this._reloadSource(id); + } else if (action === 'clear') { + this._clearSource(id); + } + } + + this._updateTilesForChangedImages(); + + for (const id in this._updatedPaintProps) { + this._layers[id].updateTransitions(parameters); + } + + this.light.updateTransitions(parameters); + if (this.fog) { + this.fog.updateTransitions(parameters); + } + + this._resetUpdates(); + } + + const sourcesUsedBefore = {}; + + for (const sourceId in this._sourceCaches) { + const sourceCache = this._sourceCaches[sourceId]; + sourcesUsedBefore[sourceId] = sourceCache.used; + sourceCache.used = false; + } + + for (const layerId of this._order) { + const layer = this._layers[layerId]; + + layer.recalculate(parameters, this._availableImages); + if (!layer.isHidden(parameters.zoom)) { + const sourceCache = this._getLayerSourceCache(layer); + if (sourceCache) sourceCache.used = true; + } + + const painter = this.map.painter; + if (painter) { + const programIds = layer.getProgramIds(); + if (!programIds) continue; + + const programConfiguration = layer.getProgramConfiguration( + parameters.zoom, + ); + + for (const programId of programIds) { + painter.useProgram(programId, programConfiguration); + } + } + } + + for (const sourceId in sourcesUsedBefore) { + const sourceCache = this._sourceCaches[sourceId]; + if (sourcesUsedBefore[sourceId] !== sourceCache.used) { + sourceCache.getSource().fire( + new Event( + 'data', + { + sourceDataType: 'visibility', + dataType: 'source', + sourceId: sourceCache.getSource().id, + }, + ), + ); + } + } + + this.light.recalculate(parameters); + if (this.terrain) { + this.terrain.recalculate(parameters); + } + if (this.fog) { + this.fog.recalculate(parameters); + } + this.z = parameters.zoom; + + if (this._markersNeedUpdate) { + this._updateMarkersOpacity(); + this._markersNeedUpdate = false; + } + + if (changed) { + this.fire(new Event('data', {dataType: 'style'})); + } + } + + /* * Apply any queued image changes. */ - _updateTilesForChangedImages() { - const changedImages = Object.keys(this._changedImages); - if (changedImages.length) { - for (const name in this._sourceCaches) { - this._sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], changedImages); - } - this._changedImages = {}; - } - } - - _updateWorkerLayers(updatedIds: Array, removedIds: Array) { - this.dispatcher.broadcast('updateLayers', { - layers: this._serializeLayers(updatedIds), - removedIds - }); - } - - _resetUpdates() { - this._changed = false; - - this._updatedLayers = {}; - this._removedLayers = {}; - - this._updatedSources = {}; - this._updatedPaintProps = {}; - - this._changedImages = {}; - } - - /** + _updateTilesForChangedImages() { + const changedImages = Object.keys(this._changedImages); + if (changedImages.length) { + for (const name in this._sourceCaches) { + this._sourceCaches[name].reloadTilesForDependencies( + ['icons', 'patterns'], + changedImages, + ); + } + this._changedImages = {}; + } + } + + _updateWorkerLayers(updatedIds: Array, removedIds: Array) { + this.dispatcher.broadcast( + 'updateLayers', + { + layers: this._serializeLayers(updatedIds), + removedIds, + }, + ); + } + + _resetUpdates() { + this._changed = false; + + this._updatedLayers = {}; + this._removedLayers = {}; + + this._updatedSources = {}; + this._updatedPaintProps = {}; + + this._changedImages = {}; + } + + /** * Update this style's state to match the given style JSON, performing only * the necessary mutations. * @@ -636,202 +696,252 @@ class Style extends Evented { * @returns {boolean} true if any changes were made; false otherwise * @private */ - setState(nextState: StyleSpecification): boolean { - this._checkLoaded(); - - if (emitValidationErrors(this, validateStyle(nextState))) return false; - - nextState = clone(nextState); - nextState.layers = deref(nextState.layers); - - const changes = diffStyles(this.serialize(), nextState) - .filter(op => !(op.command in ignoredDiffOperations)); - - if (changes.length === 0) { - return false; - } - - const unimplementedOps = changes.filter(op => !(op.command in supportedDiffOperations)); - if (unimplementedOps.length > 0) { - throw new Error(`Unimplemented: ${unimplementedOps.map(op => op.command).join(', ')}.`); - } - - changes.forEach((op) => { - if (op.command === 'setTransition' || op.command === 'setProjection') { - // `transition` and `projection` are always read directly from - // `this.stylesheet`, which we update below - return; - } - (this: any)[op.command].apply(this, op.args); - }); - - this.stylesheet = nextState; - this._updateMapProjection(); - - return true; - } - - addImage(id: string, image: StyleImage): this { - if (this.getImage(id)) { - return this.fire(new ErrorEvent(new Error('An image with this name already exists.'))); - } - this.imageManager.addImage(id, image); - this._afterImageUpdated(id); - return this; - } - - updateImage(id: string, image: StyleImage) { - this.imageManager.updateImage(id, image); - } - - getImage(id: string): ?StyleImage { - return this.imageManager.getImage(id); - } - - removeImage(id: string): this { - if (!this.getImage(id)) { - return this.fire(new ErrorEvent(new Error('No image with this name exists.'))); - } - this.imageManager.removeImage(id); - this._afterImageUpdated(id); - return this; - } - - _afterImageUpdated(id: string) { - this._availableImages = this.imageManager.listImages(); - this._changedImages[id] = true; - this._changed = true; - this.dispatcher.broadcast('setImages', this._availableImages); - this.fire(new Event('data', {dataType: 'style'})); - } - - listImages(): Array { - this._checkLoaded(); - return this._availableImages.slice(); - } - - addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - if (this.getSource(id) !== undefined) { - throw new Error('There is already a source with this ID'); - } - - if (!source.type) { - throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(source).join(', ')}.`); - } - - const builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; - const shouldValidate = builtIns.indexOf(source.type) >= 0; - if (shouldValidate && this._validate(validateSource, `sources.${id}`, source, null, options)) return; - - if (this.map && this.map._collectResourceTiming) (source: any).collectResourceTiming = true; - - const sourceInstance = createSource(id, source, this.dispatcher, this); - - sourceInstance.setEventedParent(this, () => ({ - isSourceLoaded: this._isSourceCacheLoaded(id), - source: sourceInstance.serialize(), - sourceId: id - })); - - const addSourceCache = (onlySymbols) => { - const sourceCacheId = (onlySymbols ? 'symbol:' : 'other:') + id; - const sourceCache = this._sourceCaches[sourceCacheId] = new SourceCache(sourceCacheId, sourceInstance, onlySymbols); - (onlySymbols ? this._symbolSourceCaches : this._otherSourceCaches)[id] = sourceCache; - sourceCache.style = this; - - sourceCache.onAdd(this.map); - }; - - addSourceCache(false); - if (source.type === 'vector' || source.type === 'geojson') { - addSourceCache(true); - } - - if (sourceInstance.onAdd) sourceInstance.onAdd(this.map); - - this._changed = true; - } - - /** + setState(nextState: StyleSpecification): boolean { + this._checkLoaded(); + + if (emitValidationErrors(this, validateStyle(nextState))) return false; + + nextState = clone(nextState); + nextState.layers = deref(nextState.layers); + + const changes = diffStyles(this.serialize(), nextState).filter( + op => !(op.command in ignoredDiffOperations), + ); + + if (changes.length === 0) { + return false; + } + + const unimplementedOps = changes.filter( + op => !(op.command in supportedDiffOperations), + ); + if (unimplementedOps.length > 0) { + throw ( + new Error(`Unimplemented: ${unimplementedOps.map(op => op.command).join( + ', ', + )}.`,) + ); + } + + changes.forEach( + op => { + if (op.command === 'setTransition' || op.command === 'setProjection') { + // `transition` and `projection` are always read directly from + // `this.stylesheet`, which we update below + return; + } + (this: any)[op.command].apply(this, op.args); + }, + ); + + this.stylesheet = nextState; + this._updateMapProjection(); + + return true; + } + + addImage(id: string, image: StyleImage): this { + if (this.getImage(id)) { + return this.fire( + new ErrorEvent(new Error('An image with this name already exists.')), + ); + } + this.imageManager.addImage(id, image); + this._afterImageUpdated(id); + return this; + } + + updateImage(id: string, image: StyleImage) { + this.imageManager.updateImage(id, image); + } + + getImage(id: string): ?StyleImage { + return this.imageManager.getImage(id); + } + + removeImage(id: string): this { + if (!this.getImage(id)) { + return this.fire( + new ErrorEvent(new Error('No image with this name exists.')), + ); + } + this.imageManager.removeImage(id); + this._afterImageUpdated(id); + return this; + } + + _afterImageUpdated(id: string) { + this._availableImages = this.imageManager.listImages(); + this._changedImages[id] = true; + this._changed = true; + this.dispatcher.broadcast('setImages', this._availableImages); + this.fire(new Event('data', {dataType: 'style'})); + } + + listImages(): Array { + this._checkLoaded(); + return this._availableImages.slice(); + } + + addSource( + id: string, + source: SourceSpecification, + options: StyleSetterOptions = {}, + ) { + this._checkLoaded(); + + if (this.getSource(id) !== undefined) { + throw new Error('There is already a source with this ID'); + } + + if (!source.type) { + throw ( + new Error(`The type property must be defined, but only the following properties were given: ${Object.keys( + source, + ).join(', ')}.`,) + ); + } + + const builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; + const shouldValidate = builtIns.indexOf(source.type) >= 0; + if ( + shouldValidate && + this._validate(validateSource, `sources.${id}`, source, null, options) + ) + return; + + if (this.map && this.map._collectResourceTiming) + (source: any).collectResourceTiming = true; + + const sourceInstance = createSource(id, source, this.dispatcher, this); + + sourceInstance.setEventedParent( + this, + () => ({ + isSourceLoaded: this._isSourceCacheLoaded(id), + source: sourceInstance.serialize(), + sourceId: id, + }), + ); + + const addSourceCache = (onlySymbols => { + const sourceCacheId = (onlySymbols ? 'symbol:' : 'other:') + id; + const sourceCache = this._sourceCaches[sourceCacheId] = new SourceCache( + sourceCacheId, + sourceInstance, + onlySymbols, + ); + (onlySymbols ? this._symbolSourceCaches : this._otherSourceCaches)[id] = sourceCache; + sourceCache.style = this; + + sourceCache.onAdd(this.map); + }); + + addSourceCache(false); + if (source.type === 'vector' || source.type === 'geojson') { + addSourceCache(true); + } + + if (sourceInstance.onAdd) sourceInstance.onAdd(this.map); + + this._changed = true; + } + + /** * Remove a source from this stylesheet, given its ID. * @param {string} id ID of the source to remove. * @throws {Error} If no source is found with the given ID. * @returns {Map} The {@link Map} object. */ - removeSource(id: string): this { - this._checkLoaded(); - - const source = this.getSource(id); - if (!source) { - throw new Error('There is no source with this ID'); - } - for (const layerId in this._layers) { - if (this._layers[layerId].source === id) { - return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); - } - } - if (this.terrain && this.terrain.get().source === id) { - return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while terrain is using it.`))); - } - - const sourceCaches = this._getSourceCaches(id); - for (const sourceCache of sourceCaches) { - delete this._sourceCaches[sourceCache.id]; - delete this._updatedSources[sourceCache.id]; - sourceCache.fire(new Event('data', {sourceDataType: 'metadata', dataType:'source', sourceId: sourceCache.getSource().id})); - sourceCache.setEventedParent(null); - sourceCache.clearTiles(); - } - delete this._otherSourceCaches[id]; - delete this._symbolSourceCaches[id]; - - source.setEventedParent(null); - if (source.onRemove) { - source.onRemove(this.map); - } - this._changed = true; - return this; - } - - /** + removeSource(id: string): this { + this._checkLoaded(); + + const source = this.getSource(id); + if (!source) { + throw new Error('There is no source with this ID'); + } + for (const layerId in this._layers) { + if (this._layers[layerId].source === id) { + return this.fire( + new ErrorEvent( + new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`,), + ), + ); + } + } + if (this.terrain && this.terrain.get().source === id) { + return this.fire( + new ErrorEvent( + new Error(`Source "${id}" cannot be removed while terrain is using it.`,), + ), + ); + } + + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + delete this._sourceCaches[sourceCache.id]; + delete this._updatedSources[sourceCache.id]; + sourceCache.fire( + new Event( + 'data', + { + sourceDataType: 'metadata', + dataType: 'source', + sourceId: sourceCache.getSource().id, + }, + ), + ); + sourceCache.setEventedParent(null); + sourceCache.clearTiles(); + } + delete this._otherSourceCaches[id]; + delete this._symbolSourceCaches[id]; + + source.setEventedParent(null); + if (source.onRemove) { + source.onRemove(this.map); + } + this._changed = true; + return this; + } + + /** * Set the data of a GeoJSON source, given its ID. * @param {string} id ID of the source. * @param {GeoJSON|string} data GeoJSON source. */ - setGeoJSONSourceData(id: string, data: GeoJSON | string) { - this._checkLoaded(); + setGeoJSONSourceData(id: string, data: GeoJSON | string) { + this._checkLoaded(); - assert(this.getSource(id) !== undefined, 'There is no source with this ID'); - const geojsonSource: GeoJSONSource = (this.getSource(id): any); - assert(geojsonSource.type === 'geojson'); + assert(this.getSource(id) !== undefined, 'There is no source with this ID'); + const geojsonSource: GeoJSONSource = (this.getSource(id): any); + assert(geojsonSource.type === 'geojson'); - geojsonSource.setData(data); - this._changed = true; - } + geojsonSource.setData(data); + this._changed = true; + } - /** + /** * Get a source by ID. * @param {string} id ID of the desired source. * @returns {?Source} The source object. */ - getSource(id: string): ?Source { - const sourceCache = this._getSourceCache(id); - return sourceCache && sourceCache.getSource(); - } - - _getSources(): Source[] { - const sources = []; - for (const id in this._otherSourceCaches) { - const sourceCache = this._getSourceCache(id); - if (sourceCache) sources.push(sourceCache.getSource()); - } - - return sources; - } - - /** + getSource(id: string): ?Source { + const sourceCache = this._getSourceCache(id); + return sourceCache && sourceCache.getSource(); + } + + _getSources(): Array { + const sources = []; + for (const id in this._otherSourceCaches) { + const sourceCache = this._getSourceCache(id); + if (sourceCache) sources.push(sourceCache.getSource()); + } + + return sources; + } + + /** * Add a layer to the map style. The layer will be inserted before the layer with * ID `before`, or appended if `before` is omitted. * @param {Object | CustomLayerInterface} layerObject The style layer to add. @@ -839,116 +949,146 @@ class Style extends Evented { * @param {Object} options Style setter options. * @returns {Map} The {@link Map} object. */ - addLayer(layerObject: LayerSpecification | CustomLayerInterface, before?: string, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - const id = layerObject.id; - - if (this.getLayer(id)) { - this.fire(new ErrorEvent(new Error(`Layer with id "${id}" already exists on this map`))); - return; - } - - let layer; - if (layerObject.type === 'custom') { - - if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return; - - layer = createStyleLayer(layerObject); - - } else { - if (typeof layerObject.source === 'object') { - this.addSource(id, layerObject.source); - layerObject = clone(layerObject); - layerObject = (extend(layerObject, {source: id}): any); - } - - // this layer is not in the style.layers array, so we pass an impossible array index - if (this._validate(validateLayer, - `layers.${id}`, layerObject, {arrayIndex: -1}, options)) return; - - layer = createStyleLayer(layerObject); - this._validateLayer(layer); - - layer.setEventedParent(this, {layer: {id}}); - this._serializedLayers[layer.id] = layer.serialize(); - this._updateLayerCount(layer, true); - } - - const index = before ? this._order.indexOf(before) : this._order.length; - if (before && index === -1) { - this.fire(new ErrorEvent(new Error(`Layer with id "${before}" does not exist on this map.`))); - return; - } - - this._order.splice(index, 0, id); - this._layerOrderChanged = true; - - this._layers[id] = layer; - - const sourceCache = this._getLayerSourceCache(layer); - if (this._removedLayers[id] && layer.source && sourceCache && layer.type !== 'custom') { - // If, in the current batch, we have already removed this layer - // and we are now re-adding it with a different `type`, then we - // need to clear (rather than just reload) the underyling source's - // tiles. Otherwise, tiles marked 'reloading' will have buckets / - // buffers that are set up for the _previous_ version of this - // layer, causing, e.g.: - // https://github.com/mapbox/mapbox-gl-js/issues/3633 - const removed = this._removedLayers[id]; - delete this._removedLayers[id]; - if (removed.type !== layer.type) { - this._updatedSources[layer.source] = 'clear'; - } else { - this._updatedSources[layer.source] = 'reload'; - sourceCache.pause(); - } - } - this._updateLayer(layer); - - if (layer.onAdd) { - layer.onAdd(this.map); - } - - this._updateDrapeFirstLayers(); - } - - /** + addLayer( + layerObject: LayerSpecification | CustomLayerInterface, + before?: string, + options: StyleSetterOptions = {}, + ) { + this._checkLoaded(); + + const id = layerObject.id; + + if (this.getLayer(id)) { + this.fire( + new ErrorEvent( + new Error(`Layer with id "${id}" already exists on this map`), + ), + ); + return; + } + + let layer; + if (layerObject.type === 'custom') { + if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) + return; + + layer = createStyleLayer(layerObject); + } else { + if (typeof layerObject.source === 'object') { + this.addSource(id, layerObject.source); + layerObject = clone(layerObject); + layerObject = (extend(layerObject, {source: id}): any); + } + + // this layer is not in the style.layers array, so we pass an impossible array index + if ( + this._validate( + validateLayer, + `layers.${id}`, + layerObject, + {arrayIndex: -1}, + options, + ) + ) + return; + + layer = createStyleLayer(layerObject); + this._validateLayer(layer); + + layer.setEventedParent(this, {layer: {id}}); + this._serializedLayers[layer.id] = layer.serialize(); + this._updateLayerCount(layer, true); + } + + const index = before ? this._order.indexOf(before) : this._order.length; + if (before && index === -1) { + this.fire( + new ErrorEvent( + new Error(`Layer with id "${before}" does not exist on this map.`), + ), + ); + return; + } + + this._order.splice(index, 0, id); + this._layerOrderChanged = true; + + this._layers[id] = layer; + + const sourceCache = this._getLayerSourceCache(layer); + if ( + this._removedLayers[id] && layer.source && sourceCache && + layer.type !== 'custom' + ) { + // If, in the current batch, we have already removed this layer + // and we are now re-adding it with a different `type`, then we + // need to clear (rather than just reload) the underyling source's + // tiles. Otherwise, tiles marked 'reloading' will have buckets / + // buffers that are set up for the _previous_ version of this + // layer, causing, e.g.: + // https://github.com/mapbox/mapbox-gl-js/issues/3633 + const removed = this._removedLayers[id]; + delete this._removedLayers[id]; + if (removed.type !== layer.type) { + this._updatedSources[layer.source] = 'clear'; + } else { + this._updatedSources[layer.source] = 'reload'; + sourceCache.pause(); + } + } + this._updateLayer(layer); + + if (layer.onAdd) { + layer.onAdd(this.map); + } + + this._updateDrapeFirstLayers(); + } + + /** * Moves a layer to a different z-position. The layer will be inserted before the layer with * ID `before`, or appended if `before` is omitted. * @param {string} id ID of the layer to move. * @param {string} [before] ID of an existing layer to insert before. */ - moveLayer(id: string, before?: string) { - this._checkLoaded(); - this._changed = true; - - const layer = this._layers[id]; - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`))); - return; - } - - if (id === before) { - return; - } - - const index = this._order.indexOf(id); - this._order.splice(index, 1); - - const newIndex = before ? this._order.indexOf(before) : this._order.length; - if (before && newIndex === -1) { - this.fire(new ErrorEvent(new Error(`Layer with id "${before}" does not exist on this map.`))); - return; - } - this._order.splice(newIndex, 0, id); - - this._layerOrderChanged = true; - - this._updateDrapeFirstLayers(); - } - - /** + moveLayer(id: string, before?: string) { + this._checkLoaded(); + this._changed = true; + + const layer = this._layers[id]; + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`,), + ), + ); + return; + } + + if (id === before) { + return; + } + + const index = this._order.indexOf(id); + this._order.splice(index, 1); + + const newIndex = before ? this._order.indexOf(before) : this._order.length; + if (before && newIndex === -1) { + this.fire( + new ErrorEvent( + new Error(`Layer with id "${before}" does not exist on this map.`), + ), + ); + return; + } + this._order.splice(newIndex, 0, id); + + this._layerOrderChanged = true; + + this._updateDrapeFirstLayers(); + } + + /** * Remove the layer with the given id from the style. * * If no such layer exists, an `error` event is fired. @@ -956,921 +1096,1171 @@ class Style extends Evented { * @param {string} id ID of the layer to remove. * @fires Map.event:error */ - removeLayer(id: string) { - this._checkLoaded(); - - const layer = this._layers[id]; - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be removed.`))); - return; - } - - layer.setEventedParent(null); - - this._updateLayerCount(layer, false); - - const index = this._order.indexOf(id); - this._order.splice(index, 1); - - this._layerOrderChanged = true; - this._changed = true; - this._removedLayers[id] = layer; - delete this._layers[id]; - delete this._serializedLayers[id]; - delete this._updatedLayers[id]; - delete this._updatedPaintProps[id]; - - if (layer.onRemove) { - layer.onRemove(this.map); - } - - this._updateDrapeFirstLayers(); - } - - /** + removeLayer(id: string) { + this._checkLoaded(); + + const layer = this._layers[id]; + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${id}' does not exist in the map's style and cannot be removed.`,), + ), + ); + return; + } + + layer.setEventedParent(null); + + this._updateLayerCount(layer, false); + + const index = this._order.indexOf(id); + this._order.splice(index, 1); + + this._layerOrderChanged = true; + this._changed = true; + this._removedLayers[id] = layer; + delete this._layers[id]; + delete this._serializedLayers[id]; + delete this._updatedLayers[id]; + delete this._updatedPaintProps[id]; + + if (layer.onRemove) { + layer.onRemove(this.map); + } + + this._updateDrapeFirstLayers(); + } + + /** * Return the style layer object with the given `id`. * * @param {string} id ID of the desired layer. * @returns {?StyleLayer} A layer, if one with the given `id` exists. */ - getLayer(id: string): ?StyleLayer { - return this._layers[id]; - } + getLayer(id: string): ?StyleLayer { + return this._layers[id]; + } - /** + /** * Checks if a specific layer is present within the style. * * @param {string} id ID of the desired layer. * @returns {boolean} A boolean specifying if the given layer is present. */ - hasLayer(id: string): boolean { - return id in this._layers; - } + hasLayer(id: string): boolean { + return id in this._layers; + } - /** + /** * Checks if a specific layer type is present within the style. * * @param {string} type Type of the desired layer. * @returns {boolean} A boolean specifying if the given layer type is present. */ - hasLayerType(type: string): boolean { - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.type === type) { - return true; - } - } - return false; - } - - setLayerZoomRange(layerId: string, minzoom: ?number, maxzoom: ?number) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot have zoom extent.`))); - return; - } - - if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; - - if (minzoom != null) { - layer.minzoom = minzoom; - } - if (maxzoom != null) { - layer.maxzoom = maxzoom; - } - this._updateLayer(layer); - } - - setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be filtered.`))); - return; - } - - if (deepEqual(layer.filter, filter)) { - return; - } - - if (filter === null || filter === undefined) { - layer.filter = undefined; - this._updateLayer(layer); - return; - } - - if (this._validate(validateFilter, `layers.${layer.id}.filter`, filter, {layerType: layer.type}, options)) { - return; - } - - layer.filter = clone(filter); - this._updateLayer(layer); - } - - /** + hasLayerType(type: string): boolean { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === type) { + return true; + } + } + return false; + } + + setLayerZoomRange(layerId: string, minzoom: ?number, maxzoom: ?number) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${layerId}' does not exist in the map's style and cannot have zoom extent.`,), + ), + ); + return; + } + + if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; + + if (minzoom != null) { + layer.minzoom = minzoom; + } + if (maxzoom != null) { + layer.maxzoom = maxzoom; + } + this._updateLayer(layer); + } + + setFilter( + layerId: string, + filter: ?FilterSpecification, + options: StyleSetterOptions = {}, + ) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${layerId}' does not exist in the map's style and cannot be filtered.`,), + ), + ); + return; + } + + if (deepEqual(layer.filter, filter)) { + return; + } + + if (filter === null || filter === undefined) { + layer.filter = undefined; + this._updateLayer(layer); + return; + } + + if ( + this._validate( + validateFilter, + `layers.${layer.id}.filter`, + filter, + {layerType: layer.type}, + options, + ) + ) { + return; + } + + layer.filter = clone(filter); + this._updateLayer(layer); + } + + /** * Get a layer's filter object. * @param {string} layerId The layer to inspect. * @returns {*} The layer's filter, if any. */ - getFilter(layerId: string): ?FilterSpecification { - const layer = this.getLayer(layerId); - return layer && clone(layer.filter); - } - - setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`))); - return; - } - - if (deepEqual(layer.getLayoutProperty(name), value)) return; - - layer.setLayoutProperty(name, value, options); - this._updateLayer(layer); - } - - /** + getFilter(layerId: string): ?FilterSpecification { + const layer = this.getLayer(layerId); + return layer && clone(layer.filter); + } + + setLayoutProperty( + layerId: string, + name: string, + value: any, + options: StyleSetterOptions = {}, + ) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`,), + ), + ); + return; + } + + if (deepEqual(layer.getLayoutProperty(name), value)) return; + + layer.setLayoutProperty(name, value, options); + this._updateLayer(layer); + } + + /** * Get a layout property's value from a given layer. * @param {string} layerId The layer to inspect. * @param {string} name The name of the layout property. * @returns {*} The property value. */ - getLayoutProperty(layerId: string, name: string): ?PropertyValueSpecification { - const layer = this.getLayer(layerId); - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style.`))); - return; - } - - return layer.getLayoutProperty(name); - } - - setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`))); - return; - } - - if (deepEqual(layer.getPaintProperty(name), value)) return; - - const requiresRelayout = layer.setPaintProperty(name, value, options); - if (requiresRelayout) { - this._updateLayer(layer); - } - - this._changed = true; - this._updatedPaintProps[layerId] = true; - } - - getPaintProperty(layerId: string, name: string): void | TransitionSpecification | PropertyValueSpecification { - const layer = this.getLayer(layerId); - return layer && layer.getPaintProperty(name); - } - - setFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }, state: Object) { - this._checkLoaded(); - const sourceId = target.source; - const sourceLayer = target.sourceLayer; - const source = this.getSource(sourceId); - - if (!source) { - this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); - return; - } - const sourceType = source.type; - if (sourceType === 'geojson' && sourceLayer) { - this.fire(new ErrorEvent(new Error(`GeoJSON sources cannot have a sourceLayer parameter.`))); - return; - } - if (sourceType === 'vector' && !sourceLayer) { - this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); - return; - } - if (target.id === undefined) { - this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); - } - - const sourceCaches = this._getSourceCaches(sourceId); - for (const sourceCache of sourceCaches) { - sourceCache.setFeatureState(sourceLayer, target.id, state); - } - } - - removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { - this._checkLoaded(); - const sourceId = target.source; - const source = this.getSource(sourceId); - - if (!source) { - this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); - return; - } - - const sourceType = source.type; - const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; - - if (sourceType === 'vector' && !sourceLayer) { - this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); - return; - } - - if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) { - this.fire(new ErrorEvent(new Error(`A feature id is required to remove its specific state property.`))); - return; - } - - const sourceCaches = this._getSourceCaches(sourceId); - for (const sourceCache of sourceCaches) { - sourceCache.removeFeatureState(sourceLayer, target.id, key); - } - } - - getFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }): ?FeatureStates { - this._checkLoaded(); - const sourceId = target.source; - const sourceLayer = target.sourceLayer; - const source = this.getSource(sourceId); - - if (!source) { - this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); - return; - } - const sourceType = source.type; - if (sourceType === 'vector' && !sourceLayer) { - this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); - return; - } - if (target.id === undefined) { - this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); - } - - const sourceCaches = this._getSourceCaches(sourceId); - return sourceCaches[0].getFeatureState(sourceLayer, target.id); - } - - getTransition(): TransitionSpecification { - return extend({duration: 300, delay: 0}, this.stylesheet && this.stylesheet.transition); - } - - serialize(): StyleSpecification { - const sources = {}; - for (const cacheId in this._sourceCaches) { - const source = this._sourceCaches[cacheId].getSource(); - if (!sources[source.id]) { - sources[source.id] = source.serialize(); - } - } - - return filterObject({ - version: this.stylesheet.version, - name: this.stylesheet.name, - metadata: this.stylesheet.metadata, - light: this.stylesheet.light, - terrain: this.getTerrain() || undefined, - fog: this.stylesheet.fog, - center: this.stylesheet.center, - zoom: this.stylesheet.zoom, - bearing: this.stylesheet.bearing, - pitch: this.stylesheet.pitch, - sprite: this.stylesheet.sprite, - glyphs: this.stylesheet.glyphs, - transition: this.stylesheet.transition, - projection: this.stylesheet.projection, - sources, - layers: this._serializeLayers(this._order) - }, (value) => { return value !== undefined; }); - } - - _updateLayer(layer: StyleLayer) { - this._updatedLayers[layer.id] = true; - const sourceCache = this._getLayerSourceCache(layer); - if (layer.source && !this._updatedSources[layer.source] && - //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865) - sourceCache && - sourceCache.getSource().type !== 'raster') { - this._updatedSources[layer.source] = 'reload'; - sourceCache.pause(); - } - this._changed = true; - layer.invalidateCompiledFilter(); - - } - - _flattenAndSortRenderedFeatures(sourceResults: Array): Array { - // Feature order is complicated. - // The order between features in two 2D layers is always determined by layer order. - // The order between features in two 3D layers is always determined by depth. - // The order between a feature in a 2D layer and a 3D layer is tricky: - // Most often layer order determines the feature order in this case. If - // a line layer is above a extrusion layer the line feature will be rendered - // above the extrusion. If the line layer is below the extrusion layer, - // it will be rendered below it. - // - // There is a weird case though. - // You have layers in this order: extrusion_layer_a, line_layer, extrusion_layer_b - // Each layer has a feature that overlaps the other features. - // The feature in extrusion_layer_a is closer than the feature in extrusion_layer_b so it is rendered above. - // The feature in line_layer is rendered above extrusion_layer_a. - // This means that that the line_layer feature is above the extrusion_layer_b feature despite - // it being in an earlier layer. - - const isLayer3D = layerId => this._layers[layerId].type === 'fill-extrusion'; - - const layerIndex = {}; - const features3D = []; - for (let l = this._order.length - 1; l >= 0; l--) { - const layerId = this._order[l]; - if (isLayer3D(layerId)) { - layerIndex[layerId] = l; - for (const sourceResult of sourceResults) { - const layerFeatures = sourceResult[layerId]; - if (layerFeatures) { - for (const featureWrapper of layerFeatures) { - features3D.push(featureWrapper); - } - } - } - } - } - - features3D.sort((a, b) => { - return b.intersectionZ - a.intersectionZ; - }); - - const features = []; - for (let l = this._order.length - 1; l >= 0; l--) { - const layerId = this._order[l]; - - if (isLayer3D(layerId)) { - // add all 3D features that are in or above the current layer - for (let i = features3D.length - 1; i >= 0; i--) { - const topmost3D = features3D[i].feature; - if (layerIndex[topmost3D.layer.id] < l) break; - features.push(topmost3D); - features3D.pop(); - } - } else { - for (const sourceResult of sourceResults) { - const layerFeatures = sourceResult[layerId]; - if (layerFeatures) { - for (const featureWrapper of layerFeatures) { - features.push(featureWrapper.feature); - } - } - } - } - } - - return features; - } - - queryRenderedFeatures(queryGeometry: PointLike | [PointLike, PointLike], params: any, transform: Transform): Array { - if (params && params.filter) { - this._validate(validateFilter, 'queryRenderedFeatures.filter', params.filter, null, params); - } - - const includedSources = {}; - if (params && params.layers) { - if (!Array.isArray(params.layers)) { - this.fire(new ErrorEvent(new Error('parameters.layers must be an Array.'))); - return []; - } - for (const layerId of params.layers) { - const layer = this._layers[layerId]; - if (!layer) { - // this layer is not in the style.layers array - this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`))); - return []; - } - includedSources[layer.source] = true; - } - } - - const sourceResults: Array = []; - - params.availableImages = this._availableImages; - - const has3DLayer = (params && params.layers) ? - params.layers.some((layerId) => { - const layer = this.getLayer(layerId); - return layer && layer.is3D(); - }) : - this.has3DLayers(); - const queryGeometryStruct = QueryGeometry.createFromScreenPoints(queryGeometry, transform); - - for (const id in this._sourceCaches) { - const sourceId = this._sourceCaches[id].getSource().id; - if (params.layers && !includedSources[sourceId]) continue; - sourceResults.push( - queryRenderedFeatures( - this._sourceCaches[id], - this._layers, - this._serializedLayers, - queryGeometryStruct, - params, - transform, - has3DLayer, - !!this.map._showQueryGeometry) - ); - } - - if (this.placement) { - // If a placement has run, query against its CollisionIndex - // for symbol results, and treat it as an extra source to merge - sourceResults.push( - queryRenderedSymbols( - this._layers, - this._serializedLayers, - this._getLayerSourceCache.bind(this), - queryGeometryStruct.screenGeometry, - params, - this.placement.collisionIndex, - this.placement.retainedQueryData) - ); - } - - return (this._flattenAndSortRenderedFeatures(sourceResults): any); - } - - querySourceFeatures(sourceID: string, params: ?{sourceLayer: ?string, filter: ?Array, validate?: boolean}): Array { - if (params && params.filter) { - this._validate(validateFilter, 'querySourceFeatures.filter', params.filter, null, params); - } - const sourceCaches = this._getSourceCaches(sourceID); - let results = []; - for (const sourceCache of sourceCaches) { - results = results.concat(querySourceFeatures(sourceCache, params)); - } - return results; - } - - addSourceType(name: string, SourceType: SourceClass, callback: Callback): void { - if (Style.getSourceType(name)) { - return callback(new Error(`A source type called "${name}" already exists.`)); - } - - Style.setSourceType(name, SourceType); - - if (!SourceType.workerSourceURL) { - return callback(null, null); - } - - this.dispatcher.broadcast('loadWorkerSource', { - name, - url: SourceType.workerSourceURL - }, callback); - } - - getLight(): LightSpecification { - return this.light.getLight(); - } - - setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - const light = this.light.getLight(); - let _update = false; - for (const key in lightOptions) { - if (!deepEqual(lightOptions[key], light[key])) { - _update = true; - break; - } - } - if (!_update) return; - - const parameters = this._setTransitionParameters({duration: 300, delay: 0}); - - this.light.setLight(lightOptions, options); - this.light.updateTransitions(parameters); - } - - getTerrain(): ?TerrainSpecification { - return this.terrain && this.terrain.drapeRenderMode === DrapeRenderMode.elevated ? this.terrain.get() : null; - } - - setTerrainForDraping() { - const mockTerrainOptions = {source: '', exaggeration: 0}; - this.setTerrain(mockTerrainOptions, DrapeRenderMode.deferred); - } - - // eslint-disable-next-line no-warning-comments - // TODO: generic approach for root level property: light, terrain, skybox. - // It is not done here to prevent rebasing issues. - setTerrain(terrainOptions: ?TerrainSpecification, drapeRenderMode: number = DrapeRenderMode.elevated) { - this._checkLoaded(); - - // Disabling - if (!terrainOptions) { - delete this.terrain; - delete this.stylesheet.terrain; - this.dispatcher.broadcast('enableTerrain', false); - this._force3DLayerUpdate(); - this._markersNeedUpdate = true; - return; - } - - if (drapeRenderMode === DrapeRenderMode.elevated) { - // Input validation and source object unrolling - if (typeof terrainOptions.source === 'object') { - const id = 'terrain-dem-src'; - this.addSource(id, ((terrainOptions.source): any)); - terrainOptions = clone(terrainOptions); - terrainOptions = (extend(terrainOptions, {source: id}): any); - } - - if (this._validate(validateTerrain, 'terrain', terrainOptions)) { - return; - } - } - - // Enabling - if (!this.terrain || (this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode)) { - this._createTerrain(terrainOptions, drapeRenderMode); - } else { // Updating - const terrain = this.terrain; - const currSpec = terrain.get(); - - for (const name of Object.keys(styleSpec.terrain)) { - // Fallback to use default style specification when the properties wasn't set - if (!terrainOptions.hasOwnProperty(name) && !!styleSpec.terrain[name].default) { - terrainOptions[name] = styleSpec.terrain[name].default; - } - } - for (const key in terrainOptions) { - if (!deepEqual(terrainOptions[key], currSpec[key])) { - terrain.set(terrainOptions); - this.stylesheet.terrain = terrainOptions; - const parameters = this._setTransitionParameters({duration: 0}); - terrain.updateTransitions(parameters); - break; - } - } - } - - this._updateDrapeFirstLayers(); - this._markersNeedUpdate = true; - } - - _createFog(fogOptions: FogSpecification) { - const fog = this.fog = new Fog(fogOptions, this.map.transform); - this.stylesheet.fog = fogOptions; - const parameters = this._setTransitionParameters({duration: 0}); - fog.updateTransitions(parameters); - } - - _updateMarkersOpacity() { - if (this.map._markers.length === 0) { - return; - } - this.map._requestDomTask(() => { - for (const marker of this.map._markers) { - marker._evaluateOpacity(); - } - }); - } - - getFog(): ?FogSpecification { - return this.fog ? this.fog.get() : null; - } - - setFog(fogOptions: FogSpecification) { - this._checkLoaded(); - - if (!fogOptions) { - // Remove fog - delete this.fog; - delete this.stylesheet.fog; - this._markersNeedUpdate = true; - return; - } - - if (!this.fog) { - // Initialize Fog - this._createFog(fogOptions); - } else { - // Updating fog - const fog = this.fog; - const currSpec = fog.get(); - - // empty object should pass through to set default values - if (Object.keys(fogOptions).length === 0) fog.set(fogOptions); - - for (const key in fogOptions) { - if (!deepEqual(fogOptions[key], currSpec[key])) { - fog.set(fogOptions); - this.stylesheet.fog = fogOptions; - const parameters = this._setTransitionParameters({duration: 0}); - fog.updateTransitions(parameters); - break; - } - } - } - - this._markersNeedUpdate = true; - } - - _setTransitionParameters(transitionOptions: Object): TransitionParameters { - return { - now: browser.now(), - transition: extend( - transitionOptions, - this.stylesheet.transition) - }; - } - - _updateDrapeFirstLayers() { - if (!this.map._optimizeForTerrain || !this.terrain) { - return; - } - - const draped = this._order.filter((id) => { - return this.isLayerDraped(this._layers[id]); - }); - - const nonDraped = this._order.filter((id) => { - return !this.isLayerDraped(this._layers[id]); - }); - this._drapedFirstOrder = []; - this._drapedFirstOrder.push(...draped); - this._drapedFirstOrder.push(...nonDraped); - } - - _createTerrain(terrainOptions: TerrainSpecification, drapeRenderMode: number) { - const terrain = this.terrain = new Terrain(terrainOptions, drapeRenderMode); - this.stylesheet.terrain = terrainOptions; - this.dispatcher.broadcast('enableTerrain', !this.terrainSetForDrapingOnly()); - this._force3DLayerUpdate(); - const parameters = this._setTransitionParameters({duration: 0}); - terrain.updateTransitions(parameters); - } - - _force3DLayerUpdate() { - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.type === 'fill-extrusion') { - this._updateLayer(layer); - } - } - } - - _forceSymbolLayerUpdate() { - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.type === 'symbol') { - this._updateLayer(layer); - } - } - } - - _validate(validate: Validator, key: string, value: any, props: any, options: { validate?: boolean } = {}): boolean { - if (options && options.validate === false) { - return false; - } - return emitValidationErrors(this, validate.call(validateStyle, extend({ - key, - style: this.serialize(), - value, - styleSpec - }, props))); - } - - _remove() { - if (this._request) { - this._request.cancel(); - this._request = null; - } - if (this._spriteRequest) { - this._spriteRequest.cancel(); - this._spriteRequest = null; - } - rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback); - for (const layerId in this._layers) { - const layer: StyleLayer = this._layers[layerId]; - layer.setEventedParent(null); - } - for (const id in this._sourceCaches) { - this._sourceCaches[id].clearTiles(); - this._sourceCaches[id].setEventedParent(null); - } - this.imageManager.setEventedParent(null); - this.setEventedParent(null); - this.dispatcher.remove(); - } - - _clearSource(id: string) { - const sourceCaches = this._getSourceCaches(id); - for (const sourceCache of sourceCaches) { - sourceCache.clearTiles(); - } - } - - _reloadSource(id: string) { - const sourceCaches = this._getSourceCaches(id); - for (const sourceCache of sourceCaches) { - sourceCache.resume(); - sourceCache.reload(); - } - } - - _reloadSources() { - for (const source of this._getSources()) { - if (source.reload) { - source.reload(); - } - } - } - - _updateSources(transform: Transform) { - for (const id in this._sourceCaches) { - this._sourceCaches[id].update(transform); - } - } - - _generateCollisionBoxes() { - for (const id in this._sourceCaches) { - const sourceCache = this._sourceCaches[id]; - sourceCache.resume(); - sourceCache.reload(); - } - } - - _updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number, crossSourceCollisions: boolean, forceFullPlacement: boolean = false): boolean { - let symbolBucketsChanged = false; - let placementCommitted = false; - - const layerTiles = {}; - - for (const layerID of this._order) { - const styleLayer = this._layers[layerID]; - if (styleLayer.type !== 'symbol') continue; - - if (!layerTiles[styleLayer.source]) { - const sourceCache = this._getLayerSourceCache(styleLayer); - if (!sourceCache) continue; - layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true) - .map((id) => sourceCache.getTileByID(id)) - .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1)); - } - - const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source], transform.center.lng, transform.projection); - symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged; - } - this.crossTileSymbolIndex.pruneUnusedLayers(this._order); - - // Anything that changes our "in progress" layer and tile indices requires us - // to start over. When we start over, we do a full placement instead of incremental - // to prevent starvation. - // We need to restart placement to keep layer indices in sync. - // Also force full placement when fadeDuration === 0 to ensure that newly loaded - // tiles will fully display symbols in their first frame - forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0; - - if (this._layerOrderChanged) { - this.fire(new Event('neworder')); - } - - if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) { - const fogState = this.fog && transform.projection.supportsFog ? this.fog.state : null; - this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement, fogState); - this._layerOrderChanged = false; - } - - if (this.pauseablePlacement.isDone()) { - // the last placement finished running, but the next one hasn’t - // started yet because of the `stillRecent` check immediately - // above, so mark it stale to ensure that we request another - // render frame - this.placement.setStale(); - } else { - this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); - - if (this.pauseablePlacement.isDone()) { - this.placement = this.pauseablePlacement.commit(browser.now()); - placementCommitted = true; - } - - if (symbolBucketsChanged) { - // since the placement gets split over multiple frames it is possible - // these buckets were processed before they were changed and so the - // placement is already stale while it is in progress - this.pauseablePlacement.placement.setStale(); - } - } - - if (placementCommitted || symbolBucketsChanged) { - for (const layerID of this._order) { - const styleLayer = this._layers[layerID]; - if (styleLayer.type !== 'symbol') continue; - this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]); - } - } - - // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols - const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(browser.now()); - return needsRerender; - } - - _releaseSymbolFadeTiles() { - for (const id in this._sourceCaches) { - this._sourceCaches[id].releaseSymbolFadeTiles(); - } - } - - // Callbacks from web workers - - getImages(mapId: string, params: {icons: Array, source: string, tileID: OverscaledTileID, type: string}, callback: Callback<{[_: string]: StyleImage}>) { - - this.imageManager.getImages(params.icons, callback); - - // Apply queued image changes before setting the tile's dependencies so that the tile - // is not reloaded unecessarily. Without this forced update the reload could happen in cases - // like this one: - // - icons contains "my-image" - // - imageManager.getImages(...) triggers `onstyleimagemissing` - // - the user adds "my-image" within the callback - // - addImage adds "my-image" to this._changedImages - // - the next frame triggers a reload of this tile even though it already has the latest version - this._updateTilesForChangedImages(); - - const setDependencies = (sourceCache: SourceCache) => { - if (sourceCache) { - sourceCache.setDependencies(params.tileID.key, params.type, params.icons); - } - }; - setDependencies(this._otherSourceCaches[params.source]); - setDependencies(this._symbolSourceCaches[params.source]); - } - - getGlyphs(mapId: string, params: {stacks: {[_: string]: Array}}, callback: Callback<{[_: string]: {glyphs: {[_: number]: ?StyleGlyph}, ascender?: number, descender?: number}}>) { - this.glyphManager.getGlyphs(params.stacks, callback); - } - - getResource(mapId: string, params: RequestParameters, callback: ResponseCallback): Cancelable { - return makeRequest(params, callback); - } - - _getSourceCache(source: string): SourceCache | void { - return this._otherSourceCaches[source]; - } - - _getLayerSourceCache(layer: StyleLayer): SourceCache | void { - return layer.type === 'symbol' ? - this._symbolSourceCaches[layer.source] : - this._otherSourceCaches[layer.source]; - } - - _getSourceCaches(source: string): Array { - const sourceCaches = []; - if (this._otherSourceCaches[source]) { - sourceCaches.push(this._otherSourceCaches[source]); - } - if (this._symbolSourceCaches[source]) { - sourceCaches.push(this._symbolSourceCaches[source]); - } - return sourceCaches; - } - - _isSourceCacheLoaded(source: string): boolean { - const sourceCaches = this._getSourceCaches(source); - if (sourceCaches.length === 0) { - this.fire(new ErrorEvent(new Error(`There is no source with ID '${source}'`))); - return false; - } - return sourceCaches.every(sc => sc.loaded()); - } - - has3DLayers(): boolean { - return this._num3DLayers > 0; - } - - hasSymbolLayers(): boolean { - return this._numSymbolLayers > 0; - } - - hasCircleLayers(): boolean { - return this._numCircleLayers > 0; - } - - _clearWorkerCaches() { - this.dispatcher.broadcast('clearCaches'); - } - - destroy() { - this._clearWorkerCaches(); - if (this.terrainSetForDrapingOnly()) { - delete this.terrain; - delete this.stylesheet.terrain; - } - } + getLayoutProperty( + layerId: string, + name: string, + ): ?PropertyValueSpecification { + const layer = this.getLayer(layerId); + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${layerId}' does not exist in the map's style.`), + ), + ); + return; + } + + return layer.getLayoutProperty(name); + } + + setPaintProperty( + layerId: string, + name: string, + value: any, + options: StyleSetterOptions = {}, + ) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire( + new ErrorEvent( + new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`,), + ), + ); + return; + } + + if (deepEqual(layer.getPaintProperty(name), value)) return; + + const requiresRelayout = layer.setPaintProperty(name, value, options); + if (requiresRelayout) { + this._updateLayer(layer); + } + + this._changed = true; + this._updatedPaintProps[layerId] = true; + } + + getPaintProperty( + layerId: string, + name: string, + ): void | TransitionSpecification | PropertyValueSpecification { + const layer = this.getLayer(layerId); + return layer && layer.getPaintProperty(name); + } + + setFeatureState( + target: { source: string, sourceLayer?: string, id: string | number }, + state: Object, + ) { + this._checkLoaded(); + const sourceId = target.source; + const sourceLayer = target.sourceLayer; + const source = this.getSource(sourceId); + + if (!source) { + this.fire( + new ErrorEvent( + new Error(`The source '${sourceId}' does not exist in the map's style.`,), + ), + ); + return; + } + const sourceType = source.type; + if (sourceType === 'geojson' && sourceLayer) { + this.fire( + new ErrorEvent( + new Error(`GeoJSON sources cannot have a sourceLayer parameter.`), + ), + ); + return; + } + if (sourceType === 'vector' && !sourceLayer) { + this.fire( + new ErrorEvent( + new Error(`The sourceLayer parameter must be provided for vector source types.`,), + ), + ); + return; + } + if (target.id === undefined) { + this.fire( + new ErrorEvent(new Error(`The feature id parameter must be provided.`)), + ); + } + + const sourceCaches = this._getSourceCaches(sourceId); + for (const sourceCache of sourceCaches) { + sourceCache.setFeatureState(sourceLayer, target.id, state); + } + } + + removeFeatureState( + target: { source: string, sourceLayer?: string, id?: string | number }, + key?: string, + ) { + this._checkLoaded(); + const sourceId = target.source; + const source = this.getSource(sourceId); + + if (!source) { + this.fire( + new ErrorEvent( + new Error(`The source '${sourceId}' does not exist in the map's style.`,), + ), + ); + return; + } + + const sourceType = source.type; + const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; + + if (sourceType === 'vector' && !sourceLayer) { + this.fire( + new ErrorEvent( + new Error(`The sourceLayer parameter must be provided for vector source types.`,), + ), + ); + return; + } + + if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) { + this.fire( + new ErrorEvent( + new Error(`A feature id is required to remove its specific state property.`,), + ), + ); + return; + } + + const sourceCaches = this._getSourceCaches(sourceId); + for (const sourceCache of sourceCaches) { + sourceCache.removeFeatureState(sourceLayer, target.id, key); + } + } + + getFeatureState( + target: { source: string, sourceLayer?: string, id: string | number }, + ): ?FeatureStates { + this._checkLoaded(); + const sourceId = target.source; + const sourceLayer = target.sourceLayer; + const source = this.getSource(sourceId); + + if (!source) { + this.fire( + new ErrorEvent( + new Error(`The source '${sourceId}' does not exist in the map's style.`,), + ), + ); + return; + } + const sourceType = source.type; + if (sourceType === 'vector' && !sourceLayer) { + this.fire( + new ErrorEvent( + new Error(`The sourceLayer parameter must be provided for vector source types.`,), + ), + ); + return; + } + if (target.id === undefined) { + this.fire( + new ErrorEvent(new Error(`The feature id parameter must be provided.`)), + ); + } + + const sourceCaches = this._getSourceCaches(sourceId); + return sourceCaches[0].getFeatureState(sourceLayer, target.id); + } + + getTransition(): TransitionSpecification { + return extend( + {duration: 300, delay: 0}, + this.stylesheet && this.stylesheet.transition, + ); + } + + serialize(): StyleSpecification { + const sources = {}; + for (const cacheId in this._sourceCaches) { + const source = this._sourceCaches[cacheId].getSource(); + if (!sources[source.id]) { + sources[source.id] = source.serialize(); + } + } + + return filterObject( + { + version: this.stylesheet.version, + name: this.stylesheet.name, + metadata: this.stylesheet.metadata, + light: this.stylesheet.light, + terrain: this.getTerrain() || undefined, + fog: this.stylesheet.fog, + center: this.stylesheet.center, + zoom: this.stylesheet.zoom, + bearing: this.stylesheet.bearing, + pitch: this.stylesheet.pitch, + sprite: this.stylesheet.sprite, + glyphs: this.stylesheet.glyphs, + transition: this.stylesheet.transition, + projection: this.stylesheet.projection, + sources, + layers: this._serializeLayers(this._order), + }, + value => { + return value !== undefined; + }, + ); + } + + _updateLayer(layer: StyleLayer) { + this._updatedLayers[layer.id] = true; + const sourceCache = this._getLayerSourceCache(layer); + if ( + layer.source && !this._updatedSources[layer.source] && + //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865) + sourceCache && + sourceCache.getSource().type !== 'raster' + ) { + this._updatedSources[layer.source] = 'reload'; + sourceCache.pause(); + } + this._changed = true; + layer.invalidateCompiledFilter(); + } + + _flattenAndSortRenderedFeatures(sourceResults: Array): Array { + // Feature order is complicated. + // The order between features in two 2D layers is always determined by layer order. + // The order between features in two 3D layers is always determined by depth. + // The order between a feature in a 2D layer and a 3D layer is tricky: + // Most often layer order determines the feature order in this case. If + // a line layer is above a extrusion layer the line feature will be rendered + // above the extrusion. If the line layer is below the extrusion layer, + // it will be rendered below it. + // + // There is a weird case though. + // You have layers in this order: extrusion_layer_a, line_layer, extrusion_layer_b + // Each layer has a feature that overlaps the other features. + // The feature in extrusion_layer_a is closer than the feature in extrusion_layer_b so it is rendered above. + // The feature in line_layer is rendered above extrusion_layer_a. + // This means that that the line_layer feature is above the extrusion_layer_b feature despite + // it being in an earlier layer. + + const isLayer3D = (layerId => this._layers[layerId].type === 'fill-extrusion'); + + const layerIndex = {}; + const features3D = []; + for (let l = this._order.length - 1; l >= 0; l--) { + const layerId = this._order[l]; + if (isLayer3D(layerId)) { + layerIndex[layerId] = l; + for (const sourceResult of sourceResults) { + const layerFeatures = sourceResult[layerId]; + if (layerFeatures) { + for (const featureWrapper of layerFeatures) { + features3D.push(featureWrapper); + } + } + } + } + } + + features3D.sort( + (a, b) => { + return b.intersectionZ - a.intersectionZ; + }, + ); + + const features = []; + for (let l = this._order.length - 1; l >= 0; l--) { + const layerId = this._order[l]; + + if (isLayer3D(layerId)) { + // add all 3D features that are in or above the current layer + for (let i = features3D.length - 1; i >= 0; i--) { + const topmost3D = features3D[i].feature; + if (layerIndex[topmost3D.layer.id] < l) break; + features.push(topmost3D); + features3D.pop(); + } + } else { + for (const sourceResult of sourceResults) { + const layerFeatures = sourceResult[layerId]; + if (layerFeatures) { + for (const featureWrapper of layerFeatures) { + features.push(featureWrapper.feature); + } + } + } + } + } + + return features; + } + + queryRenderedFeatures( + queryGeometry: PointLike | [PointLike, PointLike], + params: any, + transform: Transform, + ): Array { + if (params && params.filter) { + this._validate( + validateFilter, + 'queryRenderedFeatures.filter', + params.filter, + null, + params, + ); + } + + const includedSources = {}; + if (params && params.layers) { + if (!Array.isArray(params.layers)) { + this.fire( + new ErrorEvent(new Error('parameters.layers must be an Array.')), + ); + return []; + } + for (const layerId of params.layers) { + const layer = this._layers[layerId]; + if (!layer) { + // this layer is not in the style.layers array + this.fire( + new ErrorEvent( + new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`,), + ), + ); + return []; + } + includedSources[layer.source] = true; + } + } + + const sourceResults: Array = []; + + params.availableImages = this._availableImages; + + const has3DLayer = params && params.layers ? + params.layers.some( + layerId => { + const layer = this.getLayer(layerId); + return layer && layer.is3D(); + }, + ) : + this.has3DLayers(); + const queryGeometryStruct = QueryGeometry.createFromScreenPoints( + queryGeometry, + transform, + ); + + for (const id in this._sourceCaches) { + const sourceId = this._sourceCaches[id].getSource().id; + if (params.layers && !includedSources[sourceId]) continue; + sourceResults.push( + queryRenderedFeatures( + this._sourceCaches[id], + this._layers, + this._serializedLayers, + queryGeometryStruct, + params, + transform, + has3DLayer, + !!this.map._showQueryGeometry, + ), + ); + } + + if (this.placement) { + // If a placement has run, query against its CollisionIndex + // for symbol results, and treat it as an extra source to merge + sourceResults.push( + queryRenderedSymbols( + this._layers, + this._serializedLayers, + this._getLayerSourceCache.bind(this), + queryGeometryStruct.screenGeometry, + params, + this.placement.collisionIndex, + this.placement.retainedQueryData, + ), + ); + } + + return (this._flattenAndSortRenderedFeatures(sourceResults): any); + } + + querySourceFeatures( + sourceID: string, + params: ?{ sourceLayer: ?string, filter: ?Array, validate?: boolean }, + ): Array { + if (params && params.filter) { + this._validate( + validateFilter, + 'querySourceFeatures.filter', + params.filter, + null, + params, + ); + } + const sourceCaches = this._getSourceCaches(sourceID); + let results = []; + for (const sourceCache of sourceCaches) { + results = results.concat(querySourceFeatures(sourceCache, params)); + } + return results; + } + + addSourceType( + name: string, + SourceType: SourceClass, + callback: Callback, + ): void { + if (Style.getSourceType(name)) { + return callback( + new Error(`A source type called "${name}" already exists.`), + ); + } + + Style.setSourceType(name, SourceType); + + if (!SourceType.workerSourceURL) { + return callback(null, null); + } + + this.dispatcher.broadcast( + 'loadWorkerSource', + { + name, + url: SourceType.workerSourceURL, + }, + callback, + ); + } + + getLight(): LightSpecification { + return this.light.getLight(); + } + + setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + const light = this.light.getLight(); + let _update = false; + for (const key in lightOptions) { + if (!deepEqual(lightOptions[key], light[key])) { + _update = true; + break; + } + } + if (!_update) return; + + const parameters = this._setTransitionParameters( + {duration: 300, delay: 0}, + ); + + this.light.setLight(lightOptions, options); + this.light.updateTransitions(parameters); + } + + getTerrain(): ?TerrainSpecification { + return this.terrain && + this.terrain.drapeRenderMode === DrapeRenderMode.elevated ? + this.terrain.get() : + null; + } + + setTerrainForDraping() { + const mockTerrainOptions = {source: '', exaggeration: 0}; + this.setTerrain(mockTerrainOptions, DrapeRenderMode.deferred); + } + + // eslint-disable-next-line no-warning-comments + // TODO: generic approach for root level property: light, terrain, skybox. + // It is not done here to prevent rebasing issues. + setTerrain( + terrainOptions: ?TerrainSpecification, + drapeRenderMode: number = DrapeRenderMode.elevated, + ) { + this._checkLoaded(); + + // Disabling + if (!terrainOptions) { + delete this.terrain; + delete this.stylesheet.terrain; + this.dispatcher.broadcast('enableTerrain', false); + this._force3DLayerUpdate(); + this._markersNeedUpdate = true; + return; + } + + if (drapeRenderMode === DrapeRenderMode.elevated) { + // Input validation and source object unrolling + if (typeof terrainOptions.source === 'object') { + const id = 'terrain-dem-src'; + this.addSource(id, (terrainOptions.source: any)); + terrainOptions = clone(terrainOptions); + terrainOptions = (extend(terrainOptions, {source: id}): any); + } + + if (this._validate(validateTerrain, 'terrain', terrainOptions)) { + return; + } + } + + // Enabling + if ( + !this.terrain || + this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode + ) { + this._createTerrain(terrainOptions, drapeRenderMode); + } else { + // Updating + const terrain = this.terrain; + const currSpec = terrain.get(); + + for (const name of Object.keys(styleSpec.terrain)) { + // Fallback to use default style specification when the properties wasn't set + if ( + !terrainOptions.hasOwnProperty(name) && + !!styleSpec.terrain[name].default + ) { + terrainOptions[name] = styleSpec.terrain[name].default; + } + } + for (const key in terrainOptions) { + if (!deepEqual(terrainOptions[key], currSpec[key])) { + terrain.set(terrainOptions); + this.stylesheet.terrain = terrainOptions; + const parameters = this._setTransitionParameters({duration: 0}); + terrain.updateTransitions(parameters); + break; + } + } + } + + this._updateDrapeFirstLayers(); + this._markersNeedUpdate = true; + } + + _createFog(fogOptions: FogSpecification) { + const fog = this.fog = new Fog(fogOptions, this.map.transform); + this.stylesheet.fog = fogOptions; + const parameters = this._setTransitionParameters({duration: 0}); + fog.updateTransitions(parameters); + } + + _updateMarkersOpacity() { + if (this.map._markers.length === 0) { + return; + } + this.map._requestDomTask( + () => { + for (const marker of this.map._markers) { + marker._evaluateOpacity(); + } + }, + ); + } + + getFog(): ?FogSpecification { + return this.fog ? this.fog.get() : null; + } + + setFog(fogOptions: FogSpecification) { + this._checkLoaded(); + + if (!fogOptions) { + // Remove fog + delete this.fog; + delete this.stylesheet.fog; + this._markersNeedUpdate = true; + return; + } + + if (!this.fog) { + // Initialize Fog + this._createFog(fogOptions); + } else { + // Updating fog + const fog = this.fog; + const currSpec = fog.get(); + + // empty object should pass through to set default values + if (Object.keys(fogOptions).length === 0) fog.set(fogOptions); + + for (const key in fogOptions) { + if (!deepEqual(fogOptions[key], currSpec[key])) { + fog.set(fogOptions); + this.stylesheet.fog = fogOptions; + const parameters = this._setTransitionParameters({duration: 0}); + fog.updateTransitions(parameters); + break; + } + } + } + + this._markersNeedUpdate = true; + } + + _setTransitionParameters(transitionOptions: Object): TransitionParameters { + return { + now: browser.now(), + transition: extend(transitionOptions, this.stylesheet.transition), + }; + } + + _updateDrapeFirstLayers() { + if (!this.map._optimizeForTerrain || !this.terrain) { + return; + } + + const draped = this._order.filter( + id => { + return this.isLayerDraped(this._layers[id]); + }, + ); + + const nonDraped = this._order.filter( + id => { + return !this.isLayerDraped(this._layers[id]); + }, + ); + this._drapedFirstOrder = []; + this._drapedFirstOrder.push(...draped); + this._drapedFirstOrder.push(...nonDraped); + } + + _createTerrain(terrainOptions: TerrainSpecification, drapeRenderMode: number) { + const terrain = this.terrain = new Terrain(terrainOptions, drapeRenderMode); + this.stylesheet.terrain = terrainOptions; + this.dispatcher.broadcast('enableTerrain', !this.terrainSetForDrapingOnly()); + this._force3DLayerUpdate(); + const parameters = this._setTransitionParameters({duration: 0}); + terrain.updateTransitions(parameters); + } + + _force3DLayerUpdate() { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === 'fill-extrusion') { + this._updateLayer(layer); + } + } + } + + _forceSymbolLayerUpdate() { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === 'symbol') { + this._updateLayer(layer); + } + } + } + + _validate( + validate: Validator, + key: string, + value: any, + props: any, + options: { validate?: boolean } = {}, + ): boolean { + if (options && options.validate === false) { + return false; + } + return emitValidationErrors( + this, + validate.call( + validateStyle, + extend( + { + key, + style: this.serialize(), + value, + styleSpec, + }, + props, + ), + ), + ); + } + + _remove() { + if (this._request) { + this._request.cancel(); + this._request = null; + } + if (this._spriteRequest) { + this._spriteRequest.cancel(); + this._spriteRequest = null; + } + rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback); + for (const layerId in this._layers) { + const layer: StyleLayer = this._layers[layerId]; + layer.setEventedParent(null); + } + for (const id in this._sourceCaches) { + this._sourceCaches[id].clearTiles(); + this._sourceCaches[id].setEventedParent(null); + } + this.imageManager.setEventedParent(null); + this.setEventedParent(null); + this.dispatcher.remove(); + } + + _clearSource(id: string) { + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + sourceCache.clearTiles(); + } + } + + _reloadSource(id: string) { + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + sourceCache.resume(); + sourceCache.reload(); + } + } + + _reloadSources() { + for (const source of this._getSources()) { + if (source.reload) { + source.reload(); + } + } + } + + _updateSources(transform: Transform) { + for (const id in this._sourceCaches) { + this._sourceCaches[id].update(transform); + } + } + + _generateCollisionBoxes() { + for (const id in this._sourceCaches) { + const sourceCache = this._sourceCaches[id]; + sourceCache.resume(); + sourceCache.reload(); + } + } + + _updatePlacement( + transform: Transform, + showCollisionBoxes: boolean, + fadeDuration: number, + crossSourceCollisions: boolean, + forceFullPlacement: boolean = false, + ): boolean { + let symbolBucketsChanged = false; + let placementCommitted = false; + + const layerTiles = {}; + + for (const layerID of this._order) { + const styleLayer = this._layers[layerID]; + if (styleLayer.type !== 'symbol') continue; + + if (!layerTiles[styleLayer.source]) { + const sourceCache = this._getLayerSourceCache(styleLayer); + if (!sourceCache) continue; + layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true).map( + id => sourceCache.getTileByID(id), + ).sort( + (a, b) => b.tileID.overscaledZ - a.tileID.overscaledZ || + (a.tileID.isLessThan(b.tileID) ? -1 : 1), + ); + } + + const layerBucketsChanged = this.crossTileSymbolIndex.addLayer( + styleLayer, + layerTiles[styleLayer.source], + transform.center.lng, + transform.projection, + ); + symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged; + } + this.crossTileSymbolIndex.pruneUnusedLayers(this._order); + + // Anything that changes our "in progress" layer and tile indices requires us + // to start over. When we start over, we do a full placement instead of incremental + // to prevent starvation. + // We need to restart placement to keep layer indices in sync. + // Also force full placement when fadeDuration === 0 to ensure that newly loaded + // tiles will fully display symbols in their first frame + forceFullPlacement = forceFullPlacement || this._layerOrderChanged || + fadeDuration === 0; + + if (this._layerOrderChanged) { + this.fire(new Event('neworder')); + } + + if ( + forceFullPlacement || !this.pauseablePlacement || + this.pauseablePlacement.isDone() && + !this.placement.stillRecent(browser.now(), transform.zoom) + ) { + const fogState = this.fog && transform.projection.supportsFog ? + this.fog.state : + null; + this.pauseablePlacement = new PauseablePlacement( + transform, + this._order, + forceFullPlacement, + showCollisionBoxes, + fadeDuration, + crossSourceCollisions, + this.placement, + fogState, + ); + this._layerOrderChanged = false; + } + + if (this.pauseablePlacement.isDone()) { + // the last placement finished running, but the next one hasn’t + // started yet because of the `stillRecent` check immediately + // above, so mark it stale to ensure that we request another + // render frame + this.placement.setStale(); + } else { + this.pauseablePlacement.continuePlacement( + this._order, + this._layers, + layerTiles, + ); + + if (this.pauseablePlacement.isDone()) { + this.placement = this.pauseablePlacement.commit(browser.now()); + placementCommitted = true; + } + + if (symbolBucketsChanged) { + // since the placement gets split over multiple frames it is possible + // these buckets were processed before they were changed and so the + // placement is already stale while it is in progress + this.pauseablePlacement.placement.setStale(); + } + } + + if (placementCommitted || symbolBucketsChanged) { + for (const layerID of this._order) { + const styleLayer = this._layers[layerID]; + if (styleLayer.type !== 'symbol') continue; + this.placement.updateLayerOpacities( + styleLayer, + layerTiles[styleLayer.source], + ); + } + } + + // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols + const needsRerender = !this.pauseablePlacement.isDone() || + this.placement.hasTransitions(browser.now()); + return needsRerender; + } + + _releaseSymbolFadeTiles() { + for (const id in this._sourceCaches) { + this._sourceCaches[id].releaseSymbolFadeTiles(); + } + } + + // Callbacks from web workers + + getImages( + mapId: string, + params: { + icons: Array, + source: string, + tileID: OverscaledTileID, + type: string, + }, + callback: Callback<{ [_: string]: StyleImage }>, + ) { + this.imageManager.getImages(params.icons, callback); + + // Apply queued image changes before setting the tile's dependencies so that the tile + // is not reloaded unecessarily. Without this forced update the reload could happen in cases + // like this one: + // - icons contains "my-image" + // - imageManager.getImages(...) triggers `onstyleimagemissing` + // - the user adds "my-image" within the callback + // - addImage adds "my-image" to this._changedImages + // - the next frame triggers a reload of this tile even though it already has the latest version + this._updateTilesForChangedImages(); + + const setDependencies = ((sourceCache: SourceCache) => { + if (sourceCache) { + sourceCache.setDependencies( + params.tileID.key, + params.type, + params.icons, + ); + } + }); + setDependencies(this._otherSourceCaches[params.source]); + setDependencies(this._symbolSourceCaches[params.source]); + } + + getGlyphs( + mapId: string, + params: { stacks: { [_: string]: Array } }, + callback: Callback< + { + [_: string]: { + glyphs: { [_: number]: ?StyleGlyph }, + ascender?: number, + descender?: number, + }, + }, >, + ) { + this.glyphManager.getGlyphs(params.stacks, callback); + } + + getResource( + mapId: string, + params: RequestParameters, + callback: ResponseCallback, + ): Cancelable { + return makeRequest(params, callback); + } + + _getSourceCache(source: string): SourceCache | void { + return this._otherSourceCaches[source]; + } + + _getLayerSourceCache = (layer: StyleLayer): SourceCache | void => { + return layer.type === 'symbol' ? + this._symbolSourceCaches[layer.source] : + this._otherSourceCaches[layer.source]; + }; + + _getSourceCaches(source: string): Array { + const sourceCaches = []; + if (this._otherSourceCaches[source]) { + sourceCaches.push(this._otherSourceCaches[source]); + } + if (this._symbolSourceCaches[source]) { + sourceCaches.push(this._symbolSourceCaches[source]); + } + return sourceCaches; + } + + _isSourceCacheLoaded(source: string): boolean { + const sourceCaches = this._getSourceCaches(source); + if (sourceCaches.length === 0) { + this.fire( + new ErrorEvent(new Error(`There is no source with ID '${source}'`)), + ); + return false; + } + return sourceCaches.every(sc => sc.loaded()); + } + + has3DLayers(): boolean { + return this._num3DLayers > 0; + } + + hasSymbolLayers(): boolean { + return this._numSymbolLayers > 0; + } + + hasCircleLayers(): boolean { + return this._numCircleLayers > 0; + } + + _clearWorkerCaches() { + this.dispatcher.broadcast('clearCaches'); + } + + destroy() { + this._clearWorkerCaches(); + if (this.terrainSetForDrapingOnly()) { + delete this.terrain; + delete this.stylesheet.terrain; + } + } } Style.getSourceType = getSourceType; diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index 7dc7f4d7e19..9d7b9397825 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -189,18 +189,18 @@ export function validateCustomStyleLayer(layerObject: CustomLayerInterface): Val return errors; } -class CustomStyleLayer extends StyleLayer { +class CustomStyleLayer + extends StyleLayer { + implementation: CustomLayerInterface; - implementation: CustomLayerInterface; + constructor(implementation: CustomLayerInterface) { + super(implementation, {}); + this.implementation = implementation; + } - constructor(implementation: CustomLayerInterface) { - super(implementation, {}); - this.implementation = implementation; - } - - is3D(): boolean { - return this.implementation.renderingMode === '3d'; - } + is3D(): boolean { + return this.implementation.renderingMode === '3d'; + } hasOffscreenPass(): boolean { return this.implementation.prerender !== undefined; @@ -214,28 +214,28 @@ class CustomStyleLayer extends StyleLayer { return !!this.implementation.shouldRerenderTiles && this.implementation.shouldRerenderTiles(); } - recalculate() {} - updateTransitions() {} - hasTransition(): boolean { - return false; - } - - // $FlowFixMe[incompatible-extend] - CustomStyleLayer is not serializable - serialize() { - assert(false, "Custom layers cannot be serialized"); - } - - onAdd(map: Map) { - if (this.implementation.onAdd) { - this.implementation.onAdd(map, map.painter.context.gl); - } - } - - onRemove(map: Map) { - if (this.implementation.onRemove) { - this.implementation.onRemove(map, map.painter.context.gl); - } - } + recalculate() {} + updateTransitions() {} + hasTransition(): boolean { + return false; + } + + // $FlowFixMe[incompatible-extend] - CustomStyleLayer is not serializable + serialize() { + assert(false, "Custom layers cannot be serialized"); + } + + onAdd = (map: Map) => { + if (this.implementation.onAdd) { + this.implementation.onAdd(map, map.painter.context.gl); + } + }; + + onRemove(map: Map) { + if (this.implementation.onRemove) { + this.implementation.onRemove(map, map.painter.context.gl); + } + } } export default CustomStyleLayer; diff --git a/src/symbol/grid_index.js b/src/symbol/grid_index.js index 266c4a01975..b0f7b74368d 100644 --- a/src/symbol/grid_index.js +++ b/src/symbol/grid_index.js @@ -22,322 +22,453 @@ type GridItem = { * @private */ class GridIndex { - circleKeys: Array; - boxKeys: Array; - boxCells: Array>; - circleCells: Array>; - bboxes: Array; - circles: Array; - xCellCount: number; - yCellCount: number; - width: number; - height: number; - xScale: number; - yScale: number; - boxUid: number; - circleUid: number; + circleKeys: Array; + boxKeys: Array; + boxCells: Array>; + circleCells: Array>; + bboxes: Array; + circles: Array; + xCellCount: number; + yCellCount: number; + width: number; + height: number; + xScale: number; + yScale: number; + boxUid: number; + circleUid: number; - constructor (width: number, height: number, cellSize: number) { - const boxCells = this.boxCells = []; - const circleCells = this.circleCells = []; + constructor(width: number, height: number, cellSize: number) { + const boxCells = this.boxCells = []; + const circleCells = this.circleCells = []; - // More cells -> fewer geometries to check per cell, but items tend - // to be split across more cells. - // Sweet spot allows most small items to fit in one cell - this.xCellCount = Math.ceil(width / cellSize); - this.yCellCount = Math.ceil(height / cellSize); + // More cells -> fewer geometries to check per cell, but items tend + // to be split across more cells. + // Sweet spot allows most small items to fit in one cell + this.xCellCount = Math.ceil(width / cellSize); + this.yCellCount = Math.ceil(height / cellSize); - for (let i = 0; i < this.xCellCount * this.yCellCount; i++) { - boxCells.push([]); - circleCells.push([]); - } - this.circleKeys = []; - this.boxKeys = []; - this.bboxes = []; - this.circles = []; + for (let i = 0; i < this.xCellCount * this.yCellCount; i++) { + boxCells.push([]); + circleCells.push([]); + } + this.circleKeys = []; + this.boxKeys = []; + this.bboxes = []; + this.circles = []; - this.width = width; - this.height = height; - this.xScale = this.xCellCount / width; - this.yScale = this.yCellCount / height; - this.boxUid = 0; - this.circleUid = 0; - } + this.width = width; + this.height = height; + this.xScale = this.xCellCount / width; + this.yScale = this.yCellCount / height; + this.boxUid = 0; + this.circleUid = 0; + } - keysLength(): number { - return this.boxKeys.length + this.circleKeys.length; - } + keysLength(): number { + return this.boxKeys.length + this.circleKeys.length; + } - insert(key: any, x1: number, y1: number, x2: number, y2: number) { - this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++); - this.boxKeys.push(key); - this.bboxes.push(x1); - this.bboxes.push(y1); - this.bboxes.push(x2); - this.bboxes.push(y2); - } + insert(key: any, x1: number, y1: number, x2: number, y2: number) { + this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++); + this.boxKeys.push(key); + this.bboxes.push(x1); + this.bboxes.push(y1); + this.bboxes.push(x2); + this.bboxes.push(y2); + } - insertCircle(key: any, x: number, y: number, radius: number) { - // Insert circle into grid for all cells in the circumscribing square - // It's more than necessary (by a factor of 4/PI), but fast to insert - this._forEachCell(x - radius, y - radius, x + radius, y + radius, this._insertCircleCell, this.circleUid++); - this.circleKeys.push(key); - this.circles.push(x); - this.circles.push(y); - this.circles.push(radius); - } + insertCircle(key: any, x: number, y: number, radius: number) { + // Insert circle into grid for all cells in the circumscribing square + // It's more than necessary (by a factor of 4/PI), but fast to insert + this._forEachCell( + x - radius, + y - radius, + x + radius, + y + radius, + this._insertCircleCell, + this.circleUid++, + ); + this.circleKeys.push(key); + this.circles.push(x); + this.circles.push(y); + this.circles.push(radius); + } - _insertBoxCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number) { - this.boxCells[cellIndex].push(uid); - } + _insertBoxCell = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number, + ) => { + this.boxCells[cellIndex].push(uid); + }; - _insertCircleCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number) { - this.circleCells[cellIndex].push(uid); - } + _insertCircleCell = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number, + ) => { + this.circleCells[cellIndex].push(uid); + }; - _query(x1: number, y1: number, x2: number, y2: number, hitTest: boolean, predicate?: any): boolean | Array { - if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { - return hitTest ? false : []; - } - const result = []; - if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) { - if (hitTest) { - return true; - } - for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) { - result.push({ - key: this.boxKeys[boxUid], - x1: this.bboxes[boxUid * 4], - y1: this.bboxes[boxUid * 4 + 1], - x2: this.bboxes[boxUid * 4 + 2], - y2: this.bboxes[boxUid * 4 + 3] - }); - } - for (let circleUid = 0; circleUid < this.circleKeys.length; circleUid++) { - const x = this.circles[circleUid * 3]; - const y = this.circles[circleUid * 3 + 1]; - const radius = this.circles[circleUid * 3 + 2]; - result.push({ - key: this.circleKeys[circleUid], - x1: x - radius, - y1: y - radius, - x2: x + radius, - y2: y + radius - }); - } - return predicate ? result.filter(predicate) : result; - } else { - const queryArgs = { - hitTest, - seenUids: {box: {}, circle: {}} - }; - this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate); - return hitTest ? result.length > 0 : result; - } - } + _query( + x1: number, + y1: number, + x2: number, + y2: number, + hitTest: boolean, + predicate?: any, + ): boolean | Array { + if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { + return hitTest ? false : []; + } + const result = []; + if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) { + if (hitTest) { + return true; + } + for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) { + result.push( + { + key: this.boxKeys[boxUid], + x1: this.bboxes[boxUid * 4], + y1: this.bboxes[boxUid * 4 + 1], + x2: this.bboxes[boxUid * 4 + 2], + y2: this.bboxes[boxUid * 4 + 3], + }, + ); + } + for (let circleUid = 0; circleUid < this.circleKeys.length; circleUid++) { + const x = this.circles[circleUid * 3]; + const y = this.circles[circleUid * 3 + 1]; + const radius = this.circles[circleUid * 3 + 2]; + result.push( + { + key: this.circleKeys[circleUid], + x1: x - radius, + y1: y - radius, + x2: x + radius, + y2: y + radius, + }, + ); + } + return predicate ? result.filter(predicate) : result; + } else { + const queryArgs = { + hitTest, + seenUids: {box: {}, circle: {}}, + }; + this._forEachCell( + x1, + y1, + x2, + y2, + this._queryCell, + result, + queryArgs, + predicate, + ); + return hitTest ? result.length > 0 : result; + } + } - _queryCircle(x: number, y: number, radius: number, hitTest: boolean, predicate?: any): boolean | Array { - // Insert circle into grid for all cells in the circumscribing square - // It's more than necessary (by a factor of 4/PI), but fast to insert - const x1 = x - radius; - const x2 = x + radius; - const y1 = y - radius; - const y2 = y + radius; - if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { - return hitTest ? false : []; - } + _queryCircle( + x: number, + y: number, + radius: number, + hitTest: boolean, + predicate?: any, + ): boolean | Array { + // Insert circle into grid for all cells in the circumscribing square + // It's more than necessary (by a factor of 4/PI), but fast to insert + const x1 = x - radius; + const x2 = x + radius; + const y1 = y - radius; + const y2 = y + radius; + if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { + return hitTest ? false : []; + } - // Box query early exits if the bounding box is larger than the grid, but we don't do - // the equivalent calculation for circle queries because early exit is less likely - // and the calculation is more expensive - const result = []; - const queryArgs = { - hitTest, - circle: {x, y, radius}, - seenUids: {box: {}, circle: {}} - }; - this._forEachCell(x1, y1, x2, y2, this._queryCellCircle, result, queryArgs, predicate); - return hitTest ? result.length > 0 : result; - } + // Box query early exits if the bounding box is larger than the grid, but we don't do + // the equivalent calculation for circle queries because early exit is less likely + // and the calculation is more expensive + const result = []; + const queryArgs = { + hitTest, + circle: {x, y, radius}, + seenUids: {box: {}, circle: {}}, + }; + this._forEachCell( + x1, + y1, + x2, + y2, + this._queryCellCircle, + result, + queryArgs, + predicate, + ); + return hitTest ? result.length > 0 : result; + } - query(x1: number, y1: number, x2: number, y2: number, predicate?: any): Array { - return (this._query(x1, y1, x2, y2, false, predicate): any); - } + query( + x1: number, + y1: number, + x2: number, + y2: number, + predicate?: any, + ): Array { + return (this._query(x1, y1, x2, y2, false, predicate): any); + } - hitTest(x1: number, y1: number, x2: number, y2: number, predicate?: any): boolean { - return (this._query(x1, y1, x2, y2, true, predicate): any); - } + hitTest( + x1: number, + y1: number, + x2: number, + y2: number, + predicate?: any, + ): boolean { + return (this._query(x1, y1, x2, y2, true, predicate): any); + } - hitTestCircle(x: number, y: number, radius: number, predicate?: any): boolean { - return (this._queryCircle(x, y, radius, true, predicate): any); - } + hitTestCircle(x: number, y: number, radius: number, predicate?: any): boolean { + return (this._queryCircle(x, y, radius, true, predicate): any); + } - _queryCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any, predicate?: any): void | boolean { - const seenUids = queryArgs.seenUids; - const boxCell = this.boxCells[cellIndex]; - if (boxCell !== null) { - const bboxes = this.bboxes; - for (const boxUid of boxCell) { - if (!seenUids.box[boxUid]) { - seenUids.box[boxUid] = true; - const offset = boxUid * 4; - if ((x1 <= bboxes[offset + 2]) && - (y1 <= bboxes[offset + 3]) && - (x2 >= bboxes[offset + 0]) && - (y2 >= bboxes[offset + 1]) && - (!predicate || predicate(this.boxKeys[boxUid]))) { - if (queryArgs.hitTest) { - result.push(true); - return true; - } else { - result.push({ - key: this.boxKeys[boxUid], - x1: bboxes[offset], - y1: bboxes[offset + 1], - x2: bboxes[offset + 2], - y2: bboxes[offset + 3] - }); - } - } - } - } - } - const circleCell = this.circleCells[cellIndex]; - if (circleCell !== null) { - const circles = this.circles; - for (const circleUid of circleCell) { - if (!seenUids.circle[circleUid]) { - seenUids.circle[circleUid] = true; - const offset = circleUid * 3; - if (this._circleAndRectCollide( - circles[offset], - circles[offset + 1], - circles[offset + 2], - x1, - y1, - x2, - y2) && - (!predicate || predicate(this.circleKeys[circleUid]))) { - if (queryArgs.hitTest) { - result.push(true); - return true; - } else { - const x = circles[offset]; - const y = circles[offset + 1]; - const radius = circles[offset + 2]; - result.push({ - key: this.circleKeys[circleUid], - x1: x - radius, - y1: y - radius, - x2: x + radius, - y2: y + radius - }); - } - } - } - } - } - } + _queryCell = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any, + ): void | boolean => { + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if ( + x1 <= bboxes[offset + 2] && y1 <= bboxes[offset + 3] && + x2 >= bboxes[offset + 0] && + y2 >= bboxes[offset + 1] && + (!predicate || predicate(this.boxKeys[boxUid])) + ) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } else { + result.push( + { + key: this.boxKeys[boxUid], + x1: bboxes[offset], + y1: bboxes[offset + 1], + x2: bboxes[offset + 2], + y2: bboxes[offset + 3], + }, + ); + } + } + } + } + } + const circleCell = this.circleCells[cellIndex]; + if (circleCell !== null) { + const circles = this.circles; + for (const circleUid of circleCell) { + if (!seenUids.circle[circleUid]) { + seenUids.circle[circleUid] = true; + const offset = circleUid * 3; + if ( + this._circleAndRectCollide( + circles[offset], + circles[offset + 1], + circles[offset + 2], + x1, + y1, + x2, + y2, + ) && + (!predicate || predicate(this.circleKeys[circleUid])) + ) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } else { + const x = circles[offset]; + const y = circles[offset + 1]; + const radius = circles[offset + 2]; + result.push( + { + key: this.circleKeys[circleUid], + x1: x - radius, + y1: y - radius, + x2: x + radius, + y2: y + radius, + }, + ); + } + } + } + } + } + }; - _queryCellCircle(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any, predicate?: any): void | boolean { - const circle = queryArgs.circle; - const seenUids = queryArgs.seenUids; - const boxCell = this.boxCells[cellIndex]; - if (boxCell !== null) { - const bboxes = this.bboxes; - for (const boxUid of boxCell) { - if (!seenUids.box[boxUid]) { - seenUids.box[boxUid] = true; - const offset = boxUid * 4; - if (this._circleAndRectCollide( - circle.x, - circle.y, - circle.radius, - bboxes[offset + 0], - bboxes[offset + 1], - bboxes[offset + 2], - bboxes[offset + 3]) && - (!predicate || predicate(this.boxKeys[boxUid]))) { - result.push(true); - return true; - } - } - } - } + _queryCellCircle = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any, + ): void | boolean => { + const circle = queryArgs.circle; + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if ( + this._circleAndRectCollide( + circle.x, + circle.y, + circle.radius, + bboxes[offset + 0], + bboxes[offset + 1], + bboxes[offset + 2], + bboxes[offset + 3], + ) && + (!predicate || predicate(this.boxKeys[boxUid])) + ) { + result.push(true); + return true; + } + } + } + } - const circleCell = this.circleCells[cellIndex]; - if (circleCell !== null) { - const circles = this.circles; - for (const circleUid of circleCell) { - if (!seenUids.circle[circleUid]) { - seenUids.circle[circleUid] = true; - const offset = circleUid * 3; - if (this._circlesCollide( - circles[offset], - circles[offset + 1], - circles[offset + 2], - circle.x, - circle.y, - circle.radius) && - (!predicate || predicate(this.circleKeys[circleUid]))) { - result.push(true); - return true; - } - } - } - } - } + const circleCell = this.circleCells[cellIndex]; + if (circleCell !== null) { + const circles = this.circles; + for (const circleUid of circleCell) { + if (!seenUids.circle[circleUid]) { + seenUids.circle[circleUid] = true; + const offset = circleUid * 3; + if ( + this._circlesCollide( + circles[offset], + circles[offset + 1], + circles[offset + 2], + circle.x, + circle.y, + circle.radius, + ) && + (!predicate || predicate(this.circleKeys[circleUid])) + ) { + result.push(true); + return true; + } + } + } + } + }; - _forEachCell(x1: number, y1: number, x2: number, y2: number, fn: any, arg1: any, arg2?: any, predicate?: any) { - const cx1 = this._convertToXCellCoord(x1); - const cy1 = this._convertToYCellCoord(y1); - const cx2 = this._convertToXCellCoord(x2); - const cy2 = this._convertToYCellCoord(y2); + _forEachCell( + x1: number, + y1: number, + x2: number, + y2: number, + fn: any, + arg1: any, + arg2?: any, + predicate?: any, + ) { + const cx1 = this._convertToXCellCoord(x1); + const cy1 = this._convertToYCellCoord(y1); + const cx2 = this._convertToXCellCoord(x2); + const cy2 = this._convertToYCellCoord(y2); - for (let x = cx1; x <= cx2; x++) { - for (let y = cy1; y <= cy2; y++) { - const cellIndex = this.xCellCount * y + x; - if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) return; - } - } - } + for (let x = cx1; x <= cx2; x++) { + for (let y = cy1; y <= cy2; y++) { + const cellIndex = this.xCellCount * y + x; + if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) + return; + } + } + } - _convertToXCellCoord(x: number): number { - return Math.max(0, Math.min(this.xCellCount - 1, Math.floor(x * this.xScale))); - } + _convertToXCellCoord(x: number): number { + return Math.max( + 0, + Math.min(this.xCellCount - 1, Math.floor(x * this.xScale)), + ); + } - _convertToYCellCoord(y: number): number { - return Math.max(0, Math.min(this.yCellCount - 1, Math.floor(y * this.yScale))); - } + _convertToYCellCoord(y: number): number { + return Math.max( + 0, + Math.min(this.yCellCount - 1, Math.floor(y * this.yScale)), + ); + } - _circlesCollide(x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): boolean { - const dx = x2 - x1; - const dy = y2 - y1; - const bothRadii = r1 + r2; - return (bothRadii * bothRadii) > (dx * dx + dy * dy); - } + _circlesCollide( + x1: number, + y1: number, + r1: number, + x2: number, + y2: number, + r2: number, + ): boolean { + const dx = x2 - x1; + const dy = y2 - y1; + const bothRadii = r1 + r2; + return bothRadii * bothRadii > dx * dx + dy * dy; + } - _circleAndRectCollide(circleX: number, circleY: number, radius: number, x1: number, y1: number, x2: number, y2: number): boolean { - const halfRectWidth = (x2 - x1) / 2; - const distX = Math.abs(circleX - (x1 + halfRectWidth)); - if (distX > (halfRectWidth + radius)) { - return false; - } + _circleAndRectCollide( + circleX: number, + circleY: number, + radius: number, + x1: number, + y1: number, + x2: number, + y2: number, + ): boolean { + const halfRectWidth = (x2 - x1) / 2; + const distX = Math.abs(circleX - (x1 + halfRectWidth)); + if (distX > halfRectWidth + radius) { + return false; + } - const halfRectHeight = (y2 - y1) / 2; - const distY = Math.abs(circleY - (y1 + halfRectHeight)); - if (distY > (halfRectHeight + radius)) { - return false; - } + const halfRectHeight = (y2 - y1) / 2; + const distY = Math.abs(circleY - (y1 + halfRectHeight)); + if (distY > halfRectHeight + radius) { + return false; + } - if (distX <= halfRectWidth || distY <= halfRectHeight) { - return true; - } + if (distX <= halfRectWidth || distY <= halfRectHeight) { + return true; + } - const dx = distX - halfRectWidth; - const dy = distY - halfRectHeight; - return (dx * dx + dy * dy <= (radius * radius)); - } + const dx = distX - halfRectWidth; + const dy = distY - halfRectHeight; + return dx * dx + dy * dy <= radius * radius; + } } export default GridIndex; diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index c16005825fb..922359e4027 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -191,92 +191,117 @@ class ProxiedTileID extends OverscaledTileID { type OverlapStencilType = false | 'Clip' | 'Mask'; type FBO = {fb: Framebuffer, tex: Texture, dirty: boolean}; -export class Terrain extends Elevation { - terrainTileForTile: {[number | string]: Tile}; - prevTerrainTileForTile: {[number | string]: Tile}; - painter: Painter; - sourceCache: SourceCache; - gridBuffer: VertexBuffer; - gridIndexBuffer: IndexBuffer; - gridSegments: SegmentVector; - gridNoSkirtSegments: SegmentVector; - wireframeSegments: SegmentVector; - wireframeIndexBuffer: IndexBuffer; - proxiedCoords: {[string]: Array}; - proxyCoords: Array; - proxyToSource: {[number]: {[string]: Array}}; - proxySourceCache: ProxySourceCache; - renderingToTexture: boolean; - _style: Style; - _mockSourceCache: MockSourceCache; - orthoMatrix: Float32Array; - enabled: boolean; - renderMode: number; - - _visibleDemTiles: Array; - _sourceTilesOverlap: {[string]: boolean}; - _overlapStencilMode: StencilMode; - _overlapStencilType: OverlapStencilType; - _stencilRef: number; - - _exaggeration: number; - _depthFBO: ?Framebuffer; - _depthTexture: ?Texture; - _previousZoom: number; - _updateTimestamp: number; - _useVertexMorphing: boolean; - pool: Array; - renderedToTile: boolean; - _drapedRenderBatches: Array; - _sharedDepthStencil: ?WebGLRenderbuffer; - - _findCoveringTileCache: {[string]: {[number]: ?number}}; - - _tilesDirty: {[string]: {[number]: boolean}}; - _invalidateRenderCache: boolean; - - _emptyDepthBufferTexture: ?Texture; - _emptyDEMTexture: ?Texture; - _initializing: ?boolean; - _emptyDEMTextureDirty: ?boolean; - - constructor(painter: Painter, style: Style) { - super(); - this.painter = painter; - this.terrainTileForTile = {}; - this.prevTerrainTileForTile = {}; - - // Terrain rendering grid is 129x129 cell grid, made by 130x130 points. - // 130 vertices map to 128 DEM data + 1px padding on both sides. - // DEM texture is padded (1, 1, 1, 1) and padding pixels are backfilled - // by neighboring tile edges. This way we achieve tile stitching as - // edge vertices from neighboring tiles evaluate to the same 3D point. - const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid(GRID_DIM + 1); - const context = painter.context; - this.gridBuffer = context.createVertexBuffer(triangleGridArray, posAttributes.members); - this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices); - this.gridSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, triangleGridIndices.length); - this.gridNoSkirtSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, skirtIndicesOffset); - this.proxyCoords = []; - this.proxiedCoords = {}; - this._visibleDemTiles = []; - this._drapedRenderBatches = []; - this._sourceTilesOverlap = {}; - this.proxySourceCache = new ProxySourceCache(style.map); - this.orthoMatrix = mat4.create(); - const epsilon = this.painter.transform.projection.name === 'globe' ? .015 : 0; // Experimentally the smallest value to avoid rendering artifacts (https://github.com/mapbox/mapbox-gl-js/issues/11975) - mat4.ortho(this.orthoMatrix, epsilon, EXTENT, 0, EXTENT, 0, 1); - const gl = context.gl; - this._overlapStencilMode = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, 0, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); - this._previousZoom = painter.transform.zoom; - this.pool = []; - this._findCoveringTileCache = {}; - this._tilesDirty = {}; - this.style = style; - this._useVertexMorphing = true; - this._exaggeration = 1; - this._mockSourceCache = new MockSourceCache(style.map); - } +export class Terrain + extends Elevation { + terrainTileForTile: { [number | string]: Tile }; + prevTerrainTileForTile: { [number | string]: Tile }; + painter: Painter; + sourceCache: SourceCache; + gridBuffer: VertexBuffer; + gridIndexBuffer: IndexBuffer; + gridSegments: SegmentVector; + gridNoSkirtSegments: SegmentVector; + wireframeSegments: SegmentVector; + wireframeIndexBuffer: IndexBuffer; + proxiedCoords: { [string]: Array }; + proxyCoords: Array; + proxyToSource: { [number]: { [string]: Array } }; + proxySourceCache: ProxySourceCache; + renderingToTexture: boolean; + _style: Style; + _mockSourceCache: MockSourceCache; + orthoMatrix: Float32Array; + enabled: boolean; + renderMode: number; + + _visibleDemTiles: Array; + _sourceTilesOverlap: { [string]: boolean }; + _overlapStencilMode: StencilMode; + _overlapStencilType: OverlapStencilType; + _stencilRef: number; + + _exaggeration: number; + _depthFBO: ?Framebuffer; + _depthTexture: ?Texture; + _previousZoom: number; + _updateTimestamp: number; + _useVertexMorphing: boolean; + pool: Array; + renderedToTile: boolean; + _drapedRenderBatches: Array; + _sharedDepthStencil: ?WebGLRenderbuffer; + + _findCoveringTileCache: { [string]: { [number]: ?number } }; + + _tilesDirty: { [string]: { [number]: boolean } }; + _invalidateRenderCache: boolean; + + _emptyDepthBufferTexture: ?Texture; + _emptyDEMTexture: ?Texture; + _initializing: ?boolean; + _emptyDEMTextureDirty: ?boolean; + + constructor(painter: Painter, style: Style) { + super(); + this.painter = painter; + this.terrainTileForTile = {}; + this.prevTerrainTileForTile = {}; + + // Terrain rendering grid is 129x129 cell grid, made by 130x130 points. + // 130 vertices map to 128 DEM data + 1px padding on both sides. + // DEM texture is padded (1, 1, 1, 1) and padding pixels are backfilled + // by neighboring tile edges. This way we achieve tile stitching as + // edge vertices from neighboring tiles evaluate to the same 3D point. + const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid( + GRID_DIM + 1, + ); + const context = painter.context; + this.gridBuffer = context.createVertexBuffer( + triangleGridArray, + posAttributes.members, + ); + this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices); + this.gridSegments = SegmentVector.simpleSegment( + 0, + 0, + triangleGridArray.length, + triangleGridIndices.length, + ); + this.gridNoSkirtSegments = SegmentVector.simpleSegment( + 0, + 0, + triangleGridArray.length, + skirtIndicesOffset, + ); + this.proxyCoords = []; + this.proxiedCoords = {}; + this._visibleDemTiles = []; + this._drapedRenderBatches = []; + this._sourceTilesOverlap = {}; + this.proxySourceCache = new ProxySourceCache(style.map); + this.orthoMatrix = mat4.create(); + const epsilon = this.painter.transform.projection.name === 'globe' ? + .015 : + 0; // Experimentally the smallest value to avoid rendering artifacts (https://github.com/mapbox/mapbox-gl-js/issues/11975) + mat4.ortho(this.orthoMatrix, epsilon, EXTENT, 0, EXTENT, 0, 1); + const gl = context.gl; + this._overlapStencilMode = new StencilMode( + {func: gl.GEQUAL, mask: 0xFF}, + 0, + 0xFF, + gl.KEEP, + gl.KEEP, + gl.REPLACE, + ); + this._previousZoom = painter.transform.zoom; + this.pool = []; + this._findCoveringTileCache = {}; + this._tilesDirty = {}; + this.style = style; + this._useVertexMorphing = true; + this._exaggeration = 1; + this._mockSourceCache = new MockSourceCache(style.map); + } set style(style: Style) { style.on('data', this._onStyleDataEvent.bind(this)); @@ -288,383 +313,447 @@ export class Terrain extends Elevation { }); } - /* + /* * Validate terrain and update source cache used for elevation. * Explicitly pass transform to update elevation (Transform.updateElevation) * before using transform for source cache update. */ - update(style: Style, transform: Transform, adaptCameraAltitude: boolean) { - if (style && style.terrain) { - if (this._style !== style) { - this.style = style; - } - this.enabled = true; - const terrainProps = style.terrain.properties; - const isDrapeModeDeferred = style.terrain.drapeRenderMode === DrapeRenderMode.deferred; - this.sourceCache = isDrapeModeDeferred ? this._mockSourceCache : - ((style._getSourceCache(terrainProps.get('source')): any): SourceCache); - this._exaggeration = terrainProps.get('exaggeration'); - - const updateSourceCache = () => { - if (this.sourceCache.used) { - warnOnce(`Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + - 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.'); - } - // Lower tile zoom is sufficient for terrain, given the size of terrain grid. - const scaledDemTileSize = this.getScaledDemTileSize(); - // Dem tile needs to be parent or at least of the same zoom level as proxy tile. - // Tile cover roundZoom behavior is set to the same as for proxy (false) in SourceCache.update(). - this.sourceCache.update(transform, scaledDemTileSize, true); - // As a result of update, we get new set of tiles: reset lookup cache. - this.resetTileLookupCache(this.sourceCache.id); - }; - - if (!this.sourceCache.usedForTerrain) { - // Init cache entry. - this.resetTileLookupCache(this.sourceCache.id); - // When toggling terrain on/off load available terrain tiles from cache - // before reading elevation at center. - this.sourceCache.usedForTerrain = true; - updateSourceCache(); - this._initializing = true; - } - - updateSourceCache(); - // Camera gets constrained over terrain. Issue constrainCameraOverTerrain = true - // here to cover potential under terrain situation on data, style, or other camera changes. - transform.updateElevation(true, adaptCameraAltitude); - - // Reset tile lookup cache and update draped tiles coordinates. - this.resetTileLookupCache(this.proxySourceCache.id); - this.proxySourceCache.update(transform); - - this._emptyDEMTextureDirty = true; - } else { - this._disable(); - } - } - - resetTileLookupCache(sourceCacheID: string) { - this._findCoveringTileCache[sourceCacheID] = {}; - } - - getScaledDemTileSize(): number { - const demScale = this.sourceCache.getSource().tileSize / GRID_DIM; - const proxyTileSize = this.proxySourceCache.getSource().tileSize; - return demScale * proxyTileSize; - } - - _checkRenderCacheEfficiency() { - const renderCacheInfo = this.renderCacheEfficiency(this._style); - if (this._style.map._optimizeForTerrain) { - assert(renderCacheInfo.efficiency === 100); - } else if (renderCacheInfo.efficiency !== 100) { - warnOnce(`Terrain render cache efficiency is not optimal (${renderCacheInfo.efficiency}%) and performance + update(style: Style, transform: Transform, adaptCameraAltitude: boolean) { + if (style && style.terrain) { + if (this._style !== style) { + this.style = style; + } + this.enabled = true; + const terrainProps = style.terrain.properties; + const isDrapeModeDeferred = style.terrain.drapeRenderMode === DrapeRenderMode.deferred; + this.sourceCache = isDrapeModeDeferred ? + this._mockSourceCache : + ((style._getSourceCache(terrainProps.get('source')): any): SourceCache); + this._exaggeration = terrainProps.get('exaggeration'); + + const updateSourceCache = (() => { + if (this.sourceCache.used) { + warnOnce( + `Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.', + ); + } + // Lower tile zoom is sufficient for terrain, given the size of terrain grid. + const scaledDemTileSize = this.getScaledDemTileSize(); + // Dem tile needs to be parent or at least of the same zoom level as proxy tile. + // Tile cover roundZoom behavior is set to the same as for proxy (false) in SourceCache.update(). + this.sourceCache.update(transform, scaledDemTileSize, true); + // As a result of update, we get new set of tiles: reset lookup cache. + this.resetTileLookupCache(this.sourceCache.id); + }); + + if (!this.sourceCache.usedForTerrain) { + // Init cache entry. + this.resetTileLookupCache(this.sourceCache.id); + // When toggling terrain on/off load available terrain tiles from cache + // before reading elevation at center. + this.sourceCache.usedForTerrain = true; + updateSourceCache(); + this._initializing = true; + } + + updateSourceCache(); + // Camera gets constrained over terrain. Issue constrainCameraOverTerrain = true + // here to cover potential under terrain situation on data, style, or other camera changes. + transform.updateElevation(true, adaptCameraAltitude); + + // Reset tile lookup cache and update draped tiles coordinates. + this.resetTileLookupCache(this.proxySourceCache.id); + this.proxySourceCache.update(transform); + + this._emptyDEMTextureDirty = true; + } else { + this._disable(); + } + } + + resetTileLookupCache(sourceCacheID: string) { + this._findCoveringTileCache[sourceCacheID] = {}; + } + + getScaledDemTileSize(): number { + const demScale = this.sourceCache.getSource().tileSize / GRID_DIM; + const proxyTileSize = this.proxySourceCache.getSource().tileSize; + return demScale * proxyTileSize; + } + + _checkRenderCacheEfficiency = () => { + const renderCacheInfo = this.renderCacheEfficiency(this._style); + if (this._style.map._optimizeForTerrain) { + assert(renderCacheInfo.efficiency === 100); + } else if (renderCacheInfo.efficiency !== 100) { + warnOnce(`Terrain render cache efficiency is not optimal (${renderCacheInfo.efficiency}%) and performance may be affected negatively, consider placing all background, fill and line layers before layer - with id '${renderCacheInfo.firstUndrapedLayer}' or create a map using optimizeForTerrain: true option.`); - } - } - - _onStyleDataEvent(event: any) { - if (event.coord && event.dataType === 'source') { - this._clearRenderCacheForTile(event.sourceCacheId, event.coord); - } else if (event.dataType === 'style') { - this._invalidateRenderCache = true; - } - } - - // Terrain - _disable() { - if (!this.enabled) return; - this.enabled = false; - this._sharedDepthStencil = undefined; - this.proxySourceCache.deallocRenderCache(); - if (this._style) { - for (const id in this._style._sourceCaches) { - this._style._sourceCaches[id].usedForTerrain = false; - } - } - } - - destroy() { - this._disable(); - if (this._emptyDEMTexture) this._emptyDEMTexture.destroy(); - if (this._emptyDepthBufferTexture) this._emptyDepthBufferTexture.destroy(); - this.pool.forEach(fbo => fbo.fb.destroy()); - this.pool = []; - if (this._depthFBO) { - this._depthFBO.destroy(); - this._depthFBO = undefined; - this._depthTexture = undefined; - } - } - - // Implements Elevation::_source. - _source(): ?SourceCache { - return this.enabled ? this.sourceCache : null; - } - - isUsingMockSource(): boolean { - return this.sourceCache === this._mockSourceCache; - } - - // Implements Elevation::exaggeration. - exaggeration(): number { - return this._exaggeration; - } - - get visibleDemTiles(): Array { - return this._visibleDemTiles; - } - - get drapeBufferSize(): [number, number] { - const extent = this.proxySourceCache.getSource().tileSize * 2; // *2 is to avoid upscaling bitmap on zoom. - return [extent, extent]; - } - - set useVertexMorphing(enable: boolean) { - this._useVertexMorphing = enable; - } - - // For every renderable coordinate in every source cache, assign one proxy - // tile (see _setupProxiedCoordsForOrtho). Mapping of source tile to proxy - // tile is modeled by ProxiedTileID. In general case, source and proxy tile - // are of different zoom: ProxiedTileID.projMatrix models ortho, scale and - // translate from source to proxy. This matrix is used when rendering source - // tile to proxy tile's texture. - // One proxy tile can have multiple source tiles, or pieces of source tiles, - // that get rendered to it. - // For each proxy tile we assign one terrain tile (_assignTerrainTiles). The - // terrain tile provides elevation data when rendering (draping) proxy tile - // texture over terrain grid. - updateTileBinding(sourcesCoords: {[string]: Array}) { - if (!this.enabled) return; - this.prevTerrainTileForTile = this.terrainTileForTile; - - const psc = this.proxySourceCache; - const tr = this.painter.transform; - if (this._initializing) { - // Don't activate terrain until center tile gets loaded. - this._initializing = tr._centerAltitude === 0 && this.getAtPointOrZero(MercatorCoordinate.fromLngLat(tr.center), -1) === -1; - this._emptyDEMTextureDirty = !this._initializing; - } - - const coords = this.proxyCoords = psc.getIds().map((id) => { - const tileID = psc.getTileByID(id).tileID; - tileID.projMatrix = tr.calculateProjMatrix(tileID.toUnwrapped()); - return tileID; - }); - sortByDistanceToCamera(coords, this.painter); - this._previousZoom = tr.zoom; - - const previousProxyToSource = this.proxyToSource || {}; - this.proxyToSource = {}; - coords.forEach((tileID) => { - this.proxyToSource[tileID.key] = {}; - }); - - this.terrainTileForTile = {}; - const sourceCaches = this._style._sourceCaches; - for (const id in sourceCaches) { - const sourceCache = sourceCaches[id]; - if (!sourceCache.used) continue; - if (sourceCache !== this.sourceCache) this.resetTileLookupCache(sourceCache.id); - this._setupProxiedCoordsForOrtho(sourceCache, sourcesCoords[id], previousProxyToSource); - if (sourceCache.usedForTerrain) continue; - const coordinates = sourcesCoords[id]; - if (sourceCache.getSource().reparseOverscaled) { - // Do this for layers that are not rasterized to proxy tile. - this._assignTerrainTiles(coordinates); - } - } - - // Background has no source. Using proxy coords with 1-1 ortho (this.proxiedCoords[psc.id]) - // when rendering background to proxy tiles. - this.proxiedCoords[psc.id] = coords.map(tileID => new ProxiedTileID(tileID, tileID.key, this.orthoMatrix)); - this._assignTerrainTiles(coords); - this._prepareDEMTextures(); - this._setupDrapedRenderBatches(); - this._initFBOPool(); - this._setupRenderCache(previousProxyToSource); - - this.renderingToTexture = false; - this._updateTimestamp = browser.now(); - - // Gather all dem tiles that are assigned to proxy tiles - const visibleKeys = {}; - this._visibleDemTiles = []; - - for (const id of this.proxyCoords) { - const demTile = this.terrainTileForTile[id.key]; - if (!demTile) - continue; - const key = demTile.tileID.key; - if (key in visibleKeys) - continue; - this._visibleDemTiles.push(demTile); - visibleKeys[key] = key; - } - - } - - _assignTerrainTiles(coords: Array) { - if (this._initializing) return; - coords.forEach((tileID) => { - if (this.terrainTileForTile[tileID.key]) return; - const demTile = this._findTileCoveringTileID(tileID, this.sourceCache); - if (demTile) this.terrainTileForTile[tileID.key] = demTile; - }); - } - - _prepareDEMTextures() { - const context = this.painter.context; - const gl = context.gl; - for (const key in this.terrainTileForTile) { - const tile = this.terrainTileForTile[key]; - const dem = tile.dem; - if (dem && (!tile.demTexture || tile.needsDEMTextureUpload)) { - context.activeTexture.set(gl.TEXTURE1); - prepareDEMTexture(this.painter, tile, dem); - } - } - } - - _prepareDemTileUniforms(proxyTile: Tile, demTile: ?Tile, uniforms: UniformValues, uniformSuffix: ?string): boolean { - if (!demTile || demTile.demTexture == null) - return false; - - assert(demTile.dem); - const proxyId = proxyTile.tileID.canonical; - const demId = demTile.tileID.canonical; - const demScaleBy = Math.pow(2, demId.z - proxyId.z); - const suffix = uniformSuffix || ""; - uniforms[`u_dem_tl${suffix}`] = [proxyId.x * demScaleBy % 1, proxyId.y * demScaleBy % 1]; - uniforms[`u_dem_scale${suffix}`] = demScaleBy; - return true; - } - - get emptyDEMTexture(): Texture { - return !this._emptyDEMTextureDirty && this._emptyDEMTexture ? - this._emptyDEMTexture : this._updateEmptyDEMTexture(); - } - - get emptyDepthBufferTexture(): Texture { - const context = this.painter.context; - const gl = context.gl; - if (!this._emptyDepthBufferTexture) { - const image = new RGBAImage({width: 1, height: 1}, Uint8Array.of(255, 255, 255, 255)); - this._emptyDepthBufferTexture = new Texture(context, image, gl.RGBA, {premultiply: false}); - } - return this._emptyDepthBufferTexture; - } - - _getLoadedAreaMinimum(): number { - let nonzero = 0; - const min = this._visibleDemTiles.reduce((acc, tile) => { - if (!tile.dem) return acc; - const m = tile.dem.tree.minimums[0]; - acc += m; - if (m > 0) nonzero++; - return acc; - }, 0); - return nonzero ? min / nonzero : 0; - } - - _updateEmptyDEMTexture(): Texture { - const context = this.painter.context; - const gl = context.gl; - context.activeTexture.set(gl.TEXTURE2); - - const min = this._getLoadedAreaMinimum(); - const image = new RGBAImage( - {width: 1, height: 1}, - new Uint8Array(DEMData.pack(min, ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding)) - ); - - this._emptyDEMTextureDirty = false; - let texture = this._emptyDEMTexture; - if (!texture) { - texture = this._emptyDEMTexture = new Texture(context, image, gl.RGBA, {premultiply: false}); - } else { - texture.update(image, {premultiply: false}); - } - return texture; - } - - // useDepthForOcclusion: Pre-rendered depth to texture (this._depthTexture) is - // used to hide (actually moves all object's vertices out of viewport). - // useMeterToDem: u_meter_to_dem uniform is not used for all terrain programs, - // optimization to avoid unnecessary computation and upload. - setupElevationDraw(tile: Tile, program: Program<*>, - options?: { - useDepthForOcclusion?: boolean, - useMeterToDem?: boolean, - labelPlaneMatrixInv?: ?Float32Array, - morphing?: { srcDemTile: Tile, dstDemTile: Tile, phase: number }, - useDenormalizedUpVectorScale?: boolean - }) { - const context = this.painter.context; - const gl = context.gl; - const uniforms = defaultTerrainUniforms(((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding); - uniforms['u_dem_size'] = this.sourceCache.getSource().tileSize; - uniforms['u_exaggeration'] = this.exaggeration(); - - let demTile = null; - let prevDemTile = null; - let morphingPhase = 1.0; - - if (options && options.morphing && this._useVertexMorphing) { - const srcTile = options.morphing.srcDemTile; - const dstTile = options.morphing.dstDemTile; - morphingPhase = options.morphing.phase; - - if (srcTile && dstTile) { - if (this._prepareDemTileUniforms(tile, srcTile, uniforms, "_prev")) - prevDemTile = srcTile; - if (this._prepareDemTileUniforms(tile, dstTile, uniforms)) - demTile = dstTile; - } - } - - if (prevDemTile && demTile) { - // Both DEM textures are expected to be correctly set if geomorphing is enabled - context.activeTexture.set(gl.TEXTURE2); - (demTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); - context.activeTexture.set(gl.TEXTURE4); - (prevDemTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); - - uniforms["u_dem_lerp"] = morphingPhase; - } else { - demTile = this.terrainTileForTile[tile.tileID.key]; - context.activeTexture.set(gl.TEXTURE2); - const demTexture = this._prepareDemTileUniforms(tile, demTile, uniforms) ? - (demTile.demTexture: any) : this.emptyDEMTexture; - demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - } - - context.activeTexture.set(gl.TEXTURE3); - if (options && options.useDepthForOcclusion) { - if (this._depthTexture) this._depthTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - if (this._depthFBO) uniforms['u_depth_size_inv'] = [1 / this._depthFBO.width, 1 / this._depthFBO.height]; - } else { - this.emptyDepthBufferTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - uniforms['u_depth_size_inv'] = [1, 1]; - } - - if (options && options.useMeterToDem && demTile) { - const meterToDEM = (1 << demTile.tileID.canonical.z) * mercatorZfromAltitude(1, this.painter.transform.center.lat) * this.sourceCache.getSource().tileSize; - uniforms['u_meter_to_dem'] = meterToDEM; - } - if (options && options.labelPlaneMatrixInv) { - uniforms['u_label_plane_matrix_inv'] = options.labelPlaneMatrixInv; - } - program.setTerrainUniformValues(context, uniforms); - - if (this.painter.transform.projection.name === 'globe') { - const globeUniforms = this.globeUniformValues(this.painter.transform, tile.tileID.canonical, options && options.useDenormalizedUpVectorScale); - program.setGlobeUniformValues(context, globeUniforms); - } - } + with id '${renderCacheInfo.firstUndrapedLayer}' or create a map using optimizeForTerrain: true option.`,); + } + }; + + _onStyleDataEvent = (event: any) => { + if (event.coord && event.dataType === 'source') { + this._clearRenderCacheForTile(event.sourceCacheId, event.coord); + } else if (event.dataType === 'style') { + this._invalidateRenderCache = true; + } + }; + + // Terrain + _disable() { + if (!this.enabled) return; + this.enabled = false; + this._sharedDepthStencil = undefined; + this.proxySourceCache.deallocRenderCache(); + if (this._style) { + for (const id in this._style._sourceCaches) { + this._style._sourceCaches[id].usedForTerrain = false; + } + } + } + + destroy() { + this._disable(); + if (this._emptyDEMTexture) this._emptyDEMTexture.destroy(); + if (this._emptyDepthBufferTexture) this._emptyDepthBufferTexture.destroy(); + this.pool.forEach(fbo => fbo.fb.destroy()); + this.pool = []; + if (this._depthFBO) { + this._depthFBO.destroy(); + this._depthFBO = undefined; + this._depthTexture = undefined; + } + } + + // Implements Elevation::_source. + _source(): ?SourceCache { + return this.enabled ? this.sourceCache : null; + } + + isUsingMockSource(): boolean { + return this.sourceCache === this._mockSourceCache; + } + + // Implements Elevation::exaggeration. + exaggeration(): number { + return this._exaggeration; + } + + get visibleDemTiles(): Array { + return this._visibleDemTiles; + } + + get drapeBufferSize(): [number, number] { + const extent = this.proxySourceCache.getSource().tileSize * 2; // *2 is to avoid upscaling bitmap on zoom. + return [extent, extent]; + } + + set useVertexMorphing(enable: boolean) { + this._useVertexMorphing = enable; + } + + // For every renderable coordinate in every source cache, assign one proxy + // tile (see _setupProxiedCoordsForOrtho). Mapping of source tile to proxy + // tile is modeled by ProxiedTileID. In general case, source and proxy tile + // are of different zoom: ProxiedTileID.projMatrix models ortho, scale and + // translate from source to proxy. This matrix is used when rendering source + // tile to proxy tile's texture. + // One proxy tile can have multiple source tiles, or pieces of source tiles, + // that get rendered to it. + // For each proxy tile we assign one terrain tile (_assignTerrainTiles). The + // terrain tile provides elevation data when rendering (draping) proxy tile + // texture over terrain grid. + updateTileBinding(sourcesCoords: { [string]: Array }) { + if (!this.enabled) return; + this.prevTerrainTileForTile = this.terrainTileForTile; + + const psc = this.proxySourceCache; + const tr = this.painter.transform; + if (this._initializing) { + // Don't activate terrain until center tile gets loaded. + this._initializing = tr._centerAltitude === 0 && + this.getAtPointOrZero(MercatorCoordinate.fromLngLat(tr.center), -1) === -1; + this._emptyDEMTextureDirty = !this._initializing; + } + + const coords = this.proxyCoords = psc.getIds().map( + id => { + const tileID = psc.getTileByID(id).tileID; + tileID.projMatrix = tr.calculateProjMatrix(tileID.toUnwrapped()); + return tileID; + }, + ); + sortByDistanceToCamera(coords, this.painter); + this._previousZoom = tr.zoom; + + const previousProxyToSource = this.proxyToSource || {}; + this.proxyToSource = {}; + coords.forEach( + tileID => { + this.proxyToSource[tileID.key] = {}; + }, + ); + + this.terrainTileForTile = {}; + const sourceCaches = this._style._sourceCaches; + for (const id in sourceCaches) { + const sourceCache = sourceCaches[id]; + if (!sourceCache.used) continue; + if (sourceCache !== this.sourceCache) + this.resetTileLookupCache(sourceCache.id); + this._setupProxiedCoordsForOrtho( + sourceCache, + sourcesCoords[id], + previousProxyToSource, + ); + if (sourceCache.usedForTerrain) continue; + const coordinates = sourcesCoords[id]; + if (sourceCache.getSource().reparseOverscaled) { + // Do this for layers that are not rasterized to proxy tile. + this._assignTerrainTiles(coordinates); + } + } + + // Background has no source. Using proxy coords with 1-1 ortho (this.proxiedCoords[psc.id]) + // when rendering background to proxy tiles. + this.proxiedCoords[psc.id] = coords.map( + tileID => new ProxiedTileID(tileID, tileID.key, this.orthoMatrix), + ); + this._assignTerrainTiles(coords); + this._prepareDEMTextures(); + this._setupDrapedRenderBatches(); + this._initFBOPool(); + this._setupRenderCache(previousProxyToSource); + + this.renderingToTexture = false; + this._updateTimestamp = browser.now(); + + // Gather all dem tiles that are assigned to proxy tiles + const visibleKeys = {}; + this._visibleDemTiles = []; + + for (const id of this.proxyCoords) { + const demTile = this.terrainTileForTile[id.key]; + if (!demTile) continue; + const key = demTile.tileID.key; + if (key in visibleKeys) continue; + this._visibleDemTiles.push(demTile); + visibleKeys[key] = key; + } + } + + _assignTerrainTiles(coords: Array) { + if (this._initializing) return; + coords.forEach( + tileID => { + if (this.terrainTileForTile[tileID.key]) return; + const demTile = this._findTileCoveringTileID(tileID, this.sourceCache); + if (demTile) this.terrainTileForTile[tileID.key] = demTile; + }, + ); + } + + _prepareDEMTextures() { + const context = this.painter.context; + const gl = context.gl; + for (const key in this.terrainTileForTile) { + const tile = this.terrainTileForTile[key]; + const dem = tile.dem; + if (dem && (!tile.demTexture || tile.needsDEMTextureUpload)) { + context.activeTexture.set(gl.TEXTURE1); + prepareDEMTexture(this.painter, tile, dem); + } + } + } + + _prepareDemTileUniforms( + proxyTile: Tile, + demTile: ?Tile, + uniforms: UniformValues, + uniformSuffix: ?string, + ): boolean { + if (!demTile || demTile.demTexture == null) return false; + + assert(demTile.dem); + const proxyId = proxyTile.tileID.canonical; + const demId = demTile.tileID.canonical; + const demScaleBy = Math.pow(2, demId.z - proxyId.z); + const suffix = uniformSuffix || ""; + uniforms[`u_dem_tl${suffix}`] = [ + proxyId.x * demScaleBy % 1, + proxyId.y * demScaleBy % 1, + ]; + uniforms[`u_dem_scale${suffix}`] = demScaleBy; + return true; + } + + get emptyDEMTexture(): Texture { + return !this._emptyDEMTextureDirty && this._emptyDEMTexture ? + this._emptyDEMTexture : + this._updateEmptyDEMTexture(); + } + + get emptyDepthBufferTexture(): Texture { + const context = this.painter.context; + const gl = context.gl; + if (!this._emptyDepthBufferTexture) { + const image = new RGBAImage( + {width: 1, height: 1}, + Uint8Array.of(255, 255, 255, 255), + ); + this._emptyDepthBufferTexture = new Texture( + context, + image, + gl.RGBA, + {premultiply: false}, + ); + } + return this._emptyDepthBufferTexture; + } + + _getLoadedAreaMinimum(): number { + let nonzero = 0; + const min = this._visibleDemTiles.reduce( + (acc, tile) => { + if (!tile.dem) return acc; + const m = tile.dem.tree.minimums[0]; + acc += m; + if (m > 0) nonzero++; + return acc; + }, + 0, + ); + return nonzero ? min / nonzero : 0; + } + + _updateEmptyDEMTexture(): Texture { + const context = this.painter.context; + const gl = context.gl; + context.activeTexture.set(gl.TEXTURE2); + + const min = this._getLoadedAreaMinimum(); + const image = new RGBAImage( + {width: 1, height: 1}, + new Uint8Array( + DEMData.pack( + min, + ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding, + ), + ), + ); + + this._emptyDEMTextureDirty = false; + let texture = this._emptyDEMTexture; + if (!texture) { + texture = this._emptyDEMTexture = new Texture( + context, + image, + gl.RGBA, + {premultiply: false}, + ); + } else { + texture.update(image, {premultiply: false}); + } + return texture; + } + + // useDepthForOcclusion: Pre-rendered depth to texture (this._depthTexture) is + // used to hide (actually moves all object's vertices out of viewport). + // useMeterToDem: u_meter_to_dem uniform is not used for all terrain programs, + // optimization to avoid unnecessary computation and upload. + setupElevationDraw( + tile: Tile, + program: Program<*>, + options?: { + useDepthForOcclusion?: boolean, + useMeterToDem?: boolean, + labelPlaneMatrixInv?: ?Float32Array, + morphing?: { srcDemTile: Tile, dstDemTile: Tile, phase: number }, + useDenormalizedUpVectorScale?: boolean, + }, + ) { + const context = this.painter.context; + const gl = context.gl; + const uniforms = defaultTerrainUniforms( + ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding, + ); + uniforms['u_dem_size'] = this.sourceCache.getSource().tileSize; + uniforms['u_exaggeration'] = this.exaggeration(); + + let demTile = null; + let prevDemTile = null; + let morphingPhase = 1.0; + + if (options && options.morphing && this._useVertexMorphing) { + const srcTile = options.morphing.srcDemTile; + const dstTile = options.morphing.dstDemTile; + morphingPhase = options.morphing.phase; + + if (srcTile && dstTile) { + if (this._prepareDemTileUniforms(tile, srcTile, uniforms, "_prev")) + prevDemTile = srcTile; + if (this._prepareDemTileUniforms(tile, dstTile, uniforms)) + demTile = dstTile; + } + } + + if (prevDemTile && demTile) { + // Both DEM textures are expected to be correctly set if geomorphing is enabled + context.activeTexture.set(gl.TEXTURE2); + (demTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + context.activeTexture.set(gl.TEXTURE4); + (prevDemTile.demTexture: any).bind( + gl.NEAREST, + gl.CLAMP_TO_EDGE, + gl.NEAREST, + ); + + uniforms["u_dem_lerp"] = morphingPhase; + } else { + demTile = this.terrainTileForTile[tile.tileID.key]; + context.activeTexture.set(gl.TEXTURE2); + const demTexture = this._prepareDemTileUniforms(tile, demTile, uniforms) ? + (demTile.demTexture: any) : + this.emptyDEMTexture; + demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + } + + context.activeTexture.set(gl.TEXTURE3); + if (options && options.useDepthForOcclusion) { + if (this._depthTexture) + this._depthTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + if (this._depthFBO) + uniforms['u_depth_size_inv'] = [ + 1 / this._depthFBO.width, + 1 / this._depthFBO.height, + ]; + } else { + this.emptyDepthBufferTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + uniforms['u_depth_size_inv'] = [1, 1]; + } + + if (options && options.useMeterToDem && demTile) { + const meterToDEM = (1 << demTile.tileID.canonical.z) * mercatorZfromAltitude( + 1, + this.painter.transform.center.lat, + ) * this.sourceCache.getSource().tileSize; + uniforms['u_meter_to_dem'] = meterToDEM; + } + if (options && options.labelPlaneMatrixInv) { + uniforms['u_label_plane_matrix_inv'] = options.labelPlaneMatrixInv; + } + program.setTerrainUniformValues(context, uniforms); + + if (this.painter.transform.projection.name === 'globe') { + const globeUniforms = this.globeUniformValues( + this.painter.transform, + tile.tileID.canonical, + options && options.useDenormalizedUpVectorScale, + ); + program.setGlobeUniformValues(context, globeUniforms); + } + } globeUniformValues(tr: Transform, id: CanonicalTileID, useDenormalizedUpVectorScale: ?boolean): UniformValues { const projection = tr.projection; @@ -677,265 +766,308 @@ export class Terrain extends Elevation { }; } - renderToBackBuffer(accumulatedDrapes: Array) { - const painter = this.painter; - const context = this.painter.context; - - if (accumulatedDrapes.length === 0) { - return; - } - - context.bindFramebuffer.set(null); - context.viewport.set([0, 0, painter.width, painter.height]); - - painter.gpuTimingDeferredRenderStart(); - - this.renderingToTexture = false; - drawTerrainRaster(painter, this, this.proxySourceCache, accumulatedDrapes, this._updateTimestamp); - this.renderingToTexture = true; - - painter.gpuTimingDeferredRenderEnd(); - - accumulatedDrapes.splice(0, accumulatedDrapes.length); - } - - // For each proxy tile, render all layers until the non-draped layer (and - // render the tile to the screen) before advancing to the next proxy tile. - // Returns the last drawn index that is used as a start - // layer for interleaved draped rendering. - // Apart to layer-by-layer rendering used in 2D, here we have proxy-tile-by-proxy-tile - // rendering. - renderBatch(startLayerIndex: number): number { - if (this._drapedRenderBatches.length === 0) { - return startLayerIndex + 1; - } - - this.renderingToTexture = true; - const painter = this.painter; - const context = this.painter.context; - const psc = this.proxySourceCache; - const proxies = this.proxiedCoords[psc.id]; - - // Consume batch of sequential drape layers and move next - const drapedLayerBatch = this._drapedRenderBatches.shift(); - assert(drapedLayerBatch.start === startLayerIndex); - - const accumulatedDrapes = []; - const layerIds = painter.style.order; - - let poolIndex = 0; - for (const proxy of proxies) { - // bind framebuffer and assign texture to the tile (texture used in drawTerrainRaster). - const tile = psc.getTileByID(proxy.proxyTileKey); - const renderCacheIndex = psc.proxyCachedFBO[proxy.key] ? psc.proxyCachedFBO[proxy.key][startLayerIndex] : undefined; - const fbo = renderCacheIndex !== undefined ? psc.renderCache[renderCacheIndex] : this.pool[poolIndex++]; - const useRenderCache = renderCacheIndex !== undefined; - - tile.texture = fbo.tex; - - if (useRenderCache && !fbo.dirty) { - // Use cached render from previous pass, no need to render again. - accumulatedDrapes.push(tile.tileID); - continue; - } - - context.bindFramebuffer.set(fbo.fb.framebuffer); - this.renderedToTile = false; // reset flag. - if (fbo.dirty) { - // Clear on start. - context.clear({color: Color.transparent, stencil: 0}); - fbo.dirty = false; - } - - let currentStencilSource; // There is no need to setup stencil for the same source for consecutive layers. - for (let j = drapedLayerBatch.start; j <= drapedLayerBatch.end; ++j) { - const layer = painter.style._layers[layerIds[j]]; - const hidden = layer.isHidden(painter.transform.zoom); - assert(this._style.isLayerDraped(layer) || hidden); - if (hidden) continue; - - const sourceCache = painter.style._getLayerSourceCache(layer); - const proxiedCoords = sourceCache ? this.proxyToSource[proxy.key][sourceCache.id] : [proxy]; - if (!proxiedCoords) continue; // when tile is not loaded yet for the source cache. - - const coords = ((proxiedCoords: any): Array); - context.viewport.set([0, 0, fbo.fb.width, fbo.fb.height]); - if (currentStencilSource !== (sourceCache ? sourceCache.id : null)) { - this._setupStencil(fbo, proxiedCoords, layer, sourceCache); - currentStencilSource = sourceCache ? sourceCache.id : null; - } - painter.renderLayer(painter, sourceCache, layer, coords); - } - - if (this.renderedToTile) { - fbo.dirty = true; - accumulatedDrapes.push(tile.tileID); - } else if (!useRenderCache) { - --poolIndex; - assert(poolIndex >= 0); - } - if (poolIndex === FBO_POOL_SIZE) { - poolIndex = 0; - this.renderToBackBuffer(accumulatedDrapes); - } - } - - // Reset states and render last drapes - this.renderToBackBuffer(accumulatedDrapes); - this.renderingToTexture = false; - - context.bindFramebuffer.set(null); - context.viewport.set([0, 0, painter.width, painter.height]); - - return drapedLayerBatch.end + 1; - } - - postRender() { - // Make sure we consumed all the draped terrain batches at this point - assert(this._drapedRenderBatches.length === 0); - } - - renderCacheEfficiency(style: Style): Object { - const layerCount = style.order.length; - - if (layerCount === 0) { - return {efficiency: 100.0}; - } - - let uncacheableLayerCount = 0; - let drapedLayerCount = 0; - let reachedUndrapedLayer = false; - let firstUndrapedLayer; - - for (let i = 0; i < layerCount; ++i) { - const layer = style._layers[style.order[i]]; - if (!this._style.isLayerDraped(layer)) { - if (!reachedUndrapedLayer) { - reachedUndrapedLayer = true; - firstUndrapedLayer = layer.id; - } - } else { - if (reachedUndrapedLayer) { - ++uncacheableLayerCount; - } - ++drapedLayerCount; - } - } - - if (drapedLayerCount === 0) { - return {efficiency: 100.0}; - } - - return {efficiency: (1.0 - uncacheableLayerCount / drapedLayerCount) * 100.0, firstUndrapedLayer}; - } - - getMinElevationBelowMSL(): number { - let min = 0.0; - // The maximum DEM error in meters to be conservative (SRTM). - const maxDEMError = 30.0; - this._visibleDemTiles.filter(tile => tile.dem).forEach(tile => { - const minMaxTree = (tile.dem: any).tree; - min = Math.min(min, minMaxTree.minimums[0]); - }); - return min === 0.0 ? min : (min - maxDEMError) * this._exaggeration; - } - - // Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray. - // x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters. - raycast(pos: Vec3, dir: Vec3, exaggeration: number): ?number { - if (!this._visibleDemTiles) - return null; - - // Perform initial raycasts against root nodes of the available dem tiles - // and use this information to sort them from closest to furthest. - const preparedTiles = this._visibleDemTiles.filter(tile => tile.dem).map(tile => { - const id = tile.tileID; - const tiles = 1 << id.overscaledZ; - const {x, y} = id.canonical; - - // Compute tile boundaries in mercator coordinates - const minx = x / tiles; - const maxx = (x + 1) / tiles; - const miny = y / tiles; - const maxy = (y + 1) / tiles; - const tree = (tile.dem: any).tree; - - return { - minx, miny, maxx, maxy, - t: tree.raycastRoot(minx, miny, maxx, maxy, pos, dir, exaggeration), - tile - }; - }); - - preparedTiles.sort((a, b) => { - const at = a.t !== null ? a.t : Number.MAX_VALUE; - const bt = b.t !== null ? b.t : Number.MAX_VALUE; - return at - bt; - }); - - for (const obj of preparedTiles) { - if (obj.t == null) - return null; - - // Perform more accurate raycast against the dem tree. First intersection is the closest on - // as all tiles are sorted from closest to furthest - const tree = (obj.tile.dem: any).tree; - const t = tree.raycast(obj.minx, obj.miny, obj.maxx, obj.maxy, pos, dir, exaggeration); - - if (t != null) - return t; - } - - return null; - } - - _createFBO(): FBO { - const painter = this.painter; - const context = painter.context; - const gl = context.gl; - const bufferSize = this.drapeBufferSize; - context.activeTexture.set(gl.TEXTURE0); - const tex = new Texture(context, {width: bufferSize[0], height: bufferSize[1], data: null}, gl.RGBA); - tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); - const fb = context.createFramebuffer(bufferSize[0], bufferSize[1], false); - fb.colorAttachment.set(tex.texture); - fb.depthAttachment = new DepthStencilAttachment(context, fb.framebuffer); - - if (this._sharedDepthStencil === undefined) { - this._sharedDepthStencil = context.createRenderbuffer(context.gl.DEPTH_STENCIL, bufferSize[0], bufferSize[1]); - this._stencilRef = 0; - fb.depthAttachment.set(this._sharedDepthStencil); - context.clear({stencil: 0}); - } else { - fb.depthAttachment.set(this._sharedDepthStencil); - } - - if (context.extTextureFilterAnisotropic && !context.extTextureFilterAnisotropicForceOff) { - gl.texParameterf(gl.TEXTURE_2D, - context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, - context.extTextureFilterAnisotropicMax); - } - - return {fb, tex, dirty: false}; - } - - _initFBOPool() { - while (this.pool.length < Math.min(FBO_POOL_SIZE, this.proxyCoords.length)) { - this.pool.push(this._createFBO()); - } - } - - _shouldDisableRenderCache(): boolean { - // Disable render caches on dynamic events due to fading or transitioning. - if (this._style.light && this._style.light.hasTransition()) { - return true; - } - - for (const id in this._style._sourceCaches) { - if (this._style._sourceCaches[id].hasTransition()) { - return true; - } - } + renderToBackBuffer(accumulatedDrapes: Array) { + const painter = this.painter; + const context = this.painter.context; + + if (accumulatedDrapes.length === 0) { + return; + } + + context.bindFramebuffer.set(null); + context.viewport.set([0, 0, painter.width, painter.height]); + + painter.gpuTimingDeferredRenderStart(); + + this.renderingToTexture = false; + drawTerrainRaster( + painter, + this, + this.proxySourceCache, + accumulatedDrapes, + this._updateTimestamp, + ); + this.renderingToTexture = true; + + painter.gpuTimingDeferredRenderEnd(); + + accumulatedDrapes.splice(0, accumulatedDrapes.length); + } + + // For each proxy tile, render all layers until the non-draped layer (and + // render the tile to the screen) before advancing to the next proxy tile. + // Returns the last drawn index that is used as a start + // layer for interleaved draped rendering. + // Apart to layer-by-layer rendering used in 2D, here we have proxy-tile-by-proxy-tile + // rendering. + renderBatch(startLayerIndex: number): number { + if (this._drapedRenderBatches.length === 0) { + return startLayerIndex + 1; + } + + this.renderingToTexture = true; + const painter = this.painter; + const context = this.painter.context; + const psc = this.proxySourceCache; + const proxies = this.proxiedCoords[psc.id]; + + // Consume batch of sequential drape layers and move next + const drapedLayerBatch = this._drapedRenderBatches.shift(); + assert(drapedLayerBatch.start === startLayerIndex); + + const accumulatedDrapes = []; + const layerIds = painter.style.order; + + let poolIndex = 0; + for (const proxy of proxies) { + // bind framebuffer and assign texture to the tile (texture used in drawTerrainRaster). + const tile = psc.getTileByID(proxy.proxyTileKey); + const renderCacheIndex = psc.proxyCachedFBO[proxy.key] ? + psc.proxyCachedFBO[proxy.key][startLayerIndex] : + undefined; + const fbo = renderCacheIndex !== undefined ? + psc.renderCache[renderCacheIndex] : + this.pool[poolIndex++]; + const useRenderCache = renderCacheIndex !== undefined; + + tile.texture = fbo.tex; + + if (useRenderCache && !fbo.dirty) { + // Use cached render from previous pass, no need to render again. + accumulatedDrapes.push(tile.tileID); + continue; + } + + context.bindFramebuffer.set(fbo.fb.framebuffer); + this.renderedToTile = false; // reset flag. + if (fbo.dirty) { + // Clear on start. + context.clear({color: Color.transparent, stencil: 0}); + fbo.dirty = false; + } + + let currentStencilSource; // There is no need to setup stencil for the same source for consecutive layers. + for (let j = drapedLayerBatch.start; j <= drapedLayerBatch.end; ++j) { + const layer = painter.style._layers[layerIds[j]]; + const hidden = layer.isHidden(painter.transform.zoom); + assert(this._style.isLayerDraped(layer) || hidden); + if (hidden) continue; + + const sourceCache = painter.style._getLayerSourceCache(layer); + const proxiedCoords = sourceCache ? + this.proxyToSource[proxy.key][sourceCache.id] : + [proxy]; + if (!proxiedCoords) + continue; // when tile is not loaded yet for the source cache. + + const coords = ((proxiedCoords: any): Array); + context.viewport.set([0, 0, fbo.fb.width, fbo.fb.height]); + if (currentStencilSource !== (sourceCache ? sourceCache.id : null)) { + this._setupStencil(fbo, proxiedCoords, layer, sourceCache); + currentStencilSource = sourceCache ? sourceCache.id : null; + } + painter.renderLayer(painter, sourceCache, layer, coords); + } + + if (this.renderedToTile) { + fbo.dirty = true; + accumulatedDrapes.push(tile.tileID); + } else if (!useRenderCache) { + --poolIndex; + assert(poolIndex >= 0); + } + if (poolIndex === FBO_POOL_SIZE) { + poolIndex = 0; + this.renderToBackBuffer(accumulatedDrapes); + } + } + + // Reset states and render last drapes + this.renderToBackBuffer(accumulatedDrapes); + this.renderingToTexture = false; + + context.bindFramebuffer.set(null); + context.viewport.set([0, 0, painter.width, painter.height]); + + return drapedLayerBatch.end + 1; + } + + postRender() { + // Make sure we consumed all the draped terrain batches at this point + assert(this._drapedRenderBatches.length === 0); + } + + renderCacheEfficiency(style: Style): Object { + const layerCount = style.order.length; + + if (layerCount === 0) { + return {efficiency: 100.0}; + } + + let uncacheableLayerCount = 0; + let drapedLayerCount = 0; + let reachedUndrapedLayer = false; + let firstUndrapedLayer; + + for (let i = 0; i < layerCount; ++i) { + const layer = style._layers[style.order[i]]; + if (!this._style.isLayerDraped(layer)) { + if (!reachedUndrapedLayer) { + reachedUndrapedLayer = true; + firstUndrapedLayer = layer.id; + } + } else { + if (reachedUndrapedLayer) { + ++uncacheableLayerCount; + } + ++drapedLayerCount; + } + } + + if (drapedLayerCount === 0) { + return {efficiency: 100.0}; + } + + return { + efficiency: (1.0 - uncacheableLayerCount / drapedLayerCount) * 100.0, + firstUndrapedLayer, + }; + } + + getMinElevationBelowMSL(): number { + let min = 0.0; + // The maximum DEM error in meters to be conservative (SRTM). + const maxDEMError = 30.0; + this._visibleDemTiles.filter(tile => tile.dem).forEach( + tile => { + const minMaxTree = (tile.dem: any).tree; + min = Math.min(min, minMaxTree.minimums[0]); + }, + ); + return min === 0.0 ? min : (min - maxDEMError) * this._exaggeration; + } + + // Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray. + // x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters. + raycast(pos: Vec3, dir: Vec3, exaggeration: number): ?number { + if (!this._visibleDemTiles) return null; + + // Perform initial raycasts against root nodes of the available dem tiles + // and use this information to sort them from closest to furthest. + const preparedTiles = this._visibleDemTiles.filter(tile => tile.dem).map( + tile => { + const id = tile.tileID; + const tiles = 1 << id.overscaledZ; + const {x, y} = id.canonical; + + // Compute tile boundaries in mercator coordinates + const minx = x / tiles; + const maxx = (x + 1) / tiles; + const miny = y / tiles; + const maxy = (y + 1) / tiles; + const tree = (tile.dem: any).tree; + + return { + minx, + miny, + maxx, + maxy, + t: tree.raycastRoot(minx, miny, maxx, maxy, pos, dir, exaggeration), + tile, + }; + }, + ); + + preparedTiles.sort( + (a, b) => { + const at = a.t !== null ? a.t : Number.MAX_VALUE; + const bt = b.t !== null ? b.t : Number.MAX_VALUE; + return at - bt; + }, + ); + + for (const obj of preparedTiles) { + if (obj.t == null) return null; + + // Perform more accurate raycast against the dem tree. First intersection is the closest on + // as all tiles are sorted from closest to furthest + const tree = (obj.tile.dem: any).tree; + const t = tree.raycast( + obj.minx, + obj.miny, + obj.maxx, + obj.maxy, + pos, + dir, + exaggeration, + ); + + if (t != null) return t; + } + + return null; + } + + _createFBO(): FBO { + const painter = this.painter; + const context = painter.context; + const gl = context.gl; + const bufferSize = this.drapeBufferSize; + context.activeTexture.set(gl.TEXTURE0); + const tex = new Texture( + context, + {width: bufferSize[0], height: bufferSize[1], data: null}, + gl.RGBA, + ); + tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + const fb = context.createFramebuffer(bufferSize[0], bufferSize[1], false); + fb.colorAttachment.set(tex.texture); + fb.depthAttachment = new DepthStencilAttachment(context, fb.framebuffer); + + if (this._sharedDepthStencil === undefined) { + this._sharedDepthStencil = context.createRenderbuffer( + context.gl.DEPTH_STENCIL, + bufferSize[0], + bufferSize[1], + ); + this._stencilRef = 0; + fb.depthAttachment.set(this._sharedDepthStencil); + context.clear({stencil: 0}); + } else { + fb.depthAttachment.set(this._sharedDepthStencil); + } + + if ( + context.extTextureFilterAnisotropic && + !context.extTextureFilterAnisotropicForceOff + ) { + gl.texParameterf( + gl.TEXTURE_2D, + context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, + context.extTextureFilterAnisotropicMax, + ); + } + + return {fb, tex, dirty: false}; + } + + _initFBOPool() { + while (this.pool.length < Math.min(FBO_POOL_SIZE, this.proxyCoords.length)) { + this.pool.push(this._createFBO()); + } + } + + _shouldDisableRenderCache(): boolean { + // Disable render caches on dynamic events due to fading or transitioning. + if (this._style.light && this._style.light.hasTransition()) { + return true; + } + + for (const id in this._style._sourceCaches) { + if (this._style._sourceCaches[id].hasTransition()) { + return true; + } + } const isTransitioning = id => { const layer = this._style._layers[id]; @@ -1013,498 +1145,633 @@ export class Terrain extends Elevation { const coords = ((proxiedCoords: any): Array); if (!coords) continue; - for (const coord of coords) { - const tile = sourceCache.getTile(coord); - const parent = sourceCache.findLoadedParent(coord, 0); - const fade = rasterFade(tile, parent, sourceCache, this.painter.transform, fadeDuration); - const isFading = fade.opacity !== 1 || fade.mix !== 0; - if (isFading) { - this._clearRenderCacheForTile(sourceCache.id, coord); - } - } - } - } - } - - _setupDrapedRenderBatches() { - const layerIds = this._style.order; - const layerCount = layerIds.length; - if (layerCount === 0) { - return; - } - - const batches = []; - - let currentLayer = 0; - let layer = this._style._layers[layerIds[currentLayer]]; - while (!this._style.isLayerDraped(layer) && layer.isHidden(this.painter.transform.zoom) && ++currentLayer < layerCount) { - layer = this._style._layers[layerIds[currentLayer]]; - } - - let batchStart; - for (; currentLayer < layerCount; ++currentLayer) { - const layer = this._style._layers[layerIds[currentLayer]]; - if (layer.isHidden(this.painter.transform.zoom)) { - continue; - } - if (!this._style.isLayerDraped(layer)) { - if (batchStart !== undefined) { - batches.push({start: batchStart, end: currentLayer - 1}); - batchStart = undefined; - } - continue; - } - if (batchStart === undefined) { - batchStart = currentLayer; - } - } - - if (batchStart !== undefined) { - batches.push({start: batchStart, end: currentLayer - 1}); - } - - if (this._style.map._optimizeForTerrain) { - // Draped first approach should result in a single or no batch - assert(batches.length === 1 || batches.length === 0); - } - - this._drapedRenderBatches = batches; - } - - _setupRenderCache(previousProxyToSource: {[number]: {[string]: Array}}) { - const psc = this.proxySourceCache; - if (this._shouldDisableRenderCache() || this._invalidateRenderCache) { - this._invalidateRenderCache = false; - if (psc.renderCache.length > psc.renderCachePool.length) { - const used = ((Object.values(psc.proxyCachedFBO): any): Array<{[string | number]: number}>); - psc.proxyCachedFBO = {}; - for (let i = 0; i < used.length; ++i) { - const fbos = ((Object.values(used[i]): any): Array); - psc.renderCachePool.push(...fbos); - } - assert(psc.renderCache.length === psc.renderCachePool.length); - } - return; - } + for (const coord of coords) { + const tile = sourceCache.getTile(coord); + const parent = sourceCache.findLoadedParent(coord, 0); + const fade = rasterFade( + tile, + parent, + sourceCache, + this.painter.transform, + fadeDuration, + ); + const isFading = fade.opacity !== 1 || fade.mix !== 0; + if (isFading) { + this._clearRenderCacheForTile(sourceCache.id, coord); + } + } + } + } + } + + _setupDrapedRenderBatches() { + const layerIds = this._style.order; + const layerCount = layerIds.length; + if (layerCount === 0) { + return; + } + + const batches = []; + + let currentLayer = 0; + let layer = this._style._layers[layerIds[currentLayer]]; + while ( + !this._style.isLayerDraped(layer) && + layer.isHidden(this.painter.transform.zoom) && + ++currentLayer < layerCount + ) { + layer = this._style._layers[layerIds[currentLayer]]; + } + + let batchStart; + for (; currentLayer < layerCount; ++currentLayer) { + const layer = this._style._layers[layerIds[currentLayer]]; + if (layer.isHidden(this.painter.transform.zoom)) { + continue; + } + if (!this._style.isLayerDraped(layer)) { + if (batchStart !== undefined) { + batches.push({start: batchStart, end: currentLayer - 1}); + batchStart = undefined; + } + continue; + } + if (batchStart === undefined) { + batchStart = currentLayer; + } + } + + if (batchStart !== undefined) { + batches.push({start: batchStart, end: currentLayer - 1}); + } + + if (this._style.map._optimizeForTerrain) { + // Draped first approach should result in a single or no batch + assert(batches.length === 1 || batches.length === 0); + } + + this._drapedRenderBatches = batches; + } + + _setupRenderCache( + previousProxyToSource: { [number]: { [string]: Array } }, + ) { + const psc = this.proxySourceCache; + if (this._shouldDisableRenderCache() || this._invalidateRenderCache) { + this._invalidateRenderCache = false; + if (psc.renderCache.length > psc.renderCachePool.length) { + const used = ((Object.values(psc.proxyCachedFBO): any): Array< + { [string | number]: number }, >); + psc.proxyCachedFBO = {}; + for (let i = 0; i < used.length; ++i) { + const fbos = ((Object.values(used[i]): any): Array); + psc.renderCachePool.push(...fbos); + } + assert(psc.renderCache.length === psc.renderCachePool.length); + } + return; + } this._clearRasterLayersFromRenderCache(); - const coords = this.proxyCoords; - const dirty = this._tilesDirty; - for (let i = coords.length - 1; i >= 0; i--) { - const proxy = coords[i]; - const tile = psc.getTileByID(proxy.key); - - if (psc.proxyCachedFBO[proxy.key] !== undefined) { - assert(tile.texture); - const prev = previousProxyToSource[proxy.key]; - assert(prev); - // Reuse previous render from cache if there was no change of - // content that was used to render proxy tile. - const current = this.proxyToSource[proxy.key]; - let equal = 0; - for (const source in current) { - const tiles = current[source]; - const prevTiles = prev[source]; - if (!prevTiles || prevTiles.length !== tiles.length || - tiles.some((t, index) => - (t !== prevTiles[index] || - (dirty[source] && dirty[source].hasOwnProperty(t.key) - ))) - ) { - equal = -1; - break; - } - ++equal; - } - // dirty === false: doesn't need to be rendered to, just use cached render. - for (const proxyFBO in psc.proxyCachedFBO[proxy.key]) { - psc.renderCache[psc.proxyCachedFBO[proxy.key][proxyFBO]].dirty = equal < 0 || equal !== Object.values(prev).length; - } - } - } - - const sortedRenderBatches = [...this._drapedRenderBatches]; - sortedRenderBatches.sort((batchA, batchB) => { - const batchASize = batchA.end - batchA.start; - const batchBSize = batchB.end - batchB.start; - return batchBSize - batchASize; - }); - - for (const batch of sortedRenderBatches) { - for (const id of coords) { - if (psc.proxyCachedFBO[id.key]) { - continue; - } - - // Assign renderCache FBO if there are available FBOs in pool. - let index = psc.renderCachePool.pop(); - if (index === undefined && psc.renderCache.length < RENDER_CACHE_MAX_SIZE) { - index = psc.renderCache.length; - psc.renderCache.push(this._createFBO()); - } - if (index !== undefined) { - psc.proxyCachedFBO[id.key] = {}; - psc.proxyCachedFBO[id.key][batch.start] = index; - psc.renderCache[index].dirty = true; // needs to be rendered to. - } - } - } - this._tilesDirty = {}; - } - - _setupStencil(fbo: FBO, proxiedCoords: Array, layer: StyleLayer, sourceCache?: SourceCache) { - if (!sourceCache || !this._sourceTilesOverlap[sourceCache.id]) { - if (this._overlapStencilType) this._overlapStencilType = false; - return; - } - const context = this.painter.context; - const gl = context.gl; - - // If needed, setup stencilling. Don't bother to remove when there is no - // more need: in such case, if there is no overlap, stencilling is disabled. - if (proxiedCoords.length <= 1) { this._overlapStencilType = false; return; } - - let stencilRange; - if (layer.isTileClipped()) { - stencilRange = proxiedCoords.length; - this._overlapStencilMode.test = {func: gl.EQUAL, mask: 0xFF}; - this._overlapStencilType = 'Clip'; - } else if (proxiedCoords[0].overscaledZ > proxiedCoords[proxiedCoords.length - 1].overscaledZ) { - stencilRange = 1; - this._overlapStencilMode.test = {func: gl.GREATER, mask: 0xFF}; - this._overlapStencilType = 'Mask'; - } else { - this._overlapStencilType = false; - return; - } - if (this._stencilRef + stencilRange > 255) { - context.clear({stencil: 0}); - this._stencilRef = 0; - } - this._stencilRef += stencilRange; - this._overlapStencilMode.ref = this._stencilRef; - if (layer.isTileClipped()) { - this._renderTileClippingMasks(proxiedCoords, this._overlapStencilMode.ref); - } - } - - clipOrMaskOverlapStencilType(): boolean { - return this._overlapStencilType === 'Clip' || this._overlapStencilType === 'Mask'; - } - - stencilModeForRTTOverlap(id: OverscaledTileID): $ReadOnly { - if (!this.renderingToTexture || !this._overlapStencilType) { - return StencilMode.disabled; - } - // All source tiles contributing to the same proxy are processed in sequence, in zoom descending order. - // For raster / hillshade overlap masking, ref is based on zoom dif. - // For vector layer clipping, every tile gets dedicated stencil ref. - if (this._overlapStencilType === 'Clip') { - // In immediate 2D mode, we render rects to mark clipping area and handle behavior on tile borders. - // Here, there is no need for now for this: - // 1. overlap is handled by proxy render to texture tiles (there is no overlap there) - // 2. here we handle only brief zoom out semi-transparent color intensity flickering - // and that is avoided fine by stenciling primitives as part of drawing (instead of additional tile quad step). - this._overlapStencilMode.ref = this.painter._tileClippingMaskIDs[id.key]; - } // else this._overlapStencilMode.ref is set to a single value used per proxy tile, in _setupStencil. - return this._overlapStencilMode; - } - - _renderTileClippingMasks(proxiedCoords: Array, ref: number) { - const painter = this.painter; - const context = this.painter.context; - const gl = context.gl; - painter._tileClippingMaskIDs = {}; - context.setColorMode(ColorMode.disabled); - context.setDepthMode(DepthMode.disabled); - - const program = painter.useProgram('clippingMask'); - - for (const tileID of proxiedCoords) { - const id = painter._tileClippingMaskIDs[tileID.key] = --ref; - program.draw(context, gl.TRIANGLES, DepthMode.disabled, - // Tests will always pass, and ref value will be written to stencil buffer. - new StencilMode({func: gl.ALWAYS, mask: 0}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE), - ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.projMatrix), - '$clipping', painter.tileExtentBuffer, - painter.quadTriangleIndexBuffer, painter.tileExtentSegments); - } - } - - // Casts a ray from a point on screen and returns the intersection point with the terrain. - // The returned point contains the mercator coordinates in its first 3 components, and elevation - // in meter in its 4th coordinate. - pointCoordinate(screenPoint: Point): ?Vec4 { - const transform = this.painter.transform; - if (screenPoint.x < 0 || screenPoint.x > transform.width || - screenPoint.y < 0 || screenPoint.y > transform.height) { - return null; - } - - const far = [screenPoint.x, screenPoint.y, 1, 1]; - vec4.transformMat4(far, far, transform.pixelMatrixInverse); - vec4.scale(far, far, 1.0 / far[3]); - // x & y in pixel coordinates, z is altitude in meters - far[0] /= transform.worldSize; - far[1] /= transform.worldSize; - const camera = transform._camera.position; - const mercatorZScale = mercatorZfromAltitude(1, transform.center.lat); - const p = [camera[0], camera[1], camera[2] / mercatorZScale, 0.0]; - const dir = vec3.subtract([], far.slice(0, 3), p); - vec3.normalize(dir, dir); - - const exaggeration = this._exaggeration; - const distanceAlongRay = this.raycast(p, dir, exaggeration); - - if (distanceAlongRay === null || !distanceAlongRay) return null; - vec3.scaleAndAdd(p, p, dir, distanceAlongRay); - p[3] = p[2]; - p[2] *= mercatorZScale; - return p; - } - - drawDepth() { - const painter = this.painter; - const context = painter.context; - const psc = this.proxySourceCache; - - const width = Math.ceil(painter.width), height = Math.ceil(painter.height); - if (this._depthFBO && (this._depthFBO.width !== width || this._depthFBO.height !== height)) { - this._depthFBO.destroy(); - this._depthFBO = undefined; - this._depthTexture = undefined; - } - if (!this._depthFBO) { - const gl = context.gl; - const fbo = context.createFramebuffer(width, height, true); - context.activeTexture.set(gl.TEXTURE0); - const texture = new Texture(context, {width, height, data: null}, gl.RGBA); - texture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - fbo.colorAttachment.set(texture.texture); - const renderbuffer = context.createRenderbuffer(context.gl.DEPTH_COMPONENT16, width, height); - fbo.depthAttachment.set(renderbuffer); - this._depthFBO = fbo; - this._depthTexture = texture; - } - context.bindFramebuffer.set(this._depthFBO.framebuffer); - context.viewport.set([0, 0, width, height]); - - drawTerrainDepth(painter, this, psc, this.proxyCoords); - } - - _setupProxiedCoordsForOrtho(sourceCache: SourceCache, sourceCoords: Array, previousProxyToSource: {[number]: {[string]: Array}}): void { - if (sourceCache.getSource() instanceof ImageSource) { - return this._setupProxiedCoordsForImageSource(sourceCache, sourceCoords, previousProxyToSource); - } - this._findCoveringTileCache[sourceCache.id] = this._findCoveringTileCache[sourceCache.id] || {}; - const coords = this.proxiedCoords[sourceCache.id] = []; - const proxys = this.proxyCoords; - for (let i = 0; i < proxys.length; i++) { - const proxyTileID = proxys[i]; - const proxied = this._findTileCoveringTileID(proxyTileID, sourceCache); - if (proxied) { - assert(proxied.hasData()); - const id = this._createProxiedId(proxyTileID, proxied, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]); - coords.push(id); - this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; - } - } - let hasOverlap = false; - for (let i = 0; i < sourceCoords.length; i++) { - const tile = sourceCache.getTile(sourceCoords[i]); - if (!tile || !tile.hasData()) continue; - const proxy = this._findTileCoveringTileID(tile.tileID, this.proxySourceCache); - // Don't add the tile if already added in loop above. - if (proxy && proxy.tileID.canonical.z !== tile.tileID.canonical.z) { - const array = this.proxyToSource[proxy.tileID.key][sourceCache.id]; - const id = this._createProxiedId(proxy.tileID, tile, previousProxyToSource[proxy.tileID.key] && previousProxyToSource[proxy.tileID.key][sourceCache.id]); - if (!array) { - this.proxyToSource[proxy.tileID.key][sourceCache.id] = [id]; - } else { - // The last element is parent added in loop above. This way we get - // a list in Z descending order which is needed for stencil masking. - array.splice(array.length - 1, 0, id); - } - coords.push(id); - hasOverlap = true; - } - } - this._sourceTilesOverlap[sourceCache.id] = hasOverlap; - } - - _setupProxiedCoordsForImageSource(sourceCache: SourceCache, sourceCoords: Array, previousProxyToSource: {[number]: {[string]: Array}}) { - if (!sourceCache.getSource().loaded()) return; - - const coords = this.proxiedCoords[sourceCache.id] = []; - const proxys = this.proxyCoords; - const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource); - - const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div(1 << imageSource.tileID.z); - const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce((acc, coord) => { - acc.min.x = Math.min(acc.min.x, coord.x - anchor.x); - acc.min.y = Math.min(acc.min.y, coord.y - anchor.y); - acc.max.x = Math.max(acc.max.x, coord.x - anchor.x); - acc.max.y = Math.max(acc.max.y, coord.y - anchor.y); - return acc; - }, {min: new Point(Number.MAX_VALUE, Number.MAX_VALUE), max: new Point(-Number.MAX_VALUE, -Number.MAX_VALUE)}); - - // Fast conservative check using aabb: content outside proxy tile gets clipped out by on render, anyway. - const tileOutsideImage = (tileID, imageTileID) => { - const x = tileID.wrap + tileID.canonical.x / (1 << tileID.canonical.z); - const y = tileID.canonical.y / (1 << tileID.canonical.z); - const d = EXTENT / (1 << tileID.canonical.z); - - const ix = imageTileID.wrap + imageTileID.canonical.x / (1 << imageTileID.canonical.z); - const iy = imageTileID.canonical.y / (1 << imageTileID.canonical.z); - - return x + d < ix + aabb.min.x || x > ix + aabb.max.x || y + d < iy + aabb.min.y || y > iy + aabb.max.y; - }; - - for (let i = 0; i < proxys.length; i++) { - const proxyTileID = proxys[i]; - for (let j = 0; j < sourceCoords.length; j++) { - const tile = sourceCache.getTile(sourceCoords[j]); - if (!tile || !tile.hasData()) continue; - - // Setup proxied -> proxy mapping only if image on given tile wrap intersects the proxy tile. - if (tileOutsideImage(proxyTileID, tile.tileID)) continue; - - const id = this._createProxiedId(proxyTileID, tile, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]); - const array = this.proxyToSource[proxyTileID.key][sourceCache.id]; - if (!array) { - this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; - } else { - array.push(id); - } - coords.push(id); - } - } - } - - // recycle is previous pass content that likely contains proxied ID combining proxy and source tile. - _createProxiedId(proxyTileID: OverscaledTileID, tile: Tile, recycle: Array): ProxiedTileID { - let matrix = this.orthoMatrix; - if (recycle) { - const recycled = recycle.find(proxied => (proxied.key === tile.tileID.key)); - if (recycled) return recycled; - } - if (tile.tileID.key !== proxyTileID.key) { - const scale = proxyTileID.canonical.z - tile.tileID.canonical.z; - matrix = mat4.create(); - let size, xOffset, yOffset; - const wrap = (tile.tileID.wrap - proxyTileID.wrap) << proxyTileID.overscaledZ; - if (scale > 0) { - size = EXTENT >> scale; - xOffset = size * ((tile.tileID.canonical.x << scale) - proxyTileID.canonical.x + wrap); - yOffset = size * ((tile.tileID.canonical.y << scale) - proxyTileID.canonical.y); - } else { - size = EXTENT << -scale; - xOffset = EXTENT * (tile.tileID.canonical.x - ((proxyTileID.canonical.x + wrap) << -scale)); - yOffset = EXTENT * (tile.tileID.canonical.y - (proxyTileID.canonical.y << -scale)); - } - mat4.ortho(matrix, 0, size, 0, size, 0, 1); - mat4.translate(matrix, matrix, [xOffset, yOffset, 0]); - } - return new ProxiedTileID(tile.tileID, proxyTileID.key, matrix); - } - - // A variant of SourceCache.findLoadedParent that considers only visible - // tiles (and doesn't check SourceCache._cache). Another difference is in - // caching "not found" results along the lookup, to leave the lookup early. - // Not found is cached by this._findCoveringTileCache[key] = null; - _findTileCoveringTileID(tileID: OverscaledTileID, sourceCache: SourceCache): ?Tile { - let tile = sourceCache.getTile(tileID); - if (tile && tile.hasData()) return tile; - - const lookup = this._findCoveringTileCache[sourceCache.id]; - const key = lookup[tileID.key]; - tile = key ? sourceCache.getTileByID(key) : null; - if ((tile && tile.hasData()) || key === null) return tile; - - assert(!key || tile); - - let sourceTileID = tile ? tile.tileID : tileID; - let z = sourceTileID.overscaledZ; - const minzoom = sourceCache.getSource().minzoom; - const path = []; - if (!key) { - const maxzoom = sourceCache.getSource().maxzoom; - if (tileID.canonical.z >= maxzoom) { - const downscale = tileID.canonical.z - maxzoom; - if (sourceCache.getSource().reparseOverscaled) { - z = Math.max(tileID.canonical.z + 2, sourceCache.transform.tileZoom); - sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom, - tileID.canonical.x >> downscale, tileID.canonical.y >> downscale); - } else if (downscale !== 0) { - z = maxzoom; - sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom, - tileID.canonical.x >> downscale, tileID.canonical.y >> downscale); - } - } - if (sourceTileID.key !== tileID.key) { - path.push(sourceTileID.key); - tile = sourceCache.getTile(sourceTileID); - } - } - - const pathToLookup = (key) => { - path.forEach(id => { lookup[id] = key; }); - path.length = 0; - }; - - for (z = z - 1; z >= minzoom && !(tile && tile.hasData()); z--) { - if (tile) { - pathToLookup(tile.tileID.key); // Store lookup to parents not loaded (yet). - } - const id = sourceTileID.calculateScaledKey(z); - tile = sourceCache.getTileByID(id); - if (tile && tile.hasData()) break; - const key = lookup[id]; - if (key === null) { - break; // There's no tile loaded and no point searching further. - } else if (key !== undefined) { - tile = sourceCache.getTileByID(key); - assert(tile); - continue; - } - path.push(id); - } - - pathToLookup(tile ? tile.tileID.key : null); - return tile && tile.hasData() ? tile : null; - } - - findDEMTileFor(tileID: OverscaledTileID): ?Tile { - return this.enabled ? this._findTileCoveringTileID(tileID, this.sourceCache) : null; - } - - /* + const coords = this.proxyCoords; + const dirty = this._tilesDirty; + for (let i = coords.length - 1; i >= 0; i--) { + const proxy = coords[i]; + const tile = psc.getTileByID(proxy.key); + + if (psc.proxyCachedFBO[proxy.key] !== undefined) { + assert(tile.texture); + const prev = previousProxyToSource[proxy.key]; + assert(prev); + // Reuse previous render from cache if there was no change of + // content that was used to render proxy tile. + const current = this.proxyToSource[proxy.key]; + let equal = 0; + for (const source in current) { + const tiles = current[source]; + const prevTiles = prev[source]; + if ( + !prevTiles || prevTiles.length !== tiles.length || + tiles.some( + (t, index) => t !== prevTiles[index] || + dirty[source] && dirty[source].hasOwnProperty(t.key), + ) + ) { + equal = -1; + break; + } + ++equal; + } + // dirty === false: doesn't need to be rendered to, just use cached render. + for (const proxyFBO in psc.proxyCachedFBO[proxy.key]) { + psc.renderCache[psc.proxyCachedFBO[proxy.key][proxyFBO]].dirty = equal < 0 || + equal !== Object.values(prev).length; + } + } + } + + const sortedRenderBatches = [...this._drapedRenderBatches]; + sortedRenderBatches.sort( + (batchA, batchB) => { + const batchASize = batchA.end - batchA.start; + const batchBSize = batchB.end - batchB.start; + return batchBSize - batchASize; + }, + ); + + for (const batch of sortedRenderBatches) { + for (const id of coords) { + if (psc.proxyCachedFBO[id.key]) { + continue; + } + + // Assign renderCache FBO if there are available FBOs in pool. + let index = psc.renderCachePool.pop(); + if ( + index === undefined && psc.renderCache.length < RENDER_CACHE_MAX_SIZE + ) { + index = psc.renderCache.length; + psc.renderCache.push(this._createFBO()); + } + if (index !== undefined) { + psc.proxyCachedFBO[id.key] = {}; + psc.proxyCachedFBO[id.key][batch.start] = index; + psc.renderCache[index].dirty = true; // needs to be rendered to. + + } + } + } + this._tilesDirty = {}; + } + + _setupStencil( + fbo: FBO, + proxiedCoords: Array, + layer: StyleLayer, + sourceCache?: SourceCache, + ) { + if (!sourceCache || !this._sourceTilesOverlap[sourceCache.id]) { + if (this._overlapStencilType) this._overlapStencilType = false; + return; + } + const context = this.painter.context; + const gl = context.gl; + + // If needed, setup stencilling. Don't bother to remove when there is no + // more need: in such case, if there is no overlap, stencilling is disabled. + if (proxiedCoords.length <= 1) { + this._overlapStencilType = false; + return; + } + + let stencilRange; + if (layer.isTileClipped()) { + stencilRange = proxiedCoords.length; + this._overlapStencilMode.test = {func: gl.EQUAL, mask: 0xFF}; + this._overlapStencilType = 'Clip'; + } else if ( + proxiedCoords[0].overscaledZ > proxiedCoords[proxiedCoords.length - 1].overscaledZ + ) { + stencilRange = 1; + this._overlapStencilMode.test = {func: gl.GREATER, mask: 0xFF}; + this._overlapStencilType = 'Mask'; + } else { + this._overlapStencilType = false; + return; + } + if (this._stencilRef + stencilRange > 255) { + context.clear({stencil: 0}); + this._stencilRef = 0; + } + this._stencilRef += stencilRange; + this._overlapStencilMode.ref = this._stencilRef; + if (layer.isTileClipped()) { + this._renderTileClippingMasks(proxiedCoords, this._overlapStencilMode.ref); + } + } + + clipOrMaskOverlapStencilType(): boolean { + return ( + this._overlapStencilType === 'Clip' || this._overlapStencilType === 'Mask' + ); + } + + stencilModeForRTTOverlap(id: OverscaledTileID): $ReadOnly { + if (!this.renderingToTexture || !this._overlapStencilType) { + return StencilMode.disabled; + } + // All source tiles contributing to the same proxy are processed in sequence, in zoom descending order. + // For raster / hillshade overlap masking, ref is based on zoom dif. + // For vector layer clipping, every tile gets dedicated stencil ref. + if (this._overlapStencilType === 'Clip') { + // In immediate 2D mode, we render rects to mark clipping area and handle behavior on tile borders. + // Here, there is no need for now for this: + // 1. overlap is handled by proxy render to texture tiles (there is no overlap there) + // 2. here we handle only brief zoom out semi-transparent color intensity flickering + // and that is avoided fine by stenciling primitives as part of drawing (instead of additional tile quad step). + this._overlapStencilMode.ref = this.painter._tileClippingMaskIDs[id.key]; + } // else this._overlapStencilMode.ref is set to a single value used per proxy tile, in _setupStencil. + return this._overlapStencilMode; + } + + _renderTileClippingMasks(proxiedCoords: Array, ref: number) { + const painter = this.painter; + const context = this.painter.context; + const gl = context.gl; + painter._tileClippingMaskIDs = {}; + context.setColorMode(ColorMode.disabled); + context.setDepthMode(DepthMode.disabled); + + const program = painter.useProgram('clippingMask'); + + for (const tileID of proxiedCoords) { + const id = painter._tileClippingMaskIDs[tileID.key] = --ref; + program.draw( + context, + gl.TRIANGLES, + DepthMode.disabled, + // Tests will always pass, and ref value will be written to stencil buffer. + new StencilMode( + {func: gl.ALWAYS, mask: 0}, + id, + 0xFF, + gl.KEEP, + gl.KEEP, + gl.REPLACE, + ), + ColorMode.disabled, + CullFaceMode.disabled, + clippingMaskUniformValues(tileID.projMatrix), + '$clipping', + painter.tileExtentBuffer, + painter.quadTriangleIndexBuffer, + painter.tileExtentSegments, + ); + } + } + + // Casts a ray from a point on screen and returns the intersection point with the terrain. + // The returned point contains the mercator coordinates in its first 3 components, and elevation + // in meter in its 4th coordinate. + pointCoordinate(screenPoint: Point): ?Vec4 { + const transform = this.painter.transform; + if ( + screenPoint.x < 0 || screenPoint.x > transform.width || screenPoint.y < 0 || + screenPoint.y > transform.height + ) { + return null; + } + + const far = [screenPoint.x, screenPoint.y, 1, 1]; + vec4.transformMat4(far, far, transform.pixelMatrixInverse); + vec4.scale(far, far, 1.0 / far[3]); + // x & y in pixel coordinates, z is altitude in meters + far[0] /= transform.worldSize; + far[1] /= transform.worldSize; + const camera = transform._camera.position; + const mercatorZScale = mercatorZfromAltitude(1, transform.center.lat); + const p = [camera[0], camera[1], camera[2] / mercatorZScale, 0.0]; + const dir = vec3.subtract([], far.slice(0, 3), p); + vec3.normalize(dir, dir); + + const exaggeration = this._exaggeration; + const distanceAlongRay = this.raycast(p, dir, exaggeration); + + if (distanceAlongRay === null || !distanceAlongRay) return null; + vec3.scaleAndAdd(p, p, dir, distanceAlongRay); + p[3] = p[2]; + p[2] *= mercatorZScale; + return p; + } + + drawDepth() { + const painter = this.painter; + const context = painter.context; + const psc = this.proxySourceCache; + + const width = Math.ceil(painter.width), + height = Math.ceil(painter.height); + if ( + this._depthFBO && + (this._depthFBO.width !== width || this._depthFBO.height !== height) + ) { + this._depthFBO.destroy(); + this._depthFBO = undefined; + this._depthTexture = undefined; + } + if (!this._depthFBO) { + const gl = context.gl; + const fbo = context.createFramebuffer(width, height, true); + context.activeTexture.set(gl.TEXTURE0); + const texture = new Texture( + context, + {width, height, data: null}, + gl.RGBA, + ); + texture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + fbo.colorAttachment.set(texture.texture); + const renderbuffer = context.createRenderbuffer( + context.gl.DEPTH_COMPONENT16, + width, + height, + ); + fbo.depthAttachment.set(renderbuffer); + this._depthFBO = fbo; + this._depthTexture = texture; + } + context.bindFramebuffer.set(this._depthFBO.framebuffer); + context.viewport.set([0, 0, width, height]); + + drawTerrainDepth(painter, this, psc, this.proxyCoords); + } + + _setupProxiedCoordsForOrtho( + sourceCache: SourceCache, + sourceCoords: Array, + previousProxyToSource: { [number]: { [string]: Array } }, + ): void { + if (sourceCache.getSource() instanceof ImageSource) { + return this._setupProxiedCoordsForImageSource( + sourceCache, + sourceCoords, + previousProxyToSource, + ); + } + this._findCoveringTileCache[sourceCache.id] = this._findCoveringTileCache[sourceCache.id] || + {}; + const coords = this.proxiedCoords[sourceCache.id] = []; + const proxys = this.proxyCoords; + for (let i = 0; i < proxys.length; i++) { + const proxyTileID = proxys[i]; + const proxied = this._findTileCoveringTileID(proxyTileID, sourceCache); + if (proxied) { + assert(proxied.hasData()); + const id = this._createProxiedId( + proxyTileID, + proxied, + previousProxyToSource[proxyTileID.key] && + previousProxyToSource[proxyTileID.key][sourceCache.id], + ); + coords.push(id); + this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; + } + } + let hasOverlap = false; + for (let i = 0; i < sourceCoords.length; i++) { + const tile = sourceCache.getTile(sourceCoords[i]); + if (!tile || !tile.hasData()) continue; + const proxy = this._findTileCoveringTileID( + tile.tileID, + this.proxySourceCache, + ); + // Don't add the tile if already added in loop above. + if (proxy && proxy.tileID.canonical.z !== tile.tileID.canonical.z) { + const array = this.proxyToSource[proxy.tileID.key][sourceCache.id]; + const id = this._createProxiedId( + proxy.tileID, + tile, + previousProxyToSource[proxy.tileID.key] && + previousProxyToSource[proxy.tileID.key][sourceCache.id], + ); + if (!array) { + this.proxyToSource[proxy.tileID.key][sourceCache.id] = [id]; + } else { + // The last element is parent added in loop above. This way we get + // a list in Z descending order which is needed for stencil masking. + array.splice(array.length - 1, 0, id); + } + coords.push(id); + hasOverlap = true; + } + } + this._sourceTilesOverlap[sourceCache.id] = hasOverlap; + } + + _setupProxiedCoordsForImageSource( + sourceCache: SourceCache, + sourceCoords: Array, + previousProxyToSource: { [number]: { [string]: Array } }, + ) { + if (!sourceCache.getSource().loaded()) return; + + const coords = this.proxiedCoords[sourceCache.id] = []; + const proxys = this.proxyCoords; + const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource); + + const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div( + 1 << imageSource.tileID.z, + ); + const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce( + (acc, coord) => { + acc.min.x = Math.min(acc.min.x, coord.x - anchor.x); + acc.min.y = Math.min(acc.min.y, coord.y - anchor.y); + acc.max.x = Math.max(acc.max.x, coord.x - anchor.x); + acc.max.y = Math.max(acc.max.y, coord.y - anchor.y); + return acc; + }, + { + min: new Point(Number.MAX_VALUE, Number.MAX_VALUE), + max: new Point(-Number.MAX_VALUE, -Number.MAX_VALUE), + }, + ); + + // Fast conservative check using aabb: content outside proxy tile gets clipped out by on render, anyway. + const tileOutsideImage = ((tileID, imageTileID) => { + const x = tileID.wrap + tileID.canonical.x / (1 << tileID.canonical.z); + const y = tileID.canonical.y / (1 << tileID.canonical.z); + const d = EXTENT / (1 << tileID.canonical.z); + + const ix = imageTileID.wrap + imageTileID.canonical.x / (1 << imageTileID.canonical.z); + const iy = imageTileID.canonical.y / (1 << imageTileID.canonical.z); + + return ( + x + d < ix + aabb.min.x || x > ix + aabb.max.x || + y + d < iy + aabb.min.y || + y > iy + aabb.max.y + ); + }); + + for (let i = 0; i < proxys.length; i++) { + const proxyTileID = proxys[i]; + for (let j = 0; j < sourceCoords.length; j++) { + const tile = sourceCache.getTile(sourceCoords[j]); + if (!tile || !tile.hasData()) continue; + + // Setup proxied -> proxy mapping only if image on given tile wrap intersects the proxy tile. + if (tileOutsideImage(proxyTileID, tile.tileID)) continue; + + const id = this._createProxiedId( + proxyTileID, + tile, + previousProxyToSource[proxyTileID.key] && + previousProxyToSource[proxyTileID.key][sourceCache.id], + ); + const array = this.proxyToSource[proxyTileID.key][sourceCache.id]; + if (!array) { + this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; + } else { + array.push(id); + } + coords.push(id); + } + } + } + + // recycle is previous pass content that likely contains proxied ID combining proxy and source tile. + _createProxiedId( + proxyTileID: OverscaledTileID, + tile: Tile, + recycle: Array, + ): ProxiedTileID { + let matrix = this.orthoMatrix; + if (recycle) { + const recycled = recycle.find(proxied => proxied.key === tile.tileID.key); + if (recycled) return recycled; + } + if (tile.tileID.key !== proxyTileID.key) { + const scale = proxyTileID.canonical.z - tile.tileID.canonical.z; + matrix = mat4.create(); + let size, xOffset, yOffset; + const wrap = tile.tileID.wrap - proxyTileID.wrap << proxyTileID.overscaledZ; + if (scale > 0) { + size = EXTENT >> scale; + xOffset = size * ((tile.tileID.canonical.x << scale) - proxyTileID.canonical.x + wrap); + yOffset = size * ((tile.tileID.canonical.y << scale) - proxyTileID.canonical.y); + } else { + size = EXTENT << -scale; + xOffset = EXTENT * (tile.tileID.canonical.x - (proxyTileID.canonical.x + wrap << -scale)); + yOffset = EXTENT * (tile.tileID.canonical.y - (proxyTileID.canonical.y << -scale)); + } + mat4.ortho(matrix, 0, size, 0, size, 0, 1); + mat4.translate(matrix, matrix, [xOffset, yOffset, 0]); + } + return new ProxiedTileID(tile.tileID, proxyTileID.key, matrix); + } + + // A variant of SourceCache.findLoadedParent that considers only visible + // tiles (and doesn't check SourceCache._cache). Another difference is in + // caching "not found" results along the lookup, to leave the lookup early. + // Not found is cached by this._findCoveringTileCache[key] = null; + _findTileCoveringTileID( + tileID: OverscaledTileID, + sourceCache: SourceCache, + ): ?Tile { + let tile = sourceCache.getTile(tileID); + if (tile && tile.hasData()) return tile; + + const lookup = this._findCoveringTileCache[sourceCache.id]; + const key = lookup[tileID.key]; + tile = key ? sourceCache.getTileByID(key) : null; + if (tile && tile.hasData() || key === null) return tile; + + assert(!key || tile); + + let sourceTileID = tile ? tile.tileID : tileID; + let z = sourceTileID.overscaledZ; + const minzoom = sourceCache.getSource().minzoom; + const path = []; + if (!key) { + const maxzoom = sourceCache.getSource().maxzoom; + if (tileID.canonical.z >= maxzoom) { + const downscale = tileID.canonical.z - maxzoom; + if (sourceCache.getSource().reparseOverscaled) { + z = Math.max(tileID.canonical.z + 2, sourceCache.transform.tileZoom); + sourceTileID = new OverscaledTileID( + z, + tileID.wrap, + maxzoom, + tileID.canonical.x >> downscale, + tileID.canonical.y >> downscale, + ); + } else if (downscale !== 0) { + z = maxzoom; + sourceTileID = new OverscaledTileID( + z, + tileID.wrap, + maxzoom, + tileID.canonical.x >> downscale, + tileID.canonical.y >> downscale, + ); + } + } + if (sourceTileID.key !== tileID.key) { + path.push(sourceTileID.key); + tile = sourceCache.getTile(sourceTileID); + } + } + + const pathToLookup = (key => { + path.forEach( + id => { + lookup[id] = key; + }, + ); + path.length = 0; + }); + + for (z = z - 1; z >= minzoom && !(tile && tile.hasData()); z--) { + if (tile) { + pathToLookup(tile.tileID.key); // Store lookup to parents not loaded (yet). + + } + const id = sourceTileID.calculateScaledKey(z); + tile = sourceCache.getTileByID(id); + if (tile && tile.hasData()) break; + const key = lookup[id]; + if (key === null) { + break; // There's no tile loaded and no point searching further. + + } else if (key !== undefined) { + tile = sourceCache.getTileByID(key); + assert(tile); + continue; + } + path.push(id); + } + + pathToLookup(tile ? tile.tileID.key : null); + return tile && tile.hasData() ? tile : null; + } + + findDEMTileFor(tileID: OverscaledTileID): ?Tile { + return this.enabled ? + this._findTileCoveringTileID(tileID, this.sourceCache) : + null; + } + + /* * Bookkeeping if something gets rendered to the tile. */ - prepareDrawTile() { - this.renderedToTile = true; - } + prepareDrawTile() { + this.renderedToTile = true; + } - _clearRenderCacheForTile(source: string, coord: OverscaledTileID) { - let sourceTiles = this._tilesDirty[source]; - if (!sourceTiles) sourceTiles = this._tilesDirty[source] = {}; - sourceTiles[coord.key] = true; - } + _clearRenderCacheForTile(source: string, coord: OverscaledTileID) { + let sourceTiles = this._tilesDirty[source]; + if (!sourceTiles) sourceTiles = this._tilesDirty[source] = {}; + sourceTiles[coord.key] = true; + } - /* + /* * Lazily instantiate the wireframe index buffer and segment vector so that we don't * allocate the geometry for rendering a debug wireframe until it's needed. */ - getWirefameBuffer(): [IndexBuffer, SegmentVector] { - if (!this.wireframeSegments) { - const wireframeGridIndices = createWireframeGrid(GRID_DIM + 1); - this.wireframeIndexBuffer = this.painter.context.createIndexBuffer(wireframeGridIndices); - this.wireframeSegments = SegmentVector.simpleSegment(0, 0, this.gridBuffer.length, wireframeGridIndices.length); - } - return [this.wireframeIndexBuffer, this.wireframeSegments]; - } - + getWirefameBuffer(): [IndexBuffer, SegmentVector] { + if (!this.wireframeSegments) { + const wireframeGridIndices = createWireframeGrid(GRID_DIM + 1); + this.wireframeIndexBuffer = this.painter.context.createIndexBuffer( + wireframeGridIndices, + ); + this.wireframeSegments = SegmentVector.simpleSegment( + 0, + 0, + this.gridBuffer.length, + wireframeGridIndices.length, + ); + } + return [this.wireframeIndexBuffer, this.wireframeSegments]; + } } function sortByDistanceToCamera(tileIDs, painter) { diff --git a/src/types/cancelable.js b/src/types/cancelable.js index e155168ac00..d1761818f45 100644 --- a/src/types/cancelable.js +++ b/src/types/cancelable.js @@ -1,3 +1,3 @@ // @flow strict -export type Cancelable = { cancel: () => void }; +export type Cancelable = interface { cancel: () => void }; diff --git a/src/ui/camera.js b/src/ui/camera.js index edf6733f724..fadee08d6e7 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -179,46 +179,51 @@ const freeCameraNotSupportedWarning = 'map.setFreeCameraOptions(...) and map.get * @see [Example: Fit a map to a bounding box](https://docs.mapbox.com/mapbox-gl-js/example/fitbounds/) */ -class Camera extends Evented { - transform: Transform; - _moving: boolean; - _zooming: boolean; - _rotating: boolean; - _pitching: boolean; - _padding: boolean; - - _bearingSnap: number; - _easeStart: number; - _easeOptions: {duration: number, easing: (_: number) => number}; - _easeId: string | void; - - _onEaseFrame: ?(_: number) => Transform | void; - _onEaseEnd: ?(easeId?: string) => void; - _easeFrameId: ?TaskID; - - +_requestRenderFrame: (() => void) => TaskID; - +_cancelRenderFrame: (_: TaskID) => void; - - +_preloadTiles: (transform: Transform | Array, callback?: Callback) => any; - - constructor(transform: Transform, options: {bearingSnap: number}) { - super(); - this._moving = false; - this._zooming = false; - this.transform = transform; - this._bearingSnap = options.bearingSnap; - - bindAll(['_renderFrameCallback'], this); - - //addAssertions(this); - } - - /** @section {Camera} +class Camera + extends Evented { + transform: Transform; + _moving: boolean; + _zooming: boolean; + _rotating: boolean; + _pitching: boolean; + _padding: boolean; + + _bearingSnap: number; + _easeStart: number; + _easeOptions: { duration: number, easing: (_: number) => number }; + _easeId: string | void; + + _onEaseFrame: ?((_: number) => Transform | void); + _onEaseEnd: ?((easeId?: string) => void); + _easeFrameId: ?TaskID; + + +_requestRenderFrame: (() => void) => TaskID; + +_cancelRenderFrame: (_: TaskID) => void; + + +_preloadTiles: ( + transform: Transform | Array, + callback?: Callback + ) => any; + + constructor(transform: Transform, options: { bearingSnap: number }) { + super(); + this._moving = false; + this._zooming = false; + this.transform = transform; + this._bearingSnap = options.bearingSnap; + + bindAll(['_renderFrameCallback'], this); + + //addAssertions(this); + + } + + /** @section {Camera} * @method * @instance * @memberof Map */ - /** + /** * Returns the map's geographical centerpoint. * * @memberof Map# @@ -230,9 +235,11 @@ class Camera extends Evented { * const {lng, lat} = map.getCenter(); * @see [Tutorial: Use Mapbox GL JS in a React app](https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/#store-the-new-coordinates) */ - getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); } + getCenter(): LngLat { + return new LngLat(this.transform.center.lng, this.transform.center.lat); + } - /** + /** * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`. * * @memberof Map# @@ -244,11 +251,11 @@ class Camera extends Evented { * @example * map.setCenter([-74, 38]); */ - setCenter(center: LngLatLike, eventData?: Object): this { - return this.jumpTo({center}, eventData); - } + setCenter(center: LngLatLike, eventData?: Object): this { + return this.jumpTo({center}, eventData); + } - /** + /** * Pans the map by the specified offset. * * @memberof Map# @@ -265,12 +272,16 @@ class Camera extends Evented { * map.panBy([-74, 38], {duration: 5000}); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - panBy(offset: PointLike, options?: AnimationOptions, eventData?: Object): this { - offset = Point.convert(offset).mult(-1); - return this.panTo(this.transform.center, extend({offset}, options), eventData); - } - - /** + panBy(offset: PointLike, options?: AnimationOptions, eventData?: Object): this { + offset = Point.convert(offset).mult(-1); + return this.panTo( + this.transform.center, + extend({offset}, options), + eventData, + ); + } + + /** * Pans the map to the specified location with an animated transition. * * @memberof Map# @@ -287,13 +298,23 @@ class Camera extends Evented { * map.panTo([-74, 38], {duration: 5000}); * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ - panTo(lnglat: LngLatLike, options?: AnimationOptions, eventData?: Object): this { - return this.easeTo(extend({ - center: lnglat - }, options), eventData); - } - - /** + panTo( + lnglat: LngLatLike, + options?: AnimationOptions, + eventData?: Object, + ): this { + return this.easeTo( + extend( + { + center: lnglat, + }, + options, + ), + eventData, + ); + } + + /** * Returns the map's current zoom level. * * @memberof Map# @@ -301,9 +322,11 @@ class Camera extends Evented { * @example * map.getZoom(); */ - getZoom(): number { return this.transform.zoom; } + getZoom(): number { + return this.transform.zoom; + } - /** + /** * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`. * * @memberof Map# @@ -320,12 +343,12 @@ class Camera extends Evented { * // Zoom to the zoom level 5 without an animated transition * map.setZoom(5); */ - setZoom(zoom: number, eventData?: Object): this { - this.jumpTo({zoom}, eventData); - return this; - } + setZoom(zoom: number, eventData?: Object): this { + this.jumpTo({zoom}, eventData); + return this; + } - /** + /** * Zooms the map to the specified zoom level, with an animated transition. * * @memberof Map# @@ -348,13 +371,19 @@ class Camera extends Evented { * offset: [100, 50] * }); */ - zoomTo(zoom: number, options: ? AnimationOptions, eventData?: Object): this { - return this.easeTo(extend({ - zoom - }, options), eventData); - } - - /** + zoomTo(zoom: number, options: ?AnimationOptions, eventData?: Object): this { + return this.easeTo( + extend( + { + zoom, + }, + options, + ), + eventData, + ); + } + + /** * Increases the map's zoom level by 1. * * @memberof Map# @@ -371,12 +400,12 @@ class Camera extends Evented { * // zoom the map in one level with a custom animation duration * map.zoomIn({duration: 1000}); */ - zoomIn(options?: AnimationOptions, eventData?: Object): this { - this.zoomTo(this.getZoom() + 1, options, eventData); - return this; - } + zoomIn(options?: AnimationOptions, eventData?: Object): this { + this.zoomTo(this.getZoom() + 1, options, eventData); + return this; + } - /** + /** * Decreases the map's zoom level by 1. * * @memberof Map# @@ -393,12 +422,12 @@ class Camera extends Evented { * // zoom the map out one level with a custom animation offset * map.zoomOut({offset: [80, 60]}); */ - zoomOut(options?: AnimationOptions, eventData?: Object): this { - this.zoomTo(this.getZoom() - 1, options, eventData); - return this; - } + zoomOut(options?: AnimationOptions, eventData?: Object): this { + this.zoomTo(this.getZoom() - 1, options, eventData); + return this; + } - /** + /** * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * @@ -408,11 +437,11 @@ class Camera extends Evented { * const bearing = map.getBearing(); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - getBearing(): number { - return this.transform.bearing; - } + getBearing(): number { + return this.transform.bearing; + } - /** + /** * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * @@ -428,12 +457,12 @@ class Camera extends Evented { * // Rotate the map to 90 degrees. * map.setBearing(90); */ - setBearing(bearing: number, eventData?: Object): this { - this.jumpTo({bearing}, eventData); - return this; - } + setBearing(bearing: number, eventData?: Object): this { + this.jumpTo({bearing}, eventData); + return this; + } - /** + /** * Returns the current padding applied around the map viewport. * * @memberof Map# @@ -441,9 +470,11 @@ class Camera extends Evented { * @example * const padding = map.getPadding(); */ - getPadding(): PaddingOptions { return this.transform.padding; } + getPadding(): PaddingOptions { + return this.transform.padding; + } - /** + /** * Sets the padding in pixels around the viewport. * * Equivalent to `jumpTo({padding: padding})`. @@ -458,12 +489,12 @@ class Camera extends Evented { * // Sets a left padding of 300px, and a top padding of 50px * map.setPadding({left: 300, top: 50}); */ - setPadding(padding: PaddingOptions, eventData?: Object): this { - this.jumpTo({padding}, eventData); - return this; - } + setPadding(padding: PaddingOptions, eventData?: Object): this { + this.jumpTo({padding}, eventData); + return this; + } - /** + /** * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction * that is \"up\"; for example, a bearing of 90° orients the map so that east is up. * @@ -481,13 +512,19 @@ class Camera extends Evented { * // rotateTo with an animation of 2 seconds. * map.rotateTo(30, {duration: 2000}); */ - rotateTo(bearing: number, options?: EasingOptions, eventData?: Object): this { - return this.easeTo(extend({ - bearing - }, options), eventData); - } - - /** + rotateTo(bearing: number, options?: EasingOptions, eventData?: Object): this { + return this.easeTo( + extend( + { + bearing, + }, + options, + ), + eventData, + ); + } + + /** * Rotates the map so that north is up (0° bearing), with an animated transition. * * @memberof Map# @@ -501,12 +538,12 @@ class Camera extends Evented { * // resetNorth with an animation of 2 seconds. * map.resetNorth({duration: 2000}); */ - resetNorth(options?: EasingOptions, eventData?: Object): this { - this.rotateTo(0, extend({duration: 1000}, options), eventData); - return this; - } + resetNorth(options?: EasingOptions, eventData?: Object): this { + this.rotateTo(0, extend({duration: 1000}, options), eventData); + return this; + } - /** + /** * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition. * * @memberof Map# @@ -520,16 +557,22 @@ class Camera extends Evented { * // resetNorthPitch with an animation of 2 seconds. * map.resetNorthPitch({duration: 2000}); */ - resetNorthPitch(options?: EasingOptions, eventData?: Object): this { - this.easeTo(extend({ + resetNorthPitch(options?: EasingOptions, eventData?: Object): this { + this.easeTo( + extend( + { bearing: 0, pitch: 0, - duration: 1000 - }, options), eventData); - return this; - } - - /** + duration: 1000, + }, + options, + ), + eventData, + ); + return this; + } + + /** * Snaps the map so that north is up (0° bearing), if the current bearing is * close enough to it (within the `bearingSnap` threshold). * @@ -544,14 +587,14 @@ class Camera extends Evented { * // snapToNorth with an animation of 2 seconds. * map.snapToNorth({duration: 2000}); */ - snapToNorth(options?: EasingOptions, eventData?: Object): this { - if (Math.abs(this.getBearing()) < this._bearingSnap) { - return this.resetNorth(options, eventData); - } - return this; - } - - /** + snapToNorth(options?: EasingOptions, eventData?: Object): this { + if (Math.abs(this.getBearing()) < this._bearingSnap) { + return this.resetNorth(options, eventData); + } + return this; + } + + /** * Returns the map's current [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). * * @memberof Map# @@ -559,9 +602,11 @@ class Camera extends Evented { * @example * const pitch = map.getPitch(); */ - getPitch(): number { return this.transform.pitch; } + getPitch(): number { + return this.transform.pitch; + } - /** + /** * Sets the map's [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). Equivalent to `jumpTo({pitch: pitch})`. * * @memberof Map# @@ -575,12 +620,12 @@ class Camera extends Evented { * // setPitch with an animation of 2 seconds. * map.setPitch(80, {duration: 2000}); */ - setPitch(pitch: number, eventData?: Object): this { - this.jumpTo({pitch}, eventData); - return this; - } + setPitch(pitch: number, eventData?: Object): this { + this.jumpTo({pitch}, eventData); + return this; + } - /** + /** * Returns a {@link CameraOptions} object for the highest zoom level * up to and including `Map#getMaxZoom()` that fits the bounds * in the viewport at the specified bearing. @@ -603,149 +648,200 @@ class Camera extends Evented { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ - cameraForBounds(bounds: LngLatBoundsLike, options?: CameraOptions): ?EasingOptions { - bounds = LngLatBounds.convert(bounds); - const bearing = (options && options.bearing) || 0; - const pitch = (options && options.pitch) || 0; - const lnglat0 = bounds.getNorthWest(); - const lnglat1 = bounds.getSouthEast(); - return this._cameraForBounds(this.transform, lnglat0, lnglat1, bearing, pitch, options); - } - - _extendCameraOptions(options?: CameraOptions): FullCameraOptions { - const defaultPadding = { - top: 0, - bottom: 0, - right: 0, - left: 0 - }; - options = extend({ - padding: defaultPadding, - offset: [0, 0], - maxZoom: this.transform.maxZoom - }, options); - - if (typeof options.padding === 'number') { - const p = options.padding; - options.padding = { - top: p, - bottom: p, - right: p, - left: p - }; - } - options.padding = extend(defaultPadding, options.padding); - return options; - } - - _minimumAABBFrustumDistance(tr: Transform, aabb: Aabb): number { - const aabbW = aabb.max[0] - aabb.min[0]; - const aabbH = aabb.max[1] - aabb.min[1]; - const aabbAspectRatio = aabbW / aabbH; - const selectXAxis = aabbAspectRatio > tr.aspect; - - const minimumDistance = selectXAxis ? - aabbW / (2 * Math.tan(tr.fovX * 0.5) * tr.aspect) : - aabbH / (2 * Math.tan(tr.fovY * 0.5) * tr.aspect); - - return minimumDistance; - } - - _cameraForBoundsOnGlobe(transform: Transform, p0: LngLatLike, p1: LngLatLike, bearing: number, pitch: number, options?: CameraOptions): ?EasingOptions { - const tr = transform.clone(); - const eOptions = this._extendCameraOptions(options); - - tr.bearing = bearing; - tr.pitch = pitch; - - const coord0 = LngLat.convert(p0); - const coord1 = LngLat.convert(p1); - - const midLat = (coord0.lat + coord1.lat) * 0.5; - const midLng = (coord0.lng + coord1.lng) * 0.5; - - const origin = latLngToECEF(midLat, midLng); - - const zAxis = vec3.normalize([], origin); - const xAxis = vec3.normalize([], vec3.cross([], zAxis, [0, 1, 0])); - const yAxis = vec3.cross([], xAxis, zAxis); - - const aabbOrientation = [ - xAxis[0], xAxis[1], xAxis[2], 0, - yAxis[0], yAxis[1], yAxis[2], 0, - zAxis[0], zAxis[1], zAxis[2], 0, - 0, 0, 0, 1 - ]; - - const ecefCoords = [ - origin, - - latLngToECEF(coord0.lat, coord0.lng), - latLngToECEF(coord1.lat, coord0.lng), - latLngToECEF(coord1.lat, coord1.lng), - latLngToECEF(coord0.lat, coord1.lng), - - latLngToECEF(midLat, coord0.lng), - latLngToECEF(midLat, coord1.lng), - latLngToECEF(coord0.lat, midLng), - latLngToECEF(coord1.lat, midLng), - ]; - - let aabb = Aabb.fromPoints(ecefCoords.map(p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)])); - - const center = vec3.transformMat4([], aabb.center, aabbOrientation); - - if (vec3.squaredLength(center) === 0) { - vec3.set(center, 0, 0, 1); - } - - vec3.normalize(center, center); - vec3.scale(center, center, GLOBE_RADIUS); - tr.center = ecefToLatLng(center); - - const worldToCamera = tr.getWorldToCameraMatrix(); - const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); - - aabb = Aabb.applyTransform(aabb, mat4.multiply([], worldToCamera, aabbOrientation)); - - vec3.transformMat4(center, center, worldToCamera); - - const aabbHalfExtentZ = (aabb.max[2] - aabb.min[2]) * 0.5; - const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); - - const offsetZ = vec3.scale([], [0, 0, 1], aabbHalfExtentZ); - const aabbClosestPoint = vec3.add(offsetZ, center, offsetZ); - const offsetDistance = frustumDistance + (tr.pitch === 0 ? 0 : vec3.distance(center, aabbClosestPoint)); - - const globeCenter = tr.globeCenterInViewSpace; - const normal = vec3.sub([], center, [globeCenter[0], globeCenter[1], globeCenter[2]]); - vec3.normalize(normal, normal); - vec3.scale(normal, normal, offsetDistance); - - const cameraPosition = vec3.add([], center, normal); - - vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); - - const meterPerECEF = earthRadius / GLOBE_RADIUS; - const altitudeECEF = vec3.length(cameraPosition); - const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius; - const mercatorZ = mercatorZfromAltitude(Math.max(altitudeMeter, Number.EPSILON), 0); - - const zoom = Math.min(tr.zoomFromMercatorZAdjusted(mercatorZ), eOptions.maxZoom); - - const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; - if (zoom > halfZoomTransition) { - tr.setProjection({name: 'mercator'}); - tr.zoom = zoom; - return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); - } + cameraForBounds( + bounds: LngLatBoundsLike, + options?: CameraOptions, + ): ?EasingOptions { + bounds = LngLatBounds.convert(bounds); + const bearing = options && options.bearing || 0; + const pitch = options && options.pitch || 0; + const lnglat0 = bounds.getNorthWest(); + const lnglat1 = bounds.getSouthEast(); + return this._cameraForBounds( + this.transform, + lnglat0, + lnglat1, + bearing, + pitch, + options, + ); + } + + _extendCameraOptions(options?: CameraOptions): FullCameraOptions { + const defaultPadding = { + top: 0, + bottom: 0, + right: 0, + left: 0, + }; + options = extend( + { + padding: defaultPadding, + offset: [0, 0], + maxZoom: this.transform.maxZoom, + }, + options, + ); + + if (typeof options.padding === 'number') { + const p = options.padding; + options.padding = { + top: p, + bottom: p, + right: p, + left: p, + }; + } + options.padding = extend(defaultPadding, options.padding); + return options; + } + + _minimumAABBFrustumDistance(tr: Transform, aabb: Aabb): number { + const aabbW = aabb.max[0] - aabb.min[0]; + const aabbH = aabb.max[1] - aabb.min[1]; + const aabbAspectRatio = aabbW / aabbH; + const selectXAxis = aabbAspectRatio > tr.aspect; + + const minimumDistance = selectXAxis ? + aabbW / (2 * Math.tan(tr.fovX * 0.5) * tr.aspect) : + aabbH / (2 * Math.tan(tr.fovY * 0.5) * tr.aspect); + + return minimumDistance; + } + + _cameraForBoundsOnGlobe( + transform: Transform, + p0: LngLatLike, + p1: LngLatLike, + bearing: number, + pitch: number, + options?: CameraOptions, + ): ?EasingOptions { + const tr = transform.clone(); + const eOptions = this._extendCameraOptions(options); + + tr.bearing = bearing; + tr.pitch = pitch; + + const coord0 = LngLat.convert(p0); + const coord1 = LngLat.convert(p1); + + const midLat = (coord0.lat + coord1.lat) * 0.5; + const midLng = (coord0.lng + coord1.lng) * 0.5; + + const origin = latLngToECEF(midLat, midLng); + + const zAxis = vec3.normalize([], origin); + const xAxis = vec3.normalize([], vec3.cross([], zAxis, [0, 1, 0])); + const yAxis = vec3.cross([], xAxis, zAxis); + + const aabbOrientation = [ + xAxis[0], + xAxis[1], + xAxis[2], + 0, + yAxis[0], + yAxis[1], + yAxis[2], + 0, + zAxis[0], + zAxis[1], + zAxis[2], + 0, + 0, + 0, + 0, + 1, + ]; + + const ecefCoords = [ + origin, + + latLngToECEF(coord0.lat, coord0.lng), + latLngToECEF(coord1.lat, coord0.lng), + latLngToECEF(coord1.lat, coord1.lng), + latLngToECEF(coord0.lat, coord1.lng), + + latLngToECEF(midLat, coord0.lng), + latLngToECEF(midLat, coord1.lng), + latLngToECEF(coord0.lat, midLng), + latLngToECEF(coord1.lat, midLng), + ]; + + let aabb = Aabb.fromPoints( + ecefCoords.map( + p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)], + ), + ); + + const center = vec3.transformMat4([], aabb.center, aabbOrientation); + + if (vec3.squaredLength(center) === 0) { + vec3.set(center, 0, 0, 1); + } + + vec3.normalize(center, center); + vec3.scale(center, center, GLOBE_RADIUS); + tr.center = ecefToLatLng(center); + + const worldToCamera = tr.getWorldToCameraMatrix(); + const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); + + aabb = Aabb.applyTransform( + aabb, + mat4.multiply([], worldToCamera, aabbOrientation), + ); + + vec3.transformMat4(center, center, worldToCamera); + + const aabbHalfExtentZ = (aabb.max[2] - aabb.min[2]) * 0.5; + const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); + + const offsetZ = vec3.scale([], [0, 0, 1], aabbHalfExtentZ); + const aabbClosestPoint = vec3.add(offsetZ, center, offsetZ); + const offsetDistance = frustumDistance + (tr.pitch === 0 ? + 0 : + vec3.distance(center, aabbClosestPoint)); + + const globeCenter = tr.globeCenterInViewSpace; + const normal = vec3.sub( + [], + center, + [globeCenter[0], globeCenter[1], globeCenter[2]], + ); + vec3.normalize(normal, normal); + vec3.scale(normal, normal, offsetDistance); + + const cameraPosition = vec3.add([], center, normal); + + vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); + + const meterPerECEF = earthRadius / GLOBE_RADIUS; + const altitudeECEF = vec3.length(cameraPosition); + const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius; + const mercatorZ = mercatorZfromAltitude( + Math.max(altitudeMeter, Number.EPSILON), + 0, + ); + + const zoom = Math.min( + tr.zoomFromMercatorZAdjusted(mercatorZ), + eOptions.maxZoom, + ); + + const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; + if (zoom > halfZoomTransition) { + tr.setProjection({name: 'mercator'}); + tr.zoom = zoom; + return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); + } return {center: tr.center, zoom, bearing, pitch}; } /** @section {Querying features} */ - /** + /** * Queries the currently loaded data for elevation at a geographical location. The elevation is returned in `meters` relative to mean sea-level. * Returns `null` if `terrain` is disabled or if terrain data for the location hasn't been loaded yet. * @@ -762,14 +858,21 @@ class Camera extends Evented { * const elevation = map.queryTerrainElevation(coordinate); * @see [Example: Query terrain elevation](https://docs.mapbox.com/mapbox-gl-js/example/query-terrain-elevation/) */ - queryTerrainElevation(lnglat: LngLatLike, options: ?ElevationQueryOptions): number | null { - const elevation = this.transform.elevation; - if (elevation) { - options = extend({}, {exaggerated: true}, options); - return elevation.getAtPoint(MercatorCoordinate.fromLngLat(lnglat), null, options.exaggerated); - } - return null; - } + queryTerrainElevation( + lnglat: LngLatLike, + options: ?ElevationQueryOptions, + ): number | null { + const elevation = this.transform.elevation; + if (elevation) { + options = extend({}, {exaggerated: true}, options); + return elevation.getAtPoint( + MercatorCoordinate.fromLngLat(lnglat), + null, + options.exaggerated, + ); + } + return null; + } /** * Calculate the center of these two points in the viewport and use @@ -795,110 +898,141 @@ class Camera extends Evented { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ - _cameraForBounds(transform: Transform, p0: LngLatLike, p1: LngLatLike, bearing: number, pitch: number, options?: CameraOptions): ?EasingOptions { - if (transform.projection.name === 'globe') { - return this._cameraForBoundsOnGlobe(transform, p0, p1, bearing, pitch, options); - } - - const tr = transform.clone(); - const eOptions = this._extendCameraOptions(options); - const edgePadding = tr.padding; - - tr.bearing = bearing; - tr.pitch = pitch; - - const coord0 = LngLat.convert(p0); - const coord1 = LngLat.convert(p1); - const coord2 = new LngLat(coord0.lng, coord1.lat); - const coord3 = new LngLat(coord1.lng, coord0.lat); - - const p0world = tr.project(coord0); - const p1world = tr.project(coord1); - - const z0 = this.queryTerrainElevation(coord0); - const z1 = this.queryTerrainElevation(coord1); - const z2 = this.queryTerrainElevation(coord2); - const z3 = this.queryTerrainElevation(coord3); - - const worldCoords = [ - [p0world.x, p0world.y, Math.min(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], - [p1world.x, p1world.y, Math.max(z0 || 0, z1 || 0, z2 || 0, z3 || 0)] - ]; - - let aabb = Aabb.fromPoints(worldCoords); - - const worldToCamera = tr.getWorldToCameraMatrix(); - const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); - - aabb = Aabb.applyTransform(aabb, worldToCamera); - - const size = vec3.sub([], aabb.max, aabb.min); - - const screenPadL = edgePadding.left || 0; - const screenPadR = edgePadding.right || 0; - const screenPadB = edgePadding.bottom || 0; - const screenPadT = edgePadding.top || 0; - - const {left: padL, right: padR, top: padT, bottom: padB} = eOptions.padding; - - const halfScreenPadX = (screenPadL + screenPadR) * 0.5; - const halfScreenPadY = (screenPadT + screenPadB) * 0.5; - - const scaleX = (tr.width - (screenPadL + screenPadR + padL + padR)) / size[0]; - const scaleY = (tr.height - (screenPadB + screenPadT + padB + padT)) / size[1]; - - const zoomRef = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), eOptions.maxZoom); - - const scaleRatio = tr.scale / tr.zoomScale(zoomRef); - - aabb = new Aabb( - [aabb.min[0] - (padL + halfScreenPadX) * scaleRatio, aabb.min[1] - (padB + halfScreenPadY) * scaleRatio, aabb.min[2]], - [aabb.max[0] + (padR + halfScreenPadX) * scaleRatio, aabb.max[1] + (padT + halfScreenPadY) * scaleRatio, aabb.max[2]]); - - const aabbHalfExtentZ = size[2] * 0.5; - const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); + _cameraForBounds( + transform: Transform, + p0: LngLatLike, + p1: LngLatLike, + bearing: number, + pitch: number, + options?: CameraOptions, + ): ?EasingOptions { + if (transform.projection.name === 'globe') { + return this._cameraForBoundsOnGlobe( + transform, + p0, + p1, + bearing, + pitch, + options, + ); + } + + const tr = transform.clone(); + const eOptions = this._extendCameraOptions(options); + const edgePadding = tr.padding; + + tr.bearing = bearing; + tr.pitch = pitch; + + const coord0 = LngLat.convert(p0); + const coord1 = LngLat.convert(p1); + const coord2 = new LngLat(coord0.lng, coord1.lat); + const coord3 = new LngLat(coord1.lng, coord0.lat); + + const p0world = tr.project(coord0); + const p1world = tr.project(coord1); + + const z0 = this.queryTerrainElevation(coord0); + const z1 = this.queryTerrainElevation(coord1); + const z2 = this.queryTerrainElevation(coord2); + const z3 = this.queryTerrainElevation(coord3); + + const worldCoords = [ + [p0world.x, p0world.y, Math.min(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], + [p1world.x, p1world.y, Math.max(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], + ]; + + let aabb = Aabb.fromPoints(worldCoords); + + const worldToCamera = tr.getWorldToCameraMatrix(); + const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); + + aabb = Aabb.applyTransform(aabb, worldToCamera); + + const size = vec3.sub([], aabb.max, aabb.min); + + const screenPadL = edgePadding.left || 0; + const screenPadR = edgePadding.right || 0; + const screenPadB = edgePadding.bottom || 0; + const screenPadT = edgePadding.top || 0; + + const {left: padL, right: padR, top: padT, bottom: padB} = eOptions.padding; + + const halfScreenPadX = (screenPadL + screenPadR) * 0.5; + const halfScreenPadY = (screenPadT + screenPadB) * 0.5; + + const scaleX = (tr.width - (screenPadL + screenPadR + padL + padR)) / size[0]; + const scaleY = (tr.height - (screenPadB + screenPadT + padB + padT)) / size[1]; + + const zoomRef = Math.min( + tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), + eOptions.maxZoom, + ); + + const scaleRatio = tr.scale / tr.zoomScale(zoomRef); + + aabb = new Aabb( + [ + aabb.min[0] - (padL + halfScreenPadX) * scaleRatio, + aabb.min[1] - (padB + halfScreenPadY) * scaleRatio, + aabb.min[2], + ], + [ + aabb.max[0] + (padR + halfScreenPadX) * scaleRatio, + aabb.max[1] + (padT + halfScreenPadY) * scaleRatio, + aabb.max[2], + ], + ); + + const aabbHalfExtentZ = size[2] * 0.5; + const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); + + const normalZ = [0, 0, 1, 0]; + + vec4.transformMat4(normalZ, normalZ, worldToCamera); + vec4.normalize(normalZ, normalZ); + + const offset = vec3.scale([], normalZ, frustumDistance + aabbHalfExtentZ); + const cameraPosition = vec3.add([], aabb.center, offset); + + const centerOffset = typeof eOptions.offset.x === 'number' && + typeof eOptions.offset.y === 'number' ? + new Point(eOptions.offset.x, eOptions.offset.y) : + Point.convert(eOptions.offset); + + const rotatedOffset = centerOffset.rotate(-degToRad(bearing)); + + aabb.center[0] -= rotatedOffset.x * scaleRatio; + aabb.center[1] += rotatedOffset.y * scaleRatio; - const normalZ = [0, 0, 1, 0]; + vec3.transformMat4(aabb.center, aabb.center, cameraToWorld); + vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); - vec4.transformMat4(normalZ, normalZ, worldToCamera); - vec4.normalize(normalZ, normalZ); + const mercator = [ + aabb.center[0], + aabb.center[1], + cameraPosition[2] * tr.pixelsPerMeter, + ]; + vec3.scale(mercator, mercator, 1.0 / tr.worldSize); - const offset = vec3.scale([], normalZ, frustumDistance + aabbHalfExtentZ); - const cameraPosition = vec3.add([], aabb.center, offset); + const lng = lngFromMercatorX(mercator[0]); + const lat = latFromMercatorY(mercator[1]); - const centerOffset = (typeof eOptions.offset.x === 'number' && typeof eOptions.offset.y === 'number') ? - new Point(eOptions.offset.x, eOptions.offset.y) : - Point.convert(eOptions.offset); + const zoom = Math.min(tr._zoomFromMercatorZ(mercator[2]), eOptions.maxZoom); + const center = new LngLat(lng, lat); - const rotatedOffset = centerOffset.rotate(-degToRad(bearing)); + const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; - aabb.center[0] -= rotatedOffset.x * scaleRatio; - aabb.center[1] += rotatedOffset.y * scaleRatio; + if (tr.mercatorFromTransition && zoom < halfZoomTransition) { + tr.setProjection({name: 'globe'}); + tr.zoom = zoom; + return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); + } - vec3.transformMat4(aabb.center, aabb.center, cameraToWorld); - vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); + return {center, zoom, bearing, pitch}; + } - const mercator = [aabb.center[0], aabb.center[1], cameraPosition[2] * tr.pixelsPerMeter]; - vec3.scale(mercator, mercator, 1.0 / tr.worldSize); - - const lng = lngFromMercatorX(mercator[0]); - const lat = latFromMercatorY(mercator[1]); - - const zoom = Math.min(tr._zoomFromMercatorZ(mercator[2]), eOptions.maxZoom); - const center = new LngLat(lng, lat); - - const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; - - if (tr.mercatorFromTransition && zoom < halfZoomTransition) { - tr.setProjection({name: 'globe'}); - tr.zoom = zoom; - return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); - } - - return {center, zoom, bearing, pitch}; - } - - /** + /** * Pans and zooms the map to contain its visible area within the specified geographical bounds. * If a padding is set on the map, the bounds are fit to the inset. * @@ -926,12 +1060,16 @@ class Camera extends Evented { * }); * @see [Example: Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/) */ - fitBounds(bounds: LngLatBoundsLike, options?: EasingOptions, eventData?: Object): this { - const cameraPlacement = this.cameraForBounds(bounds, options); - return this._fitInternal(cameraPlacement, options, eventData); - } - - /** + fitBounds( + bounds: LngLatBoundsLike, + options?: EasingOptions, + eventData?: Object, + ): this { + const cameraPlacement = this.cameraForBounds(bounds, options); + return this._fitInternal(cameraPlacement, options, eventData); + } + + /** * Pans, rotates and zooms the map to to fit the box made by points p0 and p1 * once the map is rotated to the specified bearing. To zoom without rotating, * pass in the current map bearing. @@ -962,51 +1100,77 @@ class Camera extends Evented { * }); * @see Used by {@link BoxZoomHandler} */ - fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: EasingOptions, eventData?: Object): this { - const screen0 = Point.convert(p0); - const screen1 = Point.convert(p1); - - const min = new Point(Math.min(screen0.x, screen1.x), Math.min(screen0.y, screen1.y)); - const max = new Point(Math.max(screen0.x, screen1.x), Math.max(screen0.y, screen1.y)); - - if (this.transform.projection.name === 'mercator' && this.transform.anyCornerOffEdge(screen0, screen1)) { - return this; - } - - const lnglat0 = this.transform.pointLocation3D(min); - const lnglat1 = this.transform.pointLocation3D(max); - const lnglat2 = this.transform.pointLocation3D(new Point(min.x, max.y)); - const lnglat3 = this.transform.pointLocation3D(new Point(max.x, min.y)); - - const p0coord = [ - Math.min(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), - Math.min(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), - ]; - const p1coord = [ - Math.max(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), - Math.max(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), - ]; - - const pitch = options && options.pitch ? options.pitch : this.getPitch(); - - const cameraPlacement = this._cameraForBounds(this.transform, p0coord, p1coord, bearing, pitch, options); - return this._fitInternal(cameraPlacement, options, eventData); - } - - _fitInternal(calculatedOptions?: ?EasingOptions, options?: EasingOptions, eventData?: Object): this { - // cameraForBounds warns + returns undefined if unable to fit: - if (!calculatedOptions) return this; - - options = extend(calculatedOptions, options); - // Explicitly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly. - delete options.padding; - - return options.linear ? - this.easeTo(options, eventData) : - this.flyTo(options, eventData); - } - - /** + fitScreenCoordinates( + p0: PointLike, + p1: PointLike, + bearing: number, + options?: EasingOptions, + eventData?: Object, + ): this { + const screen0 = Point.convert(p0); + const screen1 = Point.convert(p1); + + const min = new Point( + Math.min(screen0.x, screen1.x), + Math.min(screen0.y, screen1.y), + ); + const max = new Point( + Math.max(screen0.x, screen1.x), + Math.max(screen0.y, screen1.y), + ); + + if ( + this.transform.projection.name === 'mercator' && + this.transform.anyCornerOffEdge(screen0, screen1) + ) { + return this; + } + + const lnglat0 = this.transform.pointLocation3D(min); + const lnglat1 = this.transform.pointLocation3D(max); + const lnglat2 = this.transform.pointLocation3D(new Point(min.x, max.y)); + const lnglat3 = this.transform.pointLocation3D(new Point(max.x, min.y)); + + const p0coord = [ + Math.min(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), + Math.min(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), + ]; + const p1coord = [ + Math.max(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), + Math.max(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), + ]; + + const pitch = options && options.pitch ? options.pitch : this.getPitch(); + + const cameraPlacement = this._cameraForBounds( + this.transform, + p0coord, + p1coord, + bearing, + pitch, + options, + ); + return this._fitInternal(cameraPlacement, options, eventData); + } + + _fitInternal( + calculatedOptions?: ?EasingOptions, + options?: EasingOptions, + eventData?: Object, + ): this { + // cameraForBounds warns + returns undefined if unable to fit: + if (!calculatedOptions) return this; + + options = extend(calculatedOptions, options); + // Explicitly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly. + delete options.padding; + + return options.linear ? + this.easeTo(options, eventData) : + this.flyTo(options, eventData); + } + + /** * Changes any combination of center, zoom, bearing, and pitch, without * an animated transition. The map will retain its current values for any * details not specified in `options`. @@ -1038,67 +1202,72 @@ class Camera extends Evented { * @see [Example: Jump to a series of locations](https://docs.mapbox.com/mapbox-gl-js/example/jump-to/) * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ - jumpTo(options: CameraOptions & {preloadOnly?: $PropertyType}, eventData?: Object): this { - this.stop(); - - const tr = options.preloadOnly ? this.transform.clone() : this.transform; - let zoomChanged = false, - bearingChanged = false, - pitchChanged = false; - - if ('zoom' in options && tr.zoom !== +options.zoom) { - zoomChanged = true; - tr.zoom = +options.zoom; - } - - if (options.center !== undefined) { - tr.center = LngLat.convert(options.center); - } - - if ('bearing' in options && tr.bearing !== +options.bearing) { - bearingChanged = true; - tr.bearing = +options.bearing; - } - - if ('pitch' in options && tr.pitch !== +options.pitch) { - pitchChanged = true; - tr.pitch = +options.pitch; - } - - if (options.padding != null && !tr.isPaddingEqual(options.padding)) { - tr.padding = options.padding; - } - - if (options.preloadOnly) { - this._preloadTiles(tr); - return this; - } - - this.fire(new Event('movestart', eventData)) - .fire(new Event('move', eventData)); - - if (zoomChanged) { - this.fire(new Event('zoomstart', eventData)) - .fire(new Event('zoom', eventData)) - .fire(new Event('zoomend', eventData)); - } - - if (bearingChanged) { - this.fire(new Event('rotatestart', eventData)) - .fire(new Event('rotate', eventData)) - .fire(new Event('rotateend', eventData)); - } - - if (pitchChanged) { - this.fire(new Event('pitchstart', eventData)) - .fire(new Event('pitch', eventData)) - .fire(new Event('pitchend', eventData)); - } - - return this.fire(new Event('moveend', eventData)); - } - - /** + jumpTo( + options: & CameraOptions + & { preloadOnly?: $PropertyType }, + eventData?: Object, + ): this { + this.stop(); + + const tr = options.preloadOnly ? this.transform.clone() : this.transform; + let zoomChanged = false, + bearingChanged = false, + pitchChanged = false; + + if ('zoom' in options && tr.zoom !== +options.zoom) { + zoomChanged = true; + tr.zoom = +options.zoom; + } + + if (options.center !== undefined) { + tr.center = LngLat.convert(options.center); + } + + if ('bearing' in options && tr.bearing !== +options.bearing) { + bearingChanged = true; + tr.bearing = +options.bearing; + } + + if ('pitch' in options && tr.pitch !== +options.pitch) { + pitchChanged = true; + tr.pitch = +options.pitch; + } + + if (options.padding != null && !tr.isPaddingEqual(options.padding)) { + tr.padding = options.padding; + } + + if (options.preloadOnly) { + this._preloadTiles(tr); + return this; + } + + this.fire(new Event('movestart', eventData)).fire( + new Event('move', eventData), + ); + + if (zoomChanged) { + this.fire(new Event('zoomstart', eventData)).fire( + new Event('zoom', eventData), + ).fire(new Event('zoomend', eventData)); + } + + if (bearingChanged) { + this.fire(new Event('rotatestart', eventData)).fire( + new Event('rotate', eventData), + ).fire(new Event('rotateend', eventData)); + } + + if (pitchChanged) { + this.fire(new Event('pitchstart', eventData)).fire( + new Event('pitch', eventData), + ).fire(new Event('pitchend', eventData)); + } + + return this.fire(new Event('moveend', eventData)); + } + + /** * Returns position and orientation of the camera entity. * * This method is not supported for projections other than mercator. @@ -1116,14 +1285,14 @@ class Camera extends Evented { * * map.setFreeCameraOptions(camera); */ - getFreeCameraOptions(): FreeCameraOptions { - if (!this.transform.projection.supportsFreeCamera) { - warnOnce(freeCameraNotSupportedWarning); - } - return this.transform.getFreeCameraOptions(); - } - - /** + getFreeCameraOptions(): FreeCameraOptions { + if (!this.transform.projection.supportsFreeCamera) { + warnOnce(freeCameraNotSupportedWarning); + } + return this.transform.getFreeCameraOptions(); + } + + /** * `FreeCameraOptions` provides more direct access to the underlying camera entity. * For backwards compatibility the state set using this API must be representable with * `CameraOptions` as well. Parameters are clamped into a valid range or discarded as invalid @@ -1158,52 +1327,53 @@ class Camera extends Evented { * * map.setFreeCameraOptions(camera); */ - setFreeCameraOptions(options: FreeCameraOptions, eventData?: Object): this { - const tr = this.transform; + setFreeCameraOptions(options: FreeCameraOptions, eventData?: Object): this { + const tr = this.transform; - if (!tr.projection.supportsFreeCamera) { - warnOnce(freeCameraNotSupportedWarning); - return this; - } + if (!tr.projection.supportsFreeCamera) { + warnOnce(freeCameraNotSupportedWarning); + return this; + } - this.stop(); + this.stop(); - const prevZoom = tr.zoom; - const prevPitch = tr.pitch; - const prevBearing = tr.bearing; + const prevZoom = tr.zoom; + const prevPitch = tr.pitch; + const prevBearing = tr.bearing; - tr.setFreeCameraOptions(options); + tr.setFreeCameraOptions(options); - const zoomChanged = prevZoom !== tr.zoom; - const pitchChanged = prevPitch !== tr.pitch; - const bearingChanged = prevBearing !== tr.bearing; + const zoomChanged = prevZoom !== tr.zoom; + const pitchChanged = prevPitch !== tr.pitch; + const bearingChanged = prevBearing !== tr.bearing; - this.fire(new Event('movestart', eventData)) - .fire(new Event('move', eventData)); + this.fire(new Event('movestart', eventData)).fire( + new Event('move', eventData), + ); - if (zoomChanged) { - this.fire(new Event('zoomstart', eventData)) - .fire(new Event('zoom', eventData)) - .fire(new Event('zoomend', eventData)); - } + if (zoomChanged) { + this.fire(new Event('zoomstart', eventData)).fire( + new Event('zoom', eventData), + ).fire(new Event('zoomend', eventData)); + } - if (bearingChanged) { - this.fire(new Event('rotatestart', eventData)) - .fire(new Event('rotate', eventData)) - .fire(new Event('rotateend', eventData)); - } + if (bearingChanged) { + this.fire(new Event('rotatestart', eventData)).fire( + new Event('rotate', eventData), + ).fire(new Event('rotateend', eventData)); + } - if (pitchChanged) { - this.fire(new Event('pitchstart', eventData)) - .fire(new Event('pitch', eventData)) - .fire(new Event('pitchend', eventData)); - } + if (pitchChanged) { + this.fire(new Event('pitchstart', eventData)).fire( + new Event('pitch', eventData), + ).fire(new Event('pitchend', eventData)); + } - this.fire(new Event('moveend', eventData)); - return this; - } + this.fire(new Event('moveend', eventData)); + return this; + } - /** + /** * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition * between old and new values. The map will retain its current values for any * details not specified in `options`. @@ -1244,199 +1414,219 @@ class Camera extends Evented { * }); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - easeTo(options: EasingOptions & {easeId?: string}, eventData?: Object): this { - this._stop(false, options.easeId); - - options = extend({ - offset: [0, 0], - duration: 500, - easing: defaultEasing - }, options); - - if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0; - - const tr = this.transform, - startZoom = this.getZoom(), - startBearing = this.getBearing(), - startPitch = this.getPitch(), - startPadding = this.getPadding(), - - zoom = 'zoom' in options ? +options.zoom : startZoom, - bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing, - pitch = 'pitch' in options ? +options.pitch : startPitch, - padding = 'padding' in options ? options.padding : tr.padding; - - const offsetAsPoint = Point.convert(options.offset); - - let pointAtOffset; - let from; - let delta; - - if (tr.projection.name === 'globe') { - // Pixel coordinates will be applied directly to translate the globe - const centerCoord = MercatorCoordinate.fromLngLat(tr.center); - - const rotatedOffset = offsetAsPoint.rotate(-tr.angle); - centerCoord.x += rotatedOffset.x / tr.worldSize; - centerCoord.y += rotatedOffset.y / tr.worldSize; - - const locationAtOffset = centerCoord.toLngLat(); - const center = LngLat.convert(options.center || locationAtOffset); - this._normalizeCenter(center); - - pointAtOffset = tr.centerPoint.add(rotatedOffset); - from = new Point(centerCoord.x, centerCoord.y).mult(tr.worldSize); - delta = new Point(mercatorXfromLng(center.lng), mercatorYfromLat(center.lat)).mult(tr.worldSize).sub(from); - } else { - pointAtOffset = tr.centerPoint.add(offsetAsPoint); - const locationAtOffset = tr.pointLocation(pointAtOffset); - const center = LngLat.convert(options.center || locationAtOffset); - this._normalizeCenter(center); - - from = tr.project(locationAtOffset); - delta = tr.project(center).sub(from); - } - const finalScale = tr.zoomScale(zoom - startZoom); - - let around, aroundPoint; - - if (options.around) { - around = LngLat.convert(options.around); - aroundPoint = tr.locationPoint(around); - } - - const zoomChanged = this._zooming || (zoom !== startZoom); - const bearingChanged = this._rotating || (startBearing !== bearing); - const pitchChanged = this._pitching || (pitch !== startPitch); - const paddingChanged = !tr.isPaddingEqual(padding); - - const frame = (tr) => (k) => { - if (zoomChanged) { - tr.zoom = interpolate(startZoom, zoom, k); - } - if (bearingChanged) { - tr.bearing = interpolate(startBearing, bearing, k); - } - if (pitchChanged) { - tr.pitch = interpolate(startPitch, pitch, k); - } - if (paddingChanged) { - tr.interpolatePadding(startPadding, padding, k); - // When padding is being applied, Transform#centerPoint is changing continuously, - // thus we need to recalculate offsetPoint every fra,e - pointAtOffset = tr.centerPoint.add(offsetAsPoint); - } - - if (around) { - tr.setLocationAtPoint(around, aroundPoint); - } else { - const scale = tr.zoomScale(tr.zoom - startZoom); - const base = zoom > startZoom ? - Math.min(2, finalScale) : - Math.max(0.5, finalScale); - const speedup = Math.pow(base, 1 - k); - const newCenter = tr.unproject(from.add(delta.mult(k * speedup)).mult(scale)); - tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); - } - - if (!options.preloadOnly) { - this._fireMoveEvents(eventData); - } - - return tr; - }; - - if (options.preloadOnly) { - const predictedTransforms = this._emulate(frame, options.duration, tr); - this._preloadTiles(predictedTransforms); - return this; - } - - const currently = { - moving: this._moving, - zooming: this._zooming, - rotating: this._rotating, - pitching: this._pitching - }; - - this._zooming = zoomChanged; - this._rotating = bearingChanged; - this._pitching = pitchChanged; - this._padding = paddingChanged; - - this._easeId = options.easeId; - this._prepareEase(eventData, options.noMoveStart, currently); - - this._ease(frame(tr), (interruptingEaseId?: string) => { - tr.recenterOnTerrain(); - this._afterEase(eventData, interruptingEaseId); - }, options); - - return this; - } - - _prepareEase(eventData?: Object, noMoveStart: boolean, currently: Object = {}) { - this._moving = true; - this.transform.cameraElevationReference = "sea"; - - if (!noMoveStart && !currently.moving) { - this.fire(new Event('movestart', eventData)); - } - if (this._zooming && !currently.zooming) { - this.fire(new Event('zoomstart', eventData)); - } - if (this._rotating && !currently.rotating) { - this.fire(new Event('rotatestart', eventData)); - } - if (this._pitching && !currently.pitching) { - this.fire(new Event('pitchstart', eventData)); - } - } - - _fireMoveEvents(eventData?: Object) { - this.fire(new Event('move', eventData)); - if (this._zooming) { - this.fire(new Event('zoom', eventData)); - } - if (this._rotating) { - this.fire(new Event('rotate', eventData)); - } - if (this._pitching) { - this.fire(new Event('pitch', eventData)); - } - } - - _afterEase(eventData?: Object, easeId?: string) { - // if this easing is being stopped to start another easing with - // the same id then don't fire any events to avoid extra start/stop events - if (this._easeId && easeId && this._easeId === easeId) { - return; - } - this._easeId = undefined; - this.transform.cameraElevationReference = "ground"; - - const wasZooming = this._zooming; - const wasRotating = this._rotating; - const wasPitching = this._pitching; - this._moving = false; - this._zooming = false; - this._rotating = false; - this._pitching = false; - this._padding = false; - - if (wasZooming) { - this.fire(new Event('zoomend', eventData)); - } - if (wasRotating) { - this.fire(new Event('rotateend', eventData)); - } - if (wasPitching) { - this.fire(new Event('pitchend', eventData)); - } - this.fire(new Event('moveend', eventData)); - } - - /** + easeTo(options: EasingOptions & { easeId?: string }, eventData?: Object): this { + this._stop(false, options.easeId); + + options = extend( + { + offset: [0, 0], + duration: 500, + easing: defaultEasing, + }, + options, + ); + + if ( + options.animate === false || + !options.essential && browser.prefersReducedMotion + ) + options.duration = 0; + + const tr = this.transform, + startZoom = this.getZoom(), + startBearing = this.getBearing(), + startPitch = this.getPitch(), + startPadding = this.getPadding(), + zoom = 'zoom' in options ? +options.zoom : startZoom, + bearing = 'bearing' in options ? + this._normalizeBearing(options.bearing, startBearing) : + startBearing, + pitch = 'pitch' in options ? +options.pitch : startPitch, + padding = 'padding' in options ? options.padding : tr.padding; + + const offsetAsPoint = Point.convert(options.offset); + + let pointAtOffset; + let from; + let delta; + + if (tr.projection.name === 'globe') { + // Pixel coordinates will be applied directly to translate the globe + const centerCoord = MercatorCoordinate.fromLngLat(tr.center); + + const rotatedOffset = offsetAsPoint.rotate(-tr.angle); + centerCoord.x += rotatedOffset.x / tr.worldSize; + centerCoord.y += rotatedOffset.y / tr.worldSize; + + const locationAtOffset = centerCoord.toLngLat(); + const center = LngLat.convert(options.center || locationAtOffset); + this._normalizeCenter(center); + + pointAtOffset = tr.centerPoint.add(rotatedOffset); + from = new Point(centerCoord.x, centerCoord.y).mult(tr.worldSize); + delta = new Point( + mercatorXfromLng(center.lng), + mercatorYfromLat(center.lat), + ).mult(tr.worldSize).sub(from); + } else { + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + const locationAtOffset = tr.pointLocation(pointAtOffset); + const center = LngLat.convert(options.center || locationAtOffset); + this._normalizeCenter(center); + + from = tr.project(locationAtOffset); + delta = tr.project(center).sub(from); + } + const finalScale = tr.zoomScale(zoom - startZoom); + + let around, aroundPoint; + + if (options.around) { + around = LngLat.convert(options.around); + aroundPoint = tr.locationPoint(around); + } + + const zoomChanged = this._zooming || zoom !== startZoom; + const bearingChanged = this._rotating || startBearing !== bearing; + const pitchChanged = this._pitching || pitch !== startPitch; + const paddingChanged = !tr.isPaddingEqual(padding); + + const frame = (tr => k => { + if (zoomChanged) { + tr.zoom = interpolate(startZoom, zoom, k); + } + if (bearingChanged) { + tr.bearing = interpolate(startBearing, bearing, k); + } + if (pitchChanged) { + tr.pitch = interpolate(startPitch, pitch, k); + } + if (paddingChanged) { + tr.interpolatePadding(startPadding, padding, k); + // When padding is being applied, Transform#centerPoint is changing continuously, + // thus we need to recalculate offsetPoint every fra,e + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + } + + if (around) { + tr.setLocationAtPoint(around, aroundPoint); + } else { + const scale = tr.zoomScale(tr.zoom - startZoom); + const base = zoom > startZoom ? + Math.min(2, finalScale) : + Math.max(0.5, finalScale); + const speedup = Math.pow(base, 1 - k); + const newCenter = tr.unproject( + from.add(delta.mult(k * speedup)).mult(scale), + ); + tr.setLocationAtPoint( + tr.renderWorldCopies ? newCenter.wrap() : newCenter, + pointAtOffset, + ); + } + + if (!options.preloadOnly) { + this._fireMoveEvents(eventData); + } + + return tr; + }); + + if (options.preloadOnly) { + const predictedTransforms = this._emulate(frame, options.duration, tr); + this._preloadTiles(predictedTransforms); + return this; + } + + const currently = { + moving: this._moving, + zooming: this._zooming, + rotating: this._rotating, + pitching: this._pitching, + }; + + this._zooming = zoomChanged; + this._rotating = bearingChanged; + this._pitching = pitchChanged; + this._padding = paddingChanged; + + this._easeId = options.easeId; + this._prepareEase(eventData, options.noMoveStart, currently); + + this._ease( + frame(tr), + (interruptingEaseId?: string) => { + tr.recenterOnTerrain(); + this._afterEase(eventData, interruptingEaseId); + }, + options, + ); + + return this; + } + + _prepareEase(eventData?: Object, noMoveStart: boolean, currently: Object = {}) { + this._moving = true; + this.transform.cameraElevationReference = "sea"; + + if (!noMoveStart && !currently.moving) { + this.fire(new Event('movestart', eventData)); + } + if (this._zooming && !currently.zooming) { + this.fire(new Event('zoomstart', eventData)); + } + if (this._rotating && !currently.rotating) { + this.fire(new Event('rotatestart', eventData)); + } + if (this._pitching && !currently.pitching) { + this.fire(new Event('pitchstart', eventData)); + } + } + + _fireMoveEvents(eventData?: Object) { + this.fire(new Event('move', eventData)); + if (this._zooming) { + this.fire(new Event('zoom', eventData)); + } + if (this._rotating) { + this.fire(new Event('rotate', eventData)); + } + if (this._pitching) { + this.fire(new Event('pitch', eventData)); + } + } + + _afterEase(eventData?: Object, easeId?: string) { + // if this easing is being stopped to start another easing with + // the same id then don't fire any events to avoid extra start/stop events + if (this._easeId && easeId && this._easeId === easeId) { + return; + } + this._easeId = undefined; + this.transform.cameraElevationReference = "ground"; + + const wasZooming = this._zooming; + const wasRotating = this._rotating; + const wasPitching = this._pitching; + this._moving = false; + this._zooming = false; + this._rotating = false; + this._pitching = false; + this._padding = false; + + if (wasZooming) { + this.fire(new Event('zoomend', eventData)); + } + if (wasRotating) { + this.fire(new Event('rotateend', eventData)); + } + if (wasPitching) { + this.fire(new Event('pitchend', eventData)); + } + this.fire(new Event('moveend', eventData)); + } + + /** * Changes any combination of center, zoom, bearing, and pitch, animating the transition along a curve that * evokes flight. The animation seamlessly incorporates zooming and panning to help * the user maintain their bearings even after traversing a great distance. @@ -1495,185 +1685,218 @@ class Camera extends Evented { * @see [Example: Slowly fly to a location](https://www.mapbox.com/mapbox-gl-js/example/flyto-options/) * @see [Example: Fly to a location based on scroll position](https://www.mapbox.com/mapbox-gl-js/example/scroll-fly-to/) */ - flyTo(options: EasingOptions, eventData?: Object): this { - // Fall through to jumpTo if user has set prefers-reduced-motion - if (!options.essential && browser.prefersReducedMotion) { - const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'around']); - return this.jumpTo(coercedOptions, eventData); - } - - // This method implements an “optimal path” animation, as detailed in: - // - // Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and panning.” INFOVIS - // ’03. pp. 15–22. . - // - // Where applicable, local variable documentation begins with the associated variable or - // function in van Wijk (2003). - - this.stop(); - - options = extend({ - offset: [0, 0], - speed: 1.2, - curve: 1.42, - easing: defaultEasing - }, options); - - const tr = this.transform, - startZoom = this.getZoom(), - startBearing = this.getBearing(), - startPitch = this.getPitch(), - startPadding = this.getPadding(); - - const zoom = 'zoom' in options ? clamp(+options.zoom, tr.minZoom, tr.maxZoom) : startZoom; - const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing; - const pitch = 'pitch' in options ? +options.pitch : startPitch; - const padding = 'padding' in options ? options.padding : tr.padding; - - const scale = tr.zoomScale(zoom - startZoom); - const offsetAsPoint = Point.convert(options.offset); - let pointAtOffset = tr.centerPoint.add(offsetAsPoint); - const locationAtOffset = tr.pointLocation(pointAtOffset); - const center = LngLat.convert(options.center || locationAtOffset); - this._normalizeCenter(center); - - const from = tr.project(locationAtOffset); - const delta = tr.project(center).sub(from); - - let rho = options.curve; - - // w₀: Initial visible span, measured in pixels at the initial scale. - const w0 = Math.max(tr.width, tr.height), - // w₁: Final visible span, measured in pixels with respect to the initial scale. - w1 = w0 / scale, - // Length of the flight path as projected onto the ground plane, measured in pixels from - // the world image origin at the initial scale. - u1 = delta.mag(); - - if ('minZoom' in options) { - const minZoom = clamp(Math.min(options.minZoom, startZoom, zoom), tr.minZoom, tr.maxZoom); - // wm: Maximum visible span, measured in pixels with respect to the initial - // scale. - const wMax = w0 / tr.zoomScale(minZoom - startZoom); - rho = Math.sqrt(wMax / u1 * 2); - } - - // ρ² - const rho2 = rho * rho; - - /** + flyTo(options: EasingOptions, eventData?: Object): this { + // Fall through to jumpTo if user has set prefers-reduced-motion + if (!options.essential && browser.prefersReducedMotion) { + const coercedOptions = pick( + options, + ['center', 'zoom', 'bearing', 'pitch', 'around'], + ); + return this.jumpTo(coercedOptions, eventData); + } + + // This method implements an “optimal path” animation, as detailed in: + // + // Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and panning.” INFOVIS + // ’03. pp. 15–22. . + // + // Where applicable, local variable documentation begins with the associated variable or + // function in van Wijk (2003). + + this.stop(); + + options = extend( + { + offset: [0, 0], + speed: 1.2, + curve: 1.42, + easing: defaultEasing, + }, + options, + ); + + const tr = this.transform, + startZoom = this.getZoom(), + startBearing = this.getBearing(), + startPitch = this.getPitch(), + startPadding = this.getPadding(); + + const zoom = 'zoom' in options ? + clamp(+options.zoom, tr.minZoom, tr.maxZoom) : + startZoom; + const bearing = 'bearing' in options ? + this._normalizeBearing(options.bearing, startBearing) : + startBearing; + const pitch = 'pitch' in options ? +options.pitch : startPitch; + const padding = 'padding' in options ? options.padding : tr.padding; + + const scale = tr.zoomScale(zoom - startZoom); + const offsetAsPoint = Point.convert(options.offset); + let pointAtOffset = tr.centerPoint.add(offsetAsPoint); + const locationAtOffset = tr.pointLocation(pointAtOffset); + const center = LngLat.convert(options.center || locationAtOffset); + this._normalizeCenter(center); + + const from = tr.project(locationAtOffset); + const delta = tr.project(center).sub(from); + + let rho = options.curve; + + // w₀: Initial visible span, measured in pixels at the initial scale. + const w0 = Math.max(tr.width, tr.height), + // w₁: Final visible span, measured in pixels with respect to the initial scale. + w1 = w0 / scale, + // Length of the flight path as projected onto the ground plane, measured in pixels from + // the world image origin at the initial scale. + u1 = delta.mag(); + + if ('minZoom' in options) { + const minZoom = clamp( + Math.min(options.minZoom, startZoom, zoom), + tr.minZoom, + tr.maxZoom, + ); + // wm: Maximum visible span, measured in pixels with respect to the initial + // scale. + const wMax = w0 / tr.zoomScale(minZoom - startZoom); + rho = Math.sqrt(wMax / u1 * 2); + } + + // ρ² + const rho2 = rho * rho; + + /** * rᵢ: Returns the zoom-out factor at one end of the animation. * * @param i 0 for the ascent or 1 for the descent. * @private */ - function r(i) { - const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1); - return Math.log(Math.sqrt(b * b + 1) - b); - } - - function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } - function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } - function tanh(n) { return sinh(n) / cosh(n); } - - // r₀: Zoom-out factor during ascent. - const r0 = r(0); - - // w(s): Returns the visible span on the ground, measured in pixels with respect to the - // initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°. - let w: (_: number) => number = function (s) { - return (cosh(r0) / cosh(r0 + rho * s)); - }; - - // u(s): Returns the distance along the flight path as projected onto the ground plane, - // measured in pixels from the world image origin at the initial scale. - let u: (_: number) => number = function (s) { - return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1; - }; - - // S: Total length of the flight path, measured in ρ-screenfuls. - let S = (r(1) - r0) / rho; - - // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. - if (Math.abs(u1) < 0.000001 || !isFinite(S)) { - // Perform a more or less instantaneous transition if the path is too short. - if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData); - - const k = w1 < w0 ? -1 : 1; - S = Math.abs(Math.log(w1 / w0)) / rho; - - u = function() { return 0; }; - w = function(s) { return Math.exp(k * rho * s); }; - } - - if ('duration' in options) { - options.duration = +options.duration; - } else { - const V = 'screenSpeed' in options ? +options.screenSpeed / rho : +options.speed; - options.duration = 1000 * S / V; - } - - if (options.maxDuration && options.duration > options.maxDuration) { - options.duration = 0; - } - - const zoomChanged = true; - const bearingChanged = (startBearing !== bearing); - const pitchChanged = (pitch !== startPitch); - const paddingChanged = !tr.isPaddingEqual(padding); - - const frame = (tr) => (k) => { - // s: The distance traveled along the flight path, measured in ρ-screenfuls. - const s = k * S; - const scale = 1 / w(s); - tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(scale); - - if (bearingChanged) { - tr.bearing = interpolate(startBearing, bearing, k); - } - if (pitchChanged) { - tr.pitch = interpolate(startPitch, pitch, k); - } - if (paddingChanged) { - tr.interpolatePadding(startPadding, padding, k); - // When padding is being applied, Transform#centerPoint is changing continuously, - // thus we need to recalculate offsetPoint every frame - pointAtOffset = tr.centerPoint.add(offsetAsPoint); - } - - const newCenter = k === 1 ? center : tr.unproject(from.add(delta.mult(u(s))).mult(scale)); - tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); - tr._updateCameraOnTerrain(); - - if (!options.preloadOnly) { - this._fireMoveEvents(eventData); - } - - return tr; - }; - - if (options.preloadOnly) { - const predictedTransforms = this._emulate(frame, options.duration, tr); - this._preloadTiles(predictedTransforms); - return this; - } - - this._zooming = zoomChanged; - this._rotating = bearingChanged; - this._pitching = pitchChanged; - this._padding = paddingChanged; - - this._prepareEase(eventData, false); - this._ease(frame(tr), () => this._afterEase(eventData), options); - - return this; - } - - isEasing(): boolean { - return !!this._easeFrameId; - } - - /** + function r(i) { + const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? + w1 : + w0) * rho2 * u1); + return Math.log(Math.sqrt(b * b + 1) - b); + } + + function sinh(n) { + return (Math.exp(n) - Math.exp(-n)) / 2; + } + function cosh(n) { + return (Math.exp(n) + Math.exp(-n)) / 2; + } + function tanh(n) { + return sinh(n) / cosh(n); + } + + // r₀: Zoom-out factor during ascent. + const r0 = r(0); + + // w(s): Returns the visible span on the ground, measured in pixels with respect to the + // initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°. + let w: (_: number) => number = function(s) { + return cosh(r0) / cosh(r0 + rho * s); + }; + + // u(s): Returns the distance along the flight path as projected onto the ground plane, + // measured in pixels from the world image origin at the initial scale. + let u: (_: number) => number = function(s) { + return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1; + }; + + // S: Total length of the flight path, measured in ρ-screenfuls. + let S = (r(1) - r0) / rho; + + // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. + if (Math.abs(u1) < 0.000001 || !isFinite(S)) { + // Perform a more or less instantaneous transition if the path is too short. + if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData); + + const k = w1 < w0 ? -1 : 1; + S = Math.abs(Math.log(w1 / w0)) / rho; + + u = function() { + return 0; + }; + w = function(s) { + return Math.exp(k * rho * s); + }; + } + + if ('duration' in options) { + options.duration = +options.duration; + } else { + const V = 'screenSpeed' in options ? + +options.screenSpeed / rho : + +options.speed; + options.duration = 1000 * S / V; + } + + if (options.maxDuration && options.duration > options.maxDuration) { + options.duration = 0; + } + + const zoomChanged = true; + const bearingChanged = startBearing !== bearing; + const pitchChanged = pitch !== startPitch; + const paddingChanged = !tr.isPaddingEqual(padding); + + const frame = (tr => k => { + // s: The distance traveled along the flight path, measured in ρ-screenfuls. + const s = k * S; + const scale = 1 / w(s); + tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(scale); + + if (bearingChanged) { + tr.bearing = interpolate(startBearing, bearing, k); + } + if (pitchChanged) { + tr.pitch = interpolate(startPitch, pitch, k); + } + if (paddingChanged) { + tr.interpolatePadding(startPadding, padding, k); + // When padding is being applied, Transform#centerPoint is changing continuously, + // thus we need to recalculate offsetPoint every frame + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + } + + const newCenter = k === 1 ? + center : + tr.unproject(from.add(delta.mult(u(s))).mult(scale)); + tr.setLocationAtPoint( + tr.renderWorldCopies ? newCenter.wrap() : newCenter, + pointAtOffset, + ); + tr._updateCameraOnTerrain(); + + if (!options.preloadOnly) { + this._fireMoveEvents(eventData); + } + + return tr; + }); + + if (options.preloadOnly) { + const predictedTransforms = this._emulate(frame, options.duration, tr); + this._preloadTiles(predictedTransforms); + return this; + } + + this._zooming = zoomChanged; + this._rotating = bearingChanged; + this._pitching = pitchChanged; + this._padding = paddingChanged; + + this._prepareEase(eventData, false); + this._ease(frame(tr), () => this._afterEase(eventData), options); + + return this; + } + + isEasing(): boolean { + return !!this._easeFrameId; + } + + /** * Stops any animated transition underway. * * @memberof Map# @@ -1681,94 +1904,105 @@ class Camera extends Evented { * @example * map.stop(); */ - stop(): this { - return this._stop(); - } - - _stop(allowGestures?: boolean, easeId?: string): this { - if (this._easeFrameId) { - this._cancelRenderFrame(this._easeFrameId); - this._easeFrameId = undefined; - this._onEaseFrame = undefined; - } - - if (this._onEaseEnd) { - // The _onEaseEnd function might emit events which trigger new - // animation, which sets a new _onEaseEnd. Ensure we don't delete - // it unintentionally. - const onEaseEnd = this._onEaseEnd; - this._onEaseEnd = undefined; - onEaseEnd.call(this, easeId); - } - if (!allowGestures) { - const handlers = (this: any).handlers; - if (handlers) handlers.stop(false); - } - return this; - } - - _ease(frame: (_: number) => Transform | void, - finish: () => void, - options: {animate: boolean, duration: number, easing: (_: number) => number}) { - if (options.animate === false || options.duration === 0) { - frame(1); - finish(); - } else { - this._easeStart = browser.now(); - this._easeOptions = options; - this._onEaseFrame = frame; - this._onEaseEnd = finish; - this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); - } - } - - // Callback for map._requestRenderFrame - _renderFrameCallback() { - const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1); - const frame = this._onEaseFrame; - if (frame) frame(this._easeOptions.easing(t)); - if (t < 1) { - this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); - } else { - this.stop(); - } - } - - // convert bearing so that it's numerically close to the current one so that it interpolates properly - _normalizeBearing(bearing: number, currentBearing: number): number { - bearing = wrap(bearing, -180, 180); - const diff = Math.abs(bearing - currentBearing); - if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360; - if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360; - return bearing; - } - - // If a path crossing the antimeridian would be shorter, extend the final coordinate so that - // interpolating between the two endpoints will cross it. - _normalizeCenter(center: LngLat) { - const tr = this.transform; - if (!tr.renderWorldCopies || tr.maxBounds) return; - - const delta = center.lng - tr.center.lng; - center.lng += - delta > 180 ? -360 : - delta < -180 ? 360 : 0; - } - - // emulates frame function for some transform - _emulate(frame: Function, duration: number, initialTransform: Transform): Array { - const frameRate = 15; - const numFrames = Math.ceil(duration * frameRate / 1000); - - const transforms = []; - const emulateFrame = frame(initialTransform.clone()); - for (let i = 0; i <= numFrames; i++) { - const transform = emulateFrame(i / numFrames); - transforms.push(transform.clone()); - } - - return transforms; - } + stop(): this { + return this._stop(); + } + + _stop(allowGestures?: boolean, easeId?: string): this { + if (this._easeFrameId) { + this._cancelRenderFrame(this._easeFrameId); + this._easeFrameId = undefined; + this._onEaseFrame = undefined; + } + + if (this._onEaseEnd) { + // The _onEaseEnd function might emit events which trigger new + // animation, which sets a new _onEaseEnd. Ensure we don't delete + // it unintentionally. + const onEaseEnd = this._onEaseEnd; + this._onEaseEnd = undefined; + onEaseEnd.call(this, easeId); + } + if (!allowGestures) { + const handlers = (this: any).handlers; + if (handlers) handlers.stop(false); + } + return this; + } + + _ease( + frame: (_: number) => Transform | void, + finish: () => void, + options: { + animate: boolean, + duration: number, + easing: (_: number) => number, + }, + ) { + if (options.animate === false || options.duration === 0) { + frame(1); + finish(); + } else { + this._easeStart = browser.now(); + this._easeOptions = options; + this._onEaseFrame = frame; + this._onEaseEnd = finish; + this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); + } + } + + // Callback for map._requestRenderFrame + _renderFrameCallback = () => { + const t = Math.min( + (browser.now() - this._easeStart) / this._easeOptions.duration, + 1, + ); + const frame = this._onEaseFrame; + if (frame) frame(this._easeOptions.easing(t)); + if (t < 1) { + this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); + } else { + this.stop(); + } + }; + + // convert bearing so that it's numerically close to the current one so that it interpolates properly + _normalizeBearing(bearing: number, currentBearing: number): number { + bearing = wrap(bearing, -180, 180); + const diff = Math.abs(bearing - currentBearing); + if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360; + if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360; + return bearing; + } + + // If a path crossing the antimeridian would be shorter, extend the final coordinate so that + // interpolating between the two endpoints will cross it. + _normalizeCenter(center: LngLat) { + const tr = this.transform; + if (!tr.renderWorldCopies || tr.maxBounds) return; + + const delta = center.lng - tr.center.lng; + center.lng += delta > 180 ? -360 : delta < -180 ? 360 : 0; + } + + // emulates frame function for some transform + _emulate( + frame: Function, + duration: number, + initialTransform: Transform, + ): Array { + const frameRate = 15; + const numFrames = Math.ceil(duration * frameRate / 1000); + + const transforms = []; + const emulateFrame = frame(initialTransform.clone()); + for (let i = 0; i <= numFrames; i++) { + const transform = emulateFrame(i / numFrames); + transforms.push(transform.clone()); + } + + return transforms; + } } // In debug builds, check that camera change events are fired in the correct order. diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index ee9dbe7d521..8fa0a30d5e8 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -27,187 +27,219 @@ type Options = { * })); */ class AttributionControl { - options: Options; - _map: Map; - _container: HTMLElement; - _innerContainer: HTMLElement; - _compactButton: HTMLButtonElement; - _editLink: ?HTMLAnchorElement; - _attribHTML: string; - styleId: string; - styleOwner: string; - - constructor(options: Options = {}) { - this.options = options; - - bindAll([ - '_toggleAttribution', - '_updateEditLink', - '_updateData', - '_updateCompact' - ], this); - } - - getDefaultPosition(): ControlPosition { - return 'bottom-right'; - } - - onAdd(map: Map): HTMLElement { - const compact = this.options && this.options.compact; - - this._map = map; - this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-attrib'); - this._compactButton = DOM.create('button', 'mapboxgl-ctrl-attrib-button', this._container); - DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute('aria-hidden', 'true'); - this._compactButton.type = 'button'; - this._compactButton.addEventListener('click', this._toggleAttribution); - this._setElementTitle(this._compactButton, 'ToggleAttribution'); - this._innerContainer = DOM.create('div', 'mapboxgl-ctrl-attrib-inner', this._container); - this._innerContainer.setAttribute('role', 'list'); - - if (compact) { - this._container.classList.add('mapboxgl-compact'); - } - - this._updateAttributions(); - this._updateEditLink(); - - this._map.on('styledata', this._updateData); - this._map.on('sourcedata', this._updateData); - this._map.on('moveend', this._updateEditLink); - - if (compact === undefined) { - this._map.on('resize', this._updateCompact); - this._updateCompact(); - } - - return this._container; - } - - onRemove() { - this._container.remove(); - - this._map.off('styledata', this._updateData); - this._map.off('sourcedata', this._updateData); - this._map.off('moveend', this._updateEditLink); - this._map.off('resize', this._updateCompact); - - this._map = (undefined: any); - this._attribHTML = (undefined: any); - } - - _setElementTitle(element: HTMLElement, title: string) { - const str = this._map._getUIString(`AttributionControl.${title}`); - element.setAttribute('aria-label', str); - element.removeAttribute('title'); - if (element.firstElementChild) element.firstElementChild.setAttribute('title', str); - } - - _toggleAttribution() { - if (this._container.classList.contains('mapboxgl-compact-show')) { - this._container.classList.remove('mapboxgl-compact-show'); - this._compactButton.setAttribute('aria-expanded', 'false'); - } else { - this._container.classList.add('mapboxgl-compact-show'); - this._compactButton.setAttribute('aria-expanded', 'true'); - } - } - - _updateEditLink() { - let editLink = this._editLink; - if (!editLink) { - editLink = this._editLink = (this._container.querySelector('.mapbox-improve-map'): any); - } - - const params = [ - {key: 'owner', value: this.styleOwner}, - {key: 'id', value: this.styleId}, - {key: 'access_token', value: this._map._requestManager._customAccessToken || config.ACCESS_TOKEN} - ]; - - if (editLink) { - const paramString = params.reduce((acc, next, i) => { - if (next.value) { - acc += `${next.key}=${next.value}${i < params.length - 1 ? '&' : ''}`; - } - return acc; - }, `?`); - editLink.href = `${config.FEEDBACK_URL}/${paramString}#${getHashString(this._map, true)}`; - editLink.rel = 'noopener nofollow'; - this._setElementTitle(editLink, 'MapFeedback'); - } - } - - _updateData(e: any) { - if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style')) { - this._updateAttributions(); - this._updateEditLink(); - } - } - - _updateAttributions() { - if (!this._map.style) return; - let attributions: Array = []; - - if (this._map.style.stylesheet) { - const stylesheet: any = this._map.style.stylesheet; - this.styleOwner = stylesheet.owner; - this.styleId = stylesheet.id; - } - - const sourceCaches = this._map.style._sourceCaches; - for (const id in sourceCaches) { - const sourceCache = sourceCaches[id]; - if (sourceCache.used) { - const source = sourceCache.getSource(); - if (source.attribution && attributions.indexOf(source.attribution) < 0) { - attributions.push(source.attribution); - } + options: Options; + _map: Map; + _container: HTMLElement; + _innerContainer: HTMLElement; + _compactButton: HTMLButtonElement; + _editLink: ?HTMLAnchorElement; + _attribHTML: string; + styleId: string; + styleOwner: string; + + constructor(options: Options = {}) { + this.options = options; + + bindAll( + ['_toggleAttribution', '_updateEditLink', '_updateData', '_updateCompact'], + this, + ); + } + + getDefaultPosition(): ControlPosition { + return 'bottom-right'; + } + + onAdd(map: Map): HTMLElement { + const compact = this.options && this.options.compact; + + this._map = map; + this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-attrib'); + this._compactButton = DOM.create( + 'button', + 'mapboxgl-ctrl-attrib-button', + this._container, + ); + DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute( + 'aria-hidden', + 'true', + ); + this._compactButton.type = 'button'; + this._compactButton.addEventListener('click', this._toggleAttribution); + this._setElementTitle(this._compactButton, 'ToggleAttribution'); + this._innerContainer = DOM.create( + 'div', + 'mapboxgl-ctrl-attrib-inner', + this._container, + ); + this._innerContainer.setAttribute('role', 'list'); + + if (compact) { + this._container.classList.add('mapboxgl-compact'); + } + + this._updateAttributions(); + this._updateEditLink(); + + this._map.on('styledata', this._updateData); + this._map.on('sourcedata', this._updateData); + this._map.on('moveend', this._updateEditLink); + + if (compact === undefined) { + this._map.on('resize', this._updateCompact); + this._updateCompact(); + } + + return this._container; + } + + onRemove() { + this._container.remove(); + + this._map.off('styledata', this._updateData); + this._map.off('sourcedata', this._updateData); + this._map.off('moveend', this._updateEditLink); + this._map.off('resize', this._updateCompact); + + this._map = (undefined: any); + this._attribHTML = (undefined: any); + } + + _setElementTitle(element: HTMLElement, title: string) { + const str = this._map._getUIString(`AttributionControl.${title}`); + element.setAttribute('aria-label', str); + element.removeAttribute('title'); + if (element.firstElementChild) + element.firstElementChild.setAttribute('title', str); + } + + _toggleAttribution = () => { + if (this._container.classList.contains('mapboxgl-compact-show')) { + this._container.classList.remove('mapboxgl-compact-show'); + this._compactButton.setAttribute('aria-expanded', 'false'); + } else { + this._container.classList.add('mapboxgl-compact-show'); + this._compactButton.setAttribute('aria-expanded', 'true'); + } + }; + + _updateEditLink = () => { + let editLink = this._editLink; + if (!editLink) { + editLink = this._editLink = (this._container.querySelector( + '.mapbox-improve-map', + ): any); + } + + const params = [ + {key: 'owner', value: this.styleOwner}, + {key: 'id', value: this.styleId}, + { + key: 'access_token', + value: this._map._requestManager._customAccessToken || + config.ACCESS_TOKEN, + }, + ]; + + if (editLink) { + const paramString = params.reduce( + (acc, next, i) => { + if (next.value) { + acc += `${next.key}=${next.value}${i < params.length - 1 ? '&' : ''}`; } - } - - // remove any entries that are substrings of another entry. - // first sort by length so that substrings come first - attributions.sort((a, b) => a.length - b.length); - attributions = attributions.filter((attrib, i) => { - for (let j = i + 1; j < attributions.length; j++) { - if (attributions[j].indexOf(attrib) >= 0) { return false; } - } - return true; - }); - - if (this.options.customAttribution) { - if (Array.isArray(this.options.customAttribution)) { - attributions = [...this.options.customAttribution, ...attributions]; - } else { - attributions.unshift(this.options.customAttribution); - } - } - - // check if attribution string is different to minimize DOM changes - const attribHTML = attributions.join(' | '); - if (attribHTML === this._attribHTML) return; - - this._attribHTML = attribHTML; - - if (attributions.length) { - this._innerContainer.innerHTML = attribHTML; - this._container.classList.remove('mapboxgl-attrib-empty'); - } else { - this._container.classList.add('mapboxgl-attrib-empty'); - } - // remove old DOM node from _editLink - this._editLink = null; - } - - _updateCompact() { - if (this._map.getCanvasContainer().offsetWidth <= 640) { - this._container.classList.add('mapboxgl-compact'); - } else { - this._container.classList.remove('mapboxgl-compact', 'mapboxgl-compact-show'); - } - } - + return acc; + }, + `?`, + ); + editLink.href = `${config.FEEDBACK_URL}/${paramString}#${getHashString( + this._map, + true, + )}`; + editLink.rel = 'noopener nofollow'; + this._setElementTitle(editLink, 'MapFeedback'); + } + }; + + _updateData = (e: any) => { + if ( + e && + (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || + e.dataType === 'style') + ) { + this._updateAttributions(); + this._updateEditLink(); + } + }; + + _updateAttributions() { + if (!this._map.style) return; + let attributions: Array = []; + + if (this._map.style.stylesheet) { + const stylesheet: any = this._map.style.stylesheet; + this.styleOwner = stylesheet.owner; + this.styleId = stylesheet.id; + } + + const sourceCaches = this._map.style._sourceCaches; + for (const id in sourceCaches) { + const sourceCache = sourceCaches[id]; + if (sourceCache.used) { + const source = sourceCache.getSource(); + if (source.attribution && attributions.indexOf(source.attribution) < 0) { + attributions.push(source.attribution); + } + } + } + + // remove any entries that are substrings of another entry. + // first sort by length so that substrings come first + attributions.sort((a, b) => a.length - b.length); + attributions = attributions.filter( + (attrib, i) => { + for (let j = i + 1; j < attributions.length; j++) { + if (attributions[j].indexOf(attrib) >= 0) { + return false; + } + } + return true; + }, + ); + + if (this.options.customAttribution) { + if (Array.isArray(this.options.customAttribution)) { + attributions = [...this.options.customAttribution, ...attributions]; + } else { + attributions.unshift(this.options.customAttribution); + } + } + + // check if attribution string is different to minimize DOM changes + const attribHTML = attributions.join(' | '); + if (attribHTML === this._attribHTML) return; + + this._attribHTML = attribHTML; + + if (attributions.length) { + this._innerContainer.innerHTML = attribHTML; + this._container.classList.remove('mapboxgl-attrib-empty'); + } else { + this._container.classList.add('mapboxgl-attrib-empty'); + } + // remove old DOM node from _editLink + this._editLink = null; + } + + _updateCompact = () => { + if (this._map.getCanvasContainer().offsetWidth <= 640) { + this._container.classList.add('mapboxgl-compact'); + } else { + this._container.classList.remove( + 'mapboxgl-compact', + 'mapboxgl-compact-show', + ); + } + }; } export default AttributionControl; diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index 1a43d233b4c..f87f53aadc4 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -25,108 +25,120 @@ type Options = { */ class FullscreenControl { - _map: Map; - _controlContainer: HTMLElement; - _fullscreen: boolean; - _fullscreenchange: string; - _fullscreenButton: HTMLElement; - _container: HTMLElement; - - constructor(options: Options) { - this._fullscreen = false; - if (options && options.container) { - if (options.container instanceof window.HTMLElement) { - this._container = options.container; - } else { - warnOnce('Full screen control \'container\' must be a DOM element.'); - } - } - bindAll([ - '_onClickFullscreen', - '_changeIcon' - ], this); - if ('onfullscreenchange' in window.document) { - this._fullscreenchange = 'fullscreenchange'; - } else if ('onwebkitfullscreenchange' in window.document) { - this._fullscreenchange = 'webkitfullscreenchange'; - } - } - - onAdd(map: Map): HTMLElement { - this._map = map; - if (!this._container) this._container = this._map.getContainer(); - this._controlContainer = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); - if (this._checkFullscreenSupport()) { - this._setupUI(); - } else { - this._controlContainer.style.display = 'none'; - warnOnce('This device does not support fullscreen mode.'); - } - return this._controlContainer; - } - - onRemove() { - this._controlContainer.remove(); - this._map = (null: any); - window.document.removeEventListener(this._fullscreenchange, this._changeIcon); - } - - _checkFullscreenSupport(): boolean { - return !!( - window.document.fullscreenEnabled || - (window.document: any).webkitFullscreenEnabled - ); - } - - _setupUI() { - const button = this._fullscreenButton = DOM.create('button', (`mapboxgl-ctrl-fullscreen`), this._controlContainer); - DOM.create('span', `mapboxgl-ctrl-icon`, button).setAttribute('aria-hidden', 'true'); - button.type = 'button'; - this._updateTitle(); - this._fullscreenButton.addEventListener('click', this._onClickFullscreen); - window.document.addEventListener(this._fullscreenchange, this._changeIcon); - } - - _updateTitle() { - const title = this._getTitle(); - this._fullscreenButton.setAttribute("aria-label", title); - if (this._fullscreenButton.firstElementChild) this._fullscreenButton.firstElementChild.setAttribute('title', title); - } - - _getTitle(): string { - return this._map._getUIString(this._isFullscreen() ? 'FullscreenControl.Exit' : 'FullscreenControl.Enter'); - } - - _isFullscreen(): boolean { - return this._fullscreen; - } - - _changeIcon() { - const fullscreenElement = - window.document.fullscreenElement || - (window.document: any).webkitFullscreenElement; - - if ((fullscreenElement === this._container) !== this._fullscreen) { - this._fullscreen = !this._fullscreen; - this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-shrink`); - this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-fullscreen`); - this._updateTitle(); - } - } - - _onClickFullscreen() { - if (this._isFullscreen()) { - if (window.document.exitFullscreen) { - (window.document: any).exitFullscreen(); - } else if (window.document.webkitCancelFullScreen) { - (window.document: any).webkitCancelFullScreen(); - } - } else if (this._container.requestFullscreen) { - this._container.requestFullscreen(); - } else if ((this._container: any).webkitRequestFullscreen) { - (this._container: any).webkitRequestFullscreen(); - } - } + _map: Map; + _controlContainer: HTMLElement; + _fullscreen: boolean; + _fullscreenchange: string; + _fullscreenButton: HTMLElement; + _container: HTMLElement; + + constructor(options: Options) { + this._fullscreen = false; + if (options && options.container) { + if (options.container instanceof window.HTMLElement) { + this._container = options.container; + } else { + warnOnce('Full screen control \'container\' must be a DOM element.'); + } + } + bindAll(['_onClickFullscreen', '_changeIcon'], this); + if ('onfullscreenchange' in window.document) { + this._fullscreenchange = 'fullscreenchange'; + } else if ('onwebkitfullscreenchange' in window.document) { + this._fullscreenchange = 'webkitfullscreenchange'; + } + } + + onAdd(map: Map): HTMLElement { + this._map = map; + if (!this._container) this._container = this._map.getContainer(); + this._controlContainer = DOM.create( + 'div', + `mapboxgl-ctrl mapboxgl-ctrl-group`, + ); + if (this._checkFullscreenSupport()) { + this._setupUI(); + } else { + this._controlContainer.style.display = 'none'; + warnOnce('This device does not support fullscreen mode.'); + } + return this._controlContainer; + } + + onRemove() { + this._controlContainer.remove(); + this._map = (null: any); + window.document.removeEventListener( + this._fullscreenchange, + this._changeIcon, + ); + } + + _checkFullscreenSupport(): boolean { + return !!(window.document.fullscreenEnabled || + (window.document: any).webkitFullscreenEnabled); + } + + _setupUI() { + const button = this._fullscreenButton = DOM.create( + 'button', + `mapboxgl-ctrl-fullscreen`, + this._controlContainer, + ); + DOM.create('span', `mapboxgl-ctrl-icon`, button).setAttribute( + 'aria-hidden', + 'true', + ); + button.type = 'button'; + this._updateTitle(); + this._fullscreenButton.addEventListener('click', this._onClickFullscreen); + window.document.addEventListener(this._fullscreenchange, this._changeIcon); + } + + _updateTitle() { + const title = this._getTitle(); + this._fullscreenButton.setAttribute("aria-label", title); + if (this._fullscreenButton.firstElementChild) + this._fullscreenButton.firstElementChild.setAttribute('title', title); + } + + _getTitle(): string { + return this._map._getUIString( + this._isFullscreen() ? + 'FullscreenControl.Exit' : + 'FullscreenControl.Enter', + ); + } + + _isFullscreen(): boolean { + return this._fullscreen; + } + + _changeIcon = () => { + const fullscreenElement = window.document.fullscreenElement || + (window.document: any).webkitFullscreenElement; + + if (fullscreenElement === this._container !== this._fullscreen) { + this._fullscreen = !this._fullscreen; + this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-shrink`); + this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-fullscreen`); + this._updateTitle(); + } + }; + + _onClickFullscreen = () => { + if (this._isFullscreen()) { + if (window.document.exitFullscreen) { + (window.document: any).exitFullscreen(); + } else if (window.document.webkitCancelFullScreen) { + (window.document: any).webkitCancelFullScreen(); + } + } else if (this._container.requestFullscreen) { + this._container.requestFullscreen(); + } else if ((this._container: any).webkitRequestFullscreen) { + (this._container: any).webkitRequestFullscreen(); + } + }; } export default FullscreenControl; diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 67a7badab45..077c8c6f98c 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -87,406 +87,496 @@ const defaultOptions = { * })); * @see [Example: Locate the user](https://www.mapbox.com/mapbox-gl-js/example/locate-user/) */ -class GeolocateControl extends Evented { - _map: Map; - options: Options; - _container: HTMLElement; - _dotElement: HTMLElement; - _circleElement: HTMLElement; - _geolocateButton: HTMLButtonElement; - _geolocationWatchID: number; - _timeoutId: ?TimeoutID; - _watchState: 'OFF' | 'ACTIVE_LOCK' | 'WAITING_ACTIVE' | 'ACTIVE_ERROR' | 'BACKGROUND' | 'BACKGROUND_ERROR'; - _lastKnownPosition: any; - _userLocationDotMarker: Marker; - _accuracyCircleMarker: Marker; - _accuracy: number; - _setup: boolean; // set to true once the control has been setup - _heading: ?number; - _updateMarkerRotationThrottled: Function; - - _numberOfWatches: number; - _noTimeout: boolean; - _supportsGeolocation: boolean; - - constructor(options: $Shape) { - super(); - const geolocation = window.navigator.geolocation; - this.options = extend({geolocation}, defaultOptions, options); - - bindAll([ - '_onSuccess', - '_onError', - '_onZoom', - '_finish', - '_setupUI', - '_updateCamera', - '_updateMarker', - '_updateMarkerRotation', - '_onDeviceOrientation' - ], this); - - this._updateMarkerRotationThrottled = throttle(this._updateMarkerRotation, 20); - this._numberOfWatches = 0; - } - - onAdd(map: Map): HTMLElement { - this._map = map; - this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); - this._checkGeolocationSupport(this._setupUI); - return this._container; - } - - onRemove() { - // clear the geolocation watch if exists - if (this._geolocationWatchID !== undefined) { - this.options.geolocation.clearWatch(this._geolocationWatchID); - this._geolocationWatchID = (undefined: any); - } - - // clear the markers from the map - if (this.options.showUserLocation && this._userLocationDotMarker) { - this._userLocationDotMarker.remove(); - } - if (this.options.showAccuracyCircle && this._accuracyCircleMarker) { - this._accuracyCircleMarker.remove(); - } - - this._container.remove(); - this._map.off('zoom', this._onZoom); - this._map = (undefined: any); - this._numberOfWatches = 0; - this._noTimeout = false; - } - - _checkGeolocationSupport(callback: boolean => void) { - const updateSupport = (supported = !!this.options.geolocation) => { - this._supportsGeolocation = supported; - callback(supported); - }; - - if (this._supportsGeolocation !== undefined) { - callback(this._supportsGeolocation); - - } else if (window.navigator.permissions !== undefined) { - // navigator.permissions has incomplete browser support http://caniuse.com/#feat=permissions-api - // Test for the case where a browser disables Geolocation because of an insecure origin; - // in some environments like iOS16 WebView, permissions reject queries but still support geolocation - window.navigator.permissions.query({name: 'geolocation'}) - .then(p => updateSupport(p.state !== 'denied')) - .catch(() => updateSupport()); - - } else { - updateSupport(); - } - } - - /** +class GeolocateControl + extends Evented { + _map: Map; + options: Options; + _container: HTMLElement; + _dotElement: HTMLElement; + _circleElement: HTMLElement; + _geolocateButton: HTMLButtonElement; + _geolocationWatchID: number; + _timeoutId: ?TimeoutID; + _watchState: | 'OFF' + | 'ACTIVE_LOCK' + | 'WAITING_ACTIVE' + | 'ACTIVE_ERROR' + | 'BACKGROUND' + | 'BACKGROUND_ERROR'; + _lastKnownPosition: any; + _userLocationDotMarker: Marker; + _accuracyCircleMarker: Marker; + _accuracy: number; + _setup: boolean; // set to true once the control has been setup + _heading: ?number; + _updateMarkerRotationThrottled: Function; + + _numberOfWatches: number; + _noTimeout: boolean; + _supportsGeolocation: boolean; + + constructor(options: $Shape) { + super(); + const geolocation = window.navigator.geolocation; + this.options = extend({geolocation}, defaultOptions, options); + + bindAll( + [ + '_onSuccess', + '_onError', + '_onZoom', + '_finish', + '_setupUI', + '_updateCamera', + '_updateMarker', + '_updateMarkerRotation', + '_onDeviceOrientation', + ], + this, + ); + + this._updateMarkerRotationThrottled = throttle( + this._updateMarkerRotation, + 20, + ); + this._numberOfWatches = 0; + } + + onAdd(map: Map): HTMLElement { + this._map = map; + this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); + this._checkGeolocationSupport(this._setupUI); + return this._container; + } + + onRemove() { + // clear the geolocation watch if exists + if (this._geolocationWatchID !== undefined) { + this.options.geolocation.clearWatch(this._geolocationWatchID); + this._geolocationWatchID = (undefined: any); + } + + // clear the markers from the map + if (this.options.showUserLocation && this._userLocationDotMarker) { + this._userLocationDotMarker.remove(); + } + if (this.options.showAccuracyCircle && this._accuracyCircleMarker) { + this._accuracyCircleMarker.remove(); + } + + this._container.remove(); + this._map.off('zoom', this._onZoom); + this._map = (undefined: any); + this._numberOfWatches = 0; + this._noTimeout = false; + } + + _checkGeolocationSupport(callback: (boolean) => void) { + const updateSupport = ((supported = !!this.options.geolocation) => { + this._supportsGeolocation = supported; + callback(supported); + }); + + if (this._supportsGeolocation !== undefined) { + callback(this._supportsGeolocation); + } else if (window.navigator.permissions !== undefined) { + // navigator.permissions has incomplete browser support http://caniuse.com/#feat=permissions-api + // Test for the case where a browser disables Geolocation because of an insecure origin; + // in some environments like iOS16 WebView, permissions reject queries but still support geolocation + window.navigator.permissions.query({name: 'geolocation'}).then( + p => updateSupport(p.state !== 'denied'), + ).catch(() => updateSupport()); + } else { + updateSupport(); + } + } + + /** * Check if the Geolocation API Position is outside the map's maxbounds. * * @param {Position} position the Geolocation API Position * @returns {boolean} Returns `true` if position is outside the map's maxbounds, otherwise returns `false`. * @private */ - _isOutOfMapMaxBounds(position: Position): boolean { - const bounds = this._map.getMaxBounds(); - const coordinates = position.coords; - - return !!bounds && ( - coordinates.longitude < bounds.getWest() || - coordinates.longitude > bounds.getEast() || - coordinates.latitude < bounds.getSouth() || - coordinates.latitude > bounds.getNorth() - ); - } - - _setErrorState() { - switch (this._watchState) { - case 'WAITING_ACTIVE': - this._watchState = 'ACTIVE_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); - break; - case 'ACTIVE_LOCK': - this._watchState = 'ACTIVE_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - // turn marker grey - break; - case 'BACKGROUND': - this._watchState = 'BACKGROUND_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background-error'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - // turn marker grey - break; - case 'ACTIVE_ERROR': - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - } - - /** + _isOutOfMapMaxBounds(position: Position): boolean { + const bounds = this._map.getMaxBounds(); + const coordinates = position.coords; + + return ( + !!bounds && + (coordinates.longitude < bounds.getWest() || + coordinates.longitude > bounds.getEast() || + coordinates.latitude < bounds.getSouth() || + coordinates.latitude > bounds.getNorth()) + ); + } + + _setErrorState() { + switch (this._watchState) { + case 'WAITING_ACTIVE': + this._watchState = 'ACTIVE_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-active-error', + ); + break; + case 'ACTIVE_LOCK': + this._watchState = 'ACTIVE_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-active-error', + ); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + // turn marker grey + break; + case 'BACKGROUND': + this._watchState = 'BACKGROUND_ERROR'; + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background', + ); + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-background-error', + ); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + // turn marker grey + break; + case 'ACTIVE_ERROR': + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + } + + /** * When the Geolocation API returns a new location, update the GeolocateControl. * * @param {Position} position the Geolocation API Position * @private */ - _onSuccess(position: Position) { - if (!this._map) { - // control has since been removed - return; - } - - if (this._isOutOfMapMaxBounds(position)) { - this._setErrorState(); - - this.fire(new Event('outofmaxbounds', position)); - this._updateMarker(); - this._finish(); - - return; - } - - if (this.options.trackUserLocation) { - // keep a record of the position so that if the state is BACKGROUND and the user - // clicks the button, we can move to ACTIVE_LOCK immediately without waiting for - // watchPosition to trigger _onSuccess - this._lastKnownPosition = position; - - switch (this._watchState) { - case 'WAITING_ACTIVE': - case 'ACTIVE_LOCK': - case 'ACTIVE_ERROR': - this._watchState = 'ACTIVE_LOCK'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); - break; - case 'BACKGROUND': - case 'BACKGROUND_ERROR': - this._watchState = 'BACKGROUND'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background'); - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - } - - // if showUserLocation and the watch state isn't off then update the marker location - if (this.options.showUserLocation && this._watchState !== 'OFF') { - this._updateMarker(position); - } - - // if in normal mode (not watch mode), or if in watch mode and the state is active watch - // then update the camera - if (!this.options.trackUserLocation || this._watchState === 'ACTIVE_LOCK') { - this._updateCamera(position); - } - - if (this.options.showUserLocation) { - this._dotElement.classList.remove('mapboxgl-user-location-dot-stale'); - } - - this.fire(new Event('geolocate', position)); - this._finish(); - } - - /** + _onSuccess = (position: Position) => { + if (!this._map) { + // control has since been removed + return; + } + + if (this._isOutOfMapMaxBounds(position)) { + this._setErrorState(); + + this.fire(new Event('outofmaxbounds', position)); + this._updateMarker(); + this._finish(); + + return; + } + + if (this.options.trackUserLocation) { + // keep a record of the position so that if the state is BACKGROUND and the user + // clicks the button, we can move to ACTIVE_LOCK immediately without waiting for + // watchPosition to trigger _onSuccess + this._lastKnownPosition = position; + + switch (this._watchState) { + case 'WAITING_ACTIVE': + case 'ACTIVE_LOCK': + case 'ACTIVE_ERROR': + this._watchState = 'ACTIVE_LOCK'; + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-waiting', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-active-error', + ); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); + break; + case 'BACKGROUND': + case 'BACKGROUND_ERROR': + this._watchState = 'BACKGROUND'; + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-waiting', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background-error', + ); + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-background', + ); + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + } + + // if showUserLocation and the watch state isn't off then update the marker location + if (this.options.showUserLocation && this._watchState !== 'OFF') { + this._updateMarker(position); + } + + // if in normal mode (not watch mode), or if in watch mode and the state is active watch + // then update the camera + if (!this.options.trackUserLocation || this._watchState === 'ACTIVE_LOCK') { + this._updateCamera(position); + } + + if (this.options.showUserLocation) { + this._dotElement.classList.remove('mapboxgl-user-location-dot-stale'); + } + + this.fire(new Event('geolocate', position)); + this._finish(); + }; + + /** * Update the camera location to center on the current position * * @param {Position} position the Geolocation API Position * @private */ - _updateCamera(position: Position) { - const center = new LngLat(position.coords.longitude, position.coords.latitude); - const radius = position.coords.accuracy; - const bearing = this._map.getBearing(); - const options = extend({bearing}, this.options.fitBoundsOptions); - - this._map.fitBounds(center.toBounds(radius), options, { - geolocateSource: true // tag this camera change so it won't cause the control to change to background state - }); - } - - /** + _updateCamera(position: Position) { + const center = new LngLat( + position.coords.longitude, + position.coords.latitude, + ); + const radius = position.coords.accuracy; + const bearing = this._map.getBearing(); + const options = extend({bearing}, this.options.fitBoundsOptions); + + this._map.fitBounds( + center.toBounds(radius), + options, + { + geolocateSource: true // tag this camera change so it won't cause the control to change to background state + , + }, + ); + } + + /** * Update the user location dot Marker to the current position * * @param {Position} [position] the Geolocation API Position * @private */ - _updateMarker(position: ?Position) { - if (position) { - const center = new LngLat(position.coords.longitude, position.coords.latitude); - this._accuracyCircleMarker.setLngLat(center).addTo(this._map); - this._userLocationDotMarker.setLngLat(center).addTo(this._map); - this._accuracy = position.coords.accuracy; - if (this.options.showUserLocation && this.options.showAccuracyCircle) { - this._updateCircleRadius(); - } - } else { - this._userLocationDotMarker.remove(); - this._accuracyCircleMarker.remove(); - } - } - - _updateCircleRadius() { - assert(this._circleElement); - const map = this._map; - const tr = map.transform; - - const pixelsPerMeter = mercatorZfromAltitude(1.0, tr._center.lat) * tr.worldSize; - assert(pixelsPerMeter !== 0.0); - const circleDiameter = Math.ceil(2.0 * this._accuracy * pixelsPerMeter); - - this._circleElement.style.width = `${circleDiameter}px`; - this._circleElement.style.height = `${circleDiameter}px`; - } - - _onZoom() { - if (this.options.showUserLocation && this.options.showAccuracyCircle) { - this._updateCircleRadius(); - } - } - - /** + _updateMarker(position: ?Position) { + if (position) { + const center = new LngLat( + position.coords.longitude, + position.coords.latitude, + ); + this._accuracyCircleMarker.setLngLat(center).addTo(this._map); + this._userLocationDotMarker.setLngLat(center).addTo(this._map); + this._accuracy = position.coords.accuracy; + if (this.options.showUserLocation && this.options.showAccuracyCircle) { + this._updateCircleRadius(); + } + } else { + this._userLocationDotMarker.remove(); + this._accuracyCircleMarker.remove(); + } + } + + _updateCircleRadius() { + assert(this._circleElement); + const map = this._map; + const tr = map.transform; + + const pixelsPerMeter = mercatorZfromAltitude(1.0, tr._center.lat) * tr.worldSize; + assert(pixelsPerMeter !== 0.0); + const circleDiameter = Math.ceil(2.0 * this._accuracy * pixelsPerMeter); + + this._circleElement.style.width = `${circleDiameter}px`; + this._circleElement.style.height = `${circleDiameter}px`; + } + + _onZoom = () => { + if (this.options.showUserLocation && this.options.showAccuracyCircle) { + this._updateCircleRadius(); + } + }; + + /** * Update the user location dot Marker rotation to the current heading * * @private */ - _updateMarkerRotation() { - if (this._userLocationDotMarker && typeof this._heading === 'number') { - this._userLocationDotMarker.setRotation(this._heading); - this._dotElement.classList.add('mapboxgl-user-location-show-heading'); - } else { - this._dotElement.classList.remove('mapboxgl-user-location-show-heading'); - this._userLocationDotMarker.setRotation(0); - } - } - - _onError(error: PositionError) { - if (!this._map) { - // control has since been removed - return; - } - - if (this.options.trackUserLocation) { - if (error.code === 1) { - // PERMISSION_DENIED - this._watchState = 'OFF'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); - this._geolocateButton.disabled = true; - const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); - this._geolocateButton.setAttribute('aria-label', title); - if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); - - if (this._geolocationWatchID !== undefined) { - this._clearWatch(); - } - } else if (error.code === 3 && this._noTimeout) { - // this represents a forced error state - // this was triggered to force immediate geolocation when a watch is already present - // see https://github.com/mapbox/mapbox-gl-js/issues/8214 - // and https://w3c.github.io/geolocation-api/#example-5-forcing-the-user-agent-to-return-a-fresh-cached-position - return; - } else { - this._setErrorState(); + _updateMarkerRotation = () => { + if (this._userLocationDotMarker && typeof this._heading === 'number') { + this._userLocationDotMarker.setRotation(this._heading); + this._dotElement.classList.add('mapboxgl-user-location-show-heading'); + } else { + this._dotElement.classList.remove('mapboxgl-user-location-show-heading'); + this._userLocationDotMarker.setRotation(0); + } + }; + + _onError = (error: PositionError) => { + if (!this._map) { + // control has since been removed + return; + } + + if (this.options.trackUserLocation) { + if (error.code === 1) { + // PERMISSION_DENIED + this._watchState = 'OFF'; + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-waiting', + ); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-active-error', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background-error', + ); + this._geolocateButton.disabled = true; + const title = this._map._getUIString( + 'GeolocateControl.LocationNotAvailable', + ); + this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) + this._geolocateButton.firstElementChild.setAttribute('title', title); + + if (this._geolocationWatchID !== undefined) { + this._clearWatch(); + } + } else if (error.code === 3 && this._noTimeout) { + // this represents a forced error state + // this was triggered to force immediate geolocation when a watch is already present + // see https://github.com/mapbox/mapbox-gl-js/issues/8214 + // and https://w3c.github.io/geolocation-api/#example-5-forcing-the-user-agent-to-return-a-fresh-cached-position + return; + } else { + this._setErrorState(); + } + } + + if (this._watchState !== 'OFF' && this.options.showUserLocation) { + this._dotElement.classList.add('mapboxgl-user-location-dot-stale'); + } + + this.fire(new Event('error', error)); + + this._finish(); + }; + + _finish = () => { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } + this._timeoutId = undefined; + }; + + _setupUI = (supported: boolean) => { + if (this._map === undefined) { + // This control was removed from the map before geolocation + // support was determined. + return; + } + this._container.addEventListener( + 'contextmenu', + (e: MouseEvent) => e.preventDefault(), + ); + this._geolocateButton = DOM.create( + 'button', + `mapboxgl-ctrl-geolocate`, + this._container, + ); + DOM.create('span', `mapboxgl-ctrl-icon`, this._geolocateButton).setAttribute( + 'aria-hidden', + 'true', + ); + + this._geolocateButton.type = 'button'; + + if (supported === false) { + warnOnce( + 'Geolocation support is not available so the GeolocateControl will be disabled.', + ); + const title = this._map._getUIString( + 'GeolocateControl.LocationNotAvailable', + ); + this._geolocateButton.disabled = true; + this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) + this._geolocateButton.firstElementChild.setAttribute('title', title); + } else { + const title = this._map._getUIString('GeolocateControl.FindMyLocation'); + this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) + this._geolocateButton.firstElementChild.setAttribute('title', title); + } + + if (this.options.trackUserLocation) { + this._geolocateButton.setAttribute('aria-pressed', 'false'); + this._watchState = 'OFF'; + } + + // when showUserLocation is enabled, keep the Geolocate button disabled until the device location marker is setup on the map + if (this.options.showUserLocation) { + this._dotElement = DOM.create('div', 'mapboxgl-user-location'); + this._dotElement.appendChild( + DOM.create('div', 'mapboxgl-user-location-dot'), + ); + this._dotElement.appendChild( + DOM.create('div', 'mapboxgl-user-location-heading'), + ); + + this._userLocationDotMarker = new Marker( + { + element: this._dotElement, + rotationAlignment: 'map', + pitchAlignment: 'map', + }, + ); + + this._circleElement = DOM.create( + 'div', + 'mapboxgl-user-location-accuracy-circle', + ); + this._accuracyCircleMarker = new Marker( + {element: this._circleElement, pitchAlignment: 'map'}, + ); + + if (this.options.trackUserLocation) this._watchState = 'OFF'; + + this._map.on('zoom', this._onZoom); + } + + this._geolocateButton.addEventListener('click', this.trigger.bind(this)); + + this._setup = true; + + // when the camera is changed (and it's not as a result of the Geolocation Control) change + // the watch mode to background watch, so that the marker is updated but not the camera. + if (this.options.trackUserLocation) { + this._map.on( + 'movestart', + event => { + const fromResize = event.originalEvent && + event.originalEvent.type === 'resize'; + if ( + !event.geolocateSource && this._watchState === 'ACTIVE_LOCK' && + !fromResize + ) { + this._watchState = 'BACKGROUND'; + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-background', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-active', + ); + + this.fire(new Event('trackuserlocationend')); } - } - - if (this._watchState !== 'OFF' && this.options.showUserLocation) { - this._dotElement.classList.add('mapboxgl-user-location-dot-stale'); - } - - this.fire(new Event('error', error)); - - this._finish(); - } - - _finish() { - if (this._timeoutId) { clearTimeout(this._timeoutId); } - this._timeoutId = undefined; - } - - _setupUI(supported: boolean) { - if (this._map === undefined) { - // This control was removed from the map before geolocation - // support was determined. - return; - } - this._container.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault()); - this._geolocateButton = DOM.create('button', `mapboxgl-ctrl-geolocate`, this._container); - DOM.create('span', `mapboxgl-ctrl-icon`, this._geolocateButton).setAttribute('aria-hidden', 'true'); - - this._geolocateButton.type = 'button'; - - if (supported === false) { - warnOnce('Geolocation support is not available so the GeolocateControl will be disabled.'); - const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); - this._geolocateButton.disabled = true; - this._geolocateButton.setAttribute('aria-label', title); - if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); - } else { - const title = this._map._getUIString('GeolocateControl.FindMyLocation'); - this._geolocateButton.setAttribute('aria-label', title); - if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); - } - - if (this.options.trackUserLocation) { - this._geolocateButton.setAttribute('aria-pressed', 'false'); - this._watchState = 'OFF'; - } - - // when showUserLocation is enabled, keep the Geolocate button disabled until the device location marker is setup on the map - if (this.options.showUserLocation) { - this._dotElement = DOM.create('div', 'mapboxgl-user-location'); - this._dotElement.appendChild(DOM.create('div', 'mapboxgl-user-location-dot')); - this._dotElement.appendChild(DOM.create('div', 'mapboxgl-user-location-heading')); - - this._userLocationDotMarker = new Marker({ - element: this._dotElement, - rotationAlignment: 'map', - pitchAlignment: 'map' - }); - - this._circleElement = DOM.create('div', 'mapboxgl-user-location-accuracy-circle'); - this._accuracyCircleMarker = new Marker({element: this._circleElement, pitchAlignment: 'map'}); - - if (this.options.trackUserLocation) this._watchState = 'OFF'; - - this._map.on('zoom', this._onZoom); - } - - this._geolocateButton.addEventListener('click', - this.trigger.bind(this)); - - this._setup = true; - - // when the camera is changed (and it's not as a result of the Geolocation Control) change - // the watch mode to background watch, so that the marker is updated but not the camera. - if (this.options.trackUserLocation) { - this._map.on('movestart', (event) => { - const fromResize = event.originalEvent && event.originalEvent.type === 'resize'; - if (!event.geolocateSource && this._watchState === 'ACTIVE_LOCK' && !fromResize) { - this._watchState = 'BACKGROUND'; - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - - this.fire(new Event('trackuserlocationend')); - } - }); - } - } - - /** + }, + ); + } + }; + + /** * Programmatically request and move the map to the user's location. * * @returns {boolean} Returns `false` if called before control was added to a map, otherwise returns `true`. @@ -508,21 +598,21 @@ class GeolocateControl extends Evented { * geolocate.trigger(); * }); */ - _onDeviceOrientation(deviceOrientationEvent: DeviceOrientationEvent) { - // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. - if (this._userLocationDotMarker) { - if (deviceOrientationEvent.webkitCompassHeading) { - // Safari - this._heading = deviceOrientationEvent.webkitCompassHeading; - } else if (deviceOrientationEvent.absolute === true) { - // non-Safari alpha increases counter clockwise around the z axis - this._heading = deviceOrientationEvent.alpha * -1; - } - this._updateMarkerRotationThrottled(); - } - } - - /** + _onDeviceOrientation = (deviceOrientationEvent: DeviceOrientationEvent) => { + // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. + if (this._userLocationDotMarker) { + if (deviceOrientationEvent.webkitCompassHeading) { + // Safari + this._heading = deviceOrientationEvent.webkitCompassHeading; + } else if (deviceOrientationEvent.absolute === true) { + // non-Safari alpha increases counter clockwise around the z axis + this._heading = deviceOrientationEvent.alpha * -1; + } + this._updateMarkerRotationThrottled(); + } + }; + + /** * Trigger a geolocation event. * * @example @@ -540,151 +630,184 @@ class GeolocateControl extends Evented { * }); * @returns {boolean} Returns `false` if called before control was added to a map, otherwise returns `true`. */ - trigger(): boolean { - if (!this._setup) { - warnOnce('Geolocate control triggered before added to a map'); - return false; - } - if (this.options.trackUserLocation) { - // update watchState and do any outgoing state cleanup - switch (this._watchState) { - case 'OFF': - // turn on the GeolocateControl - this._watchState = 'WAITING_ACTIVE'; - - this.fire(new Event('trackuserlocationstart')); - break; - case 'WAITING_ACTIVE': - case 'ACTIVE_LOCK': - case 'ACTIVE_ERROR': - case 'BACKGROUND_ERROR': - // turn off the Geolocate Control - this._numberOfWatches--; - this._noTimeout = false; - this._watchState = 'OFF'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); - - this.fire(new Event('trackuserlocationend')); - break; - case 'BACKGROUND': - this._watchState = 'ACTIVE_LOCK'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); - // set camera to last known location - if (this._lastKnownPosition) this._updateCamera(this._lastKnownPosition); - - this.fire(new Event('trackuserlocationstart')); - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - - // incoming state setup - switch (this._watchState) { - case 'WAITING_ACTIVE': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); - break; - case 'ACTIVE_LOCK': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); - break; - case 'ACTIVE_ERROR': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); - break; - case 'BACKGROUND': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background'); - break; - case 'BACKGROUND_ERROR': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background-error'); - break; - case 'OFF': - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - - // manage geolocation.watchPosition / geolocation.clearWatch - if (this._watchState === 'OFF' && this._geolocationWatchID !== undefined) { - // clear watchPosition as we've changed to an OFF state - this._clearWatch(); - } else if (this._geolocationWatchID === undefined) { - // enable watchPosition since watchState is not OFF and there is no watchPosition already running - - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.setAttribute('aria-pressed', 'true'); - - this._numberOfWatches++; - let positionOptions; - if (this._numberOfWatches > 1) { - positionOptions = {maximumAge:600000, timeout:0}; - this._noTimeout = true; - } else { - positionOptions = this.options.positionOptions; - this._noTimeout = false; - } - - this._geolocationWatchID = this.options.geolocation.watchPosition( - this._onSuccess, this._onError, positionOptions); - - if (this.options.showUserHeading) { - this._addDeviceOrientationListener(); - } - } - } else { - this.options.geolocation.getCurrentPosition( - this._onSuccess, this._onError, this.options.positionOptions); - - // This timeout ensures that we still call finish() even if - // the user declines to share their location in Firefox - this._timeoutId = setTimeout(this._finish, 10000 /* 10sec */); - } - - return true; - } - - _addDeviceOrientationListener() { - const addListener = () => { - if ('ondeviceorientationabsolute' in window) { - window.addEventListener('deviceorientationabsolute', this._onDeviceOrientation); - } else { - window.addEventListener('deviceorientation', this._onDeviceOrientation); + trigger = (): boolean => { + if (!this._setup) { + warnOnce('Geolocate control triggered before added to a map'); + return false; + } + if (this.options.trackUserLocation) { + // update watchState and do any outgoing state cleanup + switch (this._watchState) { + case 'OFF': + // turn on the GeolocateControl + this._watchState = 'WAITING_ACTIVE'; + + this.fire(new Event('trackuserlocationstart')); + break; + case 'WAITING_ACTIVE': + case 'ACTIVE_LOCK': + case 'ACTIVE_ERROR': + case 'BACKGROUND_ERROR': + // turn off the Geolocate Control + this._numberOfWatches--; + this._noTimeout = false; + this._watchState = 'OFF'; + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-waiting', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-active', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-active-error', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background', + ); + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background-error', + ); + + this.fire(new Event('trackuserlocationend')); + break; + case 'BACKGROUND': + this._watchState = 'ACTIVE_LOCK'; + this._geolocateButton.classList.remove( + 'mapboxgl-ctrl-geolocate-background', + ); + // set camera to last known location + if (this._lastKnownPosition) + this._updateCamera(this._lastKnownPosition); + + this.fire(new Event('trackuserlocationstart')); + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + + // incoming state setup + switch (this._watchState) { + case 'WAITING_ACTIVE': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); + break; + case 'ACTIVE_LOCK': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); + break; + case 'ACTIVE_ERROR': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-active-error', + ); + break; + case 'BACKGROUND': + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-background', + ); + break; + case 'BACKGROUND_ERROR': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.add( + 'mapboxgl-ctrl-geolocate-background-error', + ); + break; + case 'OFF': + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + + // manage geolocation.watchPosition / geolocation.clearWatch + if (this._watchState === 'OFF' && this._geolocationWatchID !== undefined) { + // clear watchPosition as we've changed to an OFF state + this._clearWatch(); + } else if (this._geolocationWatchID === undefined) { + // enable watchPosition since watchState is not OFF and there is no watchPosition already running + + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.setAttribute('aria-pressed', 'true'); + + this._numberOfWatches++; + let positionOptions; + if (this._numberOfWatches > 1) { + positionOptions = {maximumAge: 600000, timeout: 0}; + this._noTimeout = true; + } else { + positionOptions = this.options.positionOptions; + this._noTimeout = false; + } + + this._geolocationWatchID = this.options.geolocation.watchPosition( + this._onSuccess, + this._onError, + positionOptions, + ); + + if (this.options.showUserHeading) { + this._addDeviceOrientationListener(); + } + } + } else { + this.options.geolocation.getCurrentPosition( + this._onSuccess, + this._onError, + this.options.positionOptions, + ); + + // This timeout ensures that we still call finish() even if + // the user declines to share their location in Firefox + this._timeoutId = setTimeout(this._finish, 10000 /* 10sec */); + } + + return true; + }; + + _addDeviceOrientationListener() { + const addListener = (() => { + if ('ondeviceorientationabsolute' in window) { + window.addEventListener( + 'deviceorientationabsolute', + this._onDeviceOrientation, + ); + } else { + window.addEventListener('deviceorientation', this._onDeviceOrientation); + } + }); + + if ( + typeof window.DeviceMotionEvent !== "undefined" && + typeof window.DeviceMotionEvent.requestPermission === 'function' + ) { + // $FlowFixMe + DeviceOrientationEvent.requestPermission().then( + response => { + if (response === 'granted') { + addListener(); } - }; - - if (typeof window.DeviceMotionEvent !== "undefined" && - typeof window.DeviceMotionEvent.requestPermission === 'function') { - // $FlowFixMe - DeviceOrientationEvent.requestPermission() - .then(response => { - if (response === 'granted') { - addListener(); - } - }) - .catch(console.error); - } else { - addListener(); - } - } - - _clearWatch() { - this.options.geolocation.clearWatch(this._geolocationWatchID); - - window.removeEventListener('deviceorientation', this._onDeviceOrientation); - window.removeEventListener('deviceorientationabsolute', this._onDeviceOrientation); - - this._geolocationWatchID = (undefined: any); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.setAttribute('aria-pressed', 'false'); - - if (this.options.showUserLocation) { - this._updateMarker(null); - } - } + }, + ).catch(console.error); + } else { + addListener(); + } + } + + _clearWatch() { + this.options.geolocation.clearWatch(this._geolocationWatchID); + + window.removeEventListener('deviceorientation', this._onDeviceOrientation); + window.removeEventListener( + 'deviceorientationabsolute', + this._onDeviceOrientation, + ); + + this._geolocationWatchID = (undefined: any); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.setAttribute('aria-pressed', 'false'); + + if (this.options.showUserLocation) { + this._updateMarker(null); + } + } } export default GeolocateControl; diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index 8488cc0e6a2..d5641f1e82f 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -16,76 +16,78 @@ import type Map, {ControlPosition} from '../map.js'; **/ class LogoControl { - _map: Map; - _container: HTMLElement; - - constructor() { - bindAll(['_updateLogo', '_updateCompact'], this); - } - - onAdd(map: Map): HTMLElement { - this._map = map; - this._container = DOM.create('div', 'mapboxgl-ctrl'); - const anchor = DOM.create('a', 'mapboxgl-ctrl-logo'); - anchor.target = "_blank"; - anchor.rel = "noopener nofollow"; - anchor.href = "https://www.mapbox.com/"; - anchor.setAttribute("aria-label", this._map._getUIString('LogoControl.Title')); - anchor.setAttribute("rel", "noopener nofollow"); - this._container.appendChild(anchor); - this._container.style.display = 'none'; - - this._map.on('sourcedata', this._updateLogo); - this._updateLogo(); - - this._map.on('resize', this._updateCompact); - this._updateCompact(); - - return this._container; - } - - onRemove() { - this._container.remove(); - this._map.off('sourcedata', this._updateLogo); - this._map.off('resize', this._updateCompact); - } - - getDefaultPosition(): ControlPosition { - return 'bottom-left'; - } - - _updateLogo(e: any) { - if (!e || e.sourceDataType === 'metadata') { - this._container.style.display = this._logoRequired() ? 'block' : 'none'; - } - } - - _logoRequired(): boolean { - if (!this._map.style) return true; - const sourceCaches = this._map.style._sourceCaches; - if (Object.entries(sourceCaches).length === 0) return true; - for (const id in sourceCaches) { - const source = sourceCaches[id].getSource(); - if (source.hasOwnProperty('mapbox_logo') && !source.mapbox_logo) { - return false; - } - } - - return true; - } - - _updateCompact() { - const containerChildren = this._container.children; - if (containerChildren.length) { - const anchor = containerChildren[0]; - if (this._map.getCanvasContainer().offsetWidth < 250) { - anchor.classList.add('mapboxgl-compact'); - } else { - anchor.classList.remove('mapboxgl-compact'); - } - } - } - + _map: Map; + _container: HTMLElement; + + constructor() { + bindAll(['_updateLogo', '_updateCompact'], this); + } + + onAdd(map: Map): HTMLElement { + this._map = map; + this._container = DOM.create('div', 'mapboxgl-ctrl'); + const anchor = DOM.create('a', 'mapboxgl-ctrl-logo'); + anchor.target = "_blank"; + anchor.rel = "noopener nofollow"; + anchor.href = "https://www.mapbox.com/"; + anchor.setAttribute( + "aria-label", + this._map._getUIString('LogoControl.Title'), + ); + anchor.setAttribute("rel", "noopener nofollow"); + this._container.appendChild(anchor); + this._container.style.display = 'none'; + + this._map.on('sourcedata', this._updateLogo); + this._updateLogo(); + + this._map.on('resize', this._updateCompact); + this._updateCompact(); + + return this._container; + } + + onRemove() { + this._container.remove(); + this._map.off('sourcedata', this._updateLogo); + this._map.off('resize', this._updateCompact); + } + + getDefaultPosition(): ControlPosition { + return 'bottom-left'; + } + + _updateLogo = (e: any) => { + if (!e || e.sourceDataType === 'metadata') { + this._container.style.display = this._logoRequired() ? 'block' : 'none'; + } + }; + + _logoRequired(): boolean { + if (!this._map.style) return true; + const sourceCaches = this._map.style._sourceCaches; + if (Object.entries(sourceCaches).length === 0) return true; + for (const id in sourceCaches) { + const source = sourceCaches[id].getSource(); + if (source.hasOwnProperty('mapbox_logo') && !source.mapbox_logo) { + return false; + } + } + + return true; + } + + _updateCompact = () => { + const containerChildren = this._container.children; + if (containerChildren.length) { + const anchor = containerChildren[0]; + if (this._map.getCanvasContainer().offsetWidth < 250) { + anchor.classList.add('mapboxgl-compact'); + } else { + anchor.classList.remove('mapboxgl-compact'); + } + } + }; } export default LogoControl; diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index de8ec58be9f..d92be7a61c6 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -41,240 +41,307 @@ const defaultOptions: Options = { * @see [Example: Add a third party vector tile source](https://www.mapbox.com/mapbox-gl-js/example/third-party/) */ class NavigationControl { - _map: ?Map; - options: Options; - _container: HTMLElement; - _zoomInButton: HTMLButtonElement; - _zoomOutButton: HTMLButtonElement; - _compass: HTMLButtonElement; - _compassIcon: HTMLElement; - _handler: ?MouseRotateWrapper; - - constructor(options: Options) { - this.options = extend({}, defaultOptions, options); - - this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-group'); - this._container.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault()); - - if (this.options.showZoom) { - bindAll([ - '_setButtonTitle', - '_updateZoomButtons' - ], this); - this._zoomInButton = this._createButton('mapboxgl-ctrl-zoom-in', (e) => { if (this._map) this._map.zoomIn({}, {originalEvent: e}); }); - DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomInButton).setAttribute('aria-hidden', 'true'); - this._zoomOutButton = this._createButton('mapboxgl-ctrl-zoom-out', (e) => { if (this._map) this._map.zoomOut({}, {originalEvent: e}); }); - DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomOutButton).setAttribute('aria-hidden', 'true'); - } - if (this.options.showCompass) { - bindAll([ - '_rotateCompassArrow' - ], this); - this._compass = this._createButton('mapboxgl-ctrl-compass', (e) => { - const map = this._map; - if (!map) return; - if (this.options.visualizePitch) { - map.resetNorthPitch({}, {originalEvent: e}); - } else { - map.resetNorth({}, {originalEvent: e}); - } - }); - this._compassIcon = DOM.create('span', 'mapboxgl-ctrl-icon', this._compass); - this._compassIcon.setAttribute('aria-hidden', 'true'); - } - } - - _updateZoomButtons() { - const map = this._map; - if (!map) return; - - const zoom = map.getZoom(); - const isMax = zoom === map.getMaxZoom(); - const isMin = zoom === map.getMinZoom(); - this._zoomInButton.disabled = isMax; - this._zoomOutButton.disabled = isMin; - this._zoomInButton.setAttribute('aria-disabled', isMax.toString()); - this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); - } - - _rotateCompassArrow() { - const map = this._map; - if (!map) return; - - const rotate = this.options.visualizePitch ? - `scale(${1 / Math.pow(Math.cos(map.transform.pitch * (Math.PI / 180)), 0.5)}) rotateX(${map.transform.pitch}deg) rotateZ(${map.transform.angle * (180 / Math.PI)}deg)` : - `rotate(${map.transform.angle * (180 / Math.PI)}deg)`; - - map._requestDomTask(() => { - if (this._compassIcon) { - this._compassIcon.style.transform = rotate; - } - }); - } - - onAdd(map: Map): HTMLElement { - this._map = map; - if (this.options.showZoom) { - this._setButtonTitle(this._zoomInButton, 'ZoomIn'); - this._setButtonTitle(this._zoomOutButton, 'ZoomOut'); - map.on('zoom', this._updateZoomButtons); - this._updateZoomButtons(); - } - if (this.options.showCompass) { - this._setButtonTitle(this._compass, 'ResetBearing'); - if (this.options.visualizePitch) { - map.on('pitch', this._rotateCompassArrow); - } - map.on('rotate', this._rotateCompassArrow); - this._rotateCompassArrow(); - this._handler = new MouseRotateWrapper(map, this._compass, this.options.visualizePitch); - } - return this._container; - } - - onRemove() { - const map = this._map; - if (!map) return; - this._container.remove(); - if (this.options.showZoom) { - map.off('zoom', this._updateZoomButtons); - } - if (this.options.showCompass) { + _map: ?Map; + options: Options; + _container: HTMLElement; + _zoomInButton: HTMLButtonElement; + _zoomOutButton: HTMLButtonElement; + _compass: HTMLButtonElement; + _compassIcon: HTMLElement; + _handler: ?MouseRotateWrapper; + + constructor(options: Options) { + this.options = extend({}, defaultOptions, options); + + this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-group'); + this._container.addEventListener( + 'contextmenu', + (e: MouseEvent) => e.preventDefault(), + ); + + if (this.options.showZoom) { + bindAll(['_setButtonTitle', '_updateZoomButtons'], this); + this._zoomInButton = this._createButton( + 'mapboxgl-ctrl-zoom-in', + e => { + if (this._map) this._map.zoomIn({}, {originalEvent: e}); + }, + ); + DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomInButton).setAttribute( + 'aria-hidden', + 'true', + ); + this._zoomOutButton = this._createButton( + 'mapboxgl-ctrl-zoom-out', + e => { + if (this._map) this._map.zoomOut({}, {originalEvent: e}); + }, + ); + DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomOutButton).setAttribute( + 'aria-hidden', + 'true', + ); + } + if (this.options.showCompass) { + bindAll(['_rotateCompassArrow'], this); + this._compass = this._createButton( + 'mapboxgl-ctrl-compass', + e => { + const map = this._map; + if (!map) return; if (this.options.visualizePitch) { - map.off('pitch', this._rotateCompassArrow); + map.resetNorthPitch({}, {originalEvent: e}); + } else { + map.resetNorth({}, {originalEvent: e}); } - map.off('rotate', this._rotateCompassArrow); - if (this._handler) this._handler.off(); - this._handler = undefined; - } - this._map = undefined; - } - - _createButton(className: string, fn: () => mixed): HTMLButtonElement { - const a = DOM.create('button', className, this._container); - a.type = 'button'; - a.addEventListener('click', fn); - return a; - } - - _setButtonTitle(button: HTMLButtonElement, title: string) { - if (!this._map) return; - const str = this._map._getUIString(`NavigationControl.${title}`); - button.setAttribute('aria-label', str); - if (button.firstElementChild) button.firstElementChild.setAttribute('title', str); - } + }, + ); + this._compassIcon = DOM.create( + 'span', + 'mapboxgl-ctrl-icon', + this._compass, + ); + this._compassIcon.setAttribute('aria-hidden', 'true'); + } + } + + _updateZoomButtons = () => { + const map = this._map; + if (!map) return; + + const zoom = map.getZoom(); + const isMax = zoom === map.getMaxZoom(); + const isMin = zoom === map.getMinZoom(); + this._zoomInButton.disabled = isMax; + this._zoomOutButton.disabled = isMin; + this._zoomInButton.setAttribute('aria-disabled', isMax.toString()); + this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); + }; + + _rotateCompassArrow = () => { + const map = this._map; + if (!map) return; + + const rotate = this.options.visualizePitch ? + `scale(${1 / Math.pow( + Math.cos(map.transform.pitch * (Math.PI / 180)), + 0.5, + )}) rotateX(${map.transform.pitch}deg) rotateZ(${map.transform.angle * (180 / Math.PI)}deg)` : + `rotate(${map.transform.angle * (180 / Math.PI)}deg)`; + + map._requestDomTask( + () => { + if (this._compassIcon) { + this._compassIcon.style.transform = rotate; + } + }, + ); + }; + + onAdd(map: Map): HTMLElement { + this._map = map; + if (this.options.showZoom) { + this._setButtonTitle(this._zoomInButton, 'ZoomIn'); + this._setButtonTitle(this._zoomOutButton, 'ZoomOut'); + map.on('zoom', this._updateZoomButtons); + this._updateZoomButtons(); + } + if (this.options.showCompass) { + this._setButtonTitle(this._compass, 'ResetBearing'); + if (this.options.visualizePitch) { + map.on('pitch', this._rotateCompassArrow); + } + map.on('rotate', this._rotateCompassArrow); + this._rotateCompassArrow(); + this._handler = new MouseRotateWrapper( + map, + this._compass, + this.options.visualizePitch, + ); + } + return this._container; + } + + onRemove() { + const map = this._map; + if (!map) return; + this._container.remove(); + if (this.options.showZoom) { + map.off('zoom', this._updateZoomButtons); + } + if (this.options.showCompass) { + if (this.options.visualizePitch) { + map.off('pitch', this._rotateCompassArrow); + } + map.off('rotate', this._rotateCompassArrow); + if (this._handler) this._handler.off(); + this._handler = undefined; + } + this._map = undefined; + } + + _createButton(className: string, fn: () => mixed): HTMLButtonElement { + const a = DOM.create('button', className, this._container); + a.type = 'button'; + a.addEventListener('click', fn); + return a; + } + + _setButtonTitle(button: HTMLButtonElement, title: string) { + if (!this._map) return; + const str = this._map._getUIString(`NavigationControl.${title}`); + button.setAttribute('aria-label', str); + if (button.firstElementChild) + button.firstElementChild.setAttribute('title', str); + } } class MouseRotateWrapper { - - map: Map; - _clickTolerance: number; - element: HTMLElement; - mouseRotate: MouseRotateHandler; - mousePitch: MousePitchHandler; - _startPos: ?Point; - _lastPos: ?Point; - - constructor(map: Map, element: HTMLElement, pitch?: boolean = false) { - this._clickTolerance = 10; - this.element = element; - this.mouseRotate = new MouseRotateHandler({clickTolerance: map.dragRotate._mouseRotate._clickTolerance}); - this.map = map; - if (pitch) this.mousePitch = new MousePitchHandler({clickTolerance: map.dragRotate._mousePitch._clickTolerance}); - - bindAll(['mousedown', 'mousemove', 'mouseup', 'touchstart', 'touchmove', 'touchend', 'reset'], this); - element.addEventListener('mousedown', this.mousedown); - element.addEventListener('touchstart', this.touchstart, {passive: false}); - element.addEventListener('touchmove', this.touchmove); - element.addEventListener('touchend', this.touchend); - element.addEventListener('touchcancel', this.reset); - } - - down(e: MouseEvent, point: Point) { - this.mouseRotate.mousedown(e, point); - if (this.mousePitch) this.mousePitch.mousedown(e, point); - DOM.disableDrag(); - } - - move(e: MouseEvent, point: Point) { - const map = this.map; - const r = this.mouseRotate.mousemoveWindow(e, point); - const delta = r && r.bearingDelta; - if (delta) map.setBearing(map.getBearing() + delta); - if (this.mousePitch) { - const p = this.mousePitch.mousemoveWindow(e, point); - const delta = p && p.pitchDelta; - if (delta) map.setPitch(map.getPitch() + delta); - } - } - - off() { - const element = this.element; - element.removeEventListener('mousedown', this.mousedown); - element.removeEventListener('touchstart', this.touchstart, {passive: false}); - element.removeEventListener('touchmove', this.touchmove); - element.removeEventListener('touchend', this.touchend); - element.removeEventListener('touchcancel', this.reset); - this.offTemp(); - } - - offTemp() { - DOM.enableDrag(); - window.removeEventListener('mousemove', this.mousemove); - window.removeEventListener('mouseup', this.mouseup); - } - - mousedown(e: MouseEvent) { - this.down(extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), DOM.mousePos(this.element, e)); - window.addEventListener('mousemove', this.mousemove); - window.addEventListener('mouseup', this.mouseup); - } - - mousemove(e: MouseEvent) { - this.move(e, DOM.mousePos(this.element, e)); - } - - mouseup(e: MouseEvent) { - this.mouseRotate.mouseupWindow(e); - if (this.mousePitch) this.mousePitch.mouseupWindow(e); - this.offTemp(); - } - - touchstart(e: TouchEvent) { - if (e.targetTouches.length !== 1) { - this.reset(); - } else { - this._startPos = this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; - this.down((({type: 'mousedown', button: 0, ctrlKey: true, preventDefault: () => e.preventDefault()}: any): MouseEvent), this._startPos); - } - } - - touchmove(e: TouchEvent) { - if (e.targetTouches.length !== 1) { - this.reset(); - } else { - this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; - this.move((({preventDefault: () => e.preventDefault()}: any): MouseEvent), this._lastPos); - } - } - - touchend(e: TouchEvent) { - if (e.targetTouches.length === 0 && - this._startPos && - this._lastPos && - this._startPos.dist(this._lastPos) < this._clickTolerance) { - this.element.click(); - } - this.reset(); - } - - reset() { - this.mouseRotate.reset(); - if (this.mousePitch) this.mousePitch.reset(); - delete this._startPos; - delete this._lastPos; - this.offTemp(); - } + map: Map; + _clickTolerance: number; + element: HTMLElement; + mouseRotate: MouseRotateHandler; + mousePitch: MousePitchHandler; + _startPos: ?Point; + _lastPos: ?Point; + + constructor(map: Map, element: HTMLElement, pitch?: boolean = false) { + this._clickTolerance = 10; + this.element = element; + this.mouseRotate = new MouseRotateHandler( + {clickTolerance: map.dragRotate._mouseRotate._clickTolerance}, + ); + this.map = map; + if (pitch) + this.mousePitch = new MousePitchHandler( + {clickTolerance: map.dragRotate._mousePitch._clickTolerance}, + ); + + bindAll( + [ + 'mousedown', + 'mousemove', + 'mouseup', + 'touchstart', + 'touchmove', + 'touchend', + 'reset', + ], + this, + ); + element.addEventListener('mousedown', this.mousedown); + element.addEventListener('touchstart', this.touchstart, {passive: false}); + element.addEventListener('touchmove', this.touchmove); + element.addEventListener('touchend', this.touchend); + element.addEventListener('touchcancel', this.reset); + } + + down(e: MouseEvent, point: Point) { + this.mouseRotate.mousedown(e, point); + if (this.mousePitch) this.mousePitch.mousedown(e, point); + DOM.disableDrag(); + } + + move(e: MouseEvent, point: Point) { + const map = this.map; + const r = this.mouseRotate.mousemoveWindow(e, point); + const delta = r && r.bearingDelta; + if (delta) map.setBearing(map.getBearing() + delta); + if (this.mousePitch) { + const p = this.mousePitch.mousemoveWindow(e, point); + const delta = p && p.pitchDelta; + if (delta) map.setPitch(map.getPitch() + delta); + } + } + + off() { + const element = this.element; + element.removeEventListener('mousedown', this.mousedown); + element.removeEventListener( + 'touchstart', + this.touchstart, + {passive: false}, + ); + element.removeEventListener('touchmove', this.touchmove); + element.removeEventListener('touchend', this.touchend); + element.removeEventListener('touchcancel', this.reset); + this.offTemp(); + } + + offTemp() { + DOM.enableDrag(); + window.removeEventListener('mousemove', this.mousemove); + window.removeEventListener('mouseup', this.mouseup); + } + + mousedown = (e: MouseEvent) => { + this.down( + extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), + DOM.mousePos(this.element, e), + ); + window.addEventListener('mousemove', this.mousemove); + window.addEventListener('mouseup', this.mouseup); + }; + + mousemove = (e: MouseEvent) => { + this.move(e, DOM.mousePos(this.element, e)); + }; + + mouseup = (e: MouseEvent) => { + this.mouseRotate.mouseupWindow(e); + if (this.mousePitch) this.mousePitch.mouseupWindow(e); + this.offTemp(); + }; + + touchstart = (e: TouchEvent) => { + if (e.targetTouches.length !== 1) { + this.reset(); + } else { + this._startPos = this._lastPos = DOM.touchPos( + this.element, + e.targetTouches, + )[0]; + this.down( + (({ + type: 'mousedown', + button: 0, + ctrlKey: true, + preventDefault: () => e.preventDefault(), + }: any): MouseEvent), + this._startPos, + ); + } + }; + + touchmove = (e: TouchEvent) => { + if (e.targetTouches.length !== 1) { + this.reset(); + } else { + this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; + this.move( + (({preventDefault: () => e.preventDefault()}: any): MouseEvent), + this._lastPos, + ); + } + }; + + touchend = (e: TouchEvent) => { + if ( + e.targetTouches.length === 0 && this._startPos && this._lastPos && + this._startPos.dist(this._lastPos) < this._clickTolerance + ) { + this.element.click(); + } + this.reset(); + }; + + reset = () => { + this.mouseRotate.reset(); + if (this.mousePitch) this.mousePitch.reset(); + delete this._startPos; + delete this._lastPos; + this.offTemp(); + }; } export default NavigationControl; diff --git a/src/ui/control/scale_control.js b/src/ui/control/scale_control.js index d42a679546c..590db056791 100644 --- a/src/ui/control/scale_control.js +++ b/src/ui/control/scale_control.js @@ -35,117 +35,122 @@ const defaultOptions: Options = { * scale.setUnit('metric'); */ class ScaleControl { - _map: Map; - _container: HTMLElement; - _language: ?string | ?string[]; - options: Options; - - constructor(options: Options) { - this.options = extend({}, defaultOptions, options); - - // Some old browsers (e.g., Safari < 14.1) don't support the "unit" style. - // This is a workaround to display the scale without proper internationalization support. - if (!isNumberFormatSupported()) { - // $FlowIgnore[cannot-write] - this._setScale = legacySetScale.bind(this); - } - - bindAll([ - '_update', - '_setScale', - 'setUnit' - ], this); - } - - getDefaultPosition(): ControlPosition { - return 'bottom-left'; - } - - _update() { - // A horizontal scale is imagined to be present at center of the map - // container with maximum length (Default) as 100px. - // Using spherical law of cosines approximation, the real distance is - // found between the two coordinates. - const maxWidth = this.options.maxWidth || 100; - - const map = this._map; - const y = map._containerHeight / 2; - const x = (map._containerWidth / 2) - maxWidth / 2; - const left = map.unproject([x, y]); - const right = map.unproject([x + maxWidth, y]); - const maxMeters = left.distanceTo(right); - // The real distance corresponding to 100px scale length is rounded off to - // near pretty number and the scale length for the same is found out. - // Default unit of the scale is based on User's locale. - if (this.options.unit === 'imperial') { - const maxFeet = 3.2808 * maxMeters; - if (maxFeet > 5280) { - const maxMiles = maxFeet / 5280; - this._setScale(maxWidth, maxMiles, 'mile'); - } else { - this._setScale(maxWidth, maxFeet, 'foot'); - } - } else if (this.options.unit === 'nautical') { - const maxNauticals = maxMeters / 1852; - this._setScale(maxWidth, maxNauticals, 'nautical-mile'); - } else if (maxMeters >= 1000) { - this._setScale(maxWidth, maxMeters / 1000, 'kilometer'); - } else { - this._setScale(maxWidth, maxMeters, 'meter'); - } - } - - _setScale(maxWidth: number, maxDistance: number, unit: string) { - const distance = getRoundNum(maxDistance); - const ratio = distance / maxDistance; - - this._map._requestDomTask(() => { - this._container.style.width = `${maxWidth * ratio}px`; - - // Intl.NumberFormat doesn't support nautical-mile as a unit, - // so we are hardcoding `nm` as a unit symbol for all locales - if (unit === 'nautical-mile') { - this._container.innerHTML = `${distance} nm`; - return; - } - - // $FlowFixMe — flow v0.142.0 doesn't support optional `locales` argument and `unit` style option - this._container.innerHTML = new Intl.NumberFormat(this._language, {style: 'unit', unitDisplay: 'narrow', unit}).format(distance); - }); - } - - onAdd(map: Map): HTMLElement { - this._map = map; - this._language = map.getLanguage(); - this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-scale', map.getContainer()); - this._container.dir = 'auto'; - - this._map.on('move', this._update); - this._update(); - - return this._container; - } - - onRemove() { - this._container.remove(); - this._map.off('move', this._update); - this._map = (undefined: any); - } - - _setLanguage(language: string) { - this._language = language; - this._update(); - } - - /** + _map: Map; + _container: HTMLElement; + _language: ?string | ?Array; + options: Options; + + constructor(options: Options) { + this.options = extend({}, defaultOptions, options); + + // Some old browsers (e.g., Safari < 14.1) don't support the "unit" style. + // This is a workaround to display the scale without proper internationalization support. + if (!isNumberFormatSupported()) { + // $FlowIgnore[cannot-write] + this._setScale = legacySetScale.bind(this); + } + + bindAll(['_update', '_setScale', 'setUnit'], this); + } + + getDefaultPosition(): ControlPosition { + return 'bottom-left'; + } + + _update = () => { + // A horizontal scale is imagined to be present at center of the map + // container with maximum length (Default) as 100px. + // Using spherical law of cosines approximation, the real distance is + // found between the two coordinates. + const maxWidth = this.options.maxWidth || 100; + + const map = this._map; + const y = map._containerHeight / 2; + const x = map._containerWidth / 2 - maxWidth / 2; + const left = map.unproject([x, y]); + const right = map.unproject([x + maxWidth, y]); + const maxMeters = left.distanceTo(right); + // The real distance corresponding to 100px scale length is rounded off to + // near pretty number and the scale length for the same is found out. + // Default unit of the scale is based on User's locale. + if (this.options.unit === 'imperial') { + const maxFeet = 3.2808 * maxMeters; + if (maxFeet > 5280) { + const maxMiles = maxFeet / 5280; + this._setScale(maxWidth, maxMiles, 'mile'); + } else { + this._setScale(maxWidth, maxFeet, 'foot'); + } + } else if (this.options.unit === 'nautical') { + const maxNauticals = maxMeters / 1852; + this._setScale(maxWidth, maxNauticals, 'nautical-mile'); + } else if (maxMeters >= 1000) { + this._setScale(maxWidth, maxMeters / 1000, 'kilometer'); + } else { + this._setScale(maxWidth, maxMeters, 'meter'); + } + }; + + _setScale(maxWidth: number, maxDistance: number, unit: string) { + const distance = getRoundNum(maxDistance); + const ratio = distance / maxDistance; + + this._map._requestDomTask( + () => { + this._container.style.width = `${maxWidth * ratio}px`; + + // Intl.NumberFormat doesn't support nautical-mile as a unit, + // so we are hardcoding `nm` as a unit symbol for all locales + if (unit === 'nautical-mile') { + this._container.innerHTML = `${distance} nm`; + return; + } + + // $FlowFixMe — flow v0.142.0 doesn't support optional `locales` argument and `unit` style option + this._container.innerHTML = new Intl.NumberFormat( + this._language, + {style: 'unit', unitDisplay: 'narrow', unit}, + ).format(distance); + }, + ); + } + + onAdd(map: Map): HTMLElement { + this._map = map; + this._language = map.getLanguage(); + this._container = DOM.create( + 'div', + 'mapboxgl-ctrl mapboxgl-ctrl-scale', + map.getContainer(), + ); + this._container.dir = 'auto'; + + this._map.on('move', this._update); + this._update(); + + return this._container; + } + + onRemove() { + this._container.remove(); + this._map.off('move', this._update); + this._map = (undefined: any); + } + + _setLanguage(language: string) { + this._language = language; + this._update(); + } + + /** * Set the scale's unit of the distance. * * @param {'imperial' | 'metric' | 'nautical'} unit Unit of the distance (`'imperial'`, `'metric'` or `'nautical'`). */ - setUnit(unit: Unit) { - this.options.unit = unit; - this._update(); - } + setUnit(unit: Unit) { + this.options.unit = unit; + this._update(); + } } export default ScaleControl; diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 08c17939266..325a99363b7 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -32,55 +32,54 @@ const maxScalePerFrame = 2; * @see [Example: Disable scroll zoom](https://docs.mapbox.com/mapbox-gl-js/example/disable-scroll-zoom/) */ class ScrollZoomHandler { - _map: Map; - _el: HTMLElement; - _enabled: boolean; - _active: boolean; - _zooming: boolean; - _aroundCenter: boolean; - _aroundPoint: Point; - _aroundCoord: MercatorCoordinate; - _type: 'wheel' | 'trackpad' | null; - _lastValue: number; - _timeout: ?TimeoutID; // used for delayed-handling of a single wheel movement - _finishTimeout: ?TimeoutID; // used to delay final '{move,zoom}end' events - - _lastWheelEvent: any; - _lastWheelEventTime: number; - - _startZoom: ?number; - _targetZoom: ?number; - _delta: number; - _easing: ?((number) => number); - _prevEase: ?{start: number, duration: number, easing: (_: number) => number}; - - _frameId: ?boolean; - _handler: HandlerManager; - - _defaultZoomRate: number; - _wheelZoomRate: number; - - _alertContainer: HTMLElement; // used to display the scroll zoom blocker alert - _alertTimer: TimeoutID; - - /** + _map: Map; + _el: HTMLElement; + _enabled: boolean; + _active: boolean; + _zooming: boolean; + _aroundCenter: boolean; + _aroundPoint: Point; + _aroundCoord: MercatorCoordinate; + _type: 'wheel' | 'trackpad' | null; + _lastValue: number; + _timeout: ?TimeoutID; // used for delayed-handling of a single wheel movement + _finishTimeout: ?TimeoutID; // used to delay final '{move,zoom}end' events + + _lastWheelEvent: any; + _lastWheelEventTime: number; + + _startZoom: ?number; + _targetZoom: ?number; + _delta: number; + _easing: ?((number) => number); + _prevEase: ?{ start: number, duration: number, easing: (_: number) => number }; + + _frameId: ?boolean; + _handler: HandlerManager; + + _defaultZoomRate: number; + _wheelZoomRate: number; + + _alertContainer: HTMLElement; // used to display the scroll zoom blocker alert + _alertTimer: TimeoutID; + + /** * @private */ - constructor(map: Map, handler: HandlerManager) { - this._map = map; - this._el = map.getCanvasContainer(); - this._handler = handler; + constructor(map: Map, handler: HandlerManager) { + this._map = map; + this._el = map.getCanvasContainer(); + this._handler = handler; - this._delta = 0; + this._delta = 0; - this._defaultZoomRate = defaultZoomRate; - this._wheelZoomRate = wheelZoomRate; + this._defaultZoomRate = defaultZoomRate; + this._wheelZoomRate = wheelZoomRate; - bindAll(['_onTimeout', '_addScrollZoomBlocker', '_showBlockerAlert'], this); + bindAll(['_onTimeout', '_addScrollZoomBlocker', '_showBlockerAlert'], this); + } - } - - /** + /** * Sets the zoom rate of a trackpad. * * @param {number} [zoomRate=1/100] The rate used to scale trackpad movement to a zoom value. @@ -88,11 +87,11 @@ class ScrollZoomHandler { * // Speed up trackpad zoom * map.scrollZoom.setZoomRate(1 / 25); */ - setZoomRate(zoomRate: number) { - this._defaultZoomRate = zoomRate; - } + setZoomRate(zoomRate: number) { + this._defaultZoomRate = zoomRate; + } - /** + /** * Sets the zoom rate of a mouse wheel. * * @param {number} [wheelZoomRate=1/450] The rate used to scale mouse wheel movement to a zoom value. @@ -100,35 +99,35 @@ class ScrollZoomHandler { * // Slow down zoom of mouse wheel * map.scrollZoom.setWheelZoomRate(1 / 600); */ - setWheelZoomRate(wheelZoomRate: number) { - this._wheelZoomRate = wheelZoomRate; - } + setWheelZoomRate(wheelZoomRate: number) { + this._wheelZoomRate = wheelZoomRate; + } - /** + /** * Returns a Boolean indicating whether the "scroll to zoom" interaction is enabled. * * @returns {boolean} `true` if the "scroll to zoom" interaction is enabled. * @example * const isScrollZoomEnabled = map.scrollZoom.isEnabled(); */ - isEnabled(): boolean { - return !!this._enabled; - } + isEnabled(): boolean { + return !!this._enabled; + } - /* + /* * Active state is turned on and off with every scroll wheel event and is set back to false before the map * render is called, so _active is not a good candidate for determining if a scroll zoom animation is in * progress. */ - isActive(): boolean { - return this._active || this._finishTimeout !== undefined; - } + isActive(): boolean { + return this._active || this._finishTimeout !== undefined; + } - isZooming(): boolean { - return !!this._zooming; - } + isZooming(): boolean { + return !!this._zooming; + } - /** + /** * Enables the "scroll to zoom" interaction. * * @param {Object} [options] Options object. @@ -139,285 +138,319 @@ class ScrollZoomHandler { * @example * map.scrollZoom.enable({around: 'center'}); */ - enable(options: ?{around?: 'center'}) { - if (this.isEnabled()) return; - this._enabled = true; - this._aroundCenter = !!options && options.around === 'center'; - if (this._map._cooperativeGestures) this._addScrollZoomBlocker(); - } - - /** + enable(options: ?{ around?: 'center' }) { + if (this.isEnabled()) return; + this._enabled = true; + this._aroundCenter = !!options && options.around === 'center'; + if (this._map._cooperativeGestures) this._addScrollZoomBlocker(); + } + + /** * Disables the "scroll to zoom" interaction. * * @example * map.scrollZoom.disable(); */ - disable() { - if (!this.isEnabled()) return; - this._enabled = false; - if (this._map._cooperativeGestures) { - clearTimeout(this._alertTimer); - this._alertContainer.remove(); - } - } - - wheel(e: WheelEvent) { - if (!this.isEnabled()) return; - - if (this._map._cooperativeGestures) { - if (!e.ctrlKey && !e.metaKey && !this.isZooming() && !isFullscreen()) { - this._showBlockerAlert(); - return; - } else if (this._alertContainer.style.visibility !== 'hidden') { - // immediately hide alert if it is visible when ctrl or ⌘ is pressed while scroll zooming. - this._alertContainer.style.visibility = 'hidden'; - clearTimeout(this._alertTimer); - } - } - - // Remove `any` cast when https://github.com/facebook/flow/issues/4879 is fixed. - let value = e.deltaMode === (window.WheelEvent: any).DOM_DELTA_LINE ? e.deltaY * 40 : e.deltaY; - const now = browser.now(), - timeDelta = now - (this._lastWheelEventTime || 0); - - this._lastWheelEventTime = now; - - if (value !== 0 && (value % wheelZoomDelta) === 0) { - // This one is definitely a mouse wheel event. - this._type = 'wheel'; - - } else if (value !== 0 && Math.abs(value) < 4) { - // This one is definitely a trackpad event because it is so small. - this._type = 'trackpad'; - - } else if (timeDelta > 400) { - // This is likely a new scroll action. - this._type = null; - this._lastValue = value; - - // Start a timeout in case this was a singular event, and delay it by up to 40ms. - this._timeout = setTimeout(this._onTimeout, 40, e); - - } else if (!this._type) { - // This is a repeating event, but we don't know the type of event just yet. - // If the delta per time is small, we assume it's a fast trackpad; otherwise we switch into wheel mode. - this._type = (Math.abs(timeDelta * value) < 200) ? 'trackpad' : 'wheel'; - - // Make sure our delayed event isn't fired again, because we accumulate - // the previous event (which was less than 40ms ago) into this event. - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - value += this._lastValue; - } - } - - // Slow down zoom if shift key is held for more precise zooming - if (e.shiftKey && value) value = value / 4; - - // Only fire the callback if we actually know what type of scrolling device the user uses. - if (this._type) { - this._lastWheelEvent = e; - this._delta -= value; - if (!this._active) { - this._start(e); - } - } - - e.preventDefault(); - } - - _onTimeout(initialEvent: WheelEvent) { - this._type = 'wheel'; - this._delta -= this._lastValue; - if (!this._active) { - this._start(initialEvent); - } - } - - _start(e: WheelEvent) { - if (!this._delta) return; - - if (this._frameId) { - this._frameId = null; - } - - this._active = true; - if (!this.isZooming()) { - this._zooming = true; - } - - if (this._finishTimeout) { - clearTimeout(this._finishTimeout); - delete this._finishTimeout; - } - - const pos = DOM.mousePos(this._el, e); - this._aroundPoint = this._aroundCenter ? this._map.transform.centerPoint : pos; - this._aroundCoord = this._map.transform.pointCoordinate3D(this._aroundPoint); - this._targetZoom = undefined; - - if (!this._frameId) { - this._frameId = true; + disable() { + if (!this.isEnabled()) return; + this._enabled = false; + if (this._map._cooperativeGestures) { + clearTimeout(this._alertTimer); + this._alertContainer.remove(); + } + } + + wheel(e: WheelEvent) { + if (!this.isEnabled()) return; + + if (this._map._cooperativeGestures) { + if (!e.ctrlKey && !e.metaKey && !this.isZooming() && !isFullscreen()) { + this._showBlockerAlert(); + return; + } else if (this._alertContainer.style.visibility !== 'hidden') { + // immediately hide alert if it is visible when ctrl or ⌘ is pressed while scroll zooming. + this._alertContainer.style.visibility = 'hidden'; + clearTimeout(this._alertTimer); + } + } + + // Remove `any` cast when https://github.com/facebook/flow/issues/4879 is fixed. + let value = e.deltaMode === (window.WheelEvent: any).DOM_DELTA_LINE ? + e.deltaY * 40 : + e.deltaY; + const now = browser.now(), + timeDelta = now - (this._lastWheelEventTime || 0); + + this._lastWheelEventTime = now; + + if (value !== 0 && value % wheelZoomDelta === 0) { + // This one is definitely a mouse wheel event. + this._type = 'wheel'; + } else if (value !== 0 && Math.abs(value) < 4) { + // This one is definitely a trackpad event because it is so small. + this._type = 'trackpad'; + } else if (timeDelta > 400) { + // This is likely a new scroll action. + this._type = null; + this._lastValue = value; + + // Start a timeout in case this was a singular event, and delay it by up to 40ms. + this._timeout = setTimeout(this._onTimeout, 40, e); + } else if (!this._type) { + // This is a repeating event, but we don't know the type of event just yet. + // If the delta per time is small, we assume it's a fast trackpad; otherwise we switch into wheel mode. + this._type = Math.abs(timeDelta * value) < 200 ? 'trackpad' : 'wheel'; + + // Make sure our delayed event isn't fired again, because we accumulate + // the previous event (which was less than 40ms ago) into this event. + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + value += this._lastValue; + } + } + + // Slow down zoom if shift key is held for more precise zooming + if (e.shiftKey && value) value = value / 4; + + // Only fire the callback if we actually know what type of scrolling device the user uses. + if (this._type) { + this._lastWheelEvent = e; + this._delta -= value; + if (!this._active) { + this._start(e); + } + } + + e.preventDefault(); + } + + _onTimeout = (initialEvent: WheelEvent) => { + this._type = 'wheel'; + this._delta -= this._lastValue; + if (!this._active) { + this._start(initialEvent); + } + }; + + _start(e: WheelEvent) { + if (!this._delta) return; + + if (this._frameId) { + this._frameId = null; + } + + this._active = true; + if (!this.isZooming()) { + this._zooming = true; + } + + if (this._finishTimeout) { + clearTimeout(this._finishTimeout); + delete this._finishTimeout; + } + + const pos = DOM.mousePos(this._el, e); + this._aroundPoint = this._aroundCenter ? + this._map.transform.centerPoint : + pos; + this._aroundCoord = this._map.transform.pointCoordinate3D(this._aroundPoint); + this._targetZoom = undefined; + + if (!this._frameId) { + this._frameId = true; + this._handler._triggerRenderFrame(); + } + } + + renderFrame(): ?HandlerResult { + if (!this._frameId) return; + this._frameId = null; + + if (!this.isActive()) return; + + const tr = this._map.transform; + + // If projection wraps and center crosses the antimeridian, reset previous mouse scroll easing settings to resolve https://github.com/mapbox/mapbox-gl-js/issues/11910 + if ( + this._type === 'wheel' && tr.projection.wrap && + (tr._center.lng >= 180 || tr._center.lng <= -180) + ) { + this._prevEase = null; + this._easing = null; + this._lastWheelEvent = null; + this._lastWheelEventTime = 0; + } + + const startingZoom = (() => { + return tr._terrainEnabled() && this._aroundCoord ? + tr.computeZoomRelativeTo(this._aroundCoord) : + tr.zoom; + }); + + // if we've had scroll events since the last render frame, consume the + // accumulated delta, and update the target zoom level accordingly + if (this._delta !== 0) { + // For trackpad events and single mouse wheel ticks, use the default zoom rate + const zoomRate = this._type === 'wheel' && + Math.abs(this._delta) > wheelZoomDelta ? + this._wheelZoomRate : + this._defaultZoomRate; + // Scale by sigmoid of scroll wheel delta. + let scale = maxScalePerFrame / (1 + Math.exp( + -Math.abs(this._delta * zoomRate), + )); + + if (this._delta < 0 && scale !== 0) { + scale = 1 / scale; + } + + const startZoom = startingZoom(); + const startScale = Math.pow(2.0, startZoom); + + const fromScale = typeof this._targetZoom === 'number' ? + tr.zoomScale(this._targetZoom) : + startScale; + this._targetZoom = Math.min( + tr.maxZoom, + Math.max(tr.minZoom, tr.scaleZoom(fromScale * scale)), + ); + + // if this is a mouse wheel, refresh the starting zoom and easing + // function we're using to smooth out the zooming between wheel + // events + if (this._type === 'wheel') { + this._startZoom = startZoom; + this._easing = this._smoothOutEasing(200); + } + + this._delta = 0; + } + const targetZoom = typeof this._targetZoom === 'number' ? + this._targetZoom : + startingZoom(); + const startZoom = this._startZoom; + const easing = this._easing; + + let finished = false; + let zoom; + if (this._type === 'wheel' && startZoom && easing) { + assert(easing && typeof startZoom === 'number'); + + const t = Math.min((browser.now() - this._lastWheelEventTime) / 200, 1); + const k = easing(t); + zoom = interpolate(startZoom, targetZoom, k); + if (t < 1) { + if (!this._frameId) { + this._frameId = true; + } + } else { + finished = true; + } + } else { + zoom = targetZoom; + finished = true; + } + + this._active = true; + + if (finished) { + this._active = false; + this._finishTimeout = setTimeout( + () => { + this._zooming = false; this._handler._triggerRenderFrame(); - } - } - - renderFrame(): ?HandlerResult { - if (!this._frameId) return; - this._frameId = null; - - if (!this.isActive()) return; - - const tr = this._map.transform; - - // If projection wraps and center crosses the antimeridian, reset previous mouse scroll easing settings to resolve https://github.com/mapbox/mapbox-gl-js/issues/11910 - if (this._type === 'wheel' && tr.projection.wrap && (tr._center.lng >= 180 || tr._center.lng <= -180)) { - this._prevEase = null; - this._easing = null; - this._lastWheelEvent = null; - this._lastWheelEventTime = 0; - } - - const startingZoom = () => { - return (tr._terrainEnabled() && this._aroundCoord) ? tr.computeZoomRelativeTo(this._aroundCoord) : tr.zoom; - }; - - // if we've had scroll events since the last render frame, consume the - // accumulated delta, and update the target zoom level accordingly - if (this._delta !== 0) { - // For trackpad events and single mouse wheel ticks, use the default zoom rate - const zoomRate = (this._type === 'wheel' && Math.abs(this._delta) > wheelZoomDelta) ? this._wheelZoomRate : this._defaultZoomRate; - // Scale by sigmoid of scroll wheel delta. - let scale = maxScalePerFrame / (1 + Math.exp(-Math.abs(this._delta * zoomRate))); - - if (this._delta < 0 && scale !== 0) { - scale = 1 / scale; - } - - const startZoom = startingZoom(); - const startScale = Math.pow(2.0, startZoom); - - const fromScale = typeof this._targetZoom === 'number' ? tr.zoomScale(this._targetZoom) : startScale; - this._targetZoom = Math.min(tr.maxZoom, Math.max(tr.minZoom, tr.scaleZoom(fromScale * scale))); - - // if this is a mouse wheel, refresh the starting zoom and easing - // function we're using to smooth out the zooming between wheel - // events - if (this._type === 'wheel') { - this._startZoom = startZoom; - this._easing = this._smoothOutEasing(200); - } - - this._delta = 0; - } - const targetZoom = typeof this._targetZoom === 'number' ? - this._targetZoom : startingZoom(); - const startZoom = this._startZoom; - const easing = this._easing; - - let finished = false; - let zoom; - if (this._type === 'wheel' && startZoom && easing) { - assert(easing && typeof startZoom === 'number'); - - const t = Math.min((browser.now() - this._lastWheelEventTime) / 200, 1); - const k = easing(t); - zoom = interpolate(startZoom, targetZoom, k); - if (t < 1) { - if (!this._frameId) { - this._frameId = true; - } - } else { - finished = true; - } - } else { - zoom = targetZoom; - finished = true; - } - - this._active = true; - - if (finished) { - this._active = false; - this._finishTimeout = setTimeout(() => { - this._zooming = false; - this._handler._triggerRenderFrame(); - delete this._targetZoom; - delete this._finishTimeout; - }, 200); - } - - return { - noInertia: true, - needsRenderFrame: !finished, - zoomDelta: zoom - startingZoom(), - around: this._aroundPoint, - aroundCoord: this._aroundCoord, - originalEvent: this._lastWheelEvent - }; - } - - _smoothOutEasing(duration: number): (number) => number { - let easing = _ease; - - if (this._prevEase) { - const ease = this._prevEase, - t = (browser.now() - ease.start) / ease.duration, - speed = ease.easing(t + 0.01) - ease.easing(t), - - // Quick hack to make new bezier that is continuous with last - x = 0.27 / Math.sqrt(speed * speed + 0.0001) * 0.01, - y = Math.sqrt(0.27 * 0.27 - x * x); - - easing = bezier(x, y, 0.25, 1); - } - - this._prevEase = { - start: browser.now(), - duration, - easing - }; - - return easing; - } - - blur() { - this.reset(); - } - - reset() { - this._active = false; - } - - _addScrollZoomBlocker() { - if (this._map && !this._alertContainer) { - this._alertContainer = DOM.create('div', 'mapboxgl-scroll-zoom-blocker', this._map._container); - - if (/(Mac|iPad)/i.test(window.navigator.userAgent)) { - this._alertContainer.textContent = this._map._getUIString('ScrollZoomBlocker.CmdMessage'); - } else { - this._alertContainer.textContent = this._map._getUIString('ScrollZoomBlocker.CtrlMessage'); - } - - // dynamically set the font size of the scroll zoom blocker alert message - this._alertContainer.style.fontSize = `${Math.max(10, Math.min(24, Math.floor(this._el.clientWidth * 0.05)))}px`; - } - } - - _showBlockerAlert() { - this._alertContainer.style.visibility = 'visible'; - this._alertContainer.classList.add('mapboxgl-scroll-zoom-blocker-show'); - this._alertContainer.setAttribute("role", "alert"); - - clearTimeout(this._alertTimer); - - this._alertTimer = setTimeout(() => { - this._alertContainer.classList.remove('mapboxgl-scroll-zoom-blocker-show'); - this._alertContainer.setAttribute("role", "null"); - }, 200); - } - + delete this._targetZoom; + delete this._finishTimeout; + }, + 200, + ); + } + + return { + noInertia: true, + needsRenderFrame: !finished, + zoomDelta: zoom - startingZoom(), + around: this._aroundPoint, + aroundCoord: this._aroundCoord, + originalEvent: this._lastWheelEvent, + }; + } + + _smoothOutEasing(duration: number): (number) => number { + let easing = _ease; + + if (this._prevEase) { + const ease = this._prevEase, + t = (browser.now() - ease.start) / ease.duration, + speed = ease.easing(t + 0.01) - ease.easing(t), + // Quick hack to make new bezier that is continuous with last + x = 0.27 / Math.sqrt(speed * speed + 0.0001) * 0.01, + y = Math.sqrt(0.27 * 0.27 - x * x); + + easing = bezier(x, y, 0.25, 1); + } + + this._prevEase = { + start: browser.now(), + duration, + easing, + }; + + return easing; + } + + blur() { + this.reset(); + } + + reset() { + this._active = false; + } + + _addScrollZoomBlocker() { + if (this._map && !this._alertContainer) { + this._alertContainer = DOM.create( + 'div', + 'mapboxgl-scroll-zoom-blocker', + this._map._container, + ); + + if (/(Mac|iPad)/i.test(window.navigator.userAgent)) { + this._alertContainer.textContent = this._map._getUIString( + 'ScrollZoomBlocker.CmdMessage', + ); + } else { + this._alertContainer.textContent = this._map._getUIString( + 'ScrollZoomBlocker.CtrlMessage', + ); + } + + // dynamically set the font size of the scroll zoom blocker alert message + this._alertContainer.style.fontSize = `${Math.max( + 10, + Math.min(24, Math.floor(this._el.clientWidth * 0.05)), + )}px`; + } + } + + _showBlockerAlert() { + this._alertContainer.style.visibility = 'visible'; + this._alertContainer.classList.add('mapboxgl-scroll-zoom-blocker-show'); + this._alertContainer.setAttribute("role", "alert"); + + clearTimeout(this._alertTimer); + + this._alertTimer = setTimeout( + () => { + this._alertContainer.classList.remove( + 'mapboxgl-scroll-zoom-blocker-show', + ); + this._alertContainer.setAttribute("role", "null"); + }, + 200, + ); + } } export default ScrollZoomHandler; diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 3714944be4b..927501c9353 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -144,376 +144,457 @@ function hasChange(result: HandlerResult) { } class HandlerManager { - _map: Map; - _el: HTMLElement; - _handlers: Array<{ handlerName: string, handler: Handler, allowed: any }>; - _eventsInProgress: Object; - _frameId: ?number; - _inertia: HandlerInertia; - _bearingSnap: number; - _handlersById: { [string]: Handler }; - _updatingCamera: boolean; - _changes: Array<[HandlerResult, Object, any]>; - _previousActiveHandlers: { [string]: Handler }; - _listeners: Array<[HTMLElement, string, void | EventListenerOptionsOrUseCapture]>; - _trackingEllipsoid: TrackingEllipsoid; - _dragOrigin: ?Vec3; - - constructor(map: Map, options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number, bearingSnap: number}) { - this._map = map; - this._el = this._map.getCanvasContainer(); - this._handlers = []; - this._handlersById = {}; - this._changes = []; - - this._inertia = new HandlerInertia(map); - this._bearingSnap = options.bearingSnap; - this._previousActiveHandlers = {}; - this._trackingEllipsoid = new TrackingEllipsoid(); - this._dragOrigin = null; - - // Track whether map is currently moving, to compute start/move/end events - this._eventsInProgress = {}; - - this._addDefaultHandlers(options); - - bindAll(['handleEvent', 'handleWindowEvent'], this); - - const el = this._el; - - this._listeners = [ - // This needs to be `passive: true` so that a double tap fires two - // pairs of touchstart/end events in iOS Safari 13. If this is set to - // `passive: false` then the second pair of events is only fired if - // preventDefault() is called on the first touchstart. Calling preventDefault() - // undesirably prevents click events. - [el, 'touchstart', {passive: true}], - // This needs to be `passive: false` so that scrolls and pinches can be - // prevented in browsers that don't support `touch-actions: none`, for example iOS Safari 12. - [el, 'touchmove', {passive: false}], - [el, 'touchend', undefined], - [el, 'touchcancel', undefined], - - [el, 'mousedown', undefined], - [el, 'mousemove', undefined], - [el, 'mouseup', undefined], - - // Bind window-level event listeners for move and up/end events. In the absence of - // the pointer capture API, which is not supported by all necessary platforms, - // window-level event listeners give us the best shot at capturing events that - // fall outside the map canvas element. Use `{capture: true}` for the move event - // to prevent map move events from being fired during a drag. - [window.document, 'mousemove', {capture: true}], - [window.document, 'mouseup', undefined], - - [el, 'mouseover', undefined], - [el, 'mouseout', undefined], - [el, 'dblclick', undefined], - [el, 'click', undefined], - - [el, 'keydown', {capture: false}], - [el, 'keyup', undefined], - - [el, 'wheel', {passive: false}], - [el, 'contextmenu', undefined], - - [window, 'blur', undefined] - ]; - - for (const [target, type, listenerOptions] of this._listeners) { - const listener = target === window.document ? this.handleWindowEvent : this.handleEvent; - target.addEventListener((type: any), (listener: any), listenerOptions); - } - } - - destroy() { - for (const [target, type, listenerOptions] of this._listeners) { - const listener = target === window.document ? this.handleWindowEvent : this.handleEvent; - target.removeEventListener((type: any), (listener: any), listenerOptions); - } - } - - _addDefaultHandlers(options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number }) { - const map = this._map; - const el = map.getCanvasContainer(); - this._add('mapEvent', new MapEventHandler(map, options)); - - const boxZoom = map.boxZoom = new BoxZoomHandler(map, options); - this._add('boxZoom', boxZoom); - - const tapZoom = new TapZoomHandler(); - const clickZoom = new ClickZoomHandler(); - map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom); - this._add('tapZoom', tapZoom); - this._add('clickZoom', clickZoom); - - const tapDragZoom = new TapDragZoomHandler(); - this._add('tapDragZoom', tapDragZoom); - - const touchPitch = map.touchPitch = new TouchPitchHandler(map); - this._add('touchPitch', touchPitch); - - const mouseRotate = new MouseRotateHandler(options); - const mousePitch = new MousePitchHandler(options); - map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch); - this._add('mouseRotate', mouseRotate, ['mousePitch']); - this._add('mousePitch', mousePitch, ['mouseRotate']); - - const mousePan = new MousePanHandler(options); - const touchPan = new TouchPanHandler(map, options); - map.dragPan = new DragPanHandler(el, mousePan, touchPan); - this._add('mousePan', mousePan); - this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); - - const touchRotate = new TouchRotateHandler(); - const touchZoom = new TouchZoomHandler(); - map.touchZoomRotate = new TouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom); - this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']); - this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']); - - this._add('blockableMapEvent', new BlockableMapEventHandler(map)); - - const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, this); - this._add('scrollZoom', scrollZoom, ['mousePan']); - - const keyboard = map.keyboard = new KeyboardHandler(); - this._add('keyboard', keyboard); - - for (const name of ['boxZoom', 'doubleClickZoom', 'tapDragZoom', 'touchPitch', 'dragRotate', 'dragPan', 'touchZoomRotate', 'scrollZoom', 'keyboard']) { - if (options.interactive && (options: any)[name]) { - (map: any)[name].enable((options: any)[name]); - } - } - } - - _add(handlerName: string, handler: Handler, allowed?: Array) { - this._handlers.push({handlerName, handler, allowed}); - this._handlersById[handlerName] = handler; - } - - stop(allowEndAnimation: boolean) { - // do nothing if this method was triggered by a gesture update - if (this._updatingCamera) return; - - for (const {handler} of this._handlers) { - handler.reset(); - } - this._inertia.clear(); - this._fireEvents({}, {}, allowEndAnimation); - this._changes = []; - } - - isActive(): boolean { - for (const {handler} of this._handlers) { - if (handler.isActive()) return true; - } - return false; - } - - isZooming(): boolean { - return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming(); - } - - isRotating(): boolean { - return !!this._eventsInProgress.rotate; - } - - isMoving(): boolean { - return !!isMoving(this._eventsInProgress) || this.isZooming(); - } - - _isDragging(): boolean { - return !!this._eventsInProgress.drag; - } - - _blockedByActive(activeHandlers: { [string]: Handler }, allowed: Array, myName: string): boolean { - for (const name in activeHandlers) { - if (name === myName) continue; - if (!allowed || allowed.indexOf(name) < 0) { - return true; - } - } - return false; - } - - handleWindowEvent(e: InputEvent) { - this.handleEvent(e, `${e.type}Window`); - } - - _getMapTouches(touches: TouchList): TouchList { - const mapTouches = []; - for (const t of touches) { - const target = ((t.target: any): Node); - if (this._el.contains(target)) { - mapTouches.push(t); - } - } - return ((mapTouches: any): TouchList); - } - - handleEvent(e: InputEvent | RenderFrameEvent, eventName?: string) { - - this._updatingCamera = true; - assert(e.timeStamp !== undefined); - - const isRenderFrame = e.type === 'renderFrame'; - const inputEvent = isRenderFrame ? undefined : ((e: any): InputEvent); - - /* + _map: Map; + _el: HTMLElement; + _handlers: Array<{ handlerName: string, handler: Handler, allowed: any }>; + _eventsInProgress: Object; + _frameId: ?number; + _inertia: HandlerInertia; + _bearingSnap: number; + _handlersById: { [string]: Handler }; + _updatingCamera: boolean; + _changes: Array<[HandlerResult, Object, any]>; + _previousActiveHandlers: { [string]: Handler }; + _listeners: Array< + [HTMLElement, string, void | EventListenerOptionsOrUseCapture], >; + _trackingEllipsoid: TrackingEllipsoid; + _dragOrigin: ?Vec3; + + constructor( + map: Map, + options: { + interactive: boolean, + pitchWithRotate: boolean, + clickTolerance: number, + bearingSnap: number, + }, + ) { + this._map = map; + this._el = this._map.getCanvasContainer(); + this._handlers = []; + this._handlersById = {}; + this._changes = []; + + this._inertia = new HandlerInertia(map); + this._bearingSnap = options.bearingSnap; + this._previousActiveHandlers = {}; + this._trackingEllipsoid = new TrackingEllipsoid(); + this._dragOrigin = null; + + // Track whether map is currently moving, to compute start/move/end events + this._eventsInProgress = {}; + + this._addDefaultHandlers(options); + + bindAll(['handleEvent', 'handleWindowEvent'], this); + + const el = this._el; + + this._listeners = [ + // This needs to be `passive: true` so that a double tap fires two + // pairs of touchstart/end events in iOS Safari 13. If this is set to + // `passive: false` then the second pair of events is only fired if + // preventDefault() is called on the first touchstart. Calling preventDefault() + // undesirably prevents click events. + [el, 'touchstart', {passive: true}], + // This needs to be `passive: false` so that scrolls and pinches can be + // prevented in browsers that don't support `touch-actions: none`, for example iOS Safari 12. + [el, 'touchmove', {passive: false}], + [el, 'touchend', undefined], + [el, 'touchcancel', undefined], + + [el, 'mousedown', undefined], + [el, 'mousemove', undefined], + [el, 'mouseup', undefined], + + // Bind window-level event listeners for move and up/end events. In the absence of + // the pointer capture API, which is not supported by all necessary platforms, + // window-level event listeners give us the best shot at capturing events that + // fall outside the map canvas element. Use `{capture: true}` for the move event + // to prevent map move events from being fired during a drag. + [window.document, 'mousemove', {capture: true}], + [window.document, 'mouseup', undefined], + + [el, 'mouseover', undefined], + [el, 'mouseout', undefined], + [el, 'dblclick', undefined], + [el, 'click', undefined], + + [el, 'keydown', {capture: false}], + [el, 'keyup', undefined], + + [el, 'wheel', {passive: false}], + [el, 'contextmenu', undefined], + + [window, 'blur', undefined], + ]; + + for (const [target, type, listenerOptions] of this._listeners) { + const listener = target === window.document ? + this.handleWindowEvent : + this.handleEvent; + target.addEventListener((type: any), (listener: any), listenerOptions); + } + } + + destroy() { + for (const [target, type, listenerOptions] of this._listeners) { + const listener = target === window.document ? + this.handleWindowEvent : + this.handleEvent; + target.removeEventListener((type: any), (listener: any), listenerOptions); + } + } + + _addDefaultHandlers( + options: { + interactive: boolean, + pitchWithRotate: boolean, + clickTolerance: number, + }, + ) { + const map = this._map; + const el = map.getCanvasContainer(); + this._add('mapEvent', new MapEventHandler(map, options)); + + const boxZoom = map.boxZoom = new BoxZoomHandler(map, options); + this._add('boxZoom', boxZoom); + + const tapZoom = new TapZoomHandler(); + const clickZoom = new ClickZoomHandler(); + map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom); + this._add('tapZoom', tapZoom); + this._add('clickZoom', clickZoom); + + const tapDragZoom = new TapDragZoomHandler(); + this._add('tapDragZoom', tapDragZoom); + + const touchPitch = map.touchPitch = new TouchPitchHandler(map); + this._add('touchPitch', touchPitch); + + const mouseRotate = new MouseRotateHandler(options); + const mousePitch = new MousePitchHandler(options); + map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch); + this._add('mouseRotate', mouseRotate, ['mousePitch']); + this._add('mousePitch', mousePitch, ['mouseRotate']); + + const mousePan = new MousePanHandler(options); + const touchPan = new TouchPanHandler(map, options); + map.dragPan = new DragPanHandler(el, mousePan, touchPan); + this._add('mousePan', mousePan); + this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); + + const touchRotate = new TouchRotateHandler(); + const touchZoom = new TouchZoomHandler(); + map.touchZoomRotate = new TouchZoomRotateHandler( + el, + touchZoom, + touchRotate, + tapDragZoom, + ); + this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']); + this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']); + + this._add('blockableMapEvent', new BlockableMapEventHandler(map)); + + const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, this); + this._add('scrollZoom', scrollZoom, ['mousePan']); + + const keyboard = map.keyboard = new KeyboardHandler(); + this._add('keyboard', keyboard); + + for (const name of [ + 'boxZoom', + 'doubleClickZoom', + 'tapDragZoom', + 'touchPitch', + 'dragRotate', + 'dragPan', + 'touchZoomRotate', + 'scrollZoom', + 'keyboard', + ]) { + if (options.interactive && (options: any)[name]) { + (map: any)[name].enable((options: any)[name]); + } + } + } + + _add(handlerName: string, handler: Handler, allowed?: Array) { + this._handlers.push({handlerName, handler, allowed}); + this._handlersById[handlerName] = handler; + } + + stop(allowEndAnimation: boolean) { + // do nothing if this method was triggered by a gesture update + if (this._updatingCamera) return; + + for (const {handler} of this._handlers) { + handler.reset(); + } + this._inertia.clear(); + this._fireEvents({}, {}, allowEndAnimation); + this._changes = []; + } + + isActive(): boolean { + for (const {handler} of this._handlers) { + if (handler.isActive()) return true; + } + return false; + } + + isZooming(): boolean { + return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming(); + } + + isRotating(): boolean { + return !!this._eventsInProgress.rotate; + } + + isMoving(): boolean { + return !!isMoving(this._eventsInProgress) || this.isZooming(); + } + + _isDragging(): boolean { + return !!this._eventsInProgress.drag; + } + + _blockedByActive( + activeHandlers: { [string]: Handler }, + allowed: Array, + myName: string, + ): boolean { + for (const name in activeHandlers) { + if (name === myName) continue; + if (!allowed || allowed.indexOf(name) < 0) { + return true; + } + } + return false; + } + + handleWindowEvent = (e: InputEvent) => { + this.handleEvent(e, `${e.type}Window`); + }; + + _getMapTouches(touches: TouchList): TouchList { + const mapTouches = []; + for (const t of touches) { + const target = ((t.target: any): Node); + if (this._el.contains(target)) { + mapTouches.push(t); + } + } + return ((mapTouches: any): TouchList); + } + + handleEvent = (e: InputEvent | RenderFrameEvent, eventName?: string) => { + this._updatingCamera = true; + assert(e.timeStamp !== undefined); + + const isRenderFrame = e.type === 'renderFrame'; + const inputEvent = isRenderFrame ? undefined : ((e: any): InputEvent); + + /* * We don't call e.preventDefault() for any events by default. * Handlers are responsible for calling it where necessary. */ - const mergedHandlerResult: HandlerResult = {needsRenderFrame: false}; - const eventsInProgress = {}; - const activeHandlers = {}; - - const mapTouches = e.touches ? this._getMapTouches(((e: any): TouchEvent).touches) : undefined; - const points = mapTouches ? DOM.touchPos(this._el, mapTouches) : - isRenderFrame ? undefined : // renderFrame event doesn't have any points - DOM.mousePos(this._el, ((e: any): MouseEvent)); - - for (const {handlerName, handler, allowed} of this._handlers) { - if (!handler.isEnabled()) continue; - - let data: ?HandlerResult; - if (this._blockedByActive(activeHandlers, allowed, handlerName)) { - handler.reset(); - - } else { - if ((handler: any)[eventName || e.type]) { - data = (handler: any)[eventName || e.type](e, points, mapTouches); - this.mergeHandlerResult(mergedHandlerResult, eventsInProgress, data, handlerName, inputEvent); - if (data && data.needsRenderFrame) { - this._triggerRenderFrame(); - } - } - } - - if (data || handler.isActive()) { - activeHandlers[handlerName] = handler; - } - } - - const deactivatedHandlers = {}; - for (const name in this._previousActiveHandlers) { - if (!activeHandlers[name]) { - deactivatedHandlers[name] = inputEvent; - } - } - this._previousActiveHandlers = activeHandlers; - - if (Object.keys(deactivatedHandlers).length || hasChange(mergedHandlerResult)) { - this._changes.push([mergedHandlerResult, eventsInProgress, deactivatedHandlers]); - this._triggerRenderFrame(); - } - - if (Object.keys(activeHandlers).length || hasChange(mergedHandlerResult)) { - this._map._stop(true); - } - - this._updatingCamera = false; - - const {cameraAnimation} = mergedHandlerResult; - if (cameraAnimation) { - this._inertia.clear(); - this._fireEvents({}, {}, true); - this._changes = []; - cameraAnimation(this._map); - } - } - - mergeHandlerResult(mergedHandlerResult: HandlerResult, eventsInProgress: Object, handlerResult: HandlerResult, name: string, e?: InputEvent) { - if (!handlerResult) return; - - extend(mergedHandlerResult, handlerResult); - - const eventData = {handlerName: name, originalEvent: handlerResult.originalEvent || e}; - - // track which handler changed which camera property - if (handlerResult.zoomDelta !== undefined) { - eventsInProgress.zoom = eventData; - } - if (handlerResult.panDelta !== undefined) { - eventsInProgress.drag = eventData; - } - if (handlerResult.pitchDelta !== undefined) { - eventsInProgress.pitch = eventData; - } - if (handlerResult.bearingDelta !== undefined) { - eventsInProgress.rotate = eventData; - } - } - - _applyChanges() { - const combined = {}; - const combinedEventsInProgress = {}; - const combinedDeactivatedHandlers = {}; - - for (const [change, eventsInProgress, deactivatedHandlers] of this._changes) { - - if (change.panDelta) combined.panDelta = (combined.panDelta || new Point(0, 0))._add(change.panDelta); - if (change.zoomDelta) combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta; - if (change.bearingDelta) combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; - if (change.pitchDelta) combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; - if (change.around !== undefined) combined.around = change.around; - if (change.aroundCoord !== undefined) combined.aroundCoord = change.aroundCoord; - if (change.pinchAround !== undefined) combined.pinchAround = change.pinchAround; - if (change.noInertia) combined.noInertia = change.noInertia; - - extend(combinedEventsInProgress, eventsInProgress); - extend(combinedDeactivatedHandlers, deactivatedHandlers); - } - - this._updateMapTransform(combined, combinedEventsInProgress, combinedDeactivatedHandlers); - this._changes = []; - } - - _updateMapTransform(combinedResult: any, combinedEventsInProgress: Object, deactivatedHandlers: Object) { - - const map = this._map; - const tr = map.transform; - - const eventStarted = (type) => { - const newEvent = combinedEventsInProgress[type]; - return newEvent && !this._eventsInProgress[type]; - }; - - const eventEnded = (type) => { - const event = this._eventsInProgress[type]; - return event && !this._handlersById[event.handlerName].isActive(); - }; - - const toVec3 = (p: MercatorCoordinate): Vec3 => [p.x, p.y, p.z]; - - if (eventEnded("drag") && !hasChange(combinedResult)) { - const preZoom = tr.zoom; - tr.cameraElevationReference = "sea"; - tr.recenterOnTerrain(); - tr.cameraElevationReference = "ground"; - // Map zoom might change during the pan operation due to terrain elevation. - if (preZoom !== tr.zoom) this._map._update(true); - } - - // Catches double click and double tap zooms when camera is constrained over terrain - if (tr._isCameraConstrained) map._stop(true); - - if (!hasChange(combinedResult)) { - this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); - return; - } - - let {panDelta, zoomDelta, bearingDelta, pitchDelta, around, aroundCoord, pinchAround} = combinedResult; - - if (tr._isCameraConstrained) { - // Catches wheel zoom events when camera is constrained over terrain - if (zoomDelta > 0) zoomDelta = 0; - tr._isCameraConstrained = false; - } - - if (pinchAround !== undefined) { - around = pinchAround; - } + const mergedHandlerResult: HandlerResult = {needsRenderFrame: false}; + const eventsInProgress = {}; + const activeHandlers = {}; + + const mapTouches = e.touches ? + this._getMapTouches(((e: any): TouchEvent).touches) : + undefined; + const points = mapTouches ? + DOM.touchPos(this._el, mapTouches) : + isRenderFrame ? + undefined : // renderFrame event doesn't have any points + DOM.mousePos(this._el, ((e: any): MouseEvent)); + + for (const {handlerName, handler, allowed} of this._handlers) { + if (!handler.isEnabled()) continue; + + let data: ?HandlerResult; + if (this._blockedByActive(activeHandlers, allowed, handlerName)) { + handler.reset(); + } else { + if ((handler: any)[eventName || e.type]) { + data = (handler: any)[eventName || e.type](e, points, mapTouches); + this.mergeHandlerResult( + mergedHandlerResult, + eventsInProgress, + data, + handlerName, + inputEvent, + ); + if (data && data.needsRenderFrame) { + this._triggerRenderFrame(); + } + } + } + + if (data || handler.isActive()) { + activeHandlers[handlerName] = handler; + } + } + + const deactivatedHandlers = {}; + for (const name in this._previousActiveHandlers) { + if (!activeHandlers[name]) { + deactivatedHandlers[name] = inputEvent; + } + } + this._previousActiveHandlers = activeHandlers; + + if ( + Object.keys(deactivatedHandlers).length || hasChange(mergedHandlerResult) + ) { + this._changes.push( + [mergedHandlerResult, eventsInProgress, deactivatedHandlers], + ); + this._triggerRenderFrame(); + } + + if (Object.keys(activeHandlers).length || hasChange(mergedHandlerResult)) { + this._map._stop(true); + } + + this._updatingCamera = false; + + const {cameraAnimation} = mergedHandlerResult; + if (cameraAnimation) { + this._inertia.clear(); + this._fireEvents({}, {}, true); + this._changes = []; + cameraAnimation(this._map); + } + }; + + mergeHandlerResult( + mergedHandlerResult: HandlerResult, + eventsInProgress: Object, + handlerResult: HandlerResult, + name: string, + e?: InputEvent, + ) { + if (!handlerResult) return; + + extend(mergedHandlerResult, handlerResult); + + const eventData = { + handlerName: name, + originalEvent: handlerResult.originalEvent || e, + }; + + // track which handler changed which camera property + if (handlerResult.zoomDelta !== undefined) { + eventsInProgress.zoom = eventData; + } + if (handlerResult.panDelta !== undefined) { + eventsInProgress.drag = eventData; + } + if (handlerResult.pitchDelta !== undefined) { + eventsInProgress.pitch = eventData; + } + if (handlerResult.bearingDelta !== undefined) { + eventsInProgress.rotate = eventData; + } + } + + _applyChanges() { + const combined = {}; + const combinedEventsInProgress = {}; + const combinedDeactivatedHandlers = {}; + + for (const [change, eventsInProgress, deactivatedHandlers] of this._changes) { + if (change.panDelta) + combined.panDelta = (combined.panDelta || new Point(0, 0))._add( + change.panDelta, + ); + if (change.zoomDelta) + combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta; + if (change.bearingDelta) + combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; + if (change.pitchDelta) + combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; + if (change.around !== undefined) combined.around = change.around; + if (change.aroundCoord !== undefined) + combined.aroundCoord = change.aroundCoord; + if (change.pinchAround !== undefined) + combined.pinchAround = change.pinchAround; + if (change.noInertia) combined.noInertia = change.noInertia; + + extend(combinedEventsInProgress, eventsInProgress); + extend(combinedDeactivatedHandlers, deactivatedHandlers); + } + + this._updateMapTransform( + combined, + combinedEventsInProgress, + combinedDeactivatedHandlers, + ); + this._changes = []; + } + + _updateMapTransform( + combinedResult: any, + combinedEventsInProgress: Object, + deactivatedHandlers: Object, + ) { + const map = this._map; + const tr = map.transform; + + const eventStarted = (type => { + const newEvent = combinedEventsInProgress[type]; + return newEvent && !this._eventsInProgress[type]; + }); + + const eventEnded = (type => { + const event = this._eventsInProgress[type]; + return event && !this._handlersById[event.handlerName].isActive(); + }); + + const toVec3 = ((p: MercatorCoordinate): Vec3 => [p.x, p.y, p.z]); + + if (eventEnded("drag") && !hasChange(combinedResult)) { + const preZoom = tr.zoom; + tr.cameraElevationReference = "sea"; + tr.recenterOnTerrain(); + tr.cameraElevationReference = "ground"; + // Map zoom might change during the pan operation due to terrain elevation. + if (preZoom !== tr.zoom) this._map._update(true); + } + + // Catches double click and double tap zooms when camera is constrained over terrain + if (tr._isCameraConstrained) map._stop(true); + + if (!hasChange(combinedResult)) { + this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); + return; + } + + let { + panDelta, + zoomDelta, + bearingDelta, + pitchDelta, + around, + aroundCoord, + pinchAround + } = combinedResult; + + if (tr._isCameraConstrained) { + // Catches wheel zoom events when camera is constrained over terrain + if (zoomDelta > 0) zoomDelta = 0; + tr._isCameraConstrained = false; + } + + if (pinchAround !== undefined) { + around = pinchAround; + } if ((zoomDelta || eventStarted("drag")) && around) { this._dragOrigin = toVec3(tr.pointCoordinate3D(around)); @@ -522,172 +603,196 @@ class HandlerManager { this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin); } - // All movement of the camera is done relative to the sea level - tr.cameraElevationReference = "sea"; - - // stop any ongoing camera animations (easeTo, flyTo) - map._stop(true); - - around = around || map.transform.centerPoint; - if (bearingDelta) tr.bearing += bearingDelta; - if (pitchDelta) tr.pitch += pitchDelta; - tr._updateCameraState(); - - // Compute Mercator 3D camera offset based on screenspace panDelta - const panVec = [0, 0, 0]; - if (panDelta) { - if (tr.projection.name === 'mercator') { - assert(this._dragOrigin, '_dragOrigin should have been setup with a previous dragstart'); - const startPoint = this._trackingEllipsoid.projectRay(tr.screenPointToMercatorRay(around).dir); - const endPoint = this._trackingEllipsoid.projectRay(tr.screenPointToMercatorRay(around.sub(panDelta)).dir); - panVec[0] = endPoint[0] - startPoint[0]; - panVec[1] = endPoint[1] - startPoint[1]; - - } else { - const startPoint = tr.pointCoordinate(around); - if (tr.projection.name === 'globe') { - // Compute pan vector directly in pixel coordinates for the globe. - // Rotate the globe a bit faster when dragging near poles to compensate - // different pixel-per-meter ratios (ie. pixel-to-physical-rotation is lower) - panDelta = panDelta.rotate(-tr.angle); - const scale = tr._pixelsPerMercatorPixel / tr.worldSize; - panVec[0] = -panDelta.x * mercatorScale(latFromMercatorY(startPoint.y)) * scale; - panVec[1] = -panDelta.y * mercatorScale(tr.center.lat) * scale; - - } else { - const endPoint = tr.pointCoordinate(around.sub(panDelta)); - - if (startPoint && endPoint) { - panVec[0] = endPoint.x - startPoint.x; - panVec[1] = endPoint.y - startPoint.y; - } - } - } - } - - const originalZoom = tr.zoom; - // Compute Mercator 3D camera offset based on screenspace requested ZoomDelta - const zoomVec = [0, 0, 0]; - if (zoomDelta) { - // Zoom value has to be computed relative to a secondary map plane that is created from the terrain position below the cursor. - // This way the zoom interpolation can be kept linear and independent of the (possible) terrain elevation - const pickedPosition: Vec3 = aroundCoord ? toVec3(aroundCoord) : toVec3(tr.pointCoordinate3D(around)); - - const aroundRay = {dir: vec3.normalize([], vec3.sub([], pickedPosition, tr._camera.position))}; - if (aroundRay.dir[2] < 0) { - // Special handling is required if the ray created from the cursor is heading up. - // This scenario is possible if user is trying to zoom towards a feature like a hill or a mountain. - // Convert zoomDelta to a movement vector as if the camera would be orbiting around the picked point - const movement = tr.zoomDeltaToMovement(pickedPosition, zoomDelta); - vec3.scale(zoomVec, aroundRay.dir, movement); - } - } - - // Mutate camera state via CameraAPI - const translation = vec3.add(panVec, panVec, zoomVec); - tr._translateCameraConstrained(translation); - - if (zoomDelta && Math.abs(tr.zoom - originalZoom) > 0.0001) { - tr.recenterOnTerrain(); - } - - tr.cameraElevationReference = "ground"; - - this._map._update(); - if (!combinedResult.noInertia) this._inertia.record(combinedResult); - this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); - } - - _fireEvents(newEventsInProgress: { [string]: Object }, deactivatedHandlers: Object, allowEndAnimation: boolean) { - - const wasMoving = isMoving(this._eventsInProgress); - const nowMoving = isMoving(newEventsInProgress); - - const startEvents = {}; - - for (const eventName in newEventsInProgress) { - const {originalEvent} = newEventsInProgress[eventName]; - if (!this._eventsInProgress[eventName]) { - startEvents[`${eventName}start`] = originalEvent; - } - this._eventsInProgress[eventName] = newEventsInProgress[eventName]; - } - - // fire start events only after this._eventsInProgress has been updated - if (!wasMoving && nowMoving) { - this._fireEvent('movestart', nowMoving.originalEvent); - } - - for (const name in startEvents) { - this._fireEvent(name, startEvents[name]); - } - - if (nowMoving) { - this._fireEvent('move', nowMoving.originalEvent); - } - - for (const eventName in newEventsInProgress) { - const {originalEvent} = newEventsInProgress[eventName]; - this._fireEvent(eventName, originalEvent); - } - - const endEvents = {}; - - let originalEndEvent; - for (const eventName in this._eventsInProgress) { - const {handlerName, originalEvent} = this._eventsInProgress[eventName]; - if (!this._handlersById[handlerName].isActive()) { - delete this._eventsInProgress[eventName]; - originalEndEvent = deactivatedHandlers[handlerName] || originalEvent; - endEvents[`${eventName}end`] = originalEndEvent; - } - } - - for (const name in endEvents) { - this._fireEvent(name, endEvents[name]); - } - - const stillMoving = isMoving(this._eventsInProgress); - if (allowEndAnimation && (wasMoving || nowMoving) && !stillMoving) { - this._updatingCamera = true; - const inertialEase = this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions); - - const shouldSnapToNorth = bearing => bearing !== 0 && -this._bearingSnap < bearing && bearing < this._bearingSnap; - - if (inertialEase) { - if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) { - inertialEase.bearing = 0; - } - this._map.easeTo(inertialEase, {originalEvent: originalEndEvent}); - } else { - this._map.fire(new Event('moveend', {originalEvent: originalEndEvent})); - if (shouldSnapToNorth(this._map.getBearing())) { - this._map.resetNorth(); - } - } - this._updatingCamera = false; - } - - } - - _fireEvent(type: string, e: *) { - this._map.fire(new Event(type, e ? {originalEvent: e} : {})); - } - - _requestFrame(): number { - this._map.triggerRepaint(); - return this._map._renderTaskQueue.add(timeStamp => { - this._frameId = undefined; - this.handleEvent(new RenderFrameEvent('renderFrame', {timeStamp})); - this._applyChanges(); - }); - } - - _triggerRenderFrame() { - if (this._frameId === undefined) { - this._frameId = this._requestFrame(); - } - } + // All movement of the camera is done relative to the sea level + tr.cameraElevationReference = "sea"; + + // stop any ongoing camera animations (easeTo, flyTo) + map._stop(true); + + around = around || map.transform.centerPoint; + if (bearingDelta) tr.bearing += bearingDelta; + if (pitchDelta) tr.pitch += pitchDelta; + tr._updateCameraState(); + + // Compute Mercator 3D camera offset based on screenspace panDelta + const panVec = [0, 0, 0]; + if (panDelta) { + if (tr.projection.name === 'mercator') { + assert( + this._dragOrigin, + '_dragOrigin should have been setup with a previous dragstart', + ); + const startPoint = this._trackingEllipsoid.projectRay( + tr.screenPointToMercatorRay(around).dir, + ); + const endPoint = this._trackingEllipsoid.projectRay( + tr.screenPointToMercatorRay(around.sub(panDelta)).dir, + ); + panVec[0] = endPoint[0] - startPoint[0]; + panVec[1] = endPoint[1] - startPoint[1]; + } else { + const startPoint = tr.pointCoordinate(around); + if (tr.projection.name === 'globe') { + // Compute pan vector directly in pixel coordinates for the globe. + // Rotate the globe a bit faster when dragging near poles to compensate + // different pixel-per-meter ratios (ie. pixel-to-physical-rotation is lower) + panDelta = panDelta.rotate(-tr.angle); + const scale = tr._pixelsPerMercatorPixel / tr.worldSize; + panVec[0] = -panDelta.x * mercatorScale( + latFromMercatorY(startPoint.y), + ) * scale; + panVec[1] = -panDelta.y * mercatorScale(tr.center.lat) * scale; + } else { + const endPoint = tr.pointCoordinate(around.sub(panDelta)); + + if (startPoint && endPoint) { + panVec[0] = endPoint.x - startPoint.x; + panVec[1] = endPoint.y - startPoint.y; + } + } + } + } + + const originalZoom = tr.zoom; + // Compute Mercator 3D camera offset based on screenspace requested ZoomDelta + const zoomVec = [0, 0, 0]; + if (zoomDelta) { + // Zoom value has to be computed relative to a secondary map plane that is created from the terrain position below the cursor. + // This way the zoom interpolation can be kept linear and independent of the (possible) terrain elevation + const pickedPosition: Vec3 = aroundCoord ? + toVec3(aroundCoord) : + toVec3(tr.pointCoordinate3D(around)); + + const aroundRay = { + dir: vec3.normalize( + [], + vec3.sub([], pickedPosition, tr._camera.position), + ), + }; + if (aroundRay.dir[2] < 0) { + // Special handling is required if the ray created from the cursor is heading up. + // This scenario is possible if user is trying to zoom towards a feature like a hill or a mountain. + // Convert zoomDelta to a movement vector as if the camera would be orbiting around the picked point + const movement = tr.zoomDeltaToMovement(pickedPosition, zoomDelta); + vec3.scale(zoomVec, aroundRay.dir, movement); + } + } + + // Mutate camera state via CameraAPI + const translation = vec3.add(panVec, panVec, zoomVec); + tr._translateCameraConstrained(translation); + + if (zoomDelta && Math.abs(tr.zoom - originalZoom) > 0.0001) { + tr.recenterOnTerrain(); + } + + tr.cameraElevationReference = "ground"; + + this._map._update(); + if (!combinedResult.noInertia) this._inertia.record(combinedResult); + this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); + } + + _fireEvents( + newEventsInProgress: { [string]: Object }, + deactivatedHandlers: Object, + allowEndAnimation: boolean, + ) { + const wasMoving = isMoving(this._eventsInProgress); + const nowMoving = isMoving(newEventsInProgress); + + const startEvents = {}; + + for (const eventName in newEventsInProgress) { + const {originalEvent} = newEventsInProgress[eventName]; + if (!this._eventsInProgress[eventName]) { + startEvents[`${eventName}start`] = originalEvent; + } + this._eventsInProgress[eventName] = newEventsInProgress[eventName]; + } + + // fire start events only after this._eventsInProgress has been updated + if (!wasMoving && nowMoving) { + this._fireEvent('movestart', nowMoving.originalEvent); + } + + for (const name in startEvents) { + this._fireEvent(name, startEvents[name]); + } + + if (nowMoving) { + this._fireEvent('move', nowMoving.originalEvent); + } + + for (const eventName in newEventsInProgress) { + const {originalEvent} = newEventsInProgress[eventName]; + this._fireEvent(eventName, originalEvent); + } + + const endEvents = {}; + + let originalEndEvent; + for (const eventName in this._eventsInProgress) { + const {handlerName, originalEvent} = this._eventsInProgress[eventName]; + if (!this._handlersById[handlerName].isActive()) { + delete this._eventsInProgress[eventName]; + originalEndEvent = deactivatedHandlers[handlerName] || originalEvent; + endEvents[`${eventName}end`] = originalEndEvent; + } + } + + for (const name in endEvents) { + this._fireEvent(name, endEvents[name]); + } + + const stillMoving = isMoving(this._eventsInProgress); + if (allowEndAnimation && (wasMoving || nowMoving) && !stillMoving) { + this._updatingCamera = true; + const inertialEase = this._inertia._onMoveEnd( + this._map.dragPan._inertiaOptions, + ); + + const shouldSnapToNorth = (bearing => bearing !== 0 && + -this._bearingSnap < bearing && + bearing < this._bearingSnap); + + if (inertialEase) { + if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) { + inertialEase.bearing = 0; + } + this._map.easeTo(inertialEase, {originalEvent: originalEndEvent}); + } else { + this._map.fire( + new Event('moveend', {originalEvent: originalEndEvent}), + ); + if (shouldSnapToNorth(this._map.getBearing())) { + this._map.resetNorth(); + } + } + this._updatingCamera = false; + } + } + + _fireEvent(type: string, e: *) { + this._map.fire(new Event(type, e ? {originalEvent: e} : {})); + } + + _requestFrame(): number { + this._map.triggerRepaint(); + return this._map._renderTaskQueue.add( + timeStamp => { + this._frameId = undefined; + this.handleEvent(new RenderFrameEvent('renderFrame', {timeStamp})); + this._applyChanges(); + }, + ); + } + + _triggerRenderFrame() { + if (this._frameId === undefined) { + this._frameId = this._requestFrame(); + } + } } export default HandlerManager; diff --git a/src/ui/hash.js b/src/ui/hash.js index 4aee3a047cf..667ebd6c41c 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -13,117 +13,126 @@ import type Map from './map.js'; * @returns {Hash} `this` */ export default class Hash { - _map: ?Map; - _updateHash: () => ?TimeoutID; - _hashName: ?string; - - constructor(hashName: ?string) { - this._hashName = hashName && encodeURIComponent(hashName); - bindAll([ - '_getCurrentHash', - '_onHashChange', - '_updateHash' - ], this); - - // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. - this._updateHash = throttle(this._updateHashUnthrottled.bind(this), 30 * 1000 / 100); - } - - /* + _map: ?Map; + _updateHash: () => ?TimeoutID; + _hashName: ?string; + + constructor(hashName: ?string) { + this._hashName = hashName && encodeURIComponent(hashName); + bindAll(['_getCurrentHash', '_onHashChange', '_updateHash'], this); + + // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. + this._updateHash = throttle( + this._updateHashUnthrottled.bind(this), + 30 * 1000 / 100, + ); + } + + /* * Map element to listen for coordinate changes * * @param {Object} map * @returns {Hash} `this` */ - addTo(map: Map): this { - this._map = map; - window.addEventListener('hashchange', this._onHashChange, false); - map.on('moveend', this._updateHash); - return this; - } - - /* + addTo(map: Map): this { + this._map = map; + window.addEventListener('hashchange', this._onHashChange, false); + map.on('moveend', this._updateHash); + return this; + } + + /* * Removes hash * * @returns {Popup} `this` */ - remove(): this { - if (!this._map) return this; - - this._map.off('moveend', this._updateHash); - window.removeEventListener('hashchange', this._onHashChange, false); - clearTimeout(this._updateHash()); - - this._map = undefined; - return this; - } - - getHashString(): string { - const map = this._map; - if (!map) return ''; - - const hash = getHashString(map); - - if (this._hashName) { - const hashName = this._hashName; - let found = false; - const parts = window.location.hash.slice(1).split('&').map(part => { - const key = part.split('=')[0]; - if (key === hashName) { - found = true; - return `${key}=${hash}`; - } - return part; - }).filter(a => a); - if (!found) { - parts.push(`${hashName}=${hash}`); + remove(): this { + if (!this._map) return this; + + this._map.off('moveend', this._updateHash); + window.removeEventListener('hashchange', this._onHashChange, false); + clearTimeout(this._updateHash()); + + this._map = undefined; + return this; + } + + getHashString(): string { + const map = this._map; + if (!map) return ''; + + const hash = getHashString(map); + + if (this._hashName) { + const hashName = this._hashName; + let found = false; + const parts = window.location.hash.slice(1).split('&').map( + part => { + const key = part.split('=')[0]; + if (key === hashName) { + found = true; + return `${key}=${hash}`; } - return `#${parts.join('&')}`; - } - - return `#${hash}`; - } - - _getCurrentHash(): Array { - // Get the current hash from location, stripped from its number sign - const hash = window.location.hash.replace('#', ''); - if (this._hashName) { - // Split the parameter-styled hash into parts and find the value we need - let keyval; - hash.split('&').map( - part => part.split('=') - ).forEach(part => { - if (part[0] === this._hashName) { - keyval = part; - } - }); - return (keyval ? keyval[1] || '' : '').split('/'); - } - return hash.split('/'); - } - - _onHashChange(): boolean { - const map = this._map; - if (!map) return false; - const loc = this._getCurrentHash(); - if (loc.length >= 3 && !loc.some(v => isNaN(v))) { - const bearing = map.dragRotate.isEnabled() && map.touchZoomRotate.isEnabled() ? +(loc[3] || 0) : map.getBearing(); - map.jumpTo({ - center: [+loc[2], +loc[1]], - zoom: +loc[0], - bearing, - pitch: +(loc[4] || 0) - }); - return true; - } - return false; - } - - _updateHashUnthrottled() { - // Replace if already present, else append the updated hash string - const location = window.location.href.replace(/(#.+)?$/, this.getHashString()); - window.history.replaceState(window.history.state, null, location); - } + return part; + }, + ).filter(a => a); + if (!found) { + parts.push(`${hashName}=${hash}`); + } + return `#${parts.join('&')}`; + } + + return `#${hash}`; + } + + _getCurrentHash(): Array { + // Get the current hash from location, stripped from its number sign + const hash = window.location.hash.replace('#', ''); + if (this._hashName) { + // Split the parameter-styled hash into parts and find the value we need + let keyval; + hash.split('&').map(part => part.split('=')).forEach( + part => { + if (part[0] === this._hashName) { + keyval = part; + } + }, + ); + return (keyval ? keyval[1] || '' : '').split('/'); + } + return hash.split('/'); + } + + _onHashChange = (): boolean => { + const map = this._map; + if (!map) return false; + const loc = this._getCurrentHash(); + if (loc.length >= 3 && !loc.some(v => isNaN(v))) { + const bearing = map.dragRotate.isEnabled() && + map.touchZoomRotate.isEnabled() ? + +(loc[3] || 0) : + map.getBearing(); + map.jumpTo( + { + center: [+loc[2], +loc[1]], + zoom: +loc[0], + bearing, + pitch: +(loc[4] || 0), + }, + ); + return true; + } + return false; + }; + + _updateHashUnthrottled = () => { + // Replace if already present, else append the updated hash string + const location = window.location.href.replace( + /(#.+)?$/, + this.getHashString(), + ); + window.history.replaceState(window.history.state, null, location); + }; } export function getHashString(map: Map, mapFeedback?: boolean): string { diff --git a/src/ui/map.js b/src/ui/map.js index 8707b563db5..fcfe383b2f1 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -326,315 +326,374 @@ const defaultOptions = { * @see [Example: Display a map with a custom style](https://docs.mapbox.com/mapbox-gl-js/example/custom-style-id/) * @see [Example: Check if Mapbox GL JS is supported](https://docs.mapbox.com/mapbox-gl-js/example/check-for-support/) */ -class Map extends Camera { - style: Style; - painter: Painter; - handlers: ?HandlerManager; - - _container: HTMLElement; - _missingCSSCanary: HTMLElement; - _canvasContainer: HTMLElement; - _controlContainer: HTMLElement; - _controlPositions: {[_: string]: HTMLElement}; - _interactive: ?boolean; - _showTileBoundaries: ?boolean; - _showTerrainWireframe: ?boolean; - _showQueryGeometry: ?boolean; - _showCollisionBoxes: ?boolean; - _showPadding: ?boolean; - _showTileAABBs: ?boolean; - _showOverdrawInspector: boolean; - _repaint: ?boolean; - _vertices: ?boolean; - _canvas: HTMLCanvasElement; - _minTileCacheSize: number; - _maxTileCacheSize: number; - _frame: ?Cancelable; - _renderNextFrame: ?boolean; - _styleDirty: ?boolean; - _sourcesDirty: ?boolean; - _placementDirty: ?boolean; - _loaded: boolean; - _fullyLoaded: boolean; // accounts for placement finishing as well - _trackResize: boolean; - _preserveDrawingBuffer: boolean; - _failIfMajorPerformanceCaveat: boolean; - _antialias: boolean; - _useWebGL2: boolean; - _refreshExpiredTiles: boolean; - _hash: Hash; - _delegatedListeners: any; - _isInitialLoad: boolean; - _shouldCheckAccess: boolean; - _fadeDuration: number; - _crossSourceCollisions: boolean; - _collectResourceTiming: boolean; - _optimizeForTerrain: boolean; - _renderTaskQueue: TaskQueue; - _domRenderTaskQueue: TaskQueue; - _controls: Array; - _markers: Array; - _popups: Array; - _logoControl: IControl; - _mapId: number; - _localIdeographFontFamily: string; - _localFontFamily: string; - _requestManager: RequestManager; - _locale: Object; - _removed: boolean; - _speedIndexTiming: boolean; - _clickTolerance: number; - _cooperativeGestures: boolean; - _silenceAuthErrors: boolean; - _averageElevationLastSampledAt: number; - _averageElevationExaggeration: number; - _averageElevation: EasedVariable; - _containerWidth: number; - _containerHeight: number; - _language: ?string | ?string[]; - _worldview: ?string; - _interactionRange: [number, number]; - _visibilityHidden: number; - _performanceMetricsCollection: boolean; - - // `_useExplicitProjection` indicates that a projection is set by a call to map.setProjection() - _useExplicitProjection: boolean; - - /** @section {Interaction handlers} */ - - /** +class Map + extends Camera { + style: Style; + painter: Painter; + handlers: ?HandlerManager; + + _container: HTMLElement; + _missingCSSCanary: HTMLElement; + _canvasContainer: HTMLElement; + _controlContainer: HTMLElement; + _controlPositions: { [_: string]: HTMLElement }; + _interactive: ?boolean; + _showTileBoundaries: ?boolean; + _showTerrainWireframe: ?boolean; + _showQueryGeometry: ?boolean; + _showCollisionBoxes: ?boolean; + _showPadding: ?boolean; + _showTileAABBs: ?boolean; + _showOverdrawInspector: boolean; + _repaint: ?boolean; + _vertices: ?boolean; + _canvas: HTMLCanvasElement; + _minTileCacheSize: number; + _maxTileCacheSize: number; + _frame: ?Cancelable; + _renderNextFrame: ?boolean; + _styleDirty: ?boolean; + _sourcesDirty: ?boolean; + _placementDirty: ?boolean; + _loaded: boolean; + _fullyLoaded: boolean; // accounts for placement finishing as well + _trackResize: boolean; + _preserveDrawingBuffer: boolean; + _failIfMajorPerformanceCaveat: boolean; + _antialias: boolean; + _useWebGL2: boolean; + _refreshExpiredTiles: boolean; + _hash: Hash; + _delegatedListeners: any; + _isInitialLoad: boolean; + _shouldCheckAccess: boolean; + _fadeDuration: number; + _crossSourceCollisions: boolean; + _collectResourceTiming: boolean; + _optimizeForTerrain: boolean; + _renderTaskQueue: TaskQueue; + _domRenderTaskQueue: TaskQueue; + _controls: Array; + _markers: Array; + _popups: Array; + _logoControl: IControl; + _mapId: number; + _localIdeographFontFamily: string; + _localFontFamily: string; + _requestManager: RequestManager; + _locale: Object; + _removed: boolean; + _speedIndexTiming: boolean; + _clickTolerance: number; + _cooperativeGestures: boolean; + _silenceAuthErrors: boolean; + _averageElevationLastSampledAt: number; + _averageElevationExaggeration: number; + _averageElevation: EasedVariable; + _containerWidth: number; + _containerHeight: number; + _language: ?string | ?Array; + _worldview: ?string; + _interactionRange: [number, number]; + _visibilityHidden: number; + _performanceMetricsCollection: boolean; + + // `_useExplicitProjection` indicates that a projection is set by a call to map.setProjection() + _useExplicitProjection: boolean; + + /** @section {Interaction handlers} */ + + /** * The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad. * Find more details and examples using `scrollZoom` in the {@link ScrollZoomHandler} section. */ - scrollZoom: ScrollZoomHandler; + scrollZoom: ScrollZoomHandler; - /** + /** * The map's {@link BoxZoomHandler}, which implements zooming using a drag gesture with the Shift key pressed. * Find more details and examples using `boxZoom` in the {@link BoxZoomHandler} section. */ - boxZoom: BoxZoomHandler; + boxZoom: BoxZoomHandler; - /** + /** * The map's {@link DragRotateHandler}, which implements rotating the map while dragging with the right * mouse button or with the Control key pressed. Find more details and examples using `dragRotate` * in the {@link DragRotateHandler} section. */ - dragRotate: DragRotateHandler; + dragRotate: DragRotateHandler; - /** + /** * The map's {@link DragPanHandler}, which implements dragging the map with a mouse or touch gesture. * Find more details and examples using `dragPan` in the {@link DragPanHandler} section. */ - dragPan: DragPanHandler; + dragPan: DragPanHandler; - /** + /** * The map's {@link KeyboardHandler}, which allows the user to zoom, rotate, and pan the map using keyboard * shortcuts. Find more details and examples using `keyboard` in the {@link KeyboardHandler} section. */ - keyboard: KeyboardHandler; + keyboard: KeyboardHandler; - /** + /** * The map's {@link DoubleClickZoomHandler}, which allows the user to zoom by double clicking. * Find more details and examples using `doubleClickZoom` in the {@link DoubleClickZoomHandler} section. */ - doubleClickZoom: DoubleClickZoomHandler; + doubleClickZoom: DoubleClickZoomHandler; - /** + /** * The map's {@link TouchZoomRotateHandler}, which allows the user to zoom or rotate the map with touch gestures. * Find more details and examples using `touchZoomRotate` in the {@link TouchZoomRotateHandler} section. */ - touchZoomRotate: TouchZoomRotateHandler; + touchZoomRotate: TouchZoomRotateHandler; - /** + /** * The map's {@link TouchPitchHandler}, which allows the user to pitch the map with touch gestures. * Find more details and examples using `touchPitch` in the {@link TouchPitchHandler} section. */ - touchPitch: TouchPitchHandler; - - constructor(options: MapOptions) { - LivePerformanceUtils.mark(PerformanceMarkers.create); - - options = extend({}, defaultOptions, options); - - if (options.minZoom != null && options.maxZoom != null && options.minZoom > options.maxZoom) { - throw new Error(`maxZoom must be greater than or equal to minZoom`); - } - - if (options.minPitch != null && options.maxPitch != null && options.minPitch > options.maxPitch) { - throw new Error(`maxPitch must be greater than or equal to minPitch`); - } - - if (options.minPitch != null && options.minPitch < defaultMinPitch) { - throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); - } - - if (options.maxPitch != null && options.maxPitch > defaultMaxPitch) { - throw new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`); - } - - // disable antialias with OS/iOS 15.4 and 15.5 due to rendering bug - if (options.antialias && isSafariWithAntialiasingBug(window)) { - options.antialias = false; - warnOnce('Antialiasing is disabled for this WebGL context to avoid browser bug: https://github.com/mapbox/mapbox-gl-js/issues/11609'); - } - - const transform = new Transform(options.minZoom, options.maxZoom, options.minPitch, options.maxPitch, options.renderWorldCopies); - super(transform, options); - - this._interactive = options.interactive; - this._minTileCacheSize = options.minTileCacheSize; - this._maxTileCacheSize = options.maxTileCacheSize; - this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat; - this._preserveDrawingBuffer = options.preserveDrawingBuffer; - this._antialias = options.antialias; - this._useWebGL2 = options.useWebGL2; - this._trackResize = options.trackResize; - this._bearingSnap = options.bearingSnap; - this._refreshExpiredTiles = options.refreshExpiredTiles; - this._fadeDuration = options.fadeDuration; - this._isInitialLoad = true; - this._crossSourceCollisions = options.crossSourceCollisions; - this._collectResourceTiming = options.collectResourceTiming; - this._optimizeForTerrain = options.optimizeForTerrain; - this._language = this._parseLanguage(options.language); - this._worldview = options.worldview; - this._renderTaskQueue = new TaskQueue(); - this._domRenderTaskQueue = new TaskQueue(); - this._controls = []; - this._markers = []; - this._popups = []; - this._mapId = uniqueId(); - this._locale = extend({}, defaultLocale, options.locale); - this._clickTolerance = options.clickTolerance; - this._cooperativeGestures = options.cooperativeGestures; - this._performanceMetricsCollection = options.performanceMetricsCollection; - this._containerWidth = 0; - this._containerHeight = 0; - - this._averageElevationLastSampledAt = -Infinity; - this._averageElevationExaggeration = 0; - this._averageElevation = new EasedVariable(0); - - this._interactionRange = [+Infinity, -Infinity]; - this._visibilityHidden = 0; - - this._useExplicitProjection = false; // Fallback to stylesheet by default - - this._requestManager = new RequestManager(options.transformRequest, options.accessToken, options.testMode); - this._silenceAuthErrors = !!options.testMode; - - if (typeof options.container === 'string') { - this._container = window.document.getElementById(options.container); - - if (!this._container) { - throw new Error(`Container '${options.container}' not found.`); - } - } else if (options.container instanceof window.HTMLElement) { - this._container = options.container; - } else { - throw new Error(`Invalid type: 'container' must be a String or HTMLElement.`); - } - - if (this._container.childNodes.length > 0) { - warnOnce(`The map container element should be empty, otherwise the map's interactivity will be negatively impacted. If you want to display a message when WebGL is not supported, use the Mapbox GL Supported plugin instead.`); - } - - if (options.maxBounds) { - this.setMaxBounds(options.maxBounds); - } - - bindAll([ - '_onWindowOnline', - '_onWindowResize', - '_onVisibilityChange', - '_onMapScroll', - '_contextLost', - '_contextRestored' - ], this); - - this._setupContainer(); - this._setupPainter(); - if (this.painter === undefined) { - throw new Error(`Failed to initialize WebGL.`); - } - - this.on('move', () => this._update(false)); - this.on('moveend', () => this._update(false)); - this.on('zoom', () => this._update(true)); - - if (typeof window !== 'undefined') { - window.addEventListener('online', this._onWindowOnline, false); - window.addEventListener('resize', this._onWindowResize, false); - window.addEventListener('orientationchange', this._onWindowResize, false); - window.addEventListener('webkitfullscreenchange', this._onWindowResize, false); - window.addEventListener('visibilitychange', this._onVisibilityChange, false); - } - - this.handlers = new HandlerManager(this, options); - - this._localFontFamily = options.localFontFamily; - this._localIdeographFontFamily = options.localIdeographFontFamily; - - if (options.style) { - this.setStyle(options.style, {localFontFamily: this._localFontFamily, localIdeographFontFamily: this._localIdeographFontFamily}); - } - - if (options.projection) { - this.setProjection(options.projection); - } - - const hashName = (typeof options.hash === 'string' && options.hash) || undefined; - this._hash = options.hash && (new Hash(hashName)).addTo(this); - // don't set position from options if set through hash - if (!this._hash || !this._hash._onHashChange()) { - this.jumpTo({ - center: options.center, - zoom: options.zoom, - bearing: options.bearing, - pitch: options.pitch - }); - - if (options.bounds) { - this.resize(); - this.fitBounds(options.bounds, extend({}, options.fitBoundsOptions, {duration: 0})); - } - } - - this.resize(); - - if (options.attributionControl) - this.addControl(new AttributionControl({customAttribution: options.customAttribution})); - - this._logoControl = new LogoControl(); - this.addControl(this._logoControl, options.logoPosition); - - this.on('style.load', () => { - if (this.transform.unmodified) { - this.jumpTo((this.style.stylesheet: any)); - } - }); - this.on('data', (event: MapDataEvent) => { - this._update(event.dataType === 'style'); - this.fire(new Event(`${event.dataType}data`, event)); - }); - this.on('dataloading', (event: MapDataEvent) => { - this.fire(new Event(`${event.dataType}dataloading`, event)); - }); - } - - /* + touchPitch: TouchPitchHandler; + + constructor(options: MapOptions) { + LivePerformanceUtils.mark(PerformanceMarkers.create); + + options = extend({}, defaultOptions, options); + + if ( + options.minZoom != null && options.maxZoom != null && + options.minZoom > options.maxZoom + ) { + throw new Error(`maxZoom must be greater than or equal to minZoom`); + } + + if ( + options.minPitch != null && options.maxPitch != null && + options.minPitch > options.maxPitch + ) { + throw new Error(`maxPitch must be greater than or equal to minPitch`); + } + + if (options.minPitch != null && options.minPitch < defaultMinPitch) { + throw ( + new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`,) + ); + } + + if (options.maxPitch != null && options.maxPitch > defaultMaxPitch) { + throw ( + new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`) + ); + } + + // disable antialias with OS/iOS 15.4 and 15.5 due to rendering bug + if (options.antialias && isSafariWithAntialiasingBug(window)) { + options.antialias = false; + warnOnce( + 'Antialiasing is disabled for this WebGL context to avoid browser bug: https://github.com/mapbox/mapbox-gl-js/issues/11609', + ); + } + + const transform = new Transform( + options.minZoom, + options.maxZoom, + options.minPitch, + options.maxPitch, + options.renderWorldCopies, + ); + super(transform, options); + + this._interactive = options.interactive; + this._minTileCacheSize = options.minTileCacheSize; + this._maxTileCacheSize = options.maxTileCacheSize; + this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat; + this._preserveDrawingBuffer = options.preserveDrawingBuffer; + this._antialias = options.antialias; + this._useWebGL2 = options.useWebGL2; + this._trackResize = options.trackResize; + this._bearingSnap = options.bearingSnap; + this._refreshExpiredTiles = options.refreshExpiredTiles; + this._fadeDuration = options.fadeDuration; + this._isInitialLoad = true; + this._crossSourceCollisions = options.crossSourceCollisions; + this._collectResourceTiming = options.collectResourceTiming; + this._optimizeForTerrain = options.optimizeForTerrain; + this._language = this._parseLanguage(options.language); + this._worldview = options.worldview; + this._renderTaskQueue = new TaskQueue(); + this._domRenderTaskQueue = new TaskQueue(); + this._controls = []; + this._markers = []; + this._popups = []; + this._mapId = uniqueId(); + this._locale = extend({}, defaultLocale, options.locale); + this._clickTolerance = options.clickTolerance; + this._cooperativeGestures = options.cooperativeGestures; + this._performanceMetricsCollection = options.performanceMetricsCollection; + this._containerWidth = 0; + this._containerHeight = 0; + + this._averageElevationLastSampledAt = -Infinity; + this._averageElevationExaggeration = 0; + this._averageElevation = new EasedVariable(0); + + this._interactionRange = [+Infinity, -Infinity]; + this._visibilityHidden = 0; + + this._useExplicitProjection = false; // Fallback to stylesheet by default + + this._requestManager = new RequestManager( + options.transformRequest, + options.accessToken, + options.testMode, + ); + this._silenceAuthErrors = !!options.testMode; + + if (typeof options.container === 'string') { + this._container = window.document.getElementById(options.container); + + if (!this._container) { + throw new Error(`Container '${options.container}' not found.`); + } + } else if (options.container instanceof window.HTMLElement) { + this._container = options.container; + } else { + throw ( + new Error(`Invalid type: 'container' must be a String or HTMLElement.`) + ); + } + + if (this._container.childNodes.length > 0) { + warnOnce(`The map container element should be empty, otherwise the map's interactivity will be negatively impacted. If you want to display a message when WebGL is not supported, use the Mapbox GL Supported plugin instead.`,); + } + + if (options.maxBounds) { + this.setMaxBounds(options.maxBounds); + } + + bindAll( + [ + '_onWindowOnline', + '_onWindowResize', + '_onVisibilityChange', + '_onMapScroll', + '_contextLost', + '_contextRestored', + ], + this, + ); + + this._setupContainer(); + this._setupPainter(); + if (this.painter === undefined) { + throw new Error(`Failed to initialize WebGL.`); + } + + this.on('move', () => this._update(false)); + this.on('moveend', () => this._update(false)); + this.on('zoom', () => this._update(true)); + + if (typeof window !== 'undefined') { + window.addEventListener('online', this._onWindowOnline, false); + window.addEventListener('resize', this._onWindowResize, false); + window.addEventListener('orientationchange', this._onWindowResize, false); + window.addEventListener( + 'webkitfullscreenchange', + this._onWindowResize, + false, + ); + window.addEventListener( + 'visibilitychange', + this._onVisibilityChange, + false, + ); + } + + this.handlers = new HandlerManager(this, options); + + this._localFontFamily = options.localFontFamily; + this._localIdeographFontFamily = options.localIdeographFontFamily; + + if (options.style) { + this.setStyle( + options.style, + { + localFontFamily: this._localFontFamily, + localIdeographFontFamily: this._localIdeographFontFamily, + }, + ); + } + + if (options.projection) { + this.setProjection(options.projection); + } + + const hashName = typeof options.hash === 'string' && options.hash || + undefined; + this._hash = options.hash && new Hash(hashName).addTo(this); + // don't set position from options if set through hash + if (!this._hash || !this._hash._onHashChange()) { + this.jumpTo( + { + center: options.center, + zoom: options.zoom, + bearing: options.bearing, + pitch: options.pitch, + }, + ); + + if (options.bounds) { + this.resize(); + this.fitBounds( + options.bounds, + extend({}, options.fitBoundsOptions, {duration: 0}), + ); + } + } + + this.resize(); + + if (options.attributionControl) + this.addControl( + new AttributionControl({customAttribution: options.customAttribution}), + ); + + this._logoControl = new LogoControl(); + this.addControl(this._logoControl, options.logoPosition); + + this.on( + 'style.load', + () => { + if (this.transform.unmodified) { + this.jumpTo((this.style.stylesheet: any)); + } + }, + ); + this.on( + 'data', + (event: MapDataEvent) => { + this._update(event.dataType === 'style'); + this.fire(new Event(`${event.dataType}data`, event)); + }, + ); + this.on( + 'dataloading', + (event: MapDataEvent) => { + this.fire(new Event(`${event.dataType}dataloading`, event)); + }, + ); + } + + /* * Returns a unique number for this map instance which is used for the MapLoadEvent * to make sure we only fire one event per instantiated map object. * @private * @returns {number} */ - _getMapId(): number { - return this._mapId; - } + _getMapId(): number { + return this._mapId; + } - /** @section {Controls} */ + /** @section {Controls} */ - /** + /** * Adds an {@link IControl} to the map, calling `control.onAdd(this)`. * * @param {IControl} control The {@link IControl} to add. @@ -646,31 +705,39 @@ class Map extends Camera { * map.addControl(new mapboxgl.NavigationControl()); * @see [Example: Display map navigation controls](https://www.mapbox.com/mapbox-gl-js/example/navigation/) */ - addControl(control: IControl, position?: ControlPosition): this { - if (position === undefined) { - if (control.getDefaultPosition) { - position = control.getDefaultPosition(); - } else { - position = 'top-right'; - } - } - if (!control || !control.onAdd) { - return this.fire(new ErrorEvent(new Error( - 'Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.'))); - } - const controlElement = control.onAdd(this); - this._controls.push(control); - - const positionContainer = this._controlPositions[position]; - if (position.indexOf('bottom') !== -1) { - positionContainer.insertBefore(controlElement, positionContainer.firstChild); - } else { - positionContainer.appendChild(controlElement); - } - return this; - } - - /** + addControl(control: IControl, position?: ControlPosition): this { + if (position === undefined) { + if (control.getDefaultPosition) { + position = control.getDefaultPosition(); + } else { + position = 'top-right'; + } + } + if (!control || !control.onAdd) { + return this.fire( + new ErrorEvent( + new Error( + 'Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.', + ), + ), + ); + } + const controlElement = control.onAdd(this); + this._controls.push(control); + + const positionContainer = this._controlPositions[position]; + if (position.indexOf('bottom') !== -1) { + positionContainer.insertBefore( + controlElement, + positionContainer.firstChild, + ); + } else { + positionContainer.appendChild(controlElement); + } + return this; + } + + /** * Removes the control from the map. * * @param {IControl} control The {@link IControl} to remove. @@ -683,18 +750,23 @@ class Map extends Camera { * // Remove zoom and rotation controls from the map. * map.removeControl(navigation); */ - removeControl(control: IControl): this { - if (!control || !control.onRemove) { - return this.fire(new ErrorEvent(new Error( - 'Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.'))); - } - const ci = this._controls.indexOf(control); - if (ci > -1) this._controls.splice(ci, 1); - control.onRemove(this); - return this; - } - - /** + removeControl(control: IControl): this { + if (!control || !control.onRemove) { + return this.fire( + new ErrorEvent( + new Error( + 'Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.', + ), + ), + ); + } + const ci = this._controls.indexOf(control); + if (ci > -1) this._controls.splice(ci, 1); + control.onRemove(this); + return this; + } + + /** * Checks if a control is on the map. * * @param {IControl} control The {@link IControl} to check. @@ -708,22 +780,22 @@ class Map extends Camera { * const added = map.hasControl(navigation); * // added === true */ - hasControl(control: IControl): boolean { - return this._controls.indexOf(control) > -1; - } + hasControl(control: IControl): boolean { + return this._controls.indexOf(control) > -1; + } - /** + /** * Returns the map's containing HTML element. * * @returns {HTMLElement} The map's container. * @example * const container = map.getContainer(); */ - getContainer(): HTMLElement { - return this._container; - } + getContainer(): HTMLElement { + return this._container; + } - /** + /** * Returns the HTML element containing the map's `` element. * * If you want to add non-GL overlays to the map, you should append them to this element. @@ -738,11 +810,11 @@ class Map extends Camera { * @see [Example: Create a draggable point](https://www.mapbox.com/mapbox-gl-js/example/drag-a-point/) * @see [Example: Highlight features within a bounding box](https://www.mapbox.com/mapbox-gl-js/example/using-box-queryrenderedfeatures/) */ - getCanvasContainer(): HTMLElement { - return this._canvasContainer; - } + getCanvasContainer(): HTMLElement { + return this._canvasContainer; + } - /** + /** * Returns the map's `` element. * * @returns {HTMLCanvasElement} The map's `` element. @@ -752,13 +824,13 @@ class Map extends Camera { * @see [Example: Display a popup on hover](https://www.mapbox.com/mapbox-gl-js/example/popup-on-hover/) * @see [Example: Center the map on a clicked symbol](https://www.mapbox.com/mapbox-gl-js/example/center-on-symbol/) */ - getCanvas(): HTMLCanvasElement { - return this._canvas; - } + getCanvas(): HTMLCanvasElement { + return this._canvas; + } - /** @section {Map constraints} */ + /** @section {Map constraints} */ - /** + /** * Resizes the map according to the dimensions of its * `container` element. * @@ -776,31 +848,39 @@ class Map extends Camera { * const mapDiv = document.getElementById('map'); * if (mapDiv.style.visibility === true) map.resize(); */ - resize(eventData?: Object): this { - this._updateContainerDimensions(); + resize(eventData?: Object): this { + this._updateContainerDimensions(); - // do nothing if container remained the same size - if (this._containerWidth === this.transform.width && this._containerHeight === this.transform.height) return this; + // do nothing if container remained the same size + if ( + this._containerWidth === this.transform.width && + this._containerHeight === this.transform.height + ) + return this; - this._resizeCanvas(this._containerWidth, this._containerHeight); + this._resizeCanvas(this._containerWidth, this._containerHeight); - this.transform.resize(this._containerWidth, this._containerHeight); - this.painter.resize(Math.ceil(this._containerWidth), Math.ceil(this._containerHeight)); + this.transform.resize(this._containerWidth, this._containerHeight); + this.painter.resize( + Math.ceil(this._containerWidth), + Math.ceil(this._containerHeight), + ); - const fireMoving = !this._moving; - if (fireMoving) { - this.fire(new Event('movestart', eventData)) - .fire(new Event('move', eventData)); - } + const fireMoving = !this._moving; + if (fireMoving) { + this.fire(new Event('movestart', eventData)).fire( + new Event('move', eventData), + ); + } - this.fire(new Event('resize', eventData)); + this.fire(new Event('resize', eventData)); - if (fireMoving) this.fire(new Event('moveend', eventData)); + if (fireMoving) this.fire(new Event('moveend', eventData)); - return this; - } + return this; + } - /** + /** * Returns the map's geographical bounds. When the bearing or pitch is non-zero, the visible region is not * an axis-aligned rectangle, and the result is the smallest bounds that encompasses the visible region. * If a padding is set on the map, the bounds returned are for the inset. @@ -811,11 +891,11 @@ class Map extends Camera { * @example * const bounds = map.getBounds(); */ - getBounds(): LngLatBounds | null { - return this.transform.getBounds(); - } + getBounds(): LngLatBounds | null { + return this.transform.getBounds(); + } - /** + /** * Returns the maximum geographical bounds the map is constrained to, or `null` if none set. * * @returns {Map} The map object. @@ -823,11 +903,11 @@ class Map extends Camera { * @example * const maxBounds = map.getMaxBounds(); */ - getMaxBounds(): LngLatBounds | null { - return this.transform.getMaxBounds() || null; - } + getMaxBounds(): LngLatBounds | null { + return this.transform.getMaxBounds() || null; + } - /** + /** * Sets or clears the map's geographical bounds. * * Pan and zoom operations are constrained within these bounds. @@ -851,12 +931,12 @@ class Map extends Camera { * // Set the map's max bounds. * map.setMaxBounds(bounds); */ - setMaxBounds(bounds: LngLatBoundsLike): this { - this.transform.setMaxBounds(LngLatBounds.convert(bounds)); - return this._update(); - } + setMaxBounds(bounds: LngLatBoundsLike): this { + this.transform.setMaxBounds(LngLatBounds.convert(bounds)); + return this._update(); + } - /** + /** * Sets or clears the map's minimum zoom level. * If the map's current zoom level is lower than the new minimum, * the map will zoom to the new minimum. @@ -872,37 +952,41 @@ class Map extends Camera { * @example * map.setMinZoom(12.25); */ - setMinZoom(minZoom?: ?number): this { - - minZoom = minZoom === null || minZoom === undefined ? defaultMinZoom : minZoom; - - if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) { - this.transform.minZoom = minZoom; - this._update(); - - if (this.getZoom() < minZoom) { - this.setZoom(minZoom); - } else { - this.fire(new Event('zoomstart')) - .fire(new Event('zoom')) - .fire(new Event('zoomend')); - } - - return this; - - } else throw new Error(`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`); - } - - /** + setMinZoom(minZoom?: ?number): this { + minZoom = minZoom === null || minZoom === undefined ? + defaultMinZoom : + minZoom; + + if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) { + this.transform.minZoom = minZoom; + this._update(); + + if (this.getZoom() < minZoom) { + this.setZoom(minZoom); + } else { + this.fire(new Event('zoomstart')).fire(new Event('zoom')).fire( + new Event('zoomend'), + ); + } + + return this; + } else throw ( + new Error(`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`,) + ); + } + + /** * Returns the map's minimum allowable zoom level. * * @returns {number} Returns `minZoom`. * @example * const minZoom = map.getMinZoom(); */ - getMinZoom(): number { return this.transform.minZoom; } + getMinZoom(): number { + return this.transform.minZoom; + } - /** + /** * Sets or clears the map's maximum zoom level. * If the map's current zoom level is higher than the new maximum, * the map will zoom to the new maximum. @@ -913,37 +997,39 @@ class Map extends Camera { * @example * map.setMaxZoom(18.75); */ - setMaxZoom(maxZoom?: ?number): this { - - maxZoom = maxZoom === null || maxZoom === undefined ? defaultMaxZoom : maxZoom; - - if (maxZoom >= this.transform.minZoom) { - this.transform.maxZoom = maxZoom; - this._update(); - - if (this.getZoom() > maxZoom) { - this.setZoom(maxZoom); - } else { - this.fire(new Event('zoomstart')) - .fire(new Event('zoom')) - .fire(new Event('zoomend')); - } - - return this; - - } else throw new Error(`maxZoom must be greater than the current minZoom`); - } - - /** + setMaxZoom(maxZoom?: ?number): this { + maxZoom = maxZoom === null || maxZoom === undefined ? + defaultMaxZoom : + maxZoom; + + if (maxZoom >= this.transform.minZoom) { + this.transform.maxZoom = maxZoom; + this._update(); + + if (this.getZoom() > maxZoom) { + this.setZoom(maxZoom); + } else { + this.fire(new Event('zoomstart')).fire(new Event('zoom')).fire( + new Event('zoomend'), + ); + } + + return this; + } else throw new Error(`maxZoom must be greater than the current minZoom`); + } + + /** * Returns the map's maximum allowable zoom level. * * @returns {number} Returns `maxZoom`. * @example * const maxZoom = map.getMaxZoom(); */ - getMaxZoom(): number { return this.transform.maxZoom; } + getMaxZoom(): number { + return this.transform.maxZoom; + } - /** + /** * Sets or clears the map's minimum pitch. * If the map's current pitch is lower than the new minimum, * the map will pitch to the new minimum. @@ -953,41 +1039,47 @@ class Map extends Camera { * @example * map.setMinPitch(5); */ - setMinPitch(minPitch?: ?number): this { - - minPitch = minPitch === null || minPitch === undefined ? defaultMinPitch : minPitch; - - if (minPitch < defaultMinPitch) { - throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); - } - - if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) { - this.transform.minPitch = minPitch; - this._update(); - - if (this.getPitch() < minPitch) { - this.setPitch(minPitch); - } else { - this.fire(new Event('pitchstart')) - .fire(new Event('pitch')) - .fire(new Event('pitchend')); - } - - return this; - - } else throw new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`); - } - - /** + setMinPitch(minPitch?: ?number): this { + minPitch = minPitch === null || minPitch === undefined ? + defaultMinPitch : + minPitch; + + if (minPitch < defaultMinPitch) { + throw ( + new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`,) + ); + } + + if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) { + this.transform.minPitch = minPitch; + this._update(); + + if (this.getPitch() < minPitch) { + this.setPitch(minPitch); + } else { + this.fire(new Event('pitchstart')).fire(new Event('pitch')).fire( + new Event('pitchend'), + ); + } + + return this; + } else throw ( + new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`,) + ); + } + + /** * Returns the map's minimum allowable pitch. * * @returns {number} Returns `minPitch`. * @example * const minPitch = map.getMinPitch(); */ - getMinPitch(): number { return this.transform.minPitch; } + getMinPitch(): number { + return this.transform.minPitch; + } - /** + /** * Sets or clears the map's maximum pitch. * If the map's current pitch is higher than the new maximum, * the map will pitch to the new maximum. @@ -998,41 +1090,45 @@ class Map extends Camera { * @example * map.setMaxPitch(70); */ - setMaxPitch(maxPitch?: ?number): this { - - maxPitch = maxPitch === null || maxPitch === undefined ? defaultMaxPitch : maxPitch; - - if (maxPitch > defaultMaxPitch) { - throw new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`); - } - - if (maxPitch >= this.transform.minPitch) { - this.transform.maxPitch = maxPitch; - this._update(); - - if (this.getPitch() > maxPitch) { - this.setPitch(maxPitch); - } else { - this.fire(new Event('pitchstart')) - .fire(new Event('pitch')) - .fire(new Event('pitchend')); - } - - return this; - - } else throw new Error(`maxPitch must be greater than or equal to minPitch`); - } - - /** + setMaxPitch(maxPitch?: ?number): this { + maxPitch = maxPitch === null || maxPitch === undefined ? + defaultMaxPitch : + maxPitch; + + if (maxPitch > defaultMaxPitch) { + throw ( + new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`) + ); + } + + if (maxPitch >= this.transform.minPitch) { + this.transform.maxPitch = maxPitch; + this._update(); + + if (this.getPitch() > maxPitch) { + this.setPitch(maxPitch); + } else { + this.fire(new Event('pitchstart')).fire(new Event('pitch')).fire( + new Event('pitchend'), + ); + } + + return this; + } else throw new Error(`maxPitch must be greater than or equal to minPitch`); + } + + /** * Returns the map's maximum allowable pitch. * * @returns {number} Returns `maxPitch`. * @example * const maxPitch = map.getMaxPitch(); */ - getMaxPitch(): number { return this.transform.maxPitch; } + getMaxPitch(): number { + return this.transform.maxPitch; + } - /** + /** * Returns the state of `renderWorldCopies`. If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: * - When the map is zoomed out far enough that a single representation of the world does not fill the map's entire * container, there will be blank space beyond 180 and -180 degrees longitude. @@ -1044,9 +1140,11 @@ class Map extends Camera { * const worldCopiesRendered = map.getRenderWorldCopies(); * @see [Example: Render world copies](https://docs.mapbox.com/mapbox-gl-js/example/render-world-copies/) */ - getRenderWorldCopies(): boolean { return this.transform.renderWorldCopies; } + getRenderWorldCopies(): boolean { + return this.transform.renderWorldCopies; + } - /** + /** * Sets the state of `renderWorldCopies`. * * @param {boolean} renderWorldCopies If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: @@ -1061,15 +1159,15 @@ class Map extends Camera { * map.setRenderWorldCopies(true); * @see [Example: Render world copies](https://docs.mapbox.com/mapbox-gl-js/example/render-world-copies/) */ - setRenderWorldCopies(renderWorldCopies?: ?boolean): this { - this.transform.renderWorldCopies = renderWorldCopies; - if (!this.transform.renderWorldCopies) { - this._forceMarkerAndPopupUpdate(true); - } - return this._update(); - } - - /** + setRenderWorldCopies(renderWorldCopies?: ?boolean): this { + this.transform.renderWorldCopies = renderWorldCopies; + if (!this.transform.renderWorldCopies) { + this._forceMarkerAndPopupUpdate(true); + } + return this._update(); + } + + /** * Returns the map's language, which is used for translating map labels and UI components. * * @private @@ -1077,20 +1175,23 @@ class Map extends Camera { * @example * const language = map.getLanguage(); */ - getLanguage(): ?string | ?string[] { - return this._language; - } - - _parseLanguage(language?: 'auto' | ?string | ?string[]): ?string | ?string[] { - if (language === 'auto') return window.navigator.language; - if (Array.isArray(language)) return language.length === 0 ? - undefined : - language.map(l => l === 'auto' ? window.navigator.language : l); - - return language; - } - - /** + getLanguage(): ?string | ?Array { + return this._language; + } + + _parseLanguage( + language?: 'auto' | ?string | ?Array, + ): ?string | ?Array { + if (language === 'auto') return window.navigator.language; + if (Array.isArray(language)) + return language.length === 0 ? + undefined : + language.map(l => l === 'auto' ? window.navigator.language : l); + + return language; + } + + /** * Sets the map's language, which is used for translating map labels and UI components. * * @private @@ -1113,23 +1214,23 @@ class Map extends Camera { * @example * map.setLanguage(); */ - setLanguage(language?: 'auto' | ?string | ?string[]): this { - const newLanguage = this._parseLanguage(language); - if (!this.style || newLanguage === this._language) return this; - this._language = newLanguage; + setLanguage(language?: 'auto' | ?string | ?Array): this { + const newLanguage = this._parseLanguage(language); + if (!this.style || newLanguage === this._language) return this; + this._language = newLanguage; - this.style._reloadSources(); + this.style._reloadSources(); - for (const control of this._controls) { - if (control._setLanguage) { - control._setLanguage(this._language); - } - } + for (const control of this._controls) { + if (control._setLanguage) { + control._setLanguage(this._language); + } + } - return this; - } + return this; + } - /** + /** * Returns the code for the map's worldview. * * @private @@ -1137,11 +1238,11 @@ class Map extends Camera { * @example * const worldview = map.getWorldview(); */ - getWorldview(): ?string { - return this._worldview; - } + getWorldview(): ?string { + return this._worldview; + } - /** + /** * Sets the map's worldview. * * @private @@ -1157,32 +1258,32 @@ class Map extends Camera { * @example * map.setWorldView(); */ - setWorldview(worldview?: ?string): this { - if (!this.style || worldview === this._worldview) return this; + setWorldview(worldview?: ?string): this { + if (!this.style || worldview === this._worldview) return this; - this._worldview = worldview; - this.style._reloadSources(); + this._worldview = worldview; + this.style._reloadSources(); - return this; - } + return this; + } - /** @section {Point conversion} */ + /** @section {Point conversion} */ - /** + /** * Returns a [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) object that defines the current map projection. * * @returns {ProjectionSpecification} The [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) defining the current map projection. * @example * const projection = map.getProjection(); */ - getProjection(): ProjectionSpecification { - if (this.transform.mercatorFromTransition) { - return {name: "globe", center: [0, 0]}; - } - return this.transform.getProjection(); - } - - /** + getProjection(): ProjectionSpecification { + if (this.transform.mercatorFromTransition) { + return {name: "globe", center: [0, 0]}; + } + return this.transform.getProjection(); + } + + /** * Returns true if map [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) has been set to globe AND the map is at a low enough zoom level that globe view is enabled. * @private * @returns {boolean} Returns `globe-is-active` boolean. @@ -1191,9 +1292,11 @@ class Map extends Camera { * // do globe things here * } */ - _showingGlobe(): boolean { return this.transform.projection.name === 'globe'; } + _showingGlobe(): boolean { + return this.transform.projection.name === 'globe'; + } - /** + /** * Sets the map's projection. If called with `null` or `undefined`, the map will reset to Mercator. * * @param {ProjectionSpecification | string | null | undefined} projection The projection that the map should be rendered in. @@ -1209,78 +1312,87 @@ class Map extends Camera { * @see [Example: Display a web map using an alternate projection](https://docs.mapbox.com/mapbox-gl-js/example/map-projection/) * @see [Example: Use different map projections for web maps](https://docs.mapbox.com/mapbox-gl-js/example/projections/) */ - setProjection(projection?: ?ProjectionSpecification | string): this { - this._lazyInitEmptyStyle(); - - if (!projection) { - projection = null; - } else if (typeof projection === 'string') { - projection = (({name: projection}: any): ProjectionSpecification); - } - - this._useExplicitProjection = !!projection; - const stylesheetProjection = this.style.stylesheet ? this.style.stylesheet.projection : null; - return this._prioritizeAndUpdateProjection(projection, stylesheetProjection); - } - - _updateProjectionTransition() { - // The projection isn't globe, we can skip updating the transition - if (this.getProjection().name !== 'globe') { - return; - } - - const tr = this.transform; - const projection = tr.projection.name; - let projectionHasChanged; - - if (projection === 'globe' && tr.zoom >= GLOBE_ZOOM_THRESHOLD_MAX) { - tr.setMercatorFromTransition(); - projectionHasChanged = true; - } else if (projection === 'mercator' && tr.zoom < GLOBE_ZOOM_THRESHOLD_MAX) { - tr.setProjection({name: 'globe'}); - projectionHasChanged = true; - } - - if (projectionHasChanged) { - this.style.applyProjectionUpdate(); - this.style._forceSymbolLayerUpdate(); - } - } - - _prioritizeAndUpdateProjection(explicitProjection: ?ProjectionSpecification, styleProjection: ?ProjectionSpecification): this { - // Given a stylesheet and eventual runtime projection, in order of priority, we select: - // 1. the explicit projection - // 2. the stylesheet projection - // 3. mercator (fallback) - const prioritizedProjection = explicitProjection || styleProjection || {name: "mercator"}; - - return this._updateProjection(prioritizedProjection); - } - - _updateProjection(projection: ProjectionSpecification): this { - let projectionHasChanged; - - if (projection.name === 'globe' && this.transform.zoom >= GLOBE_ZOOM_THRESHOLD_MAX) { - projectionHasChanged = this.transform.setMercatorFromTransition(); - } else { - projectionHasChanged = this.transform.setProjection(projection); - } - - this.style.applyProjectionUpdate(); - - if (projectionHasChanged) { - this.painter.clearBackgroundTiles(); - for (const id in this.style._sourceCaches) { - this.style._sourceCaches[id].clearTiles(); - } - this._update(true); - this._forceMarkerAndPopupUpdate(true); - } - - return this; - } - - /** + setProjection(projection?: ?ProjectionSpecification | string): this { + this._lazyInitEmptyStyle(); + + if (!projection) { + projection = null; + } else if (typeof projection === 'string') { + projection = (({name: projection}: any): ProjectionSpecification); + } + + this._useExplicitProjection = !!projection; + const stylesheetProjection = this.style.stylesheet ? + this.style.stylesheet.projection : + null; + return this._prioritizeAndUpdateProjection(projection, stylesheetProjection); + } + + _updateProjectionTransition() { + // The projection isn't globe, we can skip updating the transition + if (this.getProjection().name !== 'globe') { + return; + } + + const tr = this.transform; + const projection = tr.projection.name; + let projectionHasChanged; + + if (projection === 'globe' && tr.zoom >= GLOBE_ZOOM_THRESHOLD_MAX) { + tr.setMercatorFromTransition(); + projectionHasChanged = true; + } else if (projection === 'mercator' && tr.zoom < GLOBE_ZOOM_THRESHOLD_MAX) { + tr.setProjection({name: 'globe'}); + projectionHasChanged = true; + } + + if (projectionHasChanged) { + this.style.applyProjectionUpdate(); + this.style._forceSymbolLayerUpdate(); + } + } + + _prioritizeAndUpdateProjection( + explicitProjection: ?ProjectionSpecification, + styleProjection: ?ProjectionSpecification, + ): this { + // Given a stylesheet and eventual runtime projection, in order of priority, we select: + // 1. the explicit projection + // 2. the stylesheet projection + // 3. mercator (fallback) + const prioritizedProjection = explicitProjection || styleProjection || + {name: "mercator"}; + + return this._updateProjection(prioritizedProjection); + } + + _updateProjection(projection: ProjectionSpecification): this { + let projectionHasChanged; + + if ( + projection.name === 'globe' && + this.transform.zoom >= GLOBE_ZOOM_THRESHOLD_MAX + ) { + projectionHasChanged = this.transform.setMercatorFromTransition(); + } else { + projectionHasChanged = this.transform.setProjection(projection); + } + + this.style.applyProjectionUpdate(); + + if (projectionHasChanged) { + this.painter.clearBackgroundTiles(); + for (const id in this.style._sourceCaches) { + this.style._sourceCaches[id].clearTiles(); + } + this._update(true); + this._forceMarkerAndPopupUpdate(true); + } + + return this; + } + + /** * Returns a {@link Point} representing pixel coordinates, relative to the map's `container`, * that correspond to the specified geographical location. * @@ -1294,11 +1406,11 @@ class Map extends Camera { * const coordinate = [-122.420679, 37.772537]; * const point = map.project(coordinate); */ - project(lnglat: LngLatLike): Point { - return this.transform.locationPoint3D(LngLat.convert(lnglat)); - } + project(lnglat: LngLatLike): Point { + return this.transform.locationPoint3D(LngLat.convert(lnglat)); + } - /** + /** * Returns a {@link LngLat} representing geographical coordinates that correspond * to the specified pixel coordinates. If horizon is visible, and specified pixel is * above horizon, returns a {@link LngLat} corresponding to point on horizon, nearest @@ -1312,106 +1424,133 @@ class Map extends Camera { * const coordinate = map.unproject(e.point); * }); */ - unproject(point: PointLike): LngLat { - return this.transform.pointLocation3D(Point.convert(point)); - } + unproject(point: PointLike): LngLat { + return this.transform.pointLocation3D(Point.convert(point)); + } - /** @section {Movement state} */ + /** @section {Movement state} */ - /** + /** * Returns true if the map is panning, zooming, rotating, or pitching due to a camera animation or user gesture. * * @returns {boolean} True if the map is moving. * @example * const isMoving = map.isMoving(); */ - isMoving(): boolean { - return this._moving || (this.handlers && this.handlers.isMoving()) || false; - } + isMoving(): boolean { + return this._moving || this.handlers && this.handlers.isMoving() || false; + } - /** + /** * Returns true if the map is zooming due to a camera animation or user gesture. * * @returns {boolean} True if the map is zooming. * @example * const isZooming = map.isZooming(); */ - isZooming(): boolean { - return this._zooming || (this.handlers && this.handlers.isZooming()) || false; - } + isZooming(): boolean { + return this._zooming || this.handlers && this.handlers.isZooming() || false; + } - /** + /** * Returns true if the map is rotating due to a camera animation or user gesture. * * @returns {boolean} True if the map is rotating. * @example * map.isRotating(); */ - isRotating(): boolean { - return this._rotating || (this.handlers && this.handlers.isRotating()) || false; - } - - _isDragging(): boolean { - return (this.handlers && this.handlers._isDragging()) || false; - } - - _createDelegatedListener(type: MapEvent, layers: Array, listener: any): any { - if (type === 'mouseenter' || type === 'mouseover') { - let mousein = false; - const mousemove = (e) => { - const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); - const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; - if (!features.length) { - mousein = false; - } else if (!mousein) { - mousein = true; - listener.call(this, new MapMouseEvent(type, this, e.originalEvent, {features})); - } - }; - const mouseout = () => { - mousein = false; - }; - - return {layers: new Set(layers), listener, delegates: {mousemove, mouseout}}; - } else if (type === 'mouseleave' || type === 'mouseout') { - let mousein = false; - const mousemove = (e) => { - const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); - const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; - if (features.length) { - mousein = true; - } else if (mousein) { - mousein = false; - listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); - } - }; - const mouseout = (e) => { - if (mousein) { - mousein = false; - listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); - } - }; - - return {layers: new Set(layers), listener, delegates: {mousemove, mouseout}}; - } else { - const delegate = (e) => { - const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); - const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; - if (features.length) { - // Here we need to mutate the original event, so that preventDefault works as expected. - e.features = features; - listener.call(this, e); - delete e.features; - } - }; - - return {layers: new Set(layers), listener, delegates: {[(type: string)]: delegate}}; - } - } - - /** @section {Working with events} */ - - /** + isRotating(): boolean { + return ( + this._rotating || this.handlers && this.handlers.isRotating() || false + ); + } + + _isDragging(): boolean { + return this.handlers && this.handlers._isDragging() || false; + } + + _createDelegatedListener( + type: MapEvent, + layers: Array, + listener: any, + ): any { + if (type === 'mouseenter' || type === 'mouseover') { + let mousein = false; + const mousemove = (e => { + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? + this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : + []; + if (!features.length) { + mousein = false; + } else if (!mousein) { + mousein = true; + listener.call( + this, + new MapMouseEvent(type, this, e.originalEvent, {features}), + ); + } + }); + const mouseout = (() => { + mousein = false; + }); + + return { + layers: new Set(layers), + listener, + delegates: {mousemove, mouseout}, + }; + } else if (type === 'mouseleave' || type === 'mouseout') { + let mousein = false; + const mousemove = (e => { + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? + this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : + []; + if (features.length) { + mousein = true; + } else if (mousein) { + mousein = false; + listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); + } + }); + const mouseout = (e => { + if (mousein) { + mousein = false; + listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); + } + }); + + return { + layers: new Set(layers), + listener, + delegates: {mousemove, mouseout}, + }; + } else { + const delegate = (e => { + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? + this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : + []; + if (features.length) { + // Here we need to mutate the original event, so that preventDefault works as expected. + e.features = features; + listener.call(this, e); + delete e.features; + } + }); + + return { + layers: new Set(layers), + listener, + delegates: {[ (type: string) ]: delegate}, + }; + } + } + + /** @section {Working with events} */ + + /** * Adds a listener for events of a specified type, * optionally limited to features in a specified style layer. * @@ -1524,28 +1663,32 @@ class Map extends Camera { * @see [Example: Create a hover effect](https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/) * @see [Example: Display popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) */ - on(type: MapEvent, layerIds: any, listener: any): this { - if (listener === undefined) { - return super.on(type, layerIds); - } - - if (!Array.isArray(layerIds)) { - layerIds = [layerIds]; - } - const delegatedListener = this._createDelegatedListener(type, layerIds, listener); - - this._delegatedListeners = this._delegatedListeners || {}; - this._delegatedListeners[type] = this._delegatedListeners[type] || []; - this._delegatedListeners[type].push(delegatedListener); - - for (const event in delegatedListener.delegates) { - this.on((event: any), delegatedListener.delegates[event]); - } - - return this; - } - - /** + on(type: MapEvent, layerIds: any, listener: any): this { + if (listener === undefined) { + return super.on(type, layerIds); + } + + if (!Array.isArray(layerIds)) { + layerIds = [layerIds]; + } + const delegatedListener = this._createDelegatedListener( + type, + layerIds, + listener, + ); + + this._delegatedListeners = this._delegatedListeners || {}; + this._delegatedListeners[type] = this._delegatedListeners[type] || []; + this._delegatedListeners[type].push(delegatedListener); + + for (const event in delegatedListener.delegates) { + this.on((event: any), delegatedListener.delegates[event]); + } + + return this; + } + + /** * Adds a listener that will be called only once to a specified event type, * optionally limited to events occurring on features in a specified style layer. * @@ -1584,25 +1727,28 @@ class Map extends Camera { * @see [Example: Animate the camera around a point with 3D terrain](https://docs.mapbox.com/mapbox-gl-js/example/free-camera-point/) * @see [Example: Play map locations as a slideshow](https://docs.mapbox.com/mapbox-gl-js/example/playback-locations/) */ - once(type: MapEvent, layerIds: any, listener: any): this | Promise { - - if (listener === undefined) { - return super.once(type, layerIds); - } - - if (!Array.isArray(layerIds)) { - layerIds = [layerIds]; - } - const delegatedListener = this._createDelegatedListener(type, layerIds, listener); - - for (const event in delegatedListener.delegates) { - this.once((event: any), delegatedListener.delegates[event]); - } - - return this; - } - - /** + once(type: MapEvent, layerIds: any, listener: any): this | Promise { + if (listener === undefined) { + return super.once(type, layerIds); + } + + if (!Array.isArray(layerIds)) { + layerIds = [layerIds]; + } + const delegatedListener = this._createDelegatedListener( + type, + layerIds, + listener, + ); + + for (const event in delegatedListener.delegates) { + this.once((event: any), delegatedListener.delegates[event]); + } + + return this; + } + + /** * Removes an event listener previously added with {@link Map#on}, * optionally limited to layer-specific events. * @@ -1627,48 +1773,54 @@ class Map extends Camera { * }); * @see [Example: Create a draggable point](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-point/) */ - off(type: MapEvent, layerIds: any, listener: any): this { - if (listener === undefined) { - return super.off(type, layerIds); - } - - layerIds = new Set(Array.isArray(layerIds) ? layerIds : [layerIds]); - const areLayerArraysEqual = (hash1, hash2) => { - if (hash1.size !== hash2.size) { - return false; // at-least 1 arr has duplicate value(s) - } - - // comparing values - for (const value of hash1) { - if (!hash2.has(value)) return false; - } - return true; - }; - - const removeDelegatedListeners = (listeners: Array) => { - for (let i = 0; i < listeners.length; i++) { - const delegatedListener = listeners[i]; - if (delegatedListener.listener === listener && areLayerArraysEqual(delegatedListener.layers, layerIds)) { - for (const event in delegatedListener.delegates) { - this.off((event: any), delegatedListener.delegates[event]); - } - listeners.splice(i, 1); - return this; - } - } - }; - - const delegatedListeners = this._delegatedListeners ? this._delegatedListeners[type] : undefined; - if (delegatedListeners) { - removeDelegatedListeners(delegatedListeners); - } - - return this; - } - - /** @section {Querying features} */ - - /** + off(type: MapEvent, layerIds: any, listener: any): this { + if (listener === undefined) { + return super.off(type, layerIds); + } + + layerIds = new Set(Array.isArray(layerIds) ? layerIds : [layerIds]); + const areLayerArraysEqual = ((hash1, hash2) => { + if (hash1.size !== hash2.size) { + return false; // at-least 1 arr has duplicate value(s) + + } + + // comparing values + for (const value of hash1) { + if (!hash2.has(value)) return false; + } + return true; + }); + + const removeDelegatedListeners = ((listeners: Array) => { + for (let i = 0; i < listeners.length; i++) { + const delegatedListener = listeners[i]; + if ( + delegatedListener.listener === listener && + areLayerArraysEqual(delegatedListener.layers, layerIds) + ) { + for (const event in delegatedListener.delegates) { + this.off((event: any), delegatedListener.delegates[event]); + } + listeners.splice(i, 1); + return this; + } + } + }); + + const delegatedListeners = this._delegatedListeners ? + this._delegatedListeners[type] : + undefined; + if (delegatedListeners) { + removeDelegatedListeners(delegatedListeners); + } + + return this; + } + + /** @section {Querying features} */ + + /** * Returns an array of [GeoJSON](http://geojson.org/) * [Feature objects](https://tools.ietf.org/html/rfc7946#section-3.2) * representing visible features that satisfy the query parameters. @@ -1748,32 +1900,43 @@ class Map extends Camera { * @see [Example: Highlight features within a bounding box](https://www.mapbox.com/mapbox-gl-js/example/using-box-queryrenderedfeatures/) * @see [Example: Filter features within map view](https://www.mapbox.com/mapbox-gl-js/example/filter-features-within-map-view/) */ - queryRenderedFeatures(geometry?: PointLike | [PointLike, PointLike], options?: Object): Array { - // The first parameter can be omitted entirely, making this effectively an overloaded method - // with two signatures: - // - // queryRenderedFeatures(geometry: PointLike | [PointLike, PointLike], options?: Object) - // queryRenderedFeatures(options?: Object) - // - // There no way to express that in a way that's compatible with both flow and documentation.js. - // Related: https://github.com/facebook/flow/issues/1556 - - if (!this.style) { - return []; - } - - if (options === undefined && geometry !== undefined && !(geometry instanceof Point) && !Array.isArray(geometry)) { - options = (geometry: Object); - geometry = undefined; - } - - options = options || {}; - geometry = geometry || [([0, 0]: PointLike), ([this.transform.width, this.transform.height]: PointLike)]; - - return this.style.queryRenderedFeatures(geometry, options, this.transform); - } - - /** + queryRenderedFeatures( + geometry?: PointLike | [PointLike, PointLike], + options?: Object, + ): Array { + // The first parameter can be omitted entirely, making this effectively an overloaded method + // with two signatures: + // + // queryRenderedFeatures(geometry: PointLike | [PointLike, PointLike], options?: Object) + // queryRenderedFeatures(options?: Object) + // + // There no way to express that in a way that's compatible with both flow and documentation.js. + // Related: https://github.com/facebook/flow/issues/1556 + + if (!this.style) { + return []; + } + + if ( + options === undefined && geometry !== undefined && + !(geometry instanceof Point) && + !Array.isArray(geometry) + ) { + options = (geometry: Object); + geometry = undefined; + } + + options = options || {}; + geometry = geometry || + [ + ([0, 0]: PointLike), + ([this.transform.width, this.transform.height]: PointLike), + ]; + + return this.style.queryRenderedFeatures(geometry, options, this.transform); + } + + /** * Returns an array of [GeoJSON](http://geojson.org/) * [Feature objects](https://tools.ietf.org/html/rfc7946#section-3.2) * representing features within the specified vector tile or GeoJSON source that satisfy the query parameters. @@ -1810,13 +1973,20 @@ class Map extends Camera { * * @see [Example: Highlight features containing similar data](https://www.mapbox.com/mapbox-gl-js/example/query-similar-features/) */ - querySourceFeatures(sourceId: string, parameters: ?{sourceLayer: ?string, filter: ?Array, validate?: boolean}): Array { - return this.style.querySourceFeatures(sourceId, parameters); - } - - /** @section {Working with styles} */ - - /** + querySourceFeatures( + sourceId: string, + parameters: ?{ + sourceLayer: ?string, + filter: ?Array, + validate?: boolean, + }, + ): Array { + return this.style.querySourceFeatures(sourceId, parameters); + } + + /** @section {Working with styles} */ + + /** * Updates the map's Mapbox style object with a new value. * * If a style is already set when this is used and the `diff` option is set to `true`, the map renderer will attempt to compare the given style @@ -1842,89 +2012,119 @@ class Map extends Camera { * * @see [Example: Change a map's style](https://www.mapbox.com/mapbox-gl-js/example/setstyle/) */ - setStyle(style: StyleSpecification | string | null, options?: {diff?: boolean} & StyleOptions): this { - options = extend({}, {localIdeographFontFamily: this._localIdeographFontFamily, localFontFamily: this._localFontFamily}, options); - - if ((options.diff !== false && - options.localIdeographFontFamily === this._localIdeographFontFamily && - options.localFontFamily === this._localFontFamily) && this.style && style) { - this._diffStyle(style, options); - return this; - } else { - this._localIdeographFontFamily = options.localIdeographFontFamily; - this._localFontFamily = options.localFontFamily; - return this._updateStyle(style, options); - } - } - - _getUIString(key: string): string { - const str = this._locale[key]; - if (str == null) { - throw new Error(`Missing UI string '${key}'`); - } - - return str; - } - - _updateStyle(style: StyleSpecification | string | null, options?: {diff?: boolean} & StyleOptions): this { - if (this.style) { - this.style.setEventedParent(null); - this.style._remove(); - this.style = (undefined: any); // we lazy-init it so it's never undefined when accessed - } - - if (style) { - this.style = new Style(this, options || {}); - this.style.setEventedParent(this, {style: this.style}); - - if (typeof style === 'string') { - this.style.loadURL(style); - } else { - this.style.loadJSON(style); + setStyle( + style: StyleSpecification | string | null, + options?: { diff?: boolean } & StyleOptions, + ): this { + options = extend( + {}, + { + localIdeographFontFamily: this._localIdeographFontFamily, + localFontFamily: this._localFontFamily, + }, + options, + ); + + if ( + options.diff !== false && + options.localIdeographFontFamily === this._localIdeographFontFamily && + options.localFontFamily === this._localFontFamily && + this.style && + style + ) { + this._diffStyle(style, options); + return this; + } else { + this._localIdeographFontFamily = options.localIdeographFontFamily; + this._localFontFamily = options.localFontFamily; + return this._updateStyle(style, options); + } + } + + _getUIString(key: string): string { + const str = this._locale[key]; + if (str == null) { + throw new Error(`Missing UI string '${key}'`); + } + + return str; + } + + _updateStyle( + style: StyleSpecification | string | null, + options?: { diff?: boolean } & StyleOptions, + ): this { + if (this.style) { + this.style.setEventedParent(null); + this.style._remove(); + this.style = (undefined: any); // we lazy-init it so it's never undefined when accessed + + } + + if (style) { + this.style = new Style(this, options || {}); + this.style.setEventedParent(this, {style: this.style}); + + if (typeof style === 'string') { + this.style.loadURL(style); + } else { + this.style.loadJSON(style); + } + } + this._updateTerrain(); + return this; + } + + _lazyInitEmptyStyle() { + if (!this.style) { + this.style = new Style(this, {}); + this.style.setEventedParent(this, {style: this.style}); + this.style.loadEmpty(); + } + } + + _diffStyle( + style: StyleSpecification | string, + options?: { diff?: boolean } & StyleOptions, + ) { + if (typeof style === 'string') { + const url = this._requestManager.normalizeStyleURL(style); + const request = this._requestManager.transformRequest( + url, + ResourceType.Style, + ); + getJSON( + request, + (error: ?Error, json: ?Object) => { + if (error) { + this.fire(new ErrorEvent(error)); + } else if (json) { + this._updateDiff(json, options); } - } - this._updateTerrain(); - return this; - } - - _lazyInitEmptyStyle() { - if (!this.style) { - this.style = new Style(this, {}); - this.style.setEventedParent(this, {style: this.style}); - this.style.loadEmpty(); - } - } - - _diffStyle(style: StyleSpecification | string, options?: {diff?: boolean} & StyleOptions) { - if (typeof style === 'string') { - const url = this._requestManager.normalizeStyleURL(style); - const request = this._requestManager.transformRequest(url, ResourceType.Style); - getJSON(request, (error: ?Error, json: ?Object) => { - if (error) { - this.fire(new ErrorEvent(error)); - } else if (json) { - this._updateDiff(json, options); - } - }); - } else if (typeof style === 'object') { - this._updateDiff(style, options); - } - } - - _updateDiff(style: StyleSpecification, options?: {diff?: boolean} & StyleOptions) { - try { - if (this.style.setState(style)) { - this._update(true); - } - } catch (e) { - warnOnce( - `Unable to perform style diff: ${e.message || e.error || e}. Rebuilding the style from scratch.` - ); - this._updateStyle(style, options); - } - } - - /** + }, + ); + } else if (typeof style === 'object') { + this._updateDiff(style, options); + } + } + + _updateDiff( + style: StyleSpecification, + options?: { diff?: boolean } & StyleOptions, + ) { + try { + if (this.style.setState(style)) { + this._update(true); + } + } catch (e) { + warnOnce( + `Unable to perform style diff: ${e.message || e.error || e}. Rebuilding the style from scratch.`, + ); + this._updateStyle(style, options); + } + } + + /** * Returns the map's Mapbox [style](https://docs.mapbox.com/help/glossary/style/) object, a JSON object which can be used to recreate the map's style. * * @returns {Object} The map's style JSON object. @@ -1935,13 +2135,13 @@ class Map extends Camera { * }); * */ - getStyle(): ?StyleSpecification { - if (this.style) { - return this.style.serialize(); - } - } + getStyle(): ?StyleSpecification { + if (this.style) { + return this.style.serialize(); + } + } - /** + /** * Returns a Boolean indicating whether the map's style is fully loaded. * * @returns {boolean} A Boolean indicating whether the style is fully loaded. @@ -1949,17 +2149,17 @@ class Map extends Camera { * @example * const styleLoadStatus = map.isStyleLoaded(); */ - isStyleLoaded(): boolean { - if (!this.style) { - warnOnce('There is no style added to the map.'); - return false; - } - return this.style.loaded(); - } + isStyleLoaded(): boolean { + if (!this.style) { + warnOnce('There is no style added to the map.'); + return false; + } + return this.style.loaded(); + } - /** @section {Sources} */ + /** @section {Sources} */ - /** + /** * Adds a source to the map's style. * * @param {string} id The ID of the source to add. Must not conflict with existing sources. @@ -1991,13 +2191,13 @@ class Map extends Camera { * @see Example: GeoJSON source: [Add live realtime data](https://docs.mapbox.com/mapbox-gl-js/example/live-geojson/) * @see Example: Raster DEM source: [Add hillshading](https://docs.mapbox.com/mapbox-gl-js/example/hillshade/) */ - addSource(id: string, source: SourceSpecification): this { - this._lazyInitEmptyStyle(); - this.style.addSource(id, source); - return this._update(true); - } + addSource(id: string, source: SourceSpecification): this { + this._lazyInitEmptyStyle(); + this.style.addSource(id, source); + return this._update(true); + } - /** + /** * Returns a Boolean indicating whether the source is loaded. Returns `true` if the source with * the given ID in the map's style has no outstanding network requests, otherwise `false`. * @@ -2006,11 +2206,11 @@ class Map extends Camera { * @example * const sourceLoaded = map.isSourceLoaded('bathymetry-data'); */ - isSourceLoaded(id: string): boolean { - return !!this.style && this.style._isSourceCacheLoaded(id); - } + isSourceLoaded(id: string): boolean { + return !!this.style && this.style._isSourceCacheLoaded(id); + } - /** + /** * Returns a Boolean indicating whether all tiles in the viewport from all sources on * the style are loaded. * @@ -2019,20 +2219,21 @@ class Map extends Camera { * const tilesLoaded = map.areTilesLoaded(); */ - areTilesLoaded(): boolean { - const sources = this.style && this.style._sourceCaches; - for (const id in sources) { - const source = sources[id]; - const tiles = source._tiles; - for (const t in tiles) { - const tile = tiles[t]; - if (!(tile.state === 'loaded' || tile.state === 'errored')) return false; - } - } - return true; - } - - /** + areTilesLoaded(): boolean { + const sources = this.style && this.style._sourceCaches; + for (const id in sources) { + const source = sources[id]; + const tiles = source._tiles; + for (const t in tiles) { + const tile = tiles[t]; + if (!(tile.state === 'loaded' || tile.state === 'errored')) + return false; + } + } + return true; + } + + /** * Adds a [custom source type](#Custom Sources), making it available for use with * {@link Map#addSource}. * @private @@ -2040,12 +2241,12 @@ class Map extends Camera { * @param {Function} SourceType A {@link Source} constructor. * @param {Function} callback Called when the source type is ready or with an error argument if there is an error. */ - addSourceType(name: string, SourceType: any, callback: Function) { - this._lazyInitEmptyStyle(); - this.style.addSourceType(name, SourceType, callback); - } + addSourceType(name: string, SourceType: any, callback: Function) { + this._lazyInitEmptyStyle(); + this.style.addSourceType(name, SourceType, callback); + } - /** + /** * Removes a source from the map's style. * * @param {string} id The ID of the source to remove. @@ -2053,13 +2254,13 @@ class Map extends Camera { * @example * map.removeSource('bathymetry-data'); */ - removeSource(id: string): this { - this.style.removeSource(id); - this._updateTerrain(); - return this._update(true); - } + removeSource(id: string): this { + this.style.removeSource(id); + this._updateTerrain(); + return this._update(true); + } - /** + /** * Returns the source with the specified ID in the map's style. * * This method is often used to update a source using the instance members for the relevant @@ -2079,14 +2280,14 @@ class Map extends Camera { * @see [Example: Animate a point](https://docs.mapbox.com/mapbox-gl-js/example/animate-point-along-line/) * @see [Example: Add live realtime data](https://docs.mapbox.com/mapbox-gl-js/example/live-geojson/) */ - getSource(id: string): ?Source { - return this.style.getSource(id); - } + getSource(id: string): ?Source { + return this.style.getSource(id); + } - /** @section {Images} */ + /** @section {Images} */ - // eslint-disable-next-line jsdoc/require-returns - /** + // eslint-disable-next-line jsdoc/require-returns + /** * Add an image to the style. This image can be displayed on the map like any other icon in the style's * [sprite](https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/) using the image's ID with * [`icon-image`](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-icon-image), @@ -2130,43 +2331,76 @@ class Map extends Camera { * @see Example: Use `HTMLImageElement`: [Add an icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image/) * @see Example: Use `ImageData`: [Add a generated icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image-generated/) */ - addImage(id: string, - image: HTMLImageElement | ImageBitmap | ImageData | {width: number, height: number, data: Uint8Array | Uint8ClampedArray} | StyleImageInterface, - {pixelRatio = 1, sdf = false, stretchX, stretchY, content}: $Shape = {}) { - this._lazyInitEmptyStyle(); - const version = 0; - - if (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) { - const {width, height, data} = browser.getImageData(image); - this.style.addImage(id, {data: new RGBAImage({width, height}, data), pixelRatio, stretchX, stretchY, content, sdf, version}); - } else if (image.width === undefined || image.height === undefined) { - this.fire(new ErrorEvent(new Error( - 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + - 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); - } else { - const {width, height} = image; - const userImage = ((image: any): StyleImageInterface); - const data = userImage.data; - - this.style.addImage(id, { - data: new RGBAImage({width, height}, new Uint8Array(data)), - pixelRatio, - stretchX, - stretchY, - content, - sdf, - version, - userImage - }); - - if (userImage.onAdd) { - userImage.onAdd(this, id); - } - } - } - - // eslint-disable-next-line jsdoc/require-returns - /** + addImage( + id: string, + image: | HTMLImageElement + | ImageBitmap + | ImageData + | { width: number, height: number, data: Uint8Array | Uint8ClampedArray } + | StyleImageInterface, + { + pixelRatio = 1, + sdf = false, + stretchX, + stretchY, + content + }: $Shape = {}, + ) { + this._lazyInitEmptyStyle(); + const version = 0; + + if ( + image instanceof window.HTMLImageElement || + window.ImageBitmap && image instanceof window.ImageBitmap + ) { + const {width, height, data} = browser.getImageData(image); + this.style.addImage( + id, + { + data: new RGBAImage({width, height}, data), + pixelRatio, + stretchX, + stretchY, + content, + sdf, + version, + }, + ); + } else if (image.width === undefined || image.height === undefined) { + this.fire( + new ErrorEvent( + new Error( + 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`', + ), + ), + ); + } else { + const {width, height} = image; + const userImage = ((image: any): StyleImageInterface); + const data = userImage.data; + + this.style.addImage( + id, + { + data: new RGBAImage({width, height}, new Uint8Array(data)), + pixelRatio, + stretchX, + stretchY, + content, + sdf, + version, + userImage, + }, + ); + + if (userImage.onAdd) { + userImage.onAdd(this, id); + } + } + } + + // eslint-disable-next-line jsdoc/require-returns + /** * Update an existing image in a style. This image can be displayed on the map like any other icon in the style's * [sprite](https://docs.mapbox.com/help/glossary/sprite/) using the image's ID with * [`icon-image`](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-icon-image), @@ -2187,42 +2421,67 @@ class Map extends Camera { * if (map.hasImage('cat')) map.updateImage('cat', image); * }); */ - updateImage(id: string, - image: HTMLImageElement | ImageBitmap | ImageData | {width: number, height: number, data: Uint8Array | Uint8ClampedArray} | StyleImageInterface) { - - const existingImage = this.style.getImage(id); - if (!existingImage) { - this.fire(new ErrorEvent(new Error( - 'The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.'))); - return; - } - const imageData = (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) ? browser.getImageData(image) : image; - const {width, height} = imageData; - // Flow can't refine the type enough to exclude ImageBitmap - const data = ((imageData: any).data: Uint8Array | Uint8ClampedArray); - - if (width === undefined || height === undefined) { - this.fire(new ErrorEvent(new Error( - 'Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + - 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); - return; - } - - if (width !== existingImage.data.width || height !== existingImage.data.height) { - this.fire(new ErrorEvent(new Error( - `The width and height of the updated image (${width}, ${height}) + updateImage( + id: string, + image: | HTMLImageElement + | ImageBitmap + | ImageData + | { width: number, height: number, data: Uint8Array | Uint8ClampedArray } + | StyleImageInterface, + ) { + const existingImage = this.style.getImage(id); + if (!existingImage) { + this.fire( + new ErrorEvent( + new Error( + 'The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.', + ), + ), + ); + return; + } + const imageData = image instanceof window.HTMLImageElement || + window.ImageBitmap && image instanceof window.ImageBitmap ? + browser.getImageData(image) : + image; + const {width, height} = imageData; + // Flow can't refine the type enough to exclude ImageBitmap + const data = ((imageData: any).data: Uint8Array | Uint8ClampedArray); + + if (width === undefined || height === undefined) { + this.fire( + new ErrorEvent( + new Error( + 'Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`', + ), + ), + ); + return; + } + + if ( + width !== existingImage.data.width || height !== existingImage.data.height + ) { + this.fire( + new ErrorEvent( + new Error( + `The width and height of the updated image (${width}, ${height}) must be that same as the previous version of the image - (${existingImage.data.width}, ${existingImage.data.height})`))); - return; - } + (${existingImage.data.width}, ${existingImage.data.height})`, + ), + ), + ); + return; + } - const copy = !(image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)); - existingImage.data.replace(data, copy); + const copy = !(image instanceof window.HTMLImageElement || + window.ImageBitmap && image instanceof window.ImageBitmap); + existingImage.data.replace(data, copy); - this.style.updateImage(id, existingImage); - } + this.style.updateImage(id, existingImage); + } - /** + /** * Check whether or not an image with a specific ID exists in the style. This checks both images * in the style's original [sprite](https://docs.mapbox.com/help/glossary/sprite/) and any images * that have been added at runtime using {@link Map#addImage}. @@ -2235,16 +2494,16 @@ class Map extends Camera { * // the style's sprite. * const catIconExists = map.hasImage('cat'); */ - hasImage(id: string): boolean { - if (!id) { - this.fire(new ErrorEvent(new Error('Missing required image id'))); - return false; - } + hasImage(id: string): boolean { + if (!id) { + this.fire(new ErrorEvent(new Error('Missing required image id'))); + return false; + } - return !!this.style.getImage(id); - } + return !!this.style.getImage(id); + } - /** + /** * Remove an image from a style. This can be an image from the style's original * [sprite](https://docs.mapbox.com/help/glossary/sprite/) or any images * that have been added at runtime using {@link Map#addImage}. @@ -2256,11 +2515,11 @@ class Map extends Camera { * // the style's sprite, remove it. * if (map.hasImage('cat')) map.removeImage('cat'); */ - removeImage(id: string) { - this.style.removeImage(id); - } + removeImage(id: string) { + this.style.removeImage(id); + } - /** + /** * Load an image from an external URL to be used with {@link Map#addImage}. External * domains must support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). * @@ -2277,13 +2536,21 @@ class Map extends Camera { * * @see [Example: Add an icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image/) */ - loadImage(url: string, callback: Function) { - getImage(this._requestManager.transformRequest(url, ResourceType.Image), (err, img) => { - callback(err, img instanceof window.HTMLImageElement ? browser.getImageData(img) : img); - }); - } - - /** + loadImage(url: string, callback: Function) { + getImage( + this._requestManager.transformRequest(url, ResourceType.Image), + (err, img) => { + callback( + err, + img instanceof window.HTMLImageElement ? + browser.getImageData(img) : + img, + ); + }, + ); + } + + /** * Returns an Array of strings containing the IDs of all images currently available in the map. * This includes both images from the style's original [sprite](https://docs.mapbox.com/help/glossary/sprite/) * and any images that have been added at runtime using {@link Map#addImage}. @@ -2294,13 +2561,13 @@ class Map extends Camera { * const allImages = map.listImages(); * */ - listImages(): Array { - return this.style.listImages(); - } + listImages(): Array { + return this.style.listImages(); + } - /** @section {Layers} */ + /** @section {Layers} */ - /** + /** * Adds a [Mapbox style layer](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) * to the map's style. * @@ -2411,13 +2678,16 @@ class Map extends Camera { * @see [Example: Add a vector tile source](https://docs.mapbox.com/mapbox-gl-js/example/vector-source/) (line layer) * @see [Example: Add a WMS layer](https://docs.mapbox.com/mapbox-gl-js/example/wms/) (raster layer) */ - addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string): this { - this._lazyInitEmptyStyle(); - this.style.addLayer(layer, beforeId); - return this._update(true); - } - - /** + addLayer( + layer: LayerSpecification | CustomLayerInterface, + beforeId?: string, + ): this { + this._lazyInitEmptyStyle(); + this.style.addLayer(layer, beforeId); + return this._update(true); + } + + /** * Moves a layer to a different z-position. * * @param {string} id The ID of the layer to move. @@ -2428,12 +2698,12 @@ class Map extends Camera { * // Move a layer with ID 'polygon' before the layer with ID 'country-label'. The `polygon` layer will appear beneath the `country-label` layer on the map. * map.moveLayer('polygon', 'country-label'); */ - moveLayer(id: string, beforeId?: string): this { - this.style.moveLayer(id, beforeId); - return this._update(true); - } + moveLayer(id: string, beforeId?: string): this { + this.style.moveLayer(id, beforeId); + return this._update(true); + } - /** + /** * Removes the layer with the given ID from the map's style. * * If no such layer exists, an `error` event is fired. @@ -2446,12 +2716,12 @@ class Map extends Camera { * // If a layer with ID 'state-data' exists, remove it. * if (map.getLayer('state-data')) map.removeLayer('state-data'); */ - removeLayer(id: string): this { - this.style.removeLayer(id); - return this._update(true); - } + removeLayer(id: string): this { + this.style.removeLayer(id); + return this._update(true); + } - /** + /** * Returns the layer with the specified ID in the map's style. * * @param {string} id The ID of the layer to get. @@ -2464,11 +2734,11 @@ class Map extends Camera { * @see [Example: Filter symbols by toggling a list](https://www.mapbox.com/mapbox-gl-js/example/filter-markers/) * @see [Example: Filter symbols by text input](https://www.mapbox.com/mapbox-gl-js/example/filter-markers-by-input/) */ - getLayer(id: string): ?StyleLayer { - return this.style.getLayer(id); - } + getLayer(id: string): ?StyleLayer { + return this.style.getLayer(id); + } - /** + /** * Sets the zoom extent for the specified style layer. The zoom extent includes the * [minimum zoom level](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layer-minzoom) * and [maximum zoom level](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layer-maxzoom)) @@ -2488,12 +2758,12 @@ class Map extends Camera { * map.setLayerZoomRange('my-layer', 2, 5); * */ - setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number): this { - this.style.setLayerZoomRange(layerId, minzoom, maxzoom); - return this._update(true); - } + setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number): this { + this.style.setLayerZoomRange(layerId, minzoom, maxzoom); + return this._update(true); + } - /** + /** * Sets the filter for the specified style layer. * * Filters control which features a style layer renders from its source. @@ -2526,12 +2796,16 @@ class Map extends Camera { * @see [Example: Create a timeline animation](https://www.mapbox.com/mapbox-gl-js/example/timeline-animation/) * @see [Tutorial: Show changes over time](https://docs.mapbox.com/help/tutorials/show-changes-over-time/) */ - setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}): this { - this.style.setFilter(layerId, filter, options); - return this._update(true); - } - - /** + setFilter( + layerId: string, + filter: ?FilterSpecification, + options: StyleSetterOptions = {}, + ): this { + this.style.setFilter(layerId, filter, options); + return this._update(true); + } + + /** * Returns the filter applied to the specified style layer. * * @param {string} layerId The ID of the style layer whose filter to get. @@ -2539,11 +2813,11 @@ class Map extends Camera { * @example * const filter = map.getFilter('myLayer'); */ - getFilter(layerId: string): ?FilterSpecification { - return this.style.getFilter(layerId); - } + getFilter(layerId: string): ?FilterSpecification { + return this.style.getFilter(layerId); + } - /** + /** * Sets the value of a paint property in the specified style layer. * * @param {string} layerId The ID of the layer to set the paint property in. @@ -2559,12 +2833,17 @@ class Map extends Camera { * @see [Example: Adjust a layer's opacity](https://www.mapbox.com/mapbox-gl-js/example/adjust-layer-opacity/) * @see [Example: Create a draggable point](https://www.mapbox.com/mapbox-gl-js/example/drag-a-point/) */ - setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}): this { - this.style.setPaintProperty(layerId, name, value, options); - return this._update(true); - } - - /** + setPaintProperty( + layerId: string, + name: string, + value: any, + options: StyleSetterOptions = {}, + ): this { + this.style.setPaintProperty(layerId, name, value, options); + return this._update(true); + } + + /** * Returns the value of a paint property in the specified style layer. * * @param {string} layerId The ID of the layer to get the paint property from. @@ -2573,11 +2852,14 @@ class Map extends Camera { * @example * const paintProperty = map.getPaintProperty('mySymbolLayer', 'icon-color'); */ - getPaintProperty(layerId: string, name: string): void | TransitionSpecification | PropertyValueSpecification { - return this.style.getPaintProperty(layerId, name); - } - - /** + getPaintProperty( + layerId: string, + name: string, + ): void | TransitionSpecification | PropertyValueSpecification { + return this.style.getPaintProperty(layerId, name); + } + + /** * Sets the value of a layout property in the specified style layer. * * @param {string} layerId The ID of the layer to set the layout property in. @@ -2590,12 +2872,17 @@ class Map extends Camera { * map.setLayoutProperty('my-layer', 'visibility', 'none'); * @see [Example: Show and hide layers](https://docs.mapbox.com/mapbox-gl-js/example/toggle-layers/) */ - setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}): this { - this.style.setLayoutProperty(layerId, name, value, options); - return this._update(true); - } - - /** + setLayoutProperty( + layerId: string, + name: string, + value: any, + options: StyleSetterOptions = {}, + ): this { + this.style.setLayoutProperty(layerId, name, value, options); + return this._update(true); + } + + /** * Returns the value of a layout property in the specified style layer. * * @param {string} layerId The ID of the layer to get the layout property from. @@ -2604,13 +2891,16 @@ class Map extends Camera { * @example * const layoutProperty = map.getLayoutProperty('mySymbolLayer', 'icon-anchor'); */ - getLayoutProperty(layerId: string, name: string): ?PropertyValueSpecification { - return this.style.getLayoutProperty(layerId, name); - } + getLayoutProperty( + layerId: string, + name: string, + ): ?PropertyValueSpecification { + return this.style.getLayoutProperty(layerId, name); + } - /** @section {Style properties} */ + /** @section {Style properties} */ - /** + /** * Sets the any combination of light values. * * @param {LightSpecification} light Light properties to set. Must conform to the [Light Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#light). @@ -2624,25 +2914,25 @@ class Map extends Camera { * "intensity": 0.5 * }); */ - setLight(light: LightSpecification, options: StyleSetterOptions = {}): this { - this._lazyInitEmptyStyle(); - this.style.setLight(light, options); - return this._update(true); - } + setLight(light: LightSpecification, options: StyleSetterOptions = {}): this { + this._lazyInitEmptyStyle(); + this.style.setLight(light, options); + return this._update(true); + } - /** + /** * Returns the value of the light object. * * @returns {LightSpecification} Light properties of the style. * @example * const light = map.getLight(); */ - getLight(): LightSpecification { - return this.style.getLight(); - } + getLight(): LightSpecification { + return this.style.getLight(); + } - // eslint-disable-next-line jsdoc/require-returns - /** + // eslint-disable-next-line jsdoc/require-returns + /** * Sets the terrain property of the style. * * @param {TerrainSpecification} terrain Terrain properties to set. Must conform to the [Terrain Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/terrain/). @@ -2658,29 +2948,29 @@ class Map extends Camera { * // add the DEM source as a terrain layer with exaggerated height * map.setTerrain({'source': 'mapbox-dem', 'exaggeration': 1.5}); */ - setTerrain(terrain: TerrainSpecification): this { - this._lazyInitEmptyStyle(); - if (!terrain && this.transform.projection.requiresDraping) { - this.style.setTerrainForDraping(); - } else { - this.style.setTerrain(terrain); - } - this._averageElevationLastSampledAt = -Infinity; - return this._update(true); - } - - /** + setTerrain(terrain: TerrainSpecification): this { + this._lazyInitEmptyStyle(); + if (!terrain && this.transform.projection.requiresDraping) { + this.style.setTerrainForDraping(); + } else { + this.style.setTerrain(terrain); + } + this._averageElevationLastSampledAt = -Infinity; + return this._update(true); + } + + /** * Returns the terrain specification or `null` if terrain isn't set on the map. * * @returns {TerrainSpecification | null} Terrain specification properties of the style. * @example * const terrain = map.getTerrain(); */ - getTerrain(): ?TerrainSpecification { - return this.style ? this.style.getTerrain() : null; - } + getTerrain(): ?TerrainSpecification { + return this.style ? this.style.getTerrain() : null; + } - /** + /** * Sets the fog property of the style. * * @param {FogSpecification} fog The fog properties to set. Must conform to the [Fog Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/fog/). @@ -2697,24 +2987,24 @@ class Map extends Camera { * }); * @see [Example: Add fog to a map](https://docs.mapbox.com/mapbox-gl-js/example/add-fog/) */ - setFog(fog: FogSpecification): this { - this._lazyInitEmptyStyle(); - this.style.setFog(fog); - return this._update(true); - } + setFog(fog: FogSpecification): this { + this._lazyInitEmptyStyle(); + this.style.setFog(fog); + return this._update(true); + } - /** + /** * Returns the fog specification or `null` if fog is not set on the map. * * @returns {FogSpecification} Fog specification properties of the style. * @example * const fog = map.getFog(); */ - getFog(): ?FogSpecification { - return this.style ? this.style.getFog() : null; - } + getFog(): ?FogSpecification { + return this.style ? this.style.getFog() : null; + } - /** + /** * Returns the fog opacity for a given location. * * An opacity of 0 means that there is no fog contribution for the given location @@ -2726,14 +3016,17 @@ class Map extends Camera { * @returns {number} A value between 0 and 1 representing the fog opacity, where 1 means fully within, and 0 means not affected by the fog effect. * @private */ - _queryFogOpacity(lnglat: LngLatLike): number { - if (!this.style || !this.style.fog) return 0.0; - return this.style.fog.getOpacityAtLatLng(LngLat.convert(lnglat), this.transform); - } + _queryFogOpacity(lnglat: LngLatLike): number { + if (!this.style || !this.style.fog) return 0.0; + return this.style.fog.getOpacityAtLatLng( + LngLat.convert(lnglat), + this.transform, + ); + } - /** @section {Feature state} */ + /** @section {Feature state} */ - /** + /** * Sets the `state` of a feature. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. * When using this method, the `state` object is merged with any existing key-value pairs in the feature's state. @@ -2771,13 +3064,16 @@ class Map extends Camera { * @see [Example: Create a hover effect](https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/) * @see [Tutorial: Create interactive hover effects with Mapbox GL JS](https://docs.mapbox.com/help/tutorials/create-interactive-hover-effects-with-mapbox-gl-js/) */ - setFeatureState(feature: { source: string; sourceLayer?: string; id: string | number; }, state: Object): this { - this.style.setFeatureState(feature, state); - return this._update(); - } - - // eslint-disable-next-line jsdoc/require-returns - /** + setFeatureState( + feature: { source: string, sourceLayer?: string, id: string | number }, + state: Object, + ): this { + this.style.setFeatureState(feature, state); + return this._update(); + } + + // eslint-disable-next-line jsdoc/require-returns + /** * Removes the `state` of a feature, setting it back to the default behavior. * If only a `feature.source` is specified, it will remove the state for all features from that source. * If `feature.id` is also specified, it will remove all keys for that feature's state. @@ -2823,12 +3119,15 @@ class Map extends Camera { * }); * */ - removeFeatureState(feature: { source: string; sourceLayer?: string; id?: string | number; }, key?: string): this { - this.style.removeFeatureState(feature, key); - return this._update(); - } - - /** + removeFeatureState( + feature: { source: string, sourceLayer?: string, id?: string | number }, + key?: string, + ): this { + this.style.removeFeatureState(feature, key); + return this._update(); + } + + /** * Gets the `state` of a feature. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. * Features are identified by their `id` attribute, which can be any number or string. @@ -2857,170 +3156,216 @@ class Map extends Camera { * }); * */ - getFeatureState(feature: { source: string; sourceLayer?: string; id: string | number; }): any { - return this.style.getFeatureState(feature); - } - - _updateContainerDimensions() { - if (!this._container) return; - - const width = this._container.getBoundingClientRect().width || 400; - const height = this._container.getBoundingClientRect().height || 300; - - let transformValues; - let transformScaleWidth; - let transformScaleHeight; - let el = this._container; - while (el && (!transformScaleWidth || !transformScaleHeight)) { - const transformMatrix = window.getComputedStyle(el).transform; - if (transformMatrix && transformMatrix !== 'none') { - transformValues = transformMatrix.match(/matrix.*\((.+)\)/)[1].split(', '); - if (transformValues[0] && transformValues[0] !== '0' && transformValues[0] !== '1') transformScaleWidth = transformValues[0]; - if (transformValues[3] && transformValues[3] !== '0' && transformValues[3] !== '1') transformScaleHeight = transformValues[3]; - } - el = el.parentElement; - } - - this._containerWidth = transformScaleWidth ? Math.abs(width / transformScaleWidth) : width; - this._containerHeight = transformScaleHeight ? Math.abs(height / transformScaleHeight) : height; - } - - _detectMissingCSS(): void { - const computedColor = window.getComputedStyle(this._missingCSSCanary).getPropertyValue('background-color'); - if (computedColor !== 'rgb(250, 128, 114)') { - warnOnce('This page appears to be missing CSS declarations for ' + - 'Mapbox GL JS, which may cause the map to display incorrectly. ' + - 'Please ensure your page includes mapbox-gl.css, as described ' + - 'in https://www.mapbox.com/mapbox-gl-js/api/.'); - } - } - - _setupContainer() { - const container = this._container; - container.classList.add('mapboxgl-map'); - - const missingCSSCanary = this._missingCSSCanary = DOM.create('div', 'mapboxgl-canary', container); - missingCSSCanary.style.visibility = 'hidden'; - this._detectMissingCSS(); - - const canvasContainer = this._canvasContainer = DOM.create('div', 'mapboxgl-canvas-container', container); - if (this._interactive) { - canvasContainer.classList.add('mapboxgl-interactive'); - } - - this._canvas = DOM.create('canvas', 'mapboxgl-canvas', canvasContainer); - this._canvas.addEventListener('webglcontextlost', this._contextLost, false); - this._canvas.addEventListener('webglcontextrestored', this._contextRestored, false); - this._canvas.setAttribute('tabindex', '0'); - this._canvas.setAttribute('aria-label', this._getUIString('Map.Title')); - this._canvas.setAttribute('role', 'region'); - - this._updateContainerDimensions(); - this._resizeCanvas(this._containerWidth, this._containerHeight); - - const controlContainer = this._controlContainer = DOM.create('div', 'mapboxgl-control-container', container); - const positions = this._controlPositions = {}; - ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach((positionName) => { - positions[positionName] = DOM.create('div', `mapboxgl-ctrl-${positionName}`, controlContainer); - }); - - this._container.addEventListener('scroll', this._onMapScroll, false); - } - - _resizeCanvas(width: number, height: number) { - const pixelRatio = browser.devicePixelRatio || 1; - - // Request the required canvas size (rounded up) taking the pixelratio into account. - this._canvas.width = pixelRatio * Math.ceil(width); - this._canvas.height = pixelRatio * Math.ceil(height); - - // Maintain the same canvas size, potentially downscaling it for HiDPI displays - this._canvas.style.width = `${width}px`; - this._canvas.style.height = `${height}px`; - } - - _addMarker(marker: Marker) { - this._markers.push(marker); - } - - _removeMarker(marker: Marker) { - const index = this._markers.indexOf(marker); - if (index !== -1) { - this._markers.splice(index, 1); - } - } - - _addPopup(popup: Popup) { - this._popups.push(popup); - } - - _removePopup(popup: Popup) { - const index = this._popups.indexOf(popup); - if (index !== -1) { - this._popups.splice(index, 1); - } - } - - _setupPainter() { - const attributes = extend({}, supported.webGLContextAttributes, { - failIfMajorPerformanceCaveat: this._failIfMajorPerformanceCaveat, - preserveDrawingBuffer: this._preserveDrawingBuffer, - antialias: this._antialias || false - }); - - const gl2 = this._useWebGL2 && ((this._canvas.getContext("webgl2", attributes): any): WebGLRenderingContext); - const gl = gl2 || - this._canvas.getContext('webgl', attributes) || - this._canvas.getContext('experimental-webgl', attributes); - - if (!gl) { - this.fire(new ErrorEvent(new Error('Failed to initialize WebGL'))); - return; - } - - if (this._useWebGL2 && !gl2) { - warnOnce('Failed to create WebGL 2 context. Using WebGL 1.'); - } - storeAuthState(gl, true); - - this.painter = new Painter(gl, this.transform, !!gl2); - this.on('data', (event: MapDataEvent) => { - if (event.dataType === 'source') { - this.painter.setTileLoadedFlag(true); - } - }); - - webpSupported.testSupport(gl); - } - - _contextLost(event: *) { - event.preventDefault(); - if (this._frame) { - this._frame.cancel(); - this._frame = null; - } - this.fire(new Event('webglcontextlost', {originalEvent: event})); - } - - _contextRestored(event: *) { - this._setupPainter(); - this.resize(); - this._update(); - this.fire(new Event('webglcontextrestored', {originalEvent: event})); - } - - _onMapScroll(event: *): ?boolean { - if (event.target !== this._container) return; - - // Revert any scroll which would move the canvas outside of the view - this._container.scrollTop = 0; - this._container.scrollLeft = 0; - return false; - } - - /** @section {Lifecycle} */ - - /** + getFeatureState( + feature: { source: string, sourceLayer?: string, id: string | number }, + ): any { + return this.style.getFeatureState(feature); + } + + _updateContainerDimensions() { + if (!this._container) return; + + const width = this._container.getBoundingClientRect().width || 400; + const height = this._container.getBoundingClientRect().height || 300; + + let transformValues; + let transformScaleWidth; + let transformScaleHeight; + let el = this._container; + while (el && (!transformScaleWidth || !transformScaleHeight)) { + const transformMatrix = window.getComputedStyle(el).transform; + if (transformMatrix && transformMatrix !== 'none') { + transformValues = transformMatrix.match(/matrix.*\((.+)\)/)[1].split( + ', ', + ); + if ( + transformValues[0] && transformValues[0] !== '0' && + transformValues[0] !== '1' + ) + transformScaleWidth = transformValues[0]; + if ( + transformValues[3] && transformValues[3] !== '0' && + transformValues[3] !== '1' + ) + transformScaleHeight = transformValues[3]; + } + el = el.parentElement; + } + + this._containerWidth = transformScaleWidth ? + Math.abs(width / transformScaleWidth) : + width; + this._containerHeight = transformScaleHeight ? + Math.abs(height / transformScaleHeight) : + height; + } + + _detectMissingCSS(): void { + const computedColor = window.getComputedStyle(this._missingCSSCanary).getPropertyValue( + 'background-color', + ); + if (computedColor !== 'rgb(250, 128, 114)') { + warnOnce( + 'This page appears to be missing CSS declarations for ' + 'Mapbox GL JS, which may cause the map to display incorrectly. ' + 'Please ensure your page includes mapbox-gl.css, as described ' + 'in https://www.mapbox.com/mapbox-gl-js/api/.', + ); + } + } + + _setupContainer() { + const container = this._container; + container.classList.add('mapboxgl-map'); + + const missingCSSCanary = this._missingCSSCanary = DOM.create( + 'div', + 'mapboxgl-canary', + container, + ); + missingCSSCanary.style.visibility = 'hidden'; + this._detectMissingCSS(); + + const canvasContainer = this._canvasContainer = DOM.create( + 'div', + 'mapboxgl-canvas-container', + container, + ); + if (this._interactive) { + canvasContainer.classList.add('mapboxgl-interactive'); + } + + this._canvas = DOM.create('canvas', 'mapboxgl-canvas', canvasContainer); + this._canvas.addEventListener('webglcontextlost', this._contextLost, false); + this._canvas.addEventListener( + 'webglcontextrestored', + this._contextRestored, + false, + ); + this._canvas.setAttribute('tabindex', '0'); + this._canvas.setAttribute('aria-label', this._getUIString('Map.Title')); + this._canvas.setAttribute('role', 'region'); + + this._updateContainerDimensions(); + this._resizeCanvas(this._containerWidth, this._containerHeight); + + const controlContainer = this._controlContainer = DOM.create( + 'div', + 'mapboxgl-control-container', + container, + ); + const positions = this._controlPositions = {}; + ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach( + positionName => { + positions[positionName] = DOM.create( + 'div', + `mapboxgl-ctrl-${positionName}`, + controlContainer, + ); + }, + ); + + this._container.addEventListener('scroll', this._onMapScroll, false); + } + + _resizeCanvas(width: number, height: number) { + const pixelRatio = browser.devicePixelRatio || 1; + + // Request the required canvas size (rounded up) taking the pixelratio into account. + this._canvas.width = pixelRatio * Math.ceil(width); + this._canvas.height = pixelRatio * Math.ceil(height); + + // Maintain the same canvas size, potentially downscaling it for HiDPI displays + this._canvas.style.width = `${width}px`; + this._canvas.style.height = `${height}px`; + } + + _addMarker(marker: Marker) { + this._markers.push(marker); + } + + _removeMarker(marker: Marker) { + const index = this._markers.indexOf(marker); + if (index !== -1) { + this._markers.splice(index, 1); + } + } + + _addPopup(popup: Popup) { + this._popups.push(popup); + } + + _removePopup(popup: Popup) { + const index = this._popups.indexOf(popup); + if (index !== -1) { + this._popups.splice(index, 1); + } + } + + _setupPainter() { + const attributes = extend( + {}, + supported.webGLContextAttributes, + { + failIfMajorPerformanceCaveat: this._failIfMajorPerformanceCaveat, + preserveDrawingBuffer: this._preserveDrawingBuffer, + antialias: this._antialias || false, + }, + ); + + const gl2 = this._useWebGL2 && + ((this._canvas.getContext("webgl2", attributes): any): WebGLRenderingContext); + const gl = gl2 || this._canvas.getContext('webgl', attributes) || + this._canvas.getContext('experimental-webgl', attributes); + + if (!gl) { + this.fire(new ErrorEvent(new Error('Failed to initialize WebGL'))); + return; + } + + if (this._useWebGL2 && !gl2) { + warnOnce('Failed to create WebGL 2 context. Using WebGL 1.'); + } + storeAuthState(gl, true); + + this.painter = new Painter(gl, this.transform, !!gl2); + this.on( + 'data', + (event: MapDataEvent) => { + if (event.dataType === 'source') { + this.painter.setTileLoadedFlag(true); + } + }, + ); + + webpSupported.testSupport(gl); + } + + _contextLost = (event: *) => { + event.preventDefault(); + if (this._frame) { + this._frame.cancel(); + this._frame = null; + } + this.fire(new Event('webglcontextlost', {originalEvent: event})); + }; + + _contextRestored = (event: *) => { + this._setupPainter(); + this.resize(); + this._update(); + this.fire(new Event('webglcontextrestored', {originalEvent: event})); + }; + + _onMapScroll = (event: *): ?boolean => { + if (event.target !== this._container) return; + + // Revert any scroll which would move the canvas outside of the view + this._container.scrollTop = 0; + this._container.scrollLeft = 0; + return false; + }; + + /** @section {Lifecycle} */ + + /** * Returns a Boolean indicating whether the map is fully loaded. * * Returns `false` if the style is not yet fully loaded, @@ -3031,11 +3376,14 @@ class Map extends Camera { * @example * const isLoaded = map.loaded(); */ - loaded(): boolean { - return !this._styleDirty && !this._sourcesDirty && !!this.style && this.style.loaded(); - } - - /** + loaded(): boolean { + return ( + !this._styleDirty && !this._sourcesDirty && !!this.style && + this.style.loaded() + ); + } + + /** * Update this map's style and sources, and re-render the map. * * @param {boolean} updateStyle mark the map's style for reprocessing as @@ -3043,47 +3391,47 @@ class Map extends Camera { * @returns {Map} this * @private */ - _update(updateStyle?: boolean): this { - if (!this.style) return this; + _update(updateStyle?: boolean): this { + if (!this.style) return this; - this._styleDirty = this._styleDirty || updateStyle; - this._sourcesDirty = true; - this.triggerRepaint(); + this._styleDirty = this._styleDirty || updateStyle; + this._sourcesDirty = true; + this.triggerRepaint(); - return this; - } + return this; + } - /** + /** * Request that the given callback be executed during the next render * frame. Schedule a render frame if one is not already scheduled. * @returns An id that can be used to cancel the callback * @private */ - _requestRenderFrame(callback: () => void): TaskID { - this._update(); - return this._renderTaskQueue.add(callback); - } + _requestRenderFrame(callback: () => void): TaskID { + this._update(); + return this._renderTaskQueue.add(callback); + } - _cancelRenderFrame(id: TaskID) { - this._renderTaskQueue.remove(id); - } + _cancelRenderFrame(id: TaskID) { + this._renderTaskQueue.remove(id); + } - /** + /** * Request that the given callback be executed during the next render frame if the map is not * idle. Otherwise it is executed immediately, to avoid triggering a new render. * @private */ - _requestDomTask(callback: () => void) { - // This condition means that the map is idle: the callback needs to be called right now as - // there won't be a triggered render to run the queue. - if (!this.loaded() || (this.loaded() && !this.isMoving())) { - callback(); - } else { - this._domRenderTaskQueue.add(callback); - } - } - - /** + _requestDomTask(callback: () => void) { + // This condition means that the map is idle: the callback needs to be called right now as + // there won't be a triggered render to run the queue. + if (!this.loaded() || this.loaded() && !this.isMoving()) { + callback(); + } else { + this._domRenderTaskQueue.add(callback); + } + } + + /** * Call when a (re-)render of the map is required: * - The style has changed (`setPaintProperty()`, etc.) * - Source data has changed (for example, tiles have finished loading) @@ -3095,295 +3443,364 @@ class Map extends Camera { * @returns {Map} this * @private */ - _render(paintStartTimeStamp: number) { - const m = PerformanceUtils.beginMeasure('render'); - - let gpuTimer; - const extTimerQuery = this.painter.context.extTimerQuery; - const frameStartTime = browser.now(); - if (this.listens('gpu-timing-frame')) { - gpuTimer = extTimerQuery.createQueryEXT(); - extTimerQuery.beginQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); - } - - // A custom layer may have used the context asynchronously. Mark the state as dirty. - this.painter.context.setDirty(); - this.painter.setBaseState(); - - if (this.isMoving() || this.isRotating() || this.isZooming()) { - this._interactionRange[0] = Math.min(this._interactionRange[0], window.performance.now()); - this._interactionRange[1] = Math.max(this._interactionRange[1], window.performance.now()); - } - - this._renderTaskQueue.run(paintStartTimeStamp); - this._domRenderTaskQueue.run(paintStartTimeStamp); - // A task queue callback may have fired a user event which may have removed the map - if (this._removed) return; - - this._updateProjectionTransition(); - - const fadeDuration = this._isInitialLoad ? 0 : this._fadeDuration; - - // If the style has changed, the map is being zoomed, or a transition or fade is in progress: - // - Apply style changes (in a batch) - // - Recalculate paint properties. - if (this.style && this._styleDirty) { - this._styleDirty = false; - - const zoom = this.transform.zoom; - const pitch = this.transform.pitch; - const now = browser.now(); - - const parameters = new EvaluationParameters(zoom, { - now, - fadeDuration, - pitch, - transition: this.style.getTransition() - }); - - this.style.update(parameters); - } - - const fogIsTransitioning = this.style && this.style.fog && this.style.fog.hasTransition(); - - if (fogIsTransitioning) { - this.style._markersNeedUpdate = true; - this._sourcesDirty = true; - } - - // If we are in _render for any reason other than an in-progress paint - // transition, update source caches to check for and load any tiles we - // need for the current transform - let averageElevationChanged = false; - if (this.style && this._sourcesDirty) { - this._sourcesDirty = false; - this.painter._updateFog(this.style); - this._updateTerrain(); // Terrain DEM source updates here and skips update in style._updateSources. - averageElevationChanged = this._updateAverageElevation(frameStartTime); - this.style._updateSources(this.transform); - // Update positions of markers and popups on enabling/disabling terrain - this._forceMarkerAndPopupUpdate(); - } else { - averageElevationChanged = this._updateAverageElevation(frameStartTime); - } - - this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions); - - // Actually draw - if (this.style) { - this.painter.render(this.style, { - showTileBoundaries: this.showTileBoundaries, - showTerrainWireframe: this.showTerrainWireframe, - showOverdrawInspector: this._showOverdrawInspector, - showQueryGeometry: !!this._showQueryGeometry, - showTileAABBs: this.showTileAABBs, - rotating: this.isRotating(), - zooming: this.isZooming(), - moving: this.isMoving(), - fadeDuration, - isInitialLoad: this._isInitialLoad, - showPadding: this.showPadding, - gpuTiming: !!this.listens('gpu-timing-layer'), - gpuTimingDeferredRender: !!this.listens('gpu-timing-deferred-render'), - speedIndexTiming: this.speedIndexTiming, - }); - } - - this.fire(new Event('render')); - - if (this.loaded() && !this._loaded) { - this._loaded = true; - PerformanceUtils.mark(PerformanceMarkers.load); - this.fire(new Event('load')); - } - - if (this.style && (this.style.hasTransitions())) { - this._styleDirty = true; - } - - if (this.style && !this._placementDirty) { - // Since no fade operations are in progress, we can release - // all tiles held for fading. If we didn't do this, the tiles - // would just sit in the SourceCaches until the next render - this.style._releaseSymbolFadeTiles(); - } - - if (gpuTimer) { - const renderCPUTime = browser.now() - frameStartTime; - extTimerQuery.endQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); - setTimeout(() => { - const renderGPUTime = extTimerQuery.getQueryObjectEXT(gpuTimer, extTimerQuery.QUERY_RESULT_EXT) / (1000 * 1000); - extTimerQuery.deleteQueryEXT(gpuTimer); - this.fire(new Event('gpu-timing-frame', { - cpuTime: renderCPUTime, - gpuTime: renderGPUTime - })); - window.performance.mark('frame-gpu', { - startTime: frameStartTime, - detail: { - gpuTime: renderGPUTime - } - }); - }, 50); // Wait 50ms to give time for all GPU calls to finish before querying - } - - PerformanceUtils.endMeasure(m); - - if (this.listens('gpu-timing-layer')) { - // Resetting the Painter's per-layer timing queries here allows us to isolate - // the queries to individual frames. - const frameLayerQueries = this.painter.collectGpuTimers(); - - setTimeout(() => { - const renderedLayerTimes = this.painter.queryGpuTimers(frameLayerQueries); - - this.fire(new Event('gpu-timing-layer', { - layerTimes: renderedLayerTimes - })); - }, 50); // Wait 50ms to give time for all GPU calls to finish before querying - } - - if (this.listens('gpu-timing-deferred-render')) { - const deferredRenderQueries = this.painter.collectDeferredRenderGpuQueries(); - - setTimeout(() => { - const gpuTime = this.painter.queryGpuTimeDeferredRender(deferredRenderQueries); - this.fire(new Event('gpu-timing-deferred-render', {gpuTime})); - }, 50); // Wait 50ms to give time for all GPU calls to finish before querying - } - - // Schedule another render frame if it's needed. - // - // Even though `_styleDirty` and `_sourcesDirty` are reset in this - // method, synchronous events fired during Style#update or - // Style#_updateSources could have caused them to be set again. - const somethingDirty = this._sourcesDirty || this._styleDirty || this._placementDirty || averageElevationChanged; - if (somethingDirty || this._repaint) { - this.triggerRepaint(); - } else { - const willIdle = !this.isMoving() && this.loaded(); - if (willIdle) { - // Before idling, we perform one last sample so that if the average elevation - // does not exactly match the terrain, we skip idle and ease it to its final state. - averageElevationChanged = this._updateAverageElevation(frameStartTime, true); - } + _render(paintStartTimeStamp: number) { + const m = PerformanceUtils.beginMeasure('render'); + + let gpuTimer; + const extTimerQuery = this.painter.context.extTimerQuery; + const frameStartTime = browser.now(); + if (this.listens('gpu-timing-frame')) { + gpuTimer = extTimerQuery.createQueryEXT(); + extTimerQuery.beginQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); + } + + // A custom layer may have used the context asynchronously. Mark the state as dirty. + this.painter.context.setDirty(); + this.painter.setBaseState(); + + if (this.isMoving() || this.isRotating() || this.isZooming()) { + this._interactionRange[0] = Math.min( + this._interactionRange[0], + window.performance.now(), + ); + this._interactionRange[1] = Math.max( + this._interactionRange[1], + window.performance.now(), + ); + } + + this._renderTaskQueue.run(paintStartTimeStamp); + this._domRenderTaskQueue.run(paintStartTimeStamp); + // A task queue callback may have fired a user event which may have removed the map + if (this._removed) return; + + this._updateProjectionTransition(); + + const fadeDuration = this._isInitialLoad ? 0 : this._fadeDuration; + + // If the style has changed, the map is being zoomed, or a transition or fade is in progress: + // - Apply style changes (in a batch) + // - Recalculate paint properties. + if (this.style && this._styleDirty) { + this._styleDirty = false; + + const zoom = this.transform.zoom; + const pitch = this.transform.pitch; + const now = browser.now(); + + const parameters = new EvaluationParameters( + zoom, + { + now, + fadeDuration, + pitch, + transition: this.style.getTransition(), + }, + ); + + this.style.update(parameters); + } + + const fogIsTransitioning = this.style && this.style.fog && + this.style.fog.hasTransition(); + + if (fogIsTransitioning) { + this.style._markersNeedUpdate = true; + this._sourcesDirty = true; + } + + // If we are in _render for any reason other than an in-progress paint + // transition, update source caches to check for and load any tiles we + // need for the current transform + let averageElevationChanged = false; + if (this.style && this._sourcesDirty) { + this._sourcesDirty = false; + this.painter._updateFog(this.style); + this._updateTerrain(); // Terrain DEM source updates here and skips update in style._updateSources. + averageElevationChanged = this._updateAverageElevation(frameStartTime); + this.style._updateSources(this.transform); + // Update positions of markers and popups on enabling/disabling terrain + this._forceMarkerAndPopupUpdate(); + } else { + averageElevationChanged = this._updateAverageElevation(frameStartTime); + } + + this._placementDirty = this.style && + this.style._updatePlacement( + this.painter.transform, + this.showCollisionBoxes, + fadeDuration, + this._crossSourceCollisions, + ); + + // Actually draw + if (this.style) { + this.painter.render( + this.style, + { + showTileBoundaries: this.showTileBoundaries, + showTerrainWireframe: this.showTerrainWireframe, + showOverdrawInspector: this._showOverdrawInspector, + showQueryGeometry: !!this._showQueryGeometry, + showTileAABBs: this.showTileAABBs, + rotating: this.isRotating(), + zooming: this.isZooming(), + moving: this.isMoving(), + fadeDuration, + isInitialLoad: this._isInitialLoad, + showPadding: this.showPadding, + gpuTiming: !!this.listens('gpu-timing-layer'), + gpuTimingDeferredRender: !!this.listens('gpu-timing-deferred-render'), + speedIndexTiming: this.speedIndexTiming, + }, + ); + } + + this.fire(new Event('render')); + + if (this.loaded() && !this._loaded) { + this._loaded = true; + PerformanceUtils.mark(PerformanceMarkers.load); + this.fire(new Event('load')); + } + + if (this.style && this.style.hasTransitions()) { + this._styleDirty = true; + } + + if (this.style && !this._placementDirty) { + // Since no fade operations are in progress, we can release + // all tiles held for fading. If we didn't do this, the tiles + // would just sit in the SourceCaches until the next render + this.style._releaseSymbolFadeTiles(); + } + + if (gpuTimer) { + const renderCPUTime = browser.now() - frameStartTime; + extTimerQuery.endQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); + setTimeout( + () => { + const renderGPUTime = extTimerQuery.getQueryObjectEXT( + gpuTimer, + extTimerQuery.QUERY_RESULT_EXT, + ) / (1000 * 1000); + extTimerQuery.deleteQueryEXT(gpuTimer); + this.fire( + new Event( + 'gpu-timing-frame', + { + cpuTime: renderCPUTime, + gpuTime: renderGPUTime, + }, + ), + ); + window.performance.mark( + 'frame-gpu', + { + startTime: frameStartTime, + detail: { + gpuTime: renderGPUTime, + }, + }, + ); + }, + 50, + ); // Wait 50ms to give time for all GPU calls to finish before querying - if (averageElevationChanged) { - this.triggerRepaint(); - } else { - this._triggerFrame(false); - if (willIdle) { - this.fire(new Event('idle')); - this._isInitialLoad = false; - // check the options to see if need to calculate the speed index - if (this.speedIndexTiming) { - const speedIndexNumber = this._calculateSpeedIndex(); - this.fire(new Event('speedindexcompleted', {speedIndex: speedIndexNumber})); - this.speedIndexTiming = false; - } - } - } - } - - if (this._loaded && !this._fullyLoaded && !somethingDirty) { - this._fullyLoaded = true; - LivePerformanceUtils.mark(PerformanceMarkers.fullLoad); - // Following lines are billing and metrics related code. Do not change. See LICENSE.txt - if (this._performanceMetricsCollection) { - postPerformanceEvent(this._requestManager._customAccessToken, { - width: this.painter.width, - height: this.painter.height, - interactionRange: this._interactionRange, - visibilityHidden: this._visibilityHidden, - terrainEnabled: !!this.painter.style.getTerrain(), - fogEnabled: !!this.painter.style.getFog(), - projection: this.getProjection().name, - zoom: this.transform.zoom, - renderer: this.painter.context.renderer, - vendor: this.painter.context.vendor - }); - } - this._authenticate(); - } - } - - _forceMarkerAndPopupUpdate(shouldWrap?: boolean) { - for (const marker of this._markers) { - // Wrap marker location when toggling to a projection without world copies - if (shouldWrap && !this.getRenderWorldCopies()) { - marker._lngLat = marker._lngLat.wrap(); - } - marker._update(); - } - for (const popup of this._popups) { - // Wrap popup location when toggling to a projection without world copies and track pointer set to false - if (shouldWrap && !this.getRenderWorldCopies() && !popup._trackPointer) { - popup._lngLat = popup._lngLat.wrap(); - } - popup._update(); - } - } + } - /** + PerformanceUtils.endMeasure(m); + + if (this.listens('gpu-timing-layer')) { + // Resetting the Painter's per-layer timing queries here allows us to isolate + // the queries to individual frames. + const frameLayerQueries = this.painter.collectGpuTimers(); + + setTimeout( + () => { + const renderedLayerTimes = this.painter.queryGpuTimers( + frameLayerQueries, + ); + + this.fire( + new Event( + 'gpu-timing-layer', + { + layerTimes: renderedLayerTimes, + }, + ), + ); + }, + 50, + ); // Wait 50ms to give time for all GPU calls to finish before querying + + } + + if (this.listens('gpu-timing-deferred-render')) { + const deferredRenderQueries = this.painter.collectDeferredRenderGpuQueries(); + + setTimeout( + () => { + const gpuTime = this.painter.queryGpuTimeDeferredRender( + deferredRenderQueries, + ); + this.fire(new Event('gpu-timing-deferred-render', {gpuTime})); + }, + 50, + ); // Wait 50ms to give time for all GPU calls to finish before querying + + } + + // Schedule another render frame if it's needed. + // + // Even though `_styleDirty` and `_sourcesDirty` are reset in this + // method, synchronous events fired during Style#update or + // Style#_updateSources could have caused them to be set again. + const somethingDirty = this._sourcesDirty || this._styleDirty || + this._placementDirty || + averageElevationChanged; + if (somethingDirty || this._repaint) { + this.triggerRepaint(); + } else { + const willIdle = !this.isMoving() && this.loaded(); + if (willIdle) { + // Before idling, we perform one last sample so that if the average elevation + // does not exactly match the terrain, we skip idle and ease it to its final state. + averageElevationChanged = this._updateAverageElevation( + frameStartTime, + true, + ); + } + + if (averageElevationChanged) { + this.triggerRepaint(); + } else { + this._triggerFrame(false); + if (willIdle) { + this.fire(new Event('idle')); + this._isInitialLoad = false; + // check the options to see if need to calculate the speed index + if (this.speedIndexTiming) { + const speedIndexNumber = this._calculateSpeedIndex(); + this.fire( + new Event('speedindexcompleted', {speedIndex: speedIndexNumber}), + ); + this.speedIndexTiming = false; + } + } + } + } + + if (this._loaded && !this._fullyLoaded && !somethingDirty) { + this._fullyLoaded = true; + LivePerformanceUtils.mark(PerformanceMarkers.fullLoad); + // Following lines are billing and metrics related code. Do not change. See LICENSE.txt + if (this._performanceMetricsCollection) { + postPerformanceEvent( + this._requestManager._customAccessToken, + { + width: this.painter.width, + height: this.painter.height, + interactionRange: this._interactionRange, + visibilityHidden: this._visibilityHidden, + terrainEnabled: !!this.painter.style.getTerrain(), + fogEnabled: !!this.painter.style.getFog(), + projection: this.getProjection().name, + zoom: this.transform.zoom, + renderer: this.painter.context.renderer, + vendor: this.painter.context.vendor, + }, + ); + } + this._authenticate(); + } + } + + _forceMarkerAndPopupUpdate(shouldWrap?: boolean) { + for (const marker of this._markers) { + // Wrap marker location when toggling to a projection without world copies + if (shouldWrap && !this.getRenderWorldCopies()) { + marker._lngLat = marker._lngLat.wrap(); + } + marker._update(); + } + for (const popup of this._popups) { + // Wrap popup location when toggling to a projection without world copies and track pointer set to false + if (shouldWrap && !this.getRenderWorldCopies() && !popup._trackPointer) { + popup._lngLat = popup._lngLat.wrap(); + } + popup._update(); + } + } + + /** * Update the average visible elevation by sampling terrain * * @returns {boolean} true if elevation has changed from the last sampling * @private */ - _updateAverageElevation(timeStamp: number, ignoreTimeout: boolean = false): boolean { - const applyUpdate = value => { - this.transform.averageElevation = value; - this._update(false); - return true; - }; - - if (!this.painter.averageElevationNeedsEasing()) { - if (this.transform.averageElevation !== 0) return applyUpdate(0); - return false; - } - - const timeoutElapsed = ignoreTimeout || timeStamp - this._averageElevationLastSampledAt > AVERAGE_ELEVATION_SAMPLING_INTERVAL; - - if (timeoutElapsed && !this._averageElevation.isEasing(timeStamp)) { - const currentElevation = this.transform.averageElevation; - let newElevation = this.transform.sampleAverageElevation(); - let exaggerationChanged = false; - if (this.transform.elevation) { - exaggerationChanged = this.transform.elevation.exaggeration() !== this._averageElevationExaggeration; - // $FlowIgnore[incompatible-use] - this._averageElevationExaggeration = this.transform.elevation.exaggeration(); - } - - // New elevation is NaN if no terrain tiles were available - if (isNaN(newElevation)) { - newElevation = 0; - } else { - // Don't activate the timeout if no data was available - this._averageElevationLastSampledAt = timeStamp; - } - const elevationChange = Math.abs(currentElevation - newElevation); - - if (elevationChange > AVERAGE_ELEVATION_EASE_THRESHOLD) { - if (this._isInitialLoad || exaggerationChanged) { - this._averageElevation.jumpTo(newElevation); - return applyUpdate(newElevation); - } else { - this._averageElevation.easeTo(newElevation, timeStamp, AVERAGE_ELEVATION_EASE_TIME); - } - } else if (elevationChange > AVERAGE_ELEVATION_CHANGE_THRESHOLD) { - this._averageElevation.jumpTo(newElevation); - return applyUpdate(newElevation); - } - } - - if (this._averageElevation.isEasing(timeStamp)) { - return applyUpdate(this._averageElevation.getValue(timeStamp)); - } - - return false; - } - - /***** START WARNING - REMOVAL OR MODIFICATION OF THE + _updateAverageElevation( + timeStamp: number, + ignoreTimeout: boolean = false, + ): boolean { + const applyUpdate = (value => { + this.transform.averageElevation = value; + this._update(false); + return true; + }); + + if (!this.painter.averageElevationNeedsEasing()) { + if (this.transform.averageElevation !== 0) return applyUpdate(0); + return false; + } + + const timeoutElapsed = ignoreTimeout || + timeStamp - this._averageElevationLastSampledAt > AVERAGE_ELEVATION_SAMPLING_INTERVAL; + + if (timeoutElapsed && !this._averageElevation.isEasing(timeStamp)) { + const currentElevation = this.transform.averageElevation; + let newElevation = this.transform.sampleAverageElevation(); + let exaggerationChanged = false; + if (this.transform.elevation) { + exaggerationChanged = this.transform.elevation.exaggeration() !== this._averageElevationExaggeration; + // $FlowIgnore[incompatible-use] + this._averageElevationExaggeration = this.transform.elevation.exaggeration(); + } + + // New elevation is NaN if no terrain tiles were available + if (isNaN(newElevation)) { + newElevation = 0; + } else { + // Don't activate the timeout if no data was available + this._averageElevationLastSampledAt = timeStamp; + } + const elevationChange = Math.abs(currentElevation - newElevation); + + if (elevationChange > AVERAGE_ELEVATION_EASE_THRESHOLD) { + if (this._isInitialLoad || exaggerationChanged) { + this._averageElevation.jumpTo(newElevation); + return applyUpdate(newElevation); + } else { + this._averageElevation.easeTo( + newElevation, + timeStamp, + AVERAGE_ELEVATION_EASE_TIME, + ); + } + } else if (elevationChange > AVERAGE_ELEVATION_CHANGE_THRESHOLD) { + this._averageElevation.jumpTo(newElevation); + return applyUpdate(newElevation); + } + } + + if (this._averageElevation.isEasing(timeStamp)) { + return applyUpdate(this._averageElevation.getValue(timeStamp)); + } + + return false; + } + + /***** START WARNING - REMOVAL OR MODIFICATION OF THE * FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** * The following code is used to access Mapbox's APIs. Removal or modification * of this code can result in higher fees and/or @@ -3396,80 +3813,124 @@ class Map extends Camera { * and the Mapbox Terms of Service are available at https://www.mapbox.com/tos/ ******************************************************************************/ - _authenticate() { - getMapSessionAPI(this._getMapId(), this._requestManager._skuToken, this._requestManager._customAccessToken, (err) => { - if (err) { - // throwing an error here will cause the callback to be called again unnecessarily - if (err.message === AUTH_ERR_MSG || (err: any).status === 401) { - const gl = this.painter.context.gl; - storeAuthState(gl, false); - if (this._logoControl instanceof LogoControl) { - this._logoControl._updateLogo(); - } - if (gl) gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); - - if (!this._silenceAuthErrors) { - this.fire(new ErrorEvent(new Error('A valid Mapbox access token is required to use Mapbox GL JS. To create an account or a new access token, visit https://account.mapbox.com/'))); - } - } - } - }); - postMapLoadEvent(this._getMapId(), this._requestManager._skuToken, this._requestManager._customAccessToken, () => {}); - } - - /***** END WARNING - REMOVAL OR MODIFICATION OF THE + _authenticate() { + getMapSessionAPI( + this._getMapId(), + this._requestManager._skuToken, + this._requestManager._customAccessToken, + err => { + if (err) { + // throwing an error here will cause the callback to be called again unnecessarily + if (err.message === AUTH_ERR_MSG || (err: any).status === 401) { + const gl = this.painter.context.gl; + storeAuthState(gl, false); + if (this._logoControl instanceof LogoControl) { + this._logoControl._updateLogo(); + } + if (gl) + gl.clear( + gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT, + ); + + if (!this._silenceAuthErrors) { + this.fire( + new ErrorEvent( + new Error( + 'A valid Mapbox access token is required to use Mapbox GL JS. To create an account or a new access token, visit https://account.mapbox.com/', + ), + ), + ); + } + } + } + }, + ); + postMapLoadEvent( + this._getMapId(), + this._requestManager._skuToken, + this._requestManager._customAccessToken, + () => {}, + ); + } + + /***** END WARNING - REMOVAL OR MODIFICATION OF THE PRECEDING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******/ - _updateTerrain() { - // Recalculate if enabled/disabled and calculate elevation cover. As camera is using elevation tiles before - // render (and deferred update after zoom recalculation), this needs to be called when removing terrain source. - const adaptCameraAltitude = this._isDragging(); - this.painter.updateTerrain(this.style, adaptCameraAltitude); - } - - _calculateSpeedIndex(): number { - const finalFrame = this.painter.canvasCopy(); - const canvasCopyInstances = this.painter.getCanvasCopiesAndTimestamps(); - canvasCopyInstances.timeStamps.push(performance.now()); - - const gl = this.painter.context.gl; - const framebuffer = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - - function read(texture) { - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); - const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); - gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - return pixels; - } - - return this._canvasPixelComparison(read(finalFrame), canvasCopyInstances.canvasCopies.map(read), canvasCopyInstances.timeStamps); - } - - _canvasPixelComparison(finalFrame: Uint8Array, allFrames: Uint8Array[], timeStamps: number[]): number { - let finalScore = timeStamps[1] - timeStamps[0]; - const numPixels = finalFrame.length / 4; - - for (let i = 0; i < allFrames.length; i++) { - const frame = allFrames[i]; - let cnt = 0; - for (let j = 0; j < frame.length; j += 4) { - if (frame[j] === finalFrame[j] && - frame[j + 1] === finalFrame[j + 1] && - frame[j + 2] === finalFrame[j + 2] && - frame[j + 3] === finalFrame[j + 3]) { - cnt = cnt + 1; - } - } - //calculate the % visual completeness - const interval = timeStamps[i + 2] - timeStamps[i + 1]; - const visualCompletness = cnt / numPixels; - finalScore += interval * (1 - visualCompletness); - } - return finalScore; - } - - /** + _updateTerrain() { + // Recalculate if enabled/disabled and calculate elevation cover. As camera is using elevation tiles before + // render (and deferred update after zoom recalculation), this needs to be called when removing terrain source. + const adaptCameraAltitude = this._isDragging(); + this.painter.updateTerrain(this.style, adaptCameraAltitude); + } + + _calculateSpeedIndex(): number { + const finalFrame = this.painter.canvasCopy(); + const canvasCopyInstances = this.painter.getCanvasCopiesAndTimestamps(); + canvasCopyInstances.timeStamps.push(performance.now()); + + const gl = this.painter.context.gl; + const framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + + function read(texture) { + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + texture, + 0, + ); + const pixels = new Uint8Array( + gl.drawingBufferWidth * gl.drawingBufferHeight * 4, + ); + gl.readPixels( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixels, + ); + return pixels; + } + + return this._canvasPixelComparison( + read(finalFrame), + canvasCopyInstances.canvasCopies.map(read), + canvasCopyInstances.timeStamps, + ); + } + + _canvasPixelComparison( + finalFrame: Uint8Array, + allFrames: Array, + timeStamps: Array, + ): number { + let finalScore = timeStamps[1] - timeStamps[0]; + const numPixels = finalFrame.length / 4; + + for (let i = 0; i < allFrames.length; i++) { + const frame = allFrames[i]; + let cnt = 0; + for (let j = 0; j < frame.length; j += 4) { + if ( + frame[j] === finalFrame[j] && frame[j + 1] === finalFrame[j + 1] && + frame[j + 2] === finalFrame[j + 2] && + frame[j + 3] === finalFrame[j + 3] + ) { + cnt = cnt + 1; + } + } + //calculate the % visual completeness + const interval = timeStamps[i + 2] - timeStamps[i + 1]; + const visualCompletness = cnt / numPixels; + finalScore += interval * (1 - visualCompletness); + } + return finalScore; + } + + /** * Clean up and release all internal resources associated with this map. * * This includes DOM elements, event bindings, web workers, and WebGL resources. @@ -3481,59 +3942,80 @@ class Map extends Camera { * @example * map.remove(); */ - remove() { - if (this._hash) this._hash.remove(); - - for (const control of this._controls) control.onRemove(this); - this._controls = []; - - if (this._frame) { - this._frame.cancel(); - this._frame = null; - } - this._renderTaskQueue.clear(); - this._domRenderTaskQueue.clear(); - if (this.style) { - this.style.destroy(); - } - this.painter.destroy(); - if (this.handlers) this.handlers.destroy(); - this.handlers = undefined; - this.setStyle(null); - - if (typeof window !== 'undefined') { - window.removeEventListener('resize', this._onWindowResize, false); - window.removeEventListener('orientationchange', this._onWindowResize, false); - window.removeEventListener('webkitfullscreenchange', this._onWindowResize, false); - window.removeEventListener('online', this._onWindowOnline, false); - window.removeEventListener('visibilitychange', this._onVisibilityChange, false); - } - - const extension = this.painter.context.gl.getExtension('WEBGL_lose_context'); - if (extension) extension.loseContext(); - - this._canvas.removeEventListener('webglcontextlost', this._contextLost, false); - this._canvas.removeEventListener('webglcontextrestored', this._contextRestored, false); - - this._canvasContainer.remove(); - this._controlContainer.remove(); - this._missingCSSCanary.remove(); - - this._canvas = (undefined: any); - this._canvasContainer = (undefined: any); - this._controlContainer = (undefined: any); - this._missingCSSCanary = (undefined: any); - - this._container.classList.remove('mapboxgl-map'); - this._container.removeEventListener('scroll', this._onMapScroll, false); - - PerformanceUtils.clearMetrics(); - removeAuthState(this.painter.context.gl); - this._removed = true; - this.fire(new Event('remove')); - } - - /** + remove() { + if (this._hash) this._hash.remove(); + + for (const control of this._controls) + control.onRemove(this); + this._controls = []; + + if (this._frame) { + this._frame.cancel(); + this._frame = null; + } + this._renderTaskQueue.clear(); + this._domRenderTaskQueue.clear(); + if (this.style) { + this.style.destroy(); + } + this.painter.destroy(); + if (this.handlers) this.handlers.destroy(); + this.handlers = undefined; + this.setStyle(null); + + if (typeof window !== 'undefined') { + window.removeEventListener('resize', this._onWindowResize, false); + window.removeEventListener( + 'orientationchange', + this._onWindowResize, + false, + ); + window.removeEventListener( + 'webkitfullscreenchange', + this._onWindowResize, + false, + ); + window.removeEventListener('online', this._onWindowOnline, false); + window.removeEventListener( + 'visibilitychange', + this._onVisibilityChange, + false, + ); + } + + const extension = this.painter.context.gl.getExtension('WEBGL_lose_context'); + if (extension) extension.loseContext(); + + this._canvas.removeEventListener( + 'webglcontextlost', + this._contextLost, + false, + ); + this._canvas.removeEventListener( + 'webglcontextrestored', + this._contextRestored, + false, + ); + + this._canvasContainer.remove(); + this._controlContainer.remove(); + this._missingCSSCanary.remove(); + + this._canvas = (undefined: any); + this._canvasContainer = (undefined: any); + this._controlContainer = (undefined: any); + this._missingCSSCanary = (undefined: any); + + this._container.classList.remove('mapboxgl-map'); + this._container.removeEventListener('scroll', this._onMapScroll, false); + + PerformanceUtils.clearMetrics(); + removeAuthState(this.painter.context.gl); + this._removed = true; + this.fire(new Event('remove')); + } + + /** * Trigger the rendering of a single frame. Use this method with custom layers to * repaint the map when the layer's properties or properties associated with the * layer's source change. Calling this multiple times before the @@ -3544,59 +4026,67 @@ class Map extends Camera { * @see [Example: Add a 3D model](https://docs.mapbox.com/mapbox-gl-js/example/add-3d-model/) * @see [Example: Add an animated icon to the map](https://docs.mapbox.com/mapbox-gl-js/example/add-image-animated/) */ - triggerRepaint() { - this._triggerFrame(true); - } - - _triggerFrame(render: boolean) { - this._renderNextFrame = this._renderNextFrame || render; - if (this.style && !this._frame) { - this._frame = browser.frame((paintStartTimeStamp: number) => { - const isRenderFrame = !!this._renderNextFrame; - PerformanceUtils.frame(paintStartTimeStamp, isRenderFrame); - this._frame = null; - this._renderNextFrame = null; - if (isRenderFrame) { - this._render(paintStartTimeStamp); - } - }); - } - } - - /** + triggerRepaint() { + this._triggerFrame(true); + } + + _triggerFrame(render: boolean) { + this._renderNextFrame = this._renderNextFrame || render; + if (this.style && !this._frame) { + this._frame = browser.frame( + (paintStartTimeStamp: number) => { + const isRenderFrame = !!this._renderNextFrame; + PerformanceUtils.frame(paintStartTimeStamp, isRenderFrame); + this._frame = null; + this._renderNextFrame = null; + if (isRenderFrame) { + this._render(paintStartTimeStamp); + } + }, + ); + } + } + + /** * Preloads all tiles that will be requested for one or a series of transformations * * @private * @returns {Object} Returns `this` | Promise. */ - _preloadTiles(transform: Transform | Array): this { - const sources: Array = this.style ? (Object.values(this.style._sourceCaches): any) : []; - asyncAll(sources, (source, done) => source._preloadTiles(transform, done), () => { - this.triggerRepaint(); - }); - - return this; - } - - _onWindowOnline() { - this._update(); - } - - _onWindowResize(event: Event) { - if (this._trackResize) { - this.resize({originalEvent: event})._update(); - } - } - - _onVisibilityChange() { - if (window.document.visibilityState === 'hidden') { - this._visibilityHidden++; - } - } - - /** @section {Debug features} */ - - /** + _preloadTiles(transform: Transform | Array): this { + const sources: Array = this.style ? + (Object.values(this.style._sourceCaches): any) : + []; + asyncAll( + sources, + (source, done) => source._preloadTiles(transform, done), + () => { + this.triggerRepaint(); + }, + ); + + return this; + } + + _onWindowOnline = () => { + this._update(); + }; + + _onWindowResize = (event: Event) => { + if (this._trackResize) { + this.resize({originalEvent: event})._update(); + } + }; + + _onVisibilityChange = () => { + if (window.document.visibilityState === 'hidden') { + this._visibilityHidden++; + } + }; + + /** @section {Debug features} */ + + /** * Gets and sets a Boolean indicating whether the map will render an outline * around each tile and the tile ID. These tile boundaries are useful for * debugging. @@ -3611,14 +4101,16 @@ class Map extends Camera { * @example * map.showTileBoundaries = true; */ - get showTileBoundaries(): boolean { return !!this._showTileBoundaries; } - set showTileBoundaries(value: boolean) { - if (this._showTileBoundaries === value) return; - this._showTileBoundaries = value; - this._update(); - } - - /** + get showTileBoundaries(): boolean { + return !!this._showTileBoundaries; + } + set showTileBoundaries(value: boolean) { + if (this._showTileBoundaries === value) return; + this._showTileBoundaries = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will render a wireframe * on top of the displayed terrain. Useful for debugging. * @@ -3631,14 +4123,16 @@ class Map extends Camera { * @example * map.showTerrainWireframe = true; */ - get showTerrainWireframe(): boolean { return !!this._showTerrainWireframe; } - set showTerrainWireframe(value: boolean) { - if (this._showTerrainWireframe === value) return; - this._showTerrainWireframe = value; - this._update(); - } - - /** + get showTerrainWireframe(): boolean { + return !!this._showTerrainWireframe; + } + set showTerrainWireframe(value: boolean) { + if (this._showTerrainWireframe === value) return; + this._showTerrainWireframe = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the speedindex metric calculation is on or off * * @private @@ -3649,14 +4143,16 @@ class Map extends Camera { * @example * map.speedIndexTiming = true; */ - get speedIndexTiming(): boolean { return !!this._speedIndexTiming; } - set speedIndexTiming(value: boolean) { - if (this._speedIndexTiming === value) return; - this._speedIndexTiming = value; - this._update(); - } - - /** + get speedIndexTiming(): boolean { + return !!this._speedIndexTiming; + } + set speedIndexTiming(value: boolean) { + if (this._speedIndexTiming === value) return; + this._speedIndexTiming = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will visualize * the padding offsets. * @@ -3665,14 +4161,16 @@ class Map extends Camera { * @instance * @memberof Map */ - get showPadding(): boolean { return !!this._showPadding; } - set showPadding(value: boolean) { - if (this._showPadding === value) return; - this._showPadding = value; - this._update(); - } - - /** + get showPadding(): boolean { + return !!this._showPadding; + } + set showPadding(value: boolean) { + if (this._showPadding === value) return; + this._showPadding = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will render boxes * around all symbols in the data source, revealing which symbols * were rendered or which were hidden due to collisions. @@ -3683,21 +4181,23 @@ class Map extends Camera { * @instance * @memberof Map */ - get showCollisionBoxes(): boolean { return !!this._showCollisionBoxes; } - set showCollisionBoxes(value: boolean) { - if (this._showCollisionBoxes === value) return; - this._showCollisionBoxes = value; - if (value) { - // When we turn collision boxes on we have to generate them for existing tiles - // When we turn them off, there's no cost to leaving existing boxes in place - this.style._generateCollisionBoxes(); - } else { - // Otherwise, call an update to remove collision boxes - this._update(); - } - } - - /** + get showCollisionBoxes(): boolean { + return !!this._showCollisionBoxes; + } + set showCollisionBoxes(value: boolean) { + if (this._showCollisionBoxes === value) return; + this._showCollisionBoxes = value; + if (value) { + // When we turn collision boxes on we have to generate them for existing tiles + // When we turn them off, there's no cost to leaving existing boxes in place + this.style._generateCollisionBoxes(); + } else { + // Otherwise, call an update to remove collision boxes + this._update(); + } + } + + /** * Gets and sets a Boolean indicating whether the map should color-code * each fragment to show how many times it has been shaded. * White fragments have been shaded 8 or more times. @@ -3709,14 +4209,16 @@ class Map extends Camera { * @instance * @memberof Map */ - get showOverdrawInspector(): boolean { return !!this._showOverdrawInspector; } - set showOverdrawInspector(value: boolean) { - if (this._showOverdrawInspector === value) return; - this._showOverdrawInspector = value; - this._update(); - } - - /** + get showOverdrawInspector(): boolean { + return !!this._showOverdrawInspector; + } + set showOverdrawInspector(value: boolean) { + if (this._showOverdrawInspector === value) return; + this._showOverdrawInspector = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will * continuously repaint. This information is useful for analyzing performance. * @@ -3725,37 +4227,49 @@ class Map extends Camera { * @instance * @memberof Map */ - get repaint(): boolean { return !!this._repaint; } - set repaint(value: boolean) { - if (this._repaint !== value) { - this._repaint = value; - this.triggerRepaint(); - } - } - // show vertices - get vertices(): boolean { return !!this._vertices; } - set vertices(value: boolean) { this._vertices = value; this._update(); } - - /** + get repaint(): boolean { + return !!this._repaint; + } + set repaint(value: boolean) { + if (this._repaint !== value) { + this._repaint = value; + this.triggerRepaint(); + } + } + // show vertices + get vertices(): boolean { + return !!this._vertices; + } + set vertices(value: boolean) { + this._vertices = value; + this._update(); + } + + /** * Display tile AABBs for debugging * * @private * @type {boolean} */ - get showTileAABBs(): boolean { return !!this._showTileAABBs; } - set showTileAABBs(value: boolean) { - if (this._showTileAABBs === value) return; - this._showTileAABBs = value; - if (!value) { Debug.clearAabbs(); return; } - this._update(); - } - - // for cache browser tests - _setCacheLimits(limit: number, checkThreshold: number) { - setCacheLimits(limit, checkThreshold); - } - - /** + get showTileAABBs(): boolean { + return !!this._showTileAABBs; + } + set showTileAABBs(value: boolean) { + if (this._showTileAABBs === value) return; + this._showTileAABBs = value; + if (!value) { + Debug.clearAabbs(); + return; + } + this._update(); + } + + // for cache browser tests + _setCacheLimits(limit: number, checkThreshold: number) { + setCacheLimits(limit, checkThreshold); + } + + /** * The version of Mapbox GL JS in use as specified in package.json, CHANGELOG.md, and the GitHub release. * * @name version @@ -3764,7 +4278,9 @@ class Map extends Camera { * @var {string} version */ - get version(): string { return version; } + get version(): string { + return version; + } } export default Map; diff --git a/src/ui/marker.js b/src/ui/marker.js index 118c4a2aad3..24d516f04b8 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -61,127 +61,166 @@ type Options = { * @see [Example: Add custom icons with Markers](https://www.mapbox.com/mapbox-gl-js/example/custom-marker-icons/) * @see [Example: Create a draggable Marker](https://www.mapbox.com/mapbox-gl-js/example/drag-a-marker/) */ -export default class Marker extends Evented { - _map: ?Map; - _anchor: Anchor; - _offset: Point; - _element: HTMLElement; - _popup: ?Popup; - _lngLat: LngLat; - _pos: ?Point; - _color: string; - _scale: number; - _defaultMarker: boolean; - _draggable: boolean; - _clickTolerance: number; - _isDragging: boolean; - _state: 'inactive' | 'pending' | 'active'; // used for handling drag events - _positionDelta: ?Point; - _pointerdownPos: ?Point; - _rotation: number; - _pitchAlignment: string; - _rotationAlignment: string; - _originalTabIndex: ?string; // original tabindex of _element - _fadeTimer: ?TimeoutID; - _updateFrameId: number; - _updateMoving: () => void; - _occludedOpacity: number; - - constructor(options?: Options, legacyOptions?: Options) { - super(); - // For backward compatibility -- the constructor used to accept the element as a - // required first argument, before it was made optional. - if (options instanceof window.HTMLElement || legacyOptions) { - options = extend({element: options}, legacyOptions); - } - - bindAll([ - '_update', - '_onMove', - '_onUp', - '_addDragHandler', - '_onMapClick', - '_onKeyPress', - '_clearFadeTimer' - ], this); - - this._anchor = (options && options.anchor) || 'center'; - this._color = (options && options.color) || '#3FB1CE'; - this._scale = (options && options.scale) || 1; - this._draggable = (options && options.draggable) || false; - this._clickTolerance = (options && options.clickTolerance) || 0; - this._isDragging = false; - this._state = 'inactive'; - this._rotation = (options && options.rotation) || 0; - this._rotationAlignment = (options && options.rotationAlignment) || 'auto'; - this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto'; - this._updateMoving = () => this._update(true); - this._occludedOpacity = (options && options.occludedOpacity) || 0.2; - - if (!options || !options.element) { - this._defaultMarker = true; - this._element = DOM.create('div'); - - // create default map marker SVG - - const DEFAULT_HEIGHT = 41; - const DEFAULT_WIDTH = 27; - - const svg = DOM.createSVG('svg', { - display: 'block', - height: `${DEFAULT_HEIGHT * this._scale}px`, - width: `${DEFAULT_WIDTH * this._scale}px`, - viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}` - }, this._element); - - const gradient = DOM.createSVG('radialGradient', {id: 'shadowGradient'}, DOM.createSVG('defs', {}, svg)); - DOM.createSVG('stop', {offset: '10%', 'stop-opacity': 0.4}, gradient); - DOM.createSVG('stop', {offset: '100%', 'stop-opacity': 0.05}, gradient); - DOM.createSVG('ellipse', {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, svg); // shadow - - DOM.createSVG('path', { // marker shape - fill: this._color, - d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z' - }, svg); - DOM.createSVG('path', { // border - opacity: 0.25, - d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z' - }, svg); - - DOM.createSVG('circle', {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, svg); // circle - - // if no element and no offset option given apply an offset for the default marker - // the -14 as the y value of the default marker offset was determined as follows - // - // the marker tip is at the center of the shadow ellipse from the default svg - // the y value of the center of the shadow ellipse relative to the svg top left is 34.8 - // offset to the svg center "height (41 / 2)" gives 34.8 - (41 / 2) and rounded for an integer pixel offset gives 14 - // negative is used to move the marker up from the center so the tip is at the Marker lngLat - this._offset = Point.convert((options && options.offset) || [0, -14]); - } else { - this._element = options.element; - this._offset = Point.convert((options && options.offset) || [0, 0]); - } - - if (!this._element.hasAttribute('aria-label')) this._element.setAttribute('aria-label', 'Map marker'); - this._element.classList.add('mapboxgl-marker'); - this._element.addEventListener('dragstart', (e: DragEvent) => { - e.preventDefault(); - }); - this._element.addEventListener('mousedown', (e: MouseEvent) => { - // prevent focusing on click - e.preventDefault(); - }); - const classList = this._element.classList; - for (const key in anchorTranslate) { - classList.remove(`mapboxgl-marker-anchor-${key}`); - } - classList.add(`mapboxgl-marker-anchor-${this._anchor}`); - - this._popup = null; - } - - /** +export default class Marker + extends Evented { + _map: ?Map; + _anchor: Anchor; + _offset: Point; + _element: HTMLElement; + _popup: ?Popup; + _lngLat: LngLat; + _pos: ?Point; + _color: string; + _scale: number; + _defaultMarker: boolean; + _draggable: boolean; + _clickTolerance: number; + _isDragging: boolean; + _state: 'inactive' | 'pending' | 'active'; // used for handling drag events + _positionDelta: ?Point; + _pointerdownPos: ?Point; + _rotation: number; + _pitchAlignment: string; + _rotationAlignment: string; + _originalTabIndex: ?string; // original tabindex of _element + _fadeTimer: ?TimeoutID; + _updateFrameId: number; + _updateMoving: () => void; + _occludedOpacity: number; + + constructor(options?: Options, legacyOptions?: Options) { + super(); + // For backward compatibility -- the constructor used to accept the element as a + // required first argument, before it was made optional. + if (options instanceof window.HTMLElement || legacyOptions) { + options = extend({element: options}, legacyOptions); + } + + bindAll( + [ + '_update', + '_onMove', + '_onUp', + '_addDragHandler', + '_onMapClick', + '_onKeyPress', + '_clearFadeTimer', + ], + this, + ); + + this._anchor = options && options.anchor || 'center'; + this._color = options && options.color || '#3FB1CE'; + this._scale = options && options.scale || 1; + this._draggable = options && options.draggable || false; + this._clickTolerance = options && options.clickTolerance || 0; + this._isDragging = false; + this._state = 'inactive'; + this._rotation = options && options.rotation || 0; + this._rotationAlignment = options && options.rotationAlignment || 'auto'; + this._pitchAlignment = options && options.pitchAlignment && + options.pitchAlignment || + 'auto'; + this._updateMoving = (() => this._update(true)); + this._occludedOpacity = options && options.occludedOpacity || 0.2; + + if (!options || !options.element) { + this._defaultMarker = true; + this._element = DOM.create('div'); + + // create default map marker SVG + + const DEFAULT_HEIGHT = 41; + const DEFAULT_WIDTH = 27; + + const svg = DOM.createSVG( + 'svg', + { + display: 'block', + height: `${DEFAULT_HEIGHT * this._scale}px`, + width: `${DEFAULT_WIDTH * this._scale}px`, + viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}`, + }, + this._element, + ); + + const gradient = DOM.createSVG( + 'radialGradient', + {id: 'shadowGradient'}, + DOM.createSVG('defs', {}, svg), + ); + DOM.createSVG('stop', {offset: '10%', 'stop-opacity': 0.4}, gradient); + DOM.createSVG('stop', {offset: '100%', 'stop-opacity': 0.05}, gradient); + DOM.createSVG( + 'ellipse', + {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, + svg, + ); // shadow + + DOM.createSVG( + 'path', + { + // marker shape + fill: this._color, + d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z', + }, + svg, + ); + DOM.createSVG( + 'path', + { + // border + opacity: 0.25, + d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z', + }, + svg, + ); + + DOM.createSVG( + 'circle', + {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, + svg, + ); // circle + + // if no element and no offset option given apply an offset for the default marker + // the -14 as the y value of the default marker offset was determined as follows + // + // the marker tip is at the center of the shadow ellipse from the default svg + // the y value of the center of the shadow ellipse relative to the svg top left is 34.8 + // offset to the svg center "height (41 / 2)" gives 34.8 - (41 / 2) and rounded for an integer pixel offset gives 14 + // negative is used to move the marker up from the center so the tip is at the Marker lngLat + this._offset = Point.convert(options && options.offset || [0, -14]); + } else { + this._element = options.element; + this._offset = Point.convert(options && options.offset || [0, 0]); + } + + if (!this._element.hasAttribute('aria-label')) + this._element.setAttribute('aria-label', 'Map marker'); + this._element.classList.add('mapboxgl-marker'); + this._element.addEventListener( + 'dragstart', + (e: DragEvent) => { + e.preventDefault(); + }, + ); + this._element.addEventListener( + 'mousedown', + (e: MouseEvent) => { + // prevent focusing on click + e.preventDefault(); + }, + ); + const classList = this._element.classList; + for (const key in anchorTranslate) { + classList.remove(`mapboxgl-marker-anchor-${key}`); + } + classList.add(`mapboxgl-marker-anchor-${this._anchor}`); + + this._popup = null; + } + + /** * Attaches the `Marker` to a `Map` object. * * @param {Map} map The Mapbox GL JS map to add the marker to. @@ -191,29 +230,29 @@ export default class Marker extends Evented { * .setLngLat([30.5, 50.5]) * .addTo(map); // add the marker to the map */ - addTo(map: Map): this { - if (map === this._map) { - return this; - } - this.remove(); - this._map = map; - map.getCanvasContainer().appendChild(this._element); - map.on('move', this._updateMoving); - map.on('moveend', this._update); - map.on('remove', this._clearFadeTimer); - map._addMarker(this); - this.setDraggable(this._draggable); - this._update(); - - // If we attached the `click` listener to the marker element, the popup - // would close once the event propogated to `map` due to the - // `Popup#_onClickClose` listener. - map.on('click', this._onMapClick); - - return this; - } - - /** + addTo(map: Map): this { + if (map === this._map) { + return this; + } + this.remove(); + this._map = map; + map.getCanvasContainer().appendChild(this._element); + map.on('move', this._updateMoving); + map.on('moveend', this._update); + map.on('remove', this._clearFadeTimer); + map._addMarker(this); + this.setDraggable(this._draggable); + this._update(); + + // If we attached the `click` listener to the marker element, the popup + // would close once the event propogated to `map` due to the + // `Popup#_onClickClose` listener. + map.on('click', this._onMapClick); + + return this; + } + + /** * Removes the marker from a map. * * @example @@ -221,29 +260,29 @@ export default class Marker extends Evented { * marker.remove(); * @returns {Marker} Returns itself to allow for method chaining. */ - remove(): this { - const map = this._map; - if (map) { - map.off('click', this._onMapClick); - map.off('move', this._updateMoving); - map.off('moveend', this._update); - map.off('mousedown', this._addDragHandler); - map.off('touchstart', this._addDragHandler); - map.off('mouseup', this._onUp); - map.off('touchend', this._onUp); - map.off('mousemove', this._onMove); - map.off('touchmove', this._onMove); - map.off('remove', this._clearFadeTimer); - map._removeMarker(this); - this._map = undefined; - } - this._clearFadeTimer(); - this._element.remove(); - if (this._popup) this._popup.remove(); - return this; - } - - /** + remove(): this { + const map = this._map; + if (map) { + map.off('click', this._onMapClick); + map.off('move', this._updateMoving); + map.off('moveend', this._update); + map.off('mousedown', this._addDragHandler); + map.off('touchstart', this._addDragHandler); + map.off('mouseup', this._onUp); + map.off('touchend', this._onUp); + map.off('mousemove', this._onMove); + map.off('touchmove', this._onMove); + map.off('remove', this._clearFadeTimer); + map._removeMarker(this); + this._map = undefined; + } + this._clearFadeTimer(); + this._element.remove(); + if (this._popup) this._popup.remove(); + return this; + } + + /** * Get the marker's geographical location. * * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously @@ -258,11 +297,11 @@ export default class Marker extends Evented { * console.log(`Longitude: ${lngLat.lng}, Latitude: ${lngLat.lat}`); * @see [Example: Create a draggable Marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/) */ - getLngLat(): LngLat { - return this._lngLat; - } + getLngLat(): LngLat { + return this._lngLat; + } - /** + /** * Set the marker's geographical position and move it. * * @param {LngLat} lnglat A {@link LngLat} describing where the marker should be located. @@ -276,26 +315,26 @@ export default class Marker extends Evented { * @see [Example: Create a draggable Marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/) * @see [Example: Add a marker using a place name](https://docs.mapbox.com/mapbox-gl-js/example/marker-from-geocode/) */ - setLngLat(lnglat: LngLatLike): this { - this._lngLat = LngLat.convert(lnglat); - this._pos = null; - if (this._popup) this._popup.setLngLat(this._lngLat); - this._update(true); - return this; - } - - /** + setLngLat(lnglat: LngLatLike): this { + this._lngLat = LngLat.convert(lnglat); + this._pos = null; + if (this._popup) this._popup.setLngLat(this._lngLat); + this._update(true); + return this; + } + + /** * Returns the `Marker`'s HTML element. * * @returns {HTMLElement} Returns the marker element. * @example * const element = marker.getElement(); */ - getElement(): HTMLElement { - return this._element; - } + getElement(): HTMLElement { + return this._element; + } - /** + /** * Binds a {@link Popup} to the {@link Marker}. * * @param {Popup | null} popup An instance of the {@link Popup} class. If undefined or null, any popup @@ -308,72 +347,84 @@ export default class Marker extends Evented { * .addTo(map); * @see [Example: Attach a popup to a marker instance](https://docs.mapbox.com/mapbox-gl-js/example/set-popup/) */ - setPopup(popup: ?Popup): this { - if (this._popup) { - this._popup.remove(); - this._popup = null; - this._element.removeAttribute('role'); - this._element.removeEventListener('keypress', this._onKeyPress); - - if (!this._originalTabIndex) { - this._element.removeAttribute('tabindex'); - } - } - - if (popup) { - if (!('offset' in popup.options)) { - const markerHeight = 41 - (5.8 / 2); - const markerRadius = 13.5; - const linearOffset = Math.sqrt(Math.pow(markerRadius, 2) / 2); - popup.options.offset = this._defaultMarker ? { - 'top': [0, 0], - 'top-left': [0, 0], - 'top-right': [0, 0], - 'bottom': [0, -markerHeight], - 'bottom-left': [linearOffset, (markerHeight - markerRadius + linearOffset) * -1], - 'bottom-right': [-linearOffset, (markerHeight - markerRadius + linearOffset) * -1], - 'left': [markerRadius, (markerHeight - markerRadius) * -1], - 'right': [-markerRadius, (markerHeight - markerRadius) * -1] - } : this._offset; - } - this._popup = popup; - popup._marker = this; - if (this._lngLat) this._popup.setLngLat(this._lngLat); - - this._element.setAttribute('role', 'button'); - this._originalTabIndex = this._element.getAttribute('tabindex'); - if (!this._originalTabIndex) { - this._element.setAttribute('tabindex', '0'); - } - this._element.addEventListener('keypress', this._onKeyPress); - this._element.setAttribute('aria-expanded', 'false'); - } - - return this; - } - - _onKeyPress(e: KeyboardEvent) { - const code = e.code; - const legacyCode = e.charCode || e.keyCode; - - if ( - (code === 'Space') || (code === 'Enter') || - (legacyCode === 32) || (legacyCode === 13) // space or enter - ) { - this.togglePopup(); - } - } - - _onMapClick(e: MapMouseEvent) { - const targetElement = e.originalEvent.target; - const element = this._element; - - if (this._popup && (targetElement === element || element.contains((targetElement: any)))) { - this.togglePopup(); - } - } - - /** + setPopup(popup: ?Popup): this { + if (this._popup) { + this._popup.remove(); + this._popup = null; + this._element.removeAttribute('role'); + this._element.removeEventListener('keypress', this._onKeyPress); + + if (!this._originalTabIndex) { + this._element.removeAttribute('tabindex'); + } + } + + if (popup) { + if (!('offset' in popup.options)) { + const markerHeight = 41 - 5.8 / 2; + const markerRadius = 13.5; + const linearOffset = Math.sqrt(Math.pow(markerRadius, 2) / 2); + popup.options.offset = this._defaultMarker ? + { + 'top': [0, 0], + 'top-left': [0, 0], + 'top-right': [0, 0], + 'bottom': [0, -markerHeight], + 'bottom-left': [ + linearOffset, + (markerHeight - markerRadius + linearOffset) * -1, + ], + 'bottom-right': [ + -linearOffset, + (markerHeight - markerRadius + linearOffset) * -1, + ], + 'left': [markerRadius, (markerHeight - markerRadius) * -1], + 'right': [-markerRadius, (markerHeight - markerRadius) * -1], + } : + this._offset; + } + this._popup = popup; + popup._marker = this; + if (this._lngLat) this._popup.setLngLat(this._lngLat); + + this._element.setAttribute('role', 'button'); + this._originalTabIndex = this._element.getAttribute('tabindex'); + if (!this._originalTabIndex) { + this._element.setAttribute('tabindex', '0'); + } + this._element.addEventListener('keypress', this._onKeyPress); + this._element.setAttribute('aria-expanded', 'false'); + } + + return this; + } + + _onKeyPress = (e: KeyboardEvent) => { + const code = e.code; + const legacyCode = e.charCode || e.keyCode; + + if ( + code === 'Space' || code === 'Enter' || legacyCode === 32 || + legacyCode === 13 // space or enter + + ) { + this.togglePopup(); + } + }; + + _onMapClick = (e: MapMouseEvent) => { + const targetElement = e.originalEvent.target; + const element = this._element; + + if ( + this._popup && + (targetElement === element || element.contains((targetElement: any))) + ) { + this.togglePopup(); + } + }; + + /** * Returns the {@link Popup} instance that is bound to the {@link Marker}. * * @returns {Popup} Returns the popup. @@ -385,11 +436,11 @@ export default class Marker extends Evented { * * console.log(marker.getPopup()); // return the popup instance */ - getPopup(): ?Popup { - return this._popup; - } + getPopup(): ?Popup { + return this._popup; + } - /** + /** * Opens or closes the {@link Popup} instance that is bound to the {@link Marker}, depending on the current state of the {@link Popup}. * * @returns {Marker} Returns itself to allow for method chaining. @@ -401,200 +452,229 @@ export default class Marker extends Evented { * * marker.togglePopup(); // toggle popup open or closed */ - togglePopup(): this { - const popup = this._popup; - if (!popup) { - return this; - } else if (popup.isOpen()) { - popup.remove(); - this._element.setAttribute('aria-expanded', 'false'); - } else if (this._map) { - popup.addTo(this._map); - this._element.setAttribute('aria-expanded', 'true'); - } - return this; - } - - _behindTerrain(): boolean { - const map = this._map; - const pos = this._pos; - if (!map || !pos) return false; - const unprojected = map.unproject(pos); - const camera = map.getFreeCameraOptions(); - if (!camera.position) return false; - const cameraLngLat = camera.position.toLngLat(); - const toClosestSurface = cameraLngLat.distanceTo(unprojected); - const toMarker = cameraLngLat.distanceTo(this._lngLat); - return toClosestSurface < toMarker * 0.9; - - } - - _evaluateOpacity() { - const map = this._map; - if (!map) return; - - const pos = this._pos; - - if (!pos || pos.x < 0 || pos.x > map.transform.width || pos.y < 0 || pos.y > map.transform.height) { - this._clearFadeTimer(); - return; - } - const mapLocation = map.unproject(pos); - let opacity; - if (map._showingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) { - opacity = 0; - } else { - opacity = 1 - map._queryFogOpacity(mapLocation); - if (map.transform._terrainEnabled() && map.getTerrain() && this._behindTerrain()) { - opacity *= this._occludedOpacity; - } - } - - this._element.style.opacity = `${opacity}`; - this._element.style.pointerEvents = opacity > 0 ? 'auto' : 'none'; - if (this._popup) { - this._popup._setOpacity(opacity); - } - - this._fadeTimer = null; - } - - _clearFadeTimer() { - if (this._fadeTimer) { - clearTimeout(this._fadeTimer); - this._fadeTimer = null; - } - } - - _updateDOM() { - const pos = this._pos; - const map = this._map; - if (!pos || !map) { return; } - - const offset = this._offset.mult(this._scale); - - this._element.style.transform = ` + togglePopup(): this { + const popup = this._popup; + if (!popup) { + return this; + } else if (popup.isOpen()) { + popup.remove(); + this._element.setAttribute('aria-expanded', 'false'); + } else if (this._map) { + popup.addTo(this._map); + this._element.setAttribute('aria-expanded', 'true'); + } + return this; + } + + _behindTerrain(): boolean { + const map = this._map; + const pos = this._pos; + if (!map || !pos) return false; + const unprojected = map.unproject(pos); + const camera = map.getFreeCameraOptions(); + if (!camera.position) return false; + const cameraLngLat = camera.position.toLngLat(); + const toClosestSurface = cameraLngLat.distanceTo(unprojected); + const toMarker = cameraLngLat.distanceTo(this._lngLat); + return toClosestSurface < toMarker * 0.9; + } + + _evaluateOpacity = () => { + const map = this._map; + if (!map) return; + + const pos = this._pos; + + if ( + !pos || pos.x < 0 || pos.x > map.transform.width || pos.y < 0 || + pos.y > map.transform.height + ) { + this._clearFadeTimer(); + return; + } + const mapLocation = map.unproject(pos); + let opacity; + if (map._showingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) { + opacity = 0; + } else { + opacity = 1 - map._queryFogOpacity(mapLocation); + if ( + map.transform._terrainEnabled() && map.getTerrain() && + this._behindTerrain() + ) { + opacity *= this._occludedOpacity; + } + } + + this._element.style.opacity = `${opacity}`; + this._element.style.pointerEvents = opacity > 0 ? 'auto' : 'none'; + if (this._popup) { + this._popup._setOpacity(opacity); + } + + this._fadeTimer = null; + }; + + _clearFadeTimer = () => { + if (this._fadeTimer) { + clearTimeout(this._fadeTimer); + this._fadeTimer = null; + } + }; + + _updateDOM() { + const pos = this._pos; + const map = this._map; + if (!pos || !map) { + return; + } + + const offset = this._offset.mult(this._scale); + + this._element.style.transform = ` translate(${pos.x}px,${pos.y}px) ${anchorTranslate[this._anchor]} ${this._calculateXYTransform()} ${this._calculateZTransform()} translate(${offset.x}px,${offset.y}px) `; - } - - _calculateXYTransform(): string { - const pos = this._pos; - const map = this._map; - const alignment = this.getPitchAlignment(); - - // `viewport', 'auto' and invalid arugments do no pitch transformation. - if (!map || !pos || alignment !== 'map') { - return ``; - } - // 'map' alignment on a flat map - if (!map._showingGlobe()) { - const pitch = map.getPitch(); - return pitch ? `rotateX(${pitch}deg)` : ''; - } - // 'map' alignment on globe - const tilt = radToDeg(globeTiltAtLngLat(map.transform, this._lngLat)); - const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform)); - const manhattanDistance = (Math.abs(posFromCenter.x) + Math.abs(posFromCenter.y)); - if (manhattanDistance === 0) { return ''; } - - const tiltOverDist = tilt / manhattanDistance; - const yTilt = posFromCenter.x * tiltOverDist; - const xTilt = -posFromCenter.y * tiltOverDist; - return `rotateX(${xTilt}deg) rotateY(${yTilt}deg)`; - - } - - _calculateZTransform(): string { - - const pos = this._pos; - const map = this._map; - if (!map || !pos) { return ''; } - - let rotation = 0; - const alignment = this.getRotationAlignment(); - if (alignment === 'map') { - if (map._showingGlobe()) { - const north = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat + .001)); - const south = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat - .001)); - const diff = south.sub(north); - rotation = radToDeg(Math.atan2(diff.y, diff.x)) - 90; - } else { - rotation = -map.getBearing(); - } - } else if (alignment === 'horizon') { - const ALIGN_TO_HORIZON_BELOW_ZOOM = 4; - const ALIGN_TO_SCREEN_ABOVE_ZOOM = 6; - assert(ALIGN_TO_SCREEN_ABOVE_ZOOM <= GLOBE_ZOOM_THRESHOLD_MAX, 'Horizon-oriented marker transition should be complete when globe switches to Mercator'); - assert(ALIGN_TO_HORIZON_BELOW_ZOOM <= ALIGN_TO_SCREEN_ABOVE_ZOOM); - - const smooth = smoothstep(ALIGN_TO_HORIZON_BELOW_ZOOM, ALIGN_TO_SCREEN_ABOVE_ZOOM, map.getZoom()); - - const centerPoint = globeCenterToScreenPoint(map.transform); - centerPoint.y += smooth * map.transform.height; - const rel = pos.sub(centerPoint); - const angle = radToDeg(Math.atan2(rel.y, rel.x)); - const up = angle > 90 ? angle - 270 : angle + 90; - rotation = up * (1 - smooth); - } - - rotation += this._rotation; - return rotation ? `rotateZ(${rotation}deg)` : ''; - } - - _update(delaySnap?: boolean) { - window.cancelAnimationFrame(this._updateFrameId); - const map = this._map; - if (!map) return; - - if (map.transform.renderWorldCopies) { - this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); - } - - this._pos = map.project(this._lngLat); - - // because rounding the coordinates at every `move` event causes stuttered zooming - // we only round them when _update is called with `moveend` or when its called with - // no arguments (when the Marker is initialized or Marker#setLngLat is invoked). - if (delaySnap === true) { - this._updateFrameId = window.requestAnimationFrame(() => { - if (this._element && this._pos && this._anchor) { - this._pos = this._pos.round(); - this._updateDOM(); - } - }); - } else { - this._pos = this._pos.round(); - } - - map._requestDomTask(() => { - if (!this._map) return; - + } + + _calculateXYTransform(): string { + const pos = this._pos; + const map = this._map; + const alignment = this.getPitchAlignment(); + + // `viewport', 'auto' and invalid arugments do no pitch transformation. + if (!map || !pos || alignment !== 'map') { + return ``; + } + // 'map' alignment on a flat map + if (!map._showingGlobe()) { + const pitch = map.getPitch(); + return pitch ? `rotateX(${pitch}deg)` : ''; + } + // 'map' alignment on globe + const tilt = radToDeg(globeTiltAtLngLat(map.transform, this._lngLat)); + const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform)); + const manhattanDistance = Math.abs(posFromCenter.x) + Math.abs( + posFromCenter.y, + ); + if (manhattanDistance === 0) { + return ''; + } + + const tiltOverDist = tilt / manhattanDistance; + const yTilt = posFromCenter.x * tiltOverDist; + const xTilt = -posFromCenter.y * tiltOverDist; + return `rotateX(${xTilt}deg) rotateY(${yTilt}deg)`; + } + + _calculateZTransform(): string { + const pos = this._pos; + const map = this._map; + if (!map || !pos) { + return ''; + } + + let rotation = 0; + const alignment = this.getRotationAlignment(); + if (alignment === 'map') { + if (map._showingGlobe()) { + const north = map.project( + new LngLat(this._lngLat.lng, this._lngLat.lat + .001), + ); + const south = map.project( + new LngLat(this._lngLat.lng, this._lngLat.lat - .001), + ); + const diff = south.sub(north); + rotation = radToDeg(Math.atan2(diff.y, diff.x)) - 90; + } else { + rotation = -map.getBearing(); + } + } else if (alignment === 'horizon') { + const ALIGN_TO_HORIZON_BELOW_ZOOM = 4; + const ALIGN_TO_SCREEN_ABOVE_ZOOM = 6; + assert( + ALIGN_TO_SCREEN_ABOVE_ZOOM <= GLOBE_ZOOM_THRESHOLD_MAX, + 'Horizon-oriented marker transition should be complete when globe switches to Mercator', + ); + assert(ALIGN_TO_HORIZON_BELOW_ZOOM <= ALIGN_TO_SCREEN_ABOVE_ZOOM); + + const smooth = smoothstep( + ALIGN_TO_HORIZON_BELOW_ZOOM, + ALIGN_TO_SCREEN_ABOVE_ZOOM, + map.getZoom(), + ); + + const centerPoint = globeCenterToScreenPoint(map.transform); + centerPoint.y += smooth * map.transform.height; + const rel = pos.sub(centerPoint); + const angle = radToDeg(Math.atan2(rel.y, rel.x)); + const up = angle > 90 ? angle - 270 : angle + 90; + rotation = up * (1 - smooth); + } + + rotation += this._rotation; + return rotation ? `rotateZ(${rotation}deg)` : ''; + } + + _update = (delaySnap?: boolean) => { + window.cancelAnimationFrame(this._updateFrameId); + const map = this._map; + if (!map) return; + + if (map.transform.renderWorldCopies) { + this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); + } + + this._pos = map.project(this._lngLat); + + // because rounding the coordinates at every `move` event causes stuttered zooming + // we only round them when _update is called with `moveend` or when its called with + // no arguments (when the Marker is initialized or Marker#setLngLat is invoked). + if (delaySnap === true) { + this._updateFrameId = window.requestAnimationFrame( + () => { if (this._element && this._pos && this._anchor) { + this._pos = this._pos.round(); this._updateDOM(); } - - if ((map._showingGlobe() || map.getTerrain() || map.getFog()) && !this._fadeTimer) { - this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60); - } - }); - } - - /** + }, + ); + } else { + this._pos = this._pos.round(); + } + + map._requestDomTask( + () => { + if (!this._map) return; + + if (this._element && this._pos && this._anchor) { + this._updateDOM(); + } + + if ( + (map._showingGlobe() || map.getTerrain() || map.getFog()) && + !this._fadeTimer + ) { + this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60); + } + }, + ); + }; + + /** * Get the marker's offset. * * @returns {Point} The marker's screen coordinates in pixels. * @example * const offset = marker.getOffset(); */ - getOffset(): Point { - return this._offset; - } + getOffset(): Point { + return this._offset; + } - /** + /** * Sets the offset of the marker. * * @param {PointLike} offset The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up. @@ -602,39 +682,39 @@ export default class Marker extends Evented { * @example * marker.setOffset([0, 1]); */ - setOffset(offset: PointLike): this { - this._offset = Point.convert(offset); - this._update(); - return this; - } - - _onMove(e: MapMouseEvent | MapTouchEvent) { - const map = this._map; - if (!map) return; - - const startPos = this._pointerdownPos; - const posDelta = this._positionDelta; - if (!startPos || !posDelta) return; - - if (!this._isDragging) { - const clickTolerance = this._clickTolerance || map._clickTolerance; - if (e.point.dist(startPos) < clickTolerance) return; - this._isDragging = true; - } - - this._pos = e.point.sub(posDelta); - this._lngLat = map.unproject(this._pos); - this.setLngLat(this._lngLat); - // suppress click event so that popups don't toggle on drag - this._element.style.pointerEvents = 'none'; - - // make sure dragstart only fires on the first move event after mousedown. - // this can't be on mousedown because that event doesn't necessarily - // imply that a drag is about to happen. - if (this._state === 'pending') { - this._state = 'active'; - - /** + setOffset(offset: PointLike): this { + this._offset = Point.convert(offset); + this._update(); + return this; + } + + _onMove = (e: MapMouseEvent | MapTouchEvent) => { + const map = this._map; + if (!map) return; + + const startPos = this._pointerdownPos; + const posDelta = this._positionDelta; + if (!startPos || !posDelta) return; + + if (!this._isDragging) { + const clickTolerance = this._clickTolerance || map._clickTolerance; + if (e.point.dist(startPos) < clickTolerance) return; + this._isDragging = true; + } + + this._pos = e.point.sub(posDelta); + this._lngLat = map.unproject(this._pos); + this.setLngLat(this._lngLat); + // suppress click event so that popups don't toggle on drag + this._element.style.pointerEvents = 'none'; + + // make sure dragstart only fires on the first move event after mousedown. + // this can't be on mousedown because that event doesn't necessarily + // imply that a drag is about to happen. + if (this._state === 'pending') { + this._state = 'active'; + + /** * Fired when dragging starts. * * @event dragstart @@ -643,10 +723,10 @@ export default class Marker extends Evented { * @type {Object} * @property {Marker} marker The object that is being dragged. */ - this.fire(new Event('dragstart')); - } + this.fire(new Event('dragstart')); + } - /** + /** * Fired while dragging. * * @event drag @@ -655,25 +735,25 @@ export default class Marker extends Evented { * @type {Object} * @property {Marker} marker The object that is being dragged. */ - this.fire(new Event('drag')); - } - - _onUp() { - // revert to normal pointer event handling - this._element.style.pointerEvents = 'auto'; - this._positionDelta = null; - this._pointerdownPos = null; - this._isDragging = false; - - const map = this._map; - if (map) { - map.off('mousemove', this._onMove); - map.off('touchmove', this._onMove); - } - - // only fire dragend if it was preceded by at least one drag event - if (this._state === 'active') { - /** + this.fire(new Event('drag')); + }; + + _onUp = () => { + // revert to normal pointer event handling + this._element.style.pointerEvents = 'auto'; + this._positionDelta = null; + this._pointerdownPos = null; + this._isDragging = false; + + const map = this._map; + if (map) { + map.off('mousemove', this._onMove); + map.off('touchmove', this._onMove); + } + + // only fire dragend if it was preceded by at least one drag event + if (this._state === 'active') { + /** * Fired when the marker is finished being dragged. * * @event dragend @@ -682,38 +762,38 @@ export default class Marker extends Evented { * @type {Object} * @property {Marker} marker The object that was dragged. */ - this.fire(new Event('dragend')); - } - - this._state = 'inactive'; - } - - _addDragHandler(e: MapMouseEvent | MapTouchEvent) { - const map = this._map; - const pos = this._pos; - if (!map || !pos) return; - - if (this._element.contains((e.originalEvent.target: any))) { - e.preventDefault(); - - // We need to calculate the pixel distance between the click point - // and the marker position, with the offset accounted for. Then we - // can subtract this distance from the mousemove event's position - // to calculate the new marker position. - // If we don't do this, the marker 'jumps' to the click position - // creating a jarring UX effect. - this._positionDelta = e.point.sub(pos); - this._pointerdownPos = e.point; - - this._state = 'pending'; - map.on('mousemove', this._onMove); - map.on('touchmove', this._onMove); - map.once('mouseup', this._onUp); - map.once('touchend', this._onUp); - } - } - - /** + this.fire(new Event('dragend')); + } + + this._state = 'inactive'; + }; + + _addDragHandler = (e: MapMouseEvent | MapTouchEvent) => { + const map = this._map; + const pos = this._pos; + if (!map || !pos) return; + + if (this._element.contains((e.originalEvent.target: any))) { + e.preventDefault(); + + // We need to calculate the pixel distance between the click point + // and the marker position, with the offset accounted for. Then we + // can subtract this distance from the mousemove event's position + // to calculate the new marker position. + // If we don't do this, the marker 'jumps' to the click position + // creating a jarring UX effect. + this._positionDelta = e.point.sub(pos); + this._pointerdownPos = e.point; + + this._state = 'pending'; + map.on('mousemove', this._onMove); + map.on('touchmove', this._onMove); + map.once('mouseup', this._onUp); + map.once('touchend', this._onUp); + } + }; + + /** * Sets the `draggable` property and functionality of the marker. * * @param {boolean} [shouldBeDraggable=false] Turns drag functionality on/off. @@ -721,37 +801,37 @@ export default class Marker extends Evented { * @example * marker.setDraggable(true); */ - setDraggable(shouldBeDraggable: boolean): this { - this._draggable = !!shouldBeDraggable; // convert possible undefined value to false - - // handle case where map may not exist yet - // for example, when setDraggable is called before addTo - const map = this._map; - if (map) { - if (shouldBeDraggable) { - map.on('mousedown', this._addDragHandler); - map.on('touchstart', this._addDragHandler); - } else { - map.off('mousedown', this._addDragHandler); - map.off('touchstart', this._addDragHandler); - } - } - - return this; - } - - /** + setDraggable(shouldBeDraggable: boolean): this { + this._draggable = !!shouldBeDraggable; // convert possible undefined value to false + + // handle case where map may not exist yet + // for example, when setDraggable is called before addTo + const map = this._map; + if (map) { + if (shouldBeDraggable) { + map.on('mousedown', this._addDragHandler); + map.on('touchstart', this._addDragHandler); + } else { + map.off('mousedown', this._addDragHandler); + map.off('touchstart', this._addDragHandler); + } + } + + return this; + } + + /** * Returns true if the marker can be dragged. * * @returns {boolean} True if the marker is draggable. * @example * const isMarkerDraggable = marker.isDraggable(); */ - isDraggable(): boolean { - return this._draggable; - } + isDraggable(): boolean { + return this._draggable; + } - /** + /** * Sets the `rotation` property of the marker. * * @param {number} [rotation=0] The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker#setRotationAlignment} setting. @@ -759,24 +839,24 @@ export default class Marker extends Evented { * @example * marker.setRotation(45); */ - setRotation(rotation: number): this { - this._rotation = rotation || 0; - this._update(); - return this; - } + setRotation(rotation: number): this { + this._rotation = rotation || 0; + this._update(); + return this; + } - /** + /** * Returns the current rotation angle of the marker (in degrees). * * @returns {number} The current rotation angle of the marker. * @example * const rotation = marker.getRotation(); */ - getRotation(): number { - return this._rotation; - } + getRotation(): number { + return this._rotation; + } - /** + /** * Sets the `rotationAlignment` property of the marker. * * @param {string} [alignment='auto'] Sets the `rotationAlignment` property of the marker. @@ -784,28 +864,30 @@ export default class Marker extends Evented { * @example * marker.setRotationAlignment('viewport'); */ - setRotationAlignment(alignment: string): this { - this._rotationAlignment = alignment || 'auto'; - this._update(); - return this; - } + setRotationAlignment(alignment: string): this { + this._rotationAlignment = alignment || 'auto'; + this._update(); + return this; + } - /** + /** * Returns the current `rotationAlignment` property of the marker. * * @returns {string} The current rotational alignment of the marker. * @example * const alignment = marker.getRotationAlignment(); */ - getRotationAlignment(): string { - if (this._rotationAlignment === 'auto') - return 'viewport'; - if (this._rotationAlignment === 'horizon' && this._map && !this._map._showingGlobe()) - return 'viewport'; - return this._rotationAlignment; - } - - /** + getRotationAlignment(): string { + if (this._rotationAlignment === 'auto') return 'viewport'; + if ( + this._rotationAlignment === 'horizon' && this._map && + !this._map._showingGlobe() + ) + return 'viewport'; + return this._rotationAlignment; + } + + /** * Sets the `pitchAlignment` property of the marker. * * @param {string} [alignment] Sets the `pitchAlignment` property of the marker. If alignment is 'auto', it will automatically match `rotationAlignment`. @@ -813,27 +895,27 @@ export default class Marker extends Evented { * @example * marker.setPitchAlignment('map'); */ - setPitchAlignment(alignment: string): this { - this._pitchAlignment = alignment || 'auto'; - this._update(); - return this; - } + setPitchAlignment(alignment: string): this { + this._pitchAlignment = alignment || 'auto'; + this._update(); + return this; + } - /** + /** * Returns the current `pitchAlignment` behavior of the marker. * * @returns {string} The current pitch alignment of the marker. * @example * const alignment = marker.getPitchAlignment(); */ - getPitchAlignment(): string { - if (this._pitchAlignment === 'auto') { - return this.getRotationAlignment(); - } - return this._pitchAlignment; - } - - /** + getPitchAlignment(): string { + if (this._pitchAlignment === 'auto') { + return this.getRotationAlignment(); + } + return this._pitchAlignment; + } + + /** * Sets the `occludedOpacity` property of the marker. * This opacity is used on the marker when the marker is occluded by terrain. * @@ -842,20 +924,20 @@ export default class Marker extends Evented { * @example * marker.setOccludedOpacity(0.3); */ - setOccludedOpacity(opacity: number): this { - this._occludedOpacity = opacity || 0.2; - this._update(); - return this; - } + setOccludedOpacity(opacity: number): this { + this._occludedOpacity = opacity || 0.2; + this._update(); + return this; + } - /** + /** * Returns the current `occludedOpacity` of the marker. * * @returns {number} The opacity of a terrain occluded marker. * @example * const opacity = marker.getOccludedOpacity(); */ - getOccludedOpacity(): number { - return this._occludedOpacity; - } + getOccludedOpacity(): number { + return this._occludedOpacity; + } } diff --git a/src/ui/popup.js b/src/ui/popup.js index 3c8813e3372..d80d68bc65b 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -101,29 +101,31 @@ const focusQuerySelector = [ * @see [Example: Display a popup on click](https://www.mapbox.com/mapbox-gl-js/example/popup-on-click/) * @see [Example: Attach a popup to a marker instance](https://www.mapbox.com/mapbox-gl-js/example/set-popup/) */ -export default class Popup extends Evented { - _map: ?Map; - options: PopupOptions; - _content: ?HTMLElement; - _container: ?HTMLElement; - _closeButton: ?HTMLElement; - _tip: ?HTMLElement; - _lngLat: LngLat; - _trackPointer: boolean; - _pos: ?Point; - _anchor: Anchor; - _classList: Set; - _marker: ?Marker; - - constructor(options: PopupOptions) { - super(); - this.options = extend(Object.create(defaultOptions), options); - bindAll(['_update', '_onClose', 'remove', '_onMouseEvent'], this); - this._classList = new Set(options && options.className ? - options.className.trim().split(/\s+/) : []); - } - - /** +export default class Popup + extends Evented { + _map: ?Map; + options: PopupOptions; + _content: ?HTMLElement; + _container: ?HTMLElement; + _closeButton: ?HTMLElement; + _tip: ?HTMLElement; + _lngLat: LngLat; + _trackPointer: boolean; + _pos: ?Point; + _anchor: Anchor; + _classList: Set; + _marker: ?Marker; + + constructor(options: PopupOptions) { + super(); + this.options = extend(Object.create(defaultOptions), options); + bindAll(['_update', '_onClose', 'remove', '_onMouseEvent'], this); + this._classList = new Set( + options && options.className ? options.className.trim().split(/\s+/) : [], + ); + } + + /** * Adds the popup to a map. * * @param {Map} map The Mapbox GL JS map to add the popup to. @@ -138,32 +140,32 @@ export default class Popup extends Evented { * @see [Example: Display a popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) * @see [Example: Show polygon information on click](https://docs.mapbox.com/mapbox-gl-js/example/polygon-popup-on-click/) */ - addTo(map: Map): this { - if (this._map) this.remove(); - - this._map = map; - if (this.options.closeOnClick) { - map.on('preclick', this._onClose); - } - - if (this.options.closeOnMove) { - map.on('move', this._onClose); - } - - map.on('remove', this.remove); - this._update(); - map._addPopup(this); - this._focusFirstElement(); - - if (this._trackPointer) { - map.on('mousemove', this._onMouseEvent); - map.on('mouseup', this._onMouseEvent); - map._canvasContainer.classList.add('mapboxgl-track-pointer'); - } else { - map.on('move', this._update); - } - - /** + addTo(map: Map): this { + if (this._map) this.remove(); + + this._map = map; + if (this.options.closeOnClick) { + map.on('preclick', this._onClose); + } + + if (this.options.closeOnMove) { + map.on('move', this._onClose); + } + + map.on('remove', this.remove); + this._update(); + map._addPopup(this); + this._focusFirstElement(); + + if (this._trackPointer) { + map.on('mousemove', this._onMouseEvent); + map.on('mouseup', this._onMouseEvent); + map._canvasContainer.classList.add('mapboxgl-track-pointer'); + } else { + map.on('move', this._update); + } + + /** * Fired when the popup is opened manually or programatically. * * @event open @@ -182,23 +184,23 @@ export default class Popup extends Evented { * }); * */ - this.fire(new Event('open')); + this.fire(new Event('open')); - return this; - } + return this; + } - /** + /** * Checks if a popup is open. * * @returns {boolean} `true` if the popup is open, `false` if it is closed. * @example * const isPopupOpen = popup.isOpen(); */ - isOpen(): boolean { - return !!this._map; - } + isOpen(): boolean { + return !!this._map; + } - /** + /** * Removes the popup from the map it has been added to. * * @example @@ -206,34 +208,34 @@ export default class Popup extends Evented { * popup.remove(); * @returns {Popup} Returns itself to allow for method chaining. */ - remove(): this { - if (this._content) { - this._content.remove(); - } - - if (this._container) { - this._container.remove(); - this._container = undefined; - } - - const map = this._map; - if (map) { - map.off('move', this._update); - map.off('move', this._onClose); - map.off('preclick', this._onClose); - map.off('click', this._onClose); - map.off('remove', this.remove); - map.off('mousemove', this._onMouseEvent); - map.off('mouseup', this._onMouseEvent); - map.off('drag', this._onMouseEvent); - if (map._canvasContainer) { - map._canvasContainer.classList.remove('mapboxgl-track-pointer'); - } - map._removePopup(this); - this._map = undefined; - } - - /** + remove = (): this => { + if (this._content) { + this._content.remove(); + } + + if (this._container) { + this._container.remove(); + this._container = undefined; + } + + const map = this._map; + if (map) { + map.off('move', this._update); + map.off('move', this._onClose); + map.off('preclick', this._onClose); + map.off('click', this._onClose); + map.off('remove', this.remove); + map.off('mousemove', this._onMouseEvent); + map.off('mouseup', this._onMouseEvent); + map.off('drag', this._onMouseEvent); + if (map._canvasContainer) { + map._canvasContainer.classList.remove('mapboxgl-track-pointer'); + } + map._removePopup(this); + this._map = undefined; + } + + /** * Fired when the popup is closed manually or programatically. * * @event close @@ -252,12 +254,12 @@ export default class Popup extends Evented { * }); * */ - this.fire(new Event('close')); + this.fire(new Event('close')); - return this; - } + return this; + }; - /** + /** * Returns the geographical location of the popup's anchor. * * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously @@ -268,11 +270,11 @@ export default class Popup extends Evented { * @example * const lngLat = popup.getLngLat(); */ - getLngLat(): LngLat { - return this._lngLat; - } + getLngLat(): LngLat { + return this._lngLat; + } - /** + /** * Sets the geographical location of the popup's anchor, and moves the popup to it. Replaces trackPointer() behavior. * * @param {LngLatLike} lnglat The geographical location to set as the popup's anchor. @@ -280,25 +282,25 @@ export default class Popup extends Evented { * @example * popup.setLngLat([-122.4194, 37.7749]); */ - setLngLat(lnglat: LngLatLike): this { - this._lngLat = LngLat.convert(lnglat); - this._pos = null; + setLngLat(lnglat: LngLatLike): this { + this._lngLat = LngLat.convert(lnglat); + this._pos = null; - this._trackPointer = false; + this._trackPointer = false; - this._update(); + this._update(); - const map = this._map; - if (map) { - map.on('move', this._update); - map.off('mousemove', this._onMouseEvent); - map._canvasContainer.classList.remove('mapboxgl-track-pointer'); - } + const map = this._map; + if (map) { + map.on('move', this._update); + map.off('mousemove', this._onMouseEvent); + map._canvasContainer.classList.remove('mapboxgl-track-pointer'); + } - return this; - } + return this; + } - /** + /** * Tracks the popup anchor to the cursor position on screens with a pointer device (it will be hidden on touchscreens). Replaces the `setLngLat` behavior. * For most use cases, set `closeOnClick` and `closeButton` to `false`. * @@ -309,23 +311,22 @@ export default class Popup extends Evented { * .addTo(map); * @returns {Popup} Returns itself to allow for method chaining. */ - trackPointer(): this { - this._trackPointer = true; - this._pos = null; - this._update(); - const map = this._map; - if (map) { - map.off('move', this._update); - map.on('mousemove', this._onMouseEvent); - map.on('drag', this._onMouseEvent); - map._canvasContainer.classList.add('mapboxgl-track-pointer'); - } - - return this; - - } - - /** + trackPointer(): this { + this._trackPointer = true; + this._pos = null; + this._update(); + const map = this._map; + if (map) { + map.off('move', this._update); + map.on('mousemove', this._onMouseEvent); + map.on('drag', this._onMouseEvent); + map._canvasContainer.classList.add('mapboxgl-track-pointer'); + } + + return this; + } + + /** * Returns the `Popup`'s HTML element. * * @example @@ -338,11 +339,11 @@ export default class Popup extends Evented { * popupElem.style.fontSize = "25px"; * @returns {HTMLElement} Returns container element. */ - getElement(): ?HTMLElement { - return this._container; - } + getElement(): ?HTMLElement { + return this._container; + } - /** + /** * Sets the popup's content to a string of text. * * This function creates a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node in the DOM, @@ -357,11 +358,11 @@ export default class Popup extends Evented { * .setText('Hello, world!') * .addTo(map); */ - setText(text: string): this { - return this.setDOMContent(window.document.createTextNode(text)); - } + setText(text: string): this { + return this.setDOMContent(window.document.createTextNode(text)); + } - /** + /** * Sets the popup's content to the HTML provided as a string. * * This method does not perform HTML filtering or sanitization, and must be @@ -380,32 +381,32 @@ export default class Popup extends Evented { * @see [Example: Display a popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) * @see [Example: Attach a popup to a marker instance](https://docs.mapbox.com/mapbox-gl-js/example/set-popup/) */ - setHTML(html: string): this { - const frag = window.document.createDocumentFragment(); - const temp = window.document.createElement('body'); - let child; - temp.innerHTML = html; - while (true) { - child = temp.firstChild; - if (!child) break; - frag.appendChild(child); - } - - return this.setDOMContent(frag); - } - - /** + setHTML(html: string): this { + const frag = window.document.createDocumentFragment(); + const temp = window.document.createElement('body'); + let child; + temp.innerHTML = html; + while (true) { + child = temp.firstChild; + if (!child) break; + frag.appendChild(child); + } + + return this.setDOMContent(frag); + } + + /** * Returns the popup's maximum width. * * @returns {string} The maximum width of the popup. * @example * const maxWidth = popup.getMaxWidth(); */ - getMaxWidth(): ?string { - return this._container && this._container.style.maxWidth; - } + getMaxWidth(): ?string { + return this._container && this._container.style.maxWidth; + } - /** + /** * Sets the popup's maximum width. This is setting the CSS property `max-width`. * Available values can be found here: https://developer.mozilla.org/en-US/docs/Web/CSS/max-width. * @@ -414,13 +415,13 @@ export default class Popup extends Evented { * @example * popup.setMaxWidth('50'); */ - setMaxWidth(maxWidth: string): this { - this.options.maxWidth = maxWidth; - this._update(); - return this; - } + setMaxWidth(maxWidth: string): this { + this.options.maxWidth = maxWidth; + this._update(); + return this; + } - /** + /** * Sets the popup's content to the element provided as a DOM node. * * @param {Element} htmlNode A DOM node to be used as content for the popup. @@ -434,36 +435,44 @@ export default class Popup extends Evented { * .setDOMContent(div) * .addTo(map); */ - setDOMContent(htmlNode: Node): this { - let content = this._content; - if (content) { - // Clear out children first. - while (content.hasChildNodes()) { - if (content.firstChild) { - content.removeChild(content.firstChild); - } - } - } else { - content = this._content = DOM.create('div', 'mapboxgl-popup-content', this._container || undefined); - } - - // The close button should be the last tabbable element inside the popup for a good keyboard UX. - content.appendChild(htmlNode); - - if (this.options.closeButton) { - const button = this._closeButton = DOM.create('button', 'mapboxgl-popup-close-button', content); - button.type = 'button'; - button.setAttribute('aria-label', 'Close popup'); - button.setAttribute('aria-hidden', 'true'); - button.innerHTML = '×'; - button.addEventListener('click', this._onClose); - } - this._update(); - this._focusFirstElement(); - return this; - } - - /** + setDOMContent(htmlNode: Node): this { + let content = this._content; + if (content) { + // Clear out children first. + while (content.hasChildNodes()) { + if (content.firstChild) { + content.removeChild(content.firstChild); + } + } + } else { + content = this._content = DOM.create( + 'div', + 'mapboxgl-popup-content', + this._container || undefined, + ); + } + + // The close button should be the last tabbable element inside the popup for a good keyboard UX. + content.appendChild(htmlNode); + + if (this.options.closeButton) { + const button = this._closeButton = DOM.create( + 'button', + 'mapboxgl-popup-close-button', + content, + ); + button.type = 'button'; + button.setAttribute('aria-label', 'Close popup'); + button.setAttribute('aria-hidden', 'true'); + button.innerHTML = '×'; + button.addEventListener('click', this._onClose); + } + this._update(); + this._focusFirstElement(); + return this; + } + + /** * Adds a CSS class to the popup container element. * * @param {string} className Non-empty string with CSS class name to add to popup container. @@ -473,13 +482,13 @@ export default class Popup extends Evented { * const popup = new mapboxgl.Popup(); * popup.addClassName('some-class'); */ - addClassName(className: string): this { - this._classList.add(className); - this._updateClassList(); - return this; - } + addClassName(className: string): this { + this._classList.add(className); + this._updateClassList(); + return this; + } - /** + /** * Removes a CSS class from the popup container element. * * @param {string} className Non-empty string with CSS class name to remove from popup container. @@ -489,13 +498,13 @@ export default class Popup extends Evented { * const popup = new mapboxgl.Popup({className: 'some classes'}); * popup.removeClassName('some'); */ - removeClassName(className: string): this { - this._classList.delete(className); - this._updateClassList(); - return this; - } + removeClassName(className: string): this { + this._classList.delete(className); + this._updateClassList(); + return this; + } - /** + /** * Sets the popup's offset. * * @param {number | PointLike | Object} offset Sets the popup's offset. The `Object` is of the following structure @@ -515,13 +524,13 @@ export default class Popup extends Evented { * @example * popup.setOffset(10); */ - setOffset (offset?: Offset): this { - this.options.offset = offset; - this._update(); - return this; - } + setOffset(offset?: Offset): this { + this.options.offset = offset; + this._update(); + return this; + } - /** + /** * Add or remove the given CSS class on the popup container, depending on whether the container currently has that class. * * @param {string} className Non-empty string with CSS class name to add/remove. @@ -532,135 +541,150 @@ export default class Popup extends Evented { * const popup = new mapboxgl.Popup(); * popup.toggleClassName('highlighted'); */ - toggleClassName(className: string): boolean { - let finalState: boolean; - if (this._classList.delete(className)) { - finalState = false; - } else { - this._classList.add(className); - finalState = true; - } - this._updateClassList(); - return finalState; - } - - _onMouseEvent(event: MapMouseEvent) { - this._update(event.point); - } - - _getAnchor(bottomY: number): Anchor { - if (this.options.anchor) { return this.options.anchor; } - - const map = this._map; - const container = this._container; - const pos = this._pos; - - if (!map || !container || !pos) return 'bottom'; - - const width = container.offsetWidth; - const height = container.offsetHeight; - - const isTop = pos.y + bottomY < height; - const isBottom = pos.y > map.transform.height - height; - const isLeft = pos.x < width / 2; - const isRight = pos.x > map.transform.width - width / 2; - - if (isTop) { - if (isLeft) return 'top-left'; - if (isRight) return 'top-right'; - return 'top'; - } - if (isBottom) { - if (isLeft) return 'bottom-left'; - if (isRight) return 'bottom-right'; - } - if (isLeft) return 'left'; - if (isRight) return 'right'; - - return 'bottom'; - } - - _updateClassList() { - const container = this._container; - if (!container) return; - - const classes = [...this._classList]; - classes.push('mapboxgl-popup'); - if (this._anchor) { - classes.push(`mapboxgl-popup-anchor-${this._anchor}`); - } - if (this._trackPointer) { - classes.push('mapboxgl-popup-track-pointer'); - } - container.className = classes.join(' '); - } - - _update(cursor?: Point) { - const hasPosition = this._lngLat || this._trackPointer; - const map = this._map; - const content = this._content; - - if (!map || !hasPosition || !content) { return; } - - let container = this._container; - - if (!container) { - container = this._container = DOM.create('div', 'mapboxgl-popup', map.getContainer()); - this._tip = DOM.create('div', 'mapboxgl-popup-tip', container); - container.appendChild(content); - } - - if (this.options.maxWidth && container.style.maxWidth !== this.options.maxWidth) { - container.style.maxWidth = this.options.maxWidth; - } - - if (map.transform.renderWorldCopies && !this._trackPointer) { - this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); - } - - if (!this._trackPointer || cursor) { - const pos = this._pos = this._trackPointer && cursor ? cursor : map.project(this._lngLat); - - const offsetBottom = normalizeOffset(this.options.offset); - const anchor = this._anchor = this._getAnchor(offsetBottom.y); - const offset = normalizeOffset(this.options.offset, anchor); - - const offsetedPos = pos.add(offset).round(); - map._requestDomTask(() => { - if (this._container && anchor) { - this._container.style.transform = `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`; - } - }); - } - - if (!this._marker && map._showingGlobe()) { - const opacity = isLngLatBehindGlobe(map.transform, this._lngLat) ? 0 : 1; - this._setOpacity(opacity); - } - - this._updateClassList(); - } - - _focusFirstElement() { - if (!this.options.focusAfterOpen || !this._container) return; - - const firstFocusable = this._container.querySelector(focusQuerySelector); - - if (firstFocusable) firstFocusable.focus(); - } - - _onClose() { - this.remove(); - } - - _setOpacity(opacity: number) { - if (this._container) { - this._container.style.opacity = `${opacity}`; - } - if (this._content) { - this._content.style.pointerEvents = opacity ? 'auto' : 'none'; - } - } + toggleClassName(className: string): boolean { + let finalState: boolean; + if (this._classList.delete(className)) { + finalState = false; + } else { + this._classList.add(className); + finalState = true; + } + this._updateClassList(); + return finalState; + } + + _onMouseEvent = (event: MapMouseEvent) => { + this._update(event.point); + }; + + _getAnchor(bottomY: number): Anchor { + if (this.options.anchor) { + return this.options.anchor; + } + + const map = this._map; + const container = this._container; + const pos = this._pos; + + if (!map || !container || !pos) return 'bottom'; + + const width = container.offsetWidth; + const height = container.offsetHeight; + + const isTop = pos.y + bottomY < height; + const isBottom = pos.y > map.transform.height - height; + const isLeft = pos.x < width / 2; + const isRight = pos.x > map.transform.width - width / 2; + + if (isTop) { + if (isLeft) return 'top-left'; + if (isRight) return 'top-right'; + return 'top'; + } + if (isBottom) { + if (isLeft) return 'bottom-left'; + if (isRight) return 'bottom-right'; + } + if (isLeft) return 'left'; + if (isRight) return 'right'; + + return 'bottom'; + } + + _updateClassList() { + const container = this._container; + if (!container) return; + + const classes = [...this._classList]; + classes.push('mapboxgl-popup'); + if (this._anchor) { + classes.push(`mapboxgl-popup-anchor-${this._anchor}`); + } + if (this._trackPointer) { + classes.push('mapboxgl-popup-track-pointer'); + } + container.className = classes.join(' '); + } + + _update = (cursor?: Point) => { + const hasPosition = this._lngLat || this._trackPointer; + const map = this._map; + const content = this._content; + + if (!map || !hasPosition || !content) { + return; + } + + let container = this._container; + + if (!container) { + container = this._container = DOM.create( + 'div', + 'mapboxgl-popup', + map.getContainer(), + ); + this._tip = DOM.create('div', 'mapboxgl-popup-tip', container); + container.appendChild(content); + } + + if ( + this.options.maxWidth && + container.style.maxWidth !== this.options.maxWidth + ) { + container.style.maxWidth = this.options.maxWidth; + } + + if (map.transform.renderWorldCopies && !this._trackPointer) { + this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); + } + + if (!this._trackPointer || cursor) { + const pos = this._pos = this._trackPointer && cursor ? + cursor : + map.project(this._lngLat); + + const offsetBottom = normalizeOffset(this.options.offset); + const anchor = this._anchor = this._getAnchor(offsetBottom.y); + const offset = normalizeOffset(this.options.offset, anchor); + + const offsetedPos = pos.add(offset).round(); + map._requestDomTask( + () => { + if (this._container && anchor) { + this._container.style.transform = `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`; + } + }, + ); + } + + if (!this._marker && map._showingGlobe()) { + const opacity = isLngLatBehindGlobe(map.transform, this._lngLat) ? 0 : 1; + this._setOpacity(opacity); + } + + this._updateClassList(); + }; + + _focusFirstElement() { + if (!this.options.focusAfterOpen || !this._container) return; + + const firstFocusable = this._container.querySelector(focusQuerySelector); + + if (firstFocusable) firstFocusable.focus(); + } + + _onClose = () => { + this.remove(); + }; + + _setOpacity(opacity: number) { + if (this._container) { + this._container.style.opacity = `${opacity}`; + } + if (this._content) { + this._content.style.pointerEvents = opacity ? 'auto' : 'none'; + } + } } // returns a normalized offset for a given anchor diff --git a/src/util/actor.js b/src/util/actor.js index dce203ab9cd..19b40da46ab 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -20,28 +20,28 @@ import type {Cancelable} from '../types/cancelable.js'; * @private */ class Actor { - target: any; - parent: any; - mapId: ?number; - callbacks: { number: any }; - name: string; - cancelCallbacks: { number: Cancelable }; - globalScope: any; - scheduler: Scheduler; + target: any; + parent: any; + mapId: ?number; + callbacks: { number: any }; + name: string; + cancelCallbacks: { number: Cancelable }; + globalScope: any; + scheduler: Scheduler; - constructor(target: any, parent: any, mapId: ?number) { - this.target = target; - this.parent = parent; - this.mapId = mapId; - this.callbacks = {}; - this.cancelCallbacks = {}; - bindAll(['receive'], this); - this.target.addEventListener('message', this.receive, false); - this.globalScope = isWorker() ? target : window; - this.scheduler = new Scheduler(); - } + constructor(target: any, parent: any, mapId: ?number) { + this.target = target; + this.parent = parent; + this.mapId = mapId; + this.callbacks = {}; + this.cancelCallbacks = {}; + bindAll(['receive'], this); + this.target.addEventListener('message', this.receive, false); + this.globalScope = isWorker() ? target : window; + this.scheduler = new Scheduler(); + } - /** + /** * Sends a message from a main-thread map to a Worker or from a Worker back to * a main-thread map instance. * @@ -49,129 +49,156 @@ class Actor { * @param targetMapId A particular mapId to which to send this message. * @private */ - send(type: string, data: mixed, callback: ?Function, targetMapId: ?string, mustQueue: boolean = false, callbackMetadata?: Object): ?Cancelable { - // We're using a string ID instead of numbers because they are being used as object keys - // anyway, and thus stringified implicitly. We use random IDs because an actor may receive - // message from multiple other actors which could run in different execution context. A - // linearly increasing ID could produce collisions. - const id = Math.round((Math.random() * 1e18)).toString(36).substring(0, 10); - if (callback) { - callback.metadata = callbackMetadata; - this.callbacks[id] = callback; - } - const buffers: ?Array = isSafari(this.globalScope) ? undefined : []; - this.target.postMessage({ - id, - type, - hasCallback: !!callback, - targetMapId, - mustQueue, - sourceMapId: this.mapId, - data: serialize(data, buffers) - }, buffers); - return { - cancel: () => { - if (callback) { - // Set the callback to null so that it never fires after the request is aborted. - delete this.callbacks[id]; - } - this.target.postMessage({ - id, - type: '', - targetMapId, - sourceMapId: this.mapId - }); - } - }; - } + send( + type: string, + data: mixed, + callback: ?Function, + targetMapId: ?string, + mustQueue: boolean = false, + callbackMetadata?: Object, + ): ?Cancelable { + // We're using a string ID instead of numbers because they are being used as object keys + // anyway, and thus stringified implicitly. We use random IDs because an actor may receive + // message from multiple other actors which could run in different execution context. A + // linearly increasing ID could produce collisions. + const id = Math.round(Math.random() * 1e18).toString(36).substring(0, 10); + if (callback) { + callback.metadata = callbackMetadata; + this.callbacks[id] = callback; + } + const buffers: ?Array = isSafari(this.globalScope) ? + undefined : + []; + this.target.postMessage( + { + id, + type, + hasCallback: !!callback, + targetMapId, + mustQueue, + sourceMapId: this.mapId, + data: serialize(data, buffers), + }, + buffers, + ); + return { + cancel: () => { + if (callback) { + // Set the callback to null so that it never fires after the request is aborted. + delete this.callbacks[id]; + } + this.target.postMessage( + { + id, + type: '', + targetMapId, + sourceMapId: this.mapId, + }, + ); + }, + }; + } - receive(message: Object) { - const data = message.data, - id = data.id; + receive = (message: Object) => { + const data = message.data, + id = data.id; - if (!id) { - return; - } + if (!id) { + return; + } - if (data.targetMapId && this.mapId !== data.targetMapId) { - return; - } + if (data.targetMapId && this.mapId !== data.targetMapId) { + return; + } - if (data.type === '') { - // Remove the original request from the queue. This is only possible if it - // hasn't been kicked off yet. The id will remain in the queue, but because - // there is no associated task, it will be dropped once it's time to execute it. - const cancel = this.cancelCallbacks[id]; - delete this.cancelCallbacks[id]; - if (cancel) { - cancel.cancel(); - } - } else { - if (data.mustQueue || isWorker()) { - // for worker tasks that are often cancelled, such as loadTile, store them before actually - // processing them. This is necessary because we want to keep receiving messages. - // Some tasks may take a while in the worker thread, so before executing the next task - // in our queue, postMessage preempts this and messages can be processed. - // We're using a MessageChannel object to get throttle the process() flow to one at a time. - const callback = this.callbacks[id]; - const metadata = (callback && callback.metadata) || {type: "message"}; - this.cancelCallbacks[id] = this.scheduler.add(() => this.processTask(id, data), metadata); - } else { - // In the main thread, process messages immediately so that other work does not slip in - // between getting partial data back from workers. - this.processTask(id, data); - } - } - } + if (data.type === '') { + // Remove the original request from the queue. This is only possible if it + // hasn't been kicked off yet. The id will remain in the queue, but because + // there is no associated task, it will be dropped once it's time to execute it. + const cancel = this.cancelCallbacks[id]; + delete this.cancelCallbacks[id]; + if (cancel) { + cancel.cancel(); + } + } else { + if (data.mustQueue || isWorker()) { + // for worker tasks that are often cancelled, such as loadTile, store them before actually + // processing them. This is necessary because we want to keep receiving messages. + // Some tasks may take a while in the worker thread, so before executing the next task + // in our queue, postMessage preempts this and messages can be processed. + // We're using a MessageChannel object to get throttle the process() flow to one at a time. + const callback = this.callbacks[id]; + const metadata = callback && callback.metadata || {type: "message"}; + this.cancelCallbacks[id] = this.scheduler.add( + () => this.processTask(id, data), + metadata, + ); + } else { + // In the main thread, process messages immediately so that other work does not slip in + // between getting partial data back from workers. + this.processTask(id, data); + } + } + }; - processTask(id: number, task: any) { - if (task.type === '') { - // The done() function in the counterpart has been called, and we are now - // firing the callback in the originating actor, if there is one. - const callback = this.callbacks[id]; - delete this.callbacks[id]; - if (callback) { - // If we get a response, but don't have a callback, the request was canceled. - if (task.error) { - callback(deserialize(task.error)); - } else { - callback(null, deserialize(task.data)); - } - } - } else { - const buffers: ?Array = isSafari(this.globalScope) ? undefined : []; - const done = task.hasCallback ? (err, data) => { - delete this.cancelCallbacks[id]; - this.target.postMessage({ - id, - type: '', - sourceMapId: this.mapId, - error: err ? serialize(err) : null, - data: serialize(data, buffers) - }, buffers); - } : (_) => { - }; + processTask(id: number, task: any) { + if (task.type === '') { + // The done() function in the counterpart has been called, and we are now + // firing the callback in the originating actor, if there is one. + const callback = this.callbacks[id]; + delete this.callbacks[id]; + if (callback) { + // If we get a response, but don't have a callback, the request was canceled. + if (task.error) { + callback(deserialize(task.error)); + } else { + callback(null, deserialize(task.data)); + } + } + } else { + const buffers: ?Array = isSafari(this.globalScope) ? + undefined : + []; + const done = task.hasCallback ? + (err, data) => { + delete this.cancelCallbacks[id]; + this.target.postMessage( + { + id, + type: '', + sourceMapId: this.mapId, + error: err ? serialize(err) : null, + data: serialize(data, buffers), + }, + buffers, + ); + } : + _ => {}; - const params = (deserialize(task.data): any); - if (this.parent[task.type]) { - // task.type == 'loadTile', 'removeTile', etc. - this.parent[task.type](task.sourceMapId, params, done); - } else if (this.parent.getWorkerSource) { - // task.type == sourcetype.method - const keys = task.type.split('.'); - const scope = (this.parent: any).getWorkerSource(task.sourceMapId, keys[0], params.source); - scope[keys[1]](params, done); - } else { - // No function was found. - done(new Error(`Could not find function ${task.type}`)); - } - } - } + const params = (deserialize(task.data): any); + if (this.parent[task.type]) { + // task.type == 'loadTile', 'removeTile', etc. + this.parent[task.type](task.sourceMapId, params, done); + } else if (this.parent.getWorkerSource) { + // task.type == sourcetype.method + const keys = task.type.split('.'); + const scope = (this.parent: any).getWorkerSource( + task.sourceMapId, + keys[0], + params.source, + ); + scope[keys[1]](params, done); + } else { + // No function was found. + done(new Error(`Could not find function ${task.type}`)); + } + } + } - remove() { - this.scheduler.remove(); - this.target.removeEventListener('message', this.receive, false); - } + remove() { + this.scheduler.remove(); + this.target.removeEventListener('message', this.receive, false); + } } export default Actor; diff --git a/src/util/image.js b/src/util/image.js index 1fa1caa0a55..673d98ccaad 100644 --- a/src/util/image.js +++ b/src/util/image.js @@ -4,9 +4,9 @@ import assert from 'assert'; import {register} from './web_worker_transfer.js'; -export type Size = { - width: number, - height: number +export type Size = interface { + width: number, + height: number, }; export interface SpritePosition { diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 824858f11c1..e7677fded36 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -413,231 +413,285 @@ class TelemetryEvent { } } -export class PerformanceEvent extends TelemetryEvent { +export class PerformanceEvent + extends TelemetryEvent { constructor() { super('gljs.performance'); } - postPerformanceEvent(customAccessToken: ?string, performanceData: LivePerformanceData) { - if (config.EVENTS_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest({timestamp: Date.now(), performanceData}, customAccessToken); - } - } - } - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) { - return; - } - - const {timestamp, performanceData} = this.queue.shift(); - - const additionalPayload = getLivePerformanceMetrics(performanceData); - - // Server will only process string for these entries - for (const metadata of additionalPayload.metadata) { - assert(typeof metadata.value === 'string'); - } - for (const counter of additionalPayload.counters) { - assert(typeof counter.value === 'string'); - } - for (const attribute of additionalPayload.attributes) { - assert(typeof attribute.value === 'string'); - } - - this.postEvent(timestamp, additionalPayload, () => {}, customAccessToken); - } + postPerformanceEvent = ( + customAccessToken: ?string, + performanceData: LivePerformanceData, + ) => { + if (config.EVENTS_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest( + {timestamp: Date.now(), performanceData}, + customAccessToken, + ); + } + } + }; + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) { + return; + } + + const {timestamp, performanceData} = this.queue.shift(); + + const additionalPayload = getLivePerformanceMetrics(performanceData); + + // Server will only process string for these entries + for (const metadata of additionalPayload.metadata) { + assert(typeof metadata.value === 'string'); + } + for (const counter of additionalPayload.counters) { + assert(typeof counter.value === 'string'); + } + for (const attribute of additionalPayload.attributes) { + assert(typeof attribute.value === 'string'); + } + + this.postEvent(timestamp, additionalPayload, () => {}, customAccessToken); + } } -export class MapLoadEvent extends TelemetryEvent { - +success: {[_: number]: boolean}; - skuToken: string; - errorCb: EventCallback; - - constructor() { - super('map.load'); - this.success = {}; - this.skuToken = ''; - } - - postMapLoadEvent(mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) { - this.skuToken = skuToken; - this.errorCb = callback; - - if (config.EVENTS_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); - } else { - this.errorCb(new Error(AUTH_ERR_MSG)); - } - } - } - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) return; - const {id, timestamp} = this.queue.shift(); - - // Only one load event should fire per map - if (id && this.success[id]) return; - - if (!this.anonId) { - this.fetchEventData(); - } - - if (!validateUuid(this.anonId)) { - this.anonId = uuid(); - } - - const additionalPayload = { - sdkIdentifier: 'mapbox-gl-js', - sdkVersion, - skuId: SKU_ID, - skuToken: this.skuToken, - userId: this.anonId - }; - - this.postEvent(timestamp, additionalPayload, (err) => { - if (err) { - this.errorCb(err); - } else { - if (id) this.success[id] = true; - } - - }, customAccessToken); - } +export class MapLoadEvent + extends TelemetryEvent { + +success: { [_: number]: boolean }; + skuToken: string; + errorCb: EventCallback; + + constructor() { + super('map.load'); + this.success = {}; + this.skuToken = ''; + } + + postMapLoadEvent = ( + mapId: number, + skuToken: string, + customAccessToken: ?string, + callback: EventCallback, + ) => { + this.skuToken = skuToken; + this.errorCb = callback; + + if (config.EVENTS_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest( + {id: mapId, timestamp: Date.now()}, + customAccessToken, + ); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } + }; + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) return; + const {id, timestamp} = this.queue.shift(); + + // Only one load event should fire per map + if (id && this.success[id]) return; + + if (!this.anonId) { + this.fetchEventData(); + } + + if (!validateUuid(this.anonId)) { + this.anonId = uuid(); + } + + const additionalPayload = { + sdkIdentifier: 'mapbox-gl-js', + sdkVersion, + skuId: SKU_ID, + skuToken: this.skuToken, + userId: this.anonId, + }; + + this.postEvent( + timestamp, + additionalPayload, + err => { + if (err) { + this.errorCb(err); + } else { + if (id) this.success[id] = true; + } + }, + customAccessToken, + ); + } } -export class MapSessionAPI extends TelemetryEvent { - +success: {[_: number]: boolean}; - skuToken: string; - errorCb: EventCallback; - - constructor() { - super('map.auth'); - this.success = {}; - this.skuToken = ''; - } - - getSession(timestamp: number, token: string, callback: EventCallback, customAccessToken?: ?string) { - if (!config.API_URL || !config.SESSION_PATH) return; - const authUrlObject: UrlObject = parseUrl(config.API_URL + config.SESSION_PATH); - authUrlObject.params.push(`sku=${token || ''}`); - authUrlObject.params.push(`access_token=${customAccessToken || config.ACCESS_TOKEN || ''}`); - - const request: RequestParameters = { - url: formatUrl(authUrlObject), - headers: { - 'Content-Type': 'text/plain', //Skip the pre-flight OPTIONS request - } - }; - - this.pendingRequest = getData(request, (error) => { - this.pendingRequest = null; - callback(error); - this.saveEventData(); - this.processRequests(customAccessToken); - }); - } - - getSessionAPI(mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) { - this.skuToken = skuToken; - this.errorCb = callback; - - if (config.SESSION_PATH && config.API_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); - } else { - this.errorCb(new Error(AUTH_ERR_MSG)); - } - } - } - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) return; - const {id, timestamp} = this.queue.shift(); - - // Only one load event should fire per map - if (id && this.success[id]) return; - - this.getSession(timestamp, this.skuToken, (err) => { - if (err) { - this.errorCb(err); - } else { - if (id) this.success[id] = true; - } - }, customAccessToken); - } +export class MapSessionAPI + extends TelemetryEvent { + +success: { [_: number]: boolean }; + skuToken: string; + errorCb: EventCallback; + + constructor() { + super('map.auth'); + this.success = {}; + this.skuToken = ''; + } + + getSession( + timestamp: number, + token: string, + callback: EventCallback, + customAccessToken?: ?string, + ) { + if (!config.API_URL || !config.SESSION_PATH) return; + const authUrlObject: UrlObject = parseUrl( + config.API_URL + config.SESSION_PATH, + ); + authUrlObject.params.push(`sku=${token || ''}`); + authUrlObject.params.push(`access_token=${customAccessToken || + config.ACCESS_TOKEN || + ''}`,); + + const request: RequestParameters = { + url: formatUrl(authUrlObject), + headers: { + 'Content-Type': 'text/plain' //Skip the pre-flight OPTIONS request + , + }, + }; + + this.pendingRequest = getData( + request, + error => { + this.pendingRequest = null; + callback(error); + this.saveEventData(); + this.processRequests(customAccessToken); + }, + ); + } + + getSessionAPI = ( + mapId: number, + skuToken: string, + customAccessToken: ?string, + callback: EventCallback, + ) => { + this.skuToken = skuToken; + this.errorCb = callback; + + if (config.SESSION_PATH && config.API_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest( + {id: mapId, timestamp: Date.now()}, + customAccessToken, + ); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } + }; + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) return; + const {id, timestamp} = this.queue.shift(); + + // Only one load event should fire per map + if (id && this.success[id]) return; + + this.getSession( + timestamp, + this.skuToken, + err => { + if (err) { + this.errorCb(err); + } else { + if (id) this.success[id] = true; + } + }, + customAccessToken, + ); + } } -export class TurnstileEvent extends TelemetryEvent { +export class TurnstileEvent + extends TelemetryEvent { constructor(customAccessToken?: ?string) { super('appUserTurnstile'); this._customAccessToken = customAccessToken; } - postTurnstileEvent(tileUrls: Array, customAccessToken?: ?string) { - //Enabled only when Mapbox Access Token is set and a source uses - // mapbox tiles. - if (config.EVENTS_URL && - config.ACCESS_TOKEN && - Array.isArray(tileUrls) && - tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) { - this.queueRequest(Date.now(), customAccessToken); - } - } - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) { - return; - } - - if (!this.anonId || !this.eventData.lastSuccess || !this.eventData.tokenU) { - //Retrieve cached data - this.fetchEventData(); - } - - const tokenData = parseAccessToken(config.ACCESS_TOKEN); - const tokenU = tokenData ? tokenData['u'] : config.ACCESS_TOKEN; - //Reset event data cache if the access token owner changed. - let dueForEvent = tokenU !== this.eventData.tokenU; - - if (!validateUuid(this.anonId)) { - this.anonId = uuid(); - dueForEvent = true; - } - - const nextUpdate = this.queue.shift(); - // Record turnstile event once per calendar day. - if (this.eventData.lastSuccess) { - const lastUpdate = new Date(this.eventData.lastSuccess); - const nextDate = new Date(nextUpdate); - const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); - dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate(); - } else { - dueForEvent = true; - } - - if (!dueForEvent) { - this.processRequests(); - return; - } - - const additionalPayload = { - sdkIdentifier: 'mapbox-gl-js', - sdkVersion, - skuId: SKU_ID, - "enabled.telemetry": false, - userId: this.anonId - }; - - this.postEvent(nextUpdate, additionalPayload, (err) => { - if (!err) { - this.eventData.lastSuccess = nextUpdate; - this.eventData.tokenU = tokenU; - } - }, customAccessToken); - } + postTurnstileEvent = (tileUrls: Array, customAccessToken?: ?string) => { + //Enabled only when Mapbox Access Token is set and a source uses + // mapbox tiles. + if ( + config.EVENTS_URL && config.ACCESS_TOKEN && Array.isArray(tileUrls) && + tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url)) + ) { + this.queueRequest(Date.now(), customAccessToken); + } + }; + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) { + return; + } + + if (!this.anonId || !this.eventData.lastSuccess || !this.eventData.tokenU) { + //Retrieve cached data + this.fetchEventData(); + } + + const tokenData = parseAccessToken(config.ACCESS_TOKEN); + const tokenU = tokenData ? tokenData['u'] : config.ACCESS_TOKEN; + //Reset event data cache if the access token owner changed. + let dueForEvent = tokenU !== this.eventData.tokenU; + + if (!validateUuid(this.anonId)) { + this.anonId = uuid(); + dueForEvent = true; + } + + const nextUpdate = this.queue.shift(); + // Record turnstile event once per calendar day. + if (this.eventData.lastSuccess) { + const lastUpdate = new Date(this.eventData.lastSuccess); + const nextDate = new Date(nextUpdate); + const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); + dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || + lastUpdate.getDate() !== nextDate.getDate(); + } else { + dueForEvent = true; + } + + if (!dueForEvent) { + this.processRequests(); + return; + } + + const additionalPayload = { + sdkIdentifier: 'mapbox-gl-js', + sdkVersion, + skuId: SKU_ID, + "enabled.telemetry": false, + userId: this.anonId, + }; + + this.postEvent( + nextUpdate, + additionalPayload, + err => { + if (!err) { + this.eventData.lastSuccess = nextUpdate; + this.eventData.tokenU = tokenU; + } + }, + customAccessToken, + ); + } } const turnstileEvent_ = new TurnstileEvent(); diff --git a/src/util/scheduler.js b/src/util/scheduler.js index c4278ef1983..c9ffb4d622c 100644 --- a/src/util/scheduler.js +++ b/src/util/scheduler.js @@ -22,98 +22,101 @@ type Task = { }; class Scheduler { - - tasks: { [number]: Task }; - taskQueue: Array; - invoker: ThrottledInvoker; - nextId: number; - - constructor() { - this.tasks = {}; - this.taskQueue = []; - bindAll(['process'], this); - this.invoker = new ThrottledInvoker(this.process); - - this.nextId = 0; - } - - add(fn: TaskFunction, metadata: TaskMetadata): Cancelable { - const id = this.nextId++; - const priority = getPriority(metadata); - - if (priority === 0) { - // Process tasks with priority 0 immediately. Do not yield to the event loop. - const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; - try { - fn(); - } finally { - if (m) PerformanceUtils.endMeasure(m); - } - return { - cancel: () => {} - }; - } - - this.tasks[id] = {fn, metadata, priority, id}; - this.taskQueue.push(id); - this.invoker.trigger(); - return { - cancel: () => { - delete this.tasks[id]; - } - }; - } - - process() { - const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; - try { - this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); - - if (!this.taskQueue.length) { - return; - } - const id = this.pick(); - if (id === null) return; - - const task = this.tasks[id]; - delete this.tasks[id]; - // Schedule another process call if we know there's more to process _before_ invoking the - // current task. This is necessary so that processing continues even if the current task - // doesn't execute successfully. - if (this.taskQueue.length) { - this.invoker.trigger(); - } - if (!task) { - // If the task ID doesn't have associated task data anymore, it was canceled. - return; - } - - task.fn(); - } finally { - if (m) PerformanceUtils.endMeasure(m); - } - } - - pick(): null | number { - let minIndex = null; - let minPriority = Infinity; - for (let i = 0; i < this.taskQueue.length; i++) { - const id = this.taskQueue[i]; - const task = this.tasks[id]; - if (task.priority < minPriority) { - minPriority = task.priority; - minIndex = i; - } - } - if (minIndex === null) return null; - const id = this.taskQueue[minIndex]; - this.taskQueue.splice(minIndex, 1); - return id; - } - - remove() { - this.invoker.remove(); - } + tasks: { [number]: Task }; + taskQueue: Array; + invoker: ThrottledInvoker; + nextId: number; + + constructor() { + this.tasks = {}; + this.taskQueue = []; + bindAll(['process'], this); + this.invoker = new ThrottledInvoker(this.process); + + this.nextId = 0; + } + + add(fn: TaskFunction, metadata: TaskMetadata): Cancelable { + const id = this.nextId++; + const priority = getPriority(metadata); + + if (priority === 0) { + // Process tasks with priority 0 immediately. Do not yield to the event loop. + const m = isWorker() ? + PerformanceUtils.beginMeasure('workerTask') : + undefined; + try { + fn(); + } finally { + if (m) PerformanceUtils.endMeasure(m); + } + return { + cancel: () => {}, + }; + } + + this.tasks[id] = {fn, metadata, priority, id}; + this.taskQueue.push(id); + this.invoker.trigger(); + return { + cancel: () => { + delete this.tasks[id]; + }, + }; + } + + process = () => { + const m = isWorker() ? + PerformanceUtils.beginMeasure('workerTask') : + undefined; + try { + this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); + + if (!this.taskQueue.length) { + return; + } + const id = this.pick(); + if (id === null) return; + + const task = this.tasks[id]; + delete this.tasks[id]; + // Schedule another process call if we know there's more to process _before_ invoking the + // current task. This is necessary so that processing continues even if the current task + // doesn't execute successfully. + if (this.taskQueue.length) { + this.invoker.trigger(); + } + if (!task) { + // If the task ID doesn't have associated task data anymore, it was canceled. + return; + } + + task.fn(); + } finally { + if (m) PerformanceUtils.endMeasure(m); + } + }; + + pick(): null | number { + let minIndex = null; + let minPriority = Infinity; + for (let i = 0; i < this.taskQueue.length; i++) { + const id = this.taskQueue[i]; + const task = this.tasks[id]; + if (task.priority < minPriority) { + minPriority = task.priority; + minIndex = i; + } + } + if (minIndex === null) return null; + const id = this.taskQueue[minIndex]; + this.taskQueue.splice(minIndex, 1); + return id; + } + + remove() { + this.invoker.remove(); + } } function getPriority({type, isSymbolTile, zoom}: TaskMetadata): number { diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index 68a11c99596..dd17f11586e 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -13,7 +13,7 @@ import {AJAXError} from './ajax.js'; import type {Transferable} from '../types/transferable.js'; -type SerializedObject = {[_: string]: Serialized }; +type SerializedObject = interface { [_: string]: Serialized }; export type Serialized = | null | void From d91278f17de7ceebf492180581bf04de1d586040 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 27 Jan 2023 16:42:15 +0200 Subject: [PATCH 04/72] Fix flow --- src/render/uniform_binding.js | 2 +- src/source/custom_source.js | 6 +- src/source/source_cache.js | 4 +- src/source/worker.js | 2 +- src/style/style.js | 2 +- src/style/style_layer/custom_style_layer.js | 2 +- src/symbol/grid_index.js | 226 +++++++++++--------- src/terrain/terrain.js | 4 +- src/ui/camera.js | 2 +- src/ui/control/attribution_control.js | 8 +- src/ui/control/fullscreen_control.js | 4 +- src/ui/control/geolocate_control.js | 16 +- src/ui/control/logo_control.js | 4 +- src/ui/control/navigation_control.js | 18 +- src/ui/control/scale_control.js | 15 +- src/ui/handler/scroll_zoom.js | 2 +- src/ui/handler_manager.js | 4 +- src/ui/hash.js | 4 +- src/ui/map.js | 6 +- src/ui/marker.js | 16 +- src/ui/popup.js | 8 +- src/util/actor.js | 2 +- src/util/mapbox.js | 18 +- src/util/scheduler.js | 2 +- 24 files changed, 207 insertions(+), 170 deletions(-) diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index bcc5fbe54d3..fabadb7226b 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -17,7 +17,7 @@ class Uniform { this.initialized = false; } - fetchUniformLocation = (program: WebGLProgram, name: string): boolean => { + fetchUniformLocation: ((program: WebGLProgram, name: string) => boolean) = (program: WebGLProgram, name: string): boolean => { if (!this.location && !this.initialized) { this.location = this.gl.getUniformLocation(program, name); this.initialized = true; diff --git a/src/source/custom_source.js b/src/source/custom_source.js index 0ce544a10c6..d4f7190a506 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -382,7 +382,7 @@ class CustomSource return false; } - _coveringTiles = (): Array<{ z: number, x: number, y: number }> => { + _coveringTiles: (() => Array<{ x: number, y: number, z: number, ... }>) = (): Array<{ z: number, x: number, y: number }> => { const tileIDs = this._map.transform.coveringTiles( { tileSize: this.tileSize, @@ -401,11 +401,11 @@ class CustomSource ); }; - _clearTiles = () => { + _clearTiles: (() => void) = () => { this._map.style._clearSource(this.id); }; - _update = () => { + _update: (() => void) = () => { this.fire( new Event('data', {dataType: 'source', sourceDataType: 'content'}), ); diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 1e3cf7b7911..036d8244237 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -170,7 +170,7 @@ class SourceCache return this._source.loadTile(tile, callback); } - _unloadTile = (tile: Tile): void => { + _unloadTile: ((tile: Tile) => void) = (tile: Tile): void => { if (this._source.unloadTile) return this._source.unloadTile(tile, () => {}); }; @@ -282,7 +282,7 @@ class SourceCache this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); } - _tileLoaded = (tile: Tile, id: number, previousState: TileState, err: ?Error) => { + _tileLoaded: ((tile: Tile, id: number, previousState: TileState, err: ?Error) => void) = (tile: Tile, id: number, previousState: TileState, err: ?Error) => { if (err) { tile.state = 'errored'; if ((err: any).status !== 404) diff --git a/src/source/worker.js b/src/source/worker.js index 91d4ddb2951..0c64b0184f4 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -135,7 +135,7 @@ export default class Worker { callback(); } - enableTerrain = (mapId: string, enable: boolean, callback: WorkerTileCallback) => { + enableTerrain: ((mapId: string, enable: boolean, callback: WorkerTileCallback) => void) = (mapId: string, enable: boolean, callback: WorkerTileCallback) => { this.terrain = enable; callback(); }; diff --git a/src/style/style.js b/src/style/style.js index 846b7546ff2..f61730b4f88 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -2210,7 +2210,7 @@ class Style return this._otherSourceCaches[source]; } - _getLayerSourceCache = (layer: StyleLayer): SourceCache | void => { + _getLayerSourceCache: ((layer: StyleLayer) => SourceCache | void) = (layer: StyleLayer): SourceCache | void => { return layer.type === 'symbol' ? this._symbolSourceCaches[layer.source] : this._otherSourceCaches[layer.source]; diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index 9d7b9397825..69de5180aa1 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -225,7 +225,7 @@ class CustomStyleLayer assert(false, "Custom layers cannot be serialized"); } - onAdd = (map: Map) => { + onAdd: ((map: Map) => void) = (map: Map) => { if (this.implementation.onAdd) { this.implementation.onAdd(map, map.painter.context.gl); } diff --git a/src/symbol/grid_index.js b/src/symbol/grid_index.js index b0f7b74368d..c30ce2ae2e0 100644 --- a/src/symbol/grid_index.js +++ b/src/symbol/grid_index.js @@ -94,27 +94,41 @@ class GridIndex { this.circles.push(radius); } - _insertBoxCell = ( + _insertBoxCell: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number +) => void) = ( x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number, - ) => { - this.boxCells[cellIndex].push(uid); - }; +) => { + this.boxCells[cellIndex].push(uid); +}; - _insertCircleCell = ( + _insertCircleCell: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number +) => void) = ( x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number, - ) => { - this.circleCells[cellIndex].push(uid); - }; +) => { + this.circleCells[cellIndex].push(uid); +}; _query( x1: number, @@ -240,7 +254,16 @@ class GridIndex { return (this._queryCircle(x, y, radius, true, predicate): any); } - _queryCell = ( + _queryCell: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any +) => void | boolean) = ( x1: number, y1: number, x2: number, @@ -249,26 +272,26 @@ class GridIndex { result: any, queryArgs: any, predicate?: any, - ): void | boolean => { - const seenUids = queryArgs.seenUids; - const boxCell = this.boxCells[cellIndex]; - if (boxCell !== null) { - const bboxes = this.bboxes; - for (const boxUid of boxCell) { - if (!seenUids.box[boxUid]) { - seenUids.box[boxUid] = true; - const offset = boxUid * 4; - if ( - x1 <= bboxes[offset + 2] && y1 <= bboxes[offset + 3] && +): void | boolean => { + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if ( + x1 <= bboxes[offset + 2] && y1 <= bboxes[offset + 3] && x2 >= bboxes[offset + 0] && y2 >= bboxes[offset + 1] && (!predicate || predicate(this.boxKeys[boxUid])) - ) { - if (queryArgs.hitTest) { - result.push(true); - return true; - } else { - result.push( + ) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } else { + result.push( { key: this.boxKeys[boxUid], x1: bboxes[offset], @@ -276,21 +299,21 @@ class GridIndex { x2: bboxes[offset + 2], y2: bboxes[offset + 3], }, - ); - } - } - } - } - } - const circleCell = this.circleCells[cellIndex]; - if (circleCell !== null) { - const circles = this.circles; - for (const circleUid of circleCell) { - if (!seenUids.circle[circleUid]) { - seenUids.circle[circleUid] = true; - const offset = circleUid * 3; - if ( - this._circleAndRectCollide( + ); + } + } + } + } + } + const circleCell = this.circleCells[cellIndex]; + if (circleCell !== null) { + const circles = this.circles; + for (const circleUid of circleCell) { + if (!seenUids.circle[circleUid]) { + seenUids.circle[circleUid] = true; + const offset = circleUid * 3; + if ( + this._circleAndRectCollide( circles[offset], circles[offset + 1], circles[offset + 2], @@ -298,17 +321,17 @@ class GridIndex { y1, x2, y2, - ) && + ) && (!predicate || predicate(this.circleKeys[circleUid])) - ) { - if (queryArgs.hitTest) { - result.push(true); - return true; - } else { - const x = circles[offset]; - const y = circles[offset + 1]; - const radius = circles[offset + 2]; - result.push( + ) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } else { + const x = circles[offset]; + const y = circles[offset + 1]; + const radius = circles[offset + 2]; + result.push( { key: this.circleKeys[circleUid], x1: x - radius, @@ -316,15 +339,24 @@ class GridIndex { x2: x + radius, y2: y + radius, }, - ); - } - } - } - } - } - }; + ); + } + } + } + } + } +}; - _queryCellCircle = ( + _queryCellCircle: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any +) => void | boolean) = ( x1: number, y1: number, x2: number, @@ -333,18 +365,18 @@ class GridIndex { result: any, queryArgs: any, predicate?: any, - ): void | boolean => { - const circle = queryArgs.circle; - const seenUids = queryArgs.seenUids; - const boxCell = this.boxCells[cellIndex]; - if (boxCell !== null) { - const bboxes = this.bboxes; - for (const boxUid of boxCell) { - if (!seenUids.box[boxUid]) { - seenUids.box[boxUid] = true; - const offset = boxUid * 4; - if ( - this._circleAndRectCollide( +): void | boolean => { + const circle = queryArgs.circle; + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if ( + this._circleAndRectCollide( circle.x, circle.y, circle.radius, @@ -352,41 +384,41 @@ class GridIndex { bboxes[offset + 1], bboxes[offset + 2], bboxes[offset + 3], - ) && + ) && (!predicate || predicate(this.boxKeys[boxUid])) - ) { - result.push(true); - return true; - } - } - } - } + ) { + result.push(true); + return true; + } + } + } + } - const circleCell = this.circleCells[cellIndex]; - if (circleCell !== null) { - const circles = this.circles; - for (const circleUid of circleCell) { - if (!seenUids.circle[circleUid]) { - seenUids.circle[circleUid] = true; - const offset = circleUid * 3; - if ( - this._circlesCollide( + const circleCell = this.circleCells[cellIndex]; + if (circleCell !== null) { + const circles = this.circles; + for (const circleUid of circleCell) { + if (!seenUids.circle[circleUid]) { + seenUids.circle[circleUid] = true; + const offset = circleUid * 3; + if ( + this._circlesCollide( circles[offset], circles[offset + 1], circles[offset + 2], circle.x, circle.y, circle.radius, - ) && + ) && (!predicate || predicate(this.circleKeys[circleUid])) - ) { - result.push(true); - return true; - } - } - } - } - }; + ) { + result.push(true); + return true; + } + } + } + } +}; _forEachCell( x1: number, diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index 922359e4027..320a9d2d26c 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -381,7 +381,7 @@ export class Terrain return demScale * proxyTileSize; } - _checkRenderCacheEfficiency = () => { + _checkRenderCacheEfficiency: (() => void) = () => { const renderCacheInfo = this.renderCacheEfficiency(this._style); if (this._style.map._optimizeForTerrain) { assert(renderCacheInfo.efficiency === 100); @@ -392,7 +392,7 @@ export class Terrain } }; - _onStyleDataEvent = (event: any) => { + _onStyleDataEvent: ((event: any) => void) = (event: any) => { if (event.coord && event.dataType === 'source') { this._clearRenderCacheForTile(event.sourceCacheId, event.coord); } else if (event.dataType === 'style') { diff --git a/src/ui/camera.js b/src/ui/camera.js index fadee08d6e7..ad683e1a7c8 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -1952,7 +1952,7 @@ class Camera } // Callback for map._requestRenderFrame - _renderFrameCallback = () => { + _renderFrameCallback: (() => void) = () => { const t = Math.min( (browser.now() - this._easeStart) / this._easeOptions.duration, 1, diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 8fa0a30d5e8..40c4463799e 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -113,7 +113,7 @@ class AttributionControl { element.firstElementChild.setAttribute('title', str); } - _toggleAttribution = () => { + _toggleAttribution: (() => void) = () => { if (this._container.classList.contains('mapboxgl-compact-show')) { this._container.classList.remove('mapboxgl-compact-show'); this._compactButton.setAttribute('aria-expanded', 'false'); @@ -123,7 +123,7 @@ class AttributionControl { } }; - _updateEditLink = () => { + _updateEditLink: (() => void) = () => { let editLink = this._editLink; if (!editLink) { editLink = this._editLink = (this._container.querySelector( @@ -160,7 +160,7 @@ class AttributionControl { } }; - _updateData = (e: any) => { + _updateData: ((e: any) => void) = (e: any) => { if ( e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || @@ -230,7 +230,7 @@ class AttributionControl { this._editLink = null; } - _updateCompact = () => { + _updateCompact: (() => void) = () => { if (this._map.getCanvasContainer().offsetWidth <= 640) { this._container.classList.add('mapboxgl-compact'); } else { diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index f87f53aadc4..0fb68bfc74b 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -114,7 +114,7 @@ class FullscreenControl { return this._fullscreen; } - _changeIcon = () => { + _changeIcon: (() => void) = () => { const fullscreenElement = window.document.fullscreenElement || (window.document: any).webkitFullscreenElement; @@ -126,7 +126,7 @@ class FullscreenControl { } }; - _onClickFullscreen = () => { + _onClickFullscreen: (() => void) = () => { if (this._isFullscreen()) { if (window.document.exitFullscreen) { (window.document: any).exitFullscreen(); diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 077c8c6f98c..5345f0f28cb 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -253,7 +253,7 @@ class GeolocateControl * @param {Position} position the Geolocation API Position * @private */ - _onSuccess = (position: Position) => { + _onSuccess: ((position: Position) => void) = (position: Position) => { if (!this._map) { // control has since been removed return; @@ -387,7 +387,7 @@ class GeolocateControl this._circleElement.style.height = `${circleDiameter}px`; } - _onZoom = () => { + _onZoom: (() => void) = () => { if (this.options.showUserLocation && this.options.showAccuracyCircle) { this._updateCircleRadius(); } @@ -398,7 +398,7 @@ class GeolocateControl * * @private */ - _updateMarkerRotation = () => { + _updateMarkerRotation: (() => void) = () => { if (this._userLocationDotMarker && typeof this._heading === 'number') { this._userLocationDotMarker.setRotation(this._heading); this._dotElement.classList.add('mapboxgl-user-location-show-heading'); @@ -408,7 +408,7 @@ class GeolocateControl } }; - _onError = (error: PositionError) => { + _onError: ((error: PositionError) => void) = (error: PositionError) => { if (!this._map) { // control has since been removed return; @@ -462,14 +462,14 @@ class GeolocateControl this._finish(); }; - _finish = () => { + _finish: (() => void) = () => { if (this._timeoutId) { clearTimeout(this._timeoutId); } this._timeoutId = undefined; }; - _setupUI = (supported: boolean) => { + _setupUI: ((supported: boolean) => void) = (supported: boolean) => { if (this._map === undefined) { // This control was removed from the map before geolocation // support was determined. @@ -598,7 +598,7 @@ class GeolocateControl * geolocate.trigger(); * }); */ - _onDeviceOrientation = (deviceOrientationEvent: DeviceOrientationEvent) => { + _onDeviceOrientation: ((deviceOrientationEvent: DeviceOrientationEvent) => void) = (deviceOrientationEvent: DeviceOrientationEvent) => { // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. if (this._userLocationDotMarker) { if (deviceOrientationEvent.webkitCompassHeading) { @@ -630,7 +630,7 @@ class GeolocateControl * }); * @returns {boolean} Returns `false` if called before control was added to a map, otherwise returns `true`. */ - trigger = (): boolean => { + trigger: (() => boolean) = (): boolean => { if (!this._setup) { warnOnce('Geolocate control triggered before added to a map'); return false; diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index d5641f1e82f..7553e9707b6 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -57,7 +57,7 @@ class LogoControl { return 'bottom-left'; } - _updateLogo = (e: any) => { + _updateLogo: ((e: any) => void) = (e: any) => { if (!e || e.sourceDataType === 'metadata') { this._container.style.display = this._logoRequired() ? 'block' : 'none'; } @@ -77,7 +77,7 @@ class LogoControl { return true; } - _updateCompact = () => { + _updateCompact: (() => void) = () => { const containerChildren = this._container.children; if (containerChildren.length) { const anchor = containerChildren[0]; diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index d92be7a61c6..89b5c2a7bed 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -105,7 +105,7 @@ class NavigationControl { } } - _updateZoomButtons = () => { + _updateZoomButtons: (() => void) = () => { const map = this._map; if (!map) return; @@ -118,7 +118,7 @@ class NavigationControl { this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); }; - _rotateCompassArrow = () => { + _rotateCompassArrow: (() => void) = () => { const map = this._map; if (!map) return; @@ -274,7 +274,7 @@ class MouseRotateWrapper { window.removeEventListener('mouseup', this.mouseup); } - mousedown = (e: MouseEvent) => { + mousedown: ((e: MouseEvent) => void) = (e: MouseEvent) => { this.down( extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), DOM.mousePos(this.element, e), @@ -283,17 +283,17 @@ class MouseRotateWrapper { window.addEventListener('mouseup', this.mouseup); }; - mousemove = (e: MouseEvent) => { + mousemove: ((e: MouseEvent) => void) = (e: MouseEvent) => { this.move(e, DOM.mousePos(this.element, e)); }; - mouseup = (e: MouseEvent) => { + mouseup: ((e: MouseEvent) => void) = (e: MouseEvent) => { this.mouseRotate.mouseupWindow(e); if (this.mousePitch) this.mousePitch.mouseupWindow(e); this.offTemp(); }; - touchstart = (e: TouchEvent) => { + touchstart: ((e: TouchEvent) => void) = (e: TouchEvent) => { if (e.targetTouches.length !== 1) { this.reset(); } else { @@ -313,7 +313,7 @@ class MouseRotateWrapper { } }; - touchmove = (e: TouchEvent) => { + touchmove: ((e: TouchEvent) => void) = (e: TouchEvent) => { if (e.targetTouches.length !== 1) { this.reset(); } else { @@ -325,7 +325,7 @@ class MouseRotateWrapper { } }; - touchend = (e: TouchEvent) => { + touchend: ((e: TouchEvent) => void) = (e: TouchEvent) => { if ( e.targetTouches.length === 0 && this._startPos && this._lastPos && this._startPos.dist(this._lastPos) < this._clickTolerance @@ -335,7 +335,7 @@ class MouseRotateWrapper { this.reset(); }; - reset = () => { + reset: (() => void) = () => { this.mouseRotate.reset(); if (this.mousePitch) this.mousePitch.reset(); delete this._startPos; diff --git a/src/ui/control/scale_control.js b/src/ui/control/scale_control.js index 590db056791..883287a537f 100644 --- a/src/ui/control/scale_control.js +++ b/src/ui/control/scale_control.js @@ -57,7 +57,7 @@ class ScaleControl { return 'bottom-left'; } - _update = () => { + _update: () => void = () => { // A horizontal scale is imagined to be present at center of the map // container with maximum length (Default) as 100px. // Using spherical law of cosines approximation, the real distance is @@ -95,8 +95,7 @@ class ScaleControl { const distance = getRoundNum(maxDistance); const ratio = distance / maxDistance; - this._map._requestDomTask( - () => { + this._map._requestDomTask(() => { this._container.style.width = `${maxWidth * ratio}px`; // Intl.NumberFormat doesn't support nautical-mile as a unit, @@ -106,13 +105,9 @@ class ScaleControl { return; } - // $FlowFixMe — flow v0.142.0 doesn't support optional `locales` argument and `unit` style option - this._container.innerHTML = new Intl.NumberFormat( - this._language, - {style: 'unit', unitDisplay: 'narrow', unit}, - ).format(distance); - }, - ); + // $FlowFixMe — flow v0.153.0 doesn't support optional `locales` argument and `unit` style option + this._container.innerHTML = new Intl.NumberFormat(this._language, {style: 'unit', unitDisplay: 'narrow', unit}).format(distance); + }); } onAdd(map: Map): HTMLElement { diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 325a99363b7..21335b5ed59 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -225,7 +225,7 @@ class ScrollZoomHandler { e.preventDefault(); } - _onTimeout = (initialEvent: WheelEvent) => { + _onTimeout: ((initialEvent: WheelEvent) => void) = (initialEvent: WheelEvent) => { this._type = 'wheel'; this._delta -= this._lastValue; if (!this._active) { diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 927501c9353..811b036bb01 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -374,7 +374,7 @@ class HandlerManager { return false; } - handleWindowEvent = (e: InputEvent) => { + handleWindowEvent: ((e: InputEvent) => void) = (e: InputEvent) => { this.handleEvent(e, `${e.type}Window`); }; @@ -389,7 +389,7 @@ class HandlerManager { return ((mapTouches: any): TouchList); } - handleEvent = (e: InputEvent | RenderFrameEvent, eventName?: string) => { + handleEvent: ((e: InputEvent | RenderFrameEvent, eventName?: string) => void) = (e: InputEvent | RenderFrameEvent, eventName?: string) => { this._updatingCamera = true; assert(e.timeStamp !== undefined); diff --git a/src/ui/hash.js b/src/ui/hash.js index 667ebd6c41c..acccb4c2bb6 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -103,7 +103,7 @@ export default class Hash { return hash.split('/'); } - _onHashChange = (): boolean => { + _onHashChange: (() => boolean) = (): boolean => { const map = this._map; if (!map) return false; const loc = this._getCurrentHash(); @@ -125,7 +125,7 @@ export default class Hash { return false; }; - _updateHashUnthrottled = () => { + _updateHashUnthrottled: (() => void) = () => { // Replace if already present, else append the updated hash string const location = window.location.href.replace( /(#.+)?$/, diff --git a/src/ui/map.js b/src/ui/map.js index fcfe383b2f1..1a6ebe63b06 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -4068,17 +4068,17 @@ class Map return this; } - _onWindowOnline = () => { + _onWindowOnline: () => void = () => { this._update(); }; - _onWindowResize = (event: Event) => { + _onWindowResize: (event: Event) => void = (event: Event) => { if (this._trackResize) { this.resize({originalEvent: event})._update(); } }; - _onVisibilityChange = () => { + _onVisibilityChange: () => void = () => { if (window.document.visibilityState === 'hidden') { this._visibilityHidden++; } diff --git a/src/ui/marker.js b/src/ui/marker.js index 24d516f04b8..f86e341a256 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -399,7 +399,7 @@ export default class Marker return this; } - _onKeyPress = (e: KeyboardEvent) => { + _onKeyPress: ((e: KeyboardEvent) => void) = (e: KeyboardEvent) => { const code = e.code; const legacyCode = e.charCode || e.keyCode; @@ -412,7 +412,7 @@ export default class Marker } }; - _onMapClick = (e: MapMouseEvent) => { + _onMapClick: ((e: MapMouseEvent) => void) = (e: MapMouseEvent) => { const targetElement = e.originalEvent.target; const element = this._element; @@ -479,7 +479,7 @@ export default class Marker return toClosestSurface < toMarker * 0.9; } - _evaluateOpacity = () => { + _evaluateOpacity: (() => void) = () => { const map = this._map; if (!map) return; @@ -515,7 +515,7 @@ export default class Marker this._fadeTimer = null; }; - _clearFadeTimer = () => { + _clearFadeTimer: (() => void) = () => { if (this._fadeTimer) { clearTimeout(this._fadeTimer); this._fadeTimer = null; @@ -618,7 +618,7 @@ export default class Marker return rotation ? `rotateZ(${rotation}deg)` : ''; } - _update = (delaySnap?: boolean) => { + _update: ((delaySnap?: boolean) => void) = (delaySnap?: boolean) => { window.cancelAnimationFrame(this._updateFrameId); const map = this._map; if (!map) return; @@ -688,7 +688,7 @@ export default class Marker return this; } - _onMove = (e: MapMouseEvent | MapTouchEvent) => { + _onMove: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { const map = this._map; if (!map) return; @@ -738,7 +738,7 @@ export default class Marker this.fire(new Event('drag')); }; - _onUp = () => { + _onUp: (() => void) = () => { // revert to normal pointer event handling this._element.style.pointerEvents = 'auto'; this._positionDelta = null; @@ -768,7 +768,7 @@ export default class Marker this._state = 'inactive'; }; - _addDragHandler = (e: MapMouseEvent | MapTouchEvent) => { + _addDragHandler: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { const map = this._map; const pos = this._pos; if (!map || !pos) return; diff --git a/src/ui/popup.js b/src/ui/popup.js index d80d68bc65b..7132353ddc9 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -208,7 +208,7 @@ export default class Popup * popup.remove(); * @returns {Popup} Returns itself to allow for method chaining. */ - remove = (): this => { + remove: (() => any) = (): this => { if (this._content) { this._content.remove(); } @@ -553,7 +553,7 @@ export default class Popup return finalState; } - _onMouseEvent = (event: MapMouseEvent) => { + _onMouseEvent: ((event: MapMouseEvent) => void) = (event: MapMouseEvent) => { this._update(event.point); }; @@ -606,7 +606,7 @@ export default class Popup container.className = classes.join(' '); } - _update = (cursor?: Point) => { + _update: ((cursor?: Point) => void) = (cursor?: Point) => { const hasPosition = this._lngLat || this._trackPointer; const map = this._map; const content = this._content; @@ -673,7 +673,7 @@ export default class Popup if (firstFocusable) firstFocusable.focus(); } - _onClose = () => { + _onClose: (() => void) = () => { this.remove(); }; diff --git a/src/util/actor.js b/src/util/actor.js index 19b40da46ab..a02e0babc0b 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -99,7 +99,7 @@ class Actor { }; } - receive = (message: Object) => { + receive: ((message: any) => void) = (message: Object) => { const data = message.data, id = data.id; diff --git a/src/util/mapbox.js b/src/util/mapbox.js index e7677fded36..db2bf88ff6c 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -419,7 +419,7 @@ export class PerformanceEvent super('gljs.performance'); } - postPerformanceEvent = ( + postPerformanceEvent: ((customAccessToken: ?string, performanceData: LivePerformanceData) => void) = ( customAccessToken: ?string, performanceData: LivePerformanceData, ) => { @@ -469,7 +469,12 @@ export class MapLoadEvent this.skuToken = ''; } - postMapLoadEvent = ( + postMapLoadEvent: (( + mapId: number, + skuToken: string, + customAccessToken: ?string, + callback: EventCallback +) => void) = ( mapId: number, skuToken: string, customAccessToken: ?string, @@ -574,7 +579,12 @@ export class MapSessionAPI ); } - getSessionAPI = ( + getSessionAPI: (( + mapId: number, + skuToken: string, + customAccessToken: ?string, + callback: EventCallback +) => void) = ( mapId: number, skuToken: string, customAccessToken: ?string, @@ -624,7 +634,7 @@ export class TurnstileEvent this._customAccessToken = customAccessToken; } - postTurnstileEvent = (tileUrls: Array, customAccessToken?: ?string) => { + postTurnstileEvent: ((tileUrls: Array, customAccessToken?: ?string) => void) = (tileUrls: Array, customAccessToken?: ?string) => { //Enabled only when Mapbox Access Token is set and a source uses // mapbox tiles. if ( diff --git a/src/util/scheduler.js b/src/util/scheduler.js index c9ffb4d622c..4e08687dd29 100644 --- a/src/util/scheduler.js +++ b/src/util/scheduler.js @@ -65,7 +65,7 @@ class Scheduler { }; } - process = () => { + process: (() => void) = () => { const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; From fec0da124481fbb0ea790aabfa663972379786ca Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 11:19:03 +0200 Subject: [PATCH 05/72] Update vectortile_to_geojson --- src/util/vectortile_to_geojson.js | 4 +++- src/util/web_worker_transfer.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/util/vectortile_to_geojson.js b/src/util/vectortile_to_geojson.js index 5d4a5c5fbb6..384e072f0c7 100644 --- a/src/util/vectortile_to_geojson.js +++ b/src/util/vectortile_to_geojson.js @@ -1,11 +1,13 @@ // @flow -import type {LayerSpecification} from '../style-spec/types.js'; +import type {LayerSpecification, SourceSpecification} from '../style-spec/types.js'; import type {GeoJSONGeometry, GeoJSONFeature} from '@mapbox/geojson-types'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; // we augment GeoJSON with custom properties in query*Features results export interface QueryFeature extends GeoJSONFeature { layer?: ?LayerSpecification; + source?: ?SourceSpecification | ?mixed; + sourceLayer?: ?string | ?mixed; state: ?mixed; [key: string]: mixed; } diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index dd17f11586e..d8db0888c17 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -194,18 +194,18 @@ export function serialize(input: mixed, transferables: ?Array): Se properties[key] = serialize(property, transferables); } if (input instanceof Error) { - properties.message = input.message; + properties['message'] = input.message; } } else { // make sure statically serialized object survives transfer of $name property assert(!transferables || properties !== transferables[transferables.length - 1]); } - if (properties.$name) { + if (properties['$name']) { throw new Error('$name property is reserved for worker serialization logic.'); } if (name !== 'Object') { - properties.$name = name; + properties['$name'] = name; } return properties; From 4adc9580da889e33c37bdac6b50404d99c19ffee Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 11:20:34 +0200 Subject: [PATCH 06/72] Fix eslint indentation --- src/style/style.js | 14 +- src/style/style_layer/custom_style_layer.js | 18 +-- src/terrain/terrain.js | 166 ++++++++++---------- src/ui/camera.js | 6 +- src/ui/handler_manager.js | 12 +- src/util/mapbox.js | 48 +++--- 6 files changed, 132 insertions(+), 132 deletions(-) diff --git a/src/style/style.js b/src/style/style.js index f61730b4f88..7944ec13318 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -526,13 +526,13 @@ class Style return this._order; } - isLayerDraped(layer: StyleLayer): boolean { - if (!this.terrain) return false; - // $FlowFixMe[prop-missing] - // $FlowFixMe[incompatible-use] - if (typeof layer.isLayerDraped === 'function') return layer.isLayerDraped(); - return drapedLayers[layer.type]; - } + isLayerDraped(layer: StyleLayer): boolean { + if (!this.terrain) return false; + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-use] + if (typeof layer.isLayerDraped === 'function') return layer.isLayerDraped(); + return drapedLayers[layer.type]; + } _checkLoaded() { if (!this._loaded) { diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index 69de5180aa1..0b6277f0ece 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -202,17 +202,17 @@ class CustomStyleLayer return this.implementation.renderingMode === '3d'; } - hasOffscreenPass(): boolean { - return this.implementation.prerender !== undefined; - } + hasOffscreenPass(): boolean { + return this.implementation.prerender !== undefined; + } - isLayerDraped(): boolean { - return this.implementation.renderToTile !== undefined; - } + isLayerDraped(): boolean { + return this.implementation.renderToTile !== undefined; + } - shouldRedrape(): boolean { - return !!this.implementation.shouldRerenderTiles && this.implementation.shouldRerenderTiles(); - } + shouldRedrape(): boolean { + return !!this.implementation.shouldRerenderTiles && this.implementation.shouldRerenderTiles(); + } recalculate() {} updateTransitions() {} diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index 320a9d2d26c..eabac57ca74 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -303,15 +303,15 @@ export class Terrain this._mockSourceCache = new MockSourceCache(style.map); } - set style(style: Style) { - style.on('data', this._onStyleDataEvent.bind(this)); - style.on('neworder', this._checkRenderCacheEfficiency.bind(this)); - this._style = style; - this._checkRenderCacheEfficiency(); - this._style.map.on('moveend', () => { - this._clearLineLayersFromRenderCache(); - }); - } + set style(style: Style) { + style.on('data', this._onStyleDataEvent.bind(this)); + style.on('neworder', this._checkRenderCacheEfficiency.bind(this)); + this._style = style; + this._checkRenderCacheEfficiency(); + this._style.map.on('moveend', () => { + this._clearLineLayersFromRenderCache(); + }); + } /* * Validate terrain and update source cache used for elevation. @@ -755,16 +755,16 @@ export class Terrain } } - globeUniformValues(tr: Transform, id: CanonicalTileID, useDenormalizedUpVectorScale: ?boolean): UniformValues { - const projection = tr.projection; - return { - 'u_tile_tl_up': (projection.upVector(id, 0, 0): any), - 'u_tile_tr_up': (projection.upVector(id, EXTENT, 0): any), - 'u_tile_br_up': (projection.upVector(id, EXTENT, EXTENT): any), - 'u_tile_bl_up': (projection.upVector(id, 0, EXTENT): any), - 'u_tile_up_scale': (useDenormalizedUpVectorScale ? globeMetersToEcef(1) : projection.upVectorScale(id, tr.center.lat, tr.worldSize).metersToTile: any) - }; - } + globeUniformValues(tr: Transform, id: CanonicalTileID, useDenormalizedUpVectorScale: ?boolean): UniformValues { + const projection = tr.projection; + return { + 'u_tile_tl_up': (projection.upVector(id, 0, 0): any), + 'u_tile_tr_up': (projection.upVector(id, EXTENT, 0): any), + 'u_tile_br_up': (projection.upVector(id, EXTENT, EXTENT): any), + 'u_tile_bl_up': (projection.upVector(id, 0, EXTENT): any), + 'u_tile_up_scale': (useDenormalizedUpVectorScale ? globeMetersToEcef(1) : projection.upVectorScale(id, tr.center.lat, tr.worldSize).metersToTile: any) + }; + } renderToBackBuffer(accumulatedDrapes: Array) { const painter = this.painter; @@ -1069,81 +1069,81 @@ export class Terrain } } - const isTransitioning = id => { - const layer = this._style._layers[id]; - const isHidden = layer.isHidden(this.painter.transform.zoom); - if (layer.type === 'custom') { - return !isHidden && ((layer: any): CustomStyleLayer).shouldRedrape(); - } - return !isHidden && layer.hasTransition(); - }; - return this._style.order.some(isTransitioning); - } + const isTransitioning = id => { + const layer = this._style._layers[id]; + const isHidden = layer.isHidden(this.painter.transform.zoom); + if (layer.type === 'custom') { + return !isHidden && ((layer: any): CustomStyleLayer).shouldRedrape(); + } + return !isHidden && layer.hasTransition(); + }; + return this._style.order.some(isTransitioning); + } - _clearLineLayersFromRenderCache() { - let hasVectorSource = false; - for (const source of this._style._getSources()) { - if (source instanceof VectorTileSource) { - hasVectorSource = true; - break; - } - } + _clearLineLayersFromRenderCache() { + let hasVectorSource = false; + for (const source of this._style._getSources()) { + if (source instanceof VectorTileSource) { + hasVectorSource = true; + break; + } + } - if (!hasVectorSource) return; + if (!hasVectorSource) return; - const clearSourceCaches = {}; - for (let i = 0; i < this._style.order.length; ++i) { - const layer = this._style._layers[this._style.order[i]]; - const sourceCache = this._style._getLayerSourceCache(layer); - if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; + const clearSourceCaches = {}; + for (let i = 0; i < this._style.order.length; ++i) { + const layer = this._style._layers[this._style.order[i]]; + const sourceCache = this._style._getLayerSourceCache(layer); + if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; - const isHidden = layer.isHidden(this.painter.transform.zoom); - if (isHidden || layer.type !== 'line') continue; + const isHidden = layer.isHidden(this.painter.transform.zoom); + if (isHidden || layer.type !== 'line') continue; - // Check if layer has a zoom dependent "line-width" expression - const widthExpression = ((layer: any): LineStyleLayer).widthExpression(); - if (!(widthExpression instanceof ZoomDependentExpression)) continue; + // Check if layer has a zoom dependent "line-width" expression + const widthExpression = ((layer: any): LineStyleLayer).widthExpression(); + if (!(widthExpression instanceof ZoomDependentExpression)) continue; - // Mark sourceCache as cleared - clearSourceCaches[sourceCache.id] = true; - for (const proxy of this.proxyCoords) { - const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; - const coords = ((proxiedCoords: any): Array); - if (!coords) continue; + // Mark sourceCache as cleared + clearSourceCaches[sourceCache.id] = true; + for (const proxy of this.proxyCoords) { + const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; + const coords = ((proxiedCoords: any): Array); + if (!coords) continue; - for (const coord of coords) { - this._clearRenderCacheForTile(sourceCache.id, coord); - } - } - } - } + for (const coord of coords) { + this._clearRenderCacheForTile(sourceCache.id, coord); + } + } + } + } - _clearRasterLayersFromRenderCache() { - let hasRasterSource = false; - for (const id in this._style._sourceCaches) { - if (this._style._sourceCaches[id]._source instanceof RasterTileSource) { - hasRasterSource = true; - break; - } - } + _clearRasterLayersFromRenderCache() { + let hasRasterSource = false; + for (const id in this._style._sourceCaches) { + if (this._style._sourceCaches[id]._source instanceof RasterTileSource) { + hasRasterSource = true; + break; + } + } - if (!hasRasterSource) return; + if (!hasRasterSource) return; - const clearSourceCaches = {}; - for (let i = 0; i < this._style.order.length; ++i) { - const layer = this._style._layers[this._style.order[i]]; - const sourceCache = this._style._getLayerSourceCache(layer); - if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; + const clearSourceCaches = {}; + for (let i = 0; i < this._style.order.length; ++i) { + const layer = this._style._layers[this._style.order[i]]; + const sourceCache = this._style._getLayerSourceCache(layer); + if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; - const isHidden = layer.isHidden(this.painter.transform.zoom); - if (isHidden || layer.type !== 'raster') continue; + const isHidden = layer.isHidden(this.painter.transform.zoom); + if (isHidden || layer.type !== 'raster') continue; - // Check if any raster tile is in a fading state - const fadeDuration = ((layer: any): RasterStyleLayer).paint.get('raster-fade-duration'); - for (const proxy of this.proxyCoords) { - const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; - const coords = ((proxiedCoords: any): Array); - if (!coords) continue; + // Check if any raster tile is in a fading state + const fadeDuration = ((layer: any): RasterStyleLayer).paint.get('raster-fade-duration'); + for (const proxy of this.proxyCoords) { + const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; + const coords = ((proxiedCoords: any): Array); + if (!coords) continue; for (const coord of coords) { const tile = sourceCache.getTile(coord); @@ -1232,7 +1232,7 @@ export class Terrain return; } - this._clearRasterLayersFromRenderCache(); + this._clearRasterLayersFromRenderCache(); const coords = this.proxyCoords; const dirty = this._tilesDirty; diff --git a/src/ui/camera.js b/src/ui/camera.js index ad683e1a7c8..3eaddc66f0b 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -836,10 +836,10 @@ class Camera return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); } - return {center: tr.center, zoom, bearing, pitch}; - } + return {center: tr.center, zoom, bearing, pitch}; + } - /** @section {Querying features} */ + /** @section {Querying features} */ /** * Queries the currently loaded data for elevation at a geographical location. The elevation is returned in `meters` relative to mean sea-level. diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 811b036bb01..7ca2b5837f7 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -596,12 +596,12 @@ class HandlerManager { around = pinchAround; } - if ((zoomDelta || eventStarted("drag")) && around) { - this._dragOrigin = toVec3(tr.pointCoordinate3D(around)); - // Construct the tracking ellipsoid every time user changes the zoom or drag origin. - // Direction of the ray will define size of the shape and hence defining the available range of movement - this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin); - } + if ((zoomDelta || eventStarted("drag")) && around) { + this._dragOrigin = toVec3(tr.pointCoordinate3D(around)); + // Construct the tracking ellipsoid every time user changes the zoom or drag origin. + // Direction of the ray will define size of the shape and hence defining the available range of movement + this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin); + } // All movement of the camera is done relative to the sea level tr.cameraElevationReference = "sea"; diff --git a/src/util/mapbox.js b/src/util/mapbox.js index db2bf88ff6c..a5a71616e47 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -479,21 +479,21 @@ export class MapLoadEvent skuToken: string, customAccessToken: ?string, callback: EventCallback, - ) => { - this.skuToken = skuToken; - this.errorCb = callback; +) => { + this.skuToken = skuToken; + this.errorCb = callback; - if (config.EVENTS_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest( + if (config.EVENTS_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest( {id: mapId, timestamp: Date.now()}, customAccessToken, - ); - } else { - this.errorCb(new Error(AUTH_ERR_MSG)); - } - } - }; + ); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } +}; processRequests(customAccessToken?: ?string) { if (this.pendingRequest || this.queue.length === 0) return; @@ -589,21 +589,21 @@ export class MapSessionAPI skuToken: string, customAccessToken: ?string, callback: EventCallback, - ) => { - this.skuToken = skuToken; - this.errorCb = callback; +) => { + this.skuToken = skuToken; + this.errorCb = callback; - if (config.SESSION_PATH && config.API_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest( + if (config.SESSION_PATH && config.API_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest( {id: mapId, timestamp: Date.now()}, customAccessToken, - ); - } else { - this.errorCb(new Error(AUTH_ERR_MSG)); - } - } - }; + ); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } +}; processRequests(customAccessToken?: ?string) { if (this.pendingRequest || this.queue.length === 0) return; From 46cd568ac0b1f1a169b536cf030e74b3421646b8 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 12:06:16 +0200 Subject: [PATCH 07/72] Fix method-unbinding --- src/ui/control/fullscreen_control.js | 1 + src/ui/map.js | 10 +++++----- src/ui/popup.js | 11 +++++------ src/util/dom.js | 1 + src/util/vectortile_to_geojson.js | 1 + 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index 0fb68bfc74b..c70e2dc5390 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -133,6 +133,7 @@ class FullscreenControl { } else if (window.document.webkitCancelFullScreen) { (window.document: any).webkitCancelFullScreen(); } + // $FlowFixMe[method-unbinding] } else if (this._container.requestFullscreen) { this._container.requestFullscreen(); } else if ((this._container: any).webkitRequestFullscreen) { diff --git a/src/ui/map.js b/src/ui/map.js index 1a6ebe63b06..d8a006e7dc9 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -75,8 +75,8 @@ import type {QueryResult} from '../data/feature_index.js'; export type ControlPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; /* eslint-disable no-use-before-define */ interface IControl { - onAdd(map: Map): HTMLElement; - onRemove(map: Map): void; + +onAdd: (map: Map) => HTMLElement; + +onRemove: (map: Map) => void; +getDefaultPosition?: () => ControlPosition; +_setLanguage?: (language: ?string | ?string[]) => void; @@ -3338,7 +3338,7 @@ class Map webpSupported.testSupport(gl); } - _contextLost = (event: *) => { + _contextLost: (event: *) => void = (event: *) => { event.preventDefault(); if (this._frame) { this._frame.cancel(); @@ -3347,14 +3347,14 @@ class Map this.fire(new Event('webglcontextlost', {originalEvent: event})); }; - _contextRestored = (event: *) => { + _contextRestored: (event: *) => void = (event: *) => { this._setupPainter(); this.resize(); this._update(); this.fire(new Event('webglcontextrestored', {originalEvent: event})); }; - _onMapScroll = (event: *): ?boolean => { + _onMapScroll: (event: *) => ?boolean = (event: *) => { if (event.target !== this._container) return; // Revert any scroll which would move the canvas outside of the view diff --git a/src/ui/popup.js b/src/ui/popup.js index 7132353ddc9..acc505b22ea 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -101,8 +101,7 @@ const focusQuerySelector = [ * @see [Example: Display a popup on click](https://www.mapbox.com/mapbox-gl-js/example/popup-on-click/) * @see [Example: Attach a popup to a marker instance](https://www.mapbox.com/mapbox-gl-js/example/set-popup/) */ -export default class Popup - extends Evented { +export default class Popup extends Evented { _map: ?Map; options: PopupOptions; _content: ?HTMLElement; @@ -208,7 +207,7 @@ export default class Popup * popup.remove(); * @returns {Popup} Returns itself to allow for method chaining. */ - remove: (() => any) = (): this => { + remove: () => Popup = () => { if (this._content) { this._content.remove(); } @@ -553,7 +552,7 @@ export default class Popup return finalState; } - _onMouseEvent: ((event: MapMouseEvent) => void) = (event: MapMouseEvent) => { + _onMouseEvent: (event: MapMouseEvent) => void = (event: MapMouseEvent) => { this._update(event.point); }; @@ -606,7 +605,7 @@ export default class Popup container.className = classes.join(' '); } - _update: ((cursor?: Point) => void) = (cursor?: Point) => { + _update: (cursor?: Point) => void = (cursor?: Point) => { const hasPosition = this._lngLat || this._trackPointer; const map = this._map; const content = this._content; @@ -673,7 +672,7 @@ export default class Popup if (firstFocusable) firstFocusable.focus(); } - _onClose: (() => void) = () => { + _onClose: () => void = () => { this.remove(); }; diff --git a/src/util/dom.js b/src/util/dom.js index ca84e4e2067..24ac033e269 100644 --- a/src/util/dom.js +++ b/src/util/dom.js @@ -6,6 +6,7 @@ import window from './window.js'; import assert from 'assert'; // refine the return type based on tagName, e.g. 'button' -> HTMLButtonElement +// $FlowFixMe[method-unbinding] export function create(tagName: T, className: ?string, container?: HTMLElement): $Call { const el = window.document.createElement(tagName); if (className !== undefined) el.className = className; diff --git a/src/util/vectortile_to_geojson.js b/src/util/vectortile_to_geojson.js index 384e072f0c7..9b3e4566c4e 100644 --- a/src/util/vectortile_to_geojson.js +++ b/src/util/vectortile_to_geojson.js @@ -56,6 +56,7 @@ class Feature { toJSON(): QueryFeature { const json: QueryFeature = { type: 'Feature', + state: undefined, geometry: this.geometry, properties: this.properties }; From 4340845614d93499d62f4594a64910ba5af15368 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 12:32:02 +0200 Subject: [PATCH 08/72] Update flow to v0.155.1 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 324b3ca6e5c..37f93e04074 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.153.0 +0.155.1 [options] diff --git a/package.json b/package.json index 5c75f1a7241..fc903e6fdf0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.153.0", + "flow-bin": "0.155.1", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 685a0b3fe24..4758d3c1b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.153.0: - version "0.153.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.153.0.tgz#44d941acaf5ef977fa26d1b4b5dc3cf56b68eefc" - integrity sha512-sxP9nfXnoyCUT6hjAO+zDyHLO3dZcWg0h+4HttHs/5wg/2oAkTDwmsWbj095IQsEmwTicq2TfqWq5QRuLxynlQ== +flow-bin@0.155.1: + version "0.155.1" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.155.1.tgz#1263ee3e0f42d11cb13ba56c3851a096213ce5f7" + integrity sha512-qy2eXkgngR6u+MYA1ydzPnclhos21BZlpkJ50Y9YOZ4eTMq6txswB3X+gUsg8XUyCteLoMeo7n30k7aY2no2Yw== follow-redirects@^1.0.0: version "1.15.1" From e519826df1c2db7adcc15d16569088d9bf211924 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 12:59:51 +0200 Subject: [PATCH 09/72] Fix method-unbinding --- src/source/custom_source.js | 10 +++++----- src/source/geojson_source.js | 8 ++++---- src/source/image_source.js | 8 ++++---- src/source/raster_dem_tile_source.js | 2 +- src/source/raster_tile_source.js | 16 ++++++++-------- src/source/vector_tile_source.js | 14 +++++++------- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/source/custom_source.js b/src/source/custom_source.js index d4f7190a506..36b2e37d0c3 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -263,7 +263,7 @@ class CustomSource return this._loaded; } - onAdd(map: Map): void { + onAdd: (map: Map) => void = (map) => { this._map = map; this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); @@ -271,13 +271,13 @@ class CustomSource this.load(); } - onRemove(map: Map): void { + onRemove: (map: Map) => void = (map) => { if (this._implementation.onRemove) { this._implementation.onRemove(map); } } - hasTile(tileID: OverscaledTileID): boolean { + hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { if (this._implementation.hasTile) { const {x, y, z} = tileID.canonical; return this._implementation.hasTile({x, y, z}); @@ -359,7 +359,7 @@ class CustomSource RasterTileSource.unloadTileData(tile, this._map.painter); } - unloadTile(tile: Tile, callback: Callback): void { + unloadTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { this.unloadTileData(tile); if (this._implementation.unloadTile) { const {x, y, z} = tile.tileID.canonical; @@ -369,7 +369,7 @@ class CustomSource callback(); } - abortTile(tile: Tile, callback: Callback): void { + abortTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { if (tile.request && tile.request.cancel) { tile.request.cancel(); delete tile.request; diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index 31af8fcc5d0..f86a013ecc6 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -148,7 +148,7 @@ class GeoJSONSource extends Evented implements Source { }, options.workerOptions); } - onAdd(map: Map) { + onAdd: (map: Map) => void = (map: Map) => { this.map = map; this.setData(this._data); } @@ -375,7 +375,7 @@ class GeoJSONSource extends Evented implements Source { }, undefined, message === 'loadTile'); } - abortTile(tile: Tile) { + abortTile: (tile: Tile) => void = (tile) => { if (tile.request) { tile.request.cancel(); delete tile.request; @@ -383,12 +383,12 @@ class GeoJSONSource extends Evented implements Source { tile.aborted = true; } - unloadTile(tile: Tile) { + unloadTile: (tile: Tile) => void = (tile) => { tile.unloadVectorData(); this.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id}); } - onRemove() { + onRemove: () => void = () => { if (this._pendingLoad) { this._pendingLoad.cancel(); } diff --git a/src/source/image_source.js b/src/source/image_source.js index 3f7b49de938..1f3db51fb55 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -228,12 +228,12 @@ class ImageSource extends Evented implements Source { } } - onAdd(map: Map) { + onAdd: (map: Map) => void = (map) => { this.map = map; this.load(); } - onRemove() { + onRemove: () => void = () => { if (this._imageRequest) { this._imageRequest.cancel(); this._imageRequest = null; @@ -293,7 +293,7 @@ class ImageSource extends Evented implements Source { return this; } - _clear() { + _clear: () => void = () => { this._boundsArray = undefined; } @@ -332,7 +332,7 @@ class ImageSource extends Evented implements Source { this.boundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); } - prepare() { + prepare: () => void = () => { if (Object.keys(this.tiles).length === 0 || !this.image) return; const context = this.map.painter.context; diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index 74ee13570fa..e177d5542c3 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -116,7 +116,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { return neighboringTiles; } - unloadTile(tile: Tile) { + unloadTile: (tile: Tile) => void = (tile) => { if (tile.demTexture) this.map.painter.saveTileTexture(tile.demTexture); if (tile.fbo) { tile.fbo.destroy(); diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 24ad2535c7a..d883b7ec93b 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -113,7 +113,7 @@ class RasterTileSource extends Evented implements Source { return this._loaded; } - onAdd(map: Map) { + onAdd: (map: Map) => void = (map) => { this.map = map; this.load(); } @@ -124,7 +124,7 @@ class RasterTileSource extends Evented implements Source { * @example * map.getSource('source-id').reload(); */ - reload() { + reload: () => void = () => { this.cancelTileJSONRequest(); this.load(() => this.map.style._clearSource(this.id)); } @@ -173,17 +173,17 @@ class RasterTileSource extends Evented implements Source { return this; } - onRemove() { + onRemove: () => void = () => { this.cancelTileJSONRequest(); - } + }; serialize(): RasterSourceSpecification | RasterDEMSourceSpecification { return extend({}, this._options); } - hasTile(tileID: OverscaledTileID): boolean { + hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { return !this.tileBounds || this.tileBounds.contains(tileID.canonical); - } + }; loadTile(tile: Tile, callback: Callback) { const use2x = browser.devicePixelRatio >= 2; @@ -222,7 +222,7 @@ class RasterTileSource extends Evented implements Source { } } - abortTile(tile: Tile, callback: Callback) { + abortTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { if (tile.request) { tile.request.cancel(); delete tile.request; @@ -230,7 +230,7 @@ class RasterTileSource extends Evented implements Source { callback(); } - unloadTile(tile: Tile, callback: Callback) { + unloadTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { if (tile.texture) this.map.painter.saveTileTexture(tile.texture); callback(); } diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 9be3c1fb9f9..af5ef0d4191 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -134,11 +134,11 @@ class VectorTileSource extends Evented implements Source { return this._loaded; } - hasTile(tileID: OverscaledTileID): boolean { + hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { return !this.tileBounds || this.tileBounds.contains(tileID.canonical); } - onAdd(map: Map) { + onAdd: (map: Map) => void = (map) => { this.map = map; this.load(); } @@ -149,7 +149,7 @@ class VectorTileSource extends Evented implements Source { * @example * map.getSource('source-id').reload(); */ - reload() { + reload: () => void = () => { this.cancelTileJSONRequest(); this.load(() => this.map.style._clearSource(this.id)); } @@ -199,7 +199,7 @@ class VectorTileSource extends Evented implements Source { return this; } - onRemove() { + onRemove: () => void = () => { this.cancelTileJSONRequest(); } @@ -288,7 +288,7 @@ class VectorTileSource extends Evented implements Source { } } - abortTile(tile: Tile) { + abortTile: (tile: Tile) => void = (tile) => { if (tile.request) { tile.request.cancel(); delete tile.request; @@ -298,7 +298,7 @@ class VectorTileSource extends Evented implements Source { } } - unloadTile(tile: Tile) { + unloadTile: (tile: Tile) => void = (tile) => { tile.unloadVectorData(); if (tile.actor) { tile.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id}); @@ -309,7 +309,7 @@ class VectorTileSource extends Evented implements Source { return false; } - afterUpdate() { + afterUpdate: () => void = () => { this._tileWorkers = {}; } From d6760944e66ca45f81bd41912b693128018b848c Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Thu, 23 Feb 2023 13:03:02 +0200 Subject: [PATCH 10/72] flow 0.153.0 eslint fix (#12586) * fix eslint problems for source_cache.js * fix eslint problems for style/style.js * fix eslint problems for util/actor.js * fix eslint problems for ui/map.js * fix eslint problems for ui/marker.js * fix eslint problems for ui/camera.js * fix eslint problems for terrain/terrain.js * fix eslint problems for ui/handler_manager.js --- src/source/source_cache.js | 11 ++-- src/style/style.js | 73 +++++++++--------------- src/terrain/terrain.js | 36 ++++-------- src/ui/camera.js | 90 +++++++----------------------- src/ui/handler_manager.js | 30 ++-------- src/ui/map.js | 111 ++++++++++++------------------------- src/ui/marker.js | 99 ++++++++++++--------------------- src/util/actor.js | 2 +- 8 files changed, 140 insertions(+), 312 deletions(-) diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 036d8244237..4a56e1d8ddc 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -110,10 +110,11 @@ class SourceCache this._coveredTiles = {}; this._state = new SourceFeatureState(); - this._isRaster = this._source.type === 'raster' || - this._source.type === 'raster-dem' || - // $FlowFixMe[prop-missing] - this._source.type === 'custom' && this._source._dataType === 'raster'; + this._isRaster = + this._source.type === 'raster' || + this._source.type === 'raster-dem' || + // $FlowFixMe[prop-missing] + (this._source.type === 'custom' && this._source._dataType === 'raster'); } onAdd(map: MapboxMap) { @@ -610,7 +611,7 @@ class SourceCache assert(tileID.key === +id); const tile = this._tiles[id]; - if (!tile || tile.fadeEndTime && tile.fadeEndTime <= browser.now()) + if (!tile || (tile.fadeEndTime && tile.fadeEndTime <= browser.now())) continue; // if the tile is loaded but still fading in, find parents to cross-fade with it diff --git a/src/style/style.js b/src/style/style.js index 7944ec13318..3a3b4fde37f 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -457,15 +457,18 @@ class Style if ( source.type === 'geojson' || - source.vectorLayerIds && - source.vectorLayerIds.indexOf(sourceLayer) === -1 + ( + source.vectorLayerIds && + source.vectorLayerIds.indexOf(sourceLayer) === -1 + ) ) { this.fire( - new ErrorEvent( - new Error( - `Source layer "${sourceLayer}" ` + `does not exist on source "${source.id}" ` + `as specified by style layer "${layer.id}"`, - ), - ), + new ErrorEvent(new Error( + `Source layer "${sourceLayer}" ` + + `does not exist on source "${source.id}" ` + + `as specified by style layer "${layer.id}"`, + ), + ), ); } } @@ -1790,13 +1793,9 @@ class Style } // Enabling - if ( - !this.terrain || - this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode - ) { + if (!this.terrain || (this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode)) { this._createTerrain(terrainOptions, drapeRenderMode); - } else { - // Updating + } else { // Updating const terrain = this.terrain; const currSpec = terrain.get(); @@ -2078,21 +2077,19 @@ class Style if ( forceFullPlacement || !this.pauseablePlacement || - this.pauseablePlacement.isDone() && - !this.placement.stillRecent(browser.now(), transform.zoom) + (this.pauseablePlacement.isDone() && + !this.placement.stillRecent(browser.now(), transform.zoom)) ) { - const fogState = this.fog && transform.projection.supportsFog ? - this.fog.state : - null; + const fogState = this.fog && transform.projection.supportsFog ? this.fog.state : null; this.pauseablePlacement = new PauseablePlacement( - transform, - this._order, - forceFullPlacement, - showCollisionBoxes, - fadeDuration, - crossSourceCollisions, - this.placement, - fogState, + transform, + this._order, + forceFullPlacement, + showCollisionBoxes, + fadeDuration, + crossSourceCollisions, + this.placement, + fogState ); this._layerOrderChanged = false; } @@ -2104,11 +2101,7 @@ class Style // render frame this.placement.setStale(); } else { - this.pauseablePlacement.continuePlacement( - this._order, - this._layers, - layerTiles, - ); + this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); if (this.pauseablePlacement.isDone()) { this.placement = this.pauseablePlacement.commit(browser.now()); @@ -2127,10 +2120,7 @@ class Style for (const layerID of this._order) { const styleLayer = this._layers[layerID]; if (styleLayer.type !== 'symbol') continue; - this.placement.updateLayerOpacities( - styleLayer, - layerTiles[styleLayer.source], - ); + this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]); } } @@ -2183,18 +2173,7 @@ class Style setDependencies(this._symbolSourceCaches[params.source]); } - getGlyphs( - mapId: string, - params: { stacks: { [_: string]: Array } }, - callback: Callback< - { - [_: string]: { - glyphs: { [_: number]: ?StyleGlyph }, - ascender?: number, - descender?: number, - }, - }, >, - ) { + getGlyphs(mapId: string, params: {stacks: {[_: string]: Array}}, callback: Callback<{[_: string]: {glyphs: {[_: number]: ?StyleGlyph}, ascender?: number, descender?: number}}>) { this.glyphManager.getGlyphs(params.stacks, callback); } diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index eabac57ca74..abdf73aff2a 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -333,9 +333,8 @@ export class Terrain const updateSourceCache = (() => { if (this.sourceCache.used) { - warnOnce( - `Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.', - ); + warnOnce(`Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + + 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.'); } // Lower tile zoom is sufficient for terrain, given the size of terrain grid. const scaledDemTileSize = this.getScaledDemTileSize(); @@ -1213,15 +1212,12 @@ export class Terrain this._drapedRenderBatches = batches; } - _setupRenderCache( - previousProxyToSource: { [number]: { [string]: Array } }, - ) { + _setupRenderCache(previousProxyToSource: {[number]: {[string]: Array}}) { const psc = this.proxySourceCache; if (this._shouldDisableRenderCache() || this._invalidateRenderCache) { this._invalidateRenderCache = false; if (psc.renderCache.length > psc.renderCachePool.length) { - const used = ((Object.values(psc.proxyCachedFBO): any): Array< - { [string | number]: number }, >); + const used = ((Object.values(psc.proxyCachedFBO): any): Array<{[string | number]: number}>); psc.proxyCachedFBO = {}; for (let i = 0; i < used.length; ++i) { const fbos = ((Object.values(used[i]): any): Array); @@ -1251,12 +1247,11 @@ export class Terrain for (const source in current) { const tiles = current[source]; const prevTiles = prev[source]; - if ( - !prevTiles || prevTiles.length !== tiles.length || - tiles.some( - (t, index) => t !== prevTiles[index] || - dirty[source] && dirty[source].hasOwnProperty(t.key), - ) + if (!prevTiles || prevTiles.length !== tiles.length || + tiles.some((t, index) => + (t !== prevTiles[index] || + (dirty[source] && dirty[source].hasOwnProperty(t.key) + ))) ) { equal = -1; break; @@ -1663,7 +1658,7 @@ export class Terrain const lookup = this._findCoveringTileCache[sourceCache.id]; const key = lookup[tileID.key]; tile = key ? sourceCache.getTileByID(key) : null; - if (tile && tile.hasData() || key === null) return tile; + if ((tile && tile.hasData()) || key === null) return tile; assert(!key || tile); @@ -1760,15 +1755,8 @@ export class Terrain getWirefameBuffer(): [IndexBuffer, SegmentVector] { if (!this.wireframeSegments) { const wireframeGridIndices = createWireframeGrid(GRID_DIM + 1); - this.wireframeIndexBuffer = this.painter.context.createIndexBuffer( - wireframeGridIndices, - ); - this.wireframeSegments = SegmentVector.simpleSegment( - 0, - 0, - this.gridBuffer.length, - wireframeGridIndices.length, - ); + this.wireframeIndexBuffer = this.painter.context.createIndexBuffer(wireframeGridIndices); + this.wireframeSegments = SegmentVector.simpleSegment(0, 0, this.gridBuffer.length, wireframeGridIndices.length); } return [this.wireframeIndexBuffer, this.wireframeSegments]; } diff --git a/src/ui/camera.js b/src/ui/camera.js index 3eaddc66f0b..0fc4710725c 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -653,18 +653,11 @@ class Camera options?: CameraOptions, ): ?EasingOptions { bounds = LngLatBounds.convert(bounds); - const bearing = options && options.bearing || 0; - const pitch = options && options.pitch || 0; + const bearing = (options && options.bearing) || 0; + const pitch = (options && options.pitch) || 0; const lnglat0 = bounds.getNorthWest(); const lnglat1 = bounds.getSouthEast(); - return this._cameraForBounds( - this.transform, - lnglat0, - lnglat1, - bearing, - pitch, - options, - ); + return this._cameraForBounds(this.transform, lnglat0, lnglat1, bearing, pitch, options); } _extendCameraOptions(options?: CameraOptions): FullCameraOptions { @@ -674,14 +667,11 @@ class Camera right: 0, left: 0, }; - options = extend( - { + options = extend({ padding: defaultPadding, offset: [0, 0], - maxZoom: this.transform.maxZoom, - }, - options, - ); + maxZoom: this.transform.maxZoom + }, options); if (typeof options.padding === 'number') { const p = options.padding; @@ -736,22 +726,10 @@ class Camera const yAxis = vec3.cross([], xAxis, zAxis); const aabbOrientation = [ - xAxis[0], - xAxis[1], - xAxis[2], - 0, - yAxis[0], - yAxis[1], - yAxis[2], - 0, - zAxis[0], - zAxis[1], - zAxis[2], - 0, - 0, - 0, - 0, - 1, + xAxis[0], xAxis[1], xAxis[2], 0, + yAxis[0], yAxis[1], yAxis[2], 0, + zAxis[0], zAxis[1], zAxis[2], 0, + 0, 0, 0, 1 ]; const ecefCoords = [ @@ -768,11 +746,7 @@ class Camera latLngToECEF(coord1.lat, midLng), ]; - let aabb = Aabb.fromPoints( - ecefCoords.map( - p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)], - ), - ); + let aabb = Aabb.fromPoints(ecefCoords.map(p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)])); const center = vec3.transformMat4([], aabb.center, aabbOrientation); @@ -787,10 +761,7 @@ class Camera const worldToCamera = tr.getWorldToCameraMatrix(); const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); - aabb = Aabb.applyTransform( - aabb, - mat4.multiply([], worldToCamera, aabbOrientation), - ); + aabb = Aabb.applyTransform(aabb, mat4.multiply([], worldToCamera, aabbOrientation)); vec3.transformMat4(center, center, worldToCamera); @@ -804,11 +775,7 @@ class Camera vec3.distance(center, aabbClosestPoint)); const globeCenter = tr.globeCenterInViewSpace; - const normal = vec3.sub( - [], - center, - [globeCenter[0], globeCenter[1], globeCenter[2]], - ); + const normal = vec3.sub([], center, [globeCenter[0], globeCenter[1], globeCenter[2]]); vec3.normalize(normal, normal); vec3.scale(normal, normal, offsetDistance); @@ -819,15 +786,8 @@ class Camera const meterPerECEF = earthRadius / GLOBE_RADIUS; const altitudeECEF = vec3.length(cameraPosition); const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius; - const mercatorZ = mercatorZfromAltitude( - Math.max(altitudeMeter, Number.EPSILON), - 0, - ); - - const zoom = Math.min( - tr.zoomFromMercatorZAdjusted(mercatorZ), - eOptions.maxZoom, - ); + const mercatorZ = mercatorZfromAltitude(Math.max(altitudeMeter, Number.EPSILON), 0); + const zoom = Math.min(tr.zoomFromMercatorZAdjusted(mercatorZ), eOptions.maxZoom); const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; if (zoom > halfZoomTransition) { @@ -1417,20 +1377,13 @@ class Camera easeTo(options: EasingOptions & { easeId?: string }, eventData?: Object): this { this._stop(false, options.easeId); - options = extend( - { + options = extend({ offset: [0, 0], duration: 500, - easing: defaultEasing, - }, - options, - ); + easing: defaultEasing + }, options); - if ( - options.animate === false || - !options.essential && browser.prefersReducedMotion - ) - options.duration = 0; + if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0; const tr = this.transform, startZoom = this.getZoom(), @@ -1464,10 +1417,7 @@ class Camera pointAtOffset = tr.centerPoint.add(rotatedOffset); from = new Point(centerCoord.x, centerCoord.y).mult(tr.worldSize); - delta = new Point( - mercatorXfromLng(center.lng), - mercatorYfromLat(center.lat), - ).mult(tr.worldSize).sub(from); + delta = new Point(mercatorXfromLng(center.lng), mercatorYfromLat(center.lat)).mult(tr.worldSize).sub(from); } else { pointAtOffset = tr.centerPoint.add(offsetAsPoint); const locationAtOffset = tr.pointLocation(pointAtOffset); diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 7ca2b5837f7..cabe5cdcacd 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -155,20 +155,11 @@ class HandlerManager { _updatingCamera: boolean; _changes: Array<[HandlerResult, Object, any]>; _previousActiveHandlers: { [string]: Handler }; - _listeners: Array< - [HTMLElement, string, void | EventListenerOptionsOrUseCapture], >; + _listeners: Array<[HTMLElement, string, void | EventListenerOptionsOrUseCapture]>; _trackingEllipsoid: TrackingEllipsoid; _dragOrigin: ?Vec3; - constructor( - map: Map, - options: { - interactive: boolean, - pitchWithRotate: boolean, - clickTolerance: number, - bearingSnap: number, - }, - ) { + constructor(map: Map, options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number, bearingSnap: number}) { this._map = map; this._el = this._map.getCanvasContainer(); this._handlers = []; @@ -226,7 +217,7 @@ class HandlerManager { [el, 'wheel', {passive: false}], [el, 'contextmenu', undefined], - [window, 'blur', undefined], + [window, 'blur', undefined] ]; for (const [target, type, listenerOptions] of this._listeners) { @@ -246,13 +237,7 @@ class HandlerManager { } } - _addDefaultHandlers( - options: { - interactive: boolean, - pitchWithRotate: boolean, - clickTolerance: number, - }, - ) { + _addDefaultHandlers(options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number }) { const map = this._map; const el = map.getCanvasContainer(); this._add('mapEvent', new MapEventHandler(map, options)); @@ -286,12 +271,7 @@ class HandlerManager { const touchRotate = new TouchRotateHandler(); const touchZoom = new TouchZoomHandler(); - map.touchZoomRotate = new TouchZoomRotateHandler( - el, - touchZoom, - touchRotate, - tapDragZoom, - ); + map.touchZoomRotate = new TouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom); this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']); this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']); diff --git a/src/ui/map.js b/src/ui/map.js index d8a006e7dc9..636e2fce348 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -625,8 +625,7 @@ class Map this.setProjection(options.projection); } - const hashName = typeof options.hash === 'string' && options.hash || - undefined; + const hashName = (typeof options.hash === 'string' && options.hash) || undefined; this._hash = options.hash && new Hash(hashName).addTo(this); // don't set position from options if set through hash if (!this._hash || !this._hash._onHashChange()) { @@ -1438,7 +1437,7 @@ class Map * const isMoving = map.isMoving(); */ isMoving(): boolean { - return this._moving || this.handlers && this.handlers.isMoving() || false; + return this._moving || (this.handlers && this.handlers.isMoving()) || false; } /** @@ -1449,7 +1448,7 @@ class Map * const isZooming = map.isZooming(); */ isZooming(): boolean { - return this._zooming || this.handlers && this.handlers.isZooming() || false; + return this._zooming || (this.handlers && this.handlers.isZooming()) || false; } /** @@ -1460,13 +1459,11 @@ class Map * map.isRotating(); */ isRotating(): boolean { - return ( - this._rotating || this.handlers && this.handlers.isRotating() || false - ); + return this._rotating || (this.handlers && this.handlers.isRotating()) || false; } _isDragging(): boolean { - return this.handlers && this.handlers._isDragging() || false; + return (this.handlers && this.handlers._isDragging()) || false; } _createDelegatedListener( @@ -2349,49 +2346,28 @@ class Map this._lazyInitEmptyStyle(); const version = 0; - if ( - image instanceof window.HTMLImageElement || - window.ImageBitmap && image instanceof window.ImageBitmap - ) { + if (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) { const {width, height, data} = browser.getImageData(image); - this.style.addImage( - id, - { - data: new RGBAImage({width, height}, data), - pixelRatio, - stretchX, - stretchY, - content, - sdf, - version, - }, - ); + this.style.addImage(id, {data: new RGBAImage({width, height}, data), pixelRatio, stretchX, stretchY, content, sdf, version}); } else if (image.width === undefined || image.height === undefined) { - this.fire( - new ErrorEvent( - new Error( - 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`', - ), - ), - ); + this.fire(new ErrorEvent(new Error( + 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); } else { const {width, height} = image; const userImage = ((image: any): StyleImageInterface); const data = userImage.data; - this.style.addImage( - id, - { - data: new RGBAImage({width, height}, new Uint8Array(data)), - pixelRatio, - stretchX, - stretchY, - content, - sdf, - version, - userImage, - }, - ); + this.style.addImage(id, { + data: new RGBAImage({width, height}, new Uint8Array(data)), + pixelRatio, + stretchX, + stretchY, + content, + sdf, + version, + userImage + }); if (userImage.onAdd) { userImage.onAdd(this, id); @@ -2440,42 +2416,28 @@ class Map ); return; } - const imageData = image instanceof window.HTMLImageElement || - window.ImageBitmap && image instanceof window.ImageBitmap ? - browser.getImageData(image) : - image; + const imageData = (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) ? browser.getImageData(image) : image; + const {width, height} = imageData; // Flow can't refine the type enough to exclude ImageBitmap const data = ((imageData: any).data: Uint8Array | Uint8ClampedArray); if (width === undefined || height === undefined) { - this.fire( - new ErrorEvent( - new Error( - 'Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`', - ), - ), - ); + this.fire(new ErrorEvent(new Error( + 'Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); return; } - if ( - width !== existingImage.data.width || height !== existingImage.data.height - ) { - this.fire( - new ErrorEvent( - new Error( - `The width and height of the updated image (${width}, ${height}) + if (width !== existingImage.data.width || height !== existingImage.data.height) { + this.fire(new ErrorEvent(new Error( + `The width and height of the updated image (${width}, ${height}) must be that same as the previous version of the image - (${existingImage.data.width}, ${existingImage.data.height})`, - ), - ), - ); + (${existingImage.data.width}, ${existingImage.data.height})`))); return; } - const copy = !(image instanceof window.HTMLImageElement || - window.ImageBitmap && image instanceof window.ImageBitmap); + const copy = !(image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)); existingImage.data.replace(data, copy); this.style.updateImage(id, existingImage); @@ -3201,13 +3163,12 @@ class Map } _detectMissingCSS(): void { - const computedColor = window.getComputedStyle(this._missingCSSCanary).getPropertyValue( - 'background-color', - ); + const computedColor = window.getComputedStyle(this._missingCSSCanary).getPropertyValue('background-color'); if (computedColor !== 'rgb(250, 128, 114)') { - warnOnce( - 'This page appears to be missing CSS declarations for ' + 'Mapbox GL JS, which may cause the map to display incorrectly. ' + 'Please ensure your page includes mapbox-gl.css, as described ' + 'in https://www.mapbox.com/mapbox-gl-js/api/.', - ); + warnOnce('This page appears to be missing CSS declarations for ' + + 'Mapbox GL JS, which may cause the map to display incorrectly. ' + + 'Please ensure your page includes mapbox-gl.css, as described ' + + 'in https://www.mapbox.com/mapbox-gl-js/api/.'); } } @@ -3424,7 +3385,7 @@ class Map _requestDomTask(callback: () => void) { // This condition means that the map is idle: the callback needs to be called right now as // there won't be a triggered render to run the queue. - if (!this.loaded() || this.loaded() && !this.isMoving()) { + if (!this.loaded() || (this.loaded() && !this.isMoving())) { callback(); } else { this._domRenderTaskQueue.add(callback); diff --git a/src/ui/marker.js b/src/ui/marker.js index f86e341a256..89e47c85b6a 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -96,33 +96,28 @@ export default class Marker options = extend({element: options}, legacyOptions); } - bindAll( - [ + bindAll([ '_update', '_onMove', '_onUp', '_addDragHandler', '_onMapClick', '_onKeyPress', - '_clearFadeTimer', - ], - this, - ); - - this._anchor = options && options.anchor || 'center'; - this._color = options && options.color || '#3FB1CE'; - this._scale = options && options.scale || 1; - this._draggable = options && options.draggable || false; - this._clickTolerance = options && options.clickTolerance || 0; + '_clearFadeTimer' + ], this); + + this._anchor = (options && options.anchor) || 'center'; + this._color = (options && options.color) || '#3FB1CE'; + this._scale = (options && options.scale) || 1; + this._draggable = (options && options.draggable) || false; + this._clickTolerance = (options && options.clickTolerance) || 0; this._isDragging = false; this._state = 'inactive'; - this._rotation = options && options.rotation || 0; - this._rotationAlignment = options && options.rotationAlignment || 'auto'; - this._pitchAlignment = options && options.pitchAlignment && - options.pitchAlignment || - 'auto'; - this._updateMoving = (() => this._update(true)); - this._occludedOpacity = options && options.occludedOpacity || 0.2; + this._rotation = (options && options.rotation) || 0; + this._rotationAlignment = (options && options.rotationAlignment) || 'auto'; + this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto'; + this._updateMoving = () => this._update(true); + this._occludedOpacity = (options && options.occludedOpacity) || 0.2; if (!options || !options.element) { this._defaultMarker = true; @@ -133,54 +128,28 @@ export default class Marker const DEFAULT_HEIGHT = 41; const DEFAULT_WIDTH = 27; - const svg = DOM.createSVG( - 'svg', - { - display: 'block', - height: `${DEFAULT_HEIGHT * this._scale}px`, - width: `${DEFAULT_WIDTH * this._scale}px`, - viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}`, - }, - this._element, - ); + const svg = DOM.createSVG('svg', { + display: 'block', + height: `${DEFAULT_HEIGHT * this._scale}px`, + width: `${DEFAULT_WIDTH * this._scale}px`, + viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}` + }, this._element); - const gradient = DOM.createSVG( - 'radialGradient', - {id: 'shadowGradient'}, - DOM.createSVG('defs', {}, svg), - ); + const gradient = DOM.createSVG('radialGradient', {id: 'shadowGradient'}, DOM.createSVG('defs', {}, svg)); DOM.createSVG('stop', {offset: '10%', 'stop-opacity': 0.4}, gradient); DOM.createSVG('stop', {offset: '100%', 'stop-opacity': 0.05}, gradient); - DOM.createSVG( - 'ellipse', - {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, - svg, - ); // shadow - - DOM.createSVG( - 'path', - { - // marker shape - fill: this._color, - d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z', - }, - svg, - ); - DOM.createSVG( - 'path', - { - // border - opacity: 0.25, - d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z', - }, - svg, - ); + DOM.createSVG('ellipse', {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, svg); // shadow + + DOM.createSVG('path', { // marker shape + fill: this._color, + d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z' + }, svg); + DOM.createSVG('path', { // border + opacity: 0.25, + d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z' + }, svg); - DOM.createSVG( - 'circle', - {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, - svg, - ); // circle + DOM.createSVG('circle', {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, svg); // circle // if no element and no offset option given apply an offset for the default marker // the -14 as the y value of the default marker offset was determined as follows @@ -189,10 +158,10 @@ export default class Marker // the y value of the center of the shadow ellipse relative to the svg top left is 34.8 // offset to the svg center "height (41 / 2)" gives 34.8 - (41 / 2) and rounded for an integer pixel offset gives 14 // negative is used to move the marker up from the center so the tip is at the Marker lngLat - this._offset = Point.convert(options && options.offset || [0, -14]); + this._offset = Point.convert((options && options.offset) || [0, -14]); } else { this._element = options.element; - this._offset = Point.convert(options && options.offset || [0, 0]); + this._offset = Point.convert((options && options.offset) || [0, 0]); } if (!this._element.hasAttribute('aria-label')) diff --git a/src/util/actor.js b/src/util/actor.js index a02e0babc0b..cbddde2e6c4 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -128,7 +128,7 @@ class Actor { // in our queue, postMessage preempts this and messages can be processed. // We're using a MessageChannel object to get throttle the process() flow to one at a time. const callback = this.callbacks[id]; - const metadata = callback && callback.metadata || {type: "message"}; + const metadata = (callback && callback.metadata) || {type: "message"}; this.cancelCallbacks[id] = this.scheduler.add( () => this.processTask(id, data), metadata, From a554168d254dd31b70ff070b7cbc1283c7c0805c Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 13:17:48 +0200 Subject: [PATCH 11/72] Fix method-unbinding in evaluation_parameters.js --- src/style/evaluation_parameters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style/evaluation_parameters.js b/src/style/evaluation_parameters.js index 38778a3407c..6584faaa00f 100644 --- a/src/style/evaluation_parameters.js +++ b/src/style/evaluation_parameters.js @@ -29,7 +29,7 @@ class EvaluationParameters { } } - isSupportedScript(str: string): boolean { + isSupportedScript: (str: string) => boolean = (str) => { return isStringInSupportedScript(str, rtlTextPlugin.isLoaded()); } } From 4e3506ad9e6e7cfd80870fb212f43f3b8585cdba Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 13:21:26 +0200 Subject: [PATCH 12/72] Fix method-unbinding in uniform_binding.js --- src/render/uniform_binding.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index fabadb7226b..77c65b56151 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -34,7 +34,7 @@ class Uniform1i extends Uniform { this.current = 0; } - set(program: WebGLProgram, name: string, v: number): void { + set: (program: WebGLProgram, name: string, v: number) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; if (this.current !== v) { this.current = v; @@ -49,7 +49,7 @@ class Uniform1f extends Uniform { this.current = 0; } - set(program: WebGLProgram, name: string, v: number): void { + set: (program: WebGLProgram, name: string, v: number) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; if (this.current !== v) { this.current = v; @@ -64,7 +64,7 @@ class Uniform2f extends Uniform<[number, number]> { this.current = [0, 0]; } - set(program: WebGLProgram, name: string, v: [number, number]): void { + set: (program: WebGLProgram, name: string, v: [number, number]) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; if (v[0] !== this.current[0] || v[1] !== this.current[1]) { this.current = v; @@ -79,7 +79,7 @@ class Uniform3f extends Uniform<[number, number, number]> { this.current = [0, 0, 0]; } - set(program: WebGLProgram, name: string, v: [number, number, number]): void { + set: (program: WebGLProgram, name: string, v: [number, number, number]) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; if (v[0] !== this.current[0] || v[1] !== this.current[1] || v[2] !== this.current[2]) { this.current = v; @@ -94,7 +94,7 @@ class Uniform4f extends Uniform<[number, number, number, number]> { this.current = [0, 0, 0, 0]; } - set(program: WebGLProgram, name: string, v: [number, number, number, number]): void { + set: (program: WebGLProgram, name: string, v: [number, number, number, number]) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; if (v[0] !== this.current[0] || v[1] !== this.current[1] || v[2] !== this.current[2] || v[3] !== this.current[3]) { @@ -110,7 +110,7 @@ class UniformColor extends Uniform { this.current = Color.transparent; } - set(program: WebGLProgram, name: string, v: Color): void { + set: (program: WebGLProgram, name: string, v: Color) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; if (v.r !== this.current.r || v.g !== this.current.g || v.b !== this.current.b || v.a !== this.current.a) { @@ -127,7 +127,7 @@ class UniformMatrix4f extends Uniform { this.current = emptyMat4; } - set(program: WebGLProgram, name: string, v: Float32Array): void { + set: (program: WebGLProgram, name: string, v: Float32Array) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; // The vast majority of matrix comparisons that will trip this set // happen at i=12 or i=0, so we check those first to avoid lots of @@ -154,7 +154,7 @@ class UniformMatrix3f extends Uniform { this.current = emptyMat3; } - set(program: WebGLProgram, name: string, v: Float32Array): void { + set: (program: WebGLProgram, name: string, v: Float32Array) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; for (let i = 0; i < 9; i++) { if (v[i] !== this.current[i]) { @@ -173,7 +173,7 @@ class UniformMatrix2f extends Uniform { this.current = emptyMat2; } - set(program: WebGLProgram, name: string, v: Float32Array): void { + set: (program: WebGLProgram, name: string, v: Float32Array) => void = (program, name, v) => { if (!this.fetchUniformLocation(program, name)) return; for (let i = 0; i < 4; i++) { if (v[i] !== this.current[i]) { From 5c2cbce126ef3d7baef0cb215fcfa193f5f4901e Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 13:27:26 +0200 Subject: [PATCH 13/72] Fix method-unbinding in lng_lat_bounds.js --- src/geo/lng_lat_bounds.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/geo/lng_lat_bounds.js b/src/geo/lng_lat_bounds.js index cb9121f83f3..1325a5e27ca 100644 --- a/src/geo/lng_lat_bounds.js +++ b/src/geo/lng_lat_bounds.js @@ -97,6 +97,7 @@ class LngLatBounds { if (!sw2 || !ne2) return this; } else if (Array.isArray(obj)) { + // $FlowFixMe[method-unbinding] if (obj.length === 4 || obj.every(Array.isArray)) { const lngLatBoundsObj = ((obj: any): LngLatBoundsLike); return this.extend(LngLatBounds.convert(lngLatBoundsObj)); From e04972eb5191eb0a5258f929c136da62ab1303e2 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 13:29:06 +0200 Subject: [PATCH 14/72] Fix method-unbinding in canvas_source.js --- src/source/canvas_source.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/source/canvas_source.js b/src/source/canvas_source.js index 3e1d4e10187..a28313bc11d 100644 --- a/src/source/canvas_source.js +++ b/src/source/canvas_source.js @@ -162,7 +162,7 @@ class CanvasSource extends ImageSource { return this.canvas; } - onAdd(map: Map) { + onAdd: (map: Map) => void = (map) => { this.map = map; this.load(); if (this.canvas) { @@ -170,7 +170,7 @@ class CanvasSource extends ImageSource { } } - onRemove() { + onRemove: () => void = () => { this.pause(); } @@ -189,7 +189,7 @@ class CanvasSource extends ImageSource { // setCoordinates inherited from ImageSource - prepare() { + prepare: () => void = () => { let resize = false; if (this.canvas.width !== this.width) { this.width = this.canvas.width; From a0d00be3909529baf8bf862950c5a1205d0dd510 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 13:31:12 +0200 Subject: [PATCH 15/72] Fix method-unbinding in video_source.js --- src/source/raster_dem_tile_source.js | 2 +- src/source/video_source.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index e177d5542c3..762fb88b122 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -116,7 +116,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { return neighboringTiles; } - unloadTile: (tile: Tile) => void = (tile) => { + unloadTile: (tile: Tile, callback: Callback) => void = (tile) => { if (tile.demTexture) this.map.painter.saveTileTexture(tile.demTexture); if (tile.fbo) { tile.fbo.destroy(); diff --git a/src/source/video_source.js b/src/source/video_source.js index 9c5682a077c..f1203bc4399 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -154,7 +154,7 @@ class VideoSource extends ImageSource { return this.video; } - onAdd(map: Map) { + onAdd: (map: Map) => void = (map: Map) => { if (this.map) return; this.map = map; this.load(); @@ -198,7 +198,7 @@ class VideoSource extends ImageSource { */ // setCoordinates inherited from ImageSource - prepare() { + prepare: () => void = () => { if (Object.keys(this.tiles).length === 0 || this.video.readyState < 2) { return; // not enough data for current position } From de55ec03a7d7c683ff3a34c1ce5af7a047883b43 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 14:31:42 +0200 Subject: [PATCH 16/72] Fix method-unbinding in style/ and style-spec/ --- src/style-spec/expression/compound_expression.js | 2 +- src/style-spec/expression/definitions/assertion.js | 2 +- src/style-spec/expression/definitions/at.js | 2 +- src/style-spec/expression/definitions/case.js | 2 +- src/style-spec/expression/definitions/coalesce.js | 2 +- src/style-spec/expression/definitions/coercion.js | 2 +- src/style-spec/expression/definitions/collator.js | 2 +- src/style-spec/expression/definitions/comparison.js | 2 +- src/style-spec/expression/definitions/format.js | 2 +- src/style-spec/expression/definitions/image.js | 2 +- src/style-spec/expression/definitions/in.js | 2 +- src/style-spec/expression/definitions/index_of.js | 2 +- src/style-spec/expression/definitions/interpolate.js | 2 +- src/style-spec/expression/definitions/length.js | 2 +- src/style-spec/expression/definitions/let.js | 2 +- src/style-spec/expression/definitions/literal.js | 2 +- src/style-spec/expression/definitions/match.js | 2 +- src/style-spec/expression/definitions/number_format.js | 2 +- src/style-spec/expression/definitions/slice.js | 2 +- src/style-spec/expression/definitions/step.js | 2 +- src/style-spec/expression/definitions/var.js | 2 +- src/style-spec/expression/definitions/within.js | 2 +- src/style-spec/expression/index.js | 10 +++++----- src/style/style_layer/circle_style_layer.js | 6 +++--- src/style/style_layer/custom_style_layer.js | 2 +- src/style/style_layer/fill_extrusion_style_layer.js | 6 +++--- src/style/style_layer/fill_style_layer.js | 6 +++--- src/style/style_layer/heatmap_style_layer.js | 6 +++--- src/style/style_layer/line_style_layer.js | 6 +++--- src/style/style_layer/symbol_style_layer.js | 4 ++-- 30 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/style-spec/expression/compound_expression.js b/src/style-spec/expression/compound_expression.js index 2da90b1373a..b8fddc51986 100644 --- a/src/style-spec/expression/compound_expression.js +++ b/src/style-spec/expression/compound_expression.js @@ -47,7 +47,7 @@ class CompoundExpression implements Expression { return [this.name].concat(this.args.map(arg => arg.serialize())); } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { const op: string = (args[0]: any); const definition = CompoundExpression.definitions[op]; if (!definition) { diff --git a/src/style-spec/expression/definitions/assertion.js b/src/style-spec/expression/definitions/assertion.js index 0c4a66ce5ae..8b64372661d 100644 --- a/src/style-spec/expression/definitions/assertion.js +++ b/src/style-spec/expression/definitions/assertion.js @@ -36,7 +36,7 @@ class Assertion implements Expression { this.args = args; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length < 2) return context.error(`Expected at least one argument.`); diff --git a/src/style-spec/expression/definitions/at.js b/src/style-spec/expression/definitions/at.js index 780a50c2954..fd62c465414 100644 --- a/src/style-spec/expression/definitions/at.js +++ b/src/style-spec/expression/definitions/at.js @@ -21,7 +21,7 @@ class At implements Expression { this.input = input; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?At { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?At = (args, context) => { if (args.length !== 3) return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/case.js b/src/style-spec/expression/definitions/case.js index fb345fe6b6b..390c1fff46f 100644 --- a/src/style-spec/expression/definitions/case.js +++ b/src/style-spec/expression/definitions/case.js @@ -23,7 +23,7 @@ class Case implements Expression { this.otherwise = otherwise; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Case { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Case = (args, context) => { if (args.length < 4) return context.error(`Expected at least 3 arguments, but found only ${args.length - 1}.`); if (args.length % 2 !== 0) diff --git a/src/style-spec/expression/definitions/coalesce.js b/src/style-spec/expression/definitions/coalesce.js index e1e52519898..1c4c2d2d8ef 100644 --- a/src/style-spec/expression/definitions/coalesce.js +++ b/src/style-spec/expression/definitions/coalesce.js @@ -19,7 +19,7 @@ class Coalesce implements Expression { this.args = args; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Coalesce { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Coalesce = (args, context) => { if (args.length < 2) { return context.error("Expectected at least one argument."); } diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index beac832fc8c..7da48ba88bb 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -38,7 +38,7 @@ class Coercion implements Expression { this.args = args; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length < 2) return context.error(`Expected at least one argument.`); diff --git a/src/style-spec/expression/definitions/collator.js b/src/style-spec/expression/definitions/collator.js index 2ec43b10765..be08a37231b 100644 --- a/src/style-spec/expression/definitions/collator.js +++ b/src/style-spec/expression/definitions/collator.js @@ -21,7 +21,7 @@ export default class CollatorExpression implements Expression { this.diacriticSensitive = diacriticSensitive; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) =>{ if (args.length !== 2) return context.error(`Expected one argument.`); diff --git a/src/style-spec/expression/definitions/comparison.js b/src/style-spec/expression/definitions/comparison.js index beb5c91e076..bdc33403f5d 100644 --- a/src/style-spec/expression/definitions/comparison.js +++ b/src/style-spec/expression/definitions/comparison.js @@ -77,7 +77,7 @@ function makeComparison(op: ComparisonOperator, compareBasic: (EvaluationContext this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value'; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length !== 3 && args.length !== 4) return context.error(`Expected two or three arguments.`); diff --git a/src/style-spec/expression/definitions/format.js b/src/style-spec/expression/definitions/format.js index 6445edd63ff..7555adcfed3 100644 --- a/src/style-spec/expression/definitions/format.js +++ b/src/style-spec/expression/definitions/format.js @@ -27,7 +27,7 @@ export default class FormatExpression implements Expression { this.sections = sections; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length < 2) { return context.error(`Expected at least one argument.`); } diff --git a/src/style-spec/expression/definitions/image.js b/src/style-spec/expression/definitions/image.js index b8095c3bbc6..90eeeeb6229 100644 --- a/src/style-spec/expression/definitions/image.js +++ b/src/style-spec/expression/definitions/image.js @@ -17,7 +17,7 @@ export default class ImageExpression implements Expression { this.input = input; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length !== 2) { return context.error(`Expected two arguments.`); } diff --git a/src/style-spec/expression/definitions/in.js b/src/style-spec/expression/definitions/in.js index fd5773049c8..42f4a9dc9fd 100644 --- a/src/style-spec/expression/definitions/in.js +++ b/src/style-spec/expression/definitions/in.js @@ -20,7 +20,7 @@ class In implements Expression { this.haystack = haystack; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?In { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?In = (args, context) => { if (args.length !== 3) { return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); } diff --git a/src/style-spec/expression/definitions/index_of.js b/src/style-spec/expression/definitions/index_of.js index 973f9b2c93c..4f8db4cce49 100644 --- a/src/style-spec/expression/definitions/index_of.js +++ b/src/style-spec/expression/definitions/index_of.js @@ -22,7 +22,7 @@ class IndexOf implements Expression { this.fromIndex = fromIndex; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?IndexOf { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?IndexOf = (args, context) => { if (args.length <= 2 || args.length >= 5) { return context.error(`Expected 3 or 4 arguments, but found ${args.length - 1} instead.`); } diff --git a/src/style-spec/expression/definitions/interpolate.js b/src/style-spec/expression/definitions/interpolate.js index 30e60d39084..0af497213fd 100644 --- a/src/style-spec/expression/definitions/interpolate.js +++ b/src/style-spec/expression/definitions/interpolate.js @@ -56,7 +56,7 @@ class Interpolate implements Expression { return t; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Interpolate { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Interpolate = (args, context) => { let [operator, interpolation, input, ...rest] = args; if (!Array.isArray(interpolation) || interpolation.length === 0) { diff --git a/src/style-spec/expression/definitions/length.js b/src/style-spec/expression/definitions/length.js index 2661adfbabf..d61a10d2da1 100644 --- a/src/style-spec/expression/definitions/length.js +++ b/src/style-spec/expression/definitions/length.js @@ -19,7 +19,7 @@ class Length implements Expression { this.input = input; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Length { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Length = (args, context) => { if (args.length !== 2) return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/let.js b/src/style-spec/expression/definitions/let.js index 9a04f2d5bac..deeaa4ab1fe 100644 --- a/src/style-spec/expression/definitions/let.js +++ b/src/style-spec/expression/definitions/let.js @@ -27,7 +27,7 @@ class Let implements Expression { fn(this.result); } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Let { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Let = (args, context) => { if (args.length < 4) return context.error(`Expected at least 3 arguments, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index ae1f90966d6..f75035a0bc5 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -18,7 +18,7 @@ class Literal implements Expression { this.value = value; } - static parse(args: $ReadOnlyArray, context: ParsingContext): void | Literal { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => void | Literal = (args, context) => { if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/match.js b/src/style-spec/expression/definitions/match.js index 8f87b07ab31..b623d8f0412 100644 --- a/src/style-spec/expression/definitions/match.js +++ b/src/style-spec/expression/definitions/match.js @@ -30,7 +30,7 @@ class Match implements Expression { this.otherwise = otherwise; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Match { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Match = (args, context) => { if (args.length < 5) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); if (args.length % 2 !== 1) diff --git a/src/style-spec/expression/definitions/number_format.js b/src/style-spec/expression/definitions/number_format.js index c187817467d..dc943797389 100644 --- a/src/style-spec/expression/definitions/number_format.js +++ b/src/style-spec/expression/definitions/number_format.js @@ -59,7 +59,7 @@ export default class NumberFormat implements Expression { this.maxFractionDigits = maxFractionDigits; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length !== 3) return context.error(`Expected two arguments.`); diff --git a/src/style-spec/expression/definitions/slice.js b/src/style-spec/expression/definitions/slice.js index 9c5cbbc017a..1e17bd1949f 100644 --- a/src/style-spec/expression/definitions/slice.js +++ b/src/style-spec/expression/definitions/slice.js @@ -23,7 +23,7 @@ class Slice implements Expression { } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Slice { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Slice = (args, context) => { if (args.length <= 2 || args.length >= 5) { return context.error(`Expected 3 or 4 arguments, but found ${args.length - 1} instead.`); } diff --git a/src/style-spec/expression/definitions/step.js b/src/style-spec/expression/definitions/step.js index 89fdf128419..43c9e5c1069 100644 --- a/src/style-spec/expression/definitions/step.js +++ b/src/style-spec/expression/definitions/step.js @@ -29,7 +29,7 @@ class Step implements Expression { } } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Step { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Step = (args, context) => { if (args.length - 1 < 4) { return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); } diff --git a/src/style-spec/expression/definitions/var.js b/src/style-spec/expression/definitions/var.js index 8146a8a04d4..61937545d2f 100644 --- a/src/style-spec/expression/definitions/var.js +++ b/src/style-spec/expression/definitions/var.js @@ -16,7 +16,7 @@ class Var implements Expression { this.boundExpression = boundExpression; } - static parse(args: $ReadOnlyArray, context: ParsingContext): void | Var { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => void | Var = (args, context) => { if (args.length !== 2 || typeof args[1] !== 'string') return context.error(`'var' expression requires exactly one string literal argument.`); diff --git a/src/style-spec/expression/definitions/within.js b/src/style-spec/expression/definitions/within.js index 6e5c98d6f4a..c5e9f4b4d00 100644 --- a/src/style-spec/expression/definitions/within.js +++ b/src/style-spec/expression/definitions/within.js @@ -299,7 +299,7 @@ class Within implements Expression { this.geometries = geometries; } - static parse(args: $ReadOnlyArray, context: ParsingContext): ?Within { + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Within = (args, context) => { if (args.length !== 2) return context.error(`'within' expression requires exactly one argument, but found ${args.length - 1} instead.`); if (isValue(args[1])) { diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 6af5c44df26..f7bc94ecf7e 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -78,7 +78,7 @@ export class StyleExpression { return this.expression.evaluate(this._evaluator); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any { + evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => any = (globals, feature, featureState, canonical, availableImages, formattedSection, featureTileCoord, featureDistanceData) => { this._evaluator.globals = globals; this._evaluator.feature = feature || null; this._evaluator.featureState = featureState || null; @@ -154,7 +154,7 @@ export class ZoomConstantExpression { return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any = (globals, feature, featureState, canonical, availableImages, formattedSection) => { return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } } @@ -175,15 +175,15 @@ export class ZoomDependentExpression { this.interpolationType = interpolationType; } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluateWithoutErrorHandling: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any = (globals, feature, featureState, canonical, availableImages, formattedSection) => { return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any = (globals, feature, featureState, canonical, availableImages, formattedSection) => { return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } - interpolationFactor(input: number, lower: number, upper: number): number { + interpolationFactor: (input: number, lower: number, upper: number) => number = (input, lower, upper) => { if (this.interpolationType) { return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper); } else { diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index 91b2c04cbc4..2365a977567 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -40,21 +40,21 @@ class CircleStyleLayer extends StyleLayer { return new CircleBucket(parameters); } - queryRadius(bucket: Bucket): number { + queryRadius: (bucket: Bucket) => number = (bucket: Bucket) => { const circleBucket: CircleBucket = (bucket: any); return getMaximumPaintValue('circle-radius', this, circleBucket) + getMaximumPaintValue('circle-stroke-width', this, circleBucket) + translateDistance(this.paint.get('circle-translate')); } - queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, feature: IVectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, transform: Transform, pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler): boolean { + elevationHelper: ?DEMSampler) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { const translation = tilespaceTranslate( this.paint.get('circle-translate'), diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index 0b6277f0ece..c90a0ae43cc 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -231,7 +231,7 @@ class CustomStyleLayer } }; - onRemove(map: Map) { + onRemove: (map: Map) => void = (map: Map) => { if (this.implementation.onRemove) { this.implementation.onRemove(map, map.painter.context.gl); } diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index 3125e9339ae..baf21da5e34 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -45,7 +45,7 @@ class FillExtrusionStyleLayer extends StyleLayer { return new FillExtrusionBucket(parameters); } - queryRadius(): number { + queryRadius: () => number = () => { return translateDistance(this.paint.get('fill-extrusion-translate')); } @@ -63,7 +63,7 @@ class FillExtrusionStyleLayer extends StyleLayer { return new ProgramConfiguration(this, zoom); } - queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, feature: IVectorTileFeature, featureState: FeatureState, geometry: Array>, @@ -71,7 +71,7 @@ class FillExtrusionStyleLayer extends StyleLayer { transform: Transform, pixelPosMatrix: Float32Array, elevationHelper: ?DEMSampler, - layoutVertexArrayOffset: number): boolean | number { + layoutVertexArrayOffset: number) => boolean | number = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper, layoutVertexArrayOffset) => { const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), this.paint.get('fill-extrusion-translate-anchor'), diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index 6c1d5f4366f..7c0efba804f 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -61,16 +61,16 @@ class FillStyleLayer extends StyleLayer { return new FillBucket(parameters); } - queryRadius(): number { + queryRadius: () => number = () => { return translateDistance(this.paint.get('fill-translate')); } - queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, feature: IVectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, - transform: Transform): boolean { + transform: Transform) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform) => { if (queryGeometry.queryGeometry.isAboveHorizon) return false; const translatedPolygon = translate(queryGeometry.tilespaceGeometry, diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index 3f64ff3433c..f682f956641 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -68,18 +68,18 @@ class HeatmapStyleLayer extends StyleLayer { } } - queryRadius(bucket: Bucket): number { + queryRadius: (bucket: Bucket) => number = (bucket) => { return getMaximumPaintValue('heatmap-radius', this, ((bucket: any): CircleBucket<*>)); } - queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, feature: IVectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, transform: Transform, pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler): boolean { + elevationHelper: ?DEMSampler) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { const size = this.paint.get('heatmap-radius').evaluate(feature, featureState); return queryIntersectsCircle( diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index ae6dc5353ca..79b222f7789 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -96,7 +96,7 @@ class LineStyleLayer extends StyleLayer { return new ProgramConfiguration(this, zoom); } - queryRadius(bucket: Bucket): number { + queryRadius: (bucket: Bucket) => number = (bucket) => { const lineBucket: LineBucket = (bucket: any); const width = getLineWidth( getMaximumPaintValue('line-width', this, lineBucket), @@ -105,12 +105,12 @@ class LineStyleLayer extends StyleLayer { return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate')); } - queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, feature: IVectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, - transform: Transform): boolean { + transform: Transform) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform) => { if (queryGeometry.queryGeometry.isAboveHorizon) return false; const translatedPolygon = translate(queryGeometry.tilespaceGeometry, diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index d37eaec6e61..88fde1212b3 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -110,11 +110,11 @@ class SymbolStyleLayer extends StyleLayer { return new SymbolBucket(parameters); } - queryRadius(): number { + queryRadius: () => number = () => { return 0; } - queryIntersectsFeature(): boolean { + queryIntersectsFeature: () => boolean = () => { assert(false); // Should take a different path in FeatureIndex return false; } From 64327c34f329e00effc5094a4f2dccaea686dec1 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Thu, 23 Feb 2023 14:41:30 +0200 Subject: [PATCH 17/72] fix eslint problems for style files --- .../expression/definitions/collator.js | 2 +- src/style/style_layer/circle_style_layer.js | 8 +- .../style_layer/fill_extrusion_style_layer.js | 76 +++++++++---------- src/style/style_layer/fill_style_layer.js | 8 +- src/style/style_layer/heatmap_style_layer.js | 6 +- src/style/style_layer/line_style_layer.js | 18 ++--- 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/style-spec/expression/definitions/collator.js b/src/style-spec/expression/definitions/collator.js index be08a37231b..8f7d79a5731 100644 --- a/src/style-spec/expression/definitions/collator.js +++ b/src/style-spec/expression/definitions/collator.js @@ -21,7 +21,7 @@ export default class CollatorExpression implements Expression { this.diacriticSensitive = diacriticSensitive; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) =>{ + static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { if (args.length !== 2) return context.error(`Expected one argument.`); diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index 2365a977567..81d997893ac 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -56,18 +56,18 @@ class CircleStyleLayer extends StyleLayer { pixelPosMatrix: Float32Array, elevationHelper: ?DEMSampler) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { - const translation = tilespaceTranslate( + const translation = tilespaceTranslate( this.paint.get('circle-translate'), this.paint.get('circle-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - const size = this.paint.get('circle-radius').evaluate(feature, featureState) + + const size = this.paint.get('circle-radius').evaluate(feature, featureState) + this.paint.get('circle-stroke-width').evaluate(feature, featureState); - return queryIntersectsCircle(queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, + return queryIntersectsCircle(queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, this.paint.get('circle-pitch-alignment') === 'map', this.paint.get('circle-pitch-scale') === 'map', translation, size); - } + } getProgramIds(): Array { return ['circle']; diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index baf21da5e34..5fc209ff029 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -73,47 +73,47 @@ class FillExtrusionStyleLayer extends StyleLayer { elevationHelper: ?DEMSampler, layoutVertexArrayOffset: number) => boolean | number = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper, layoutVertexArrayOffset) => { - const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), + const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), this.paint.get('fill-extrusion-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); - const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); - - const centroid = [0, 0]; - const terrainVisible = elevationHelper && transform.elevation; - const exaggeration = transform.elevation ? transform.elevation.exaggeration() : 1; - const bucket = queryGeometry.tile.getBucket(this); - if (terrainVisible && bucket instanceof FillExtrusionBucket) { - const centroidVertexArray = bucket.centroidVertexArray; - - // See FillExtrusionBucket#encodeCentroid(), centroid is inserted at vertexOffset + 1 - const centroidOffset = layoutVertexArrayOffset + 1; - if (centroidOffset < centroidVertexArray.length) { - centroid[0] = centroidVertexArray.geta_centroid_pos0(centroidOffset); - centroid[1] = centroidVertexArray.geta_centroid_pos1(centroidOffset); - } - } - - // Early exit if fill extrusion is still hidden while waiting for backfill - const isHidden = centroid[0] === 0 && centroid[1] === 1; - if (isHidden) return false; - - if (transform.projection.name === 'globe') { - // Fill extrusion geometry has to be resampled so that large planar polygons - // can be rendered on the curved surface - const bounds = [new Point(0, 0), new Point(EXTENT, EXTENT)]; - const resampledGeometry = resampleFillExtrusionPolygonsForGlobe([geometry], bounds, queryGeometry.tileID.canonical); - geometry = resampledGeometry.map(clipped => clipped.polygon).flat(); - } - - const demSampler = terrainVisible ? elevationHelper : null; - const [projectedBase, projectedTop] = projectExtrusion(transform, geometry, base, height, translation, pixelPosMatrix, demSampler, centroid, exaggeration, transform.center.lat, queryGeometry.tileID.canonical); - - const screenQuery = queryGeometry.queryGeometry; - const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry; - return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry); - } + const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); + const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); + + const centroid = [0, 0]; + const terrainVisible = elevationHelper && transform.elevation; + const exaggeration = transform.elevation ? transform.elevation.exaggeration() : 1; + const bucket = queryGeometry.tile.getBucket(this); + if (terrainVisible && bucket instanceof FillExtrusionBucket) { + const centroidVertexArray = bucket.centroidVertexArray; + + // See FillExtrusionBucket#encodeCentroid(), centroid is inserted at vertexOffset + 1 + const centroidOffset = layoutVertexArrayOffset + 1; + if (centroidOffset < centroidVertexArray.length) { + centroid[0] = centroidVertexArray.geta_centroid_pos0(centroidOffset); + centroid[1] = centroidVertexArray.geta_centroid_pos1(centroidOffset); + } + } + + // Early exit if fill extrusion is still hidden while waiting for backfill + const isHidden = centroid[0] === 0 && centroid[1] === 1; + if (isHidden) return false; + + if (transform.projection.name === 'globe') { + // Fill extrusion geometry has to be resampled so that large planar polygons + // can be rendered on the curved surface + const bounds = [new Point(0, 0), new Point(EXTENT, EXTENT)]; + const resampledGeometry = resampleFillExtrusionPolygonsForGlobe([geometry], bounds, queryGeometry.tileID.canonical); + geometry = resampledGeometry.map(clipped => clipped.polygon).flat(); + } + + const demSampler = terrainVisible ? elevationHelper : null; + const [projectedBase, projectedTop] = projectExtrusion(transform, geometry, base, height, translation, pixelPosMatrix, demSampler, centroid, exaggeration, transform.center.lat, queryGeometry.tileID.canonical); + + const screenQuery = queryGeometry.queryGeometry; + const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry; + return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry); + } } function dot(a, b) { diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index 7c0efba804f..d36cfe28d0d 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -71,14 +71,14 @@ class FillStyleLayer extends StyleLayer { geometry: Array>, zoom: number, transform: Transform) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform) => { - if (queryGeometry.queryGeometry.isAboveHorizon) return false; + if (queryGeometry.queryGeometry.isAboveHorizon) return false; - const translatedPolygon = translate(queryGeometry.tilespaceGeometry, + const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('fill-translate'), this.paint.get('fill-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - return polygonIntersectsMultiPolygon(translatedPolygon, geometry); - } + return polygonIntersectsMultiPolygon(translatedPolygon, geometry); + } isTileClipped(): boolean { return true; diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index f682f956641..964498a4eee 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -81,11 +81,11 @@ class HeatmapStyleLayer extends StyleLayer { pixelPosMatrix: Float32Array, elevationHelper: ?DEMSampler) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { - const size = this.paint.get('heatmap-radius').evaluate(feature, featureState); - return queryIntersectsCircle( + const size = this.paint.get('heatmap-radius').evaluate(feature, featureState); + return queryIntersectsCircle( queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, true, true, new Point(0, 0), size); - } + } hasOffscreenPass(): boolean { return this.paint.get('heatmap-opacity') !== 0 && this.visibility !== 'none'; diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 79b222f7789..59243fff51a 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -111,22 +111,22 @@ class LineStyleLayer extends StyleLayer { geometry: Array>, zoom: number, transform: Transform) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform) => { - if (queryGeometry.queryGeometry.isAboveHorizon) return false; + if (queryGeometry.queryGeometry.isAboveHorizon) return false; - const translatedPolygon = translate(queryGeometry.tilespaceGeometry, + const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('line-translate'), this.paint.get('line-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( + const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( this.paint.get('line-width').evaluate(feature, featureState), this.paint.get('line-gap-width').evaluate(feature, featureState)); - const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState); - if (lineOffset) { - geometry = offsetLine(geometry, lineOffset * queryGeometry.pixelToTileUnitsFactor); - } + const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState); + if (lineOffset) { + geometry = offsetLine(geometry, lineOffset * queryGeometry.pixelToTileUnitsFactor); + } - return polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth); - } + return polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth); + } isTileClipped(): boolean { return true; From 5ca6152b7ad94e0edc116f885c44970a3bb9690f Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Thu, 23 Feb 2023 14:40:11 +0200 Subject: [PATCH 18/72] fix flow problems for ui/handler files --- src/ui/handler/box_zoom.js | 4 ++-- src/ui/handler/click_zoom.js | 2 +- src/ui/handler/keyboard.js | 2 +- src/ui/handler/map_event.js | 32 ++++++++++++++--------------- src/ui/handler/mouse.js | 12 +++++------ src/ui/handler/scroll_zoom.js | 4 ++-- src/ui/handler/tap_drag_zoom.js | 8 ++++---- src/ui/handler/tap_zoom.js | 8 ++++---- src/ui/handler/touch_pan.js | 8 ++++---- src/ui/handler/touch_zoom_rotate.js | 8 ++++---- 10 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/ui/handler/box_zoom.js b/src/ui/handler/box_zoom.js index 31eda111372..9b2bc437e0d 100644 --- a/src/ui/handler/box_zoom.js +++ b/src/ui/handler/box_zoom.js @@ -82,7 +82,7 @@ class BoxZoomHandler { this._enabled = false; } - mousedown(e: MouseEvent, point: Point) { + mousedown: (e: MouseEvent, point: Point) => void = (e, point) => { if (!this.isEnabled()) return; if (!(e.shiftKey && e.button === 0)) return; @@ -146,7 +146,7 @@ class BoxZoomHandler { } } - keydown(e: KeyboardEvent) { + keydown: (e: KeyboardEvent) => void = (e) => { if (!this._active) return; if (e.keyCode === 27) { diff --git a/src/ui/handler/click_zoom.js b/src/ui/handler/click_zoom.js index 9010c5b4fbf..c2ff1e259a7 100644 --- a/src/ui/handler/click_zoom.js +++ b/src/ui/handler/click_zoom.js @@ -21,7 +21,7 @@ export default class ClickZoomHandler { this.reset(); } - dblclick(e: MouseEvent, point: Point): HandlerResult { + dblclick: (e: MouseEvent, point: Point) => HandlerResult = (e, point) => { e.preventDefault(); return { cameraAnimation: (map: Map) => { diff --git a/src/ui/handler/keyboard.js b/src/ui/handler/keyboard.js index f47b2b1eb85..a1a267f0014 100644 --- a/src/ui/handler/keyboard.js +++ b/src/ui/handler/keyboard.js @@ -54,7 +54,7 @@ class KeyboardHandler { this._active = false; } - keydown(e: KeyboardEvent): ?HandlerResult { + keydown: (e: KeyboardEvent) => ?HandlerResult = (e) => { if (e.altKey || e.ctrlKey || e.metaKey) return; let zoomDir = 0; diff --git a/src/ui/handler/map_event.js b/src/ui/handler/map_event.js index a611fa0cc92..a2f74a3d9c9 100644 --- a/src/ui/handler/map_event.js +++ b/src/ui/handler/map_event.js @@ -22,13 +22,13 @@ export class MapEventHandler { this._mousedownPos = undefined; } - wheel(e: WheelEvent): ?HandlerResult { + wheel: (e: WheelEvent) => ?HandlerResult = (e) => { // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - ScrollZoom return this._firePreventable(new MapWheelEvent(e.type, this._map, e)); } - mousedown(e: MouseEvent, point: Point): ?HandlerResult { + mousedown: (e: MouseEvent, point: Point) => ?HandlerResult = (e, point) => { this._mousedownPos = point; // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - MousePan @@ -36,9 +36,9 @@ export class MapEventHandler { // - MousePitch // - DblclickHandler return this._firePreventable(new MapMouseEvent(e.type, this._map, e)); - } + }; - mouseup(e: MouseEvent) { + mouseup: (e: MouseEvent) => void = (e) => { this._map.fire(new MapMouseEvent(e.type, this._map, e)); } @@ -48,27 +48,27 @@ export class MapEventHandler { this._map.fire(new MapMouseEvent(synth.type, this._map, synth)); } - click(e: MouseEvent, point: Point) { + click: (e: MouseEvent, point: Point) => void = (e, point) => { if (this._mousedownPos && this._mousedownPos.dist(point) >= this._clickTolerance) return; this.preclick(e); this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - dblclick(e: MouseEvent): ?HandlerResult { + dblclick: (e: MouseEvent) => ?HandlerResult = (e) => { // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - DblClickZoom return this._firePreventable(new MapMouseEvent(e.type, this._map, e)); } - mouseover(e: MouseEvent) { + mouseover: (e: MouseEvent) => void = (e) => { this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - mouseout(e: MouseEvent) { + mouseout: (e: MouseEvent) => void = (e) => { this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - touchstart(e: TouchEvent): ?HandlerResult { + touchstart: (e: TouchEvent) => ?HandlerResult = (e) => { // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - TouchPan // - TouchZoom @@ -79,15 +79,15 @@ export class MapEventHandler { return this._firePreventable(new MapTouchEvent(e.type, this._map, e)); } - touchmove(e: TouchEvent) { + touchmove: (e: TouchEvent) => void = (e) => { this._map.fire(new MapTouchEvent(e.type, this._map, e)); } - touchend(e: TouchEvent) { + touchend: (e: TouchEvent) => void = (e) => { this._map.fire(new MapTouchEvent(e.type, this._map, e)); } - touchcancel(e: TouchEvent) { + touchcancel: (e: TouchEvent) => void = (e) => { this._map.fire(new MapTouchEvent(e.type, this._map, e)); } @@ -124,23 +124,23 @@ export class BlockableMapEventHandler { this._contextMenuEvent = undefined; } - mousemove(e: MouseEvent) { + mousemove: (e: MouseEvent) => void = (e) => { // mousemove map events should not be fired when interaction handlers (pan, rotate, etc) are active this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - mousedown() { + mousedown: () => void = () => { this._delayContextMenu = true; } - mouseup() { + mouseup: () => void = () => { this._delayContextMenu = false; if (this._contextMenuEvent) { this._map.fire(new MapMouseEvent('contextmenu', this._map, this._contextMenuEvent)); delete this._contextMenuEvent; } } - contextmenu(e: MouseEvent) { + contextmenu: (e: MouseEvent) => void = (e) => { if (this._delayContextMenu) { // Mac: contextmenu fired on mousedown; we save it until mouseup for consistency's sake this._contextMenuEvent = e; diff --git a/src/ui/handler/mouse.js b/src/ui/handler/mouse.js index df64d23075a..3712e94ea1a 100644 --- a/src/ui/handler/mouse.js +++ b/src/ui/handler/mouse.js @@ -51,7 +51,7 @@ class MouseHandler { return {}; // implemented by child } - mousedown(e: MouseEvent, point: Point) { + mousedown: (e: MouseEvent, point: Point) => void = (e, point) => { if (this._lastPoint) return; const eventButton = DOM.mouseButton(e); @@ -61,7 +61,7 @@ class MouseHandler { this._eventButton = eventButton; } - mousemoveWindow(e: MouseEvent, point: Point): ?HandlerResult { + mousemoveWindow: (e: MouseEvent, point: Point) => ?HandlerResult = (e, point) => { const lastPoint = this._lastPoint; if (!lastPoint) return; e.preventDefault(); @@ -85,7 +85,7 @@ class MouseHandler { return this._move(lastPoint, point); } - mouseupWindow(e: MouseEvent) { + mouseupWindow: (e: MouseEvent) => void = (e) => { if (!this._lastPoint) return; const eventButton = DOM.mouseButton(e); if (eventButton !== this._eventButton) return; @@ -113,7 +113,7 @@ class MouseHandler { export class MousePanHandler extends MouseHandler { - mousedown(e: MouseEvent, point: Point) { + mousedown: (e: MouseEvent, point: Point) => void = (e, point) => { super.mousedown(e, point); if (this._lastPoint) this._active = true; } @@ -143,7 +143,7 @@ export class MouseRotateHandler extends MouseHandler { } } - contextmenu(e: MouseEvent) { + contextmenu: (e: MouseEvent) => void = (e) => { // prevent browser context menu when necessary; we don't allow it with rotation // because we can't discern rotation gesture start from contextmenu on Mac e.preventDefault(); @@ -164,7 +164,7 @@ export class MousePitchHandler extends MouseHandler { } } - contextmenu(e: MouseEvent) { + contextmenu: (e: MouseEvent) => void = (e) => { // prevent browser context menu when necessary; we don't allow it with rotation // because we can't discern rotation gesture start from contextmenu on Mac e.preventDefault(); diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 21335b5ed59..2c073fb4e0f 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -160,7 +160,7 @@ class ScrollZoomHandler { } } - wheel(e: WheelEvent) { + wheel: (e: WheelEvent) => void = (e) => { if (!this.isEnabled()) return; if (this._map._cooperativeGestures) { @@ -263,7 +263,7 @@ class ScrollZoomHandler { } } - renderFrame(): ?HandlerResult { + renderFrame: () => ?HandlerResult = () => { if (!this._frameId) return; this._frameId = null; diff --git a/src/ui/handler/tap_drag_zoom.js b/src/ui/handler/tap_drag_zoom.js index 0953fd5d6c4..4fa6e8b224c 100644 --- a/src/ui/handler/tap_drag_zoom.js +++ b/src/ui/handler/tap_drag_zoom.js @@ -31,7 +31,7 @@ export default class TapDragZoomHandler { this._tap.reset(); } - touchstart(e: TouchEvent, points: Array, mapTouches: Array) { + touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { if (this._swipePoint) return; if (this._tapTime && e.timeStamp - this._tapTime > MAX_TAP_INTERVAL) { @@ -47,7 +47,7 @@ export default class TapDragZoomHandler { } - touchmove(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { + touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { if (!this._tapTime) { this._tap.touchmove(e, points, mapTouches); } else if (this._swipePoint) { @@ -68,7 +68,7 @@ export default class TapDragZoomHandler { } } - touchend(e: TouchEvent, points: Array, mapTouches: Array) { + touchend: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { if (!this._tapTime) { const point = this._tap.touchend(e, points, mapTouches); if (point) { @@ -81,7 +81,7 @@ export default class TapDragZoomHandler { } } - touchcancel() { + touchcancel: () => void = () => { this.reset(); } diff --git a/src/ui/handler/tap_zoom.js b/src/ui/handler/tap_zoom.js index afeb7a48760..62e1b250a4e 100644 --- a/src/ui/handler/tap_zoom.js +++ b/src/ui/handler/tap_zoom.js @@ -32,17 +32,17 @@ export default class TapZoomHandler { this._zoomOut.reset(); } - touchstart(e: TouchEvent, points: Array, mapTouches: Array) { + touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { this._zoomIn.touchstart(e, points, mapTouches); this._zoomOut.touchstart(e, points, mapTouches); } - touchmove(e: TouchEvent, points: Array, mapTouches: Array) { + touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { this._zoomIn.touchmove(e, points, mapTouches); this._zoomOut.touchmove(e, points, mapTouches); } - touchend(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { + touchend: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { const zoomInPoint = this._zoomIn.touchend(e, points, mapTouches); const zoomOutPoint = this._zoomOut.touchend(e, points, mapTouches); @@ -71,7 +71,7 @@ export default class TapZoomHandler { } } - touchcancel() { + touchcancel: () => void = () => { this.reset(); } diff --git a/src/ui/handler/touch_pan.js b/src/ui/handler/touch_pan.js index 62a6d5a80d2..ed9789b8bbd 100644 --- a/src/ui/handler/touch_pan.js +++ b/src/ui/handler/touch_pan.js @@ -35,11 +35,11 @@ export default class TouchPanHandler { this._sum = new Point(0, 0); } - touchstart(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { + touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { return this._calculateTransform(e, points, mapTouches); } - touchmove(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { + touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { if (!this._active || mapTouches.length < this._minTouches) return; // if cooperative gesture handling is set to true, require two fingers to touch pan @@ -61,7 +61,7 @@ export default class TouchPanHandler { return this._calculateTransform(e, points, mapTouches); } - touchend(e: TouchEvent, points: Array, mapTouches: Array) { + touchend: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { this._calculateTransform(e, points, mapTouches); if (this._active && mapTouches.length < this._minTouches) { @@ -69,7 +69,7 @@ export default class TouchPanHandler { } } - touchcancel() { + touchcancel: () => void = () => { this.reset(); } diff --git a/src/ui/handler/touch_zoom_rotate.js b/src/ui/handler/touch_zoom_rotate.js index 400b89b06c1..5547db1c30f 100644 --- a/src/ui/handler/touch_zoom_rotate.js +++ b/src/ui/handler/touch_zoom_rotate.js @@ -27,7 +27,7 @@ class TwoTouchHandler { _start(points: [Point, Point]) {} //eslint-disable-line _move(points: [Point, Point], pinchAround: ?Point, e: TouchEvent): ?HandlerResult { return {}; } //eslint-disable-line - touchstart(e: TouchEvent, points: Array, mapTouches: Array) { + touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { //console.log(e.target, e.targetTouches.length ? e.targetTouches[0].target : null); //log('touchstart', points, e.target.innerHTML, e.targetTouches.length ? e.targetTouches[0].target.innerHTML: undefined); if (this._firstTwoTouches || mapTouches.length < 2) return; @@ -41,7 +41,7 @@ class TwoTouchHandler { this._start([points[0], points[1]]); } - touchmove(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { + touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { const firstTouches = this._firstTwoTouches; if (!firstTouches) return; @@ -58,7 +58,7 @@ class TwoTouchHandler { } - touchend(e: TouchEvent, points: Array, mapTouches: Array) { + touchend: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { if (!this._firstTwoTouches) return; const [idA, idB] = this._firstTwoTouches; @@ -71,7 +71,7 @@ class TwoTouchHandler { this.reset(); } - touchcancel() { + touchcancel: () => void = () => { this.reset(); } From 001a83d9086a871087a03689c22d6fa5e7bd06cb Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 14:52:58 +0200 Subject: [PATCH 19/72] Fix method-unbinding in mercator_coordinate.js and src/util/debug.js --- src/geo/mercator_coordinate.js | 2 +- src/util/debug.js | 58 +++++++++++++++++----------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/geo/mercator_coordinate.js b/src/geo/mercator_coordinate.js index 2e8e6669eca..705cc63c6af 100644 --- a/src/geo/mercator_coordinate.js +++ b/src/geo/mercator_coordinate.js @@ -94,7 +94,7 @@ class MercatorCoordinate { * const coord = mapboxgl.MercatorCoordinate.fromLngLat({lng: 0, lat: 0}, 0); * console.log(coord); // MercatorCoordinate(0.5, 0.5, 0) */ - static fromLngLat(lngLatLike: LngLatLike, altitude: number = 0): MercatorCoordinate { + static fromLngLat: (lngLatLike: LngLatLike, altitude?: number) => MercatorCoordinate = (lngLatLike, altitude = 0) => { const lngLat = LngLat.convert(lngLatLike); return new MercatorCoordinate( diff --git a/src/util/debug.js b/src/util/debug.js index 432666c4599..cde5c1c8fa0 100644 --- a/src/util/debug.js +++ b/src/util/debug.js @@ -47,24 +47,24 @@ export const Debug: { aabbCorners: [], _initializeCanvas(tr: Transform) { - if (!this.debugCanvas) { - this.debugCanvas = window.document.createElement('canvas'); - window.document.body.appendChild(this.debugCanvas); - this.debugCanvas.style.position = 'absolute'; - this.debugCanvas.style.left = 0; - this.debugCanvas.style.top = 0; - this.debugCanvas.style.pointerEvents = 'none'; + if (!Debug.debugCanvas) { + Debug.debugCanvas = window.document.createElement('canvas'); + window.document.body.appendChild(Debug.debugCanvas); + Debug.debugCanvas.style.position = 'absolute'; + Debug.debugCanvas.style.left = 0; + Debug.debugCanvas.style.top = 0; + Debug.debugCanvas.style.pointerEvents = 'none'; const resize = () => { - if (!this.debugCanvas) { return; } - this.debugCanvas.width = tr.width; - this.debugCanvas.height = tr.height; + if (!Debug.debugCanvas) { return; } + Debug.debugCanvas.width = tr.width; + Debug.debugCanvas.height = tr.height; }; resize(); window.addEventListener("resize", resize); } - return this.debugCanvas; + return Debug.debugCanvas; }, _drawLine(ctx: CanvasRenderingContext2D, start: ?Vec2, end: ?Vec2) { @@ -74,21 +74,21 @@ export const Debug: { }, _drawQuad(ctx: CanvasRenderingContext2D, corners: Array) { - this._drawLine(ctx, corners[0], corners[1]); - this._drawLine(ctx, corners[1], corners[2]); - this._drawLine(ctx, corners[2], corners[3]); - this._drawLine(ctx, corners[3], corners[0]); + Debug._drawLine(ctx, corners[0], corners[1]); + Debug._drawLine(ctx, corners[1], corners[2]); + Debug._drawLine(ctx, corners[2], corners[3]); + Debug._drawLine(ctx, corners[3], corners[0]); }, _drawBox(ctx: CanvasRenderingContext2D, corners: Array) { assert(corners.length === 8, `AABB needs 8 corners, found ${corners.length}`); ctx.beginPath(); - this._drawQuad(ctx, corners.slice(0, 4)); - this._drawQuad(ctx, corners.slice(4)); - this._drawLine(ctx, corners[0], corners[4]); - this._drawLine(ctx, corners[1], corners[5]); - this._drawLine(ctx, corners[2], corners[6]); - this._drawLine(ctx, corners[3], corners[7]); + Debug._drawQuad(ctx, corners.slice(0, 4)); + Debug._drawQuad(ctx, corners.slice(4)); + Debug._drawLine(ctx, corners[0], corners[4]); + Debug._drawLine(ctx, corners[1], corners[5]); + Debug._drawLine(ctx, corners[2], corners[6]); + Debug._drawLine(ctx, corners[3], corners[7]); ctx.stroke(); }, @@ -100,7 +100,7 @@ export const Debug: { const ecefToCameraMatrix = mat4.multiply([], tr._camera.getWorldToCamera(tr.worldSize, 1), tr.globeMatrix); if (!tr.freezeTileCoverage) { - this.aabbCorners = coords.map(coord => { + Debug.aabbCorners = coords.map(coord => { // Get tile AABBs in world/pixel space scaled by worldSize const aabb = aabbForTileOnGlobe(tr, tr.worldSize, coord.canonical); const corners = aabb.getCorners(); @@ -113,17 +113,17 @@ export const Debug: { }); } - const canvas = this._initializeCanvas(tr); + const canvas = Debug._initializeCanvas(tr); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); - const tileCount = this.aabbCorners.length; + const tileCount = Debug.aabbCorners.length; ctx.shadowColor = '#000'; ctx.shadowBlur = 2; ctx.lineWidth = 1.5; for (let i = 0; i < tileCount; i++) { - const pixelCorners = this.aabbCorners[i].map(ecef => { + const pixelCorners = Debug.aabbCorners[i].map(ecef => { // Clipping to prevent visual artifacts. // We don't draw any lines if one of their points is behind the camera. // This means that AABBs close to the camera may appear to be missing. @@ -134,14 +134,14 @@ export const Debug: { return vec3.transformMat4([], ecef, ecefToPixelMatrix); }); ctx.strokeStyle = `hsl(${360 * i / tileCount}, 100%, 50%)`; - this._drawBox(ctx, pixelCorners); + Debug._drawBox(ctx, pixelCorners); } }, clearAabbs() { - if (!this.debugCanvas) { return; } - this.debugCanvas.getContext('2d').clearRect(0, 0, this.debugCanvas.width, this.debugCanvas.height); - this.aabbCorners = []; + if (!Debug.debugCanvas) { return; } + Debug.debugCanvas.getContext('2d').clearRect(0, 0, Debug.debugCanvas.width, Debug.debugCanvas.height); + Debug.aabbCorners = []; } }; From ff04d629db08b650abc34ed1a7b378c88940cd81 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Thu, 23 Feb 2023 14:53:00 +0200 Subject: [PATCH 20/72] fix flow problems for ui/control files --- src/ui/control/attribution_control.js | 6 +++--- src/ui/control/logo_control.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 40c4463799e..7c4617aca00 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -46,11 +46,11 @@ class AttributionControl { ); } - getDefaultPosition(): ControlPosition { + getDefaultPosition: () => ControlPosition = () => { return 'bottom-right'; } - onAdd(map: Map): HTMLElement { + onAdd: (map: Map) => HTMLElement = (map) => { const compact = this.options && this.options.compact; this._map = map; @@ -93,7 +93,7 @@ class AttributionControl { return this._container; } - onRemove() { + onRemove: () => void = () => { this._container.remove(); this._map.off('styledata', this._updateData); diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index 7553e9707b6..c6c96b10a43 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -23,7 +23,7 @@ class LogoControl { bindAll(['_updateLogo', '_updateCompact'], this); } - onAdd(map: Map): HTMLElement { + onAdd: (map: Map) => HTMLElement = (map) => { this._map = map; this._container = DOM.create('div', 'mapboxgl-ctrl'); const anchor = DOM.create('a', 'mapboxgl-ctrl-logo'); @@ -47,13 +47,13 @@ class LogoControl { return this._container; } - onRemove() { + onRemove: () => void = () => { this._container.remove(); this._map.off('sourcedata', this._updateLogo); this._map.off('resize', this._updateCompact); } - getDefaultPosition(): ControlPosition { + getDefaultPosition: () => ControlPosition = () => { return 'bottom-left'; } From 362eb1f1162d4ddf195e5a81fd55f817fb6f28c0 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 15:03:44 +0200 Subject: [PATCH 21/72] Fix src/util/debug.js --- src/util/debug.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/util/debug.js b/src/util/debug.js index cde5c1c8fa0..7f2c88626da 100644 --- a/src/util/debug.js +++ b/src/util/debug.js @@ -23,7 +23,11 @@ export const Debug: { run: Function, logToElement: Function, drawAabbs: Function, - clearAabbs: Function + clearAabbs: Function, + _drawBox: Function, + _drawLine: Function, + _drawQuad: Function, + _initializeCanvas: Function, } = { extend(dest: Object, ...sources: Array): Object { @@ -50,9 +54,12 @@ export const Debug: { if (!Debug.debugCanvas) { Debug.debugCanvas = window.document.createElement('canvas'); window.document.body.appendChild(Debug.debugCanvas); + // Supress Flow check because we're checking for null above + if (!Debug.debugCanvas) return; + Debug.debugCanvas.style.position = 'absolute'; - Debug.debugCanvas.style.left = 0; - Debug.debugCanvas.style.top = 0; + Debug.debugCanvas.style.left = '0'; + Debug.debugCanvas.style.top = '0'; Debug.debugCanvas.style.pointerEvents = 'none'; const resize = () => { @@ -109,6 +116,7 @@ export const Debug: { for (const pos of corners) { vec3.transformMat4(pos, pos, worldToECEFMatrix); } + // $FlowFixMe[incompatible-type] return corners; }); } @@ -129,8 +137,13 @@ export const Debug: { // This means that AABBs close to the camera may appear to be missing. // (A more correct algorithm would shorten the line segments instead of removing them entirely.) // Full AABBs can be viewed by enabling `map.transform.freezeTileCoverage` and panning. + // $FlowFixMe[incompatible-call] const cameraPos = vec3.transformMat4([], ecef, ecefToCameraMatrix); + + // $FlowFixMe[incompatible-call] if (cameraPos[2] > 0) { return null; } + + // $FlowFixMe[incompatible-call] return vec3.transformMat4([], ecef, ecefToPixelMatrix); }); ctx.strokeStyle = `hsl(${360 * i / tileCount}, 100%, 50%)`; @@ -139,9 +152,9 @@ export const Debug: { }, clearAabbs() { - if (!Debug.debugCanvas) { return; } + if (!Debug.debugCanvas) return; + // $FlowFixMe[incompatible-use] - Flow doesn't know that debugCanvas is non-null here Debug.debugCanvas.getContext('2d').clearRect(0, 0, Debug.debugCanvas.width, Debug.debugCanvas.height); Debug.aabbCorners = []; } - }; From 3ff8f41270897139002bc92323bc47271fb42225 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Thu, 23 Feb 2023 15:13:06 +0200 Subject: [PATCH 22/72] fix flow problems for ui, util and source files --- src/source/geojson_wrapper.js | 1 + src/ui/map.js | 6 +++--- src/util/worker_performance_utils.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/source/geojson_wrapper.js b/src/source/geojson_wrapper.js index 4a396464d8a..f20c6f951fa 100644 --- a/src/source/geojson_wrapper.js +++ b/src/source/geojson_wrapper.js @@ -3,6 +3,7 @@ import Point from '@mapbox/point-geometry'; import {VectorTileFeature} from '@mapbox/vector-tile'; +// $FlowFixMe[method-unbinding] const toGeoJSON = VectorTileFeature.prototype.toGeoJSON; import EXTENT from '../data/extent.js'; diff --git a/src/ui/map.js b/src/ui/map.js index 636e2fce348..c11ca35e642 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -3368,12 +3368,12 @@ class Map * @returns An id that can be used to cancel the callback * @private */ - _requestRenderFrame(callback: () => void): TaskID { + _requestRenderFrame: (callback: () => void) => TaskID = (callback) => { this._update(); return this._renderTaskQueue.add(callback); } - _cancelRenderFrame(id: TaskID) { + _cancelRenderFrame: (id: TaskID) => void = (id) => { this._renderTaskQueue.remove(id); } @@ -4014,7 +4014,7 @@ class Map * @private * @returns {Object} Returns `this` | Promise. */ - _preloadTiles(transform: Transform | Array): this { + _preloadTiles: (transform: Transform | Array) => Map = (transform) => { const sources: Array = this.style ? (Object.values(this.style._sourceCaches): any) : []; diff --git a/src/util/worker_performance_utils.js b/src/util/worker_performance_utils.js index d223bc538c8..d612c53c0da 100644 --- a/src/util/worker_performance_utils.js +++ b/src/util/worker_performance_utils.js @@ -14,7 +14,7 @@ export const WorkerPerformanceUtils = { getPerformanceMetricsAsync(callback: (error: ?Error, result: ?Object) => void) { const metrics = PerformanceUtils.getPerformanceMetrics(); - const dispatcher = new Dispatcher(getWorkerPool(), this); + const dispatcher = new Dispatcher(getWorkerPool(), WorkerPerformanceUtils); const createTime = performance.getEntriesByName('create', 'mark')[0].startTime; From edc49980bd686b262aaeb3d020595438c10780ba Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 15:17:01 +0200 Subject: [PATCH 23/72] Fix leftovers --- src/source/query_features.js | 4 +++- src/style/style.js | 2 +- src/util/ajax.js | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/source/query_features.js b/src/source/query_features.js index abaab5a023f..ede68d8b225 100644 --- a/src/source/query_features.js +++ b/src/source/query_features.js @@ -78,7 +78,7 @@ export function queryRenderedFeatures(sourceCache: SourceCache, export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: StyleLayer}, - getLayerSourceCache: (layer: StyleLayer) => SourceCache, + getLayerSourceCache: (layer: StyleLayer) => SourceCache | void, queryGeometry: Array, params: { filter: FilterSpecification, layers: Array, availableImages: Array }, collisionIndex: CollisionIndex, @@ -137,6 +137,8 @@ export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, const feature = featureWrapper.feature; const layer = styleLayers[layerName]; const sourceCache = getLayerSourceCache(layer); + if (!sourceCache) return; + const state = sourceCache.getFeatureState(feature.layer['source-layer'], feature.id); feature.source = feature.layer.source; if (feature.layer['source-layer']) { diff --git a/src/style/style.js b/src/style/style.js index 3a3b4fde37f..9425dafd9b6 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -2189,7 +2189,7 @@ class Style return this._otherSourceCaches[source]; } - _getLayerSourceCache: ((layer: StyleLayer) => SourceCache | void) = (layer: StyleLayer): SourceCache | void => { + _getLayerSourceCache: (layer: StyleLayer) => SourceCache | void = (layer) => { return layer.type === 'symbol' ? this._symbolSourceCaches[layer.source] : this._otherSourceCaches[layer.source]; diff --git a/src/util/ajax.js b/src/util/ajax.js index bfe0e6512f0..d21cb3b0119 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -325,6 +325,7 @@ export const getImage = function(requestParameters: RequestParameters, callback: requestParameters, callback, cancelled: false, + // $FlowFixMe[object-this-reference] cancel() { this.cancelled = true; } }; imageQueue.push(queued); From a30c00d018c09e4f35f5c89a92b4ce16fa89be97 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 15:19:16 +0200 Subject: [PATCH 24/72] Update flow to v0.156.0 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 37f93e04074..b2e632cb7dd 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.155.1 +0.156.0 [options] diff --git a/package.json b/package.json index fc903e6fdf0..f6ab502264a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.155.1", + "flow-bin": "0.156.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 4758d3c1b4c..16606e032d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.155.1: - version "0.155.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.155.1.tgz#1263ee3e0f42d11cb13ba56c3851a096213ce5f7" - integrity sha512-qy2eXkgngR6u+MYA1ydzPnclhos21BZlpkJ50Y9YOZ4eTMq6txswB3X+gUsg8XUyCteLoMeo7n30k7aY2no2Yw== +flow-bin@0.156.0: + version "0.156.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.156.0.tgz#d60a89d35ae6019fcdce15277d4370fdc15dee95" + integrity sha512-KEEsKV7/bePZM3Ja7rYlAaSx8GPiTGr7pt0IJcX5S3GSEIZ2ieayF6JWNjbyLiu7ZUJuWe4ITDnPvyqimUpYww== follow-redirects@^1.0.0: version "1.15.1" From e8da18a0fd7b4ddb3c095feb4171c28249095c8d Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 15:36:48 +0200 Subject: [PATCH 25/72] Revert style-spec/expression/ --- .flowconfig | 2 +- package.json | 2 +- .../expression/compound_expression.js | 2 +- .../expression/definitions/assertion.js | 2 +- src/style-spec/expression/definitions/at.js | 2 +- src/style-spec/expression/definitions/case.js | 2 +- .../expression/definitions/coalesce.js | 2 +- .../expression/definitions/coercion.js | 2 +- .../expression/definitions/collator.js | 2 +- .../expression/definitions/comparison.js | 2 +- .../expression/definitions/format.js | 2 +- .../expression/definitions/image.js | 2 +- src/style-spec/expression/definitions/in.js | 2 +- .../expression/definitions/index_of.js | 2 +- .../expression/definitions/interpolate.js | 2 +- .../expression/definitions/length.js | 2 +- src/style-spec/expression/definitions/let.js | 2 +- .../expression/definitions/literal.js | 2 +- .../expression/definitions/match.js | 2 +- .../expression/definitions/number_format.js | 2 +- .../expression/definitions/slice.js | 2 +- src/style-spec/expression/definitions/step.js | 2 +- src/style-spec/expression/definitions/var.js | 2 +- .../expression/definitions/within.js | 2 +- src/style-spec/expression/index.js | 103 +++++++----------- yarn.lock | 8 +- 26 files changed, 70 insertions(+), 89 deletions(-) diff --git a/.flowconfig b/.flowconfig index b2e632cb7dd..a3ae039f3c7 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.156.0 +0.158.0 [options] diff --git a/package.json b/package.json index f6ab502264a..7149466efe1 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.156.0", + "flow-bin": "0.158.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/style-spec/expression/compound_expression.js b/src/style-spec/expression/compound_expression.js index b8fddc51986..2da90b1373a 100644 --- a/src/style-spec/expression/compound_expression.js +++ b/src/style-spec/expression/compound_expression.js @@ -47,7 +47,7 @@ class CompoundExpression implements Expression { return [this.name].concat(this.args.map(arg => arg.serialize())); } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { const op: string = (args[0]: any); const definition = CompoundExpression.definitions[op]; if (!definition) { diff --git a/src/style-spec/expression/definitions/assertion.js b/src/style-spec/expression/definitions/assertion.js index 8b64372661d..0c4a66ce5ae 100644 --- a/src/style-spec/expression/definitions/assertion.js +++ b/src/style-spec/expression/definitions/assertion.js @@ -36,7 +36,7 @@ class Assertion implements Expression { this.args = args; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length < 2) return context.error(`Expected at least one argument.`); diff --git a/src/style-spec/expression/definitions/at.js b/src/style-spec/expression/definitions/at.js index fd62c465414..780a50c2954 100644 --- a/src/style-spec/expression/definitions/at.js +++ b/src/style-spec/expression/definitions/at.js @@ -21,7 +21,7 @@ class At implements Expression { this.input = input; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?At = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?At { if (args.length !== 3) return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/case.js b/src/style-spec/expression/definitions/case.js index 390c1fff46f..fb345fe6b6b 100644 --- a/src/style-spec/expression/definitions/case.js +++ b/src/style-spec/expression/definitions/case.js @@ -23,7 +23,7 @@ class Case implements Expression { this.otherwise = otherwise; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Case = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Case { if (args.length < 4) return context.error(`Expected at least 3 arguments, but found only ${args.length - 1}.`); if (args.length % 2 !== 0) diff --git a/src/style-spec/expression/definitions/coalesce.js b/src/style-spec/expression/definitions/coalesce.js index 1c4c2d2d8ef..e1e52519898 100644 --- a/src/style-spec/expression/definitions/coalesce.js +++ b/src/style-spec/expression/definitions/coalesce.js @@ -19,7 +19,7 @@ class Coalesce implements Expression { this.args = args; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Coalesce = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Coalesce { if (args.length < 2) { return context.error("Expectected at least one argument."); } diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index 7da48ba88bb..beac832fc8c 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -38,7 +38,7 @@ class Coercion implements Expression { this.args = args; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length < 2) return context.error(`Expected at least one argument.`); diff --git a/src/style-spec/expression/definitions/collator.js b/src/style-spec/expression/definitions/collator.js index 8f7d79a5731..2ec43b10765 100644 --- a/src/style-spec/expression/definitions/collator.js +++ b/src/style-spec/expression/definitions/collator.js @@ -21,7 +21,7 @@ export default class CollatorExpression implements Expression { this.diacriticSensitive = diacriticSensitive; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length !== 2) return context.error(`Expected one argument.`); diff --git a/src/style-spec/expression/definitions/comparison.js b/src/style-spec/expression/definitions/comparison.js index bdc33403f5d..beb5c91e076 100644 --- a/src/style-spec/expression/definitions/comparison.js +++ b/src/style-spec/expression/definitions/comparison.js @@ -77,7 +77,7 @@ function makeComparison(op: ComparisonOperator, compareBasic: (EvaluationContext this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value'; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length !== 3 && args.length !== 4) return context.error(`Expected two or three arguments.`); diff --git a/src/style-spec/expression/definitions/format.js b/src/style-spec/expression/definitions/format.js index 7555adcfed3..6445edd63ff 100644 --- a/src/style-spec/expression/definitions/format.js +++ b/src/style-spec/expression/definitions/format.js @@ -27,7 +27,7 @@ export default class FormatExpression implements Expression { this.sections = sections; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length < 2) { return context.error(`Expected at least one argument.`); } diff --git a/src/style-spec/expression/definitions/image.js b/src/style-spec/expression/definitions/image.js index 90eeeeb6229..b8095c3bbc6 100644 --- a/src/style-spec/expression/definitions/image.js +++ b/src/style-spec/expression/definitions/image.js @@ -17,7 +17,7 @@ export default class ImageExpression implements Expression { this.input = input; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length !== 2) { return context.error(`Expected two arguments.`); } diff --git a/src/style-spec/expression/definitions/in.js b/src/style-spec/expression/definitions/in.js index 42f4a9dc9fd..fd5773049c8 100644 --- a/src/style-spec/expression/definitions/in.js +++ b/src/style-spec/expression/definitions/in.js @@ -20,7 +20,7 @@ class In implements Expression { this.haystack = haystack; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?In = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?In { if (args.length !== 3) { return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); } diff --git a/src/style-spec/expression/definitions/index_of.js b/src/style-spec/expression/definitions/index_of.js index 4f8db4cce49..973f9b2c93c 100644 --- a/src/style-spec/expression/definitions/index_of.js +++ b/src/style-spec/expression/definitions/index_of.js @@ -22,7 +22,7 @@ class IndexOf implements Expression { this.fromIndex = fromIndex; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?IndexOf = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?IndexOf { if (args.length <= 2 || args.length >= 5) { return context.error(`Expected 3 or 4 arguments, but found ${args.length - 1} instead.`); } diff --git a/src/style-spec/expression/definitions/interpolate.js b/src/style-spec/expression/definitions/interpolate.js index 0af497213fd..30e60d39084 100644 --- a/src/style-spec/expression/definitions/interpolate.js +++ b/src/style-spec/expression/definitions/interpolate.js @@ -56,7 +56,7 @@ class Interpolate implements Expression { return t; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Interpolate = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Interpolate { let [operator, interpolation, input, ...rest] = args; if (!Array.isArray(interpolation) || interpolation.length === 0) { diff --git a/src/style-spec/expression/definitions/length.js b/src/style-spec/expression/definitions/length.js index d61a10d2da1..2661adfbabf 100644 --- a/src/style-spec/expression/definitions/length.js +++ b/src/style-spec/expression/definitions/length.js @@ -19,7 +19,7 @@ class Length implements Expression { this.input = input; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Length = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Length { if (args.length !== 2) return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/let.js b/src/style-spec/expression/definitions/let.js index deeaa4ab1fe..9a04f2d5bac 100644 --- a/src/style-spec/expression/definitions/let.js +++ b/src/style-spec/expression/definitions/let.js @@ -27,7 +27,7 @@ class Let implements Expression { fn(this.result); } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Let = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Let { if (args.length < 4) return context.error(`Expected at least 3 arguments, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index f75035a0bc5..ae1f90966d6 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -18,7 +18,7 @@ class Literal implements Expression { this.value = value; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => void | Literal = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): void | Literal { if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`); diff --git a/src/style-spec/expression/definitions/match.js b/src/style-spec/expression/definitions/match.js index b623d8f0412..8f87b07ab31 100644 --- a/src/style-spec/expression/definitions/match.js +++ b/src/style-spec/expression/definitions/match.js @@ -30,7 +30,7 @@ class Match implements Expression { this.otherwise = otherwise; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Match = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Match { if (args.length < 5) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); if (args.length % 2 !== 1) diff --git a/src/style-spec/expression/definitions/number_format.js b/src/style-spec/expression/definitions/number_format.js index dc943797389..c187817467d 100644 --- a/src/style-spec/expression/definitions/number_format.js +++ b/src/style-spec/expression/definitions/number_format.js @@ -59,7 +59,7 @@ export default class NumberFormat implements Expression { this.maxFractionDigits = maxFractionDigits; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Expression = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length !== 3) return context.error(`Expected two arguments.`); diff --git a/src/style-spec/expression/definitions/slice.js b/src/style-spec/expression/definitions/slice.js index 1e17bd1949f..9c5cbbc017a 100644 --- a/src/style-spec/expression/definitions/slice.js +++ b/src/style-spec/expression/definitions/slice.js @@ -23,7 +23,7 @@ class Slice implements Expression { } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Slice = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Slice { if (args.length <= 2 || args.length >= 5) { return context.error(`Expected 3 or 4 arguments, but found ${args.length - 1} instead.`); } diff --git a/src/style-spec/expression/definitions/step.js b/src/style-spec/expression/definitions/step.js index 43c9e5c1069..89fdf128419 100644 --- a/src/style-spec/expression/definitions/step.js +++ b/src/style-spec/expression/definitions/step.js @@ -29,7 +29,7 @@ class Step implements Expression { } } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Step = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Step { if (args.length - 1 < 4) { return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); } diff --git a/src/style-spec/expression/definitions/var.js b/src/style-spec/expression/definitions/var.js index 61937545d2f..8146a8a04d4 100644 --- a/src/style-spec/expression/definitions/var.js +++ b/src/style-spec/expression/definitions/var.js @@ -16,7 +16,7 @@ class Var implements Expression { this.boundExpression = boundExpression; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => void | Var = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): void | Var { if (args.length !== 2 || typeof args[1] !== 'string') return context.error(`'var' expression requires exactly one string literal argument.`); diff --git a/src/style-spec/expression/definitions/within.js b/src/style-spec/expression/definitions/within.js index c5e9f4b4d00..6e5c98d6f4a 100644 --- a/src/style-spec/expression/definitions/within.js +++ b/src/style-spec/expression/definitions/within.js @@ -299,7 +299,7 @@ class Within implements Expression { this.geometries = geometries; } - static parse: (args: $ReadOnlyArray, context: ParsingContext) => ?Within = (args, context) => { + static parse(args: $ReadOnlyArray, context: ParsingContext): ?Within { if (args.length !== 2) return context.error(`'within' expression requires exactly one argument, but found ${args.length - 1} instead.`); if (isValue(args[1])) { diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index f7bc94ecf7e..1ab34b13643 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -29,25 +29,25 @@ import type Point from '@mapbox/point-geometry'; import type {CanonicalTileID} from '../../source/tile_id.js'; import type {FeatureDistanceData} from '../feature_filter/index.js'; -export interface Feature { - +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon'; - +id?: number | null; - +properties: {[_: string]: any}; - +patterns?: {[_: string]: string}; - +geometry?: Array>; -} +export type Feature = { + +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon', + +id?: number | null, + +properties: {[_: string]: any}, + +patterns?: {[_: string]: string}, + +geometry?: Array> +}; export type FeatureState = {[_: string]: any}; -export interface GlobalProperties { - +zoom: number; - +pitch?: number; - +heatmapDensity?: number; - +lineProgress?: number; - +skyRadialProgress?: number; - +isSupportedScript?: (_: string) => boolean; - +accumulated?: Value; -} +export type GlobalProperties = $ReadOnly<{ + zoom: number, + pitch?: number, + heatmapDensity?: number, + lineProgress?: number, + skyRadialProgress?: number, + isSupportedScript?: (_: string) => boolean, + accumulated?: Value +}>; export class StyleExpression { expression: Expression; @@ -78,7 +78,7 @@ export class StyleExpression { return this.expression.evaluate(this._evaluator); } - evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => any = (globals, feature, featureState, canonical, availableImages, formattedSection, featureTileCoord, featureDistanceData) => { + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any { this._evaluator.globals = globals; this._evaluator.feature = feature || null; this._evaluator.featureState = featureState || null; @@ -154,7 +154,7 @@ export class ZoomConstantExpression { return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } - evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any = (globals, feature, featureState, canonical, availableImages, formattedSection) => { + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } } @@ -175,15 +175,15 @@ export class ZoomDependentExpression { this.interpolationType = interpolationType; } - evaluateWithoutErrorHandling: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any = (globals, feature, featureState, canonical, availableImages, formattedSection) => { + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } - evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any = (globals, feature, featureState, canonical, availableImages, formattedSection) => { + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } - interpolationFactor: (input: number, lower: number, upper: number) => number = (input, lower, upper) => { + interpolationFactor(input: number, lower: number, upper: number): number { if (this.interpolationType) { return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper); } else { @@ -192,52 +192,33 @@ export class ZoomDependentExpression { } } -export type ConstantExpression = interface { - kind: 'constant', - +evaluate: ( - globals: GlobalProperties, - feature?: Feature, - featureState?: FeatureState, - canonical?: CanonicalTileID, - availableImages?: Array - ) => any, +export type ConstantExpression = { + kind: 'constant', + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, } -export type SourceExpression = interface { - kind: 'source', - isStateDependent: boolean, - +evaluate: ( - globals: GlobalProperties, - feature?: Feature, - featureState?: FeatureState, - canonical?: CanonicalTileID, - availableImages?: Array, - formattedSection?: FormattedSection - ) => any, +export type SourceExpression = { + kind: 'source', + isStateDependent: boolean, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, }; -export type CameraExpression = interface { - kind: 'camera', - +evaluate: ( - globals: GlobalProperties, - feature?: Feature, - featureState?: FeatureState, - canonical?: CanonicalTileID, - availableImages?: Array - ) => any, - +interpolationFactor: (input: number, lower: number, upper: number) => number, - zoomStops: Array, - interpolationType: ?InterpolationType, +export type CameraExpression = { + kind: 'camera', + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, + zoomStops: Array, + interpolationType: ?InterpolationType }; -export interface CompositeExpression { - kind: 'composite'; - isStateDependent: boolean; - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any; - +interpolationFactor: (input: number, lower: number, upper: number) => number; - zoomStops: Array; - interpolationType: ?InterpolationType; -} +export type CompositeExpression = { + kind: 'composite', + isStateDependent: boolean, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, + zoomStops: Array, + interpolationType: ?InterpolationType +}; export type StylePropertyExpression = | ConstantExpression diff --git a/yarn.lock b/yarn.lock index 16606e032d1..f71be285ffe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.156.0: - version "0.156.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.156.0.tgz#d60a89d35ae6019fcdce15277d4370fdc15dee95" - integrity sha512-KEEsKV7/bePZM3Ja7rYlAaSx8GPiTGr7pt0IJcX5S3GSEIZ2ieayF6JWNjbyLiu7ZUJuWe4ITDnPvyqimUpYww== +flow-bin@0.158.0: + version "0.158.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.158.0.tgz#0a09763d41eb8ec7135ced6a3b9f8fa370a393d8" + integrity sha512-Gk5md8XTwk/M+J5M+rFyS1LJfFen6ldY60jM9+meWixlKf4b0vwdoUO8R7oo471pze+GY+blrnskUeqLDxFJfg== follow-redirects@^1.0.0: version "1.15.1" From f7d210636cc9f3a83b482fa22338585ffaed2738 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 15:42:30 +0200 Subject: [PATCH 26/72] Update style-spec/expression/index.js --- src/style-spec/expression/index.js | 93 ++++++++++++++++++------------ 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 1ab34b13643..6af5c44df26 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -29,25 +29,25 @@ import type Point from '@mapbox/point-geometry'; import type {CanonicalTileID} from '../../source/tile_id.js'; import type {FeatureDistanceData} from '../feature_filter/index.js'; -export type Feature = { - +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon', - +id?: number | null, - +properties: {[_: string]: any}, - +patterns?: {[_: string]: string}, - +geometry?: Array> -}; +export interface Feature { + +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon'; + +id?: number | null; + +properties: {[_: string]: any}; + +patterns?: {[_: string]: string}; + +geometry?: Array>; +} export type FeatureState = {[_: string]: any}; -export type GlobalProperties = $ReadOnly<{ - zoom: number, - pitch?: number, - heatmapDensity?: number, - lineProgress?: number, - skyRadialProgress?: number, - isSupportedScript?: (_: string) => boolean, - accumulated?: Value -}>; +export interface GlobalProperties { + +zoom: number; + +pitch?: number; + +heatmapDensity?: number; + +lineProgress?: number; + +skyRadialProgress?: number; + +isSupportedScript?: (_: string) => boolean; + +accumulated?: Value; +} export class StyleExpression { expression: Expression; @@ -192,33 +192,52 @@ export class ZoomDependentExpression { } } -export type ConstantExpression = { - kind: 'constant', - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, +export type ConstantExpression = interface { + kind: 'constant', + +evaluate: ( + globals: GlobalProperties, + feature?: Feature, + featureState?: FeatureState, + canonical?: CanonicalTileID, + availableImages?: Array + ) => any, } -export type SourceExpression = { - kind: 'source', - isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, +export type SourceExpression = interface { + kind: 'source', + isStateDependent: boolean, + +evaluate: ( + globals: GlobalProperties, + feature?: Feature, + featureState?: FeatureState, + canonical?: CanonicalTileID, + availableImages?: Array, + formattedSection?: FormattedSection + ) => any, }; -export type CameraExpression = { - kind: 'camera', - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, - +interpolationFactor: (input: number, lower: number, upper: number) => number, - zoomStops: Array, - interpolationType: ?InterpolationType +export type CameraExpression = interface { + kind: 'camera', + +evaluate: ( + globals: GlobalProperties, + feature?: Feature, + featureState?: FeatureState, + canonical?: CanonicalTileID, + availableImages?: Array + ) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, + zoomStops: Array, + interpolationType: ?InterpolationType, }; -export type CompositeExpression = { - kind: 'composite', - isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, - +interpolationFactor: (input: number, lower: number, upper: number) => number, - zoomStops: Array, - interpolationType: ?InterpolationType -}; +export interface CompositeExpression { + kind: 'composite'; + isStateDependent: boolean; + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any; + +interpolationFactor: (input: number, lower: number, upper: number) => number; + zoomStops: Array; + interpolationType: ?InterpolationType; +} export type StylePropertyExpression = | ConstantExpression From a50b8ad2e2d24e8f26bb79220fbc72ca2d3c27c4 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 15:56:59 +0200 Subject: [PATCH 27/72] Ignore method-unbinding in style-spec/expression definitions --- .../expression/compound_expression.js | 1 + .../expression/definitions/comparison.js | 1 + .../expression/definitions/index.js | 20 +++++++++++++++++++ src/style-spec/expression/index.js | 4 ++++ src/style/style_layer/symbol_style_layer.js | 2 ++ 5 files changed, 28 insertions(+) diff --git a/src/style-spec/expression/compound_expression.js b/src/style-spec/expression/compound_expression.js index 2da90b1373a..16330d25158 100644 --- a/src/style-spec/expression/compound_expression.js +++ b/src/style-spec/expression/compound_expression.js @@ -146,6 +146,7 @@ class CompoundExpression implements Expression { assert(!CompoundExpression.definitions); CompoundExpression.definitions = definitions; for (const name in definitions) { + // $FlowFixMe[method-unbinding] registry[name] = CompoundExpression; } } diff --git a/src/style-spec/expression/definitions/comparison.js b/src/style-spec/expression/definitions/comparison.js index beb5c91e076..078df91473e 100644 --- a/src/style-spec/expression/definitions/comparison.js +++ b/src/style-spec/expression/definitions/comparison.js @@ -77,6 +77,7 @@ function makeComparison(op: ComparisonOperator, compareBasic: (EvaluationContext this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value'; } + // $FlowFixMe[method-unbinding] static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { if (args.length !== 3 && args.length !== 4) return context.error(`Expected two or three arguments.`); diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 914ece11b6c..43284dcc0bb 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -57,34 +57,54 @@ const expressions: ExpressionRegistry = { '<': LessThan, '>=': GreaterThanOrEqual, '<=': LessThanOrEqual, + // $FlowFixMe[method-unbinding] 'array': Assertion, + // $FlowFixMe[method-unbinding] 'at': At, 'boolean': Assertion, + // $FlowFixMe[method-unbinding] 'case': Case, + // $FlowFixMe[method-unbinding] 'coalesce': Coalesce, + // $FlowFixMe[method-unbinding] 'collator': CollatorExpression, + // $FlowFixMe[method-unbinding] 'format': FormatExpression, + // $FlowFixMe[method-unbinding] 'image': ImageExpression, + // $FlowFixMe[method-unbinding] 'in': In, + // $FlowFixMe[method-unbinding] 'index-of': IndexOf, + // $FlowFixMe[method-unbinding] 'interpolate': Interpolate, 'interpolate-hcl': Interpolate, 'interpolate-lab': Interpolate, + // $FlowFixMe[method-unbinding] 'length': Length, + // $FlowFixMe[method-unbinding] 'let': Let, + // $FlowFixMe[method-unbinding] 'literal': Literal, + // $FlowFixMe[method-unbinding] 'match': Match, 'number': Assertion, + // $FlowFixMe[method-unbinding] 'number-format': NumberFormat, 'object': Assertion, + // $FlowFixMe[method-unbinding] 'slice': Slice, + // $FlowFixMe[method-unbinding] 'step': Step, 'string': Assertion, + // $FlowFixMe[method-unbinding] 'to-boolean': Coercion, 'to-color': Coercion, 'to-number': Coercion, 'to-string': Coercion, + // $FlowFixMe[method-unbinding] 'var': Var, + // $FlowFixMe[method-unbinding] 'within': Within }; diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 6af5c44df26..6ff48054028 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -274,14 +274,18 @@ export function createPropertyExpression(expression: mixed, propertySpec: StyleP if (!zoomCurve) { return success(isFeatureConstant ? + // $FlowFixMe[method-unbinding] (new ZoomConstantExpression('constant', expression.value): ConstantExpression) : + // $FlowFixMe[method-unbinding] (new ZoomConstantExpression('source', expression.value): SourceExpression)); } const interpolationType = zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined; return success(isFeatureConstant ? + // $FlowFixMe[method-unbinding] (new ZoomDependentExpression('camera', expression.value, zoomCurve.labels, interpolationType): CameraExpression) : + // $FlowFixMe[method-unbinding] (new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType): CompositeExpression)); } diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 88fde1212b3..a4896a32d26 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -129,8 +129,10 @@ class SymbolStyleLayer extends StyleLayer { const styleExpression = new StyleExpression(override, overriden.property.specification); let expression = null; if (overriden.value.kind === 'constant' || overriden.value.kind === 'source') { + // $FlowFixMe[method-unbinding] expression = (new ZoomConstantExpression('source', styleExpression): SourceExpression); } else { + // $FlowFixMe[method-unbinding] expression = (new ZoomDependentExpression('composite', styleExpression, overriden.value.zoomStops, From ca72fe93392e639872172f9ea07bd19e23520642 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 16:20:20 +0200 Subject: [PATCH 28/72] Upgrade Flow to v0.171.0 --- .flowconfig | 2 +- bench/lib/benchmark.js | 1 + package.json | 2 +- src/source/geojson_worker_source.js | 1 + src/source/source_cache.js | 1 - src/style-spec/validate/validate_layer.js | 1 + yarn.lock | 8 ++++---- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.flowconfig b/.flowconfig index a3ae039f3c7..00b47f8e3f7 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.158.0 +0.171.0 [options] diff --git a/bench/lib/benchmark.js b/bench/lib/benchmark.js index 5b50aa86a7b..a4b42c66cdf 100644 --- a/bench/lib/benchmark.js +++ b/bench/lib/benchmark.js @@ -97,6 +97,7 @@ class Benchmark { return this._end(); } + // $FlowFixMe[duplicate-class-member] _measureAsync(): Promise> { const time = performance.now() - this._start; this._elapsed += time; diff --git a/package.json b/package.json index 7149466efe1..8c066da9a6f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.158.0", + "flow-bin": "0.171.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/source/geojson_worker_source.js b/src/source/geojson_worker_source.js index dcacd7bf19f..89552634762 100644 --- a/src/source/geojson_worker_source.js +++ b/src/source/geojson_worker_source.js @@ -203,6 +203,7 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource { * @param [params.data] Literal GeoJSON data. Must be provided if `params.url` is not. * @private */ + // $FlowFixMe[duplicate-class-member] loadGeoJSON(params: LoadGeoJSONParameters, callback: ResponseCallback): void { // Because of same origin issues, urls must either include an explicit // origin or absolute path. diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 4a56e1d8ddc..eb992c68e0c 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -54,7 +54,6 @@ class SourceCache _shouldReloadOnResume: boolean; _coveredTiles: { [_: number | string]: boolean }; transform: Transform; - _isIdRenderable: (id: number, symbolLayer?: boolean) => boolean; used: boolean; usedForTerrain: boolean; _state: SourceFeatureState; diff --git a/src/style-spec/validate/validate_layer.js b/src/style-spec/validate/validate_layer.js index 1ec68c7b23f..3ce0160ece2 100644 --- a/src/style-spec/validate/validate_layer.js +++ b/src/style-spec/validate/validate_layer.js @@ -58,6 +58,7 @@ export default function validateLayer(options: Options): Array if (!parent) { if (typeof ref === 'string') errors.push(new ValidationError(key, layer.ref, `ref layer "${ref}" not found`)); + // $FlowFixMe[prop-missing] - ref is not defined on the LayerSpecification subtypes } else if (parent.ref) { errors.push(new ValidationError(key, layer.ref, 'ref cannot reference another ref layer')); } else { diff --git a/yarn.lock b/yarn.lock index f71be285ffe..a7f0b551cae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.158.0: - version "0.158.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.158.0.tgz#0a09763d41eb8ec7135ced6a3b9f8fa370a393d8" - integrity sha512-Gk5md8XTwk/M+J5M+rFyS1LJfFen6ldY60jM9+meWixlKf4b0vwdoUO8R7oo471pze+GY+blrnskUeqLDxFJfg== +flow-bin@0.171.0: + version "0.171.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.171.0.tgz#43902cf3ab10704a9c8a96bd16f789d92490ba1c" + integrity sha512-2HEiXAyE60ztGs+loFk6XSskL69THL6tSjzopUcbwgfrdbuZ5Jhv23qh1jUKP5AZJh0NNwxaFZ6To2p6xR+GEA== follow-redirects@^1.0.0: version "1.15.1" From d47d44cddc27a424c2e63874ee79b6315af25a4d Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 16:30:27 +0200 Subject: [PATCH 29/72] Upgrade Flow to v0.175.0 --- .flowconfig | 2 +- package.json | 2 +- src/style-spec/feature_filter/index.js | 1 + yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 00b47f8e3f7..1f539c5c754 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.171.0 +0.175.0 [options] diff --git a/package.json b/package.json index 8c066da9a6f..bdfebf207ab 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.171.0", + "flow-bin": "0.175.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/style-spec/feature_filter/index.js b/src/style-spec/feature_filter/index.js index 4ef02631c05..294ab2114f1 100644 --- a/src/style-spec/feature_filter/index.js +++ b/src/style-spec/feature_filter/index.js @@ -73,6 +73,7 @@ function createFilter(filter: any, layerType?: string = 'fill'): FeatureFilter { } if (!isExpressionFilter(filter)) { + // $FlowFixMe[incompatible-call] filter = convertFilter(filter); } const filterExp = ((filter: any): string[] | string | boolean); diff --git a/yarn.lock b/yarn.lock index a7f0b551cae..1fa2759a5b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.171.0: - version "0.171.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.171.0.tgz#43902cf3ab10704a9c8a96bd16f789d92490ba1c" - integrity sha512-2HEiXAyE60ztGs+loFk6XSskL69THL6tSjzopUcbwgfrdbuZ5Jhv23qh1jUKP5AZJh0NNwxaFZ6To2p6xR+GEA== +flow-bin@0.175.0: + version "0.175.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.175.0.tgz#ba082ac5ad2306ce62754101c710757484c082fe" + integrity sha512-EqIwrZFM+mQW9Iahs5pNUCMbpOkh+k2/9312/zuwyNrNRga3R4uj4M+7Lb0grM1oVYRK5TopQiKh2jWyA9x+xg== follow-redirects@^1.0.0: version "1.15.1" From caf9658d310682604dc995dcdcbf37adbabd84ad Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Thu, 23 Feb 2023 16:47:52 +0200 Subject: [PATCH 30/72] fix runtime error for flow annotation --- src/ui/handler/mouse.js | 4 ++-- src/ui/handler_manager.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ui/handler/mouse.js b/src/ui/handler/mouse.js index 3712e94ea1a..f85e50f251f 100644 --- a/src/ui/handler/mouse.js +++ b/src/ui/handler/mouse.js @@ -51,7 +51,7 @@ class MouseHandler { return {}; // implemented by child } - mousedown: (e: MouseEvent, point: Point) => void = (e, point) => { + mousedown(e: MouseEvent, point: Point) { if (this._lastPoint) return; const eventButton = DOM.mouseButton(e); @@ -113,7 +113,7 @@ class MouseHandler { export class MousePanHandler extends MouseHandler { - mousedown: (e: MouseEvent, point: Point) => void = (e, point) => { + mousedown(e: MouseEvent, point: Point) { super.mousedown(e, point); if (this._lastPoint) this._active = true; } diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index cabe5cdcacd..7a47c078a1a 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -260,12 +260,15 @@ class HandlerManager { const mouseRotate = new MouseRotateHandler(options); const mousePitch = new MousePitchHandler(options); map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch); + // $FlowFixMe[method-unbinding] this._add('mouseRotate', mouseRotate, ['mousePitch']); + // $FlowFixMe[method-unbinding] this._add('mousePitch', mousePitch, ['mouseRotate']); const mousePan = new MousePanHandler(options); const touchPan = new TouchPanHandler(map, options); map.dragPan = new DragPanHandler(el, mousePan, touchPan); + // $FlowFixMe[method-unbinding] this._add('mousePan', mousePan); this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); From 246242cb6ba13e3600142eb7782f0b89f4dd1d6f Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 16:56:24 +0200 Subject: [PATCH 31/72] Upgrade Flow to v0.176.0 --- .flowconfig | 2 +- bench/lib/tile_parser.js | 2 +- package.json | 2 +- src/util/find_pole_of_inaccessibility.js | 17 ++++++++++++----- src/util/window.js | 1 + src/util/worker_pool.js | 1 + test/unit/util/util.test.js | 19 ++++++++++++------- yarn.lock | 8 ++++---- 8 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.flowconfig b/.flowconfig index 1f539c5c754..cda33027f0f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.175.0 +0.176.0 [options] diff --git a/bench/lib/tile_parser.js b/bench/lib/tile_parser.js index 5f75663daaa..6d7bf30bf93 100644 --- a/bench/lib/tile_parser.js +++ b/bench/lib/tile_parser.js @@ -102,7 +102,7 @@ export default class TileParser { return Promise.all([ createStyle(this.styleJSON), fetchTileJSON(mapStub._requestManager, (this.styleJSON.sources[this.sourceID]: any).url) - ]).then(([style: Style, tileJSON: TileJSON]) => { + ]).then(([style, tileJSON]: [Style, TileJSON]) => { this.style = style; this.tileJSON = tileJSON; }); diff --git a/package.json b/package.json index bdfebf207ab..d489badad84 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.175.0", + "flow-bin": "0.176.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/util/find_pole_of_inaccessibility.js b/src/util/find_pole_of_inaccessibility.js index f2be00c5a8b..4587dfe0028 100644 --- a/src/util/find_pole_of_inaccessibility.js +++ b/src/util/find_pole_of_inaccessibility.js @@ -82,11 +82,18 @@ function compareMax(a, b) { return b.max - a.max; } -function Cell(x, y, h, polygon) { - this.p = new Point(x, y); - this.h = h; // half the cell size - this.d = pointToPolygonDist(this.p, polygon); // distance from cell center to polygon - this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell +class Cell { + p: Point; + h: number; + d: number; + max: number; + + constructor(x, y, h, polygon) { + this.p = new Point(x, y); + this.h = h; // half the cell size + this.d = pointToPolygonDist(this.p, polygon); // distance from cell center to polygon + this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell + } } // signed distance from point to polygon outline (negative if point is outside) diff --git a/src/util/window.js b/src/util/window.js index e1c3e478f3f..006e445f945 100644 --- a/src/util/window.js +++ b/src/util/window.js @@ -80,6 +80,7 @@ function restore(): Window { window.fakeWorkerPresence = function() { global.WorkerGlobalScope = function() {}; + // $FlowFixMe[invalid-constructor] global.self = new global.WorkerGlobalScope(); }; window.clearFakeWorkerPresence = function() { diff --git a/src/util/worker_pool.js b/src/util/worker_pool.js index a23c28b2839..b708ee5965d 100644 --- a/src/util/worker_pool.js +++ b/src/util/worker_pool.js @@ -25,6 +25,7 @@ export default class WorkerPool { // client code has had a chance to set it. this.workers = []; while (this.workers.length < WorkerPool.workerCount) { + // $FlowFixMe[invalid-constructor] this.workers.push(new WebWorker()); } } diff --git a/test/unit/util/util.test.js b/test/unit/util/util.test.js index 9beffae6677..450426176a6 100644 --- a/test/unit/util/util.test.js +++ b/test/unit/util/util.test.js @@ -62,14 +62,19 @@ test('util', (t) => { t.deepEqual(getAABBPointSquareDist([2, 2], [3, 3], [2.5, -2]), 16); t.test('bindAll', (t) => { - function MyClass() { - bindAll(['ontimer'], this); - this.name = 'Tom'; + class MyClass { + name: string; + constructor() { + bindAll(['ontimer'], this); + this.name = 'Tom'; + } + + ontimer = () => { + t.equal(this.name, 'Tom'); + t.end(); + } } - MyClass.prototype.ontimer = function() { - t.equal(this.name, 'Tom'); - t.end(); - }; + const my = new MyClass(); setTimeout(my.ontimer, 0); }); diff --git a/yarn.lock b/yarn.lock index 1fa2759a5b3..d1347d320f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.175.0: - version "0.175.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.175.0.tgz#ba082ac5ad2306ce62754101c710757484c082fe" - integrity sha512-EqIwrZFM+mQW9Iahs5pNUCMbpOkh+k2/9312/zuwyNrNRga3R4uj4M+7Lb0grM1oVYRK5TopQiKh2jWyA9x+xg== +flow-bin@0.176.0: + version "0.176.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.176.0.tgz#581419d683474dafa0c837c10d030665695ad1f8" + integrity sha512-wJvHjsv5nUXnL7aumLdpwDxnCtNG4ysSnGgUHoqKPwHbbLx9Of/7NGK54g3Wi82WsPk9wnSmSDFv0neItHAYug== follow-redirects@^1.0.0: version "1.15.1" From 83685d3b84accb65ae35a17468214a89c2a75730 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 23 Feb 2023 17:07:01 +0200 Subject: [PATCH 32/72] Upgrade Flow to v0.177.0 --- .flowconfig | 2 +- package.json | 2 +- src/render/image_manager.js | 1 + src/ui/free_camera.js | 2 +- src/ui/handler/touch_zoom_rotate.js | 2 ++ src/util/web_worker_transfer.js | 2 +- yarn.lock | 8 ++++---- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.flowconfig b/.flowconfig index cda33027f0f..ea38d228bad 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.176.0 +0.177.0 [options] diff --git a/package.json b/package.json index d489badad84..c98203596ba 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.176.0", + "flow-bin": "0.177.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/render/image_manager.js b/src/render/image_manager.js index c152d248476..0193b4fa1d5 100644 --- a/src/render/image_manager.js +++ b/src/render/image_manager.js @@ -252,6 +252,7 @@ class ImageManager extends Evented { this.dirty = false; } + if (!this.atlasTexture) return; // Flow can't infer that atlasTexture is defined here this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); } diff --git a/src/ui/free_camera.js b/src/ui/free_camera.js index 761c850655f..c2155fbfb69 100644 --- a/src/ui/free_camera.js +++ b/src/ui/free_camera.js @@ -144,8 +144,8 @@ class FreeCameraOptions { return; } - const altitude = this._elevation ? this._elevation.getAtPointOrZero(MercatorCoordinate.fromLngLat(location)) : 0; const pos: MercatorCoordinate = this.position; + const altitude = this._elevation ? this._elevation.getAtPointOrZero(MercatorCoordinate.fromLngLat(location)) : 0; const target = MercatorCoordinate.fromLngLat(location, altitude); const forward = [target.x - pos.x, target.y - pos.y, target.z - pos.z]; if (!up) diff --git a/src/ui/handler/touch_zoom_rotate.js b/src/ui/handler/touch_zoom_rotate.js index 5547db1c30f..6206623ca97 100644 --- a/src/ui/handler/touch_zoom_rotate.js +++ b/src/ui/handler/touch_zoom_rotate.js @@ -5,6 +5,7 @@ import * as DOM from '../../util/dom.js'; import type Map from '../map.js'; import type {HandlerResult} from '../handler_manager.js'; import {isFullscreen} from '../../util/util.js'; +import assert from 'assert'; class TwoTouchHandler { @@ -140,6 +141,7 @@ export class TouchZoomHandler extends TwoTouchHandler { const ROTATION_THRESHOLD = 25; // pixels along circumference of touch circle function getBearingDelta(a, b) { + if (!a) throw new Error('Point in `getBearingDelta` is undefined'); return a.angleWith(b) * 180 / Math.PI; } diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index d8db0888c17..fdb80ee6f8c 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -251,8 +251,8 @@ export function deserialize(input: Serialized): mixed { const result = Object.create(klass.prototype); for (const key of Object.keys(input)) { + // $FlowFixMe[incompatible-type] if (key === '$name') continue; - // $FlowFixMe[class-object-subtyping] const value = (input: SerializedObject)[key]; result[key] = deserialize(value); } diff --git a/yarn.lock b/yarn.lock index d1347d320f6..6c92a1ffda5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.176.0: - version "0.176.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.176.0.tgz#581419d683474dafa0c837c10d030665695ad1f8" - integrity sha512-wJvHjsv5nUXnL7aumLdpwDxnCtNG4ysSnGgUHoqKPwHbbLx9Of/7NGK54g3Wi82WsPk9wnSmSDFv0neItHAYug== +flow-bin@0.177.0: + version "0.177.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.177.0.tgz#dd537424dcbdd56f3cc85fd72330840a590e4711" + integrity sha512-hEm9VDf07iGcfjiCaxZAbpp/bRcgPf/Q3f7UucWpMotrM0MmyZ2hCBvhw53XCd3M7+fP8eyZKRvUWtrMqEC/Sg== follow-redirects@^1.0.0: version "1.15.1" From d9eb1b8dff680c3eef7fe4554379da95f6fdb3e8 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 10:56:48 +0200 Subject: [PATCH 33/72] fix formatting for custom_source.js --- src/source/custom_source.js | 469 ++++++++++++++++-------------------- 1 file changed, 206 insertions(+), 263 deletions(-) diff --git a/src/source/custom_source.js b/src/source/custom_source.js index 36b2e37d0c3..f96e201731d 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -147,269 +147,212 @@ export type CustomSourceInterface = { onRemove: ?(map: Map) => void, } -class CustomSource - extends Evented - implements Source { - id: string; - type: 'custom'; - scheme: string; - minzoom: number; - maxzoom: number; - tileSize: number; - attribution: string | void; - - roundZoom: boolean | void; - tileBounds: ?TileBounds; - minTileCacheSize: ?number; - maxTileCacheSize: ?number; - - _map: Map; - _loaded: boolean; - _dispatcher: Dispatcher; - _dataType: ?DataType; - _implementation: CustomSourceInterface; - - constructor( - id: string, - implementation: CustomSourceInterface, - dispatcher: Dispatcher, - eventedParent: Evented, - ) { - super(); - this.id = id; - this.type = 'custom'; - this._dataType = 'raster'; - this._dispatcher = dispatcher; - this._implementation = implementation; - this.setEventedParent(eventedParent); - - this.scheme = 'xyz'; - this.minzoom = 0; - this.maxzoom = 22; - this.tileSize = 512; - - this._loaded = false; - this.roundZoom = true; - - if (!this._implementation) { - this.fire( - new ErrorEvent( - new Error(`Missing implementation for ${this.id} custom source`), - ), - ); - } - - if (!this._implementation.loadTile) { - this.fire( - new ErrorEvent( - new Error(`Missing loadTile implementation for ${this.id} custom source`,), - ), - ); - } - - if (this._implementation.bounds) { - this.tileBounds = new TileBounds( - this._implementation.bounds, - this.minzoom, - this.maxzoom, - ); - } - - // $FlowFixMe[prop-missing] - implementation.update = this._update.bind(this); - - // $FlowFixMe[prop-missing] - implementation.clearTiles = this._clearTiles.bind(this); - - // $FlowFixMe[prop-missing] - implementation.coveringTiles = this._coveringTiles.bind(this); - - extend( - this, - pick( - implementation, - [ - 'dataType', - 'scheme', - 'minzoom', - 'maxzoom', - 'tileSize', - 'attribution', - 'minTileCacheSize', - 'maxTileCacheSize', - ], - ), - ); - } - - serialize(): Source { - return pick( - this, - ['type', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution'], - ); - } - - load() { - this._loaded = true; - this.fire( - new Event('data', {dataType: 'source', sourceDataType: 'metadata'}), - ); - this.fire( - new Event('data', {dataType: 'source', sourceDataType: 'content'}), - ); - } - - loaded(): boolean { - return this._loaded; - } - - onAdd: (map: Map) => void = (map) => { - this._map = map; - this._loaded = false; - this.fire(new Event('dataloading', {dataType: 'source'})); - if (this._implementation.onAdd) this._implementation.onAdd(map); - this.load(); - } - - onRemove: (map: Map) => void = (map) => { - if (this._implementation.onRemove) { - this._implementation.onRemove(map); - } - } - - hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { - if (this._implementation.hasTile) { - const {x, y, z} = tileID.canonical; - return this._implementation.hasTile({x, y, z}); - } - - return !this.tileBounds || this.tileBounds.contains(tileID.canonical); - } - - loadTile(tile: Tile, callback: Callback): void { - const {x, y, z} = tile.tileID.canonical; - const controller = new window.AbortController(); - const signal = controller.signal; - - // $FlowFixMe[prop-missing] - tile.request = Promise.resolve( - this._implementation.loadTile({x, y, z}, {signal}), - ).then(tileLoaded.bind(this)).catch( - error => { - // silence AbortError - if (error.code === 20) return; - tile.state = 'errored'; - callback(error); - }, - ); - - // $FlowFixMe[prop-missing] - tile.request.cancel = (() => controller.abort()); - - function tileLoaded(data) { - delete tile.request; - - if (tile.aborted) { - tile.state = 'unloaded'; - return callback(null); - } - - // If the implementation returned `undefined` as tile data, - // mark the tile as `errored` to indicate that we have no data for it. - // A map will render an overscaled parent tile in the tile’s space. - if (data === undefined) { - tile.state = 'errored'; - return callback(null); - } - - // If the implementation returned `null` as tile data, - // mark the tile as `loaded` and use an an empty image as tile data. - // A map will render nothing in the tile’s space. - if (data === null) { - const emptyImage = { - width: this.tileSize, - height: this.tileSize, - data: null, - }; - this.loadTileData(tile, (emptyImage: any)); - tile.state = 'loaded'; - return callback(null); - } - - if (!isRaster(data)) { - tile.state = 'errored'; - return callback( - new Error(`Can't infer data type for ${this.id}, only raster data supported at the moment`,), - ); - } - - this.loadTileData(tile, data); - tile.state = 'loaded'; - callback(null); - } - } - - loadTileData(tile: Tile, data: T): void { - // Only raster data supported at the moment - RasterTileSource.loadTileData(tile, (data: any), this._map.painter); - } - - unloadTileData(tile: Tile): void { - // Only raster data supported at the moment - RasterTileSource.unloadTileData(tile, this._map.painter); - } - - unloadTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { - this.unloadTileData(tile); - if (this._implementation.unloadTile) { - const {x, y, z} = tile.tileID.canonical; - this._implementation.unloadTile({x, y, z}); - } - - callback(); - } - - abortTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { - if (tile.request && tile.request.cancel) { - tile.request.cancel(); - delete tile.request; - } - - callback(); - } - - hasTransition(): boolean { - return false; - } - - _coveringTiles: (() => Array<{ x: number, y: number, z: number, ... }>) = (): Array<{ z: number, x: number, y: number }> => { - const tileIDs = this._map.transform.coveringTiles( - { - tileSize: this.tileSize, - minzoom: this.minzoom, - maxzoom: this.maxzoom, - roundZoom: this.roundZoom, - }, - ); - - return tileIDs.map( - tileID => ({ - x: tileID.canonical.x, - y: tileID.canonical.y, - z: tileID.canonical.z, - }), - ); - }; - - _clearTiles: (() => void) = () => { - this._map.style._clearSource(this.id); - }; - - _update: (() => void) = () => { - this.fire( - new Event('data', {dataType: 'source', sourceDataType: 'content'}), - ); - }; +class CustomSource extends Evented implements Source { + + id: string; + type: 'custom'; + scheme: string; + minzoom: number; + maxzoom: number; + tileSize: number; + attribution: string | void; + + roundZoom: boolean | void; + tileBounds: ?TileBounds; + minTileCacheSize: ?number; + maxTileCacheSize: ?number; + + _map: Map; + _loaded: boolean; + _dispatcher: Dispatcher; + _dataType: ?DataType; + _implementation: CustomSourceInterface; + + constructor(id: string, implementation: CustomSourceInterface, dispatcher: Dispatcher, eventedParent: Evented) { + super(); + this.id = id; + this.type = 'custom'; + this._dataType = 'raster'; + this._dispatcher = dispatcher; + this._implementation = implementation; + this.setEventedParent(eventedParent); + + this.scheme = 'xyz'; + this.minzoom = 0; + this.maxzoom = 22; + this.tileSize = 512; + + this._loaded = false; + this.roundZoom = true; + + if (!this._implementation) { + this.fire(new ErrorEvent(new Error(`Missing implementation for ${this.id} custom source`))); + } + + if (!this._implementation.loadTile) { + this.fire(new ErrorEvent(new Error(`Missing loadTile implementation for ${this.id} custom source`))); + } + + if (this._implementation.bounds) { + this.tileBounds = new TileBounds(this._implementation.bounds, this.minzoom, this.maxzoom); + } + + // $FlowFixMe[prop-missing] + implementation.update = this._update.bind(this); + + // $FlowFixMe[prop-missing] + implementation.clearTiles = this._clearTiles.bind(this); + + // $FlowFixMe[prop-missing] + implementation.coveringTiles = this._coveringTiles.bind(this); + + extend(this, pick(implementation, ['dataType', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution', 'minTileCacheSize', 'maxTileCacheSize'])); + } + + serialize(): Source { + return pick(this, ['type', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution']); + } + + load() { + this._loaded = true; + this.fire(new Event('data', {dataType: 'source', sourceDataType: 'metadata'})); + this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); + } + + loaded(): boolean { + return this._loaded; + } + + onAdd: (map: Map) => void = (map) => { + this._map = map; + this._loaded = false; + this.fire(new Event('dataloading', {dataType: 'source'})); + if (this._implementation.onAdd) this._implementation.onAdd(map); + this.load(); + } + + onRemove: (map: Map) => void = (map) => { + if (this._implementation.onRemove) { + this._implementation.onRemove(map); + } + } + + hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { + if (this._implementation.hasTile) { + const {x, y, z} = tileID.canonical; + return this._implementation.hasTile({x, y, z}); + } + + return !this.tileBounds || this.tileBounds.contains(tileID.canonical); + } + + loadTile(tile: Tile, callback: Callback): void { + const {x, y, z} = tile.tileID.canonical; + const controller = new window.AbortController(); + const signal = controller.signal; + + // $FlowFixMe[prop-missing] + tile.request = Promise + .resolve(this._implementation.loadTile({x, y, z}, {signal})) + .then(tileLoaded.bind(this)) + .catch(error => { + // silence AbortError + if (error.code === 20) return; + tile.state = 'errored'; + callback(error); + }); + + // $FlowFixMe[prop-missing] + tile.request.cancel = () => controller.abort(); + + function tileLoaded(data) { + delete tile.request; + + if (tile.aborted) { + tile.state = 'unloaded'; + return callback(null); + } + + // If the implementation returned `undefined` as tile data, + // mark the tile as `errored` to indicate that we have no data for it. + // A map will render an overscaled parent tile in the tile’s space. + if (data === undefined) { + tile.state = 'errored'; + return callback(null); + } + + // If the implementation returned `null` as tile data, + // mark the tile as `loaded` and use an an empty image as tile data. + // A map will render nothing in the tile’s space. + if (data === null) { + const emptyImage = {width: this.tileSize, height: this.tileSize, data: null}; + this.loadTileData(tile, (emptyImage: any)); + tile.state = 'loaded'; + return callback(null); + } + + if (!isRaster(data)) { + tile.state = 'errored'; + return callback(new Error(`Can't infer data type for ${this.id}, only raster data supported at the moment`)); + } + + this.loadTileData(tile, data); + tile.state = 'loaded'; + callback(null); + } + } + + loadTileData(tile: Tile, data: T): void { + // Only raster data supported at the moment + RasterTileSource.loadTileData(tile, (data: any), this._map.painter); + } + + unloadTileData(tile: Tile): void { + // Only raster data supported at the moment + RasterTileSource.unloadTileData(tile, this._map.painter); + } + + unloadTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { + this.unloadTileData(tile); + if (this._implementation.unloadTile) { + const {x, y, z} = tile.tileID.canonical; + this._implementation.unloadTile({x, y, z}); + } + + callback(); + } + + abortTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { + if (tile.request && tile.request.cancel) { + tile.request.cancel(); + delete tile.request; + } + + callback(); + } + + hasTransition(): boolean { + return false; + } + + _coveringTiles: (() => Array<{ x: number, y: number, z: number, ... }>) = (): Array<{ z: number, x: number, y: number }> => { + const tileIDs = this._map.transform.coveringTiles({ + tileSize: this.tileSize, + minzoom: this.minzoom, + maxzoom: this.maxzoom, + roundZoom: this.roundZoom + }); + + return tileIDs.map(tileID => ({x: tileID.canonical.x, y: tileID.canonical.y, z: tileID.canonical.z})); + } + + _clearTiles: (() => void) = () => { + this._map.style._clearSource(this.id); + } + + _update: (() => void) = () => { + this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); + } } export default CustomSource; From 6c6abcd4d7ea02b02b82d094fb040ed3b919ae08 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 11:00:34 +0200 Subject: [PATCH 34/72] fix lint for touch_zoom_rotate.js * remove unused assert import --- src/ui/handler/touch_zoom_rotate.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/handler/touch_zoom_rotate.js b/src/ui/handler/touch_zoom_rotate.js index 6206623ca97..f39ce6052be 100644 --- a/src/ui/handler/touch_zoom_rotate.js +++ b/src/ui/handler/touch_zoom_rotate.js @@ -5,7 +5,6 @@ import * as DOM from '../../util/dom.js'; import type Map from '../map.js'; import type {HandlerResult} from '../handler_manager.js'; import {isFullscreen} from '../../util/util.js'; -import assert from 'assert'; class TwoTouchHandler { From 505a21a1e27ba7011a3c1781e124ca9d8564821c Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 11:15:57 +0200 Subject: [PATCH 35/72] Update src/util/mapbox.js --- src/util/mapbox.js | 484 ++++++++++++++++++++------------------------- 1 file changed, 210 insertions(+), 274 deletions(-) diff --git a/src/util/mapbox.js b/src/util/mapbox.js index a5a71616e47..d735c7c8b9b 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -413,295 +413,231 @@ class TelemetryEvent { } } -export class PerformanceEvent - extends TelemetryEvent { +export class PerformanceEvent extends TelemetryEvent { constructor() { super('gljs.performance'); } - postPerformanceEvent: ((customAccessToken: ?string, performanceData: LivePerformanceData) => void) = ( - customAccessToken: ?string, - performanceData: LivePerformanceData, - ) => { - if (config.EVENTS_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest( - {timestamp: Date.now(), performanceData}, - customAccessToken, - ); - } - } - }; - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) { - return; - } - - const {timestamp, performanceData} = this.queue.shift(); - - const additionalPayload = getLivePerformanceMetrics(performanceData); - - // Server will only process string for these entries - for (const metadata of additionalPayload.metadata) { - assert(typeof metadata.value === 'string'); - } - for (const counter of additionalPayload.counters) { - assert(typeof counter.value === 'string'); - } - for (const attribute of additionalPayload.attributes) { - assert(typeof attribute.value === 'string'); - } - - this.postEvent(timestamp, additionalPayload, () => {}, customAccessToken); - } + postPerformanceEvent: (customAccessToken: ?string, performanceData: LivePerformanceData) => void = (customAccessToken, performanceData) => { + if (config.EVENTS_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest({timestamp: Date.now(), performanceData}, customAccessToken); + } + } + } + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) { + return; + } + + const {timestamp, performanceData} = this.queue.shift(); + + const additionalPayload = getLivePerformanceMetrics(performanceData); + + // Server will only process string for these entries + for (const metadata of additionalPayload.metadata) { + assert(typeof metadata.value === 'string'); + } + for (const counter of additionalPayload.counters) { + assert(typeof counter.value === 'string'); + } + for (const attribute of additionalPayload.attributes) { + assert(typeof attribute.value === 'string'); + } + + this.postEvent(timestamp, additionalPayload, () => {}, customAccessToken); + } } -export class MapLoadEvent - extends TelemetryEvent { - +success: { [_: number]: boolean }; - skuToken: string; - errorCb: EventCallback; - - constructor() { - super('map.load'); - this.success = {}; - this.skuToken = ''; - } - - postMapLoadEvent: (( - mapId: number, - skuToken: string, - customAccessToken: ?string, - callback: EventCallback -) => void) = ( - mapId: number, - skuToken: string, - customAccessToken: ?string, - callback: EventCallback, -) => { - this.skuToken = skuToken; - this.errorCb = callback; - - if (config.EVENTS_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest( - {id: mapId, timestamp: Date.now()}, - customAccessToken, - ); - } else { - this.errorCb(new Error(AUTH_ERR_MSG)); - } - } -}; - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) return; - const {id, timestamp} = this.queue.shift(); - - // Only one load event should fire per map - if (id && this.success[id]) return; - - if (!this.anonId) { - this.fetchEventData(); - } - - if (!validateUuid(this.anonId)) { - this.anonId = uuid(); - } - - const additionalPayload = { - sdkIdentifier: 'mapbox-gl-js', - sdkVersion, - skuId: SKU_ID, - skuToken: this.skuToken, - userId: this.anonId, - }; - - this.postEvent( - timestamp, - additionalPayload, - err => { - if (err) { - this.errorCb(err); - } else { - if (id) this.success[id] = true; - } - }, - customAccessToken, - ); - } +export class MapLoadEvent extends TelemetryEvent { + +success: {[_: number]: boolean}; + skuToken: string; + errorCb: EventCallback; + + constructor() { + super('map.load'); + this.success = {}; + this.skuToken = ''; + } + + postMapLoadEvent: (mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) => void = (mapId, skuToken, customAccessToken, callback) => { + this.skuToken = skuToken; + this.errorCb = callback; + + if (config.EVENTS_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } + } + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) return; + const {id, timestamp} = this.queue.shift(); + + // Only one load event should fire per map + if (id && this.success[id]) return; + + if (!this.anonId) { + this.fetchEventData(); + } + + if (!validateUuid(this.anonId)) { + this.anonId = uuid(); + } + + const additionalPayload = { + sdkIdentifier: 'mapbox-gl-js', + sdkVersion, + skuId: SKU_ID, + skuToken: this.skuToken, + userId: this.anonId + }; + + this.postEvent(timestamp, additionalPayload, (err) => { + if (err) { + this.errorCb(err); + } else { + if (id) this.success[id] = true; + } + + }, customAccessToken); + } } -export class MapSessionAPI - extends TelemetryEvent { - +success: { [_: number]: boolean }; - skuToken: string; - errorCb: EventCallback; - - constructor() { - super('map.auth'); - this.success = {}; - this.skuToken = ''; - } - - getSession( - timestamp: number, - token: string, - callback: EventCallback, - customAccessToken?: ?string, - ) { - if (!config.API_URL || !config.SESSION_PATH) return; - const authUrlObject: UrlObject = parseUrl( - config.API_URL + config.SESSION_PATH, - ); - authUrlObject.params.push(`sku=${token || ''}`); - authUrlObject.params.push(`access_token=${customAccessToken || - config.ACCESS_TOKEN || - ''}`,); - - const request: RequestParameters = { - url: formatUrl(authUrlObject), - headers: { - 'Content-Type': 'text/plain' //Skip the pre-flight OPTIONS request - , - }, - }; - - this.pendingRequest = getData( - request, - error => { - this.pendingRequest = null; - callback(error); - this.saveEventData(); - this.processRequests(customAccessToken); - }, - ); - } - - getSessionAPI: (( - mapId: number, - skuToken: string, - customAccessToken: ?string, - callback: EventCallback -) => void) = ( - mapId: number, - skuToken: string, - customAccessToken: ?string, - callback: EventCallback, -) => { - this.skuToken = skuToken; - this.errorCb = callback; - - if (config.SESSION_PATH && config.API_URL) { - if (customAccessToken || config.ACCESS_TOKEN) { - this.queueRequest( - {id: mapId, timestamp: Date.now()}, - customAccessToken, - ); - } else { - this.errorCb(new Error(AUTH_ERR_MSG)); - } - } -}; - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) return; - const {id, timestamp} = this.queue.shift(); - - // Only one load event should fire per map - if (id && this.success[id]) return; - - this.getSession( - timestamp, - this.skuToken, - err => { - if (err) { - this.errorCb(err); - } else { - if (id) this.success[id] = true; - } - }, - customAccessToken, - ); - } +export class MapSessionAPI extends TelemetryEvent { + +success: {[_: number]: boolean}; + skuToken: string; + errorCb: EventCallback; + + constructor() { + super('map.auth'); + this.success = {}; + this.skuToken = ''; + } + + getSession(timestamp: number, token: string, callback: EventCallback, customAccessToken?: ?string) { + if (!config.API_URL || !config.SESSION_PATH) return; + const authUrlObject: UrlObject = parseUrl(config.API_URL + config.SESSION_PATH); + authUrlObject.params.push(`sku=${token || ''}`); + authUrlObject.params.push(`access_token=${customAccessToken || config.ACCESS_TOKEN || ''}`); + + const request: RequestParameters = { + url: formatUrl(authUrlObject), + headers: { + 'Content-Type': 'text/plain', //Skip the pre-flight OPTIONS request + } + }; + + this.pendingRequest = getData(request, (error) => { + this.pendingRequest = null; + callback(error); + this.saveEventData(); + this.processRequests(customAccessToken); + }); + } + + getSessionAPI: (mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) => void = (mapId, skuToken, customAccessToken, callback) => { + this.skuToken = skuToken; + this.errorCb = callback; + + if (config.SESSION_PATH && config.API_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } + } + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) return; + const {id, timestamp} = this.queue.shift(); + + // Only one load event should fire per map + if (id && this.success[id]) return; + + this.getSession(timestamp, this.skuToken, (err) => { + if (err) { + this.errorCb(err); + } else { + if (id) this.success[id] = true; + } + }, customAccessToken); + } } -export class TurnstileEvent - extends TelemetryEvent { +export class TurnstileEvent extends TelemetryEvent { constructor(customAccessToken?: ?string) { super('appUserTurnstile'); this._customAccessToken = customAccessToken; } - postTurnstileEvent: ((tileUrls: Array, customAccessToken?: ?string) => void) = (tileUrls: Array, customAccessToken?: ?string) => { - //Enabled only when Mapbox Access Token is set and a source uses - // mapbox tiles. - if ( - config.EVENTS_URL && config.ACCESS_TOKEN && Array.isArray(tileUrls) && - tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url)) - ) { - this.queueRequest(Date.now(), customAccessToken); - } - }; - - processRequests(customAccessToken?: ?string) { - if (this.pendingRequest || this.queue.length === 0) { - return; - } - - if (!this.anonId || !this.eventData.lastSuccess || !this.eventData.tokenU) { - //Retrieve cached data - this.fetchEventData(); - } - - const tokenData = parseAccessToken(config.ACCESS_TOKEN); - const tokenU = tokenData ? tokenData['u'] : config.ACCESS_TOKEN; - //Reset event data cache if the access token owner changed. - let dueForEvent = tokenU !== this.eventData.tokenU; - - if (!validateUuid(this.anonId)) { - this.anonId = uuid(); - dueForEvent = true; - } - - const nextUpdate = this.queue.shift(); - // Record turnstile event once per calendar day. - if (this.eventData.lastSuccess) { - const lastUpdate = new Date(this.eventData.lastSuccess); - const nextDate = new Date(nextUpdate); - const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); - dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || - lastUpdate.getDate() !== nextDate.getDate(); - } else { - dueForEvent = true; - } - - if (!dueForEvent) { - this.processRequests(); - return; - } - - const additionalPayload = { - sdkIdentifier: 'mapbox-gl-js', - sdkVersion, - skuId: SKU_ID, - "enabled.telemetry": false, - userId: this.anonId, - }; - - this.postEvent( - nextUpdate, - additionalPayload, - err => { - if (!err) { - this.eventData.lastSuccess = nextUpdate; - this.eventData.tokenU = tokenU; - } - }, - customAccessToken, - ); - } + postTurnstileEvent: (tileUrls: Array, customAccessToken?: ?string) => void = (tileUrls, customAccessToken) => { + //Enabled only when Mapbox Access Token is set and a source uses + // mapbox tiles. + if (config.EVENTS_URL && + config.ACCESS_TOKEN && + Array.isArray(tileUrls) && + tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) { + this.queueRequest(Date.now(), customAccessToken); + } + } + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) { + return; + } + + if (!this.anonId || !this.eventData.lastSuccess || !this.eventData.tokenU) { + //Retrieve cached data + this.fetchEventData(); + } + + const tokenData = parseAccessToken(config.ACCESS_TOKEN); + const tokenU = tokenData ? tokenData['u'] : config.ACCESS_TOKEN; + //Reset event data cache if the access token owner changed. + let dueForEvent = tokenU !== this.eventData.tokenU; + + if (!validateUuid(this.anonId)) { + this.anonId = uuid(); + dueForEvent = true; + } + + const nextUpdate = this.queue.shift(); + // Record turnstile event once per calendar day. + if (this.eventData.lastSuccess) { + const lastUpdate = new Date(this.eventData.lastSuccess); + const nextDate = new Date(nextUpdate); + const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); + dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate(); + } else { + dueForEvent = true; + } + + if (!dueForEvent) { + this.processRequests(); + return; + } + + const additionalPayload = { + sdkIdentifier: 'mapbox-gl-js', + sdkVersion, + skuId: SKU_ID, + "enabled.telemetry": false, + userId: this.anonId + }; + + this.postEvent(nextUpdate, additionalPayload, (err) => { + if (!err) { + this.eventData.lastSuccess = nextUpdate; + this.eventData.tokenU = tokenU; + } + }, customAccessToken); + } } const turnstileEvent_ = new TurnstileEvent(); From 27590535a466678188b3760c91b10109b7e3a2dd Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 11:12:33 +0200 Subject: [PATCH 36/72] fix formatting for worker.js --- src/source/worker.js | 570 +++++++++++++++++++------------------------ 1 file changed, 247 insertions(+), 323 deletions(-) diff --git a/src/source/worker.js b/src/source/worker.js index 0c64b0184f4..f0437586a9a 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -33,334 +33,258 @@ import type Projection from '../geo/projection/projection.js'; * @private */ export default class Worker { - self: WorkerGlobalScopeInterface; - actor: Actor; - layerIndexes: { [_: string]: StyleLayerIndex }; - availableImages: { [_: string]: Array }; - workerSourceTypes: { [_: string]: Class }; - workerSources: { [_: string]: { [_: string]: { [_: string]: WorkerSource } } }; - demWorkerSources: { [_: string]: { [_: string]: RasterDEMTileWorkerSource } }; - projections: { [_: string]: Projection }; - defaultProjection: Projection; - isSpriteLoaded: { [_: string]: boolean }; - referrer: ?string; - terrain: ?boolean; - - constructor(self: WorkerGlobalScopeInterface) { - PerformanceUtils.measure('workerEvaluateScript'); - this.self = self; - this.actor = new Actor(self, this); - - this.layerIndexes = {}; - this.availableImages = {}; - this.isSpriteLoaded = {}; - - this.projections = {}; - this.defaultProjection = getProjection({name: 'mercator'}); - - this.workerSourceTypes = { - vector: VectorTileWorkerSource, - geojson: GeoJSONWorkerSource, - }; - - // [mapId][sourceType][sourceName] => worker source instance - this.workerSources = {}; - this.demWorkerSources = {}; - - this.self.registerWorkerSource = (( - name: string, - WorkerSource: Class, - ) => { - if (this.workerSourceTypes[name]) { - throw new Error(`Worker source with name "${name}" already registered.`); - } - this.workerSourceTypes[name] = WorkerSource; - }); - - // This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed. - this.self.registerRTLTextPlugin = (( - rtlTextPlugin: { - applyArabicShaping: Function, - processBidirectionalText: Function, - processStyledBidirectionalText?: Function, - }, - ) => { - if (globalRTLTextPlugin.isParsed()) { - throw new Error('RTL text plugin already registered.'); - } - globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping; - globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText; - globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText; - }); - } - - clearCaches(mapId: string, unused: mixed, callback: WorkerTileCallback) { - delete this.layerIndexes[mapId]; - delete this.availableImages[mapId]; - delete this.workerSources[mapId]; - delete this.demWorkerSources[mapId]; - callback(); - } - - checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) { - // noop, used to check if a worker is fully set up and ready to receive messages - callback(); - } - - setReferrer(mapID: string, referrer: string) { - this.referrer = referrer; - } - - spriteLoaded(mapId: string, bool: boolean) { - this.isSpriteLoaded[mapId] = bool; - for (const workerSource in this.workerSources[mapId]) { - const ws = this.workerSources[mapId][workerSource]; - for (const source in ws) { - if (ws[source] instanceof VectorTileWorkerSource) { - ws[source].isSpriteLoaded = bool; - ws[source].fire(new Event('isSpriteLoaded')); - } - } - } - } - - setImages(mapId: string, images: Array, callback: WorkerTileCallback) { - this.availableImages[mapId] = images; - for (const workerSource in this.workerSources[mapId]) { - const ws = this.workerSources[mapId][workerSource]; - for (const source in ws) { - ws[source].availableImages = images; - } - } - callback(); - } - - enableTerrain: ((mapId: string, enable: boolean, callback: WorkerTileCallback) => void) = (mapId: string, enable: boolean, callback: WorkerTileCallback) => { - this.terrain = enable; - callback(); - }; - - setProjection(mapId: string, config: ProjectionSpecification) { - this.projections[mapId] = getProjection(config); - } - - setLayers( - mapId: string, - layers: Array, - callback: WorkerTileCallback, - ) { - this.getLayerIndex(mapId).replace(layers); - callback(); - } - - updateLayers( - mapId: string, - params: { layers: Array, removedIds: Array }, - callback: WorkerTileCallback, - ) { - this.getLayerIndex(mapId).update(params.layers, params.removedIds); - callback(); - } - - loadTile( - mapId: string, - params: WorkerTileParameters & { type: string }, - callback: WorkerTileCallback, - ) { - assert(params.type); - const p = this.enableTerrain ? - extend({enableTerrain: this.terrain}, params) : - params; - p.projection = this.projections[mapId] || this.defaultProjection; - this.getWorkerSource(mapId, params.type, params.source).loadTile( - p, - callback, - ); - } - - loadDEMTile( - mapId: string, - params: WorkerDEMTileParameters, - callback: WorkerDEMTileCallback, - ) { - const p = this.enableTerrain ? - extend({buildQuadTree: this.terrain}, params) : - params; - this.getDEMWorkerSource(mapId, params.source).loadTile(p, callback); - } - - reloadTile( - mapId: string, - params: WorkerTileParameters & { type: string }, - callback: WorkerTileCallback, - ) { - assert(params.type); - const p = this.enableTerrain ? - extend({enableTerrain: this.terrain}, params) : - params; - p.projection = this.projections[mapId] || this.defaultProjection; - this.getWorkerSource(mapId, params.type, params.source).reloadTile( - p, - callback, - ); - } - - abortTile( - mapId: string, - params: TileParameters & { type: string }, - callback: WorkerTileCallback, - ) { - assert(params.type); - this.getWorkerSource(mapId, params.type, params.source).abortTile( - params, - callback, - ); - } - - removeTile( - mapId: string, - params: TileParameters & { type: string }, - callback: WorkerTileCallback, - ) { - assert(params.type); - this.getWorkerSource(mapId, params.type, params.source).removeTile( - params, - callback, - ); - } - - removeSource( - mapId: string, - params: { source: string } & { type: string }, - callback: WorkerTileCallback, - ) { - assert(params.type); - assert(params.source); - - if ( - !this.workerSources[mapId] || !this.workerSources[mapId][params.type] || - !this.workerSources[mapId][params.type][params.source] - ) { - return; - } - - const worker = this.workerSources[mapId][params.type][params.source]; - delete this.workerSources[mapId][params.type][params.source]; - - if (worker.removeSource !== undefined) { - worker.removeSource(params, callback); - } else { - callback(); - } - } - - /** + self: WorkerGlobalScopeInterface; + actor: Actor; + layerIndexes: {[_: string]: StyleLayerIndex }; + availableImages: {[_: string]: Array }; + workerSourceTypes: {[_: string]: Class }; + workerSources: {[_: string]: {[_: string]: {[_: string]: WorkerSource } } }; + demWorkerSources: {[_: string]: {[_: string]: RasterDEMTileWorkerSource } }; + projections: {[_: string]: Projection }; + defaultProjection: Projection; + isSpriteLoaded: {[_: string]: boolean }; + referrer: ?string; + terrain: ?boolean; + + constructor(self: WorkerGlobalScopeInterface) { + PerformanceUtils.measure('workerEvaluateScript'); + this.self = self; + this.actor = new Actor(self, this); + + this.layerIndexes = {}; + this.availableImages = {}; + this.isSpriteLoaded = {}; + + this.projections = {}; + this.defaultProjection = getProjection({name: 'mercator'}); + + this.workerSourceTypes = { + vector: VectorTileWorkerSource, + geojson: GeoJSONWorkerSource + }; + + // [mapId][sourceType][sourceName] => worker source instance + this.workerSources = {}; + this.demWorkerSources = {}; + + this.self.registerWorkerSource = (name: string, WorkerSource: Class) => { + if (this.workerSourceTypes[name]) { + throw new Error(`Worker source with name "${name}" already registered.`); + } + this.workerSourceTypes[name] = WorkerSource; + }; + + // This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed. + this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => { + if (globalRTLTextPlugin.isParsed()) { + throw new Error('RTL text plugin already registered.'); + } + globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping; + globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText; + globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText; + }; + } + + clearCaches(mapId: string, unused: mixed, callback: WorkerTileCallback) { + delete this.layerIndexes[mapId]; + delete this.availableImages[mapId]; + delete this.workerSources[mapId]; + delete this.demWorkerSources[mapId]; + callback(); + } + + checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) { + // noop, used to check if a worker is fully set up and ready to receive messages + callback(); + } + + setReferrer(mapID: string, referrer: string) { + this.referrer = referrer; + } + + spriteLoaded(mapId: string, bool: boolean) { + this.isSpriteLoaded[mapId] = bool; + for (const workerSource in this.workerSources[mapId]) { + const ws = this.workerSources[mapId][workerSource]; + for (const source in ws) { + if (ws[source] instanceof VectorTileWorkerSource) { + ws[source].isSpriteLoaded = bool; + ws[source].fire(new Event('isSpriteLoaded')); + } + } + } + } + + setImages(mapId: string, images: Array, callback: WorkerTileCallback) { + this.availableImages[mapId] = images; + for (const workerSource in this.workerSources[mapId]) { + const ws = this.workerSources[mapId][workerSource]; + for (const source in ws) { + ws[source].availableImages = images; + } + } + callback(); + } + + enableTerrain: ((mapId: string, enable: boolean, callback: WorkerTileCallback) => void) = (mapId: string, enable: boolean, callback: WorkerTileCallback) => { + this.terrain = enable; + callback(); + }; + + setProjection(mapId: string, config: ProjectionSpecification) { + this.projections[mapId] = getProjection(config); + } + + setLayers(mapId: string, layers: Array, callback: WorkerTileCallback) { + this.getLayerIndex(mapId).replace(layers); + callback(); + } + + updateLayers(mapId: string, params: {layers: Array, removedIds: Array}, callback: WorkerTileCallback) { + this.getLayerIndex(mapId).update(params.layers, params.removedIds); + callback(); + } + + loadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { + assert(params.type); + const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; + p.projection = this.projections[mapId] || this.defaultProjection; + this.getWorkerSource(mapId, params.type, params.source).loadTile(p, callback); + } + + loadDEMTile(mapId: string, params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { + const p = this.enableTerrain ? extend({buildQuadTree: this.terrain}, params) : params; + this.getDEMWorkerSource(mapId, params.source).loadTile(p, callback); + } + + reloadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { + assert(params.type); + const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; + p.projection = this.projections[mapId] || this.defaultProjection; + this.getWorkerSource(mapId, params.type, params.source).reloadTile(p, callback); + } + + abortTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) { + assert(params.type); + this.getWorkerSource(mapId, params.type, params.source).abortTile(params, callback); + } + + removeTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) { + assert(params.type); + this.getWorkerSource(mapId, params.type, params.source).removeTile(params, callback); + } + + removeSource(mapId: string, params: {source: string} & {type: string}, callback: WorkerTileCallback) { + assert(params.type); + assert(params.source); + + if (!this.workerSources[mapId] || + !this.workerSources[mapId][params.type] || + !this.workerSources[mapId][params.type][params.source]) { + return; + } + + const worker = this.workerSources[mapId][params.type][params.source]; + delete this.workerSources[mapId][params.type][params.source]; + + if (worker.removeSource !== undefined) { + worker.removeSource(params, callback); + } else { + callback(); + } + } + + /** * Load a {@link WorkerSource} script at params.url. The script is run * (using importScripts) with `registerWorkerSource` in scope, which is a * function taking `(name, workerSourceObject)`. * @private */ - loadWorkerSource( - map: string, - params: { url: string }, - callback: Callback, - ) { - try { - this.self.importScripts(params.url); - callback(); - } catch (e) { - callback(e.toString()); - } - } - - syncRTLPluginState( - map: string, - state: PluginState, - callback: Callback, - ) { - try { - globalRTLTextPlugin.setState(state); - const pluginURL = globalRTLTextPlugin.getPluginURL(); - if ( - globalRTLTextPlugin.isLoaded() && !globalRTLTextPlugin.isParsed() && - pluginURL != null // Not possible when `isLoaded` is true, but keeps flow happy - - ) { - this.self.importScripts(pluginURL); - const complete = globalRTLTextPlugin.isParsed(); - const error = complete ? - undefined : - new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`,); - callback(error, complete); - } - } catch (e) { - callback(e.toString()); - } - } - - getAvailableImages(mapId: string): Array { - let availableImages = this.availableImages[mapId]; - - if (!availableImages) { - availableImages = []; - } - - return availableImages; - } - - getLayerIndex(mapId: string): StyleLayerIndex { - let layerIndexes = this.layerIndexes[mapId]; - if (!layerIndexes) { - layerIndexes = this.layerIndexes[mapId] = new StyleLayerIndex(); - } - return layerIndexes; - } - - getWorkerSource(mapId: string, type: string, source: string): WorkerSource { - if (!this.workerSources[mapId]) this.workerSources[mapId] = {}; - if (!this.workerSources[mapId][type]) this.workerSources[mapId][type] = {}; - - if (!this.workerSources[mapId][type][source]) { - // use a wrapped actor so that we can attach a target mapId param - // to any messages invoked by the WorkerSource - const actor = { - send: (type, data, callback, _, mustQueue, metadata) => { - this.actor.send(type, data, callback, mapId, mustQueue, metadata); - }, - scheduler: this.actor.scheduler, - }; - this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)( - (actor: any), - this.getLayerIndex(mapId), - this.getAvailableImages(mapId), - this.isSpriteLoaded[mapId], - ); - } - - return this.workerSources[mapId][type][source]; - } - - getDEMWorkerSource(mapId: string, source: string): RasterDEMTileWorkerSource { - if (!this.demWorkerSources[mapId]) this.demWorkerSources[mapId] = {}; - - if (!this.demWorkerSources[mapId][source]) { - this.demWorkerSources[mapId][source] = new RasterDEMTileWorkerSource(); - } - - return this.demWorkerSources[mapId][source]; - } - - enforceCacheSizeLimit(mapId: string, limit: number) { - enforceCacheSizeLimit(limit); - } - - getWorkerPerformanceMetrics( - mapId: string, - params: any, - callback: (error: ?Error, result: ?Object) => void, - ) { - callback(undefined, PerformanceUtils.getWorkerPerformanceMetrics()); - } + loadWorkerSource(map: string, params: { url: string }, callback: Callback) { + try { + this.self.importScripts(params.url); + callback(); + } catch (e) { + callback(e.toString()); + } + } + + syncRTLPluginState(map: string, state: PluginState, callback: Callback) { + try { + globalRTLTextPlugin.setState(state); + const pluginURL = globalRTLTextPlugin.getPluginURL(); + if ( + globalRTLTextPlugin.isLoaded() && + !globalRTLTextPlugin.isParsed() && + pluginURL != null // Not possible when `isLoaded` is true, but keeps flow happy + ) { + this.self.importScripts(pluginURL); + const complete = globalRTLTextPlugin.isParsed(); + const error = complete ? undefined : new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`); + callback(error, complete); + } + } catch (e) { + callback(e.toString()); + } + } + + getAvailableImages(mapId: string): Array { + let availableImages = this.availableImages[mapId]; + + if (!availableImages) { + availableImages = []; + } + + return availableImages; + } + + getLayerIndex(mapId: string): StyleLayerIndex { + let layerIndexes = this.layerIndexes[mapId]; + if (!layerIndexes) { + layerIndexes = this.layerIndexes[mapId] = new StyleLayerIndex(); + } + return layerIndexes; + } + + getWorkerSource(mapId: string, type: string, source: string): WorkerSource { + if (!this.workerSources[mapId]) + this.workerSources[mapId] = {}; + if (!this.workerSources[mapId][type]) + this.workerSources[mapId][type] = {}; + + if (!this.workerSources[mapId][type][source]) { + // use a wrapped actor so that we can attach a target mapId param + // to any messages invoked by the WorkerSource + const actor = { + send: (type, data, callback, _, mustQueue, metadata) => { + this.actor.send(type, data, callback, mapId, mustQueue, metadata); + }, + scheduler: this.actor.scheduler + }; + this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)((actor: any), this.getLayerIndex(mapId), this.getAvailableImages(mapId), this.isSpriteLoaded[mapId]); + } + + return this.workerSources[mapId][type][source]; + } + + getDEMWorkerSource(mapId: string, source: string): RasterDEMTileWorkerSource { + if (!this.demWorkerSources[mapId]) + this.demWorkerSources[mapId] = {}; + + if (!this.demWorkerSources[mapId][source]) { + this.demWorkerSources[mapId][source] = new RasterDEMTileWorkerSource(); + } + + return this.demWorkerSources[mapId][source]; + } + + enforceCacheSizeLimit(mapId: string, limit: number) { + enforceCacheSizeLimit(limit); + } + + getWorkerPerformanceMetrics(mapId: string, params: any, callback: (error: ?Error, result: ?Object) => void) { + callback(undefined, PerformanceUtils.getWorkerPerformanceMetrics()); + } } /* global self, WorkerGlobalScope */ From 47a350a8ecedea2d74de99de364a0ee085e3abb8 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 11:33:09 +0200 Subject: [PATCH 37/72] fix formatting for utils/actor.js --- src/util/actor.js | 299 +++++++++++++++++++++------------------------- 1 file changed, 136 insertions(+), 163 deletions(-) diff --git a/src/util/actor.js b/src/util/actor.js index cbddde2e6c4..b981c2c0f5a 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -20,28 +20,28 @@ import type {Cancelable} from '../types/cancelable.js'; * @private */ class Actor { - target: any; - parent: any; - mapId: ?number; - callbacks: { number: any }; - name: string; - cancelCallbacks: { number: Cancelable }; - globalScope: any; - scheduler: Scheduler; + target: any; + parent: any; + mapId: ?number; + callbacks: { number: any }; + name: string; + cancelCallbacks: { number: Cancelable }; + globalScope: any; + scheduler: Scheduler; - constructor(target: any, parent: any, mapId: ?number) { - this.target = target; - this.parent = parent; - this.mapId = mapId; - this.callbacks = {}; - this.cancelCallbacks = {}; - bindAll(['receive'], this); - this.target.addEventListener('message', this.receive, false); - this.globalScope = isWorker() ? target : window; - this.scheduler = new Scheduler(); - } + constructor(target: any, parent: any, mapId: ?number) { + this.target = target; + this.parent = parent; + this.mapId = mapId; + this.callbacks = {}; + this.cancelCallbacks = {}; + bindAll(['receive'], this); + this.target.addEventListener('message', this.receive, false); + this.globalScope = isWorker() ? target : window; + this.scheduler = new Scheduler(); + } - /** + /** * Sends a message from a main-thread map to a Worker or from a Worker back to * a main-thread map instance. * @@ -49,156 +49,129 @@ class Actor { * @param targetMapId A particular mapId to which to send this message. * @private */ - send( - type: string, - data: mixed, - callback: ?Function, - targetMapId: ?string, - mustQueue: boolean = false, - callbackMetadata?: Object, - ): ?Cancelable { - // We're using a string ID instead of numbers because they are being used as object keys - // anyway, and thus stringified implicitly. We use random IDs because an actor may receive - // message from multiple other actors which could run in different execution context. A - // linearly increasing ID could produce collisions. - const id = Math.round(Math.random() * 1e18).toString(36).substring(0, 10); - if (callback) { - callback.metadata = callbackMetadata; - this.callbacks[id] = callback; - } - const buffers: ?Array = isSafari(this.globalScope) ? - undefined : - []; - this.target.postMessage( - { - id, - type, - hasCallback: !!callback, - targetMapId, - mustQueue, - sourceMapId: this.mapId, - data: serialize(data, buffers), - }, - buffers, - ); - return { - cancel: () => { - if (callback) { - // Set the callback to null so that it never fires after the request is aborted. - delete this.callbacks[id]; - } - this.target.postMessage( - { - id, - type: '', - targetMapId, - sourceMapId: this.mapId, - }, - ); - }, - }; - } + send(type: string, data: mixed, callback: ?Function, targetMapId: ?string, mustQueue: boolean = false, callbackMetadata?: Object): ?Cancelable { + // We're using a string ID instead of numbers because they are being used as object keys + // anyway, and thus stringified implicitly. We use random IDs because an actor may receive + // message from multiple other actors which could run in different execution context. A + // linearly increasing ID could produce collisions. + const id = Math.round((Math.random() * 1e18)).toString(36).substring(0, 10); + if (callback) { + callback.metadata = callbackMetadata; + this.callbacks[id] = callback; + } + const buffers: ?Array = isSafari(this.globalScope) ? undefined : []; + this.target.postMessage({ + id, + type, + hasCallback: !!callback, + targetMapId, + mustQueue, + sourceMapId: this.mapId, + data: serialize(data, buffers) + }, buffers); + return { + cancel: () => { + if (callback) { + // Set the callback to null so that it never fires after the request is aborted. + delete this.callbacks[id]; + } + this.target.postMessage({ + id, + type: '', + targetMapId, + sourceMapId: this.mapId + }); + } + }; + } - receive: ((message: any) => void) = (message: Object) => { - const data = message.data, - id = data.id; + receive: ((message: any) => void) = (message: Object) => { + const data = message.data, + id = data.id; - if (!id) { - return; - } + if (!id) { + return; + } - if (data.targetMapId && this.mapId !== data.targetMapId) { - return; - } + if (data.targetMapId && this.mapId !== data.targetMapId) { + return; + } - if (data.type === '') { - // Remove the original request from the queue. This is only possible if it - // hasn't been kicked off yet. The id will remain in the queue, but because - // there is no associated task, it will be dropped once it's time to execute it. - const cancel = this.cancelCallbacks[id]; - delete this.cancelCallbacks[id]; - if (cancel) { - cancel.cancel(); - } - } else { - if (data.mustQueue || isWorker()) { - // for worker tasks that are often cancelled, such as loadTile, store them before actually - // processing them. This is necessary because we want to keep receiving messages. - // Some tasks may take a while in the worker thread, so before executing the next task - // in our queue, postMessage preempts this and messages can be processed. - // We're using a MessageChannel object to get throttle the process() flow to one at a time. - const callback = this.callbacks[id]; - const metadata = (callback && callback.metadata) || {type: "message"}; - this.cancelCallbacks[id] = this.scheduler.add( - () => this.processTask(id, data), - metadata, - ); - } else { - // In the main thread, process messages immediately so that other work does not slip in - // between getting partial data back from workers. - this.processTask(id, data); - } - } - }; + if (data.type === '') { + // Remove the original request from the queue. This is only possible if it + // hasn't been kicked off yet. The id will remain in the queue, but because + // there is no associated task, it will be dropped once it's time to execute it. + const cancel = this.cancelCallbacks[id]; + delete this.cancelCallbacks[id]; + if (cancel) { + cancel.cancel(); + } + } else { + if (data.mustQueue || isWorker()) { + // for worker tasks that are often cancelled, such as loadTile, store them before actually + // processing them. This is necessary because we want to keep receiving messages. + // Some tasks may take a while in the worker thread, so before executing the next task + // in our queue, postMessage preempts this and messages can be processed. + // We're using a MessageChannel object to get throttle the process() flow to one at a time. + const callback = this.callbacks[id]; + const metadata = (callback && callback.metadata) || {type: "message"}; + this.cancelCallbacks[id] = this.scheduler.add(() => this.processTask(id, data), metadata); + } else { + // In the main thread, process messages immediately so that other work does not slip in + // between getting partial data back from workers. + this.processTask(id, data); + } + } + } - processTask(id: number, task: any) { - if (task.type === '') { - // The done() function in the counterpart has been called, and we are now - // firing the callback in the originating actor, if there is one. - const callback = this.callbacks[id]; - delete this.callbacks[id]; - if (callback) { - // If we get a response, but don't have a callback, the request was canceled. - if (task.error) { - callback(deserialize(task.error)); - } else { - callback(null, deserialize(task.data)); - } - } - } else { - const buffers: ?Array = isSafari(this.globalScope) ? - undefined : - []; - const done = task.hasCallback ? - (err, data) => { - delete this.cancelCallbacks[id]; - this.target.postMessage( - { - id, - type: '', - sourceMapId: this.mapId, - error: err ? serialize(err) : null, - data: serialize(data, buffers), - }, - buffers, - ); - } : - _ => {}; + processTask(id: number, task: any) { + if (task.type === '') { + // The done() function in the counterpart has been called, and we are now + // firing the callback in the originating actor, if there is one. + const callback = this.callbacks[id]; + delete this.callbacks[id]; + if (callback) { + // If we get a response, but don't have a callback, the request was canceled. + if (task.error) { + callback(deserialize(task.error)); + } else { + callback(null, deserialize(task.data)); + } + } + } else { + const buffers: ?Array = isSafari(this.globalScope) ? undefined : []; + const done = task.hasCallback ? (err, data) => { + delete this.cancelCallbacks[id]; + this.target.postMessage({ + id, + type: '', + sourceMapId: this.mapId, + error: err ? serialize(err) : null, + data: serialize(data, buffers) + }, buffers); + } : (_) => { + }; - const params = (deserialize(task.data): any); - if (this.parent[task.type]) { - // task.type == 'loadTile', 'removeTile', etc. - this.parent[task.type](task.sourceMapId, params, done); - } else if (this.parent.getWorkerSource) { - // task.type == sourcetype.method - const keys = task.type.split('.'); - const scope = (this.parent: any).getWorkerSource( - task.sourceMapId, - keys[0], - params.source, - ); - scope[keys[1]](params, done); - } else { - // No function was found. - done(new Error(`Could not find function ${task.type}`)); - } - } - } + const params = (deserialize(task.data): any); + if (this.parent[task.type]) { + // task.type == 'loadTile', 'removeTile', etc. + this.parent[task.type](task.sourceMapId, params, done); + } else if (this.parent.getWorkerSource) { + // task.type == sourcetype.method + const keys = task.type.split('.'); + const scope = (this.parent: any).getWorkerSource(task.sourceMapId, keys[0], params.source); + scope[keys[1]](params, done); + } else { + // No function was found. + done(new Error(`Could not find function ${task.type}`)); + } + } + } - remove() { - this.scheduler.remove(); - this.target.removeEventListener('message', this.receive, false); - } + remove() { + this.scheduler.remove(); + this.target.removeEventListener('message', this.receive, false); + } } export default Actor; From 96ac32b5bbe1fed81794256cbbd62352f9f484a5 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 11:34:50 +0200 Subject: [PATCH 38/72] fix formatting for utils/scheduler.js --- src/util/scheduler.js | 187 +++++++++++++++++++++--------------------- 1 file changed, 92 insertions(+), 95 deletions(-) diff --git a/src/util/scheduler.js b/src/util/scheduler.js index 4e08687dd29..862ed9eb937 100644 --- a/src/util/scheduler.js +++ b/src/util/scheduler.js @@ -22,101 +22,98 @@ type Task = { }; class Scheduler { - tasks: { [number]: Task }; - taskQueue: Array; - invoker: ThrottledInvoker; - nextId: number; - - constructor() { - this.tasks = {}; - this.taskQueue = []; - bindAll(['process'], this); - this.invoker = new ThrottledInvoker(this.process); - - this.nextId = 0; - } - - add(fn: TaskFunction, metadata: TaskMetadata): Cancelable { - const id = this.nextId++; - const priority = getPriority(metadata); - - if (priority === 0) { - // Process tasks with priority 0 immediately. Do not yield to the event loop. - const m = isWorker() ? - PerformanceUtils.beginMeasure('workerTask') : - undefined; - try { - fn(); - } finally { - if (m) PerformanceUtils.endMeasure(m); - } - return { - cancel: () => {}, - }; - } - - this.tasks[id] = {fn, metadata, priority, id}; - this.taskQueue.push(id); - this.invoker.trigger(); - return { - cancel: () => { - delete this.tasks[id]; - }, - }; - } - - process: (() => void) = () => { - const m = isWorker() ? - PerformanceUtils.beginMeasure('workerTask') : - undefined; - try { - this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); - - if (!this.taskQueue.length) { - return; - } - const id = this.pick(); - if (id === null) return; - - const task = this.tasks[id]; - delete this.tasks[id]; - // Schedule another process call if we know there's more to process _before_ invoking the - // current task. This is necessary so that processing continues even if the current task - // doesn't execute successfully. - if (this.taskQueue.length) { - this.invoker.trigger(); - } - if (!task) { - // If the task ID doesn't have associated task data anymore, it was canceled. - return; - } - - task.fn(); - } finally { - if (m) PerformanceUtils.endMeasure(m); - } - }; - - pick(): null | number { - let minIndex = null; - let minPriority = Infinity; - for (let i = 0; i < this.taskQueue.length; i++) { - const id = this.taskQueue[i]; - const task = this.tasks[id]; - if (task.priority < minPriority) { - minPriority = task.priority; - minIndex = i; - } - } - if (minIndex === null) return null; - const id = this.taskQueue[minIndex]; - this.taskQueue.splice(minIndex, 1); - return id; - } - - remove() { - this.invoker.remove(); - } + + tasks: { [number]: Task }; + taskQueue: Array; + invoker: ThrottledInvoker; + nextId: number; + + constructor() { + this.tasks = {}; + this.taskQueue = []; + bindAll(['process'], this); + this.invoker = new ThrottledInvoker(this.process); + + this.nextId = 0; + } + + add(fn: TaskFunction, metadata: TaskMetadata): Cancelable { + const id = this.nextId++; + const priority = getPriority(metadata); + + if (priority === 0) { + // Process tasks with priority 0 immediately. Do not yield to the event loop. + const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; + try { + fn(); + } finally { + if (m) PerformanceUtils.endMeasure(m); + } + return { + cancel: () => {} + }; + } + + this.tasks[id] = {fn, metadata, priority, id}; + this.taskQueue.push(id); + this.invoker.trigger(); + return { + cancel: () => { + delete this.tasks[id]; + } + }; + } + + process: (() => void) = () => { + const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; + try { + this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); + + if (!this.taskQueue.length) { + return; + } + const id = this.pick(); + if (id === null) return; + + const task = this.tasks[id]; + delete this.tasks[id]; + // Schedule another process call if we know there's more to process _before_ invoking the + // current task. This is necessary so that processing continues even if the current task + // doesn't execute successfully. + if (this.taskQueue.length) { + this.invoker.trigger(); + } + if (!task) { + // If the task ID doesn't have associated task data anymore, it was canceled. + return; + } + + task.fn(); + } finally { + if (m) PerformanceUtils.endMeasure(m); + } + } + + pick(): null | number { + let minIndex = null; + let minPriority = Infinity; + for (let i = 0; i < this.taskQueue.length; i++) { + const id = this.taskQueue[i]; + const task = this.tasks[id]; + if (task.priority < minPriority) { + minPriority = task.priority; + minIndex = i; + } + } + if (minIndex === null) return null; + const id = this.taskQueue[minIndex]; + this.taskQueue.splice(minIndex, 1); + return id; + } + + remove() { + this.invoker.remove(); + } } function getPriority({type, isSymbolTile, zoom}: TaskMetadata): number { From 019711973945d842e791f94f8505e1d0444ab422 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 11:52:48 +0200 Subject: [PATCH 39/72] fix formatting for ui/camera.js --- src/ui/camera.js | 2146 +++++++++++++++++++++------------------------- 1 file changed, 981 insertions(+), 1165 deletions(-) diff --git a/src/ui/camera.js b/src/ui/camera.js index 0fc4710725c..4cfc1cc53db 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -179,51 +179,46 @@ const freeCameraNotSupportedWarning = 'map.setFreeCameraOptions(...) and map.get * @see [Example: Fit a map to a bounding box](https://docs.mapbox.com/mapbox-gl-js/example/fitbounds/) */ -class Camera - extends Evented { - transform: Transform; - _moving: boolean; - _zooming: boolean; - _rotating: boolean; - _pitching: boolean; - _padding: boolean; - - _bearingSnap: number; - _easeStart: number; - _easeOptions: { duration: number, easing: (_: number) => number }; - _easeId: string | void; - - _onEaseFrame: ?((_: number) => Transform | void); - _onEaseEnd: ?((easeId?: string) => void); - _easeFrameId: ?TaskID; - - +_requestRenderFrame: (() => void) => TaskID; - +_cancelRenderFrame: (_: TaskID) => void; - - +_preloadTiles: ( - transform: Transform | Array, - callback?: Callback - ) => any; - - constructor(transform: Transform, options: { bearingSnap: number }) { - super(); - this._moving = false; - this._zooming = false; - this.transform = transform; - this._bearingSnap = options.bearingSnap; - - bindAll(['_renderFrameCallback'], this); - - //addAssertions(this); - - } - - /** @section {Camera} +class Camera extends Evented { + transform: Transform; + _moving: boolean; + _zooming: boolean; + _rotating: boolean; + _pitching: boolean; + _padding: boolean; + + _bearingSnap: number; + _easeStart: number; + _easeOptions: {duration: number, easing: (_: number) => number}; + _easeId: string | void; + + _onEaseFrame: ?(_: number) => Transform | void; + _onEaseEnd: ?(easeId?: string) => void; + _easeFrameId: ?TaskID; + + +_requestRenderFrame: (() => void) => TaskID; + +_cancelRenderFrame: (_: TaskID) => void; + + +_preloadTiles: (transform: Transform | Array, callback?: Callback) => any; + + constructor(transform: Transform, options: {bearingSnap: number}) { + super(); + this._moving = false; + this._zooming = false; + this.transform = transform; + this._bearingSnap = options.bearingSnap; + + bindAll(['_renderFrameCallback'], this); + + //addAssertions(this); + } + + /** @section {Camera} * @method * @instance * @memberof Map */ - /** + /** * Returns the map's geographical centerpoint. * * @memberof Map# @@ -235,11 +230,9 @@ class Camera * const {lng, lat} = map.getCenter(); * @see [Tutorial: Use Mapbox GL JS in a React app](https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/#store-the-new-coordinates) */ - getCenter(): LngLat { - return new LngLat(this.transform.center.lng, this.transform.center.lat); - } + getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); } - /** + /** * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`. * * @memberof Map# @@ -251,11 +244,11 @@ class Camera * @example * map.setCenter([-74, 38]); */ - setCenter(center: LngLatLike, eventData?: Object): this { - return this.jumpTo({center}, eventData); - } + setCenter(center: LngLatLike, eventData?: Object): this { + return this.jumpTo({center}, eventData); + } - /** + /** * Pans the map by the specified offset. * * @memberof Map# @@ -272,16 +265,12 @@ class Camera * map.panBy([-74, 38], {duration: 5000}); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - panBy(offset: PointLike, options?: AnimationOptions, eventData?: Object): this { - offset = Point.convert(offset).mult(-1); - return this.panTo( - this.transform.center, - extend({offset}, options), - eventData, - ); - } - - /** + panBy(offset: PointLike, options?: AnimationOptions, eventData?: Object): this { + offset = Point.convert(offset).mult(-1); + return this.panTo(this.transform.center, extend({offset}, options), eventData); + } + + /** * Pans the map to the specified location with an animated transition. * * @memberof Map# @@ -298,23 +287,13 @@ class Camera * map.panTo([-74, 38], {duration: 5000}); * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ - panTo( - lnglat: LngLatLike, - options?: AnimationOptions, - eventData?: Object, - ): this { - return this.easeTo( - extend( - { - center: lnglat, - }, - options, - ), - eventData, - ); - } - - /** + panTo(lnglat: LngLatLike, options?: AnimationOptions, eventData?: Object): this { + return this.easeTo(extend({ + center: lnglat + }, options), eventData); + } + + /** * Returns the map's current zoom level. * * @memberof Map# @@ -322,11 +301,9 @@ class Camera * @example * map.getZoom(); */ - getZoom(): number { - return this.transform.zoom; - } + getZoom(): number { return this.transform.zoom; } - /** + /** * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`. * * @memberof Map# @@ -343,12 +320,12 @@ class Camera * // Zoom to the zoom level 5 without an animated transition * map.setZoom(5); */ - setZoom(zoom: number, eventData?: Object): this { - this.jumpTo({zoom}, eventData); - return this; - } + setZoom(zoom: number, eventData?: Object): this { + this.jumpTo({zoom}, eventData); + return this; + } - /** + /** * Zooms the map to the specified zoom level, with an animated transition. * * @memberof Map# @@ -371,19 +348,13 @@ class Camera * offset: [100, 50] * }); */ - zoomTo(zoom: number, options: ?AnimationOptions, eventData?: Object): this { - return this.easeTo( - extend( - { - zoom, - }, - options, - ), - eventData, - ); - } - - /** + zoomTo(zoom: number, options: ? AnimationOptions, eventData?: Object): this { + return this.easeTo(extend({ + zoom + }, options), eventData); + } + + /** * Increases the map's zoom level by 1. * * @memberof Map# @@ -400,12 +371,12 @@ class Camera * // zoom the map in one level with a custom animation duration * map.zoomIn({duration: 1000}); */ - zoomIn(options?: AnimationOptions, eventData?: Object): this { - this.zoomTo(this.getZoom() + 1, options, eventData); - return this; - } + zoomIn(options?: AnimationOptions, eventData?: Object): this { + this.zoomTo(this.getZoom() + 1, options, eventData); + return this; + } - /** + /** * Decreases the map's zoom level by 1. * * @memberof Map# @@ -422,12 +393,12 @@ class Camera * // zoom the map out one level with a custom animation offset * map.zoomOut({offset: [80, 60]}); */ - zoomOut(options?: AnimationOptions, eventData?: Object): this { - this.zoomTo(this.getZoom() - 1, options, eventData); - return this; - } + zoomOut(options?: AnimationOptions, eventData?: Object): this { + this.zoomTo(this.getZoom() - 1, options, eventData); + return this; + } - /** + /** * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * @@ -437,11 +408,11 @@ class Camera * const bearing = map.getBearing(); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - getBearing(): number { - return this.transform.bearing; - } + getBearing(): number { + return this.transform.bearing; + } - /** + /** * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * @@ -457,12 +428,12 @@ class Camera * // Rotate the map to 90 degrees. * map.setBearing(90); */ - setBearing(bearing: number, eventData?: Object): this { - this.jumpTo({bearing}, eventData); - return this; - } + setBearing(bearing: number, eventData?: Object): this { + this.jumpTo({bearing}, eventData); + return this; + } - /** + /** * Returns the current padding applied around the map viewport. * * @memberof Map# @@ -470,11 +441,9 @@ class Camera * @example * const padding = map.getPadding(); */ - getPadding(): PaddingOptions { - return this.transform.padding; - } + getPadding(): PaddingOptions { return this.transform.padding; } - /** + /** * Sets the padding in pixels around the viewport. * * Equivalent to `jumpTo({padding: padding})`. @@ -489,12 +458,12 @@ class Camera * // Sets a left padding of 300px, and a top padding of 50px * map.setPadding({left: 300, top: 50}); */ - setPadding(padding: PaddingOptions, eventData?: Object): this { - this.jumpTo({padding}, eventData); - return this; - } + setPadding(padding: PaddingOptions, eventData?: Object): this { + this.jumpTo({padding}, eventData); + return this; + } - /** + /** * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction * that is \"up\"; for example, a bearing of 90° orients the map so that east is up. * @@ -512,19 +481,13 @@ class Camera * // rotateTo with an animation of 2 seconds. * map.rotateTo(30, {duration: 2000}); */ - rotateTo(bearing: number, options?: EasingOptions, eventData?: Object): this { - return this.easeTo( - extend( - { - bearing, - }, - options, - ), - eventData, - ); - } - - /** + rotateTo(bearing: number, options?: EasingOptions, eventData?: Object): this { + return this.easeTo(extend({ + bearing + }, options), eventData); + } + + /** * Rotates the map so that north is up (0° bearing), with an animated transition. * * @memberof Map# @@ -538,12 +501,12 @@ class Camera * // resetNorth with an animation of 2 seconds. * map.resetNorth({duration: 2000}); */ - resetNorth(options?: EasingOptions, eventData?: Object): this { - this.rotateTo(0, extend({duration: 1000}, options), eventData); - return this; - } + resetNorth(options?: EasingOptions, eventData?: Object): this { + this.rotateTo(0, extend({duration: 1000}, options), eventData); + return this; + } - /** + /** * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition. * * @memberof Map# @@ -557,22 +520,16 @@ class Camera * // resetNorthPitch with an animation of 2 seconds. * map.resetNorthPitch({duration: 2000}); */ - resetNorthPitch(options?: EasingOptions, eventData?: Object): this { - this.easeTo( - extend( - { + resetNorthPitch(options?: EasingOptions, eventData?: Object): this { + this.easeTo(extend({ bearing: 0, pitch: 0, - duration: 1000, - }, - options, - ), - eventData, - ); - return this; - } - - /** + duration: 1000 + }, options), eventData); + return this; + } + + /** * Snaps the map so that north is up (0° bearing), if the current bearing is * close enough to it (within the `bearingSnap` threshold). * @@ -587,14 +544,14 @@ class Camera * // snapToNorth with an animation of 2 seconds. * map.snapToNorth({duration: 2000}); */ - snapToNorth(options?: EasingOptions, eventData?: Object): this { - if (Math.abs(this.getBearing()) < this._bearingSnap) { - return this.resetNorth(options, eventData); - } - return this; - } - - /** + snapToNorth(options?: EasingOptions, eventData?: Object): this { + if (Math.abs(this.getBearing()) < this._bearingSnap) { + return this.resetNorth(options, eventData); + } + return this; + } + + /** * Returns the map's current [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). * * @memberof Map# @@ -602,11 +559,9 @@ class Camera * @example * const pitch = map.getPitch(); */ - getPitch(): number { - return this.transform.pitch; - } + getPitch(): number { return this.transform.pitch; } - /** + /** * Sets the map's [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). Equivalent to `jumpTo({pitch: pitch})`. * * @memberof Map# @@ -620,12 +575,12 @@ class Camera * // setPitch with an animation of 2 seconds. * map.setPitch(80, {duration: 2000}); */ - setPitch(pitch: number, eventData?: Object): this { - this.jumpTo({pitch}, eventData); - return this; - } + setPitch(pitch: number, eventData?: Object): this { + this.jumpTo({pitch}, eventData); + return this; + } - /** + /** * Returns a {@link CameraOptions} object for the highest zoom level * up to and including `Map#getMaxZoom()` that fits the bounds * in the viewport at the specified bearing. @@ -648,160 +603,149 @@ class Camera * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ - cameraForBounds( - bounds: LngLatBoundsLike, - options?: CameraOptions, - ): ?EasingOptions { - bounds = LngLatBounds.convert(bounds); - const bearing = (options && options.bearing) || 0; - const pitch = (options && options.pitch) || 0; - const lnglat0 = bounds.getNorthWest(); - const lnglat1 = bounds.getSouthEast(); - return this._cameraForBounds(this.transform, lnglat0, lnglat1, bearing, pitch, options); - } - - _extendCameraOptions(options?: CameraOptions): FullCameraOptions { - const defaultPadding = { - top: 0, - bottom: 0, - right: 0, - left: 0, - }; - options = extend({ - padding: defaultPadding, - offset: [0, 0], - maxZoom: this.transform.maxZoom - }, options); - - if (typeof options.padding === 'number') { - const p = options.padding; - options.padding = { - top: p, - bottom: p, - right: p, - left: p, - }; - } - options.padding = extend(defaultPadding, options.padding); - return options; - } - - _minimumAABBFrustumDistance(tr: Transform, aabb: Aabb): number { - const aabbW = aabb.max[0] - aabb.min[0]; - const aabbH = aabb.max[1] - aabb.min[1]; - const aabbAspectRatio = aabbW / aabbH; - const selectXAxis = aabbAspectRatio > tr.aspect; - - const minimumDistance = selectXAxis ? - aabbW / (2 * Math.tan(tr.fovX * 0.5) * tr.aspect) : - aabbH / (2 * Math.tan(tr.fovY * 0.5) * tr.aspect); - - return minimumDistance; - } - - _cameraForBoundsOnGlobe( - transform: Transform, - p0: LngLatLike, - p1: LngLatLike, - bearing: number, - pitch: number, - options?: CameraOptions, - ): ?EasingOptions { - const tr = transform.clone(); - const eOptions = this._extendCameraOptions(options); - - tr.bearing = bearing; - tr.pitch = pitch; - - const coord0 = LngLat.convert(p0); - const coord1 = LngLat.convert(p1); - - const midLat = (coord0.lat + coord1.lat) * 0.5; - const midLng = (coord0.lng + coord1.lng) * 0.5; - - const origin = latLngToECEF(midLat, midLng); - - const zAxis = vec3.normalize([], origin); - const xAxis = vec3.normalize([], vec3.cross([], zAxis, [0, 1, 0])); - const yAxis = vec3.cross([], xAxis, zAxis); - - const aabbOrientation = [ - xAxis[0], xAxis[1], xAxis[2], 0, - yAxis[0], yAxis[1], yAxis[2], 0, - zAxis[0], zAxis[1], zAxis[2], 0, - 0, 0, 0, 1 - ]; - - const ecefCoords = [ - origin, - - latLngToECEF(coord0.lat, coord0.lng), - latLngToECEF(coord1.lat, coord0.lng), - latLngToECEF(coord1.lat, coord1.lng), - latLngToECEF(coord0.lat, coord1.lng), - - latLngToECEF(midLat, coord0.lng), - latLngToECEF(midLat, coord1.lng), - latLngToECEF(coord0.lat, midLng), - latLngToECEF(coord1.lat, midLng), - ]; - - let aabb = Aabb.fromPoints(ecefCoords.map(p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)])); - - const center = vec3.transformMat4([], aabb.center, aabbOrientation); - - if (vec3.squaredLength(center) === 0) { - vec3.set(center, 0, 0, 1); - } - - vec3.normalize(center, center); - vec3.scale(center, center, GLOBE_RADIUS); - tr.center = ecefToLatLng(center); - - const worldToCamera = tr.getWorldToCameraMatrix(); - const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); - - aabb = Aabb.applyTransform(aabb, mat4.multiply([], worldToCamera, aabbOrientation)); - - vec3.transformMat4(center, center, worldToCamera); - - const aabbHalfExtentZ = (aabb.max[2] - aabb.min[2]) * 0.5; - const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); - - const offsetZ = vec3.scale([], [0, 0, 1], aabbHalfExtentZ); - const aabbClosestPoint = vec3.add(offsetZ, center, offsetZ); - const offsetDistance = frustumDistance + (tr.pitch === 0 ? - 0 : - vec3.distance(center, aabbClosestPoint)); - - const globeCenter = tr.globeCenterInViewSpace; - const normal = vec3.sub([], center, [globeCenter[0], globeCenter[1], globeCenter[2]]); - vec3.normalize(normal, normal); - vec3.scale(normal, normal, offsetDistance); - - const cameraPosition = vec3.add([], center, normal); + cameraForBounds(bounds: LngLatBoundsLike, options?: CameraOptions): ?EasingOptions { + bounds = LngLatBounds.convert(bounds); + const bearing = (options && options.bearing) || 0; + const pitch = (options && options.pitch) || 0; + const lnglat0 = bounds.getNorthWest(); + const lnglat1 = bounds.getSouthEast(); + return this._cameraForBounds(this.transform, lnglat0, lnglat1, bearing, pitch, options); + } + + _extendCameraOptions(options?: CameraOptions): FullCameraOptions { + const defaultPadding = { + top: 0, + bottom: 0, + right: 0, + left: 0 + }; + options = extend({ + padding: defaultPadding, + offset: [0, 0], + maxZoom: this.transform.maxZoom + }, options); + + if (typeof options.padding === 'number') { + const p = options.padding; + options.padding = { + top: p, + bottom: p, + right: p, + left: p + }; + } + options.padding = extend(defaultPadding, options.padding); + return options; + } + + _minimumAABBFrustumDistance(tr: Transform, aabb: Aabb): number { + const aabbW = aabb.max[0] - aabb.min[0]; + const aabbH = aabb.max[1] - aabb.min[1]; + const aabbAspectRatio = aabbW / aabbH; + const selectXAxis = aabbAspectRatio > tr.aspect; + + const minimumDistance = selectXAxis ? + aabbW / (2 * Math.tan(tr.fovX * 0.5) * tr.aspect) : + aabbH / (2 * Math.tan(tr.fovY * 0.5) * tr.aspect); + + return minimumDistance; + } + + _cameraForBoundsOnGlobe(transform: Transform, p0: LngLatLike, p1: LngLatLike, bearing: number, pitch: number, options?: CameraOptions): ?EasingOptions { + const tr = transform.clone(); + const eOptions = this._extendCameraOptions(options); + + tr.bearing = bearing; + tr.pitch = pitch; + + const coord0 = LngLat.convert(p0); + const coord1 = LngLat.convert(p1); + + const midLat = (coord0.lat + coord1.lat) * 0.5; + const midLng = (coord0.lng + coord1.lng) * 0.5; + + const origin = latLngToECEF(midLat, midLng); + + const zAxis = vec3.normalize([], origin); + const xAxis = vec3.normalize([], vec3.cross([], zAxis, [0, 1, 0])); + const yAxis = vec3.cross([], xAxis, zAxis); + + const aabbOrientation = [ + xAxis[0], xAxis[1], xAxis[2], 0, + yAxis[0], yAxis[1], yAxis[2], 0, + zAxis[0], zAxis[1], zAxis[2], 0, + 0, 0, 0, 1 + ]; + + const ecefCoords = [ + origin, + + latLngToECEF(coord0.lat, coord0.lng), + latLngToECEF(coord1.lat, coord0.lng), + latLngToECEF(coord1.lat, coord1.lng), + latLngToECEF(coord0.lat, coord1.lng), + + latLngToECEF(midLat, coord0.lng), + latLngToECEF(midLat, coord1.lng), + latLngToECEF(coord0.lat, midLng), + latLngToECEF(coord1.lat, midLng), + ]; + + let aabb = Aabb.fromPoints(ecefCoords.map(p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)])); + + const center = vec3.transformMat4([], aabb.center, aabbOrientation); + + if (vec3.squaredLength(center) === 0) { + vec3.set(center, 0, 0, 1); + } + + vec3.normalize(center, center); + vec3.scale(center, center, GLOBE_RADIUS); + tr.center = ecefToLatLng(center); + + const worldToCamera = tr.getWorldToCameraMatrix(); + const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); + + aabb = Aabb.applyTransform(aabb, mat4.multiply([], worldToCamera, aabbOrientation)); + + vec3.transformMat4(center, center, worldToCamera); + + const aabbHalfExtentZ = (aabb.max[2] - aabb.min[2]) * 0.5; + const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); + + const offsetZ = vec3.scale([], [0, 0, 1], aabbHalfExtentZ); + const aabbClosestPoint = vec3.add(offsetZ, center, offsetZ); + const offsetDistance = frustumDistance + (tr.pitch === 0 ? 0 : vec3.distance(center, aabbClosestPoint)); - vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); + const globeCenter = tr.globeCenterInViewSpace; + const normal = vec3.sub([], center, [globeCenter[0], globeCenter[1], globeCenter[2]]); + vec3.normalize(normal, normal); + vec3.scale(normal, normal, offsetDistance); - const meterPerECEF = earthRadius / GLOBE_RADIUS; - const altitudeECEF = vec3.length(cameraPosition); - const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius; - const mercatorZ = mercatorZfromAltitude(Math.max(altitudeMeter, Number.EPSILON), 0); - const zoom = Math.min(tr.zoomFromMercatorZAdjusted(mercatorZ), eOptions.maxZoom); + const cameraPosition = vec3.add([], center, normal); - const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; - if (zoom > halfZoomTransition) { - tr.setProjection({name: 'mercator'}); - tr.zoom = zoom; - return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); - } + vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); - return {center: tr.center, zoom, bearing, pitch}; - } + const meterPerECEF = earthRadius / GLOBE_RADIUS; + const altitudeECEF = vec3.length(cameraPosition); + const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius; + const mercatorZ = mercatorZfromAltitude(Math.max(altitudeMeter, Number.EPSILON), 0); - /** @section {Querying features} */ + const zoom = Math.min(tr.zoomFromMercatorZAdjusted(mercatorZ), eOptions.maxZoom); - /** + const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; + if (zoom > halfZoomTransition) { + tr.setProjection({name: 'mercator'}); + tr.zoom = zoom; + return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); + } + + return {center: tr.center, zoom, bearing, pitch}; + } + + /** @section {Querying features} */ + + /** * Queries the currently loaded data for elevation at a geographical location. The elevation is returned in `meters` relative to mean sea-level. * Returns `null` if `terrain` is disabled or if terrain data for the location hasn't been loaded yet. * @@ -818,21 +762,14 @@ class Camera * const elevation = map.queryTerrainElevation(coordinate); * @see [Example: Query terrain elevation](https://docs.mapbox.com/mapbox-gl-js/example/query-terrain-elevation/) */ - queryTerrainElevation( - lnglat: LngLatLike, - options: ?ElevationQueryOptions, - ): number | null { - const elevation = this.transform.elevation; - if (elevation) { - options = extend({}, {exaggerated: true}, options); - return elevation.getAtPoint( - MercatorCoordinate.fromLngLat(lnglat), - null, - options.exaggerated, - ); - } - return null; - } + queryTerrainElevation(lnglat: LngLatLike, options: ?ElevationQueryOptions): number | null { + const elevation = this.transform.elevation; + if (elevation) { + options = extend({}, {exaggerated: true}, options); + return elevation.getAtPoint(MercatorCoordinate.fromLngLat(lnglat), null, options.exaggerated); + } + return null; + } /** * Calculate the center of these two points in the viewport and use @@ -858,141 +795,110 @@ class Camera * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ - _cameraForBounds( - transform: Transform, - p0: LngLatLike, - p1: LngLatLike, - bearing: number, - pitch: number, - options?: CameraOptions, - ): ?EasingOptions { - if (transform.projection.name === 'globe') { - return this._cameraForBoundsOnGlobe( - transform, - p0, - p1, - bearing, - pitch, - options, - ); - } - - const tr = transform.clone(); - const eOptions = this._extendCameraOptions(options); - const edgePadding = tr.padding; - - tr.bearing = bearing; - tr.pitch = pitch; - - const coord0 = LngLat.convert(p0); - const coord1 = LngLat.convert(p1); - const coord2 = new LngLat(coord0.lng, coord1.lat); - const coord3 = new LngLat(coord1.lng, coord0.lat); - - const p0world = tr.project(coord0); - const p1world = tr.project(coord1); - - const z0 = this.queryTerrainElevation(coord0); - const z1 = this.queryTerrainElevation(coord1); - const z2 = this.queryTerrainElevation(coord2); - const z3 = this.queryTerrainElevation(coord3); - - const worldCoords = [ - [p0world.x, p0world.y, Math.min(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], - [p1world.x, p1world.y, Math.max(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], - ]; - - let aabb = Aabb.fromPoints(worldCoords); - - const worldToCamera = tr.getWorldToCameraMatrix(); - const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); - - aabb = Aabb.applyTransform(aabb, worldToCamera); - - const size = vec3.sub([], aabb.max, aabb.min); - - const screenPadL = edgePadding.left || 0; - const screenPadR = edgePadding.right || 0; - const screenPadB = edgePadding.bottom || 0; - const screenPadT = edgePadding.top || 0; - - const {left: padL, right: padR, top: padT, bottom: padB} = eOptions.padding; - - const halfScreenPadX = (screenPadL + screenPadR) * 0.5; - const halfScreenPadY = (screenPadT + screenPadB) * 0.5; - - const scaleX = (tr.width - (screenPadL + screenPadR + padL + padR)) / size[0]; - const scaleY = (tr.height - (screenPadB + screenPadT + padB + padT)) / size[1]; - - const zoomRef = Math.min( - tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), - eOptions.maxZoom, - ); - - const scaleRatio = tr.scale / tr.zoomScale(zoomRef); - - aabb = new Aabb( - [ - aabb.min[0] - (padL + halfScreenPadX) * scaleRatio, - aabb.min[1] - (padB + halfScreenPadY) * scaleRatio, - aabb.min[2], - ], - [ - aabb.max[0] + (padR + halfScreenPadX) * scaleRatio, - aabb.max[1] + (padT + halfScreenPadY) * scaleRatio, - aabb.max[2], - ], - ); - - const aabbHalfExtentZ = size[2] * 0.5; - const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); - - const normalZ = [0, 0, 1, 0]; - - vec4.transformMat4(normalZ, normalZ, worldToCamera); - vec4.normalize(normalZ, normalZ); - - const offset = vec3.scale([], normalZ, frustumDistance + aabbHalfExtentZ); - const cameraPosition = vec3.add([], aabb.center, offset); - - const centerOffset = typeof eOptions.offset.x === 'number' && - typeof eOptions.offset.y === 'number' ? - new Point(eOptions.offset.x, eOptions.offset.y) : - Point.convert(eOptions.offset); - - const rotatedOffset = centerOffset.rotate(-degToRad(bearing)); - - aabb.center[0] -= rotatedOffset.x * scaleRatio; - aabb.center[1] += rotatedOffset.y * scaleRatio; + _cameraForBounds(transform: Transform, p0: LngLatLike, p1: LngLatLike, bearing: number, pitch: number, options?: CameraOptions): ?EasingOptions { + if (transform.projection.name === 'globe') { + return this._cameraForBoundsOnGlobe(transform, p0, p1, bearing, pitch, options); + } - vec3.transformMat4(aabb.center, aabb.center, cameraToWorld); - vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); + const tr = transform.clone(); + const eOptions = this._extendCameraOptions(options); + const edgePadding = tr.padding; - const mercator = [ - aabb.center[0], - aabb.center[1], - cameraPosition[2] * tr.pixelsPerMeter, - ]; - vec3.scale(mercator, mercator, 1.0 / tr.worldSize); + tr.bearing = bearing; + tr.pitch = pitch; - const lng = lngFromMercatorX(mercator[0]); - const lat = latFromMercatorY(mercator[1]); + const coord0 = LngLat.convert(p0); + const coord1 = LngLat.convert(p1); + const coord2 = new LngLat(coord0.lng, coord1.lat); + const coord3 = new LngLat(coord1.lng, coord0.lat); - const zoom = Math.min(tr._zoomFromMercatorZ(mercator[2]), eOptions.maxZoom); - const center = new LngLat(lng, lat); + const p0world = tr.project(coord0); + const p1world = tr.project(coord1); - const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; + const z0 = this.queryTerrainElevation(coord0); + const z1 = this.queryTerrainElevation(coord1); + const z2 = this.queryTerrainElevation(coord2); + const z3 = this.queryTerrainElevation(coord3); - if (tr.mercatorFromTransition && zoom < halfZoomTransition) { - tr.setProjection({name: 'globe'}); - tr.zoom = zoom; - return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); - } + const worldCoords = [ + [p0world.x, p0world.y, Math.min(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], + [p1world.x, p1world.y, Math.max(z0 || 0, z1 || 0, z2 || 0, z3 || 0)] + ]; - return {center, zoom, bearing, pitch}; - } + let aabb = Aabb.fromPoints(worldCoords); - /** + const worldToCamera = tr.getWorldToCameraMatrix(); + const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); + + aabb = Aabb.applyTransform(aabb, worldToCamera); + + const size = vec3.sub([], aabb.max, aabb.min); + + const screenPadL = edgePadding.left || 0; + const screenPadR = edgePadding.right || 0; + const screenPadB = edgePadding.bottom || 0; + const screenPadT = edgePadding.top || 0; + + const {left: padL, right: padR, top: padT, bottom: padB} = eOptions.padding; + + const halfScreenPadX = (screenPadL + screenPadR) * 0.5; + const halfScreenPadY = (screenPadT + screenPadB) * 0.5; + + const scaleX = (tr.width - (screenPadL + screenPadR + padL + padR)) / size[0]; + const scaleY = (tr.height - (screenPadB + screenPadT + padB + padT)) / size[1]; + + const zoomRef = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), eOptions.maxZoom); + + const scaleRatio = tr.scale / tr.zoomScale(zoomRef); + + aabb = new Aabb( + [aabb.min[0] - (padL + halfScreenPadX) * scaleRatio, aabb.min[1] - (padB + halfScreenPadY) * scaleRatio, aabb.min[2]], + [aabb.max[0] + (padR + halfScreenPadX) * scaleRatio, aabb.max[1] + (padT + halfScreenPadY) * scaleRatio, aabb.max[2]]); + + const aabbHalfExtentZ = size[2] * 0.5; + const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); + + const normalZ = [0, 0, 1, 0]; + + vec4.transformMat4(normalZ, normalZ, worldToCamera); + vec4.normalize(normalZ, normalZ); + + const offset = vec3.scale([], normalZ, frustumDistance + aabbHalfExtentZ); + const cameraPosition = vec3.add([], aabb.center, offset); + + const centerOffset = (typeof eOptions.offset.x === 'number' && typeof eOptions.offset.y === 'number') ? + new Point(eOptions.offset.x, eOptions.offset.y) : + Point.convert(eOptions.offset); + + const rotatedOffset = centerOffset.rotate(-degToRad(bearing)); + + aabb.center[0] -= rotatedOffset.x * scaleRatio; + aabb.center[1] += rotatedOffset.y * scaleRatio; + + vec3.transformMat4(aabb.center, aabb.center, cameraToWorld); + vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); + + const mercator = [aabb.center[0], aabb.center[1], cameraPosition[2] * tr.pixelsPerMeter]; + vec3.scale(mercator, mercator, 1.0 / tr.worldSize); + + const lng = lngFromMercatorX(mercator[0]); + const lat = latFromMercatorY(mercator[1]); + + const zoom = Math.min(tr._zoomFromMercatorZ(mercator[2]), eOptions.maxZoom); + const center = new LngLat(lng, lat); + + const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; + + if (tr.mercatorFromTransition && zoom < halfZoomTransition) { + tr.setProjection({name: 'globe'}); + tr.zoom = zoom; + return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); + } + + return {center, zoom, bearing, pitch}; + } + + /** * Pans and zooms the map to contain its visible area within the specified geographical bounds. * If a padding is set on the map, the bounds are fit to the inset. * @@ -1020,16 +926,12 @@ class Camera * }); * @see [Example: Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/) */ - fitBounds( - bounds: LngLatBoundsLike, - options?: EasingOptions, - eventData?: Object, - ): this { - const cameraPlacement = this.cameraForBounds(bounds, options); - return this._fitInternal(cameraPlacement, options, eventData); - } - - /** + fitBounds(bounds: LngLatBoundsLike, options?: EasingOptions, eventData?: Object): this { + const cameraPlacement = this.cameraForBounds(bounds, options); + return this._fitInternal(cameraPlacement, options, eventData); + } + + /** * Pans, rotates and zooms the map to to fit the box made by points p0 and p1 * once the map is rotated to the specified bearing. To zoom without rotating, * pass in the current map bearing. @@ -1060,77 +962,51 @@ class Camera * }); * @see Used by {@link BoxZoomHandler} */ - fitScreenCoordinates( - p0: PointLike, - p1: PointLike, - bearing: number, - options?: EasingOptions, - eventData?: Object, - ): this { - const screen0 = Point.convert(p0); - const screen1 = Point.convert(p1); - - const min = new Point( - Math.min(screen0.x, screen1.x), - Math.min(screen0.y, screen1.y), - ); - const max = new Point( - Math.max(screen0.x, screen1.x), - Math.max(screen0.y, screen1.y), - ); - - if ( - this.transform.projection.name === 'mercator' && - this.transform.anyCornerOffEdge(screen0, screen1) - ) { - return this; - } - - const lnglat0 = this.transform.pointLocation3D(min); - const lnglat1 = this.transform.pointLocation3D(max); - const lnglat2 = this.transform.pointLocation3D(new Point(min.x, max.y)); - const lnglat3 = this.transform.pointLocation3D(new Point(max.x, min.y)); - - const p0coord = [ - Math.min(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), - Math.min(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), - ]; - const p1coord = [ - Math.max(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), - Math.max(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), - ]; - - const pitch = options && options.pitch ? options.pitch : this.getPitch(); - - const cameraPlacement = this._cameraForBounds( - this.transform, - p0coord, - p1coord, - bearing, - pitch, - options, - ); - return this._fitInternal(cameraPlacement, options, eventData); - } - - _fitInternal( - calculatedOptions?: ?EasingOptions, - options?: EasingOptions, - eventData?: Object, - ): this { - // cameraForBounds warns + returns undefined if unable to fit: - if (!calculatedOptions) return this; - - options = extend(calculatedOptions, options); - // Explicitly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly. - delete options.padding; - - return options.linear ? - this.easeTo(options, eventData) : - this.flyTo(options, eventData); - } - - /** + fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: EasingOptions, eventData?: Object): this { + const screen0 = Point.convert(p0); + const screen1 = Point.convert(p1); + + const min = new Point(Math.min(screen0.x, screen1.x), Math.min(screen0.y, screen1.y)); + const max = new Point(Math.max(screen0.x, screen1.x), Math.max(screen0.y, screen1.y)); + + if (this.transform.projection.name === 'mercator' && this.transform.anyCornerOffEdge(screen0, screen1)) { + return this; + } + + const lnglat0 = this.transform.pointLocation3D(min); + const lnglat1 = this.transform.pointLocation3D(max); + const lnglat2 = this.transform.pointLocation3D(new Point(min.x, max.y)); + const lnglat3 = this.transform.pointLocation3D(new Point(max.x, min.y)); + + const p0coord = [ + Math.min(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), + Math.min(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), + ]; + const p1coord = [ + Math.max(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), + Math.max(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), + ]; + + const pitch = options && options.pitch ? options.pitch : this.getPitch(); + + const cameraPlacement = this._cameraForBounds(this.transform, p0coord, p1coord, bearing, pitch, options); + return this._fitInternal(cameraPlacement, options, eventData); + } + + _fitInternal(calculatedOptions?: ?EasingOptions, options?: EasingOptions, eventData?: Object): this { + // cameraForBounds warns + returns undefined if unable to fit: + if (!calculatedOptions) return this; + + options = extend(calculatedOptions, options); + // Explicitly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly. + delete options.padding; + + return options.linear ? + this.easeTo(options, eventData) : + this.flyTo(options, eventData); + } + + /** * Changes any combination of center, zoom, bearing, and pitch, without * an animated transition. The map will retain its current values for any * details not specified in `options`. @@ -1162,72 +1038,67 @@ class Camera * @see [Example: Jump to a series of locations](https://docs.mapbox.com/mapbox-gl-js/example/jump-to/) * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ - jumpTo( - options: & CameraOptions - & { preloadOnly?: $PropertyType }, - eventData?: Object, - ): this { - this.stop(); - - const tr = options.preloadOnly ? this.transform.clone() : this.transform; - let zoomChanged = false, - bearingChanged = false, - pitchChanged = false; - - if ('zoom' in options && tr.zoom !== +options.zoom) { - zoomChanged = true; - tr.zoom = +options.zoom; - } - - if (options.center !== undefined) { - tr.center = LngLat.convert(options.center); - } - - if ('bearing' in options && tr.bearing !== +options.bearing) { - bearingChanged = true; - tr.bearing = +options.bearing; - } - - if ('pitch' in options && tr.pitch !== +options.pitch) { - pitchChanged = true; - tr.pitch = +options.pitch; - } - - if (options.padding != null && !tr.isPaddingEqual(options.padding)) { - tr.padding = options.padding; - } - - if (options.preloadOnly) { - this._preloadTiles(tr); - return this; - } - - this.fire(new Event('movestart', eventData)).fire( - new Event('move', eventData), - ); - - if (zoomChanged) { - this.fire(new Event('zoomstart', eventData)).fire( - new Event('zoom', eventData), - ).fire(new Event('zoomend', eventData)); - } - - if (bearingChanged) { - this.fire(new Event('rotatestart', eventData)).fire( - new Event('rotate', eventData), - ).fire(new Event('rotateend', eventData)); - } - - if (pitchChanged) { - this.fire(new Event('pitchstart', eventData)).fire( - new Event('pitch', eventData), - ).fire(new Event('pitchend', eventData)); - } - - return this.fire(new Event('moveend', eventData)); - } - - /** + jumpTo(options: CameraOptions & {preloadOnly?: $PropertyType}, eventData?: Object): this { + this.stop(); + + const tr = options.preloadOnly ? this.transform.clone() : this.transform; + let zoomChanged = false, + bearingChanged = false, + pitchChanged = false; + + if ('zoom' in options && tr.zoom !== +options.zoom) { + zoomChanged = true; + tr.zoom = +options.zoom; + } + + if (options.center !== undefined) { + tr.center = LngLat.convert(options.center); + } + + if ('bearing' in options && tr.bearing !== +options.bearing) { + bearingChanged = true; + tr.bearing = +options.bearing; + } + + if ('pitch' in options && tr.pitch !== +options.pitch) { + pitchChanged = true; + tr.pitch = +options.pitch; + } + + if (options.padding != null && !tr.isPaddingEqual(options.padding)) { + tr.padding = options.padding; + } + + if (options.preloadOnly) { + this._preloadTiles(tr); + return this; + } + + this.fire(new Event('movestart', eventData)) + .fire(new Event('move', eventData)); + + if (zoomChanged) { + this.fire(new Event('zoomstart', eventData)) + .fire(new Event('zoom', eventData)) + .fire(new Event('zoomend', eventData)); + } + + if (bearingChanged) { + this.fire(new Event('rotatestart', eventData)) + .fire(new Event('rotate', eventData)) + .fire(new Event('rotateend', eventData)); + } + + if (pitchChanged) { + this.fire(new Event('pitchstart', eventData)) + .fire(new Event('pitch', eventData)) + .fire(new Event('pitchend', eventData)); + } + + return this.fire(new Event('moveend', eventData)); + } + + /** * Returns position and orientation of the camera entity. * * This method is not supported for projections other than mercator. @@ -1245,14 +1116,14 @@ class Camera * * map.setFreeCameraOptions(camera); */ - getFreeCameraOptions(): FreeCameraOptions { - if (!this.transform.projection.supportsFreeCamera) { - warnOnce(freeCameraNotSupportedWarning); - } - return this.transform.getFreeCameraOptions(); - } - - /** + getFreeCameraOptions(): FreeCameraOptions { + if (!this.transform.projection.supportsFreeCamera) { + warnOnce(freeCameraNotSupportedWarning); + } + return this.transform.getFreeCameraOptions(); + } + + /** * `FreeCameraOptions` provides more direct access to the underlying camera entity. * For backwards compatibility the state set using this API must be representable with * `CameraOptions` as well. Parameters are clamped into a valid range or discarded as invalid @@ -1287,53 +1158,52 @@ class Camera * * map.setFreeCameraOptions(camera); */ - setFreeCameraOptions(options: FreeCameraOptions, eventData?: Object): this { - const tr = this.transform; + setFreeCameraOptions(options: FreeCameraOptions, eventData?: Object): this { + const tr = this.transform; - if (!tr.projection.supportsFreeCamera) { - warnOnce(freeCameraNotSupportedWarning); - return this; - } + if (!tr.projection.supportsFreeCamera) { + warnOnce(freeCameraNotSupportedWarning); + return this; + } - this.stop(); + this.stop(); - const prevZoom = tr.zoom; - const prevPitch = tr.pitch; - const prevBearing = tr.bearing; + const prevZoom = tr.zoom; + const prevPitch = tr.pitch; + const prevBearing = tr.bearing; - tr.setFreeCameraOptions(options); + tr.setFreeCameraOptions(options); - const zoomChanged = prevZoom !== tr.zoom; - const pitchChanged = prevPitch !== tr.pitch; - const bearingChanged = prevBearing !== tr.bearing; + const zoomChanged = prevZoom !== tr.zoom; + const pitchChanged = prevPitch !== tr.pitch; + const bearingChanged = prevBearing !== tr.bearing; - this.fire(new Event('movestart', eventData)).fire( - new Event('move', eventData), - ); + this.fire(new Event('movestart', eventData)) + .fire(new Event('move', eventData)); - if (zoomChanged) { - this.fire(new Event('zoomstart', eventData)).fire( - new Event('zoom', eventData), - ).fire(new Event('zoomend', eventData)); - } + if (zoomChanged) { + this.fire(new Event('zoomstart', eventData)) + .fire(new Event('zoom', eventData)) + .fire(new Event('zoomend', eventData)); + } - if (bearingChanged) { - this.fire(new Event('rotatestart', eventData)).fire( - new Event('rotate', eventData), - ).fire(new Event('rotateend', eventData)); - } + if (bearingChanged) { + this.fire(new Event('rotatestart', eventData)) + .fire(new Event('rotate', eventData)) + .fire(new Event('rotateend', eventData)); + } - if (pitchChanged) { - this.fire(new Event('pitchstart', eventData)).fire( - new Event('pitch', eventData), - ).fire(new Event('pitchend', eventData)); - } + if (pitchChanged) { + this.fire(new Event('pitchstart', eventData)) + .fire(new Event('pitch', eventData)) + .fire(new Event('pitchend', eventData)); + } - this.fire(new Event('moveend', eventData)); - return this; - } + this.fire(new Event('moveend', eventData)); + return this; + } - /** + /** * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition * between old and new values. The map will retain its current values for any * details not specified in `options`. @@ -1374,209 +1244,199 @@ class Camera * }); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - easeTo(options: EasingOptions & { easeId?: string }, eventData?: Object): this { - this._stop(false, options.easeId); - - options = extend({ - offset: [0, 0], - duration: 500, - easing: defaultEasing - }, options); - - if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0; - - const tr = this.transform, - startZoom = this.getZoom(), - startBearing = this.getBearing(), - startPitch = this.getPitch(), - startPadding = this.getPadding(), - zoom = 'zoom' in options ? +options.zoom : startZoom, - bearing = 'bearing' in options ? - this._normalizeBearing(options.bearing, startBearing) : - startBearing, - pitch = 'pitch' in options ? +options.pitch : startPitch, - padding = 'padding' in options ? options.padding : tr.padding; - - const offsetAsPoint = Point.convert(options.offset); - - let pointAtOffset; - let from; - let delta; - - if (tr.projection.name === 'globe') { - // Pixel coordinates will be applied directly to translate the globe - const centerCoord = MercatorCoordinate.fromLngLat(tr.center); - - const rotatedOffset = offsetAsPoint.rotate(-tr.angle); - centerCoord.x += rotatedOffset.x / tr.worldSize; - centerCoord.y += rotatedOffset.y / tr.worldSize; - - const locationAtOffset = centerCoord.toLngLat(); - const center = LngLat.convert(options.center || locationAtOffset); - this._normalizeCenter(center); - - pointAtOffset = tr.centerPoint.add(rotatedOffset); - from = new Point(centerCoord.x, centerCoord.y).mult(tr.worldSize); - delta = new Point(mercatorXfromLng(center.lng), mercatorYfromLat(center.lat)).mult(tr.worldSize).sub(from); - } else { - pointAtOffset = tr.centerPoint.add(offsetAsPoint); - const locationAtOffset = tr.pointLocation(pointAtOffset); - const center = LngLat.convert(options.center || locationAtOffset); - this._normalizeCenter(center); - - from = tr.project(locationAtOffset); - delta = tr.project(center).sub(from); - } - const finalScale = tr.zoomScale(zoom - startZoom); - - let around, aroundPoint; - - if (options.around) { - around = LngLat.convert(options.around); - aroundPoint = tr.locationPoint(around); - } - - const zoomChanged = this._zooming || zoom !== startZoom; - const bearingChanged = this._rotating || startBearing !== bearing; - const pitchChanged = this._pitching || pitch !== startPitch; - const paddingChanged = !tr.isPaddingEqual(padding); - - const frame = (tr => k => { - if (zoomChanged) { - tr.zoom = interpolate(startZoom, zoom, k); - } - if (bearingChanged) { - tr.bearing = interpolate(startBearing, bearing, k); - } - if (pitchChanged) { - tr.pitch = interpolate(startPitch, pitch, k); - } - if (paddingChanged) { - tr.interpolatePadding(startPadding, padding, k); - // When padding is being applied, Transform#centerPoint is changing continuously, - // thus we need to recalculate offsetPoint every fra,e - pointAtOffset = tr.centerPoint.add(offsetAsPoint); - } - - if (around) { - tr.setLocationAtPoint(around, aroundPoint); - } else { - const scale = tr.zoomScale(tr.zoom - startZoom); - const base = zoom > startZoom ? - Math.min(2, finalScale) : - Math.max(0.5, finalScale); - const speedup = Math.pow(base, 1 - k); - const newCenter = tr.unproject( - from.add(delta.mult(k * speedup)).mult(scale), - ); - tr.setLocationAtPoint( - tr.renderWorldCopies ? newCenter.wrap() : newCenter, - pointAtOffset, - ); - } - - if (!options.preloadOnly) { - this._fireMoveEvents(eventData); - } - - return tr; - }); - - if (options.preloadOnly) { - const predictedTransforms = this._emulate(frame, options.duration, tr); - this._preloadTiles(predictedTransforms); - return this; - } - - const currently = { - moving: this._moving, - zooming: this._zooming, - rotating: this._rotating, - pitching: this._pitching, - }; - - this._zooming = zoomChanged; - this._rotating = bearingChanged; - this._pitching = pitchChanged; - this._padding = paddingChanged; - - this._easeId = options.easeId; - this._prepareEase(eventData, options.noMoveStart, currently); - - this._ease( - frame(tr), - (interruptingEaseId?: string) => { - tr.recenterOnTerrain(); - this._afterEase(eventData, interruptingEaseId); - }, - options, - ); - - return this; - } - - _prepareEase(eventData?: Object, noMoveStart: boolean, currently: Object = {}) { - this._moving = true; - this.transform.cameraElevationReference = "sea"; - - if (!noMoveStart && !currently.moving) { - this.fire(new Event('movestart', eventData)); - } - if (this._zooming && !currently.zooming) { - this.fire(new Event('zoomstart', eventData)); - } - if (this._rotating && !currently.rotating) { - this.fire(new Event('rotatestart', eventData)); - } - if (this._pitching && !currently.pitching) { - this.fire(new Event('pitchstart', eventData)); - } - } - - _fireMoveEvents(eventData?: Object) { - this.fire(new Event('move', eventData)); - if (this._zooming) { - this.fire(new Event('zoom', eventData)); - } - if (this._rotating) { - this.fire(new Event('rotate', eventData)); - } - if (this._pitching) { - this.fire(new Event('pitch', eventData)); - } - } - - _afterEase(eventData?: Object, easeId?: string) { - // if this easing is being stopped to start another easing with - // the same id then don't fire any events to avoid extra start/stop events - if (this._easeId && easeId && this._easeId === easeId) { - return; - } - this._easeId = undefined; - this.transform.cameraElevationReference = "ground"; - - const wasZooming = this._zooming; - const wasRotating = this._rotating; - const wasPitching = this._pitching; - this._moving = false; - this._zooming = false; - this._rotating = false; - this._pitching = false; - this._padding = false; - - if (wasZooming) { - this.fire(new Event('zoomend', eventData)); - } - if (wasRotating) { - this.fire(new Event('rotateend', eventData)); - } - if (wasPitching) { - this.fire(new Event('pitchend', eventData)); - } - this.fire(new Event('moveend', eventData)); - } - - /** + easeTo(options: EasingOptions & {easeId?: string}, eventData?: Object): this { + this._stop(false, options.easeId); + + options = extend({ + offset: [0, 0], + duration: 500, + easing: defaultEasing + }, options); + + if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0; + + const tr = this.transform, + startZoom = this.getZoom(), + startBearing = this.getBearing(), + startPitch = this.getPitch(), + startPadding = this.getPadding(), + + zoom = 'zoom' in options ? +options.zoom : startZoom, + bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing, + pitch = 'pitch' in options ? +options.pitch : startPitch, + padding = 'padding' in options ? options.padding : tr.padding; + + const offsetAsPoint = Point.convert(options.offset); + + let pointAtOffset; + let from; + let delta; + + if (tr.projection.name === 'globe') { + // Pixel coordinates will be applied directly to translate the globe + const centerCoord = MercatorCoordinate.fromLngLat(tr.center); + + const rotatedOffset = offsetAsPoint.rotate(-tr.angle); + centerCoord.x += rotatedOffset.x / tr.worldSize; + centerCoord.y += rotatedOffset.y / tr.worldSize; + + const locationAtOffset = centerCoord.toLngLat(); + const center = LngLat.convert(options.center || locationAtOffset); + this._normalizeCenter(center); + + pointAtOffset = tr.centerPoint.add(rotatedOffset); + from = new Point(centerCoord.x, centerCoord.y).mult(tr.worldSize); + delta = new Point(mercatorXfromLng(center.lng), mercatorYfromLat(center.lat)).mult(tr.worldSize).sub(from); + } else { + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + const locationAtOffset = tr.pointLocation(pointAtOffset); + const center = LngLat.convert(options.center || locationAtOffset); + this._normalizeCenter(center); + + from = tr.project(locationAtOffset); + delta = tr.project(center).sub(from); + } + const finalScale = tr.zoomScale(zoom - startZoom); + + let around, aroundPoint; + + if (options.around) { + around = LngLat.convert(options.around); + aroundPoint = tr.locationPoint(around); + } + + const zoomChanged = this._zooming || (zoom !== startZoom); + const bearingChanged = this._rotating || (startBearing !== bearing); + const pitchChanged = this._pitching || (pitch !== startPitch); + const paddingChanged = !tr.isPaddingEqual(padding); + + const frame = (tr) => (k) => { + if (zoomChanged) { + tr.zoom = interpolate(startZoom, zoom, k); + } + if (bearingChanged) { + tr.bearing = interpolate(startBearing, bearing, k); + } + if (pitchChanged) { + tr.pitch = interpolate(startPitch, pitch, k); + } + if (paddingChanged) { + tr.interpolatePadding(startPadding, padding, k); + // When padding is being applied, Transform#centerPoint is changing continuously, + // thus we need to recalculate offsetPoint every fra,e + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + } + + if (around) { + tr.setLocationAtPoint(around, aroundPoint); + } else { + const scale = tr.zoomScale(tr.zoom - startZoom); + const base = zoom > startZoom ? + Math.min(2, finalScale) : + Math.max(0.5, finalScale); + const speedup = Math.pow(base, 1 - k); + const newCenter = tr.unproject(from.add(delta.mult(k * speedup)).mult(scale)); + tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); + } + + if (!options.preloadOnly) { + this._fireMoveEvents(eventData); + } + + return tr; + }; + + if (options.preloadOnly) { + const predictedTransforms = this._emulate(frame, options.duration, tr); + this._preloadTiles(predictedTransforms); + return this; + } + + const currently = { + moving: this._moving, + zooming: this._zooming, + rotating: this._rotating, + pitching: this._pitching + }; + + this._zooming = zoomChanged; + this._rotating = bearingChanged; + this._pitching = pitchChanged; + this._padding = paddingChanged; + + this._easeId = options.easeId; + this._prepareEase(eventData, options.noMoveStart, currently); + + this._ease(frame(tr), (interruptingEaseId?: string) => { + tr.recenterOnTerrain(); + this._afterEase(eventData, interruptingEaseId); + }, options); + + return this; + } + + _prepareEase(eventData?: Object, noMoveStart: boolean, currently: Object = {}) { + this._moving = true; + this.transform.cameraElevationReference = "sea"; + + if (!noMoveStart && !currently.moving) { + this.fire(new Event('movestart', eventData)); + } + if (this._zooming && !currently.zooming) { + this.fire(new Event('zoomstart', eventData)); + } + if (this._rotating && !currently.rotating) { + this.fire(new Event('rotatestart', eventData)); + } + if (this._pitching && !currently.pitching) { + this.fire(new Event('pitchstart', eventData)); + } + } + + _fireMoveEvents(eventData?: Object) { + this.fire(new Event('move', eventData)); + if (this._zooming) { + this.fire(new Event('zoom', eventData)); + } + if (this._rotating) { + this.fire(new Event('rotate', eventData)); + } + if (this._pitching) { + this.fire(new Event('pitch', eventData)); + } + } + + _afterEase(eventData?: Object, easeId?: string) { + // if this easing is being stopped to start another easing with + // the same id then don't fire any events to avoid extra start/stop events + if (this._easeId && easeId && this._easeId === easeId) { + return; + } + this._easeId = undefined; + this.transform.cameraElevationReference = "ground"; + + const wasZooming = this._zooming; + const wasRotating = this._rotating; + const wasPitching = this._pitching; + this._moving = false; + this._zooming = false; + this._rotating = false; + this._pitching = false; + this._padding = false; + + if (wasZooming) { + this.fire(new Event('zoomend', eventData)); + } + if (wasRotating) { + this.fire(new Event('rotateend', eventData)); + } + if (wasPitching) { + this.fire(new Event('pitchend', eventData)); + } + this.fire(new Event('moveend', eventData)); + } + + /** * Changes any combination of center, zoom, bearing, and pitch, animating the transition along a curve that * evokes flight. The animation seamlessly incorporates zooming and panning to help * the user maintain their bearings even after traversing a great distance. @@ -1635,218 +1495,185 @@ class Camera * @see [Example: Slowly fly to a location](https://www.mapbox.com/mapbox-gl-js/example/flyto-options/) * @see [Example: Fly to a location based on scroll position](https://www.mapbox.com/mapbox-gl-js/example/scroll-fly-to/) */ - flyTo(options: EasingOptions, eventData?: Object): this { - // Fall through to jumpTo if user has set prefers-reduced-motion - if (!options.essential && browser.prefersReducedMotion) { - const coercedOptions = pick( - options, - ['center', 'zoom', 'bearing', 'pitch', 'around'], - ); - return this.jumpTo(coercedOptions, eventData); - } - - // This method implements an “optimal path” animation, as detailed in: - // - // Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and panning.” INFOVIS - // ’03. pp. 15–22. . - // - // Where applicable, local variable documentation begins with the associated variable or - // function in van Wijk (2003). - - this.stop(); - - options = extend( - { - offset: [0, 0], - speed: 1.2, - curve: 1.42, - easing: defaultEasing, - }, - options, - ); - - const tr = this.transform, - startZoom = this.getZoom(), - startBearing = this.getBearing(), - startPitch = this.getPitch(), - startPadding = this.getPadding(); - - const zoom = 'zoom' in options ? - clamp(+options.zoom, tr.minZoom, tr.maxZoom) : - startZoom; - const bearing = 'bearing' in options ? - this._normalizeBearing(options.bearing, startBearing) : - startBearing; - const pitch = 'pitch' in options ? +options.pitch : startPitch; - const padding = 'padding' in options ? options.padding : tr.padding; - - const scale = tr.zoomScale(zoom - startZoom); - const offsetAsPoint = Point.convert(options.offset); - let pointAtOffset = tr.centerPoint.add(offsetAsPoint); - const locationAtOffset = tr.pointLocation(pointAtOffset); - const center = LngLat.convert(options.center || locationAtOffset); - this._normalizeCenter(center); - - const from = tr.project(locationAtOffset); - const delta = tr.project(center).sub(from); - - let rho = options.curve; - - // w₀: Initial visible span, measured in pixels at the initial scale. - const w0 = Math.max(tr.width, tr.height), - // w₁: Final visible span, measured in pixels with respect to the initial scale. - w1 = w0 / scale, - // Length of the flight path as projected onto the ground plane, measured in pixels from - // the world image origin at the initial scale. - u1 = delta.mag(); - - if ('minZoom' in options) { - const minZoom = clamp( - Math.min(options.minZoom, startZoom, zoom), - tr.minZoom, - tr.maxZoom, - ); - // wm: Maximum visible span, measured in pixels with respect to the initial - // scale. - const wMax = w0 / tr.zoomScale(minZoom - startZoom); - rho = Math.sqrt(wMax / u1 * 2); - } - - // ρ² - const rho2 = rho * rho; - - /** + flyTo(options: EasingOptions, eventData?: Object): this { + // Fall through to jumpTo if user has set prefers-reduced-motion + if (!options.essential && browser.prefersReducedMotion) { + const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'around']); + return this.jumpTo(coercedOptions, eventData); + } + + // This method implements an “optimal path” animation, as detailed in: + // + // Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and panning.” INFOVIS + // ’03. pp. 15–22. . + // + // Where applicable, local variable documentation begins with the associated variable or + // function in van Wijk (2003). + + this.stop(); + + options = extend({ + offset: [0, 0], + speed: 1.2, + curve: 1.42, + easing: defaultEasing + }, options); + + const tr = this.transform, + startZoom = this.getZoom(), + startBearing = this.getBearing(), + startPitch = this.getPitch(), + startPadding = this.getPadding(); + + const zoom = 'zoom' in options ? clamp(+options.zoom, tr.minZoom, tr.maxZoom) : startZoom; + const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing; + const pitch = 'pitch' in options ? +options.pitch : startPitch; + const padding = 'padding' in options ? options.padding : tr.padding; + + const scale = tr.zoomScale(zoom - startZoom); + const offsetAsPoint = Point.convert(options.offset); + let pointAtOffset = tr.centerPoint.add(offsetAsPoint); + const locationAtOffset = tr.pointLocation(pointAtOffset); + const center = LngLat.convert(options.center || locationAtOffset); + this._normalizeCenter(center); + + const from = tr.project(locationAtOffset); + const delta = tr.project(center).sub(from); + + let rho = options.curve; + + // w₀: Initial visible span, measured in pixels at the initial scale. + const w0 = Math.max(tr.width, tr.height), + // w₁: Final visible span, measured in pixels with respect to the initial scale. + w1 = w0 / scale, + // Length of the flight path as projected onto the ground plane, measured in pixels from + // the world image origin at the initial scale. + u1 = delta.mag(); + + if ('minZoom' in options) { + const minZoom = clamp(Math.min(options.minZoom, startZoom, zoom), tr.minZoom, tr.maxZoom); + // wm: Maximum visible span, measured in pixels with respect to the initial + // scale. + const wMax = w0 / tr.zoomScale(minZoom - startZoom); + rho = Math.sqrt(wMax / u1 * 2); + } + + // ρ² + const rho2 = rho * rho; + + /** * rᵢ: Returns the zoom-out factor at one end of the animation. * * @param i 0 for the ascent or 1 for the descent. * @private */ - function r(i) { - const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? - w1 : - w0) * rho2 * u1); - return Math.log(Math.sqrt(b * b + 1) - b); - } - - function sinh(n) { - return (Math.exp(n) - Math.exp(-n)) / 2; - } - function cosh(n) { - return (Math.exp(n) + Math.exp(-n)) / 2; - } - function tanh(n) { - return sinh(n) / cosh(n); - } - - // r₀: Zoom-out factor during ascent. - const r0 = r(0); - - // w(s): Returns the visible span on the ground, measured in pixels with respect to the - // initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°. - let w: (_: number) => number = function(s) { - return cosh(r0) / cosh(r0 + rho * s); - }; - - // u(s): Returns the distance along the flight path as projected onto the ground plane, - // measured in pixels from the world image origin at the initial scale. - let u: (_: number) => number = function(s) { - return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1; - }; - - // S: Total length of the flight path, measured in ρ-screenfuls. - let S = (r(1) - r0) / rho; - - // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. - if (Math.abs(u1) < 0.000001 || !isFinite(S)) { - // Perform a more or less instantaneous transition if the path is too short. - if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData); - - const k = w1 < w0 ? -1 : 1; - S = Math.abs(Math.log(w1 / w0)) / rho; - - u = function() { - return 0; - }; - w = function(s) { - return Math.exp(k * rho * s); - }; - } - - if ('duration' in options) { - options.duration = +options.duration; - } else { - const V = 'screenSpeed' in options ? - +options.screenSpeed / rho : - +options.speed; - options.duration = 1000 * S / V; - } - - if (options.maxDuration && options.duration > options.maxDuration) { - options.duration = 0; - } - - const zoomChanged = true; - const bearingChanged = startBearing !== bearing; - const pitchChanged = pitch !== startPitch; - const paddingChanged = !tr.isPaddingEqual(padding); - - const frame = (tr => k => { - // s: The distance traveled along the flight path, measured in ρ-screenfuls. - const s = k * S; - const scale = 1 / w(s); - tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(scale); - - if (bearingChanged) { - tr.bearing = interpolate(startBearing, bearing, k); - } - if (pitchChanged) { - tr.pitch = interpolate(startPitch, pitch, k); - } - if (paddingChanged) { - tr.interpolatePadding(startPadding, padding, k); - // When padding is being applied, Transform#centerPoint is changing continuously, - // thus we need to recalculate offsetPoint every frame - pointAtOffset = tr.centerPoint.add(offsetAsPoint); - } - - const newCenter = k === 1 ? - center : - tr.unproject(from.add(delta.mult(u(s))).mult(scale)); - tr.setLocationAtPoint( - tr.renderWorldCopies ? newCenter.wrap() : newCenter, - pointAtOffset, - ); - tr._updateCameraOnTerrain(); - - if (!options.preloadOnly) { - this._fireMoveEvents(eventData); - } - - return tr; - }); - - if (options.preloadOnly) { - const predictedTransforms = this._emulate(frame, options.duration, tr); - this._preloadTiles(predictedTransforms); - return this; - } - - this._zooming = zoomChanged; - this._rotating = bearingChanged; - this._pitching = pitchChanged; - this._padding = paddingChanged; - - this._prepareEase(eventData, false); - this._ease(frame(tr), () => this._afterEase(eventData), options); - - return this; - } - - isEasing(): boolean { - return !!this._easeFrameId; - } - - /** + function r(i) { + const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1); + return Math.log(Math.sqrt(b * b + 1) - b); + } + + function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } + function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } + function tanh(n) { return sinh(n) / cosh(n); } + + // r₀: Zoom-out factor during ascent. + const r0 = r(0); + + // w(s): Returns the visible span on the ground, measured in pixels with respect to the + // initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°. + let w: (_: number) => number = function (s) { + return (cosh(r0) / cosh(r0 + rho * s)); + }; + + // u(s): Returns the distance along the flight path as projected onto the ground plane, + // measured in pixels from the world image origin at the initial scale. + let u: (_: number) => number = function (s) { + return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1; + }; + + // S: Total length of the flight path, measured in ρ-screenfuls. + let S = (r(1) - r0) / rho; + + // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. + if (Math.abs(u1) < 0.000001 || !isFinite(S)) { + // Perform a more or less instantaneous transition if the path is too short. + if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData); + + const k = w1 < w0 ? -1 : 1; + S = Math.abs(Math.log(w1 / w0)) / rho; + + u = function() { return 0; }; + w = function(s) { return Math.exp(k * rho * s); }; + } + + if ('duration' in options) { + options.duration = +options.duration; + } else { + const V = 'screenSpeed' in options ? +options.screenSpeed / rho : +options.speed; + options.duration = 1000 * S / V; + } + + if (options.maxDuration && options.duration > options.maxDuration) { + options.duration = 0; + } + + const zoomChanged = true; + const bearingChanged = (startBearing !== bearing); + const pitchChanged = (pitch !== startPitch); + const paddingChanged = !tr.isPaddingEqual(padding); + + const frame = (tr) => (k) => { + // s: The distance traveled along the flight path, measured in ρ-screenfuls. + const s = k * S; + const scale = 1 / w(s); + tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(scale); + + if (bearingChanged) { + tr.bearing = interpolate(startBearing, bearing, k); + } + if (pitchChanged) { + tr.pitch = interpolate(startPitch, pitch, k); + } + if (paddingChanged) { + tr.interpolatePadding(startPadding, padding, k); + // When padding is being applied, Transform#centerPoint is changing continuously, + // thus we need to recalculate offsetPoint every frame + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + } + + const newCenter = k === 1 ? center : tr.unproject(from.add(delta.mult(u(s))).mult(scale)); + tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); + tr._updateCameraOnTerrain(); + + if (!options.preloadOnly) { + this._fireMoveEvents(eventData); + } + + return tr; + }; + + if (options.preloadOnly) { + const predictedTransforms = this._emulate(frame, options.duration, tr); + this._preloadTiles(predictedTransforms); + return this; + } + + this._zooming = zoomChanged; + this._rotating = bearingChanged; + this._pitching = pitchChanged; + this._padding = paddingChanged; + + this._prepareEase(eventData, false); + this._ease(frame(tr), () => this._afterEase(eventData), options); + + return this; + } + + isEasing(): boolean { + return !!this._easeFrameId; + } + + /** * Stops any animated transition underway. * * @memberof Map# @@ -1854,105 +1681,94 @@ class Camera * @example * map.stop(); */ - stop(): this { - return this._stop(); - } - - _stop(allowGestures?: boolean, easeId?: string): this { - if (this._easeFrameId) { - this._cancelRenderFrame(this._easeFrameId); - this._easeFrameId = undefined; - this._onEaseFrame = undefined; - } - - if (this._onEaseEnd) { - // The _onEaseEnd function might emit events which trigger new - // animation, which sets a new _onEaseEnd. Ensure we don't delete - // it unintentionally. - const onEaseEnd = this._onEaseEnd; - this._onEaseEnd = undefined; - onEaseEnd.call(this, easeId); - } - if (!allowGestures) { - const handlers = (this: any).handlers; - if (handlers) handlers.stop(false); - } - return this; - } - - _ease( - frame: (_: number) => Transform | void, - finish: () => void, - options: { - animate: boolean, - duration: number, - easing: (_: number) => number, - }, - ) { - if (options.animate === false || options.duration === 0) { - frame(1); - finish(); - } else { - this._easeStart = browser.now(); - this._easeOptions = options; - this._onEaseFrame = frame; - this._onEaseEnd = finish; - this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); - } - } - - // Callback for map._requestRenderFrame - _renderFrameCallback: (() => void) = () => { - const t = Math.min( - (browser.now() - this._easeStart) / this._easeOptions.duration, - 1, - ); - const frame = this._onEaseFrame; - if (frame) frame(this._easeOptions.easing(t)); - if (t < 1) { - this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); - } else { - this.stop(); - } - }; - - // convert bearing so that it's numerically close to the current one so that it interpolates properly - _normalizeBearing(bearing: number, currentBearing: number): number { - bearing = wrap(bearing, -180, 180); - const diff = Math.abs(bearing - currentBearing); - if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360; - if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360; - return bearing; - } - - // If a path crossing the antimeridian would be shorter, extend the final coordinate so that - // interpolating between the two endpoints will cross it. - _normalizeCenter(center: LngLat) { - const tr = this.transform; - if (!tr.renderWorldCopies || tr.maxBounds) return; - - const delta = center.lng - tr.center.lng; - center.lng += delta > 180 ? -360 : delta < -180 ? 360 : 0; - } - - // emulates frame function for some transform - _emulate( - frame: Function, - duration: number, - initialTransform: Transform, - ): Array { - const frameRate = 15; - const numFrames = Math.ceil(duration * frameRate / 1000); - - const transforms = []; - const emulateFrame = frame(initialTransform.clone()); - for (let i = 0; i <= numFrames; i++) { - const transform = emulateFrame(i / numFrames); - transforms.push(transform.clone()); - } - - return transforms; - } + stop(): this { + return this._stop(); + } + + _stop(allowGestures?: boolean, easeId?: string): this { + if (this._easeFrameId) { + this._cancelRenderFrame(this._easeFrameId); + this._easeFrameId = undefined; + this._onEaseFrame = undefined; + } + + if (this._onEaseEnd) { + // The _onEaseEnd function might emit events which trigger new + // animation, which sets a new _onEaseEnd. Ensure we don't delete + // it unintentionally. + const onEaseEnd = this._onEaseEnd; + this._onEaseEnd = undefined; + onEaseEnd.call(this, easeId); + } + if (!allowGestures) { + const handlers = (this: any).handlers; + if (handlers) handlers.stop(false); + } + return this; + } + + _ease(frame: (_: number) => Transform | void, + finish: () => void, + options: {animate: boolean, duration: number, easing: (_: number) => number}) { + if (options.animate === false || options.duration === 0) { + frame(1); + finish(); + } else { + this._easeStart = browser.now(); + this._easeOptions = options; + this._onEaseFrame = frame; + this._onEaseEnd = finish; + this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); + } + } + + // Callback for map._requestRenderFrame + _renderFrameCallback: (() => void) = () => { + const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1); + const frame = this._onEaseFrame; + if (frame) frame(this._easeOptions.easing(t)); + if (t < 1) { + this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); + } else { + this.stop(); + } + }; + + // convert bearing so that it's numerically close to the current one so that it interpolates properly + _normalizeBearing(bearing: number, currentBearing: number): number { + bearing = wrap(bearing, -180, 180); + const diff = Math.abs(bearing - currentBearing); + if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360; + if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360; + return bearing; + } + + // If a path crossing the antimeridian would be shorter, extend the final coordinate so that + // interpolating between the two endpoints will cross it. + _normalizeCenter(center: LngLat) { + const tr = this.transform; + if (!tr.renderWorldCopies || tr.maxBounds) return; + + const delta = center.lng - tr.center.lng; + center.lng += + delta > 180 ? -360 : + delta < -180 ? 360 : 0; + } + + // emulates frame function for some transform + _emulate(frame: Function, duration: number, initialTransform: Transform): Array { + const frameRate = 15; + const numFrames = Math.ceil(duration * frameRate / 1000); + + const transforms = []; + const emulateFrame = frame(initialTransform.clone()); + for (let i = 0; i <= numFrames; i++) { + const transform = emulateFrame(i / numFrames); + transforms.push(transform.clone()); + } + + return transforms; + } } // In debug builds, check that camera change events are fired in the correct order. From e79a00e0268b9b4b19fab0da43bd0ff6978875c3 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 12:09:19 +0200 Subject: [PATCH 40/72] fix formatting for ui/controls * attribution_control.js * fullscreen_control.js * geolocate_control.js --- src/ui/control/attribution_control.js | 392 ++++---- src/ui/control/fullscreen_control.js | 218 +++-- src/ui/control/geolocate_control.js | 1189 +++++++++++-------------- 3 files changed, 816 insertions(+), 983 deletions(-) diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 7c4617aca00..3fb0092d8ff 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -27,219 +27,187 @@ type Options = { * })); */ class AttributionControl { - options: Options; - _map: Map; - _container: HTMLElement; - _innerContainer: HTMLElement; - _compactButton: HTMLButtonElement; - _editLink: ?HTMLAnchorElement; - _attribHTML: string; - styleId: string; - styleOwner: string; - - constructor(options: Options = {}) { - this.options = options; - - bindAll( - ['_toggleAttribution', '_updateEditLink', '_updateData', '_updateCompact'], - this, - ); - } - - getDefaultPosition: () => ControlPosition = () => { - return 'bottom-right'; - } - - onAdd: (map: Map) => HTMLElement = (map) => { - const compact = this.options && this.options.compact; - - this._map = map; - this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-attrib'); - this._compactButton = DOM.create( - 'button', - 'mapboxgl-ctrl-attrib-button', - this._container, - ); - DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute( - 'aria-hidden', - 'true', - ); - this._compactButton.type = 'button'; - this._compactButton.addEventListener('click', this._toggleAttribution); - this._setElementTitle(this._compactButton, 'ToggleAttribution'); - this._innerContainer = DOM.create( - 'div', - 'mapboxgl-ctrl-attrib-inner', - this._container, - ); - this._innerContainer.setAttribute('role', 'list'); - - if (compact) { - this._container.classList.add('mapboxgl-compact'); - } - - this._updateAttributions(); - this._updateEditLink(); - - this._map.on('styledata', this._updateData); - this._map.on('sourcedata', this._updateData); - this._map.on('moveend', this._updateEditLink); - - if (compact === undefined) { - this._map.on('resize', this._updateCompact); - this._updateCompact(); - } - - return this._container; - } - - onRemove: () => void = () => { - this._container.remove(); - - this._map.off('styledata', this._updateData); - this._map.off('sourcedata', this._updateData); - this._map.off('moveend', this._updateEditLink); - this._map.off('resize', this._updateCompact); - - this._map = (undefined: any); - this._attribHTML = (undefined: any); - } - - _setElementTitle(element: HTMLElement, title: string) { - const str = this._map._getUIString(`AttributionControl.${title}`); - element.setAttribute('aria-label', str); - element.removeAttribute('title'); - if (element.firstElementChild) - element.firstElementChild.setAttribute('title', str); - } - - _toggleAttribution: (() => void) = () => { - if (this._container.classList.contains('mapboxgl-compact-show')) { - this._container.classList.remove('mapboxgl-compact-show'); - this._compactButton.setAttribute('aria-expanded', 'false'); - } else { - this._container.classList.add('mapboxgl-compact-show'); - this._compactButton.setAttribute('aria-expanded', 'true'); - } - }; - - _updateEditLink: (() => void) = () => { - let editLink = this._editLink; - if (!editLink) { - editLink = this._editLink = (this._container.querySelector( - '.mapbox-improve-map', - ): any); - } - - const params = [ - {key: 'owner', value: this.styleOwner}, - {key: 'id', value: this.styleId}, - { - key: 'access_token', - value: this._map._requestManager._customAccessToken || - config.ACCESS_TOKEN, - }, - ]; - - if (editLink) { - const paramString = params.reduce( - (acc, next, i) => { - if (next.value) { - acc += `${next.key}=${next.value}${i < params.length - 1 ? '&' : ''}`; + options: Options; + _map: Map; + _container: HTMLElement; + _innerContainer: HTMLElement; + _compactButton: HTMLButtonElement; + _editLink: ?HTMLAnchorElement; + _attribHTML: string; + styleId: string; + styleOwner: string; + + constructor(options: Options = {}) { + this.options = options; + + bindAll([ + '_toggleAttribution', + '_updateEditLink', + '_updateData', + '_updateCompact' + ], this); + } + + getDefaultPosition: () => ControlPosition = () => { + return 'bottom-right'; + }; + + onAdd: (map: Map) => HTMLElement = (map) => { + const compact = this.options && this.options.compact; + + this._map = map; + this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-attrib'); + this._compactButton = DOM.create('button', 'mapboxgl-ctrl-attrib-button', this._container); + DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute('aria-hidden', 'true'); + this._compactButton.type = 'button'; + this._compactButton.addEventListener('click', this._toggleAttribution); + this._setElementTitle(this._compactButton, 'ToggleAttribution'); + this._innerContainer = DOM.create('div', 'mapboxgl-ctrl-attrib-inner', this._container); + this._innerContainer.setAttribute('role', 'list'); + + if (compact) { + this._container.classList.add('mapboxgl-compact'); + } + + this._updateAttributions(); + this._updateEditLink(); + + this._map.on('styledata', this._updateData); + this._map.on('sourcedata', this._updateData); + this._map.on('moveend', this._updateEditLink); + + if (compact === undefined) { + this._map.on('resize', this._updateCompact); + this._updateCompact(); + } + + return this._container; + }; + + onRemove: () => void = () => { + this._container.remove(); + + this._map.off('styledata', this._updateData); + this._map.off('sourcedata', this._updateData); + this._map.off('moveend', this._updateEditLink); + this._map.off('resize', this._updateCompact); + + this._map = (undefined: any); + this._attribHTML = (undefined: any); + }; + + _setElementTitle(element: HTMLElement, title: string) { + const str = this._map._getUIString(`AttributionControl.${title}`); + element.setAttribute('aria-label', str); + element.removeAttribute('title'); + if (element.firstElementChild) element.firstElementChild.setAttribute('title', str); + } + + _toggleAttribution: (() => void) = () => { + if (this._container.classList.contains('mapboxgl-compact-show')) { + this._container.classList.remove('mapboxgl-compact-show'); + this._compactButton.setAttribute('aria-expanded', 'false'); + } else { + this._container.classList.add('mapboxgl-compact-show'); + this._compactButton.setAttribute('aria-expanded', 'true'); + } + }; + + _updateEditLink: (() => void) = () => { + let editLink = this._editLink; + if (!editLink) { + editLink = this._editLink = (this._container.querySelector('.mapbox-improve-map'): any); + } + + const params = [ + {key: 'owner', value: this.styleOwner}, + {key: 'id', value: this.styleId}, + {key: 'access_token', value: this._map._requestManager._customAccessToken || config.ACCESS_TOKEN} + ]; + + if (editLink) { + const paramString = params.reduce((acc, next, i) => { + if (next.value) { + acc += `${next.key}=${next.value}${i < params.length - 1 ? '&' : ''}`; + } + return acc; + }, `?`); + editLink.href = `${config.FEEDBACK_URL}/${paramString}#${getHashString(this._map, true)}`; + editLink.rel = 'noopener nofollow'; + this._setElementTitle(editLink, 'MapFeedback'); + } + }; + + _updateData: ((e: any) => void) = (e: any) => { + if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style')) { + this._updateAttributions(); + this._updateEditLink(); + } + }; + + _updateAttributions() { + if (!this._map.style) return; + let attributions: Array = []; + + if (this._map.style.stylesheet) { + const stylesheet: any = this._map.style.stylesheet; + this.styleOwner = stylesheet.owner; + this.styleId = stylesheet.id; + } + + const sourceCaches = this._map.style._sourceCaches; + for (const id in sourceCaches) { + const sourceCache = sourceCaches[id]; + if (sourceCache.used) { + const source = sourceCache.getSource(); + if (source.attribution && attributions.indexOf(source.attribution) < 0) { + attributions.push(source.attribution); + } } - return acc; - }, - `?`, - ); - editLink.href = `${config.FEEDBACK_URL}/${paramString}#${getHashString( - this._map, - true, - )}`; - editLink.rel = 'noopener nofollow'; - this._setElementTitle(editLink, 'MapFeedback'); - } - }; - - _updateData: ((e: any) => void) = (e: any) => { - if ( - e && - (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || - e.dataType === 'style') - ) { - this._updateAttributions(); - this._updateEditLink(); - } - }; - - _updateAttributions() { - if (!this._map.style) return; - let attributions: Array = []; - - if (this._map.style.stylesheet) { - const stylesheet: any = this._map.style.stylesheet; - this.styleOwner = stylesheet.owner; - this.styleId = stylesheet.id; - } - - const sourceCaches = this._map.style._sourceCaches; - for (const id in sourceCaches) { - const sourceCache = sourceCaches[id]; - if (sourceCache.used) { - const source = sourceCache.getSource(); - if (source.attribution && attributions.indexOf(source.attribution) < 0) { - attributions.push(source.attribution); - } - } - } - - // remove any entries that are substrings of another entry. - // first sort by length so that substrings come first - attributions.sort((a, b) => a.length - b.length); - attributions = attributions.filter( - (attrib, i) => { - for (let j = i + 1; j < attributions.length; j++) { - if (attributions[j].indexOf(attrib) >= 0) { - return false; - } - } - return true; - }, - ); - - if (this.options.customAttribution) { - if (Array.isArray(this.options.customAttribution)) { - attributions = [...this.options.customAttribution, ...attributions]; - } else { - attributions.unshift(this.options.customAttribution); - } - } - - // check if attribution string is different to minimize DOM changes - const attribHTML = attributions.join(' | '); - if (attribHTML === this._attribHTML) return; - - this._attribHTML = attribHTML; - - if (attributions.length) { - this._innerContainer.innerHTML = attribHTML; - this._container.classList.remove('mapboxgl-attrib-empty'); - } else { - this._container.classList.add('mapboxgl-attrib-empty'); - } - // remove old DOM node from _editLink - this._editLink = null; - } - - _updateCompact: (() => void) = () => { - if (this._map.getCanvasContainer().offsetWidth <= 640) { - this._container.classList.add('mapboxgl-compact'); - } else { - this._container.classList.remove( - 'mapboxgl-compact', - 'mapboxgl-compact-show', - ); - } - }; + } + + // remove any entries that are substrings of another entry. + // first sort by length so that substrings come first + attributions.sort((a, b) => a.length - b.length); + attributions = attributions.filter((attrib, i) => { + for (let j = i + 1; j < attributions.length; j++) { + if (attributions[j].indexOf(attrib) >= 0) { return false; } + } + return true; + }); + + if (this.options.customAttribution) { + if (Array.isArray(this.options.customAttribution)) { + attributions = [...this.options.customAttribution, ...attributions]; + } else { + attributions.unshift(this.options.customAttribution); + } + } + + // check if attribution string is different to minimize DOM changes + const attribHTML = attributions.join(' | '); + if (attribHTML === this._attribHTML) return; + + this._attribHTML = attribHTML; + + if (attributions.length) { + this._innerContainer.innerHTML = attribHTML; + this._container.classList.remove('mapboxgl-attrib-empty'); + } else { + this._container.classList.add('mapboxgl-attrib-empty'); + } + // remove old DOM node from _editLink + this._editLink = null; + } + + _updateCompact: (() => void) = () => { + if (this._map.getCanvasContainer().offsetWidth <= 640) { + this._container.classList.add('mapboxgl-compact'); + } else { + this._container.classList.remove('mapboxgl-compact', 'mapboxgl-compact-show'); + } + } + } export default AttributionControl; diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index c70e2dc5390..8121d268eb7 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -25,121 +25,109 @@ type Options = { */ class FullscreenControl { - _map: Map; - _controlContainer: HTMLElement; - _fullscreen: boolean; - _fullscreenchange: string; - _fullscreenButton: HTMLElement; - _container: HTMLElement; - - constructor(options: Options) { - this._fullscreen = false; - if (options && options.container) { - if (options.container instanceof window.HTMLElement) { - this._container = options.container; - } else { - warnOnce('Full screen control \'container\' must be a DOM element.'); - } - } - bindAll(['_onClickFullscreen', '_changeIcon'], this); - if ('onfullscreenchange' in window.document) { - this._fullscreenchange = 'fullscreenchange'; - } else if ('onwebkitfullscreenchange' in window.document) { - this._fullscreenchange = 'webkitfullscreenchange'; - } - } - - onAdd(map: Map): HTMLElement { - this._map = map; - if (!this._container) this._container = this._map.getContainer(); - this._controlContainer = DOM.create( - 'div', - `mapboxgl-ctrl mapboxgl-ctrl-group`, - ); - if (this._checkFullscreenSupport()) { - this._setupUI(); - } else { - this._controlContainer.style.display = 'none'; - warnOnce('This device does not support fullscreen mode.'); - } - return this._controlContainer; - } - - onRemove() { - this._controlContainer.remove(); - this._map = (null: any); - window.document.removeEventListener( - this._fullscreenchange, - this._changeIcon, - ); - } - - _checkFullscreenSupport(): boolean { - return !!(window.document.fullscreenEnabled || - (window.document: any).webkitFullscreenEnabled); - } - - _setupUI() { - const button = this._fullscreenButton = DOM.create( - 'button', - `mapboxgl-ctrl-fullscreen`, - this._controlContainer, - ); - DOM.create('span', `mapboxgl-ctrl-icon`, button).setAttribute( - 'aria-hidden', - 'true', - ); - button.type = 'button'; - this._updateTitle(); - this._fullscreenButton.addEventListener('click', this._onClickFullscreen); - window.document.addEventListener(this._fullscreenchange, this._changeIcon); - } - - _updateTitle() { - const title = this._getTitle(); - this._fullscreenButton.setAttribute("aria-label", title); - if (this._fullscreenButton.firstElementChild) - this._fullscreenButton.firstElementChild.setAttribute('title', title); - } - - _getTitle(): string { - return this._map._getUIString( - this._isFullscreen() ? - 'FullscreenControl.Exit' : - 'FullscreenControl.Enter', - ); - } - - _isFullscreen(): boolean { - return this._fullscreen; - } - - _changeIcon: (() => void) = () => { - const fullscreenElement = window.document.fullscreenElement || - (window.document: any).webkitFullscreenElement; - - if (fullscreenElement === this._container !== this._fullscreen) { - this._fullscreen = !this._fullscreen; - this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-shrink`); - this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-fullscreen`); - this._updateTitle(); - } - }; - - _onClickFullscreen: (() => void) = () => { - if (this._isFullscreen()) { - if (window.document.exitFullscreen) { - (window.document: any).exitFullscreen(); - } else if (window.document.webkitCancelFullScreen) { - (window.document: any).webkitCancelFullScreen(); - } - // $FlowFixMe[method-unbinding] - } else if (this._container.requestFullscreen) { - this._container.requestFullscreen(); - } else if ((this._container: any).webkitRequestFullscreen) { - (this._container: any).webkitRequestFullscreen(); - } - }; + _map: Map; + _controlContainer: HTMLElement; + _fullscreen: boolean; + _fullscreenchange: string; + _fullscreenButton: HTMLElement; + _container: HTMLElement; + + constructor(options: Options) { + this._fullscreen = false; + if (options && options.container) { + if (options.container instanceof window.HTMLElement) { + this._container = options.container; + } else { + warnOnce('Full screen control \'container\' must be a DOM element.'); + } + } + bindAll([ + '_onClickFullscreen', + '_changeIcon' + ], this); + if ('onfullscreenchange' in window.document) { + this._fullscreenchange = 'fullscreenchange'; + } else if ('onwebkitfullscreenchange' in window.document) { + this._fullscreenchange = 'webkitfullscreenchange'; + } + } + + onAdd(map: Map): HTMLElement { + this._map = map; + if (!this._container) this._container = this._map.getContainer(); + this._controlContainer = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); + if (this._checkFullscreenSupport()) { + this._setupUI(); + } else { + this._controlContainer.style.display = 'none'; + warnOnce('This device does not support fullscreen mode.'); + } + return this._controlContainer; + } + + onRemove() { + this._controlContainer.remove(); + this._map = (null: any); + window.document.removeEventListener(this._fullscreenchange, this._changeIcon); + } + + _checkFullscreenSupport(): boolean { + return !!( + window.document.fullscreenEnabled || + (window.document: any).webkitFullscreenEnabled + ); + } + + _setupUI() { + const button = this._fullscreenButton = DOM.create('button', (`mapboxgl-ctrl-fullscreen`), this._controlContainer); + DOM.create('span', `mapboxgl-ctrl-icon`, button).setAttribute('aria-hidden', 'true'); + button.type = 'button'; + this._updateTitle(); + this._fullscreenButton.addEventListener('click', this._onClickFullscreen); + window.document.addEventListener(this._fullscreenchange, this._changeIcon); + } + + _updateTitle() { + const title = this._getTitle(); + this._fullscreenButton.setAttribute("aria-label", title); + if (this._fullscreenButton.firstElementChild) this._fullscreenButton.firstElementChild.setAttribute('title', title); + } + + _getTitle(): string { + return this._map._getUIString(this._isFullscreen() ? 'FullscreenControl.Exit' : 'FullscreenControl.Enter'); + } + + _isFullscreen(): boolean { + return this._fullscreen; + } + + _changeIcon: (() => void) = () => { + const fullscreenElement = + window.document.fullscreenElement || + (window.document: any).webkitFullscreenElement; + + if ((fullscreenElement === this._container) !== this._fullscreen) { + this._fullscreen = !this._fullscreen; + this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-shrink`); + this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-fullscreen`); + this._updateTitle(); + } + }; + + _onClickFullscreen: (() => void) = () => { + if (this._isFullscreen()) { + if (window.document.exitFullscreen) { + (window.document: any).exitFullscreen(); + } else if (window.document.webkitCancelFullScreen) { + (window.document: any).webkitCancelFullScreen(); + } + // $FlowFixMe[method-unbinding] + } else if (this._container.requestFullscreen) { + this._container.requestFullscreen(); + } else if ((this._container: any).webkitRequestFullscreen) { + (this._container: any).webkitRequestFullscreen(); + } + }; } export default FullscreenControl; diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 5345f0f28cb..0aaffbbb6e6 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -87,496 +87,406 @@ const defaultOptions = { * })); * @see [Example: Locate the user](https://www.mapbox.com/mapbox-gl-js/example/locate-user/) */ -class GeolocateControl - extends Evented { - _map: Map; - options: Options; - _container: HTMLElement; - _dotElement: HTMLElement; - _circleElement: HTMLElement; - _geolocateButton: HTMLButtonElement; - _geolocationWatchID: number; - _timeoutId: ?TimeoutID; - _watchState: | 'OFF' - | 'ACTIVE_LOCK' - | 'WAITING_ACTIVE' - | 'ACTIVE_ERROR' - | 'BACKGROUND' - | 'BACKGROUND_ERROR'; - _lastKnownPosition: any; - _userLocationDotMarker: Marker; - _accuracyCircleMarker: Marker; - _accuracy: number; - _setup: boolean; // set to true once the control has been setup - _heading: ?number; - _updateMarkerRotationThrottled: Function; - - _numberOfWatches: number; - _noTimeout: boolean; - _supportsGeolocation: boolean; - - constructor(options: $Shape) { - super(); - const geolocation = window.navigator.geolocation; - this.options = extend({geolocation}, defaultOptions, options); - - bindAll( - [ - '_onSuccess', - '_onError', - '_onZoom', - '_finish', - '_setupUI', - '_updateCamera', - '_updateMarker', - '_updateMarkerRotation', - '_onDeviceOrientation', - ], - this, - ); - - this._updateMarkerRotationThrottled = throttle( - this._updateMarkerRotation, - 20, - ); - this._numberOfWatches = 0; - } - - onAdd(map: Map): HTMLElement { - this._map = map; - this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); - this._checkGeolocationSupport(this._setupUI); - return this._container; - } - - onRemove() { - // clear the geolocation watch if exists - if (this._geolocationWatchID !== undefined) { - this.options.geolocation.clearWatch(this._geolocationWatchID); - this._geolocationWatchID = (undefined: any); - } - - // clear the markers from the map - if (this.options.showUserLocation && this._userLocationDotMarker) { - this._userLocationDotMarker.remove(); - } - if (this.options.showAccuracyCircle && this._accuracyCircleMarker) { - this._accuracyCircleMarker.remove(); - } - - this._container.remove(); - this._map.off('zoom', this._onZoom); - this._map = (undefined: any); - this._numberOfWatches = 0; - this._noTimeout = false; - } - - _checkGeolocationSupport(callback: (boolean) => void) { - const updateSupport = ((supported = !!this.options.geolocation) => { - this._supportsGeolocation = supported; - callback(supported); - }); - - if (this._supportsGeolocation !== undefined) { - callback(this._supportsGeolocation); - } else if (window.navigator.permissions !== undefined) { - // navigator.permissions has incomplete browser support http://caniuse.com/#feat=permissions-api - // Test for the case where a browser disables Geolocation because of an insecure origin; - // in some environments like iOS16 WebView, permissions reject queries but still support geolocation - window.navigator.permissions.query({name: 'geolocation'}).then( - p => updateSupport(p.state !== 'denied'), - ).catch(() => updateSupport()); - } else { - updateSupport(); - } - } - - /** +class GeolocateControl extends Evented { + _map: Map; + options: Options; + _container: HTMLElement; + _dotElement: HTMLElement; + _circleElement: HTMLElement; + _geolocateButton: HTMLButtonElement; + _geolocationWatchID: number; + _timeoutId: ?TimeoutID; + _watchState: 'OFF' | 'ACTIVE_LOCK' | 'WAITING_ACTIVE' | 'ACTIVE_ERROR' | 'BACKGROUND' | 'BACKGROUND_ERROR'; + _lastKnownPosition: any; + _userLocationDotMarker: Marker; + _accuracyCircleMarker: Marker; + _accuracy: number; + _setup: boolean; // set to true once the control has been setup + _heading: ?number; + _updateMarkerRotationThrottled: Function; + + _numberOfWatches: number; + _noTimeout: boolean; + _supportsGeolocation: boolean; + + constructor(options: $Shape) { + super(); + const geolocation = window.navigator.geolocation; + this.options = extend({geolocation}, defaultOptions, options); + + bindAll([ + '_onSuccess', + '_onError', + '_onZoom', + '_finish', + '_setupUI', + '_updateCamera', + '_updateMarker', + '_updateMarkerRotation', + '_onDeviceOrientation' + ], this); + + this._updateMarkerRotationThrottled = throttle(this._updateMarkerRotation, 20); + this._numberOfWatches = 0; + } + + onAdd(map: Map): HTMLElement { + this._map = map; + this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); + this._checkGeolocationSupport(this._setupUI); + return this._container; + } + + onRemove() { + // clear the geolocation watch if exists + if (this._geolocationWatchID !== undefined) { + this.options.geolocation.clearWatch(this._geolocationWatchID); + this._geolocationWatchID = (undefined: any); + } + + // clear the markers from the map + if (this.options.showUserLocation && this._userLocationDotMarker) { + this._userLocationDotMarker.remove(); + } + if (this.options.showAccuracyCircle && this._accuracyCircleMarker) { + this._accuracyCircleMarker.remove(); + } + + this._container.remove(); + this._map.off('zoom', this._onZoom); + this._map = (undefined: any); + this._numberOfWatches = 0; + this._noTimeout = false; + } + + _checkGeolocationSupport(callback: boolean => void) { + const updateSupport = (supported = !!this.options.geolocation) => { + this._supportsGeolocation = supported; + callback(supported); + }; + + if (this._supportsGeolocation !== undefined) { + callback(this._supportsGeolocation); + + } else if (window.navigator.permissions !== undefined) { + // navigator.permissions has incomplete browser support http://caniuse.com/#feat=permissions-api + // Test for the case where a browser disables Geolocation because of an insecure origin; + // in some environments like iOS16 WebView, permissions reject queries but still support geolocation + window.navigator.permissions.query({name: 'geolocation'}) + .then(p => updateSupport(p.state !== 'denied')) + .catch(() => updateSupport()); + + } else { + updateSupport(); + } + } + + /** * Check if the Geolocation API Position is outside the map's maxbounds. * * @param {Position} position the Geolocation API Position * @returns {boolean} Returns `true` if position is outside the map's maxbounds, otherwise returns `false`. * @private */ - _isOutOfMapMaxBounds(position: Position): boolean { - const bounds = this._map.getMaxBounds(); - const coordinates = position.coords; - - return ( - !!bounds && - (coordinates.longitude < bounds.getWest() || - coordinates.longitude > bounds.getEast() || - coordinates.latitude < bounds.getSouth() || - coordinates.latitude > bounds.getNorth()) - ); - } - - _setErrorState() { - switch (this._watchState) { - case 'WAITING_ACTIVE': - this._watchState = 'ACTIVE_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-active-error', - ); - break; - case 'ACTIVE_LOCK': - this._watchState = 'ACTIVE_ERROR'; - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-active-error', - ); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - // turn marker grey - break; - case 'BACKGROUND': - this._watchState = 'BACKGROUND_ERROR'; - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background', - ); - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-background-error', - ); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - // turn marker grey - break; - case 'ACTIVE_ERROR': - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - } - - /** + _isOutOfMapMaxBounds(position: Position): boolean { + const bounds = this._map.getMaxBounds(); + const coordinates = position.coords; + + return !!bounds && ( + coordinates.longitude < bounds.getWest() || + coordinates.longitude > bounds.getEast() || + coordinates.latitude < bounds.getSouth() || + coordinates.latitude > bounds.getNorth() + ); + } + + _setErrorState() { + switch (this._watchState) { + case 'WAITING_ACTIVE': + this._watchState = 'ACTIVE_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); + break; + case 'ACTIVE_LOCK': + this._watchState = 'ACTIVE_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + // turn marker grey + break; + case 'BACKGROUND': + this._watchState = 'BACKGROUND_ERROR'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background-error'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + // turn marker grey + break; + case 'ACTIVE_ERROR': + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + } + + /** * When the Geolocation API returns a new location, update the GeolocateControl. * * @param {Position} position the Geolocation API Position * @private */ - _onSuccess: ((position: Position) => void) = (position: Position) => { - if (!this._map) { - // control has since been removed - return; - } - - if (this._isOutOfMapMaxBounds(position)) { - this._setErrorState(); - - this.fire(new Event('outofmaxbounds', position)); - this._updateMarker(); - this._finish(); - - return; - } - - if (this.options.trackUserLocation) { - // keep a record of the position so that if the state is BACKGROUND and the user - // clicks the button, we can move to ACTIVE_LOCK immediately without waiting for - // watchPosition to trigger _onSuccess - this._lastKnownPosition = position; - - switch (this._watchState) { - case 'WAITING_ACTIVE': - case 'ACTIVE_LOCK': - case 'ACTIVE_ERROR': - this._watchState = 'ACTIVE_LOCK'; - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-waiting', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-active-error', - ); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); - break; - case 'BACKGROUND': - case 'BACKGROUND_ERROR': - this._watchState = 'BACKGROUND'; - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-waiting', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background-error', - ); - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-background', - ); - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - } - - // if showUserLocation and the watch state isn't off then update the marker location - if (this.options.showUserLocation && this._watchState !== 'OFF') { - this._updateMarker(position); - } - - // if in normal mode (not watch mode), or if in watch mode and the state is active watch - // then update the camera - if (!this.options.trackUserLocation || this._watchState === 'ACTIVE_LOCK') { - this._updateCamera(position); - } - - if (this.options.showUserLocation) { - this._dotElement.classList.remove('mapboxgl-user-location-dot-stale'); - } - - this.fire(new Event('geolocate', position)); - this._finish(); - }; - - /** + _onSuccess: ((position: Position) => void) = (position: Position) => { + if (!this._map) { + // control has since been removed + return; + } + + if (this._isOutOfMapMaxBounds(position)) { + this._setErrorState(); + + this.fire(new Event('outofmaxbounds', position)); + this._updateMarker(); + this._finish(); + + return; + } + + if (this.options.trackUserLocation) { + // keep a record of the position so that if the state is BACKGROUND and the user + // clicks the button, we can move to ACTIVE_LOCK immediately without waiting for + // watchPosition to trigger _onSuccess + this._lastKnownPosition = position; + + switch (this._watchState) { + case 'WAITING_ACTIVE': + case 'ACTIVE_LOCK': + case 'ACTIVE_ERROR': + this._watchState = 'ACTIVE_LOCK'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); + break; + case 'BACKGROUND': + case 'BACKGROUND_ERROR': + this._watchState = 'BACKGROUND'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background'); + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + } + + // if showUserLocation and the watch state isn't off then update the marker location + if (this.options.showUserLocation && this._watchState !== 'OFF') { + this._updateMarker(position); + } + + // if in normal mode (not watch mode), or if in watch mode and the state is active watch + // then update the camera + if (!this.options.trackUserLocation || this._watchState === 'ACTIVE_LOCK') { + this._updateCamera(position); + } + + if (this.options.showUserLocation) { + this._dotElement.classList.remove('mapboxgl-user-location-dot-stale'); + } + + this.fire(new Event('geolocate', position)); + this._finish(); + }; + + /** * Update the camera location to center on the current position * * @param {Position} position the Geolocation API Position * @private */ - _updateCamera(position: Position) { - const center = new LngLat( - position.coords.longitude, - position.coords.latitude, - ); - const radius = position.coords.accuracy; - const bearing = this._map.getBearing(); - const options = extend({bearing}, this.options.fitBoundsOptions); - - this._map.fitBounds( - center.toBounds(radius), - options, - { - geolocateSource: true // tag this camera change so it won't cause the control to change to background state - , - }, - ); - } - - /** + _updateCamera(position: Position) { + const center = new LngLat(position.coords.longitude, position.coords.latitude); + const radius = position.coords.accuracy; + const bearing = this._map.getBearing(); + const options = extend({bearing}, this.options.fitBoundsOptions); + + this._map.fitBounds(center.toBounds(radius), options, { + geolocateSource: true // tag this camera change so it won't cause the control to change to background state + }); + } + + /** * Update the user location dot Marker to the current position * * @param {Position} [position] the Geolocation API Position * @private */ - _updateMarker(position: ?Position) { - if (position) { - const center = new LngLat( - position.coords.longitude, - position.coords.latitude, - ); - this._accuracyCircleMarker.setLngLat(center).addTo(this._map); - this._userLocationDotMarker.setLngLat(center).addTo(this._map); - this._accuracy = position.coords.accuracy; - if (this.options.showUserLocation && this.options.showAccuracyCircle) { - this._updateCircleRadius(); - } - } else { - this._userLocationDotMarker.remove(); - this._accuracyCircleMarker.remove(); - } - } - - _updateCircleRadius() { - assert(this._circleElement); - const map = this._map; - const tr = map.transform; - - const pixelsPerMeter = mercatorZfromAltitude(1.0, tr._center.lat) * tr.worldSize; - assert(pixelsPerMeter !== 0.0); - const circleDiameter = Math.ceil(2.0 * this._accuracy * pixelsPerMeter); - - this._circleElement.style.width = `${circleDiameter}px`; - this._circleElement.style.height = `${circleDiameter}px`; - } - - _onZoom: (() => void) = () => { - if (this.options.showUserLocation && this.options.showAccuracyCircle) { - this._updateCircleRadius(); - } - }; - - /** + _updateMarker(position: ?Position) { + if (position) { + const center = new LngLat(position.coords.longitude, position.coords.latitude); + this._accuracyCircleMarker.setLngLat(center).addTo(this._map); + this._userLocationDotMarker.setLngLat(center).addTo(this._map); + this._accuracy = position.coords.accuracy; + if (this.options.showUserLocation && this.options.showAccuracyCircle) { + this._updateCircleRadius(); + } + } else { + this._userLocationDotMarker.remove(); + this._accuracyCircleMarker.remove(); + } + } + + _updateCircleRadius() { + assert(this._circleElement); + const map = this._map; + const tr = map.transform; + + const pixelsPerMeter = mercatorZfromAltitude(1.0, tr._center.lat) * tr.worldSize; + assert(pixelsPerMeter !== 0.0); + const circleDiameter = Math.ceil(2.0 * this._accuracy * pixelsPerMeter); + + this._circleElement.style.width = `${circleDiameter}px`; + this._circleElement.style.height = `${circleDiameter}px`; + } + + _onZoom: (() => void) = () => { + if (this.options.showUserLocation && this.options.showAccuracyCircle) { + this._updateCircleRadius(); + } + } + + /** * Update the user location dot Marker rotation to the current heading * * @private */ - _updateMarkerRotation: (() => void) = () => { - if (this._userLocationDotMarker && typeof this._heading === 'number') { - this._userLocationDotMarker.setRotation(this._heading); - this._dotElement.classList.add('mapboxgl-user-location-show-heading'); - } else { - this._dotElement.classList.remove('mapboxgl-user-location-show-heading'); - this._userLocationDotMarker.setRotation(0); - } - }; - - _onError: ((error: PositionError) => void) = (error: PositionError) => { - if (!this._map) { - // control has since been removed - return; - } - - if (this.options.trackUserLocation) { - if (error.code === 1) { - // PERMISSION_DENIED - this._watchState = 'OFF'; - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-waiting', - ); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-active-error', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background-error', - ); - this._geolocateButton.disabled = true; - const title = this._map._getUIString( - 'GeolocateControl.LocationNotAvailable', - ); - this._geolocateButton.setAttribute('aria-label', title); - if (this._geolocateButton.firstElementChild) - this._geolocateButton.firstElementChild.setAttribute('title', title); - - if (this._geolocationWatchID !== undefined) { - this._clearWatch(); - } - } else if (error.code === 3 && this._noTimeout) { - // this represents a forced error state - // this was triggered to force immediate geolocation when a watch is already present - // see https://github.com/mapbox/mapbox-gl-js/issues/8214 - // and https://w3c.github.io/geolocation-api/#example-5-forcing-the-user-agent-to-return-a-fresh-cached-position - return; - } else { - this._setErrorState(); - } - } - - if (this._watchState !== 'OFF' && this.options.showUserLocation) { - this._dotElement.classList.add('mapboxgl-user-location-dot-stale'); - } - - this.fire(new Event('error', error)); - - this._finish(); - }; - - _finish: (() => void) = () => { - if (this._timeoutId) { - clearTimeout(this._timeoutId); - } - this._timeoutId = undefined; - }; - - _setupUI: ((supported: boolean) => void) = (supported: boolean) => { - if (this._map === undefined) { - // This control was removed from the map before geolocation - // support was determined. - return; - } - this._container.addEventListener( - 'contextmenu', - (e: MouseEvent) => e.preventDefault(), - ); - this._geolocateButton = DOM.create( - 'button', - `mapboxgl-ctrl-geolocate`, - this._container, - ); - DOM.create('span', `mapboxgl-ctrl-icon`, this._geolocateButton).setAttribute( - 'aria-hidden', - 'true', - ); - - this._geolocateButton.type = 'button'; - - if (supported === false) { - warnOnce( - 'Geolocation support is not available so the GeolocateControl will be disabled.', - ); - const title = this._map._getUIString( - 'GeolocateControl.LocationNotAvailable', - ); - this._geolocateButton.disabled = true; - this._geolocateButton.setAttribute('aria-label', title); - if (this._geolocateButton.firstElementChild) - this._geolocateButton.firstElementChild.setAttribute('title', title); - } else { - const title = this._map._getUIString('GeolocateControl.FindMyLocation'); - this._geolocateButton.setAttribute('aria-label', title); - if (this._geolocateButton.firstElementChild) - this._geolocateButton.firstElementChild.setAttribute('title', title); - } - - if (this.options.trackUserLocation) { - this._geolocateButton.setAttribute('aria-pressed', 'false'); - this._watchState = 'OFF'; - } - - // when showUserLocation is enabled, keep the Geolocate button disabled until the device location marker is setup on the map - if (this.options.showUserLocation) { - this._dotElement = DOM.create('div', 'mapboxgl-user-location'); - this._dotElement.appendChild( - DOM.create('div', 'mapboxgl-user-location-dot'), - ); - this._dotElement.appendChild( - DOM.create('div', 'mapboxgl-user-location-heading'), - ); - - this._userLocationDotMarker = new Marker( - { - element: this._dotElement, - rotationAlignment: 'map', - pitchAlignment: 'map', - }, - ); - - this._circleElement = DOM.create( - 'div', - 'mapboxgl-user-location-accuracy-circle', - ); - this._accuracyCircleMarker = new Marker( - {element: this._circleElement, pitchAlignment: 'map'}, - ); - - if (this.options.trackUserLocation) this._watchState = 'OFF'; - - this._map.on('zoom', this._onZoom); - } - - this._geolocateButton.addEventListener('click', this.trigger.bind(this)); - - this._setup = true; - - // when the camera is changed (and it's not as a result of the Geolocation Control) change - // the watch mode to background watch, so that the marker is updated but not the camera. - if (this.options.trackUserLocation) { - this._map.on( - 'movestart', - event => { - const fromResize = event.originalEvent && - event.originalEvent.type === 'resize'; - if ( - !event.geolocateSource && this._watchState === 'ACTIVE_LOCK' && - !fromResize - ) { - this._watchState = 'BACKGROUND'; - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-background', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-active', - ); - - this.fire(new Event('trackuserlocationend')); + _updateMarkerRotation: (() => void) = () => { + if (this._userLocationDotMarker && typeof this._heading === 'number') { + this._userLocationDotMarker.setRotation(this._heading); + this._dotElement.classList.add('mapboxgl-user-location-show-heading'); + } else { + this._dotElement.classList.remove('mapboxgl-user-location-show-heading'); + this._userLocationDotMarker.setRotation(0); + } + }; + + _onError: ((error: PositionError) => void) = (error: PositionError) => { + if (!this._map) { + // control has since been removed + return; + } + + if (this.options.trackUserLocation) { + if (error.code === 1) { + // PERMISSION_DENIED + this._watchState = 'OFF'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); + this._geolocateButton.disabled = true; + const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); + this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); + + if (this._geolocationWatchID !== undefined) { + this._clearWatch(); + } + } else if (error.code === 3 && this._noTimeout) { + // this represents a forced error state + // this was triggered to force immediate geolocation when a watch is already present + // see https://github.com/mapbox/mapbox-gl-js/issues/8214 + // and https://w3c.github.io/geolocation-api/#example-5-forcing-the-user-agent-to-return-a-fresh-cached-position + return; + } else { + this._setErrorState(); } - }, - ); - } - }; - - /** + } + + if (this._watchState !== 'OFF' && this.options.showUserLocation) { + this._dotElement.classList.add('mapboxgl-user-location-dot-stale'); + } + + this.fire(new Event('error', error)); + + this._finish(); + } + + _finish: (() => void) = () => { + if (this._timeoutId) { clearTimeout(this._timeoutId); } + this._timeoutId = undefined; + }; + + _setupUI: ((supported: boolean) => void) = (supported: boolean) => { + if (this._map === undefined) { + // This control was removed from the map before geolocation + // support was determined. + return; + } + this._container.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault()); + this._geolocateButton = DOM.create('button', `mapboxgl-ctrl-geolocate`, this._container); + DOM.create('span', `mapboxgl-ctrl-icon`, this._geolocateButton).setAttribute('aria-hidden', 'true'); + + this._geolocateButton.type = 'button'; + + if (supported === false) { + warnOnce('Geolocation support is not available so the GeolocateControl will be disabled.'); + const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); + this._geolocateButton.disabled = true; + this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); + } else { + const title = this._map._getUIString('GeolocateControl.FindMyLocation'); + this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); + } + + if (this.options.trackUserLocation) { + this._geolocateButton.setAttribute('aria-pressed', 'false'); + this._watchState = 'OFF'; + } + + // when showUserLocation is enabled, keep the Geolocate button disabled until the device location marker is setup on the map + if (this.options.showUserLocation) { + this._dotElement = DOM.create('div', 'mapboxgl-user-location'); + this._dotElement.appendChild(DOM.create('div', 'mapboxgl-user-location-dot')); + this._dotElement.appendChild(DOM.create('div', 'mapboxgl-user-location-heading')); + + this._userLocationDotMarker = new Marker({ + element: this._dotElement, + rotationAlignment: 'map', + pitchAlignment: 'map' + }); + + this._circleElement = DOM.create('div', 'mapboxgl-user-location-accuracy-circle'); + this._accuracyCircleMarker = new Marker({element: this._circleElement, pitchAlignment: 'map'}); + + if (this.options.trackUserLocation) this._watchState = 'OFF'; + + this._map.on('zoom', this._onZoom); + } + + this._geolocateButton.addEventListener('click', + this.trigger.bind(this)); + + this._setup = true; + + // when the camera is changed (and it's not as a result of the Geolocation Control) change + // the watch mode to background watch, so that the marker is updated but not the camera. + if (this.options.trackUserLocation) { + this._map.on('movestart', (event) => { + const fromResize = event.originalEvent && event.originalEvent.type === 'resize'; + if (!event.geolocateSource && this._watchState === 'ACTIVE_LOCK' && !fromResize) { + this._watchState = 'BACKGROUND'; + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + + this.fire(new Event('trackuserlocationend')); + } + }); + } + }; + + /** * Programmatically request and move the map to the user's location. * * @returns {boolean} Returns `false` if called before control was added to a map, otherwise returns `true`. @@ -598,21 +508,21 @@ class GeolocateControl * geolocate.trigger(); * }); */ - _onDeviceOrientation: ((deviceOrientationEvent: DeviceOrientationEvent) => void) = (deviceOrientationEvent: DeviceOrientationEvent) => { - // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. - if (this._userLocationDotMarker) { - if (deviceOrientationEvent.webkitCompassHeading) { - // Safari - this._heading = deviceOrientationEvent.webkitCompassHeading; - } else if (deviceOrientationEvent.absolute === true) { - // non-Safari alpha increases counter clockwise around the z axis - this._heading = deviceOrientationEvent.alpha * -1; - } - this._updateMarkerRotationThrottled(); - } - }; - - /** + _onDeviceOrientation: ((deviceOrientationEvent: DeviceOrientationEvent) => void) = (deviceOrientationEvent: DeviceOrientationEvent) => { + // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. + if (this._userLocationDotMarker) { + if (deviceOrientationEvent.webkitCompassHeading) { + // Safari + this._heading = deviceOrientationEvent.webkitCompassHeading; + } else if (deviceOrientationEvent.absolute === true) { + // non-Safari alpha increases counter clockwise around the z axis + this._heading = deviceOrientationEvent.alpha * -1; + } + this._updateMarkerRotationThrottled(); + } + } + + /** * Trigger a geolocation event. * * @example @@ -630,184 +540,151 @@ class GeolocateControl * }); * @returns {boolean} Returns `false` if called before control was added to a map, otherwise returns `true`. */ - trigger: (() => boolean) = (): boolean => { - if (!this._setup) { - warnOnce('Geolocate control triggered before added to a map'); - return false; - } - if (this.options.trackUserLocation) { - // update watchState and do any outgoing state cleanup - switch (this._watchState) { - case 'OFF': - // turn on the GeolocateControl - this._watchState = 'WAITING_ACTIVE'; - - this.fire(new Event('trackuserlocationstart')); - break; - case 'WAITING_ACTIVE': - case 'ACTIVE_LOCK': - case 'ACTIVE_ERROR': - case 'BACKGROUND_ERROR': - // turn off the Geolocate Control - this._numberOfWatches--; - this._noTimeout = false; - this._watchState = 'OFF'; - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-waiting', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-active', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-active-error', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background', - ); - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background-error', - ); - - this.fire(new Event('trackuserlocationend')); - break; - case 'BACKGROUND': - this._watchState = 'ACTIVE_LOCK'; - this._geolocateButton.classList.remove( - 'mapboxgl-ctrl-geolocate-background', - ); - // set camera to last known location - if (this._lastKnownPosition) - this._updateCamera(this._lastKnownPosition); - - this.fire(new Event('trackuserlocationstart')); - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - - // incoming state setup - switch (this._watchState) { - case 'WAITING_ACTIVE': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); - break; - case 'ACTIVE_LOCK': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); - break; - case 'ACTIVE_ERROR': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-active-error', - ); - break; - case 'BACKGROUND': - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-background', - ); - break; - case 'BACKGROUND_ERROR': - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.classList.add( - 'mapboxgl-ctrl-geolocate-background-error', - ); - break; - case 'OFF': - break; - default: - assert(false, `Unexpected watchState ${this._watchState}`); - } - - // manage geolocation.watchPosition / geolocation.clearWatch - if (this._watchState === 'OFF' && this._geolocationWatchID !== undefined) { - // clear watchPosition as we've changed to an OFF state - this._clearWatch(); - } else if (this._geolocationWatchID === undefined) { - // enable watchPosition since watchState is not OFF and there is no watchPosition already running - - this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.setAttribute('aria-pressed', 'true'); - - this._numberOfWatches++; - let positionOptions; - if (this._numberOfWatches > 1) { - positionOptions = {maximumAge: 600000, timeout: 0}; - this._noTimeout = true; - } else { - positionOptions = this.options.positionOptions; - this._noTimeout = false; - } - - this._geolocationWatchID = this.options.geolocation.watchPosition( - this._onSuccess, - this._onError, - positionOptions, - ); - - if (this.options.showUserHeading) { - this._addDeviceOrientationListener(); - } - } - } else { - this.options.geolocation.getCurrentPosition( - this._onSuccess, - this._onError, - this.options.positionOptions, - ); - - // This timeout ensures that we still call finish() even if - // the user declines to share their location in Firefox - this._timeoutId = setTimeout(this._finish, 10000 /* 10sec */); - } - - return true; - }; - - _addDeviceOrientationListener() { - const addListener = (() => { - if ('ondeviceorientationabsolute' in window) { - window.addEventListener( - 'deviceorientationabsolute', - this._onDeviceOrientation, - ); - } else { - window.addEventListener('deviceorientation', this._onDeviceOrientation); - } - }); - - if ( - typeof window.DeviceMotionEvent !== "undefined" && - typeof window.DeviceMotionEvent.requestPermission === 'function' - ) { - // $FlowFixMe - DeviceOrientationEvent.requestPermission().then( - response => { - if (response === 'granted') { - addListener(); + trigger: (() => boolean) = (): boolean => { + if (!this._setup) { + warnOnce('Geolocate control triggered before added to a map'); + return false; + } + if (this.options.trackUserLocation) { + // update watchState and do any outgoing state cleanup + switch (this._watchState) { + case 'OFF': + // turn on the GeolocateControl + this._watchState = 'WAITING_ACTIVE'; + + this.fire(new Event('trackuserlocationstart')); + break; + case 'WAITING_ACTIVE': + case 'ACTIVE_LOCK': + case 'ACTIVE_ERROR': + case 'BACKGROUND_ERROR': + // turn off the Geolocate Control + this._numberOfWatches--; + this._noTimeout = false; + this._watchState = 'OFF'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-active-error'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); + + this.fire(new Event('trackuserlocationend')); + break; + case 'BACKGROUND': + this._watchState = 'ACTIVE_LOCK'; + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); + // set camera to last known location + if (this._lastKnownPosition) this._updateCamera(this._lastKnownPosition); + + this.fire(new Event('trackuserlocationstart')); + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + + // incoming state setup + switch (this._watchState) { + case 'WAITING_ACTIVE': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); + break; + case 'ACTIVE_LOCK': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active'); + break; + case 'ACTIVE_ERROR': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-active-error'); + break; + case 'BACKGROUND': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background'); + break; + case 'BACKGROUND_ERROR': + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-background-error'); + break; + case 'OFF': + break; + default: + assert(false, `Unexpected watchState ${this._watchState}`); + } + + // manage geolocation.watchPosition / geolocation.clearWatch + if (this._watchState === 'OFF' && this._geolocationWatchID !== undefined) { + // clear watchPosition as we've changed to an OFF state + this._clearWatch(); + } else if (this._geolocationWatchID === undefined) { + // enable watchPosition since watchState is not OFF and there is no watchPosition already running + + this._geolocateButton.classList.add('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.setAttribute('aria-pressed', 'true'); + + this._numberOfWatches++; + let positionOptions; + if (this._numberOfWatches > 1) { + positionOptions = {maximumAge:600000, timeout:0}; + this._noTimeout = true; + } else { + positionOptions = this.options.positionOptions; + this._noTimeout = false; + } + + this._geolocationWatchID = this.options.geolocation.watchPosition( + this._onSuccess, this._onError, positionOptions); + + if (this.options.showUserHeading) { + this._addDeviceOrientationListener(); + } + } + } else { + this.options.geolocation.getCurrentPosition( + this._onSuccess, this._onError, this.options.positionOptions); + + // This timeout ensures that we still call finish() even if + // the user declines to share their location in Firefox + this._timeoutId = setTimeout(this._finish, 10000 /* 10sec */); + } + + return true; + } + + _addDeviceOrientationListener() { + const addListener = () => { + if ('ondeviceorientationabsolute' in window) { + window.addEventListener('deviceorientationabsolute', this._onDeviceOrientation); + } else { + window.addEventListener('deviceorientation', this._onDeviceOrientation); } - }, - ).catch(console.error); - } else { - addListener(); - } - } - - _clearWatch() { - this.options.geolocation.clearWatch(this._geolocationWatchID); - - window.removeEventListener('deviceorientation', this._onDeviceOrientation); - window.removeEventListener( - 'deviceorientationabsolute', - this._onDeviceOrientation, - ); - - this._geolocationWatchID = (undefined: any); - this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); - this._geolocateButton.setAttribute('aria-pressed', 'false'); - - if (this.options.showUserLocation) { - this._updateMarker(null); - } - } + }; + + if (typeof window.DeviceMotionEvent !== "undefined" && + typeof window.DeviceMotionEvent.requestPermission === 'function') { + // $FlowFixMe + DeviceOrientationEvent.requestPermission() + .then(response => { + if (response === 'granted') { + addListener(); + } + }) + .catch(console.error); + } else { + addListener(); + } + } + + _clearWatch() { + this.options.geolocation.clearWatch(this._geolocationWatchID); + + window.removeEventListener('deviceorientation', this._onDeviceOrientation); + window.removeEventListener('deviceorientationabsolute', this._onDeviceOrientation); + + this._geolocationWatchID = (undefined: any); + this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-waiting'); + this._geolocateButton.setAttribute('aria-pressed', 'false'); + + if (this.options.showUserLocation) { + this._updateMarker(null); + } + } } export default GeolocateControl; From 87fd1bb0b2013b1b0ed0504d81c42f925557f00f Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 12:20:28 +0200 Subject: [PATCH 41/72] fix formatting for ui/controls * logo_control.js * navigation_control.js * scale_control.js --- src/ui/control/logo_control.js | 142 ++++---- src/ui/control/navigation_control.js | 525 ++++++++++++--------------- src/ui/control/scale_control.js | 214 +++++------ 3 files changed, 406 insertions(+), 475 deletions(-) diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index c6c96b10a43..faa7e591e1c 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -16,78 +16,76 @@ import type Map, {ControlPosition} from '../map.js'; **/ class LogoControl { - _map: Map; - _container: HTMLElement; - - constructor() { - bindAll(['_updateLogo', '_updateCompact'], this); - } - - onAdd: (map: Map) => HTMLElement = (map) => { - this._map = map; - this._container = DOM.create('div', 'mapboxgl-ctrl'); - const anchor = DOM.create('a', 'mapboxgl-ctrl-logo'); - anchor.target = "_blank"; - anchor.rel = "noopener nofollow"; - anchor.href = "https://www.mapbox.com/"; - anchor.setAttribute( - "aria-label", - this._map._getUIString('LogoControl.Title'), - ); - anchor.setAttribute("rel", "noopener nofollow"); - this._container.appendChild(anchor); - this._container.style.display = 'none'; - - this._map.on('sourcedata', this._updateLogo); - this._updateLogo(); - - this._map.on('resize', this._updateCompact); - this._updateCompact(); - - return this._container; - } - - onRemove: () => void = () => { - this._container.remove(); - this._map.off('sourcedata', this._updateLogo); - this._map.off('resize', this._updateCompact); - } - - getDefaultPosition: () => ControlPosition = () => { - return 'bottom-left'; - } - - _updateLogo: ((e: any) => void) = (e: any) => { - if (!e || e.sourceDataType === 'metadata') { - this._container.style.display = this._logoRequired() ? 'block' : 'none'; - } - }; - - _logoRequired(): boolean { - if (!this._map.style) return true; - const sourceCaches = this._map.style._sourceCaches; - if (Object.entries(sourceCaches).length === 0) return true; - for (const id in sourceCaches) { - const source = sourceCaches[id].getSource(); - if (source.hasOwnProperty('mapbox_logo') && !source.mapbox_logo) { - return false; - } - } - - return true; - } - - _updateCompact: (() => void) = () => { - const containerChildren = this._container.children; - if (containerChildren.length) { - const anchor = containerChildren[0]; - if (this._map.getCanvasContainer().offsetWidth < 250) { - anchor.classList.add('mapboxgl-compact'); - } else { - anchor.classList.remove('mapboxgl-compact'); - } - } - }; + _map: Map; + _container: HTMLElement; + + constructor() { + bindAll(['_updateLogo', '_updateCompact'], this); + } + + onAdd: (map: Map) => HTMLElement = (map) => { + this._map = map; + this._container = DOM.create('div', 'mapboxgl-ctrl'); + const anchor = DOM.create('a', 'mapboxgl-ctrl-logo'); + anchor.target = "_blank"; + anchor.rel = "noopener nofollow"; + anchor.href = "https://www.mapbox.com/"; + anchor.setAttribute("aria-label", this._map._getUIString('LogoControl.Title')); + anchor.setAttribute("rel", "noopener nofollow"); + this._container.appendChild(anchor); + this._container.style.display = 'none'; + + this._map.on('sourcedata', this._updateLogo); + this._updateLogo(); + + this._map.on('resize', this._updateCompact); + this._updateCompact(); + + return this._container; + } + + onRemove: () => void = () => { + this._container.remove(); + this._map.off('sourcedata', this._updateLogo); + this._map.off('resize', this._updateCompact); + }; + + getDefaultPosition: () => ControlPosition = () => { + return 'bottom-left'; + }; + + _updateLogo: ((e: any) => void) = (e: any) => { + if (!e || e.sourceDataType === 'metadata') { + this._container.style.display = this._logoRequired() ? 'block' : 'none'; + } + }; + + _logoRequired(): boolean { + if (!this._map.style) return true; + const sourceCaches = this._map.style._sourceCaches; + if (Object.entries(sourceCaches).length === 0) return true; + for (const id in sourceCaches) { + const source = sourceCaches[id].getSource(); + if (source.hasOwnProperty('mapbox_logo') && !source.mapbox_logo) { + return false; + } + } + + return true; + } + + _updateCompact: (() => void) = () => { + const containerChildren = this._container.children; + if (containerChildren.length) { + const anchor = containerChildren[0]; + if (this._map.getCanvasContainer().offsetWidth < 250) { + anchor.classList.add('mapboxgl-compact'); + } else { + anchor.classList.remove('mapboxgl-compact'); + } + } + }; + } export default LogoControl; diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index 89b5c2a7bed..8b8897e4e70 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -41,307 +41,240 @@ const defaultOptions: Options = { * @see [Example: Add a third party vector tile source](https://www.mapbox.com/mapbox-gl-js/example/third-party/) */ class NavigationControl { - _map: ?Map; - options: Options; - _container: HTMLElement; - _zoomInButton: HTMLButtonElement; - _zoomOutButton: HTMLButtonElement; - _compass: HTMLButtonElement; - _compassIcon: HTMLElement; - _handler: ?MouseRotateWrapper; - - constructor(options: Options) { - this.options = extend({}, defaultOptions, options); - - this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-group'); - this._container.addEventListener( - 'contextmenu', - (e: MouseEvent) => e.preventDefault(), - ); - - if (this.options.showZoom) { - bindAll(['_setButtonTitle', '_updateZoomButtons'], this); - this._zoomInButton = this._createButton( - 'mapboxgl-ctrl-zoom-in', - e => { - if (this._map) this._map.zoomIn({}, {originalEvent: e}); - }, - ); - DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomInButton).setAttribute( - 'aria-hidden', - 'true', - ); - this._zoomOutButton = this._createButton( - 'mapboxgl-ctrl-zoom-out', - e => { - if (this._map) this._map.zoomOut({}, {originalEvent: e}); - }, - ); - DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomOutButton).setAttribute( - 'aria-hidden', - 'true', - ); - } - if (this.options.showCompass) { - bindAll(['_rotateCompassArrow'], this); - this._compass = this._createButton( - 'mapboxgl-ctrl-compass', - e => { - const map = this._map; - if (!map) return; + _map: ?Map; + options: Options; + _container: HTMLElement; + _zoomInButton: HTMLButtonElement; + _zoomOutButton: HTMLButtonElement; + _compass: HTMLButtonElement; + _compassIcon: HTMLElement; + _handler: ?MouseRotateWrapper; + + constructor(options: Options) { + this.options = extend({}, defaultOptions, options); + + this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-group'); + this._container.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault()); + + if (this.options.showZoom) { + bindAll([ + '_setButtonTitle', + '_updateZoomButtons' + ], this); + this._zoomInButton = this._createButton('mapboxgl-ctrl-zoom-in', (e) => { if (this._map) this._map.zoomIn({}, {originalEvent: e}); }); + DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomInButton).setAttribute('aria-hidden', 'true'); + this._zoomOutButton = this._createButton('mapboxgl-ctrl-zoom-out', (e) => { if (this._map) this._map.zoomOut({}, {originalEvent: e}); }); + DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomOutButton).setAttribute('aria-hidden', 'true'); + } + if (this.options.showCompass) { + bindAll([ + '_rotateCompassArrow' + ], this); + this._compass = this._createButton('mapboxgl-ctrl-compass', (e) => { + const map = this._map; + if (!map) return; + if (this.options.visualizePitch) { + map.resetNorthPitch({}, {originalEvent: e}); + } else { + map.resetNorth({}, {originalEvent: e}); + } + }); + this._compassIcon = DOM.create('span', 'mapboxgl-ctrl-icon', this._compass); + this._compassIcon.setAttribute('aria-hidden', 'true'); + } + } + + _updateZoomButtons: (() => void) = () => { + const map = this._map; + if (!map) return; + + const zoom = map.getZoom(); + const isMax = zoom === map.getMaxZoom(); + const isMin = zoom === map.getMinZoom(); + this._zoomInButton.disabled = isMax; + this._zoomOutButton.disabled = isMin; + this._zoomInButton.setAttribute('aria-disabled', isMax.toString()); + this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); + }; + + _rotateCompassArrow: (() => void) = () => { + const map = this._map; + if (!map) return; + + const rotate = this.options.visualizePitch ? + `scale(${1 / Math.pow(Math.cos(map.transform.pitch * (Math.PI / 180)), 0.5)}) rotateX(${map.transform.pitch}deg) rotateZ(${map.transform.angle * (180 / Math.PI)}deg)` : + `rotate(${map.transform.angle * (180 / Math.PI)}deg)`; + + map._requestDomTask(() => { + if (this._compassIcon) { + this._compassIcon.style.transform = rotate; + } + }); + }; + + onAdd(map: Map): HTMLElement { + this._map = map; + if (this.options.showZoom) { + this._setButtonTitle(this._zoomInButton, 'ZoomIn'); + this._setButtonTitle(this._zoomOutButton, 'ZoomOut'); + map.on('zoom', this._updateZoomButtons); + this._updateZoomButtons(); + } + if (this.options.showCompass) { + this._setButtonTitle(this._compass, 'ResetBearing'); + if (this.options.visualizePitch) { + map.on('pitch', this._rotateCompassArrow); + } + map.on('rotate', this._rotateCompassArrow); + this._rotateCompassArrow(); + this._handler = new MouseRotateWrapper(map, this._compass, this.options.visualizePitch); + } + return this._container; + } + + onRemove() { + const map = this._map; + if (!map) return; + this._container.remove(); + if (this.options.showZoom) { + map.off('zoom', this._updateZoomButtons); + } + if (this.options.showCompass) { if (this.options.visualizePitch) { - map.resetNorthPitch({}, {originalEvent: e}); - } else { - map.resetNorth({}, {originalEvent: e}); + map.off('pitch', this._rotateCompassArrow); } - }, - ); - this._compassIcon = DOM.create( - 'span', - 'mapboxgl-ctrl-icon', - this._compass, - ); - this._compassIcon.setAttribute('aria-hidden', 'true'); - } - } - - _updateZoomButtons: (() => void) = () => { - const map = this._map; - if (!map) return; - - const zoom = map.getZoom(); - const isMax = zoom === map.getMaxZoom(); - const isMin = zoom === map.getMinZoom(); - this._zoomInButton.disabled = isMax; - this._zoomOutButton.disabled = isMin; - this._zoomInButton.setAttribute('aria-disabled', isMax.toString()); - this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); - }; - - _rotateCompassArrow: (() => void) = () => { - const map = this._map; - if (!map) return; - - const rotate = this.options.visualizePitch ? - `scale(${1 / Math.pow( - Math.cos(map.transform.pitch * (Math.PI / 180)), - 0.5, - )}) rotateX(${map.transform.pitch}deg) rotateZ(${map.transform.angle * (180 / Math.PI)}deg)` : - `rotate(${map.transform.angle * (180 / Math.PI)}deg)`; - - map._requestDomTask( - () => { - if (this._compassIcon) { - this._compassIcon.style.transform = rotate; - } - }, - ); - }; - - onAdd(map: Map): HTMLElement { - this._map = map; - if (this.options.showZoom) { - this._setButtonTitle(this._zoomInButton, 'ZoomIn'); - this._setButtonTitle(this._zoomOutButton, 'ZoomOut'); - map.on('zoom', this._updateZoomButtons); - this._updateZoomButtons(); - } - if (this.options.showCompass) { - this._setButtonTitle(this._compass, 'ResetBearing'); - if (this.options.visualizePitch) { - map.on('pitch', this._rotateCompassArrow); - } - map.on('rotate', this._rotateCompassArrow); - this._rotateCompassArrow(); - this._handler = new MouseRotateWrapper( - map, - this._compass, - this.options.visualizePitch, - ); - } - return this._container; - } - - onRemove() { - const map = this._map; - if (!map) return; - this._container.remove(); - if (this.options.showZoom) { - map.off('zoom', this._updateZoomButtons); - } - if (this.options.showCompass) { - if (this.options.visualizePitch) { - map.off('pitch', this._rotateCompassArrow); - } - map.off('rotate', this._rotateCompassArrow); - if (this._handler) this._handler.off(); - this._handler = undefined; - } - this._map = undefined; - } - - _createButton(className: string, fn: () => mixed): HTMLButtonElement { - const a = DOM.create('button', className, this._container); - a.type = 'button'; - a.addEventListener('click', fn); - return a; - } - - _setButtonTitle(button: HTMLButtonElement, title: string) { - if (!this._map) return; - const str = this._map._getUIString(`NavigationControl.${title}`); - button.setAttribute('aria-label', str); - if (button.firstElementChild) - button.firstElementChild.setAttribute('title', str); - } + map.off('rotate', this._rotateCompassArrow); + if (this._handler) this._handler.off(); + this._handler = undefined; + } + this._map = undefined; + } + + _createButton(className: string, fn: () => mixed): HTMLButtonElement { + const a = DOM.create('button', className, this._container); + a.type = 'button'; + a.addEventListener('click', fn); + return a; + } + + _setButtonTitle(button: HTMLButtonElement, title: string) { + if (!this._map) return; + const str = this._map._getUIString(`NavigationControl.${title}`); + button.setAttribute('aria-label', str); + if (button.firstElementChild) button.firstElementChild.setAttribute('title', str); + } } class MouseRotateWrapper { - map: Map; - _clickTolerance: number; - element: HTMLElement; - mouseRotate: MouseRotateHandler; - mousePitch: MousePitchHandler; - _startPos: ?Point; - _lastPos: ?Point; - - constructor(map: Map, element: HTMLElement, pitch?: boolean = false) { - this._clickTolerance = 10; - this.element = element; - this.mouseRotate = new MouseRotateHandler( - {clickTolerance: map.dragRotate._mouseRotate._clickTolerance}, - ); - this.map = map; - if (pitch) - this.mousePitch = new MousePitchHandler( - {clickTolerance: map.dragRotate._mousePitch._clickTolerance}, - ); - - bindAll( - [ - 'mousedown', - 'mousemove', - 'mouseup', - 'touchstart', - 'touchmove', - 'touchend', - 'reset', - ], - this, - ); - element.addEventListener('mousedown', this.mousedown); - element.addEventListener('touchstart', this.touchstart, {passive: false}); - element.addEventListener('touchmove', this.touchmove); - element.addEventListener('touchend', this.touchend); - element.addEventListener('touchcancel', this.reset); - } - - down(e: MouseEvent, point: Point) { - this.mouseRotate.mousedown(e, point); - if (this.mousePitch) this.mousePitch.mousedown(e, point); - DOM.disableDrag(); - } - - move(e: MouseEvent, point: Point) { - const map = this.map; - const r = this.mouseRotate.mousemoveWindow(e, point); - const delta = r && r.bearingDelta; - if (delta) map.setBearing(map.getBearing() + delta); - if (this.mousePitch) { - const p = this.mousePitch.mousemoveWindow(e, point); - const delta = p && p.pitchDelta; - if (delta) map.setPitch(map.getPitch() + delta); - } - } - - off() { - const element = this.element; - element.removeEventListener('mousedown', this.mousedown); - element.removeEventListener( - 'touchstart', - this.touchstart, - {passive: false}, - ); - element.removeEventListener('touchmove', this.touchmove); - element.removeEventListener('touchend', this.touchend); - element.removeEventListener('touchcancel', this.reset); - this.offTemp(); - } - - offTemp() { - DOM.enableDrag(); - window.removeEventListener('mousemove', this.mousemove); - window.removeEventListener('mouseup', this.mouseup); - } - - mousedown: ((e: MouseEvent) => void) = (e: MouseEvent) => { - this.down( - extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), - DOM.mousePos(this.element, e), - ); - window.addEventListener('mousemove', this.mousemove); - window.addEventListener('mouseup', this.mouseup); - }; - - mousemove: ((e: MouseEvent) => void) = (e: MouseEvent) => { - this.move(e, DOM.mousePos(this.element, e)); - }; - - mouseup: ((e: MouseEvent) => void) = (e: MouseEvent) => { - this.mouseRotate.mouseupWindow(e); - if (this.mousePitch) this.mousePitch.mouseupWindow(e); - this.offTemp(); - }; - - touchstart: ((e: TouchEvent) => void) = (e: TouchEvent) => { - if (e.targetTouches.length !== 1) { - this.reset(); - } else { - this._startPos = this._lastPos = DOM.touchPos( - this.element, - e.targetTouches, - )[0]; - this.down( - (({ - type: 'mousedown', - button: 0, - ctrlKey: true, - preventDefault: () => e.preventDefault(), - }: any): MouseEvent), - this._startPos, - ); - } - }; - - touchmove: ((e: TouchEvent) => void) = (e: TouchEvent) => { - if (e.targetTouches.length !== 1) { - this.reset(); - } else { - this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; - this.move( - (({preventDefault: () => e.preventDefault()}: any): MouseEvent), - this._lastPos, - ); - } - }; - - touchend: ((e: TouchEvent) => void) = (e: TouchEvent) => { - if ( - e.targetTouches.length === 0 && this._startPos && this._lastPos && - this._startPos.dist(this._lastPos) < this._clickTolerance - ) { - this.element.click(); - } - this.reset(); - }; - - reset: (() => void) = () => { - this.mouseRotate.reset(); - if (this.mousePitch) this.mousePitch.reset(); - delete this._startPos; - delete this._lastPos; - this.offTemp(); - }; + + map: Map; + _clickTolerance: number; + element: HTMLElement; + mouseRotate: MouseRotateHandler; + mousePitch: MousePitchHandler; + _startPos: ?Point; + _lastPos: ?Point; + + constructor(map: Map, element: HTMLElement, pitch?: boolean = false) { + this._clickTolerance = 10; + this.element = element; + this.mouseRotate = new MouseRotateHandler({clickTolerance: map.dragRotate._mouseRotate._clickTolerance}); + this.map = map; + if (pitch) this.mousePitch = new MousePitchHandler({clickTolerance: map.dragRotate._mousePitch._clickTolerance}); + + bindAll(['mousedown', 'mousemove', 'mouseup', 'touchstart', 'touchmove', 'touchend', 'reset'], this); + element.addEventListener('mousedown', this.mousedown); + element.addEventListener('touchstart', this.touchstart, {passive: false}); + element.addEventListener('touchmove', this.touchmove); + element.addEventListener('touchend', this.touchend); + element.addEventListener('touchcancel', this.reset); + } + + down(e: MouseEvent, point: Point) { + this.mouseRotate.mousedown(e, point); + if (this.mousePitch) this.mousePitch.mousedown(e, point); + DOM.disableDrag(); + } + + move(e: MouseEvent, point: Point) { + const map = this.map; + const r = this.mouseRotate.mousemoveWindow(e, point); + const delta = r && r.bearingDelta; + if (delta) map.setBearing(map.getBearing() + delta); + if (this.mousePitch) { + const p = this.mousePitch.mousemoveWindow(e, point); + const delta = p && p.pitchDelta; + if (delta) map.setPitch(map.getPitch() + delta); + } + } + + off() { + const element = this.element; + element.removeEventListener('mousedown', this.mousedown); + element.removeEventListener('touchstart', this.touchstart, {passive: false}); + element.removeEventListener('touchmove', this.touchmove); + element.removeEventListener('touchend', this.touchend); + element.removeEventListener('touchcancel', this.reset); + this.offTemp(); + } + + offTemp() { + DOM.enableDrag(); + window.removeEventListener('mousemove', this.mousemove); + window.removeEventListener('mouseup', this.mouseup); + } + + mousedown: ((e: MouseEvent) => void) = (e: MouseEvent) => { + this.down(extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), DOM.mousePos(this.element, e)); + window.addEventListener('mousemove', this.mousemove); + window.addEventListener('mouseup', this.mouseup); + }; + + mousemove: ((e: MouseEvent) => void) = (e: MouseEvent) => { + this.move(e, DOM.mousePos(this.element, e)); + }; + + mouseup: ((e: MouseEvent) => void) = (e: MouseEvent) => { + this.mouseRotate.mouseupWindow(e); + if (this.mousePitch) this.mousePitch.mouseupWindow(e); + this.offTemp(); + }; + + touchstart: ((e: TouchEvent) => void) = (e: TouchEvent) => { + if (e.targetTouches.length !== 1) { + this.reset(); + } else { + this._startPos = this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; + this.down((({type: 'mousedown', button: 0, ctrlKey: true, preventDefault: () => e.preventDefault()}: any): MouseEvent), this._startPos); + } + }; + + touchmove: ((e: TouchEvent) => void) = (e: TouchEvent) => { + if (e.targetTouches.length !== 1) { + this.reset(); + } else { + this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; + this.move((({preventDefault: () => e.preventDefault()}: any): MouseEvent), this._lastPos); + } + }; + + touchend: ((e: TouchEvent) => void) = (e: TouchEvent) => { + if (e.targetTouches.length === 0 && + this._startPos && + this._lastPos && + this._startPos.dist(this._lastPos) < this._clickTolerance) { + this.element.click(); + } + this.reset(); + }; + + reset: (() => void) = () => { + this.mouseRotate.reset(); + if (this.mousePitch) this.mousePitch.reset(); + delete this._startPos; + delete this._lastPos; + this.offTemp(); + }; } export default NavigationControl; diff --git a/src/ui/control/scale_control.js b/src/ui/control/scale_control.js index 883287a537f..d36d43f7be0 100644 --- a/src/ui/control/scale_control.js +++ b/src/ui/control/scale_control.js @@ -35,117 +35,117 @@ const defaultOptions: Options = { * scale.setUnit('metric'); */ class ScaleControl { - _map: Map; - _container: HTMLElement; - _language: ?string | ?Array; - options: Options; - - constructor(options: Options) { - this.options = extend({}, defaultOptions, options); - - // Some old browsers (e.g., Safari < 14.1) don't support the "unit" style. - // This is a workaround to display the scale without proper internationalization support. - if (!isNumberFormatSupported()) { - // $FlowIgnore[cannot-write] - this._setScale = legacySetScale.bind(this); - } - - bindAll(['_update', '_setScale', 'setUnit'], this); - } - - getDefaultPosition(): ControlPosition { - return 'bottom-left'; - } - - _update: () => void = () => { - // A horizontal scale is imagined to be present at center of the map - // container with maximum length (Default) as 100px. - // Using spherical law of cosines approximation, the real distance is - // found between the two coordinates. - const maxWidth = this.options.maxWidth || 100; - - const map = this._map; - const y = map._containerHeight / 2; - const x = map._containerWidth / 2 - maxWidth / 2; - const left = map.unproject([x, y]); - const right = map.unproject([x + maxWidth, y]); - const maxMeters = left.distanceTo(right); - // The real distance corresponding to 100px scale length is rounded off to - // near pretty number and the scale length for the same is found out. - // Default unit of the scale is based on User's locale. - if (this.options.unit === 'imperial') { - const maxFeet = 3.2808 * maxMeters; - if (maxFeet > 5280) { - const maxMiles = maxFeet / 5280; - this._setScale(maxWidth, maxMiles, 'mile'); - } else { - this._setScale(maxWidth, maxFeet, 'foot'); - } - } else if (this.options.unit === 'nautical') { - const maxNauticals = maxMeters / 1852; - this._setScale(maxWidth, maxNauticals, 'nautical-mile'); - } else if (maxMeters >= 1000) { - this._setScale(maxWidth, maxMeters / 1000, 'kilometer'); - } else { - this._setScale(maxWidth, maxMeters, 'meter'); - } - }; - - _setScale(maxWidth: number, maxDistance: number, unit: string) { - const distance = getRoundNum(maxDistance); - const ratio = distance / maxDistance; - - this._map._requestDomTask(() => { - this._container.style.width = `${maxWidth * ratio}px`; - - // Intl.NumberFormat doesn't support nautical-mile as a unit, - // so we are hardcoding `nm` as a unit symbol for all locales - if (unit === 'nautical-mile') { - this._container.innerHTML = `${distance} nm`; - return; - } - - // $FlowFixMe — flow v0.153.0 doesn't support optional `locales` argument and `unit` style option - this._container.innerHTML = new Intl.NumberFormat(this._language, {style: 'unit', unitDisplay: 'narrow', unit}).format(distance); - }); - } - - onAdd(map: Map): HTMLElement { - this._map = map; - this._language = map.getLanguage(); - this._container = DOM.create( - 'div', - 'mapboxgl-ctrl mapboxgl-ctrl-scale', - map.getContainer(), - ); - this._container.dir = 'auto'; - - this._map.on('move', this._update); - this._update(); - - return this._container; - } - - onRemove() { - this._container.remove(); - this._map.off('move', this._update); - this._map = (undefined: any); - } - - _setLanguage(language: string) { - this._language = language; - this._update(); - } - - /** + _map: Map; + _container: HTMLElement; + _language: ?string | ?string[]; + options: Options; + + constructor(options: Options) { + this.options = extend({}, defaultOptions, options); + + // Some old browsers (e.g., Safari < 14.1) don't support the "unit" style. + // This is a workaround to display the scale without proper internationalization support. + if (!isNumberFormatSupported()) { + // $FlowIgnore[cannot-write] + this._setScale = legacySetScale.bind(this); + } + + bindAll([ + '_update', + '_setScale', + 'setUnit' + ], this); + } + + getDefaultPosition(): ControlPosition { + return 'bottom-left'; + } + + _update: () => void = () => { + // A horizontal scale is imagined to be present at center of the map + // container with maximum length (Default) as 100px. + // Using spherical law of cosines approximation, the real distance is + // found between the two coordinates. + const maxWidth = this.options.maxWidth || 100; + + const map = this._map; + const y = map._containerHeight / 2; + const x = (map._containerWidth / 2) - maxWidth / 2; + const left = map.unproject([x, y]); + const right = map.unproject([x + maxWidth, y]); + const maxMeters = left.distanceTo(right); + // The real distance corresponding to 100px scale length is rounded off to + // near pretty number and the scale length for the same is found out. + // Default unit of the scale is based on User's locale. + if (this.options.unit === 'imperial') { + const maxFeet = 3.2808 * maxMeters; + if (maxFeet > 5280) { + const maxMiles = maxFeet / 5280; + this._setScale(maxWidth, maxMiles, 'mile'); + } else { + this._setScale(maxWidth, maxFeet, 'foot'); + } + } else if (this.options.unit === 'nautical') { + const maxNauticals = maxMeters / 1852; + this._setScale(maxWidth, maxNauticals, 'nautical-mile'); + } else if (maxMeters >= 1000) { + this._setScale(maxWidth, maxMeters / 1000, 'kilometer'); + } else { + this._setScale(maxWidth, maxMeters, 'meter'); + } + }; + + _setScale(maxWidth: number, maxDistance: number, unit: string) { + const distance = getRoundNum(maxDistance); + const ratio = distance / maxDistance; + + this._map._requestDomTask(() => { + this._container.style.width = `${maxWidth * ratio}px`; + + // Intl.NumberFormat doesn't support nautical-mile as a unit, + // so we are hardcoding `nm` as a unit symbol for all locales + if (unit === 'nautical-mile') { + this._container.innerHTML = `${distance} nm`; + return; + } + + // $FlowFixMe — flow v0.142.0 doesn't support optional `locales` argument and `unit` style option + this._container.innerHTML = new Intl.NumberFormat(this._language, {style: 'unit', unitDisplay: 'narrow', unit}).format(distance); + }); + } + + onAdd(map: Map): HTMLElement { + this._map = map; + this._language = map.getLanguage(); + this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-scale', map.getContainer()); + this._container.dir = 'auto'; + + this._map.on('move', this._update); + this._update(); + + return this._container; + } + + onRemove() { + this._container.remove(); + this._map.off('move', this._update); + this._map = (undefined: any); + } + + _setLanguage(language: string) { + this._language = language; + this._update(); + } + + /** * Set the scale's unit of the distance. * * @param {'imperial' | 'metric' | 'nautical'} unit Unit of the distance (`'imperial'`, `'metric'` or `'nautical'`). */ - setUnit(unit: Unit) { - this.options.unit = unit; - this._update(); - } + setUnit(unit: Unit) { + this.options.unit = unit; + this._update(); + } } export default ScaleControl; From b46232ae8507cc41cfd4e7622e00da2d95b7c547 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 12:23:04 +0200 Subject: [PATCH 42/72] Upgrade Flow to v0.178.0 --- .flowconfig | 2 +- package.json | 2 +- src/data/program_configuration.js | 14 +++++++++++++- src/style/properties.js | 1 + src/style/style_layer/line_style_layer.js | 2 +- src/util/web_worker.js | 4 ++-- yarn.lock | 8 ++++---- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.flowconfig b/.flowconfig index ea38d228bad..e46742e17c2 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.177.0 +0.178.0 [options] diff --git a/package.json b/package.json index c98203596ba..ae3d9210e7c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.177.0", + "flow-bin": "0.178.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 5f84a8cc300..6e6ec3c71ff 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -387,7 +387,7 @@ export default class ProgramConfiguration { const names = paintAttributeNames(property, layer.type); const expression = value.value; const type = value.property.specification.type; - const useIntegerZoom = value.property.useIntegerZoom; + const useIntegerZoom = !!value.property.useIntegerZoom; const isPattern = property === 'line-dasharray' || property.endsWith('pattern'); const sourceException = property === 'line-dasharray' && (layer.layout: any).get('line-cap').value.kind !== 'constant'; @@ -398,14 +398,24 @@ export default class ProgramConfiguration { keys.push(`/u_${property}`); } else if (expression.kind === 'source' || sourceException || isPattern) { + assert(expression.kind === 'composite' || expression.kind === 'source', `Unexpected expression kind ${expression.kind} in program configuration`); const StructArrayLayout = layoutType(property, type, 'source'); this.binders[property] = isPattern ? + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-call] - assert should refine kind new PatternCompositeBinder(expression, names, type, StructArrayLayout, layer.id) : + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-call] - assert should refine kind new SourceExpressionBinder(expression, names, type, StructArrayLayout); + keys.push(`/a_${property}`); } else { const StructArrayLayout = layoutType(property, type, 'composite'); + + assert(expression.kind === 'composite', `Unexpected expression kind ${expression.kind} in program configuration`); + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-call] - assert should refine kind this.binders[property] = new CompositeExpressionBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout); keys.push(`/z_${property}`); } @@ -659,7 +669,9 @@ const defaultLayouts = { }; function layoutType(property, type, binderType) { + assert(type === 'color' || type === 'number', `Unknown layout type: ${type}`); const layoutException = propertyExceptions[property]; + // $FlowFixMe[prop-missing] - assert above ensures that type is a valid key return (layoutException && layoutException[binderType]) || defaultLayouts[type][binderType]; } diff --git a/src/style/properties.js b/src/style/properties.js index 284fee825b2..a9f34a1a1c7 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -540,6 +540,7 @@ export class DataConstantProperty implements Property { export class DataDrivenProperty implements Property> { specification: StylePropertySpecification; overrides: ?Object; + useIntegerZoom: ?boolean; constructor(specification: StylePropertySpecification, overrides?: Object) { this.specification = specification; diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 59243fff51a..58806f55210 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -22,7 +22,7 @@ import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; class LineFloorwidthProperty extends DataDrivenProperty { - useIntegerZoom: true; + useIntegerZoom = true; possiblyEvaluate(value, parameters) { parameters = new EvaluationParameters(Math.floor(parameters.zoom), { diff --git a/src/util/web_worker.js b/src/util/web_worker.js index bcdb4afa5a8..44c3532a67f 100644 --- a/src/util/web_worker.js +++ b/src/util/web_worker.js @@ -24,8 +24,8 @@ export interface WorkerInterface { export interface WorkerGlobalScopeInterface { importScripts(...urls: Array): void; - registerWorkerSource: (string, Class) => void, - registerRTLTextPlugin: (_: any) => void + registerWorkerSource?: (string, Class) => void, + registerRTLTextPlugin?: (_: any) => void } class MessageBus implements WorkerInterface, WorkerGlobalScopeInterface { diff --git a/yarn.lock b/yarn.lock index 6c92a1ffda5..ef8583f52dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.177.0: - version "0.177.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.177.0.tgz#dd537424dcbdd56f3cc85fd72330840a590e4711" - integrity sha512-hEm9VDf07iGcfjiCaxZAbpp/bRcgPf/Q3f7UucWpMotrM0MmyZ2hCBvhw53XCd3M7+fP8eyZKRvUWtrMqEC/Sg== +flow-bin@0.178.0: + version "0.178.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.178.0.tgz#893f32bbb2bea09a5cf53446d99d4c791b2220b2" + integrity sha512-D1S8c/oaiHAeKaxNxdJJ6SxjxwetNRgPWc9PI1wCSNfB3o0gAJKE5ic2B84c2QmRcs6dMYw6b9GDelYpWsArfg== follow-redirects@^1.0.0: version "1.15.1" From e25cb35f7a1fcd7d61f60a47c1a63d66ec456c12 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 12:26:39 +0200 Subject: [PATCH 43/72] Upgrade Flow to v0.179.0 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index e46742e17c2..d0080b9b19b 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.178.0 +0.179.0 [options] diff --git a/package.json b/package.json index ae3d9210e7c..b8c826a17a4 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.178.0", + "flow-bin": "0.179.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index ef8583f52dd..9a3b3402982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.178.0: - version "0.178.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.178.0.tgz#893f32bbb2bea09a5cf53446d99d4c791b2220b2" - integrity sha512-D1S8c/oaiHAeKaxNxdJJ6SxjxwetNRgPWc9PI1wCSNfB3o0gAJKE5ic2B84c2QmRcs6dMYw6b9GDelYpWsArfg== +flow-bin@0.179.0: + version "0.179.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.179.0.tgz#a9afbedda1687726296bfc8bd34247a6aae34d4f" + integrity sha512-odCiPkX5Vjrgupqxq2cjib0GtzAjGRHVkLk4TG15N4B+Fd2ghb8HSW6zrdX9GwaXrsuocxm5+oVzkaaUYUf+Pg== follow-redirects@^1.0.0: version "1.15.1" From 521a657cd2b6d04f39ad3a788475c2d5632513f7 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 12:33:42 +0200 Subject: [PATCH 44/72] Upgrade Flow to v0.180.0 --- .flowconfig | 2 +- package.json | 2 +- src/util/ajax.js | 1 + yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index d0080b9b19b..e8cc7b0daa7 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.179.0 +0.180.0 [options] diff --git a/package.json b/package.json index b8c826a17a4..0006d950dbd 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.179.0", + "flow-bin": "0.180.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/util/ajax.js b/src/util/ajax.js index d21cb3b0119..8824c78c33b 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -343,6 +343,7 @@ export const getImage = function(requestParameters: RequestParameters, callback: const request = imageQueue.shift(); const {requestParameters, callback, cancelled} = request; if (!cancelled) { + // $FlowFixMe[cannot-write] - Flow can't infer that cancel is a writable property request.cancel = getImage(requestParameters, callback).cancel; } } diff --git a/yarn.lock b/yarn.lock index 9a3b3402982..38025ec88d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.179.0: - version "0.179.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.179.0.tgz#a9afbedda1687726296bfc8bd34247a6aae34d4f" - integrity sha512-odCiPkX5Vjrgupqxq2cjib0GtzAjGRHVkLk4TG15N4B+Fd2ghb8HSW6zrdX9GwaXrsuocxm5+oVzkaaUYUf+Pg== +flow-bin@0.180.0: + version "0.180.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.180.0.tgz#7a26f3a0cff61e386267e72ef7971d1af73e317a" + integrity sha512-jEZoIwOxzrtQ0erUu94nEzlqUoX7OAMeVs0CjO0rN6b7SDBhI5IysVRvGSQkkFWBJpy5VQ9lvzBYzq5Sq9vcmg== follow-redirects@^1.0.0: version "1.15.1" From d46b36eb0c846baaadc0e8319f2c489c98b7fd2e Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 12:34:00 +0200 Subject: [PATCH 45/72] fix formatting for uniform_binding.js --- src/render/uniform_binding.js | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index 77c65b56151..cfb4605ee2f 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -7,25 +7,25 @@ export type UniformValues = $Exact<$ObjMap(u: Uniform) => V>>; class Uniform { - gl: WebGLRenderingContext; - location: ?WebGLUniformLocation; - current: T; - initialized: boolean; - - constructor(context: Context) { - this.gl = context.gl; - this.initialized = false; - } - - fetchUniformLocation: ((program: WebGLProgram, name: string) => boolean) = (program: WebGLProgram, name: string): boolean => { - if (!this.location && !this.initialized) { - this.location = this.gl.getUniformLocation(program, name); - this.initialized = true; - } - return !!this.location; - }; - - +set: (program: WebGLProgram, name: string, v: T) => void; + gl: WebGLRenderingContext; + location: ?WebGLUniformLocation; + current: T; + initialized: boolean; + + constructor(context: Context) { + this.gl = context.gl; + this.initialized = false; + } + + fetchUniformLocation: ((program: WebGLProgram, name: string) => boolean) = (program: WebGLProgram, name: string): boolean => { + if (!this.location && !this.initialized) { + this.location = this.gl.getUniformLocation(program, name); + this.initialized = true; + } + return !!this.location; + }; + + +set: (program: WebGLProgram, name: string, v: T) => void; } class Uniform1i extends Uniform { From eb3ab5afe5a5f805e5822b91bcf33b086b42450d Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 12:37:06 +0200 Subject: [PATCH 46/72] Upgrade Flow to v0.181.0 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index e8cc7b0daa7..17d040855bf 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.180.0 +0.181.0 [options] diff --git a/package.json b/package.json index 0006d950dbd..96676988ea0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.180.0", + "flow-bin": "0.181.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 38025ec88d1..d3a4cbaf1ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.180.0: - version "0.180.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.180.0.tgz#7a26f3a0cff61e386267e72ef7971d1af73e317a" - integrity sha512-jEZoIwOxzrtQ0erUu94nEzlqUoX7OAMeVs0CjO0rN6b7SDBhI5IysVRvGSQkkFWBJpy5VQ9lvzBYzq5Sq9vcmg== +flow-bin@0.181.0: + version "0.181.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.181.0.tgz#0a0ceee391dea166232958826c09ac2973eec9ae" + integrity sha512-JVuD0GN9IhAj0WgS3e2BDstTOpx4KmYMTdiVSjeeLud49gs8EN5Ah/Jw59d7rdSXn1fTodoepPe1cKwleDoEag== follow-redirects@^1.0.0: version "1.15.1" From ab2f9e72793c7b77a3a00979ec0d5ef92074ede0 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 12:40:09 +0200 Subject: [PATCH 47/72] Upgrade Flow to v0.182.0 --- .flowconfig | 2 +- package.json | 2 +- src/style-spec/expression/definitions/comparison.js | 1 + yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 17d040855bf..e364ad83878 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.181.0 +0.182.0 [options] diff --git a/package.json b/package.json index 96676988ea0..b4569f4f105 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.181.0", + "flow-bin": "0.182.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/style-spec/expression/definitions/comparison.js b/src/style-spec/expression/definitions/comparison.js index 078df91473e..dddc1ae4129 100644 --- a/src/style-spec/expression/definitions/comparison.js +++ b/src/style-spec/expression/definitions/comparison.js @@ -62,6 +62,7 @@ function gteqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { function makeComparison(op: ComparisonOperator, compareBasic: (EvaluationContext, any, any) => boolean, compareWithCollator: (EvaluationContext, any, any, any) => boolean): ExpressionRegistration { const isOrderComparison = op !== '==' && op !== '!='; + // $FlowFixMe[method-unbinding] return class Comparison implements Expression { type: Type; lhs: Expression; diff --git a/yarn.lock b/yarn.lock index d3a4cbaf1ea..bda4623c723 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.181.0: - version "0.181.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.181.0.tgz#0a0ceee391dea166232958826c09ac2973eec9ae" - integrity sha512-JVuD0GN9IhAj0WgS3e2BDstTOpx4KmYMTdiVSjeeLud49gs8EN5Ah/Jw59d7rdSXn1fTodoepPe1cKwleDoEag== +flow-bin@0.182.0: + version "0.182.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.182.0.tgz#1dacbd72465743670412ada015d3182deda6f966" + integrity sha512-Ux90c2sMfoV/VVjOEFT2OHFJFnyfoIbTK/5AKAMnU4Skfru1G+FyS5YLu3XxQl0R6mpA9+rrFlPfYZq/5B+J3w== follow-redirects@^1.0.0: version "1.15.1" From 0adaf84e42acf0412b6e3bf6da81752eac239a73 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 12:53:06 +0200 Subject: [PATCH 48/72] Upgrade Flow to v0.183.0 --- .flowconfig | 2 +- package.json | 2 +- src/source/worker_tile.js | 1 + src/style/style_layer/circle_style_layer.js | 2 +- src/style/style_layer/fill_style_layer.js | 2 +- src/style/style_layer/heatmap_style_layer.js | 2 +- src/style/style_layer/line_style_layer.js | 2 +- src/style/style_layer/symbol_style_layer.js | 2 +- yarn.lock | 8 ++++---- 9 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.flowconfig b/.flowconfig index e364ad83878..a2921fe2270 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.182.0 +0.183.0 [options] diff --git a/package.json b/package.json index b4569f4f105..c94228603f6 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.182.0", + "flow-bin": "0.183.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 50f7d7bfa4b..153566bb386 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -155,6 +155,7 @@ class WorkerTile { const bucket = buckets[layer.id] = layer.createBucket({ index: featureIndex.bucketLayerIDs.length, + // $FlowFixMe[incompatible-call] - Flow can;t infer proper `family` type from `layer` above layers: family, zoom: this.zoom, canonical: this.canonical, diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index 81d997893ac..b56ee236d2d 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -36,7 +36,7 @@ class CircleStyleLayer extends StyleLayer { super(layer, properties); } - createBucket(parameters: BucketParameters<*>): CircleBucket { + createBucket(parameters: BucketParameters): CircleBucket { return new CircleBucket(parameters); } diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index d36cfe28d0d..d70a44ede1e 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -57,7 +57,7 @@ class FillStyleLayer extends StyleLayer { } } - createBucket(parameters: BucketParameters<*>): FillBucket { + createBucket(parameters: BucketParameters): FillBucket { return new FillBucket(parameters); } diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index 964498a4eee..97c6f6cdf9e 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -34,7 +34,7 @@ class HeatmapStyleLayer extends StyleLayer { _transitioningPaint: Transitioning; paint: PossiblyEvaluated; - createBucket(parameters: BucketParameters<*>): HeatmapBucket { + createBucket(parameters: BucketParameters): HeatmapBucket { return new HeatmapBucket(parameters); } diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 58806f55210..1d0ac273cc4 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -81,7 +81,7 @@ class LineStyleLayer extends StyleLayer { lineFloorwidthProperty.possiblyEvaluate(this._transitioningPaint._values['line-width'].value, parameters); } - createBucket(parameters: BucketParameters<*>): LineBucket { + createBucket(parameters: BucketParameters): LineBucket { return new LineBucket(parameters); } diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index a4896a32d26..91d3fde249c 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -106,7 +106,7 @@ class SymbolStyleLayer extends StyleLayer { return value; } - createBucket(parameters: BucketParameters<*>): SymbolBucket { + createBucket(parameters: BucketParameters): SymbolBucket { return new SymbolBucket(parameters); } diff --git a/yarn.lock b/yarn.lock index bda4623c723..8ce82aef4f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.182.0: - version "0.182.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.182.0.tgz#1dacbd72465743670412ada015d3182deda6f966" - integrity sha512-Ux90c2sMfoV/VVjOEFT2OHFJFnyfoIbTK/5AKAMnU4Skfru1G+FyS5YLu3XxQl0R6mpA9+rrFlPfYZq/5B+J3w== +flow-bin@0.183.0: + version "0.183.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.183.0.tgz#17f37c94edd04b705a897b5890dd6cdc02e0c94e" + integrity sha512-7IJHUnMPYgNEZU8t9M4vJII/G+fJft9C/INm2+HRSXx5KDF2j+vD2iap6+Yg2FWgXTnNLUvk7kr1QdO5Fk/8/Q== follow-redirects@^1.0.0: version "1.15.1" From 31edc5b7dd1e675e56dcdc438863c50134d559d7 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 14:13:47 +0200 Subject: [PATCH 49/72] fix formatting for ui/map.js --- src/ui/map.js | 4207 ++++++++++++++++++++++--------------------------- 1 file changed, 1865 insertions(+), 2342 deletions(-) diff --git a/src/ui/map.js b/src/ui/map.js index c11ca35e642..1a3f3c1a835 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -326,373 +326,315 @@ const defaultOptions = { * @see [Example: Display a map with a custom style](https://docs.mapbox.com/mapbox-gl-js/example/custom-style-id/) * @see [Example: Check if Mapbox GL JS is supported](https://docs.mapbox.com/mapbox-gl-js/example/check-for-support/) */ -class Map - extends Camera { - style: Style; - painter: Painter; - handlers: ?HandlerManager; - - _container: HTMLElement; - _missingCSSCanary: HTMLElement; - _canvasContainer: HTMLElement; - _controlContainer: HTMLElement; - _controlPositions: { [_: string]: HTMLElement }; - _interactive: ?boolean; - _showTileBoundaries: ?boolean; - _showTerrainWireframe: ?boolean; - _showQueryGeometry: ?boolean; - _showCollisionBoxes: ?boolean; - _showPadding: ?boolean; - _showTileAABBs: ?boolean; - _showOverdrawInspector: boolean; - _repaint: ?boolean; - _vertices: ?boolean; - _canvas: HTMLCanvasElement; - _minTileCacheSize: number; - _maxTileCacheSize: number; - _frame: ?Cancelable; - _renderNextFrame: ?boolean; - _styleDirty: ?boolean; - _sourcesDirty: ?boolean; - _placementDirty: ?boolean; - _loaded: boolean; - _fullyLoaded: boolean; // accounts for placement finishing as well - _trackResize: boolean; - _preserveDrawingBuffer: boolean; - _failIfMajorPerformanceCaveat: boolean; - _antialias: boolean; - _useWebGL2: boolean; - _refreshExpiredTiles: boolean; - _hash: Hash; - _delegatedListeners: any; - _isInitialLoad: boolean; - _shouldCheckAccess: boolean; - _fadeDuration: number; - _crossSourceCollisions: boolean; - _collectResourceTiming: boolean; - _optimizeForTerrain: boolean; - _renderTaskQueue: TaskQueue; - _domRenderTaskQueue: TaskQueue; - _controls: Array; - _markers: Array; - _popups: Array; - _logoControl: IControl; - _mapId: number; - _localIdeographFontFamily: string; - _localFontFamily: string; - _requestManager: RequestManager; - _locale: Object; - _removed: boolean; - _speedIndexTiming: boolean; - _clickTolerance: number; - _cooperativeGestures: boolean; - _silenceAuthErrors: boolean; - _averageElevationLastSampledAt: number; - _averageElevationExaggeration: number; - _averageElevation: EasedVariable; - _containerWidth: number; - _containerHeight: number; - _language: ?string | ?Array; - _worldview: ?string; - _interactionRange: [number, number]; - _visibilityHidden: number; - _performanceMetricsCollection: boolean; - - // `_useExplicitProjection` indicates that a projection is set by a call to map.setProjection() - _useExplicitProjection: boolean; - - /** @section {Interaction handlers} */ - - /** +class Map extends Camera { + style: Style; + painter: Painter; + handlers: ?HandlerManager; + + _container: HTMLElement; + _missingCSSCanary: HTMLElement; + _canvasContainer: HTMLElement; + _controlContainer: HTMLElement; + _controlPositions: {[_: string]: HTMLElement}; + _interactive: ?boolean; + _showTileBoundaries: ?boolean; + _showTerrainWireframe: ?boolean; + _showQueryGeometry: ?boolean; + _showCollisionBoxes: ?boolean; + _showPadding: ?boolean; + _showTileAABBs: ?boolean; + _showOverdrawInspector: boolean; + _repaint: ?boolean; + _vertices: ?boolean; + _canvas: HTMLCanvasElement; + _minTileCacheSize: number; + _maxTileCacheSize: number; + _frame: ?Cancelable; + _renderNextFrame: ?boolean; + _styleDirty: ?boolean; + _sourcesDirty: ?boolean; + _placementDirty: ?boolean; + _loaded: boolean; + _fullyLoaded: boolean; // accounts for placement finishing as well + _trackResize: boolean; + _preserveDrawingBuffer: boolean; + _failIfMajorPerformanceCaveat: boolean; + _antialias: boolean; + _useWebGL2: boolean; + _refreshExpiredTiles: boolean; + _hash: Hash; + _delegatedListeners: any; + _isInitialLoad: boolean; + _shouldCheckAccess: boolean; + _fadeDuration: number; + _crossSourceCollisions: boolean; + _collectResourceTiming: boolean; + _optimizeForTerrain: boolean; + _renderTaskQueue: TaskQueue; + _domRenderTaskQueue: TaskQueue; + _controls: Array; + _markers: Array; + _popups: Array; + _logoControl: IControl; + _mapId: number; + _localIdeographFontFamily: string; + _localFontFamily: string; + _requestManager: RequestManager; + _locale: Object; + _removed: boolean; + _speedIndexTiming: boolean; + _clickTolerance: number; + _cooperativeGestures: boolean; + _silenceAuthErrors: boolean; + _averageElevationLastSampledAt: number; + _averageElevationExaggeration: number; + _averageElevation: EasedVariable; + _containerWidth: number; + _containerHeight: number; + _language: ?string | ?string[]; + _worldview: ?string; + _interactionRange: [number, number]; + _visibilityHidden: number; + _performanceMetricsCollection: boolean; + + // `_useExplicitProjection` indicates that a projection is set by a call to map.setProjection() + _useExplicitProjection: boolean; + + /** @section {Interaction handlers} */ + + /** * The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad. * Find more details and examples using `scrollZoom` in the {@link ScrollZoomHandler} section. */ - scrollZoom: ScrollZoomHandler; + scrollZoom: ScrollZoomHandler; - /** + /** * The map's {@link BoxZoomHandler}, which implements zooming using a drag gesture with the Shift key pressed. * Find more details and examples using `boxZoom` in the {@link BoxZoomHandler} section. */ - boxZoom: BoxZoomHandler; + boxZoom: BoxZoomHandler; - /** + /** * The map's {@link DragRotateHandler}, which implements rotating the map while dragging with the right * mouse button or with the Control key pressed. Find more details and examples using `dragRotate` * in the {@link DragRotateHandler} section. */ - dragRotate: DragRotateHandler; + dragRotate: DragRotateHandler; - /** + /** * The map's {@link DragPanHandler}, which implements dragging the map with a mouse or touch gesture. * Find more details and examples using `dragPan` in the {@link DragPanHandler} section. */ - dragPan: DragPanHandler; + dragPan: DragPanHandler; - /** + /** * The map's {@link KeyboardHandler}, which allows the user to zoom, rotate, and pan the map using keyboard * shortcuts. Find more details and examples using `keyboard` in the {@link KeyboardHandler} section. */ - keyboard: KeyboardHandler; + keyboard: KeyboardHandler; - /** + /** * The map's {@link DoubleClickZoomHandler}, which allows the user to zoom by double clicking. * Find more details and examples using `doubleClickZoom` in the {@link DoubleClickZoomHandler} section. */ - doubleClickZoom: DoubleClickZoomHandler; + doubleClickZoom: DoubleClickZoomHandler; - /** + /** * The map's {@link TouchZoomRotateHandler}, which allows the user to zoom or rotate the map with touch gestures. * Find more details and examples using `touchZoomRotate` in the {@link TouchZoomRotateHandler} section. */ - touchZoomRotate: TouchZoomRotateHandler; + touchZoomRotate: TouchZoomRotateHandler; - /** + /** * The map's {@link TouchPitchHandler}, which allows the user to pitch the map with touch gestures. * Find more details and examples using `touchPitch` in the {@link TouchPitchHandler} section. */ - touchPitch: TouchPitchHandler; - - constructor(options: MapOptions) { - LivePerformanceUtils.mark(PerformanceMarkers.create); - - options = extend({}, defaultOptions, options); - - if ( - options.minZoom != null && options.maxZoom != null && - options.minZoom > options.maxZoom - ) { - throw new Error(`maxZoom must be greater than or equal to minZoom`); - } - - if ( - options.minPitch != null && options.maxPitch != null && - options.minPitch > options.maxPitch - ) { - throw new Error(`maxPitch must be greater than or equal to minPitch`); - } - - if (options.minPitch != null && options.minPitch < defaultMinPitch) { - throw ( - new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`,) - ); - } - - if (options.maxPitch != null && options.maxPitch > defaultMaxPitch) { - throw ( - new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`) - ); - } - - // disable antialias with OS/iOS 15.4 and 15.5 due to rendering bug - if (options.antialias && isSafariWithAntialiasingBug(window)) { - options.antialias = false; - warnOnce( - 'Antialiasing is disabled for this WebGL context to avoid browser bug: https://github.com/mapbox/mapbox-gl-js/issues/11609', - ); - } - - const transform = new Transform( - options.minZoom, - options.maxZoom, - options.minPitch, - options.maxPitch, - options.renderWorldCopies, - ); - super(transform, options); - - this._interactive = options.interactive; - this._minTileCacheSize = options.minTileCacheSize; - this._maxTileCacheSize = options.maxTileCacheSize; - this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat; - this._preserveDrawingBuffer = options.preserveDrawingBuffer; - this._antialias = options.antialias; - this._useWebGL2 = options.useWebGL2; - this._trackResize = options.trackResize; - this._bearingSnap = options.bearingSnap; - this._refreshExpiredTiles = options.refreshExpiredTiles; - this._fadeDuration = options.fadeDuration; - this._isInitialLoad = true; - this._crossSourceCollisions = options.crossSourceCollisions; - this._collectResourceTiming = options.collectResourceTiming; - this._optimizeForTerrain = options.optimizeForTerrain; - this._language = this._parseLanguage(options.language); - this._worldview = options.worldview; - this._renderTaskQueue = new TaskQueue(); - this._domRenderTaskQueue = new TaskQueue(); - this._controls = []; - this._markers = []; - this._popups = []; - this._mapId = uniqueId(); - this._locale = extend({}, defaultLocale, options.locale); - this._clickTolerance = options.clickTolerance; - this._cooperativeGestures = options.cooperativeGestures; - this._performanceMetricsCollection = options.performanceMetricsCollection; - this._containerWidth = 0; - this._containerHeight = 0; - - this._averageElevationLastSampledAt = -Infinity; - this._averageElevationExaggeration = 0; - this._averageElevation = new EasedVariable(0); - - this._interactionRange = [+Infinity, -Infinity]; - this._visibilityHidden = 0; - - this._useExplicitProjection = false; // Fallback to stylesheet by default - - this._requestManager = new RequestManager( - options.transformRequest, - options.accessToken, - options.testMode, - ); - this._silenceAuthErrors = !!options.testMode; - - if (typeof options.container === 'string') { - this._container = window.document.getElementById(options.container); - - if (!this._container) { - throw new Error(`Container '${options.container}' not found.`); - } - } else if (options.container instanceof window.HTMLElement) { - this._container = options.container; - } else { - throw ( - new Error(`Invalid type: 'container' must be a String or HTMLElement.`) - ); - } - - if (this._container.childNodes.length > 0) { - warnOnce(`The map container element should be empty, otherwise the map's interactivity will be negatively impacted. If you want to display a message when WebGL is not supported, use the Mapbox GL Supported plugin instead.`,); - } - - if (options.maxBounds) { - this.setMaxBounds(options.maxBounds); - } - - bindAll( - [ - '_onWindowOnline', - '_onWindowResize', - '_onVisibilityChange', - '_onMapScroll', - '_contextLost', - '_contextRestored', - ], - this, - ); - - this._setupContainer(); - this._setupPainter(); - if (this.painter === undefined) { - throw new Error(`Failed to initialize WebGL.`); - } - - this.on('move', () => this._update(false)); - this.on('moveend', () => this._update(false)); - this.on('zoom', () => this._update(true)); - - if (typeof window !== 'undefined') { - window.addEventListener('online', this._onWindowOnline, false); - window.addEventListener('resize', this._onWindowResize, false); - window.addEventListener('orientationchange', this._onWindowResize, false); - window.addEventListener( - 'webkitfullscreenchange', - this._onWindowResize, - false, - ); - window.addEventListener( - 'visibilitychange', - this._onVisibilityChange, - false, - ); - } - - this.handlers = new HandlerManager(this, options); - - this._localFontFamily = options.localFontFamily; - this._localIdeographFontFamily = options.localIdeographFontFamily; - - if (options.style) { - this.setStyle( - options.style, - { - localFontFamily: this._localFontFamily, - localIdeographFontFamily: this._localIdeographFontFamily, - }, - ); - } - - if (options.projection) { - this.setProjection(options.projection); - } - - const hashName = (typeof options.hash === 'string' && options.hash) || undefined; - this._hash = options.hash && new Hash(hashName).addTo(this); - // don't set position from options if set through hash - if (!this._hash || !this._hash._onHashChange()) { - this.jumpTo( - { - center: options.center, - zoom: options.zoom, - bearing: options.bearing, - pitch: options.pitch, - }, - ); - - if (options.bounds) { - this.resize(); - this.fitBounds( - options.bounds, - extend({}, options.fitBoundsOptions, {duration: 0}), - ); - } - } - - this.resize(); - - if (options.attributionControl) - this.addControl( - new AttributionControl({customAttribution: options.customAttribution}), - ); - - this._logoControl = new LogoControl(); - this.addControl(this._logoControl, options.logoPosition); - - this.on( - 'style.load', - () => { - if (this.transform.unmodified) { - this.jumpTo((this.style.stylesheet: any)); - } - }, - ); - this.on( - 'data', - (event: MapDataEvent) => { - this._update(event.dataType === 'style'); - this.fire(new Event(`${event.dataType}data`, event)); - }, - ); - this.on( - 'dataloading', - (event: MapDataEvent) => { - this.fire(new Event(`${event.dataType}dataloading`, event)); - }, - ); - } - - /* + touchPitch: TouchPitchHandler; + + constructor(options: MapOptions) { + LivePerformanceUtils.mark(PerformanceMarkers.create); + + options = extend({}, defaultOptions, options); + + if (options.minZoom != null && options.maxZoom != null && options.minZoom > options.maxZoom) { + throw new Error(`maxZoom must be greater than or equal to minZoom`); + } + + if (options.minPitch != null && options.maxPitch != null && options.minPitch > options.maxPitch) { + throw new Error(`maxPitch must be greater than or equal to minPitch`); + } + + if (options.minPitch != null && options.minPitch < defaultMinPitch) { + throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); + } + + if (options.maxPitch != null && options.maxPitch > defaultMaxPitch) { + throw new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`); + } + + // disable antialias with OS/iOS 15.4 and 15.5 due to rendering bug + if (options.antialias && isSafariWithAntialiasingBug(window)) { + options.antialias = false; + warnOnce('Antialiasing is disabled for this WebGL context to avoid browser bug: https://github.com/mapbox/mapbox-gl-js/issues/11609'); + } + + const transform = new Transform(options.minZoom, options.maxZoom, options.minPitch, options.maxPitch, options.renderWorldCopies); + super(transform, options); + + this._interactive = options.interactive; + this._minTileCacheSize = options.minTileCacheSize; + this._maxTileCacheSize = options.maxTileCacheSize; + this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat; + this._preserveDrawingBuffer = options.preserveDrawingBuffer; + this._antialias = options.antialias; + this._useWebGL2 = options.useWebGL2; + this._trackResize = options.trackResize; + this._bearingSnap = options.bearingSnap; + this._refreshExpiredTiles = options.refreshExpiredTiles; + this._fadeDuration = options.fadeDuration; + this._isInitialLoad = true; + this._crossSourceCollisions = options.crossSourceCollisions; + this._collectResourceTiming = options.collectResourceTiming; + this._optimizeForTerrain = options.optimizeForTerrain; + this._language = this._parseLanguage(options.language); + this._worldview = options.worldview; + this._renderTaskQueue = new TaskQueue(); + this._domRenderTaskQueue = new TaskQueue(); + this._controls = []; + this._markers = []; + this._popups = []; + this._mapId = uniqueId(); + this._locale = extend({}, defaultLocale, options.locale); + this._clickTolerance = options.clickTolerance; + this._cooperativeGestures = options.cooperativeGestures; + this._performanceMetricsCollection = options.performanceMetricsCollection; + this._containerWidth = 0; + this._containerHeight = 0; + + this._averageElevationLastSampledAt = -Infinity; + this._averageElevationExaggeration = 0; + this._averageElevation = new EasedVariable(0); + + this._interactionRange = [+Infinity, -Infinity]; + this._visibilityHidden = 0; + + this._useExplicitProjection = false; // Fallback to stylesheet by default + + this._requestManager = new RequestManager(options.transformRequest, options.accessToken, options.testMode); + this._silenceAuthErrors = !!options.testMode; + + if (typeof options.container === 'string') { + this._container = window.document.getElementById(options.container); + + if (!this._container) { + throw new Error(`Container '${options.container}' not found.`); + } + } else if (options.container instanceof window.HTMLElement) { + this._container = options.container; + } else { + throw new Error(`Invalid type: 'container' must be a String or HTMLElement.`); + } + + if (this._container.childNodes.length > 0) { + warnOnce(`The map container element should be empty, otherwise the map's interactivity will be negatively impacted. If you want to display a message when WebGL is not supported, use the Mapbox GL Supported plugin instead.`); + } + + if (options.maxBounds) { + this.setMaxBounds(options.maxBounds); + } + + bindAll([ + '_onWindowOnline', + '_onWindowResize', + '_onVisibilityChange', + '_onMapScroll', + '_contextLost', + '_contextRestored' + ], this); + + this._setupContainer(); + this._setupPainter(); + if (this.painter === undefined) { + throw new Error(`Failed to initialize WebGL.`); + } + + this.on('move', () => this._update(false)); + this.on('moveend', () => this._update(false)); + this.on('zoom', () => this._update(true)); + + if (typeof window !== 'undefined') { + window.addEventListener('online', this._onWindowOnline, false); + window.addEventListener('resize', this._onWindowResize, false); + window.addEventListener('orientationchange', this._onWindowResize, false); + window.addEventListener('webkitfullscreenchange', this._onWindowResize, false); + window.addEventListener('visibilitychange', this._onVisibilityChange, false); + } + + this.handlers = new HandlerManager(this, options); + + this._localFontFamily = options.localFontFamily; + this._localIdeographFontFamily = options.localIdeographFontFamily; + + if (options.style) { + this.setStyle(options.style, {localFontFamily: this._localFontFamily, localIdeographFontFamily: this._localIdeographFontFamily}); + } + + if (options.projection) { + this.setProjection(options.projection); + } + + const hashName = (typeof options.hash === 'string' && options.hash) || undefined; + this._hash = options.hash && (new Hash(hashName)).addTo(this); + // don't set position from options if set through hash + if (!this._hash || !this._hash._onHashChange()) { + this.jumpTo({ + center: options.center, + zoom: options.zoom, + bearing: options.bearing, + pitch: options.pitch + }); + + if (options.bounds) { + this.resize(); + this.fitBounds(options.bounds, extend({}, options.fitBoundsOptions, {duration: 0})); + } + } + + this.resize(); + + if (options.attributionControl) + this.addControl(new AttributionControl({customAttribution: options.customAttribution})); + + this._logoControl = new LogoControl(); + this.addControl(this._logoControl, options.logoPosition); + + this.on('style.load', () => { + if (this.transform.unmodified) { + this.jumpTo((this.style.stylesheet: any)); + } + }); + this.on('data', (event: MapDataEvent) => { + this._update(event.dataType === 'style'); + this.fire(new Event(`${event.dataType}data`, event)); + }); + this.on('dataloading', (event: MapDataEvent) => { + this.fire(new Event(`${event.dataType}dataloading`, event)); + }); + } + + /* * Returns a unique number for this map instance which is used for the MapLoadEvent * to make sure we only fire one event per instantiated map object. * @private * @returns {number} */ - _getMapId(): number { - return this._mapId; - } + _getMapId(): number { + return this._mapId; + } - /** @section {Controls} */ + /** @section {Controls} */ - /** + /** * Adds an {@link IControl} to the map, calling `control.onAdd(this)`. * * @param {IControl} control The {@link IControl} to add. @@ -704,39 +646,31 @@ class Map * map.addControl(new mapboxgl.NavigationControl()); * @see [Example: Display map navigation controls](https://www.mapbox.com/mapbox-gl-js/example/navigation/) */ - addControl(control: IControl, position?: ControlPosition): this { - if (position === undefined) { - if (control.getDefaultPosition) { - position = control.getDefaultPosition(); - } else { - position = 'top-right'; - } - } - if (!control || !control.onAdd) { - return this.fire( - new ErrorEvent( - new Error( - 'Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.', - ), - ), - ); - } - const controlElement = control.onAdd(this); - this._controls.push(control); - - const positionContainer = this._controlPositions[position]; - if (position.indexOf('bottom') !== -1) { - positionContainer.insertBefore( - controlElement, - positionContainer.firstChild, - ); - } else { - positionContainer.appendChild(controlElement); - } - return this; - } - - /** + addControl(control: IControl, position?: ControlPosition): this { + if (position === undefined) { + if (control.getDefaultPosition) { + position = control.getDefaultPosition(); + } else { + position = 'top-right'; + } + } + if (!control || !control.onAdd) { + return this.fire(new ErrorEvent(new Error( + 'Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.'))); + } + const controlElement = control.onAdd(this); + this._controls.push(control); + + const positionContainer = this._controlPositions[position]; + if (position.indexOf('bottom') !== -1) { + positionContainer.insertBefore(controlElement, positionContainer.firstChild); + } else { + positionContainer.appendChild(controlElement); + } + return this; + } + + /** * Removes the control from the map. * * @param {IControl} control The {@link IControl} to remove. @@ -749,23 +683,18 @@ class Map * // Remove zoom and rotation controls from the map. * map.removeControl(navigation); */ - removeControl(control: IControl): this { - if (!control || !control.onRemove) { - return this.fire( - new ErrorEvent( - new Error( - 'Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.', - ), - ), - ); - } - const ci = this._controls.indexOf(control); - if (ci > -1) this._controls.splice(ci, 1); - control.onRemove(this); - return this; - } - - /** + removeControl(control: IControl): this { + if (!control || !control.onRemove) { + return this.fire(new ErrorEvent(new Error( + 'Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.'))); + } + const ci = this._controls.indexOf(control); + if (ci > -1) this._controls.splice(ci, 1); + control.onRemove(this); + return this; + } + + /** * Checks if a control is on the map. * * @param {IControl} control The {@link IControl} to check. @@ -779,22 +708,22 @@ class Map * const added = map.hasControl(navigation); * // added === true */ - hasControl(control: IControl): boolean { - return this._controls.indexOf(control) > -1; - } + hasControl(control: IControl): boolean { + return this._controls.indexOf(control) > -1; + } - /** + /** * Returns the map's containing HTML element. * * @returns {HTMLElement} The map's container. * @example * const container = map.getContainer(); */ - getContainer(): HTMLElement { - return this._container; - } + getContainer(): HTMLElement { + return this._container; + } - /** + /** * Returns the HTML element containing the map's `` element. * * If you want to add non-GL overlays to the map, you should append them to this element. @@ -809,11 +738,11 @@ class Map * @see [Example: Create a draggable point](https://www.mapbox.com/mapbox-gl-js/example/drag-a-point/) * @see [Example: Highlight features within a bounding box](https://www.mapbox.com/mapbox-gl-js/example/using-box-queryrenderedfeatures/) */ - getCanvasContainer(): HTMLElement { - return this._canvasContainer; - } + getCanvasContainer(): HTMLElement { + return this._canvasContainer; + } - /** + /** * Returns the map's `` element. * * @returns {HTMLCanvasElement} The map's `` element. @@ -823,13 +752,13 @@ class Map * @see [Example: Display a popup on hover](https://www.mapbox.com/mapbox-gl-js/example/popup-on-hover/) * @see [Example: Center the map on a clicked symbol](https://www.mapbox.com/mapbox-gl-js/example/center-on-symbol/) */ - getCanvas(): HTMLCanvasElement { - return this._canvas; - } + getCanvas(): HTMLCanvasElement { + return this._canvas; + } - /** @section {Map constraints} */ + /** @section {Map constraints} */ - /** + /** * Resizes the map according to the dimensions of its * `container` element. * @@ -847,39 +776,31 @@ class Map * const mapDiv = document.getElementById('map'); * if (mapDiv.style.visibility === true) map.resize(); */ - resize(eventData?: Object): this { - this._updateContainerDimensions(); + resize(eventData?: Object): this { + this._updateContainerDimensions(); - // do nothing if container remained the same size - if ( - this._containerWidth === this.transform.width && - this._containerHeight === this.transform.height - ) - return this; + // do nothing if container remained the same size + if (this._containerWidth === this.transform.width && this._containerHeight === this.transform.height) return this; - this._resizeCanvas(this._containerWidth, this._containerHeight); + this._resizeCanvas(this._containerWidth, this._containerHeight); - this.transform.resize(this._containerWidth, this._containerHeight); - this.painter.resize( - Math.ceil(this._containerWidth), - Math.ceil(this._containerHeight), - ); + this.transform.resize(this._containerWidth, this._containerHeight); + this.painter.resize(Math.ceil(this._containerWidth), Math.ceil(this._containerHeight)); - const fireMoving = !this._moving; - if (fireMoving) { - this.fire(new Event('movestart', eventData)).fire( - new Event('move', eventData), - ); - } + const fireMoving = !this._moving; + if (fireMoving) { + this.fire(new Event('movestart', eventData)) + .fire(new Event('move', eventData)); + } - this.fire(new Event('resize', eventData)); + this.fire(new Event('resize', eventData)); - if (fireMoving) this.fire(new Event('moveend', eventData)); + if (fireMoving) this.fire(new Event('moveend', eventData)); - return this; - } + return this; + } - /** + /** * Returns the map's geographical bounds. When the bearing or pitch is non-zero, the visible region is not * an axis-aligned rectangle, and the result is the smallest bounds that encompasses the visible region. * If a padding is set on the map, the bounds returned are for the inset. @@ -890,11 +811,11 @@ class Map * @example * const bounds = map.getBounds(); */ - getBounds(): LngLatBounds | null { - return this.transform.getBounds(); - } + getBounds(): LngLatBounds | null { + return this.transform.getBounds(); + } - /** + /** * Returns the maximum geographical bounds the map is constrained to, or `null` if none set. * * @returns {Map} The map object. @@ -902,11 +823,11 @@ class Map * @example * const maxBounds = map.getMaxBounds(); */ - getMaxBounds(): LngLatBounds | null { - return this.transform.getMaxBounds() || null; - } + getMaxBounds(): LngLatBounds | null { + return this.transform.getMaxBounds() || null; + } - /** + /** * Sets or clears the map's geographical bounds. * * Pan and zoom operations are constrained within these bounds. @@ -930,12 +851,12 @@ class Map * // Set the map's max bounds. * map.setMaxBounds(bounds); */ - setMaxBounds(bounds: LngLatBoundsLike): this { - this.transform.setMaxBounds(LngLatBounds.convert(bounds)); - return this._update(); - } + setMaxBounds(bounds: LngLatBoundsLike): this { + this.transform.setMaxBounds(LngLatBounds.convert(bounds)); + return this._update(); + } - /** + /** * Sets or clears the map's minimum zoom level. * If the map's current zoom level is lower than the new minimum, * the map will zoom to the new minimum. @@ -951,41 +872,37 @@ class Map * @example * map.setMinZoom(12.25); */ - setMinZoom(minZoom?: ?number): this { - minZoom = minZoom === null || minZoom === undefined ? - defaultMinZoom : - minZoom; - - if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) { - this.transform.minZoom = minZoom; - this._update(); - - if (this.getZoom() < minZoom) { - this.setZoom(minZoom); - } else { - this.fire(new Event('zoomstart')).fire(new Event('zoom')).fire( - new Event('zoomend'), - ); - } - - return this; - } else throw ( - new Error(`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`,) - ); - } - - /** + setMinZoom(minZoom?: ?number): this { + + minZoom = minZoom === null || minZoom === undefined ? defaultMinZoom : minZoom; + + if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) { + this.transform.minZoom = minZoom; + this._update(); + + if (this.getZoom() < minZoom) { + this.setZoom(minZoom); + } else { + this.fire(new Event('zoomstart')) + .fire(new Event('zoom')) + .fire(new Event('zoomend')); + } + + return this; + + } else throw new Error(`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`); + } + + /** * Returns the map's minimum allowable zoom level. * * @returns {number} Returns `minZoom`. * @example * const minZoom = map.getMinZoom(); */ - getMinZoom(): number { - return this.transform.minZoom; - } + getMinZoom(): number { return this.transform.minZoom; } - /** + /** * Sets or clears the map's maximum zoom level. * If the map's current zoom level is higher than the new maximum, * the map will zoom to the new maximum. @@ -996,39 +913,37 @@ class Map * @example * map.setMaxZoom(18.75); */ - setMaxZoom(maxZoom?: ?number): this { - maxZoom = maxZoom === null || maxZoom === undefined ? - defaultMaxZoom : - maxZoom; - - if (maxZoom >= this.transform.minZoom) { - this.transform.maxZoom = maxZoom; - this._update(); - - if (this.getZoom() > maxZoom) { - this.setZoom(maxZoom); - } else { - this.fire(new Event('zoomstart')).fire(new Event('zoom')).fire( - new Event('zoomend'), - ); - } - - return this; - } else throw new Error(`maxZoom must be greater than the current minZoom`); - } - - /** + setMaxZoom(maxZoom?: ?number): this { + + maxZoom = maxZoom === null || maxZoom === undefined ? defaultMaxZoom : maxZoom; + + if (maxZoom >= this.transform.minZoom) { + this.transform.maxZoom = maxZoom; + this._update(); + + if (this.getZoom() > maxZoom) { + this.setZoom(maxZoom); + } else { + this.fire(new Event('zoomstart')) + .fire(new Event('zoom')) + .fire(new Event('zoomend')); + } + + return this; + + } else throw new Error(`maxZoom must be greater than the current minZoom`); + } + + /** * Returns the map's maximum allowable zoom level. * * @returns {number} Returns `maxZoom`. * @example * const maxZoom = map.getMaxZoom(); */ - getMaxZoom(): number { - return this.transform.maxZoom; - } + getMaxZoom(): number { return this.transform.maxZoom; } - /** + /** * Sets or clears the map's minimum pitch. * If the map's current pitch is lower than the new minimum, * the map will pitch to the new minimum. @@ -1038,47 +953,41 @@ class Map * @example * map.setMinPitch(5); */ - setMinPitch(minPitch?: ?number): this { - minPitch = minPitch === null || minPitch === undefined ? - defaultMinPitch : - minPitch; - - if (minPitch < defaultMinPitch) { - throw ( - new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`,) - ); - } - - if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) { - this.transform.minPitch = minPitch; - this._update(); - - if (this.getPitch() < minPitch) { - this.setPitch(minPitch); - } else { - this.fire(new Event('pitchstart')).fire(new Event('pitch')).fire( - new Event('pitchend'), - ); - } - - return this; - } else throw ( - new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`,) - ); - } - - /** + setMinPitch(minPitch?: ?number): this { + + minPitch = minPitch === null || minPitch === undefined ? defaultMinPitch : minPitch; + + if (minPitch < defaultMinPitch) { + throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); + } + + if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) { + this.transform.minPitch = minPitch; + this._update(); + + if (this.getPitch() < minPitch) { + this.setPitch(minPitch); + } else { + this.fire(new Event('pitchstart')) + .fire(new Event('pitch')) + .fire(new Event('pitchend')); + } + + return this; + + } else throw new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`); + } + + /** * Returns the map's minimum allowable pitch. * * @returns {number} Returns `minPitch`. * @example * const minPitch = map.getMinPitch(); */ - getMinPitch(): number { - return this.transform.minPitch; - } + getMinPitch(): number { return this.transform.minPitch; } - /** + /** * Sets or clears the map's maximum pitch. * If the map's current pitch is higher than the new maximum, * the map will pitch to the new maximum. @@ -1089,45 +998,41 @@ class Map * @example * map.setMaxPitch(70); */ - setMaxPitch(maxPitch?: ?number): this { - maxPitch = maxPitch === null || maxPitch === undefined ? - defaultMaxPitch : - maxPitch; - - if (maxPitch > defaultMaxPitch) { - throw ( - new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`) - ); - } - - if (maxPitch >= this.transform.minPitch) { - this.transform.maxPitch = maxPitch; - this._update(); - - if (this.getPitch() > maxPitch) { - this.setPitch(maxPitch); - } else { - this.fire(new Event('pitchstart')).fire(new Event('pitch')).fire( - new Event('pitchend'), - ); - } - - return this; - } else throw new Error(`maxPitch must be greater than or equal to minPitch`); - } - - /** + setMaxPitch(maxPitch?: ?number): this { + + maxPitch = maxPitch === null || maxPitch === undefined ? defaultMaxPitch : maxPitch; + + if (maxPitch > defaultMaxPitch) { + throw new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`); + } + + if (maxPitch >= this.transform.minPitch) { + this.transform.maxPitch = maxPitch; + this._update(); + + if (this.getPitch() > maxPitch) { + this.setPitch(maxPitch); + } else { + this.fire(new Event('pitchstart')) + .fire(new Event('pitch')) + .fire(new Event('pitchend')); + } + + return this; + + } else throw new Error(`maxPitch must be greater than or equal to minPitch`); + } + + /** * Returns the map's maximum allowable pitch. * * @returns {number} Returns `maxPitch`. * @example * const maxPitch = map.getMaxPitch(); */ - getMaxPitch(): number { - return this.transform.maxPitch; - } + getMaxPitch(): number { return this.transform.maxPitch; } - /** + /** * Returns the state of `renderWorldCopies`. If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: * - When the map is zoomed out far enough that a single representation of the world does not fill the map's entire * container, there will be blank space beyond 180 and -180 degrees longitude. @@ -1139,11 +1044,9 @@ class Map * const worldCopiesRendered = map.getRenderWorldCopies(); * @see [Example: Render world copies](https://docs.mapbox.com/mapbox-gl-js/example/render-world-copies/) */ - getRenderWorldCopies(): boolean { - return this.transform.renderWorldCopies; - } + getRenderWorldCopies(): boolean { return this.transform.renderWorldCopies; } - /** + /** * Sets the state of `renderWorldCopies`. * * @param {boolean} renderWorldCopies If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: @@ -1158,15 +1061,15 @@ class Map * map.setRenderWorldCopies(true); * @see [Example: Render world copies](https://docs.mapbox.com/mapbox-gl-js/example/render-world-copies/) */ - setRenderWorldCopies(renderWorldCopies?: ?boolean): this { - this.transform.renderWorldCopies = renderWorldCopies; - if (!this.transform.renderWorldCopies) { - this._forceMarkerAndPopupUpdate(true); - } - return this._update(); - } - - /** + setRenderWorldCopies(renderWorldCopies?: ?boolean): this { + this.transform.renderWorldCopies = renderWorldCopies; + if (!this.transform.renderWorldCopies) { + this._forceMarkerAndPopupUpdate(true); + } + return this._update(); + } + + /** * Returns the map's language, which is used for translating map labels and UI components. * * @private @@ -1174,23 +1077,20 @@ class Map * @example * const language = map.getLanguage(); */ - getLanguage(): ?string | ?Array { - return this._language; - } - - _parseLanguage( - language?: 'auto' | ?string | ?Array, - ): ?string | ?Array { - if (language === 'auto') return window.navigator.language; - if (Array.isArray(language)) - return language.length === 0 ? - undefined : - language.map(l => l === 'auto' ? window.navigator.language : l); - - return language; - } - - /** + getLanguage(): ?string | ?string[] { + return this._language; + } + + _parseLanguage(language?: 'auto' | ?string | ?string[]): ?string | ?string[] { + if (language === 'auto') return window.navigator.language; + if (Array.isArray(language)) return language.length === 0 ? + undefined : + language.map(l => l === 'auto' ? window.navigator.language : l); + + return language; + } + + /** * Sets the map's language, which is used for translating map labels and UI components. * * @private @@ -1213,23 +1113,23 @@ class Map * @example * map.setLanguage(); */ - setLanguage(language?: 'auto' | ?string | ?Array): this { - const newLanguage = this._parseLanguage(language); - if (!this.style || newLanguage === this._language) return this; - this._language = newLanguage; + setLanguage(language?: 'auto' | ?string | ?string[]): this { + const newLanguage = this._parseLanguage(language); + if (!this.style || newLanguage === this._language) return this; + this._language = newLanguage; - this.style._reloadSources(); + this.style._reloadSources(); - for (const control of this._controls) { - if (control._setLanguage) { - control._setLanguage(this._language); - } - } + for (const control of this._controls) { + if (control._setLanguage) { + control._setLanguage(this._language); + } + } - return this; - } + return this; + } - /** + /** * Returns the code for the map's worldview. * * @private @@ -1237,11 +1137,11 @@ class Map * @example * const worldview = map.getWorldview(); */ - getWorldview(): ?string { - return this._worldview; - } + getWorldview(): ?string { + return this._worldview; + } - /** + /** * Sets the map's worldview. * * @private @@ -1257,32 +1157,32 @@ class Map * @example * map.setWorldView(); */ - setWorldview(worldview?: ?string): this { - if (!this.style || worldview === this._worldview) return this; + setWorldview(worldview?: ?string): this { + if (!this.style || worldview === this._worldview) return this; - this._worldview = worldview; - this.style._reloadSources(); + this._worldview = worldview; + this.style._reloadSources(); - return this; - } + return this; + } - /** @section {Point conversion} */ + /** @section {Point conversion} */ - /** + /** * Returns a [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) object that defines the current map projection. * * @returns {ProjectionSpecification} The [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) defining the current map projection. * @example * const projection = map.getProjection(); */ - getProjection(): ProjectionSpecification { - if (this.transform.mercatorFromTransition) { - return {name: "globe", center: [0, 0]}; - } - return this.transform.getProjection(); - } - - /** + getProjection(): ProjectionSpecification { + if (this.transform.mercatorFromTransition) { + return {name: "globe", center: [0, 0]}; + } + return this.transform.getProjection(); + } + + /** * Returns true if map [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) has been set to globe AND the map is at a low enough zoom level that globe view is enabled. * @private * @returns {boolean} Returns `globe-is-active` boolean. @@ -1291,11 +1191,9 @@ class Map * // do globe things here * } */ - _showingGlobe(): boolean { - return this.transform.projection.name === 'globe'; - } + _showingGlobe(): boolean { return this.transform.projection.name === 'globe'; } - /** + /** * Sets the map's projection. If called with `null` or `undefined`, the map will reset to Mercator. * * @param {ProjectionSpecification | string | null | undefined} projection The projection that the map should be rendered in. @@ -1311,87 +1209,78 @@ class Map * @see [Example: Display a web map using an alternate projection](https://docs.mapbox.com/mapbox-gl-js/example/map-projection/) * @see [Example: Use different map projections for web maps](https://docs.mapbox.com/mapbox-gl-js/example/projections/) */ - setProjection(projection?: ?ProjectionSpecification | string): this { - this._lazyInitEmptyStyle(); - - if (!projection) { - projection = null; - } else if (typeof projection === 'string') { - projection = (({name: projection}: any): ProjectionSpecification); - } - - this._useExplicitProjection = !!projection; - const stylesheetProjection = this.style.stylesheet ? - this.style.stylesheet.projection : - null; - return this._prioritizeAndUpdateProjection(projection, stylesheetProjection); - } - - _updateProjectionTransition() { - // The projection isn't globe, we can skip updating the transition - if (this.getProjection().name !== 'globe') { - return; - } - - const tr = this.transform; - const projection = tr.projection.name; - let projectionHasChanged; - - if (projection === 'globe' && tr.zoom >= GLOBE_ZOOM_THRESHOLD_MAX) { - tr.setMercatorFromTransition(); - projectionHasChanged = true; - } else if (projection === 'mercator' && tr.zoom < GLOBE_ZOOM_THRESHOLD_MAX) { - tr.setProjection({name: 'globe'}); - projectionHasChanged = true; - } - - if (projectionHasChanged) { - this.style.applyProjectionUpdate(); - this.style._forceSymbolLayerUpdate(); - } - } - - _prioritizeAndUpdateProjection( - explicitProjection: ?ProjectionSpecification, - styleProjection: ?ProjectionSpecification, - ): this { - // Given a stylesheet and eventual runtime projection, in order of priority, we select: - // 1. the explicit projection - // 2. the stylesheet projection - // 3. mercator (fallback) - const prioritizedProjection = explicitProjection || styleProjection || - {name: "mercator"}; - - return this._updateProjection(prioritizedProjection); - } - - _updateProjection(projection: ProjectionSpecification): this { - let projectionHasChanged; - - if ( - projection.name === 'globe' && - this.transform.zoom >= GLOBE_ZOOM_THRESHOLD_MAX - ) { - projectionHasChanged = this.transform.setMercatorFromTransition(); - } else { - projectionHasChanged = this.transform.setProjection(projection); - } - - this.style.applyProjectionUpdate(); - - if (projectionHasChanged) { - this.painter.clearBackgroundTiles(); - for (const id in this.style._sourceCaches) { - this.style._sourceCaches[id].clearTiles(); - } - this._update(true); - this._forceMarkerAndPopupUpdate(true); - } - - return this; - } - - /** + setProjection(projection?: ?ProjectionSpecification | string): this { + this._lazyInitEmptyStyle(); + + if (!projection) { + projection = null; + } else if (typeof projection === 'string') { + projection = (({name: projection}: any): ProjectionSpecification); + } + + this._useExplicitProjection = !!projection; + const stylesheetProjection = this.style.stylesheet ? this.style.stylesheet.projection : null; + return this._prioritizeAndUpdateProjection(projection, stylesheetProjection); + } + + _updateProjectionTransition() { + // The projection isn't globe, we can skip updating the transition + if (this.getProjection().name !== 'globe') { + return; + } + + const tr = this.transform; + const projection = tr.projection.name; + let projectionHasChanged; + + if (projection === 'globe' && tr.zoom >= GLOBE_ZOOM_THRESHOLD_MAX) { + tr.setMercatorFromTransition(); + projectionHasChanged = true; + } else if (projection === 'mercator' && tr.zoom < GLOBE_ZOOM_THRESHOLD_MAX) { + tr.setProjection({name: 'globe'}); + projectionHasChanged = true; + } + + if (projectionHasChanged) { + this.style.applyProjectionUpdate(); + this.style._forceSymbolLayerUpdate(); + } + } + + _prioritizeAndUpdateProjection(explicitProjection: ?ProjectionSpecification, styleProjection: ?ProjectionSpecification): this { + // Given a stylesheet and eventual runtime projection, in order of priority, we select: + // 1. the explicit projection + // 2. the stylesheet projection + // 3. mercator (fallback) + const prioritizedProjection = explicitProjection || styleProjection || {name: "mercator"}; + + return this._updateProjection(prioritizedProjection); + } + + _updateProjection(projection: ProjectionSpecification): this { + let projectionHasChanged; + + if (projection.name === 'globe' && this.transform.zoom >= GLOBE_ZOOM_THRESHOLD_MAX) { + projectionHasChanged = this.transform.setMercatorFromTransition(); + } else { + projectionHasChanged = this.transform.setProjection(projection); + } + + this.style.applyProjectionUpdate(); + + if (projectionHasChanged) { + this.painter.clearBackgroundTiles(); + for (const id in this.style._sourceCaches) { + this.style._sourceCaches[id].clearTiles(); + } + this._update(true); + this._forceMarkerAndPopupUpdate(true); + } + + return this; + } + + /** * Returns a {@link Point} representing pixel coordinates, relative to the map's `container`, * that correspond to the specified geographical location. * @@ -1405,11 +1294,11 @@ class Map * const coordinate = [-122.420679, 37.772537]; * const point = map.project(coordinate); */ - project(lnglat: LngLatLike): Point { - return this.transform.locationPoint3D(LngLat.convert(lnglat)); - } + project(lnglat: LngLatLike): Point { + return this.transform.locationPoint3D(LngLat.convert(lnglat)); + } - /** + /** * Returns a {@link LngLat} representing geographical coordinates that correspond * to the specified pixel coordinates. If horizon is visible, and specified pixel is * above horizon, returns a {@link LngLat} corresponding to point on horizon, nearest @@ -1423,131 +1312,106 @@ class Map * const coordinate = map.unproject(e.point); * }); */ - unproject(point: PointLike): LngLat { - return this.transform.pointLocation3D(Point.convert(point)); - } + unproject(point: PointLike): LngLat { + return this.transform.pointLocation3D(Point.convert(point)); + } - /** @section {Movement state} */ + /** @section {Movement state} */ - /** + /** * Returns true if the map is panning, zooming, rotating, or pitching due to a camera animation or user gesture. * * @returns {boolean} True if the map is moving. * @example * const isMoving = map.isMoving(); */ - isMoving(): boolean { - return this._moving || (this.handlers && this.handlers.isMoving()) || false; - } + isMoving(): boolean { + return this._moving || (this.handlers && this.handlers.isMoving()) || false; + } - /** + /** * Returns true if the map is zooming due to a camera animation or user gesture. * * @returns {boolean} True if the map is zooming. * @example * const isZooming = map.isZooming(); */ - isZooming(): boolean { - return this._zooming || (this.handlers && this.handlers.isZooming()) || false; - } + isZooming(): boolean { + return this._zooming || (this.handlers && this.handlers.isZooming()) || false; + } - /** + /** * Returns true if the map is rotating due to a camera animation or user gesture. * * @returns {boolean} True if the map is rotating. * @example * map.isRotating(); */ - isRotating(): boolean { - return this._rotating || (this.handlers && this.handlers.isRotating()) || false; - } - - _isDragging(): boolean { - return (this.handlers && this.handlers._isDragging()) || false; - } - - _createDelegatedListener( - type: MapEvent, - layers: Array, - listener: any, - ): any { - if (type === 'mouseenter' || type === 'mouseover') { - let mousein = false; - const mousemove = (e => { - const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); - const features = filteredLayers.length ? - this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : - []; - if (!features.length) { - mousein = false; - } else if (!mousein) { - mousein = true; - listener.call( - this, - new MapMouseEvent(type, this, e.originalEvent, {features}), - ); - } - }); - const mouseout = (() => { - mousein = false; - }); - - return { - layers: new Set(layers), - listener, - delegates: {mousemove, mouseout}, - }; - } else if (type === 'mouseleave' || type === 'mouseout') { - let mousein = false; - const mousemove = (e => { - const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); - const features = filteredLayers.length ? - this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : - []; - if (features.length) { - mousein = true; - } else if (mousein) { - mousein = false; - listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); - } - }); - const mouseout = (e => { - if (mousein) { - mousein = false; - listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); - } - }); - - return { - layers: new Set(layers), - listener, - delegates: {mousemove, mouseout}, - }; - } else { - const delegate = (e => { - const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); - const features = filteredLayers.length ? - this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : - []; - if (features.length) { - // Here we need to mutate the original event, so that preventDefault works as expected. - e.features = features; - listener.call(this, e); - delete e.features; - } - }); - - return { - layers: new Set(layers), - listener, - delegates: {[ (type: string) ]: delegate}, - }; - } - } - - /** @section {Working with events} */ - - /** + isRotating(): boolean { + return this._rotating || (this.handlers && this.handlers.isRotating()) || false; + } + + _isDragging(): boolean { + return (this.handlers && this.handlers._isDragging()) || false; + } + + _createDelegatedListener(type: MapEvent, layers: Array, listener: any): any { + if (type === 'mouseenter' || type === 'mouseover') { + let mousein = false; + const mousemove = (e) => { + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; + if (!features.length) { + mousein = false; + } else if (!mousein) { + mousein = true; + listener.call(this, new MapMouseEvent(type, this, e.originalEvent, {features})); + } + }; + const mouseout = () => { + mousein = false; + }; + + return {layers: new Set(layers), listener, delegates: {mousemove, mouseout}}; + } else if (type === 'mouseleave' || type === 'mouseout') { + let mousein = false; + const mousemove = (e) => { + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; + if (features.length) { + mousein = true; + } else if (mousein) { + mousein = false; + listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); + } + }; + const mouseout = (e) => { + if (mousein) { + mousein = false; + listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); + } + }; + + return {layers: new Set(layers), listener, delegates: {mousemove, mouseout}}; + } else { + const delegate = (e) => { + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; + if (features.length) { + // Here we need to mutate the original event, so that preventDefault works as expected. + e.features = features; + listener.call(this, e); + delete e.features; + } + }; + + return {layers: new Set(layers), listener, delegates: {[(type: string)]: delegate}}; + } + } + + /** @section {Working with events} */ + + /** * Adds a listener for events of a specified type, * optionally limited to features in a specified style layer. * @@ -1660,32 +1524,28 @@ class Map * @see [Example: Create a hover effect](https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/) * @see [Example: Display popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) */ - on(type: MapEvent, layerIds: any, listener: any): this { - if (listener === undefined) { - return super.on(type, layerIds); - } - - if (!Array.isArray(layerIds)) { - layerIds = [layerIds]; - } - const delegatedListener = this._createDelegatedListener( - type, - layerIds, - listener, - ); - - this._delegatedListeners = this._delegatedListeners || {}; - this._delegatedListeners[type] = this._delegatedListeners[type] || []; - this._delegatedListeners[type].push(delegatedListener); - - for (const event in delegatedListener.delegates) { - this.on((event: any), delegatedListener.delegates[event]); - } - - return this; - } - - /** + on(type: MapEvent, layerIds: any, listener: any): this { + if (listener === undefined) { + return super.on(type, layerIds); + } + + if (!Array.isArray(layerIds)) { + layerIds = [layerIds]; + } + const delegatedListener = this._createDelegatedListener(type, layerIds, listener); + + this._delegatedListeners = this._delegatedListeners || {}; + this._delegatedListeners[type] = this._delegatedListeners[type] || []; + this._delegatedListeners[type].push(delegatedListener); + + for (const event in delegatedListener.delegates) { + this.on((event: any), delegatedListener.delegates[event]); + } + + return this; + } + + /** * Adds a listener that will be called only once to a specified event type, * optionally limited to events occurring on features in a specified style layer. * @@ -1724,28 +1584,25 @@ class Map * @see [Example: Animate the camera around a point with 3D terrain](https://docs.mapbox.com/mapbox-gl-js/example/free-camera-point/) * @see [Example: Play map locations as a slideshow](https://docs.mapbox.com/mapbox-gl-js/example/playback-locations/) */ - once(type: MapEvent, layerIds: any, listener: any): this | Promise { - if (listener === undefined) { - return super.once(type, layerIds); - } - - if (!Array.isArray(layerIds)) { - layerIds = [layerIds]; - } - const delegatedListener = this._createDelegatedListener( - type, - layerIds, - listener, - ); - - for (const event in delegatedListener.delegates) { - this.once((event: any), delegatedListener.delegates[event]); - } - - return this; - } - - /** + once(type: MapEvent, layerIds: any, listener: any): this | Promise { + + if (listener === undefined) { + return super.once(type, layerIds); + } + + if (!Array.isArray(layerIds)) { + layerIds = [layerIds]; + } + const delegatedListener = this._createDelegatedListener(type, layerIds, listener); + + for (const event in delegatedListener.delegates) { + this.once((event: any), delegatedListener.delegates[event]); + } + + return this; + } + + /** * Removes an event listener previously added with {@link Map#on}, * optionally limited to layer-specific events. * @@ -1770,54 +1627,48 @@ class Map * }); * @see [Example: Create a draggable point](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-point/) */ - off(type: MapEvent, layerIds: any, listener: any): this { - if (listener === undefined) { - return super.off(type, layerIds); - } - - layerIds = new Set(Array.isArray(layerIds) ? layerIds : [layerIds]); - const areLayerArraysEqual = ((hash1, hash2) => { - if (hash1.size !== hash2.size) { - return false; // at-least 1 arr has duplicate value(s) - - } - - // comparing values - for (const value of hash1) { - if (!hash2.has(value)) return false; - } - return true; - }); - - const removeDelegatedListeners = ((listeners: Array) => { - for (let i = 0; i < listeners.length; i++) { - const delegatedListener = listeners[i]; - if ( - delegatedListener.listener === listener && - areLayerArraysEqual(delegatedListener.layers, layerIds) - ) { - for (const event in delegatedListener.delegates) { - this.off((event: any), delegatedListener.delegates[event]); - } - listeners.splice(i, 1); - return this; - } - } - }); - - const delegatedListeners = this._delegatedListeners ? - this._delegatedListeners[type] : - undefined; - if (delegatedListeners) { - removeDelegatedListeners(delegatedListeners); - } - - return this; - } - - /** @section {Querying features} */ - - /** + off(type: MapEvent, layerIds: any, listener: any): this { + if (listener === undefined) { + return super.off(type, layerIds); + } + + layerIds = new Set(Array.isArray(layerIds) ? layerIds : [layerIds]); + const areLayerArraysEqual = (hash1, hash2) => { + if (hash1.size !== hash2.size) { + return false; // at-least 1 arr has duplicate value(s) + } + + // comparing values + for (const value of hash1) { + if (!hash2.has(value)) return false; + } + return true; + }; + + const removeDelegatedListeners = (listeners: Array) => { + for (let i = 0; i < listeners.length; i++) { + const delegatedListener = listeners[i]; + if (delegatedListener.listener === listener && areLayerArraysEqual(delegatedListener.layers, layerIds)) { + for (const event in delegatedListener.delegates) { + this.off((event: any), delegatedListener.delegates[event]); + } + listeners.splice(i, 1); + return this; + } + } + }; + + const delegatedListeners = this._delegatedListeners ? this._delegatedListeners[type] : undefined; + if (delegatedListeners) { + removeDelegatedListeners(delegatedListeners); + } + + return this; + } + + /** @section {Querying features} */ + + /** * Returns an array of [GeoJSON](http://geojson.org/) * [Feature objects](https://tools.ietf.org/html/rfc7946#section-3.2) * representing visible features that satisfy the query parameters. @@ -1897,43 +1748,32 @@ class Map * @see [Example: Highlight features within a bounding box](https://www.mapbox.com/mapbox-gl-js/example/using-box-queryrenderedfeatures/) * @see [Example: Filter features within map view](https://www.mapbox.com/mapbox-gl-js/example/filter-features-within-map-view/) */ - queryRenderedFeatures( - geometry?: PointLike | [PointLike, PointLike], - options?: Object, - ): Array { - // The first parameter can be omitted entirely, making this effectively an overloaded method - // with two signatures: - // - // queryRenderedFeatures(geometry: PointLike | [PointLike, PointLike], options?: Object) - // queryRenderedFeatures(options?: Object) - // - // There no way to express that in a way that's compatible with both flow and documentation.js. - // Related: https://github.com/facebook/flow/issues/1556 - - if (!this.style) { - return []; - } - - if ( - options === undefined && geometry !== undefined && - !(geometry instanceof Point) && - !Array.isArray(geometry) - ) { - options = (geometry: Object); - geometry = undefined; - } - - options = options || {}; - geometry = geometry || - [ - ([0, 0]: PointLike), - ([this.transform.width, this.transform.height]: PointLike), - ]; - - return this.style.queryRenderedFeatures(geometry, options, this.transform); - } - - /** + queryRenderedFeatures(geometry?: PointLike | [PointLike, PointLike], options?: Object): Array { + // The first parameter can be omitted entirely, making this effectively an overloaded method + // with two signatures: + // + // queryRenderedFeatures(geometry: PointLike | [PointLike, PointLike], options?: Object) + // queryRenderedFeatures(options?: Object) + // + // There no way to express that in a way that's compatible with both flow and documentation.js. + // Related: https://github.com/facebook/flow/issues/1556 + + if (!this.style) { + return []; + } + + if (options === undefined && geometry !== undefined && !(geometry instanceof Point) && !Array.isArray(geometry)) { + options = (geometry: Object); + geometry = undefined; + } + + options = options || {}; + geometry = geometry || [([0, 0]: PointLike), ([this.transform.width, this.transform.height]: PointLike)]; + + return this.style.queryRenderedFeatures(geometry, options, this.transform); + } + + /** * Returns an array of [GeoJSON](http://geojson.org/) * [Feature objects](https://tools.ietf.org/html/rfc7946#section-3.2) * representing features within the specified vector tile or GeoJSON source that satisfy the query parameters. @@ -1970,20 +1810,13 @@ class Map * * @see [Example: Highlight features containing similar data](https://www.mapbox.com/mapbox-gl-js/example/query-similar-features/) */ - querySourceFeatures( - sourceId: string, - parameters: ?{ - sourceLayer: ?string, - filter: ?Array, - validate?: boolean, - }, - ): Array { - return this.style.querySourceFeatures(sourceId, parameters); - } - - /** @section {Working with styles} */ - - /** + querySourceFeatures(sourceId: string, parameters: ?{sourceLayer: ?string, filter: ?Array, validate?: boolean}): Array { + return this.style.querySourceFeatures(sourceId, parameters); + } + + /** @section {Working with styles} */ + + /** * Updates the map's Mapbox style object with a new value. * * If a style is already set when this is used and the `diff` option is set to `true`, the map renderer will attempt to compare the given style @@ -2009,119 +1842,89 @@ class Map * * @see [Example: Change a map's style](https://www.mapbox.com/mapbox-gl-js/example/setstyle/) */ - setStyle( - style: StyleSpecification | string | null, - options?: { diff?: boolean } & StyleOptions, - ): this { - options = extend( - {}, - { - localIdeographFontFamily: this._localIdeographFontFamily, - localFontFamily: this._localFontFamily, - }, - options, - ); - - if ( - options.diff !== false && - options.localIdeographFontFamily === this._localIdeographFontFamily && - options.localFontFamily === this._localFontFamily && - this.style && - style - ) { - this._diffStyle(style, options); - return this; - } else { - this._localIdeographFontFamily = options.localIdeographFontFamily; - this._localFontFamily = options.localFontFamily; - return this._updateStyle(style, options); - } - } - - _getUIString(key: string): string { - const str = this._locale[key]; - if (str == null) { - throw new Error(`Missing UI string '${key}'`); - } - - return str; - } - - _updateStyle( - style: StyleSpecification | string | null, - options?: { diff?: boolean } & StyleOptions, - ): this { - if (this.style) { - this.style.setEventedParent(null); - this.style._remove(); - this.style = (undefined: any); // we lazy-init it so it's never undefined when accessed - - } - - if (style) { - this.style = new Style(this, options || {}); - this.style.setEventedParent(this, {style: this.style}); - - if (typeof style === 'string') { - this.style.loadURL(style); - } else { - this.style.loadJSON(style); - } - } - this._updateTerrain(); - return this; - } - - _lazyInitEmptyStyle() { - if (!this.style) { - this.style = new Style(this, {}); - this.style.setEventedParent(this, {style: this.style}); - this.style.loadEmpty(); - } - } - - _diffStyle( - style: StyleSpecification | string, - options?: { diff?: boolean } & StyleOptions, - ) { - if (typeof style === 'string') { - const url = this._requestManager.normalizeStyleURL(style); - const request = this._requestManager.transformRequest( - url, - ResourceType.Style, - ); - getJSON( - request, - (error: ?Error, json: ?Object) => { - if (error) { - this.fire(new ErrorEvent(error)); - } else if (json) { - this._updateDiff(json, options); + setStyle(style: StyleSpecification | string | null, options?: {diff?: boolean} & StyleOptions): this { + options = extend({}, {localIdeographFontFamily: this._localIdeographFontFamily, localFontFamily: this._localFontFamily}, options); + + if ((options.diff !== false && + options.localIdeographFontFamily === this._localIdeographFontFamily && + options.localFontFamily === this._localFontFamily) && this.style && style) { + this._diffStyle(style, options); + return this; + } else { + this._localIdeographFontFamily = options.localIdeographFontFamily; + this._localFontFamily = options.localFontFamily; + return this._updateStyle(style, options); + } + } + + _getUIString(key: string): string { + const str = this._locale[key]; + if (str == null) { + throw new Error(`Missing UI string '${key}'`); + } + + return str; + } + + _updateStyle(style: StyleSpecification | string | null, options?: {diff?: boolean} & StyleOptions): this { + if (this.style) { + this.style.setEventedParent(null); + this.style._remove(); + this.style = (undefined: any); // we lazy-init it so it's never undefined when accessed + } + + if (style) { + this.style = new Style(this, options || {}); + this.style.setEventedParent(this, {style: this.style}); + + if (typeof style === 'string') { + this.style.loadURL(style); + } else { + this.style.loadJSON(style); } - }, - ); - } else if (typeof style === 'object') { - this._updateDiff(style, options); - } - } - - _updateDiff( - style: StyleSpecification, - options?: { diff?: boolean } & StyleOptions, - ) { - try { - if (this.style.setState(style)) { - this._update(true); - } - } catch (e) { - warnOnce( - `Unable to perform style diff: ${e.message || e.error || e}. Rebuilding the style from scratch.`, - ); - this._updateStyle(style, options); - } - } - - /** + } + this._updateTerrain(); + return this; + } + + _lazyInitEmptyStyle() { + if (!this.style) { + this.style = new Style(this, {}); + this.style.setEventedParent(this, {style: this.style}); + this.style.loadEmpty(); + } + } + + _diffStyle(style: StyleSpecification | string, options?: {diff?: boolean} & StyleOptions) { + if (typeof style === 'string') { + const url = this._requestManager.normalizeStyleURL(style); + const request = this._requestManager.transformRequest(url, ResourceType.Style); + getJSON(request, (error: ?Error, json: ?Object) => { + if (error) { + this.fire(new ErrorEvent(error)); + } else if (json) { + this._updateDiff(json, options); + } + }); + } else if (typeof style === 'object') { + this._updateDiff(style, options); + } + } + + _updateDiff(style: StyleSpecification, options?: {diff?: boolean} & StyleOptions) { + try { + if (this.style.setState(style)) { + this._update(true); + } + } catch (e) { + warnOnce( + `Unable to perform style diff: ${e.message || e.error || e}. Rebuilding the style from scratch.` + ); + this._updateStyle(style, options); + } + } + + /** * Returns the map's Mapbox [style](https://docs.mapbox.com/help/glossary/style/) object, a JSON object which can be used to recreate the map's style. * * @returns {Object} The map's style JSON object. @@ -2132,13 +1935,13 @@ class Map * }); * */ - getStyle(): ?StyleSpecification { - if (this.style) { - return this.style.serialize(); - } - } + getStyle(): ?StyleSpecification { + if (this.style) { + return this.style.serialize(); + } + } - /** + /** * Returns a Boolean indicating whether the map's style is fully loaded. * * @returns {boolean} A Boolean indicating whether the style is fully loaded. @@ -2146,17 +1949,17 @@ class Map * @example * const styleLoadStatus = map.isStyleLoaded(); */ - isStyleLoaded(): boolean { - if (!this.style) { - warnOnce('There is no style added to the map.'); - return false; - } - return this.style.loaded(); - } + isStyleLoaded(): boolean { + if (!this.style) { + warnOnce('There is no style added to the map.'); + return false; + } + return this.style.loaded(); + } - /** @section {Sources} */ + /** @section {Sources} */ - /** + /** * Adds a source to the map's style. * * @param {string} id The ID of the source to add. Must not conflict with existing sources. @@ -2188,13 +1991,13 @@ class Map * @see Example: GeoJSON source: [Add live realtime data](https://docs.mapbox.com/mapbox-gl-js/example/live-geojson/) * @see Example: Raster DEM source: [Add hillshading](https://docs.mapbox.com/mapbox-gl-js/example/hillshade/) */ - addSource(id: string, source: SourceSpecification): this { - this._lazyInitEmptyStyle(); - this.style.addSource(id, source); - return this._update(true); - } + addSource(id: string, source: SourceSpecification): this { + this._lazyInitEmptyStyle(); + this.style.addSource(id, source); + return this._update(true); + } - /** + /** * Returns a Boolean indicating whether the source is loaded. Returns `true` if the source with * the given ID in the map's style has no outstanding network requests, otherwise `false`. * @@ -2203,11 +2006,11 @@ class Map * @example * const sourceLoaded = map.isSourceLoaded('bathymetry-data'); */ - isSourceLoaded(id: string): boolean { - return !!this.style && this.style._isSourceCacheLoaded(id); - } + isSourceLoaded(id: string): boolean { + return !!this.style && this.style._isSourceCacheLoaded(id); + } - /** + /** * Returns a Boolean indicating whether all tiles in the viewport from all sources on * the style are loaded. * @@ -2216,21 +2019,20 @@ class Map * const tilesLoaded = map.areTilesLoaded(); */ - areTilesLoaded(): boolean { - const sources = this.style && this.style._sourceCaches; - for (const id in sources) { - const source = sources[id]; - const tiles = source._tiles; - for (const t in tiles) { - const tile = tiles[t]; - if (!(tile.state === 'loaded' || tile.state === 'errored')) - return false; - } - } - return true; - } - - /** + areTilesLoaded(): boolean { + const sources = this.style && this.style._sourceCaches; + for (const id in sources) { + const source = sources[id]; + const tiles = source._tiles; + for (const t in tiles) { + const tile = tiles[t]; + if (!(tile.state === 'loaded' || tile.state === 'errored')) return false; + } + } + return true; + } + + /** * Adds a [custom source type](#Custom Sources), making it available for use with * {@link Map#addSource}. * @private @@ -2238,12 +2040,12 @@ class Map * @param {Function} SourceType A {@link Source} constructor. * @param {Function} callback Called when the source type is ready or with an error argument if there is an error. */ - addSourceType(name: string, SourceType: any, callback: Function) { - this._lazyInitEmptyStyle(); - this.style.addSourceType(name, SourceType, callback); - } + addSourceType(name: string, SourceType: any, callback: Function) { + this._lazyInitEmptyStyle(); + this.style.addSourceType(name, SourceType, callback); + } - /** + /** * Removes a source from the map's style. * * @param {string} id The ID of the source to remove. @@ -2251,13 +2053,13 @@ class Map * @example * map.removeSource('bathymetry-data'); */ - removeSource(id: string): this { - this.style.removeSource(id); - this._updateTerrain(); - return this._update(true); - } + removeSource(id: string): this { + this.style.removeSource(id); + this._updateTerrain(); + return this._update(true); + } - /** + /** * Returns the source with the specified ID in the map's style. * * This method is often used to update a source using the instance members for the relevant @@ -2277,14 +2079,14 @@ class Map * @see [Example: Animate a point](https://docs.mapbox.com/mapbox-gl-js/example/animate-point-along-line/) * @see [Example: Add live realtime data](https://docs.mapbox.com/mapbox-gl-js/example/live-geojson/) */ - getSource(id: string): ?Source { - return this.style.getSource(id); - } + getSource(id: string): ?Source { + return this.style.getSource(id); + } - /** @section {Images} */ + /** @section {Images} */ - // eslint-disable-next-line jsdoc/require-returns - /** + // eslint-disable-next-line jsdoc/require-returns + /** * Add an image to the style. This image can be displayed on the map like any other icon in the style's * [sprite](https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/) using the image's ID with * [`icon-image`](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-icon-image), @@ -2328,55 +2130,43 @@ class Map * @see Example: Use `HTMLImageElement`: [Add an icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image/) * @see Example: Use `ImageData`: [Add a generated icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image-generated/) */ - addImage( - id: string, - image: | HTMLImageElement - | ImageBitmap - | ImageData - | { width: number, height: number, data: Uint8Array | Uint8ClampedArray } - | StyleImageInterface, - { - pixelRatio = 1, - sdf = false, - stretchX, - stretchY, - content - }: $Shape = {}, - ) { - this._lazyInitEmptyStyle(); - const version = 0; - - if (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) { - const {width, height, data} = browser.getImageData(image); - this.style.addImage(id, {data: new RGBAImage({width, height}, data), pixelRatio, stretchX, stretchY, content, sdf, version}); - } else if (image.width === undefined || image.height === undefined) { - this.fire(new ErrorEvent(new Error( - 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + - 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); - } else { - const {width, height} = image; - const userImage = ((image: any): StyleImageInterface); - const data = userImage.data; - - this.style.addImage(id, { - data: new RGBAImage({width, height}, new Uint8Array(data)), - pixelRatio, - stretchX, - stretchY, - content, - sdf, - version, - userImage - }); - - if (userImage.onAdd) { - userImage.onAdd(this, id); - } - } - } - - // eslint-disable-next-line jsdoc/require-returns - /** + addImage(id: string, + image: HTMLImageElement | ImageBitmap | ImageData | {width: number, height: number, data: Uint8Array | Uint8ClampedArray} | StyleImageInterface, + {pixelRatio = 1, sdf = false, stretchX, stretchY, content}: $Shape = {}) { + this._lazyInitEmptyStyle(); + const version = 0; + + if (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) { + const {width, height, data} = browser.getImageData(image); + this.style.addImage(id, {data: new RGBAImage({width, height}, data), pixelRatio, stretchX, stretchY, content, sdf, version}); + } else if (image.width === undefined || image.height === undefined) { + this.fire(new ErrorEvent(new Error( + 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); + } else { + const {width, height} = image; + const userImage = ((image: any): StyleImageInterface); + const data = userImage.data; + + this.style.addImage(id, { + data: new RGBAImage({width, height}, new Uint8Array(data)), + pixelRatio, + stretchX, + stretchY, + content, + sdf, + version, + userImage + }); + + if (userImage.onAdd) { + userImage.onAdd(this, id); + } + } + } + + // eslint-disable-next-line jsdoc/require-returns + /** * Update an existing image in a style. This image can be displayed on the map like any other icon in the style's * [sprite](https://docs.mapbox.com/help/glossary/sprite/) using the image's ID with * [`icon-image`](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-icon-image), @@ -2397,53 +2187,42 @@ class Map * if (map.hasImage('cat')) map.updateImage('cat', image); * }); */ - updateImage( - id: string, - image: | HTMLImageElement - | ImageBitmap - | ImageData - | { width: number, height: number, data: Uint8Array | Uint8ClampedArray } - | StyleImageInterface, - ) { - const existingImage = this.style.getImage(id); - if (!existingImage) { - this.fire( - new ErrorEvent( - new Error( - 'The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.', - ), - ), - ); - return; - } - const imageData = (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) ? browser.getImageData(image) : image; - - const {width, height} = imageData; - // Flow can't refine the type enough to exclude ImageBitmap - const data = ((imageData: any).data: Uint8Array | Uint8ClampedArray); - - if (width === undefined || height === undefined) { - this.fire(new ErrorEvent(new Error( - 'Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + - 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); - return; - } - - if (width !== existingImage.data.width || height !== existingImage.data.height) { - this.fire(new ErrorEvent(new Error( - `The width and height of the updated image (${width}, ${height}) + updateImage(id: string, + image: HTMLImageElement | ImageBitmap | ImageData | {width: number, height: number, data: Uint8Array | Uint8ClampedArray} | StyleImageInterface) { + + const existingImage = this.style.getImage(id); + if (!existingImage) { + this.fire(new ErrorEvent(new Error( + 'The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.'))); + return; + } + const imageData = (image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)) ? browser.getImageData(image) : image; + const {width, height} = imageData; + // Flow can't refine the type enough to exclude ImageBitmap + const data = ((imageData: any).data: Uint8Array | Uint8ClampedArray); + + if (width === undefined || height === undefined) { + this.fire(new ErrorEvent(new Error( + 'Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, ' + + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`'))); + return; + } + + if (width !== existingImage.data.width || height !== existingImage.data.height) { + this.fire(new ErrorEvent(new Error( + `The width and height of the updated image (${width}, ${height}) must be that same as the previous version of the image (${existingImage.data.width}, ${existingImage.data.height})`))); - return; - } + return; + } - const copy = !(image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)); - existingImage.data.replace(data, copy); + const copy = !(image instanceof window.HTMLImageElement || (window.ImageBitmap && image instanceof window.ImageBitmap)); + existingImage.data.replace(data, copy); - this.style.updateImage(id, existingImage); - } + this.style.updateImage(id, existingImage); + } - /** + /** * Check whether or not an image with a specific ID exists in the style. This checks both images * in the style's original [sprite](https://docs.mapbox.com/help/glossary/sprite/) and any images * that have been added at runtime using {@link Map#addImage}. @@ -2456,16 +2235,16 @@ class Map * // the style's sprite. * const catIconExists = map.hasImage('cat'); */ - hasImage(id: string): boolean { - if (!id) { - this.fire(new ErrorEvent(new Error('Missing required image id'))); - return false; - } + hasImage(id: string): boolean { + if (!id) { + this.fire(new ErrorEvent(new Error('Missing required image id'))); + return false; + } - return !!this.style.getImage(id); - } + return !!this.style.getImage(id); + } - /** + /** * Remove an image from a style. This can be an image from the style's original * [sprite](https://docs.mapbox.com/help/glossary/sprite/) or any images * that have been added at runtime using {@link Map#addImage}. @@ -2477,11 +2256,11 @@ class Map * // the style's sprite, remove it. * if (map.hasImage('cat')) map.removeImage('cat'); */ - removeImage(id: string) { - this.style.removeImage(id); - } + removeImage(id: string) { + this.style.removeImage(id); + } - /** + /** * Load an image from an external URL to be used with {@link Map#addImage}. External * domains must support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). * @@ -2498,21 +2277,13 @@ class Map * * @see [Example: Add an icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image/) */ - loadImage(url: string, callback: Function) { - getImage( - this._requestManager.transformRequest(url, ResourceType.Image), - (err, img) => { - callback( - err, - img instanceof window.HTMLImageElement ? - browser.getImageData(img) : - img, - ); - }, - ); - } - - /** + loadImage(url: string, callback: Function) { + getImage(this._requestManager.transformRequest(url, ResourceType.Image), (err, img) => { + callback(err, img instanceof window.HTMLImageElement ? browser.getImageData(img) : img); + }); + } + + /** * Returns an Array of strings containing the IDs of all images currently available in the map. * This includes both images from the style's original [sprite](https://docs.mapbox.com/help/glossary/sprite/) * and any images that have been added at runtime using {@link Map#addImage}. @@ -2523,13 +2294,13 @@ class Map * const allImages = map.listImages(); * */ - listImages(): Array { - return this.style.listImages(); - } + listImages(): Array { + return this.style.listImages(); + } - /** @section {Layers} */ + /** @section {Layers} */ - /** + /** * Adds a [Mapbox style layer](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) * to the map's style. * @@ -2640,16 +2411,13 @@ class Map * @see [Example: Add a vector tile source](https://docs.mapbox.com/mapbox-gl-js/example/vector-source/) (line layer) * @see [Example: Add a WMS layer](https://docs.mapbox.com/mapbox-gl-js/example/wms/) (raster layer) */ - addLayer( - layer: LayerSpecification | CustomLayerInterface, - beforeId?: string, - ): this { - this._lazyInitEmptyStyle(); - this.style.addLayer(layer, beforeId); - return this._update(true); - } - - /** + addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string): this { + this._lazyInitEmptyStyle(); + this.style.addLayer(layer, beforeId); + return this._update(true); + } + + /** * Moves a layer to a different z-position. * * @param {string} id The ID of the layer to move. @@ -2660,12 +2428,12 @@ class Map * // Move a layer with ID 'polygon' before the layer with ID 'country-label'. The `polygon` layer will appear beneath the `country-label` layer on the map. * map.moveLayer('polygon', 'country-label'); */ - moveLayer(id: string, beforeId?: string): this { - this.style.moveLayer(id, beforeId); - return this._update(true); - } + moveLayer(id: string, beforeId?: string): this { + this.style.moveLayer(id, beforeId); + return this._update(true); + } - /** + /** * Removes the layer with the given ID from the map's style. * * If no such layer exists, an `error` event is fired. @@ -2678,12 +2446,12 @@ class Map * // If a layer with ID 'state-data' exists, remove it. * if (map.getLayer('state-data')) map.removeLayer('state-data'); */ - removeLayer(id: string): this { - this.style.removeLayer(id); - return this._update(true); - } + removeLayer(id: string): this { + this.style.removeLayer(id); + return this._update(true); + } - /** + /** * Returns the layer with the specified ID in the map's style. * * @param {string} id The ID of the layer to get. @@ -2696,11 +2464,11 @@ class Map * @see [Example: Filter symbols by toggling a list](https://www.mapbox.com/mapbox-gl-js/example/filter-markers/) * @see [Example: Filter symbols by text input](https://www.mapbox.com/mapbox-gl-js/example/filter-markers-by-input/) */ - getLayer(id: string): ?StyleLayer { - return this.style.getLayer(id); - } + getLayer(id: string): ?StyleLayer { + return this.style.getLayer(id); + } - /** + /** * Sets the zoom extent for the specified style layer. The zoom extent includes the * [minimum zoom level](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layer-minzoom) * and [maximum zoom level](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layer-maxzoom)) @@ -2720,12 +2488,12 @@ class Map * map.setLayerZoomRange('my-layer', 2, 5); * */ - setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number): this { - this.style.setLayerZoomRange(layerId, minzoom, maxzoom); - return this._update(true); - } + setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number): this { + this.style.setLayerZoomRange(layerId, minzoom, maxzoom); + return this._update(true); + } - /** + /** * Sets the filter for the specified style layer. * * Filters control which features a style layer renders from its source. @@ -2758,16 +2526,12 @@ class Map * @see [Example: Create a timeline animation](https://www.mapbox.com/mapbox-gl-js/example/timeline-animation/) * @see [Tutorial: Show changes over time](https://docs.mapbox.com/help/tutorials/show-changes-over-time/) */ - setFilter( - layerId: string, - filter: ?FilterSpecification, - options: StyleSetterOptions = {}, - ): this { - this.style.setFilter(layerId, filter, options); - return this._update(true); - } - - /** + setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}): this { + this.style.setFilter(layerId, filter, options); + return this._update(true); + } + + /** * Returns the filter applied to the specified style layer. * * @param {string} layerId The ID of the style layer whose filter to get. @@ -2775,11 +2539,11 @@ class Map * @example * const filter = map.getFilter('myLayer'); */ - getFilter(layerId: string): ?FilterSpecification { - return this.style.getFilter(layerId); - } + getFilter(layerId: string): ?FilterSpecification { + return this.style.getFilter(layerId); + } - /** + /** * Sets the value of a paint property in the specified style layer. * * @param {string} layerId The ID of the layer to set the paint property in. @@ -2795,17 +2559,12 @@ class Map * @see [Example: Adjust a layer's opacity](https://www.mapbox.com/mapbox-gl-js/example/adjust-layer-opacity/) * @see [Example: Create a draggable point](https://www.mapbox.com/mapbox-gl-js/example/drag-a-point/) */ - setPaintProperty( - layerId: string, - name: string, - value: any, - options: StyleSetterOptions = {}, - ): this { - this.style.setPaintProperty(layerId, name, value, options); - return this._update(true); - } - - /** + setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}): this { + this.style.setPaintProperty(layerId, name, value, options); + return this._update(true); + } + + /** * Returns the value of a paint property in the specified style layer. * * @param {string} layerId The ID of the layer to get the paint property from. @@ -2814,14 +2573,11 @@ class Map * @example * const paintProperty = map.getPaintProperty('mySymbolLayer', 'icon-color'); */ - getPaintProperty( - layerId: string, - name: string, - ): void | TransitionSpecification | PropertyValueSpecification { - return this.style.getPaintProperty(layerId, name); - } - - /** + getPaintProperty(layerId: string, name: string): void | TransitionSpecification | PropertyValueSpecification { + return this.style.getPaintProperty(layerId, name); + } + + /** * Sets the value of a layout property in the specified style layer. * * @param {string} layerId The ID of the layer to set the layout property in. @@ -2834,17 +2590,12 @@ class Map * map.setLayoutProperty('my-layer', 'visibility', 'none'); * @see [Example: Show and hide layers](https://docs.mapbox.com/mapbox-gl-js/example/toggle-layers/) */ - setLayoutProperty( - layerId: string, - name: string, - value: any, - options: StyleSetterOptions = {}, - ): this { - this.style.setLayoutProperty(layerId, name, value, options); - return this._update(true); - } - - /** + setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}): this { + this.style.setLayoutProperty(layerId, name, value, options); + return this._update(true); + } + + /** * Returns the value of a layout property in the specified style layer. * * @param {string} layerId The ID of the layer to get the layout property from. @@ -2853,16 +2604,13 @@ class Map * @example * const layoutProperty = map.getLayoutProperty('mySymbolLayer', 'icon-anchor'); */ - getLayoutProperty( - layerId: string, - name: string, - ): ?PropertyValueSpecification { - return this.style.getLayoutProperty(layerId, name); - } + getLayoutProperty(layerId: string, name: string): ?PropertyValueSpecification { + return this.style.getLayoutProperty(layerId, name); + } - /** @section {Style properties} */ + /** @section {Style properties} */ - /** + /** * Sets the any combination of light values. * * @param {LightSpecification} light Light properties to set. Must conform to the [Light Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#light). @@ -2876,25 +2624,25 @@ class Map * "intensity": 0.5 * }); */ - setLight(light: LightSpecification, options: StyleSetterOptions = {}): this { - this._lazyInitEmptyStyle(); - this.style.setLight(light, options); - return this._update(true); - } + setLight(light: LightSpecification, options: StyleSetterOptions = {}): this { + this._lazyInitEmptyStyle(); + this.style.setLight(light, options); + return this._update(true); + } - /** + /** * Returns the value of the light object. * * @returns {LightSpecification} Light properties of the style. * @example * const light = map.getLight(); */ - getLight(): LightSpecification { - return this.style.getLight(); - } + getLight(): LightSpecification { + return this.style.getLight(); + } - // eslint-disable-next-line jsdoc/require-returns - /** + // eslint-disable-next-line jsdoc/require-returns + /** * Sets the terrain property of the style. * * @param {TerrainSpecification} terrain Terrain properties to set. Must conform to the [Terrain Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/terrain/). @@ -2910,29 +2658,29 @@ class Map * // add the DEM source as a terrain layer with exaggerated height * map.setTerrain({'source': 'mapbox-dem', 'exaggeration': 1.5}); */ - setTerrain(terrain: TerrainSpecification): this { - this._lazyInitEmptyStyle(); - if (!terrain && this.transform.projection.requiresDraping) { - this.style.setTerrainForDraping(); - } else { - this.style.setTerrain(terrain); - } - this._averageElevationLastSampledAt = -Infinity; - return this._update(true); - } - - /** + setTerrain(terrain: TerrainSpecification): this { + this._lazyInitEmptyStyle(); + if (!terrain && this.transform.projection.requiresDraping) { + this.style.setTerrainForDraping(); + } else { + this.style.setTerrain(terrain); + } + this._averageElevationLastSampledAt = -Infinity; + return this._update(true); + } + + /** * Returns the terrain specification or `null` if terrain isn't set on the map. * * @returns {TerrainSpecification | null} Terrain specification properties of the style. * @example * const terrain = map.getTerrain(); */ - getTerrain(): ?TerrainSpecification { - return this.style ? this.style.getTerrain() : null; - } + getTerrain(): ?TerrainSpecification { + return this.style ? this.style.getTerrain() : null; + } - /** + /** * Sets the fog property of the style. * * @param {FogSpecification} fog The fog properties to set. Must conform to the [Fog Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/fog/). @@ -2949,24 +2697,24 @@ class Map * }); * @see [Example: Add fog to a map](https://docs.mapbox.com/mapbox-gl-js/example/add-fog/) */ - setFog(fog: FogSpecification): this { - this._lazyInitEmptyStyle(); - this.style.setFog(fog); - return this._update(true); - } + setFog(fog: FogSpecification): this { + this._lazyInitEmptyStyle(); + this.style.setFog(fog); + return this._update(true); + } - /** + /** * Returns the fog specification or `null` if fog is not set on the map. * * @returns {FogSpecification} Fog specification properties of the style. * @example * const fog = map.getFog(); */ - getFog(): ?FogSpecification { - return this.style ? this.style.getFog() : null; - } + getFog(): ?FogSpecification { + return this.style ? this.style.getFog() : null; + } - /** + /** * Returns the fog opacity for a given location. * * An opacity of 0 means that there is no fog contribution for the given location @@ -2978,17 +2726,14 @@ class Map * @returns {number} A value between 0 and 1 representing the fog opacity, where 1 means fully within, and 0 means not affected by the fog effect. * @private */ - _queryFogOpacity(lnglat: LngLatLike): number { - if (!this.style || !this.style.fog) return 0.0; - return this.style.fog.getOpacityAtLatLng( - LngLat.convert(lnglat), - this.transform, - ); - } + _queryFogOpacity(lnglat: LngLatLike): number { + if (!this.style || !this.style.fog) return 0.0; + return this.style.fog.getOpacityAtLatLng(LngLat.convert(lnglat), this.transform); + } - /** @section {Feature state} */ + /** @section {Feature state} */ - /** + /** * Sets the `state` of a feature. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. * When using this method, the `state` object is merged with any existing key-value pairs in the feature's state. @@ -3026,16 +2771,13 @@ class Map * @see [Example: Create a hover effect](https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/) * @see [Tutorial: Create interactive hover effects with Mapbox GL JS](https://docs.mapbox.com/help/tutorials/create-interactive-hover-effects-with-mapbox-gl-js/) */ - setFeatureState( - feature: { source: string, sourceLayer?: string, id: string | number }, - state: Object, - ): this { - this.style.setFeatureState(feature, state); - return this._update(); - } - - // eslint-disable-next-line jsdoc/require-returns - /** + setFeatureState(feature: { source: string; sourceLayer?: string; id: string | number; }, state: Object): this { + this.style.setFeatureState(feature, state); + return this._update(); + } + + // eslint-disable-next-line jsdoc/require-returns + /** * Removes the `state` of a feature, setting it back to the default behavior. * If only a `feature.source` is specified, it will remove the state for all features from that source. * If `feature.id` is also specified, it will remove all keys for that feature's state. @@ -3081,15 +2823,12 @@ class Map * }); * */ - removeFeatureState( - feature: { source: string, sourceLayer?: string, id?: string | number }, - key?: string, - ): this { - this.style.removeFeatureState(feature, key); - return this._update(); - } - - /** + removeFeatureState(feature: { source: string; sourceLayer?: string; id?: string | number; }, key?: string): this { + this.style.removeFeatureState(feature, key); + return this._update(); + } + + /** * Gets the `state` of a feature. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. * Features are identified by their `id` attribute, which can be any number or string. @@ -3118,215 +2857,170 @@ class Map * }); * */ - getFeatureState( - feature: { source: string, sourceLayer?: string, id: string | number }, - ): any { - return this.style.getFeatureState(feature); - } - - _updateContainerDimensions() { - if (!this._container) return; - - const width = this._container.getBoundingClientRect().width || 400; - const height = this._container.getBoundingClientRect().height || 300; - - let transformValues; - let transformScaleWidth; - let transformScaleHeight; - let el = this._container; - while (el && (!transformScaleWidth || !transformScaleHeight)) { - const transformMatrix = window.getComputedStyle(el).transform; - if (transformMatrix && transformMatrix !== 'none') { - transformValues = transformMatrix.match(/matrix.*\((.+)\)/)[1].split( - ', ', - ); - if ( - transformValues[0] && transformValues[0] !== '0' && - transformValues[0] !== '1' - ) - transformScaleWidth = transformValues[0]; - if ( - transformValues[3] && transformValues[3] !== '0' && - transformValues[3] !== '1' - ) - transformScaleHeight = transformValues[3]; - } - el = el.parentElement; - } - - this._containerWidth = transformScaleWidth ? - Math.abs(width / transformScaleWidth) : - width; - this._containerHeight = transformScaleHeight ? - Math.abs(height / transformScaleHeight) : - height; - } - - _detectMissingCSS(): void { - const computedColor = window.getComputedStyle(this._missingCSSCanary).getPropertyValue('background-color'); - if (computedColor !== 'rgb(250, 128, 114)') { - warnOnce('This page appears to be missing CSS declarations for ' + - 'Mapbox GL JS, which may cause the map to display incorrectly. ' + - 'Please ensure your page includes mapbox-gl.css, as described ' + - 'in https://www.mapbox.com/mapbox-gl-js/api/.'); - } - } - - _setupContainer() { - const container = this._container; - container.classList.add('mapboxgl-map'); - - const missingCSSCanary = this._missingCSSCanary = DOM.create( - 'div', - 'mapboxgl-canary', - container, - ); - missingCSSCanary.style.visibility = 'hidden'; - this._detectMissingCSS(); - - const canvasContainer = this._canvasContainer = DOM.create( - 'div', - 'mapboxgl-canvas-container', - container, - ); - if (this._interactive) { - canvasContainer.classList.add('mapboxgl-interactive'); - } - - this._canvas = DOM.create('canvas', 'mapboxgl-canvas', canvasContainer); - this._canvas.addEventListener('webglcontextlost', this._contextLost, false); - this._canvas.addEventListener( - 'webglcontextrestored', - this._contextRestored, - false, - ); - this._canvas.setAttribute('tabindex', '0'); - this._canvas.setAttribute('aria-label', this._getUIString('Map.Title')); - this._canvas.setAttribute('role', 'region'); - - this._updateContainerDimensions(); - this._resizeCanvas(this._containerWidth, this._containerHeight); - - const controlContainer = this._controlContainer = DOM.create( - 'div', - 'mapboxgl-control-container', - container, - ); - const positions = this._controlPositions = {}; - ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach( - positionName => { - positions[positionName] = DOM.create( - 'div', - `mapboxgl-ctrl-${positionName}`, - controlContainer, - ); - }, - ); - - this._container.addEventListener('scroll', this._onMapScroll, false); - } - - _resizeCanvas(width: number, height: number) { - const pixelRatio = browser.devicePixelRatio || 1; - - // Request the required canvas size (rounded up) taking the pixelratio into account. - this._canvas.width = pixelRatio * Math.ceil(width); - this._canvas.height = pixelRatio * Math.ceil(height); - - // Maintain the same canvas size, potentially downscaling it for HiDPI displays - this._canvas.style.width = `${width}px`; - this._canvas.style.height = `${height}px`; - } - - _addMarker(marker: Marker) { - this._markers.push(marker); - } - - _removeMarker(marker: Marker) { - const index = this._markers.indexOf(marker); - if (index !== -1) { - this._markers.splice(index, 1); - } - } - - _addPopup(popup: Popup) { - this._popups.push(popup); - } - - _removePopup(popup: Popup) { - const index = this._popups.indexOf(popup); - if (index !== -1) { - this._popups.splice(index, 1); - } - } - - _setupPainter() { - const attributes = extend( - {}, - supported.webGLContextAttributes, - { - failIfMajorPerformanceCaveat: this._failIfMajorPerformanceCaveat, - preserveDrawingBuffer: this._preserveDrawingBuffer, - antialias: this._antialias || false, - }, - ); - - const gl2 = this._useWebGL2 && - ((this._canvas.getContext("webgl2", attributes): any): WebGLRenderingContext); - const gl = gl2 || this._canvas.getContext('webgl', attributes) || - this._canvas.getContext('experimental-webgl', attributes); - - if (!gl) { - this.fire(new ErrorEvent(new Error('Failed to initialize WebGL'))); - return; - } - - if (this._useWebGL2 && !gl2) { - warnOnce('Failed to create WebGL 2 context. Using WebGL 1.'); - } - storeAuthState(gl, true); - - this.painter = new Painter(gl, this.transform, !!gl2); - this.on( - 'data', - (event: MapDataEvent) => { - if (event.dataType === 'source') { - this.painter.setTileLoadedFlag(true); - } - }, - ); - - webpSupported.testSupport(gl); - } - - _contextLost: (event: *) => void = (event: *) => { - event.preventDefault(); - if (this._frame) { - this._frame.cancel(); - this._frame = null; - } - this.fire(new Event('webglcontextlost', {originalEvent: event})); - }; - - _contextRestored: (event: *) => void = (event: *) => { - this._setupPainter(); - this.resize(); - this._update(); - this.fire(new Event('webglcontextrestored', {originalEvent: event})); - }; - - _onMapScroll: (event: *) => ?boolean = (event: *) => { - if (event.target !== this._container) return; - - // Revert any scroll which would move the canvas outside of the view - this._container.scrollTop = 0; - this._container.scrollLeft = 0; - return false; - }; - - /** @section {Lifecycle} */ - - /** + getFeatureState(feature: { source: string; sourceLayer?: string; id: string | number; }): any { + return this.style.getFeatureState(feature); + } + + _updateContainerDimensions() { + if (!this._container) return; + + const width = this._container.getBoundingClientRect().width || 400; + const height = this._container.getBoundingClientRect().height || 300; + + let transformValues; + let transformScaleWidth; + let transformScaleHeight; + let el = this._container; + while (el && (!transformScaleWidth || !transformScaleHeight)) { + const transformMatrix = window.getComputedStyle(el).transform; + if (transformMatrix && transformMatrix !== 'none') { + transformValues = transformMatrix.match(/matrix.*\((.+)\)/)[1].split(', '); + if (transformValues[0] && transformValues[0] !== '0' && transformValues[0] !== '1') transformScaleWidth = transformValues[0]; + if (transformValues[3] && transformValues[3] !== '0' && transformValues[3] !== '1') transformScaleHeight = transformValues[3]; + } + el = el.parentElement; + } + + this._containerWidth = transformScaleWidth ? Math.abs(width / transformScaleWidth) : width; + this._containerHeight = transformScaleHeight ? Math.abs(height / transformScaleHeight) : height; + } + + _detectMissingCSS(): void { + const computedColor = window.getComputedStyle(this._missingCSSCanary).getPropertyValue('background-color'); + if (computedColor !== 'rgb(250, 128, 114)') { + warnOnce('This page appears to be missing CSS declarations for ' + + 'Mapbox GL JS, which may cause the map to display incorrectly. ' + + 'Please ensure your page includes mapbox-gl.css, as described ' + + 'in https://www.mapbox.com/mapbox-gl-js/api/.'); + } + } + + _setupContainer() { + const container = this._container; + container.classList.add('mapboxgl-map'); + + const missingCSSCanary = this._missingCSSCanary = DOM.create('div', 'mapboxgl-canary', container); + missingCSSCanary.style.visibility = 'hidden'; + this._detectMissingCSS(); + + const canvasContainer = this._canvasContainer = DOM.create('div', 'mapboxgl-canvas-container', container); + if (this._interactive) { + canvasContainer.classList.add('mapboxgl-interactive'); + } + + this._canvas = DOM.create('canvas', 'mapboxgl-canvas', canvasContainer); + this._canvas.addEventListener('webglcontextlost', this._contextLost, false); + this._canvas.addEventListener('webglcontextrestored', this._contextRestored, false); + this._canvas.setAttribute('tabindex', '0'); + this._canvas.setAttribute('aria-label', this._getUIString('Map.Title')); + this._canvas.setAttribute('role', 'region'); + + this._updateContainerDimensions(); + this._resizeCanvas(this._containerWidth, this._containerHeight); + + const controlContainer = this._controlContainer = DOM.create('div', 'mapboxgl-control-container', container); + const positions = this._controlPositions = {}; + ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach((positionName) => { + positions[positionName] = DOM.create('div', `mapboxgl-ctrl-${positionName}`, controlContainer); + }); + + this._container.addEventListener('scroll', this._onMapScroll, false); + } + + _resizeCanvas(width: number, height: number) { + const pixelRatio = browser.devicePixelRatio || 1; + + // Request the required canvas size (rounded up) taking the pixelratio into account. + this._canvas.width = pixelRatio * Math.ceil(width); + this._canvas.height = pixelRatio * Math.ceil(height); + + // Maintain the same canvas size, potentially downscaling it for HiDPI displays + this._canvas.style.width = `${width}px`; + this._canvas.style.height = `${height}px`; + } + + _addMarker(marker: Marker) { + this._markers.push(marker); + } + + _removeMarker(marker: Marker) { + const index = this._markers.indexOf(marker); + if (index !== -1) { + this._markers.splice(index, 1); + } + } + + _addPopup(popup: Popup) { + this._popups.push(popup); + } + + _removePopup(popup: Popup) { + const index = this._popups.indexOf(popup); + if (index !== -1) { + this._popups.splice(index, 1); + } + } + + _setupPainter() { + const attributes = extend({}, supported.webGLContextAttributes, { + failIfMajorPerformanceCaveat: this._failIfMajorPerformanceCaveat, + preserveDrawingBuffer: this._preserveDrawingBuffer, + antialias: this._antialias || false + }); + + const gl2 = this._useWebGL2 && ((this._canvas.getContext("webgl2", attributes): any): WebGLRenderingContext); + const gl = gl2 || + this._canvas.getContext('webgl', attributes) || + this._canvas.getContext('experimental-webgl', attributes); + + if (!gl) { + this.fire(new ErrorEvent(new Error('Failed to initialize WebGL'))); + return; + } + + if (this._useWebGL2 && !gl2) { + warnOnce('Failed to create WebGL 2 context. Using WebGL 1.'); + } + storeAuthState(gl, true); + + this.painter = new Painter(gl, this.transform, !!gl2); + this.on('data', (event: MapDataEvent) => { + if (event.dataType === 'source') { + this.painter.setTileLoadedFlag(true); + } + }); + + webpSupported.testSupport(gl); + } + + _contextLost: (event: *) => void = (event: *) => { + event.preventDefault(); + if (this._frame) { + this._frame.cancel(); + this._frame = null; + } + this.fire(new Event('webglcontextlost', {originalEvent: event})); + } + + _contextRestored: (event: *) => void = (event: *) => { + this._setupPainter(); + this.resize(); + this._update(); + this.fire(new Event('webglcontextrestored', {originalEvent: event})); + } + + _onMapScroll: (event: *) => ?boolean = (event: *) => { + if (event.target !== this._container) return; + + // Revert any scroll which would move the canvas outside of the view + this._container.scrollTop = 0; + this._container.scrollLeft = 0; + return false; + } + + /** @section {Lifecycle} */ + + /** * Returns a Boolean indicating whether the map is fully loaded. * * Returns `false` if the style is not yet fully loaded, @@ -3337,14 +3031,11 @@ class Map * @example * const isLoaded = map.loaded(); */ - loaded(): boolean { - return ( - !this._styleDirty && !this._sourcesDirty && !!this.style && - this.style.loaded() - ); - } - - /** + loaded(): boolean { + return !this._styleDirty && !this._sourcesDirty && !!this.style && this.style.loaded(); + } + + /** * Update this map's style and sources, and re-render the map. * * @param {boolean} updateStyle mark the map's style for reprocessing as @@ -3352,47 +3043,47 @@ class Map * @returns {Map} this * @private */ - _update(updateStyle?: boolean): this { - if (!this.style) return this; + _update(updateStyle?: boolean): this { + if (!this.style) return this; - this._styleDirty = this._styleDirty || updateStyle; - this._sourcesDirty = true; - this.triggerRepaint(); + this._styleDirty = this._styleDirty || updateStyle; + this._sourcesDirty = true; + this.triggerRepaint(); - return this; - } + return this; + } - /** + /** * Request that the given callback be executed during the next render * frame. Schedule a render frame if one is not already scheduled. * @returns An id that can be used to cancel the callback * @private */ - _requestRenderFrame: (callback: () => void) => TaskID = (callback) => { - this._update(); - return this._renderTaskQueue.add(callback); - } + _requestRenderFrame: (callback: () => void) => TaskID = (callback) => { + this._update(); + return this._renderTaskQueue.add(callback); + } - _cancelRenderFrame: (id: TaskID) => void = (id) => { - this._renderTaskQueue.remove(id); - } + _cancelRenderFrame: (id: TaskID) => void = (id) => { + this._renderTaskQueue.remove(id); + } - /** + /** * Request that the given callback be executed during the next render frame if the map is not * idle. Otherwise it is executed immediately, to avoid triggering a new render. * @private */ - _requestDomTask(callback: () => void) { - // This condition means that the map is idle: the callback needs to be called right now as - // there won't be a triggered render to run the queue. - if (!this.loaded() || (this.loaded() && !this.isMoving())) { - callback(); - } else { - this._domRenderTaskQueue.add(callback); - } - } - - /** + _requestDomTask(callback: () => void) { + // This condition means that the map is idle: the callback needs to be called right now as + // there won't be a triggered render to run the queue. + if (!this.loaded() || (this.loaded() && !this.isMoving())) { + callback(); + } else { + this._domRenderTaskQueue.add(callback); + } + } + + /** * Call when a (re-)render of the map is required: * - The style has changed (`setPaintProperty()`, etc.) * - Source data has changed (for example, tiles have finished loading) @@ -3404,364 +3095,295 @@ class Map * @returns {Map} this * @private */ - _render(paintStartTimeStamp: number) { - const m = PerformanceUtils.beginMeasure('render'); - - let gpuTimer; - const extTimerQuery = this.painter.context.extTimerQuery; - const frameStartTime = browser.now(); - if (this.listens('gpu-timing-frame')) { - gpuTimer = extTimerQuery.createQueryEXT(); - extTimerQuery.beginQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); - } - - // A custom layer may have used the context asynchronously. Mark the state as dirty. - this.painter.context.setDirty(); - this.painter.setBaseState(); - - if (this.isMoving() || this.isRotating() || this.isZooming()) { - this._interactionRange[0] = Math.min( - this._interactionRange[0], - window.performance.now(), - ); - this._interactionRange[1] = Math.max( - this._interactionRange[1], - window.performance.now(), - ); - } - - this._renderTaskQueue.run(paintStartTimeStamp); - this._domRenderTaskQueue.run(paintStartTimeStamp); - // A task queue callback may have fired a user event which may have removed the map - if (this._removed) return; - - this._updateProjectionTransition(); - - const fadeDuration = this._isInitialLoad ? 0 : this._fadeDuration; - - // If the style has changed, the map is being zoomed, or a transition or fade is in progress: - // - Apply style changes (in a batch) - // - Recalculate paint properties. - if (this.style && this._styleDirty) { - this._styleDirty = false; - - const zoom = this.transform.zoom; - const pitch = this.transform.pitch; - const now = browser.now(); - - const parameters = new EvaluationParameters( - zoom, - { - now, - fadeDuration, - pitch, - transition: this.style.getTransition(), - }, - ); - - this.style.update(parameters); - } - - const fogIsTransitioning = this.style && this.style.fog && - this.style.fog.hasTransition(); - - if (fogIsTransitioning) { - this.style._markersNeedUpdate = true; - this._sourcesDirty = true; - } - - // If we are in _render for any reason other than an in-progress paint - // transition, update source caches to check for and load any tiles we - // need for the current transform - let averageElevationChanged = false; - if (this.style && this._sourcesDirty) { - this._sourcesDirty = false; - this.painter._updateFog(this.style); - this._updateTerrain(); // Terrain DEM source updates here and skips update in style._updateSources. - averageElevationChanged = this._updateAverageElevation(frameStartTime); - this.style._updateSources(this.transform); - // Update positions of markers and popups on enabling/disabling terrain - this._forceMarkerAndPopupUpdate(); - } else { - averageElevationChanged = this._updateAverageElevation(frameStartTime); - } - - this._placementDirty = this.style && - this.style._updatePlacement( - this.painter.transform, - this.showCollisionBoxes, - fadeDuration, - this._crossSourceCollisions, - ); - - // Actually draw - if (this.style) { - this.painter.render( - this.style, - { - showTileBoundaries: this.showTileBoundaries, - showTerrainWireframe: this.showTerrainWireframe, - showOverdrawInspector: this._showOverdrawInspector, - showQueryGeometry: !!this._showQueryGeometry, - showTileAABBs: this.showTileAABBs, - rotating: this.isRotating(), - zooming: this.isZooming(), - moving: this.isMoving(), - fadeDuration, - isInitialLoad: this._isInitialLoad, - showPadding: this.showPadding, - gpuTiming: !!this.listens('gpu-timing-layer'), - gpuTimingDeferredRender: !!this.listens('gpu-timing-deferred-render'), - speedIndexTiming: this.speedIndexTiming, - }, - ); - } - - this.fire(new Event('render')); - - if (this.loaded() && !this._loaded) { - this._loaded = true; - PerformanceUtils.mark(PerformanceMarkers.load); - this.fire(new Event('load')); - } - - if (this.style && this.style.hasTransitions()) { - this._styleDirty = true; - } - - if (this.style && !this._placementDirty) { - // Since no fade operations are in progress, we can release - // all tiles held for fading. If we didn't do this, the tiles - // would just sit in the SourceCaches until the next render - this.style._releaseSymbolFadeTiles(); - } - - if (gpuTimer) { - const renderCPUTime = browser.now() - frameStartTime; - extTimerQuery.endQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); - setTimeout( - () => { - const renderGPUTime = extTimerQuery.getQueryObjectEXT( - gpuTimer, - extTimerQuery.QUERY_RESULT_EXT, - ) / (1000 * 1000); - extTimerQuery.deleteQueryEXT(gpuTimer); - this.fire( - new Event( - 'gpu-timing-frame', - { - cpuTime: renderCPUTime, - gpuTime: renderGPUTime, - }, - ), - ); - window.performance.mark( - 'frame-gpu', - { - startTime: frameStartTime, - detail: { - gpuTime: renderGPUTime, - }, - }, - ); - }, - 50, - ); // Wait 50ms to give time for all GPU calls to finish before querying - - } - - PerformanceUtils.endMeasure(m); - - if (this.listens('gpu-timing-layer')) { - // Resetting the Painter's per-layer timing queries here allows us to isolate - // the queries to individual frames. - const frameLayerQueries = this.painter.collectGpuTimers(); - - setTimeout( - () => { - const renderedLayerTimes = this.painter.queryGpuTimers( - frameLayerQueries, - ); - - this.fire( - new Event( - 'gpu-timing-layer', - { - layerTimes: renderedLayerTimes, - }, - ), - ); - }, - 50, - ); // Wait 50ms to give time for all GPU calls to finish before querying - - } + _render(paintStartTimeStamp: number) { + const m = PerformanceUtils.beginMeasure('render'); + + let gpuTimer; + const extTimerQuery = this.painter.context.extTimerQuery; + const frameStartTime = browser.now(); + if (this.listens('gpu-timing-frame')) { + gpuTimer = extTimerQuery.createQueryEXT(); + extTimerQuery.beginQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); + } + + // A custom layer may have used the context asynchronously. Mark the state as dirty. + this.painter.context.setDirty(); + this.painter.setBaseState(); + + if (this.isMoving() || this.isRotating() || this.isZooming()) { + this._interactionRange[0] = Math.min(this._interactionRange[0], window.performance.now()); + this._interactionRange[1] = Math.max(this._interactionRange[1], window.performance.now()); + } + + this._renderTaskQueue.run(paintStartTimeStamp); + this._domRenderTaskQueue.run(paintStartTimeStamp); + // A task queue callback may have fired a user event which may have removed the map + if (this._removed) return; + + this._updateProjectionTransition(); + + const fadeDuration = this._isInitialLoad ? 0 : this._fadeDuration; + + // If the style has changed, the map is being zoomed, or a transition or fade is in progress: + // - Apply style changes (in a batch) + // - Recalculate paint properties. + if (this.style && this._styleDirty) { + this._styleDirty = false; + + const zoom = this.transform.zoom; + const pitch = this.transform.pitch; + const now = browser.now(); + + const parameters = new EvaluationParameters(zoom, { + now, + fadeDuration, + pitch, + transition: this.style.getTransition() + }); + + this.style.update(parameters); + } + + const fogIsTransitioning = this.style && this.style.fog && this.style.fog.hasTransition(); + + if (fogIsTransitioning) { + this.style._markersNeedUpdate = true; + this._sourcesDirty = true; + } + + // If we are in _render for any reason other than an in-progress paint + // transition, update source caches to check for and load any tiles we + // need for the current transform + let averageElevationChanged = false; + if (this.style && this._sourcesDirty) { + this._sourcesDirty = false; + this.painter._updateFog(this.style); + this._updateTerrain(); // Terrain DEM source updates here and skips update in style._updateSources. + averageElevationChanged = this._updateAverageElevation(frameStartTime); + this.style._updateSources(this.transform); + // Update positions of markers and popups on enabling/disabling terrain + this._forceMarkerAndPopupUpdate(); + } else { + averageElevationChanged = this._updateAverageElevation(frameStartTime); + } + + this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions); + + // Actually draw + if (this.style) { + this.painter.render(this.style, { + showTileBoundaries: this.showTileBoundaries, + showTerrainWireframe: this.showTerrainWireframe, + showOverdrawInspector: this._showOverdrawInspector, + showQueryGeometry: !!this._showQueryGeometry, + showTileAABBs: this.showTileAABBs, + rotating: this.isRotating(), + zooming: this.isZooming(), + moving: this.isMoving(), + fadeDuration, + isInitialLoad: this._isInitialLoad, + showPadding: this.showPadding, + gpuTiming: !!this.listens('gpu-timing-layer'), + gpuTimingDeferredRender: !!this.listens('gpu-timing-deferred-render'), + speedIndexTiming: this.speedIndexTiming, + }); + } + + this.fire(new Event('render')); + + if (this.loaded() && !this._loaded) { + this._loaded = true; + PerformanceUtils.mark(PerformanceMarkers.load); + this.fire(new Event('load')); + } + + if (this.style && (this.style.hasTransitions())) { + this._styleDirty = true; + } + + if (this.style && !this._placementDirty) { + // Since no fade operations are in progress, we can release + // all tiles held for fading. If we didn't do this, the tiles + // would just sit in the SourceCaches until the next render + this.style._releaseSymbolFadeTiles(); + } + + if (gpuTimer) { + const renderCPUTime = browser.now() - frameStartTime; + extTimerQuery.endQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); + setTimeout(() => { + const renderGPUTime = extTimerQuery.getQueryObjectEXT(gpuTimer, extTimerQuery.QUERY_RESULT_EXT) / (1000 * 1000); + extTimerQuery.deleteQueryEXT(gpuTimer); + this.fire(new Event('gpu-timing-frame', { + cpuTime: renderCPUTime, + gpuTime: renderGPUTime + })); + window.performance.mark('frame-gpu', { + startTime: frameStartTime, + detail: { + gpuTime: renderGPUTime + } + }); + }, 50); // Wait 50ms to give time for all GPU calls to finish before querying + } + + PerformanceUtils.endMeasure(m); + + if (this.listens('gpu-timing-layer')) { + // Resetting the Painter's per-layer timing queries here allows us to isolate + // the queries to individual frames. + const frameLayerQueries = this.painter.collectGpuTimers(); + + setTimeout(() => { + const renderedLayerTimes = this.painter.queryGpuTimers(frameLayerQueries); + + this.fire(new Event('gpu-timing-layer', { + layerTimes: renderedLayerTimes + })); + }, 50); // Wait 50ms to give time for all GPU calls to finish before querying + } + + if (this.listens('gpu-timing-deferred-render')) { + const deferredRenderQueries = this.painter.collectDeferredRenderGpuQueries(); + + setTimeout(() => { + const gpuTime = this.painter.queryGpuTimeDeferredRender(deferredRenderQueries); + this.fire(new Event('gpu-timing-deferred-render', {gpuTime})); + }, 50); // Wait 50ms to give time for all GPU calls to finish before querying + } + + // Schedule another render frame if it's needed. + // + // Even though `_styleDirty` and `_sourcesDirty` are reset in this + // method, synchronous events fired during Style#update or + // Style#_updateSources could have caused them to be set again. + const somethingDirty = this._sourcesDirty || this._styleDirty || this._placementDirty || averageElevationChanged; + if (somethingDirty || this._repaint) { + this.triggerRepaint(); + } else { + const willIdle = !this.isMoving() && this.loaded(); + if (willIdle) { + // Before idling, we perform one last sample so that if the average elevation + // does not exactly match the terrain, we skip idle and ease it to its final state. + averageElevationChanged = this._updateAverageElevation(frameStartTime, true); + } - if (this.listens('gpu-timing-deferred-render')) { - const deferredRenderQueries = this.painter.collectDeferredRenderGpuQueries(); + if (averageElevationChanged) { + this.triggerRepaint(); + } else { + this._triggerFrame(false); + if (willIdle) { + this.fire(new Event('idle')); + this._isInitialLoad = false; + // check the options to see if need to calculate the speed index + if (this.speedIndexTiming) { + const speedIndexNumber = this._calculateSpeedIndex(); + this.fire(new Event('speedindexcompleted', {speedIndex: speedIndexNumber})); + this.speedIndexTiming = false; + } + } + } + } + + if (this._loaded && !this._fullyLoaded && !somethingDirty) { + this._fullyLoaded = true; + LivePerformanceUtils.mark(PerformanceMarkers.fullLoad); + // Following lines are billing and metrics related code. Do not change. See LICENSE.txt + if (this._performanceMetricsCollection) { + postPerformanceEvent(this._requestManager._customAccessToken, { + width: this.painter.width, + height: this.painter.height, + interactionRange: this._interactionRange, + visibilityHidden: this._visibilityHidden, + terrainEnabled: !!this.painter.style.getTerrain(), + fogEnabled: !!this.painter.style.getFog(), + projection: this.getProjection().name, + zoom: this.transform.zoom, + renderer: this.painter.context.renderer, + vendor: this.painter.context.vendor + }); + } + this._authenticate(); + } + } + + _forceMarkerAndPopupUpdate(shouldWrap?: boolean) { + for (const marker of this._markers) { + // Wrap marker location when toggling to a projection without world copies + if (shouldWrap && !this.getRenderWorldCopies()) { + marker._lngLat = marker._lngLat.wrap(); + } + marker._update(); + } + for (const popup of this._popups) { + // Wrap popup location when toggling to a projection without world copies and track pointer set to false + if (shouldWrap && !this.getRenderWorldCopies() && !popup._trackPointer) { + popup._lngLat = popup._lngLat.wrap(); + } + popup._update(); + } + } - setTimeout( - () => { - const gpuTime = this.painter.queryGpuTimeDeferredRender( - deferredRenderQueries, - ); - this.fire(new Event('gpu-timing-deferred-render', {gpuTime})); - }, - 50, - ); // Wait 50ms to give time for all GPU calls to finish before querying - - } - - // Schedule another render frame if it's needed. - // - // Even though `_styleDirty` and `_sourcesDirty` are reset in this - // method, synchronous events fired during Style#update or - // Style#_updateSources could have caused them to be set again. - const somethingDirty = this._sourcesDirty || this._styleDirty || - this._placementDirty || - averageElevationChanged; - if (somethingDirty || this._repaint) { - this.triggerRepaint(); - } else { - const willIdle = !this.isMoving() && this.loaded(); - if (willIdle) { - // Before idling, we perform one last sample so that if the average elevation - // does not exactly match the terrain, we skip idle and ease it to its final state. - averageElevationChanged = this._updateAverageElevation( - frameStartTime, - true, - ); - } - - if (averageElevationChanged) { - this.triggerRepaint(); - } else { - this._triggerFrame(false); - if (willIdle) { - this.fire(new Event('idle')); - this._isInitialLoad = false; - // check the options to see if need to calculate the speed index - if (this.speedIndexTiming) { - const speedIndexNumber = this._calculateSpeedIndex(); - this.fire( - new Event('speedindexcompleted', {speedIndex: speedIndexNumber}), - ); - this.speedIndexTiming = false; - } - } - } - } - - if (this._loaded && !this._fullyLoaded && !somethingDirty) { - this._fullyLoaded = true; - LivePerformanceUtils.mark(PerformanceMarkers.fullLoad); - // Following lines are billing and metrics related code. Do not change. See LICENSE.txt - if (this._performanceMetricsCollection) { - postPerformanceEvent( - this._requestManager._customAccessToken, - { - width: this.painter.width, - height: this.painter.height, - interactionRange: this._interactionRange, - visibilityHidden: this._visibilityHidden, - terrainEnabled: !!this.painter.style.getTerrain(), - fogEnabled: !!this.painter.style.getFog(), - projection: this.getProjection().name, - zoom: this.transform.zoom, - renderer: this.painter.context.renderer, - vendor: this.painter.context.vendor, - }, - ); - } - this._authenticate(); - } - } - - _forceMarkerAndPopupUpdate(shouldWrap?: boolean) { - for (const marker of this._markers) { - // Wrap marker location when toggling to a projection without world copies - if (shouldWrap && !this.getRenderWorldCopies()) { - marker._lngLat = marker._lngLat.wrap(); - } - marker._update(); - } - for (const popup of this._popups) { - // Wrap popup location when toggling to a projection without world copies and track pointer set to false - if (shouldWrap && !this.getRenderWorldCopies() && !popup._trackPointer) { - popup._lngLat = popup._lngLat.wrap(); - } - popup._update(); - } - } - - /** + /** * Update the average visible elevation by sampling terrain * * @returns {boolean} true if elevation has changed from the last sampling * @private */ - _updateAverageElevation( - timeStamp: number, - ignoreTimeout: boolean = false, - ): boolean { - const applyUpdate = (value => { - this.transform.averageElevation = value; - this._update(false); - return true; - }); - - if (!this.painter.averageElevationNeedsEasing()) { - if (this.transform.averageElevation !== 0) return applyUpdate(0); - return false; - } - - const timeoutElapsed = ignoreTimeout || - timeStamp - this._averageElevationLastSampledAt > AVERAGE_ELEVATION_SAMPLING_INTERVAL; - - if (timeoutElapsed && !this._averageElevation.isEasing(timeStamp)) { - const currentElevation = this.transform.averageElevation; - let newElevation = this.transform.sampleAverageElevation(); - let exaggerationChanged = false; - if (this.transform.elevation) { - exaggerationChanged = this.transform.elevation.exaggeration() !== this._averageElevationExaggeration; - // $FlowIgnore[incompatible-use] - this._averageElevationExaggeration = this.transform.elevation.exaggeration(); - } - - // New elevation is NaN if no terrain tiles were available - if (isNaN(newElevation)) { - newElevation = 0; - } else { - // Don't activate the timeout if no data was available - this._averageElevationLastSampledAt = timeStamp; - } - const elevationChange = Math.abs(currentElevation - newElevation); - - if (elevationChange > AVERAGE_ELEVATION_EASE_THRESHOLD) { - if (this._isInitialLoad || exaggerationChanged) { - this._averageElevation.jumpTo(newElevation); - return applyUpdate(newElevation); - } else { - this._averageElevation.easeTo( - newElevation, - timeStamp, - AVERAGE_ELEVATION_EASE_TIME, - ); - } - } else if (elevationChange > AVERAGE_ELEVATION_CHANGE_THRESHOLD) { - this._averageElevation.jumpTo(newElevation); - return applyUpdate(newElevation); - } - } - - if (this._averageElevation.isEasing(timeStamp)) { - return applyUpdate(this._averageElevation.getValue(timeStamp)); - } - - return false; - } - - /***** START WARNING - REMOVAL OR MODIFICATION OF THE + _updateAverageElevation(timeStamp: number, ignoreTimeout: boolean = false): boolean { + const applyUpdate = value => { + this.transform.averageElevation = value; + this._update(false); + return true; + }; + + if (!this.painter.averageElevationNeedsEasing()) { + if (this.transform.averageElevation !== 0) return applyUpdate(0); + return false; + } + + const timeoutElapsed = ignoreTimeout || timeStamp - this._averageElevationLastSampledAt > AVERAGE_ELEVATION_SAMPLING_INTERVAL; + + if (timeoutElapsed && !this._averageElevation.isEasing(timeStamp)) { + const currentElevation = this.transform.averageElevation; + let newElevation = this.transform.sampleAverageElevation(); + let exaggerationChanged = false; + if (this.transform.elevation) { + exaggerationChanged = this.transform.elevation.exaggeration() !== this._averageElevationExaggeration; + // $FlowIgnore[incompatible-use] + this._averageElevationExaggeration = this.transform.elevation.exaggeration(); + } + + // New elevation is NaN if no terrain tiles were available + if (isNaN(newElevation)) { + newElevation = 0; + } else { + // Don't activate the timeout if no data was available + this._averageElevationLastSampledAt = timeStamp; + } + const elevationChange = Math.abs(currentElevation - newElevation); + + if (elevationChange > AVERAGE_ELEVATION_EASE_THRESHOLD) { + if (this._isInitialLoad || exaggerationChanged) { + this._averageElevation.jumpTo(newElevation); + return applyUpdate(newElevation); + } else { + this._averageElevation.easeTo(newElevation, timeStamp, AVERAGE_ELEVATION_EASE_TIME); + } + } else if (elevationChange > AVERAGE_ELEVATION_CHANGE_THRESHOLD) { + this._averageElevation.jumpTo(newElevation); + return applyUpdate(newElevation); + } + } + + if (this._averageElevation.isEasing(timeStamp)) { + return applyUpdate(this._averageElevation.getValue(timeStamp)); + } + + return false; + } + + /***** START WARNING - REMOVAL OR MODIFICATION OF THE * FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** * The following code is used to access Mapbox's APIs. Removal or modification * of this code can result in higher fees and/or @@ -3774,124 +3396,80 @@ class Map * and the Mapbox Terms of Service are available at https://www.mapbox.com/tos/ ******************************************************************************/ - _authenticate() { - getMapSessionAPI( - this._getMapId(), - this._requestManager._skuToken, - this._requestManager._customAccessToken, - err => { - if (err) { - // throwing an error here will cause the callback to be called again unnecessarily - if (err.message === AUTH_ERR_MSG || (err: any).status === 401) { - const gl = this.painter.context.gl; - storeAuthState(gl, false); - if (this._logoControl instanceof LogoControl) { - this._logoControl._updateLogo(); - } - if (gl) - gl.clear( - gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT, - ); - - if (!this._silenceAuthErrors) { - this.fire( - new ErrorEvent( - new Error( - 'A valid Mapbox access token is required to use Mapbox GL JS. To create an account or a new access token, visit https://account.mapbox.com/', - ), - ), - ); - } - } - } - }, - ); - postMapLoadEvent( - this._getMapId(), - this._requestManager._skuToken, - this._requestManager._customAccessToken, - () => {}, - ); - } - - /***** END WARNING - REMOVAL OR MODIFICATION OF THE + _authenticate() { + getMapSessionAPI(this._getMapId(), this._requestManager._skuToken, this._requestManager._customAccessToken, (err) => { + if (err) { + // throwing an error here will cause the callback to be called again unnecessarily + if (err.message === AUTH_ERR_MSG || (err: any).status === 401) { + const gl = this.painter.context.gl; + storeAuthState(gl, false); + if (this._logoControl instanceof LogoControl) { + this._logoControl._updateLogo(); + } + if (gl) gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + + if (!this._silenceAuthErrors) { + this.fire(new ErrorEvent(new Error('A valid Mapbox access token is required to use Mapbox GL JS. To create an account or a new access token, visit https://account.mapbox.com/'))); + } + } + } + }); + postMapLoadEvent(this._getMapId(), this._requestManager._skuToken, this._requestManager._customAccessToken, () => {}); + } + + /***** END WARNING - REMOVAL OR MODIFICATION OF THE PRECEDING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******/ - _updateTerrain() { - // Recalculate if enabled/disabled and calculate elevation cover. As camera is using elevation tiles before - // render (and deferred update after zoom recalculation), this needs to be called when removing terrain source. - const adaptCameraAltitude = this._isDragging(); - this.painter.updateTerrain(this.style, adaptCameraAltitude); - } - - _calculateSpeedIndex(): number { - const finalFrame = this.painter.canvasCopy(); - const canvasCopyInstances = this.painter.getCanvasCopiesAndTimestamps(); - canvasCopyInstances.timeStamps.push(performance.now()); - - const gl = this.painter.context.gl; - const framebuffer = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - - function read(texture) { - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - texture, - 0, - ); - const pixels = new Uint8Array( - gl.drawingBufferWidth * gl.drawingBufferHeight * 4, - ); - gl.readPixels( - 0, - 0, - gl.drawingBufferWidth, - gl.drawingBufferHeight, - gl.RGBA, - gl.UNSIGNED_BYTE, - pixels, - ); - return pixels; - } - - return this._canvasPixelComparison( - read(finalFrame), - canvasCopyInstances.canvasCopies.map(read), - canvasCopyInstances.timeStamps, - ); - } - - _canvasPixelComparison( - finalFrame: Uint8Array, - allFrames: Array, - timeStamps: Array, - ): number { - let finalScore = timeStamps[1] - timeStamps[0]; - const numPixels = finalFrame.length / 4; - - for (let i = 0; i < allFrames.length; i++) { - const frame = allFrames[i]; - let cnt = 0; - for (let j = 0; j < frame.length; j += 4) { - if ( - frame[j] === finalFrame[j] && frame[j + 1] === finalFrame[j + 1] && - frame[j + 2] === finalFrame[j + 2] && - frame[j + 3] === finalFrame[j + 3] - ) { - cnt = cnt + 1; - } - } - //calculate the % visual completeness - const interval = timeStamps[i + 2] - timeStamps[i + 1]; - const visualCompletness = cnt / numPixels; - finalScore += interval * (1 - visualCompletness); - } - return finalScore; - } - - /** + _updateTerrain() { + // Recalculate if enabled/disabled and calculate elevation cover. As camera is using elevation tiles before + // render (and deferred update after zoom recalculation), this needs to be called when removing terrain source. + const adaptCameraAltitude = this._isDragging(); + this.painter.updateTerrain(this.style, adaptCameraAltitude); + } + + _calculateSpeedIndex(): number { + const finalFrame = this.painter.canvasCopy(); + const canvasCopyInstances = this.painter.getCanvasCopiesAndTimestamps(); + canvasCopyInstances.timeStamps.push(performance.now()); + + const gl = this.painter.context.gl; + const framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + + function read(texture) { + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); + gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + return pixels; + } + + return this._canvasPixelComparison(read(finalFrame), canvasCopyInstances.canvasCopies.map(read), canvasCopyInstances.timeStamps); + } + + _canvasPixelComparison(finalFrame: Uint8Array, allFrames: Uint8Array[], timeStamps: number[]): number { + let finalScore = timeStamps[1] - timeStamps[0]; + const numPixels = finalFrame.length / 4; + + for (let i = 0; i < allFrames.length; i++) { + const frame = allFrames[i]; + let cnt = 0; + for (let j = 0; j < frame.length; j += 4) { + if (frame[j] === finalFrame[j] && + frame[j + 1] === finalFrame[j + 1] && + frame[j + 2] === finalFrame[j + 2] && + frame[j + 3] === finalFrame[j + 3]) { + cnt = cnt + 1; + } + } + //calculate the % visual completeness + const interval = timeStamps[i + 2] - timeStamps[i + 1]; + const visualCompletness = cnt / numPixels; + finalScore += interval * (1 - visualCompletness); + } + return finalScore; + } + + /** * Clean up and release all internal resources associated with this map. * * This includes DOM elements, event bindings, web workers, and WebGL resources. @@ -3903,80 +3481,59 @@ class Map * @example * map.remove(); */ - remove() { - if (this._hash) this._hash.remove(); - - for (const control of this._controls) - control.onRemove(this); - this._controls = []; - - if (this._frame) { - this._frame.cancel(); - this._frame = null; - } - this._renderTaskQueue.clear(); - this._domRenderTaskQueue.clear(); - if (this.style) { - this.style.destroy(); - } - this.painter.destroy(); - if (this.handlers) this.handlers.destroy(); - this.handlers = undefined; - this.setStyle(null); - - if (typeof window !== 'undefined') { - window.removeEventListener('resize', this._onWindowResize, false); - window.removeEventListener( - 'orientationchange', - this._onWindowResize, - false, - ); - window.removeEventListener( - 'webkitfullscreenchange', - this._onWindowResize, - false, - ); - window.removeEventListener('online', this._onWindowOnline, false); - window.removeEventListener( - 'visibilitychange', - this._onVisibilityChange, - false, - ); - } - - const extension = this.painter.context.gl.getExtension('WEBGL_lose_context'); - if (extension) extension.loseContext(); - - this._canvas.removeEventListener( - 'webglcontextlost', - this._contextLost, - false, - ); - this._canvas.removeEventListener( - 'webglcontextrestored', - this._contextRestored, - false, - ); - - this._canvasContainer.remove(); - this._controlContainer.remove(); - this._missingCSSCanary.remove(); - - this._canvas = (undefined: any); - this._canvasContainer = (undefined: any); - this._controlContainer = (undefined: any); - this._missingCSSCanary = (undefined: any); - - this._container.classList.remove('mapboxgl-map'); - this._container.removeEventListener('scroll', this._onMapScroll, false); - - PerformanceUtils.clearMetrics(); - removeAuthState(this.painter.context.gl); - this._removed = true; - this.fire(new Event('remove')); - } - - /** + remove() { + if (this._hash) this._hash.remove(); + + for (const control of this._controls) control.onRemove(this); + this._controls = []; + + if (this._frame) { + this._frame.cancel(); + this._frame = null; + } + this._renderTaskQueue.clear(); + this._domRenderTaskQueue.clear(); + if (this.style) { + this.style.destroy(); + } + this.painter.destroy(); + if (this.handlers) this.handlers.destroy(); + this.handlers = undefined; + this.setStyle(null); + + if (typeof window !== 'undefined') { + window.removeEventListener('resize', this._onWindowResize, false); + window.removeEventListener('orientationchange', this._onWindowResize, false); + window.removeEventListener('webkitfullscreenchange', this._onWindowResize, false); + window.removeEventListener('online', this._onWindowOnline, false); + window.removeEventListener('visibilitychange', this._onVisibilityChange, false); + } + + const extension = this.painter.context.gl.getExtension('WEBGL_lose_context'); + if (extension) extension.loseContext(); + + this._canvas.removeEventListener('webglcontextlost', this._contextLost, false); + this._canvas.removeEventListener('webglcontextrestored', this._contextRestored, false); + + this._canvasContainer.remove(); + this._controlContainer.remove(); + this._missingCSSCanary.remove(); + + this._canvas = (undefined: any); + this._canvasContainer = (undefined: any); + this._controlContainer = (undefined: any); + this._missingCSSCanary = (undefined: any); + + this._container.classList.remove('mapboxgl-map'); + this._container.removeEventListener('scroll', this._onMapScroll, false); + + PerformanceUtils.clearMetrics(); + removeAuthState(this.painter.context.gl); + this._removed = true; + this.fire(new Event('remove')); + } + + /** * Trigger the rendering of a single frame. Use this method with custom layers to * repaint the map when the layer's properties or properties associated with the * layer's source change. Calling this multiple times before the @@ -3987,67 +3544,59 @@ class Map * @see [Example: Add a 3D model](https://docs.mapbox.com/mapbox-gl-js/example/add-3d-model/) * @see [Example: Add an animated icon to the map](https://docs.mapbox.com/mapbox-gl-js/example/add-image-animated/) */ - triggerRepaint() { - this._triggerFrame(true); - } - - _triggerFrame(render: boolean) { - this._renderNextFrame = this._renderNextFrame || render; - if (this.style && !this._frame) { - this._frame = browser.frame( - (paintStartTimeStamp: number) => { - const isRenderFrame = !!this._renderNextFrame; - PerformanceUtils.frame(paintStartTimeStamp, isRenderFrame); - this._frame = null; - this._renderNextFrame = null; - if (isRenderFrame) { - this._render(paintStartTimeStamp); - } - }, - ); - } - } - - /** + triggerRepaint() { + this._triggerFrame(true); + } + + _triggerFrame(render: boolean) { + this._renderNextFrame = this._renderNextFrame || render; + if (this.style && !this._frame) { + this._frame = browser.frame((paintStartTimeStamp: number) => { + const isRenderFrame = !!this._renderNextFrame; + PerformanceUtils.frame(paintStartTimeStamp, isRenderFrame); + this._frame = null; + this._renderNextFrame = null; + if (isRenderFrame) { + this._render(paintStartTimeStamp); + } + }); + } + } + + /** * Preloads all tiles that will be requested for one or a series of transformations * * @private * @returns {Object} Returns `this` | Promise. */ - _preloadTiles: (transform: Transform | Array) => Map = (transform) => { - const sources: Array = this.style ? - (Object.values(this.style._sourceCaches): any) : - []; - asyncAll( - sources, - (source, done) => source._preloadTiles(transform, done), - () => { - this.triggerRepaint(); - }, - ); - - return this; - } - - _onWindowOnline: () => void = () => { - this._update(); - }; - - _onWindowResize: (event: Event) => void = (event: Event) => { - if (this._trackResize) { - this.resize({originalEvent: event})._update(); - } - }; - - _onVisibilityChange: () => void = () => { - if (window.document.visibilityState === 'hidden') { - this._visibilityHidden++; - } - }; - - /** @section {Debug features} */ - - /** + _preloadTiles: (transform: Transform | Array) => Map = (transform) => { + const sources: Array = this.style ? (Object.values(this.style._sourceCaches): any) : []; + asyncAll(sources, (source, done) => source._preloadTiles(transform, done), () => { + this.triggerRepaint(); + }); + + return this; + } + + _onWindowOnline: () => void = () => { + this._update(); + } + + _onWindowResize: (event: Event) => void = (event: Event) => { + if (this._trackResize) { + this.resize({originalEvent: event})._update(); + } + } + + _onVisibilityChange: () => void = () => { + if (window.document.visibilityState === 'hidden') { + this._visibilityHidden++; + } + } + + /** @section {Debug features} */ + + /** * Gets and sets a Boolean indicating whether the map will render an outline * around each tile and the tile ID. These tile boundaries are useful for * debugging. @@ -4062,16 +3611,14 @@ class Map * @example * map.showTileBoundaries = true; */ - get showTileBoundaries(): boolean { - return !!this._showTileBoundaries; - } - set showTileBoundaries(value: boolean) { - if (this._showTileBoundaries === value) return; - this._showTileBoundaries = value; - this._update(); - } - - /** + get showTileBoundaries(): boolean { return !!this._showTileBoundaries; } + set showTileBoundaries(value: boolean) { + if (this._showTileBoundaries === value) return; + this._showTileBoundaries = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will render a wireframe * on top of the displayed terrain. Useful for debugging. * @@ -4084,16 +3631,14 @@ class Map * @example * map.showTerrainWireframe = true; */ - get showTerrainWireframe(): boolean { - return !!this._showTerrainWireframe; - } - set showTerrainWireframe(value: boolean) { - if (this._showTerrainWireframe === value) return; - this._showTerrainWireframe = value; - this._update(); - } - - /** + get showTerrainWireframe(): boolean { return !!this._showTerrainWireframe; } + set showTerrainWireframe(value: boolean) { + if (this._showTerrainWireframe === value) return; + this._showTerrainWireframe = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the speedindex metric calculation is on or off * * @private @@ -4104,16 +3649,14 @@ class Map * @example * map.speedIndexTiming = true; */ - get speedIndexTiming(): boolean { - return !!this._speedIndexTiming; - } - set speedIndexTiming(value: boolean) { - if (this._speedIndexTiming === value) return; - this._speedIndexTiming = value; - this._update(); - } - - /** + get speedIndexTiming(): boolean { return !!this._speedIndexTiming; } + set speedIndexTiming(value: boolean) { + if (this._speedIndexTiming === value) return; + this._speedIndexTiming = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will visualize * the padding offsets. * @@ -4122,16 +3665,14 @@ class Map * @instance * @memberof Map */ - get showPadding(): boolean { - return !!this._showPadding; - } - set showPadding(value: boolean) { - if (this._showPadding === value) return; - this._showPadding = value; - this._update(); - } - - /** + get showPadding(): boolean { return !!this._showPadding; } + set showPadding(value: boolean) { + if (this._showPadding === value) return; + this._showPadding = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will render boxes * around all symbols in the data source, revealing which symbols * were rendered or which were hidden due to collisions. @@ -4142,23 +3683,21 @@ class Map * @instance * @memberof Map */ - get showCollisionBoxes(): boolean { - return !!this._showCollisionBoxes; - } - set showCollisionBoxes(value: boolean) { - if (this._showCollisionBoxes === value) return; - this._showCollisionBoxes = value; - if (value) { - // When we turn collision boxes on we have to generate them for existing tiles - // When we turn them off, there's no cost to leaving existing boxes in place - this.style._generateCollisionBoxes(); - } else { - // Otherwise, call an update to remove collision boxes - this._update(); - } - } - - /** + get showCollisionBoxes(): boolean { return !!this._showCollisionBoxes; } + set showCollisionBoxes(value: boolean) { + if (this._showCollisionBoxes === value) return; + this._showCollisionBoxes = value; + if (value) { + // When we turn collision boxes on we have to generate them for existing tiles + // When we turn them off, there's no cost to leaving existing boxes in place + this.style._generateCollisionBoxes(); + } else { + // Otherwise, call an update to remove collision boxes + this._update(); + } + } + + /** * Gets and sets a Boolean indicating whether the map should color-code * each fragment to show how many times it has been shaded. * White fragments have been shaded 8 or more times. @@ -4170,16 +3709,14 @@ class Map * @instance * @memberof Map */ - get showOverdrawInspector(): boolean { - return !!this._showOverdrawInspector; - } - set showOverdrawInspector(value: boolean) { - if (this._showOverdrawInspector === value) return; - this._showOverdrawInspector = value; - this._update(); - } - - /** + get showOverdrawInspector(): boolean { return !!this._showOverdrawInspector; } + set showOverdrawInspector(value: boolean) { + if (this._showOverdrawInspector === value) return; + this._showOverdrawInspector = value; + this._update(); + } + + /** * Gets and sets a Boolean indicating whether the map will * continuously repaint. This information is useful for analyzing performance. * @@ -4188,49 +3725,37 @@ class Map * @instance * @memberof Map */ - get repaint(): boolean { - return !!this._repaint; - } - set repaint(value: boolean) { - if (this._repaint !== value) { - this._repaint = value; - this.triggerRepaint(); - } - } - // show vertices - get vertices(): boolean { - return !!this._vertices; - } - set vertices(value: boolean) { - this._vertices = value; - this._update(); - } - - /** + get repaint(): boolean { return !!this._repaint; } + set repaint(value: boolean) { + if (this._repaint !== value) { + this._repaint = value; + this.triggerRepaint(); + } + } + // show vertices + get vertices(): boolean { return !!this._vertices; } + set vertices(value: boolean) { this._vertices = value; this._update(); } + + /** * Display tile AABBs for debugging * * @private * @type {boolean} */ - get showTileAABBs(): boolean { - return !!this._showTileAABBs; - } - set showTileAABBs(value: boolean) { - if (this._showTileAABBs === value) return; - this._showTileAABBs = value; - if (!value) { - Debug.clearAabbs(); - return; - } - this._update(); - } - - // for cache browser tests - _setCacheLimits(limit: number, checkThreshold: number) { - setCacheLimits(limit, checkThreshold); - } - - /** + get showTileAABBs(): boolean { return !!this._showTileAABBs; } + set showTileAABBs(value: boolean) { + if (this._showTileAABBs === value) return; + this._showTileAABBs = value; + if (!value) { Debug.clearAabbs(); return; } + this._update(); + } + + // for cache browser tests + _setCacheLimits(limit: number, checkThreshold: number) { + setCacheLimits(limit, checkThreshold); + } + + /** * The version of Mapbox GL JS in use as specified in package.json, CHANGELOG.md, and the GitHub release. * * @name version @@ -4239,9 +3764,7 @@ class Map * @var {string} version */ - get version(): string { - return version; - } + get version(): string { return version; } } export default Map; From 2647e714a4bc5971226682684220d79e3fa07bb0 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 14:19:23 +0200 Subject: [PATCH 50/72] Upgrade Flow to v0.184.0 --- .flowconfig | 2 +- package.json | 2 +- src/source/worker_tile.js | 2 +- src/util/struct_array.js | 2 +- src/util/web_worker_transfer.js | 2 +- yarn.lock | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.flowconfig b/.flowconfig index a2921fe2270..73c818edc76 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.183.0 +0.184.0 [options] diff --git a/package.json b/package.json index c94228603f6..d02bc7fbd52 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.183.0", + "flow-bin": "0.184.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 153566bb386..010756c236e 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -155,7 +155,7 @@ class WorkerTile { const bucket = buckets[layer.id] = layer.createBucket({ index: featureIndex.bucketLayerIDs.length, - // $FlowFixMe[incompatible-call] - Flow can;t infer proper `family` type from `layer` above + // $FlowFixMe[incompatible-call] - Flow can't infer proper `family` type from `layer` above layers: family, zoom: this.zoom, canonical: this.canonical, diff --git a/src/util/struct_array.js b/src/util/struct_array.js index eddcb4b261d..ccdd0b70f17 100644 --- a/src/util/struct_array.js +++ b/src/util/struct_array.js @@ -135,7 +135,7 @@ class StructArray { static deserialize(input: SerializedStructArray): StructArray { // $FlowFixMe not-an-object - newer Flow doesn't understand this pattern, silence for now - const structArray = Object.create(this.prototype); + const structArray: {[_: string]: any} = Object.create(this.prototype); structArray.arrayBuffer = input.arrayBuffer; structArray.length = input.length; structArray.capacity = input.arrayBuffer.byteLength / structArray.bytesPerElement; diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index fdb80ee6f8c..21b1ae8193b 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -248,7 +248,7 @@ export function deserialize(input: Serialized): mixed { return (klass.deserialize: typeof deserialize)(input); } - const result = Object.create(klass.prototype); + const result: {[_: string]: any} = Object.create(klass.prototype); for (const key of Object.keys(input)) { // $FlowFixMe[incompatible-type] diff --git a/yarn.lock b/yarn.lock index 8ce82aef4f8..0665dcd0e51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.183.0: - version "0.183.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.183.0.tgz#17f37c94edd04b705a897b5890dd6cdc02e0c94e" - integrity sha512-7IJHUnMPYgNEZU8t9M4vJII/G+fJft9C/INm2+HRSXx5KDF2j+vD2iap6+Yg2FWgXTnNLUvk7kr1QdO5Fk/8/Q== +flow-bin@0.184.0: + version "0.184.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.184.0.tgz#0256b3c302ce465b60d0f0296273840d38d3f9e6" + integrity sha512-HiHuxhO06dqhV7YabluSswm3ZgxVi2L+aArcuIJMON/CRzqkGQrRjIVNbKllMs95rFk6aeuFR3FdVCCUa0SbGw== follow-redirects@^1.0.0: version "1.15.1" From 67f61a76abdd70d07c4f9418f2b3a075462e4c2e Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 14:20:51 +0200 Subject: [PATCH 51/72] fix formatting for ui/handler_manager.js --- src/ui/handler_manager.js | 1171 +++++++++++++++++-------------------- 1 file changed, 543 insertions(+), 628 deletions(-) diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 7a47c078a1a..372fa18a07b 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -144,638 +144,553 @@ function hasChange(result: HandlerResult) { } class HandlerManager { - _map: Map; - _el: HTMLElement; - _handlers: Array<{ handlerName: string, handler: Handler, allowed: any }>; - _eventsInProgress: Object; - _frameId: ?number; - _inertia: HandlerInertia; - _bearingSnap: number; - _handlersById: { [string]: Handler }; - _updatingCamera: boolean; - _changes: Array<[HandlerResult, Object, any]>; - _previousActiveHandlers: { [string]: Handler }; - _listeners: Array<[HTMLElement, string, void | EventListenerOptionsOrUseCapture]>; - _trackingEllipsoid: TrackingEllipsoid; - _dragOrigin: ?Vec3; - - constructor(map: Map, options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number, bearingSnap: number}) { - this._map = map; - this._el = this._map.getCanvasContainer(); - this._handlers = []; - this._handlersById = {}; - this._changes = []; - - this._inertia = new HandlerInertia(map); - this._bearingSnap = options.bearingSnap; - this._previousActiveHandlers = {}; - this._trackingEllipsoid = new TrackingEllipsoid(); - this._dragOrigin = null; - - // Track whether map is currently moving, to compute start/move/end events - this._eventsInProgress = {}; - - this._addDefaultHandlers(options); - - bindAll(['handleEvent', 'handleWindowEvent'], this); - - const el = this._el; - - this._listeners = [ - // This needs to be `passive: true` so that a double tap fires two - // pairs of touchstart/end events in iOS Safari 13. If this is set to - // `passive: false` then the second pair of events is only fired if - // preventDefault() is called on the first touchstart. Calling preventDefault() - // undesirably prevents click events. - [el, 'touchstart', {passive: true}], - // This needs to be `passive: false` so that scrolls and pinches can be - // prevented in browsers that don't support `touch-actions: none`, for example iOS Safari 12. - [el, 'touchmove', {passive: false}], - [el, 'touchend', undefined], - [el, 'touchcancel', undefined], - - [el, 'mousedown', undefined], - [el, 'mousemove', undefined], - [el, 'mouseup', undefined], - - // Bind window-level event listeners for move and up/end events. In the absence of - // the pointer capture API, which is not supported by all necessary platforms, - // window-level event listeners give us the best shot at capturing events that - // fall outside the map canvas element. Use `{capture: true}` for the move event - // to prevent map move events from being fired during a drag. - [window.document, 'mousemove', {capture: true}], - [window.document, 'mouseup', undefined], - - [el, 'mouseover', undefined], - [el, 'mouseout', undefined], - [el, 'dblclick', undefined], - [el, 'click', undefined], - - [el, 'keydown', {capture: false}], - [el, 'keyup', undefined], - - [el, 'wheel', {passive: false}], - [el, 'contextmenu', undefined], - - [window, 'blur', undefined] - ]; - - for (const [target, type, listenerOptions] of this._listeners) { - const listener = target === window.document ? - this.handleWindowEvent : - this.handleEvent; - target.addEventListener((type: any), (listener: any), listenerOptions); - } - } - - destroy() { - for (const [target, type, listenerOptions] of this._listeners) { - const listener = target === window.document ? - this.handleWindowEvent : - this.handleEvent; - target.removeEventListener((type: any), (listener: any), listenerOptions); - } - } - - _addDefaultHandlers(options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number }) { - const map = this._map; - const el = map.getCanvasContainer(); - this._add('mapEvent', new MapEventHandler(map, options)); - - const boxZoom = map.boxZoom = new BoxZoomHandler(map, options); - this._add('boxZoom', boxZoom); - - const tapZoom = new TapZoomHandler(); - const clickZoom = new ClickZoomHandler(); - map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom); - this._add('tapZoom', tapZoom); - this._add('clickZoom', clickZoom); - - const tapDragZoom = new TapDragZoomHandler(); - this._add('tapDragZoom', tapDragZoom); - - const touchPitch = map.touchPitch = new TouchPitchHandler(map); - this._add('touchPitch', touchPitch); - - const mouseRotate = new MouseRotateHandler(options); - const mousePitch = new MousePitchHandler(options); - map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch); - // $FlowFixMe[method-unbinding] - this._add('mouseRotate', mouseRotate, ['mousePitch']); - // $FlowFixMe[method-unbinding] - this._add('mousePitch', mousePitch, ['mouseRotate']); - - const mousePan = new MousePanHandler(options); - const touchPan = new TouchPanHandler(map, options); - map.dragPan = new DragPanHandler(el, mousePan, touchPan); - // $FlowFixMe[method-unbinding] - this._add('mousePan', mousePan); - this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); - - const touchRotate = new TouchRotateHandler(); - const touchZoom = new TouchZoomHandler(); - map.touchZoomRotate = new TouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom); - this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']); - this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']); - - this._add('blockableMapEvent', new BlockableMapEventHandler(map)); - - const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, this); - this._add('scrollZoom', scrollZoom, ['mousePan']); - - const keyboard = map.keyboard = new KeyboardHandler(); - this._add('keyboard', keyboard); - - for (const name of [ - 'boxZoom', - 'doubleClickZoom', - 'tapDragZoom', - 'touchPitch', - 'dragRotate', - 'dragPan', - 'touchZoomRotate', - 'scrollZoom', - 'keyboard', - ]) { - if (options.interactive && (options: any)[name]) { - (map: any)[name].enable((options: any)[name]); - } - } - } - - _add(handlerName: string, handler: Handler, allowed?: Array) { - this._handlers.push({handlerName, handler, allowed}); - this._handlersById[handlerName] = handler; - } - - stop(allowEndAnimation: boolean) { - // do nothing if this method was triggered by a gesture update - if (this._updatingCamera) return; - - for (const {handler} of this._handlers) { - handler.reset(); - } - this._inertia.clear(); - this._fireEvents({}, {}, allowEndAnimation); - this._changes = []; - } - - isActive(): boolean { - for (const {handler} of this._handlers) { - if (handler.isActive()) return true; - } - return false; - } - - isZooming(): boolean { - return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming(); - } - - isRotating(): boolean { - return !!this._eventsInProgress.rotate; - } - - isMoving(): boolean { - return !!isMoving(this._eventsInProgress) || this.isZooming(); - } - - _isDragging(): boolean { - return !!this._eventsInProgress.drag; - } - - _blockedByActive( - activeHandlers: { [string]: Handler }, - allowed: Array, - myName: string, - ): boolean { - for (const name in activeHandlers) { - if (name === myName) continue; - if (!allowed || allowed.indexOf(name) < 0) { - return true; - } - } - return false; - } - - handleWindowEvent: ((e: InputEvent) => void) = (e: InputEvent) => { - this.handleEvent(e, `${e.type}Window`); - }; - - _getMapTouches(touches: TouchList): TouchList { - const mapTouches = []; - for (const t of touches) { - const target = ((t.target: any): Node); - if (this._el.contains(target)) { - mapTouches.push(t); - } - } - return ((mapTouches: any): TouchList); - } - - handleEvent: ((e: InputEvent | RenderFrameEvent, eventName?: string) => void) = (e: InputEvent | RenderFrameEvent, eventName?: string) => { - this._updatingCamera = true; - assert(e.timeStamp !== undefined); - - const isRenderFrame = e.type === 'renderFrame'; - const inputEvent = isRenderFrame ? undefined : ((e: any): InputEvent); - - /* + _map: Map; + _el: HTMLElement; + _handlers: Array<{ handlerName: string, handler: Handler, allowed: any }>; + _eventsInProgress: Object; + _frameId: ?number; + _inertia: HandlerInertia; + _bearingSnap: number; + _handlersById: { [string]: Handler }; + _updatingCamera: boolean; + _changes: Array<[HandlerResult, Object, any]>; + _previousActiveHandlers: { [string]: Handler }; + _listeners: Array<[HTMLElement, string, void | EventListenerOptionsOrUseCapture]>; + _trackingEllipsoid: TrackingEllipsoid; + _dragOrigin: ?Vec3; + + constructor(map: Map, options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number, bearingSnap: number}) { + this._map = map; + this._el = this._map.getCanvasContainer(); + this._handlers = []; + this._handlersById = {}; + this._changes = []; + + this._inertia = new HandlerInertia(map); + this._bearingSnap = options.bearingSnap; + this._previousActiveHandlers = {}; + this._trackingEllipsoid = new TrackingEllipsoid(); + this._dragOrigin = null; + + // Track whether map is currently moving, to compute start/move/end events + this._eventsInProgress = {}; + + this._addDefaultHandlers(options); + + bindAll(['handleEvent', 'handleWindowEvent'], this); + + const el = this._el; + + this._listeners = [ + // This needs to be `passive: true` so that a double tap fires two + // pairs of touchstart/end events in iOS Safari 13. If this is set to + // `passive: false` then the second pair of events is only fired if + // preventDefault() is called on the first touchstart. Calling preventDefault() + // undesirably prevents click events. + [el, 'touchstart', {passive: true}], + // This needs to be `passive: false` so that scrolls and pinches can be + // prevented in browsers that don't support `touch-actions: none`, for example iOS Safari 12. + [el, 'touchmove', {passive: false}], + [el, 'touchend', undefined], + [el, 'touchcancel', undefined], + + [el, 'mousedown', undefined], + [el, 'mousemove', undefined], + [el, 'mouseup', undefined], + + // Bind window-level event listeners for move and up/end events. In the absence of + // the pointer capture API, which is not supported by all necessary platforms, + // window-level event listeners give us the best shot at capturing events that + // fall outside the map canvas element. Use `{capture: true}` for the move event + // to prevent map move events from being fired during a drag. + [window.document, 'mousemove', {capture: true}], + [window.document, 'mouseup', undefined], + + [el, 'mouseover', undefined], + [el, 'mouseout', undefined], + [el, 'dblclick', undefined], + [el, 'click', undefined], + + [el, 'keydown', {capture: false}], + [el, 'keyup', undefined], + + [el, 'wheel', {passive: false}], + [el, 'contextmenu', undefined], + + [window, 'blur', undefined] + ]; + + for (const [target, type, listenerOptions] of this._listeners) { + const listener = target === window.document ? this.handleWindowEvent : this.handleEvent; + target.addEventListener((type: any), (listener: any), listenerOptions); + } + } + + destroy() { + for (const [target, type, listenerOptions] of this._listeners) { + const listener = target === window.document ? this.handleWindowEvent : this.handleEvent; + target.removeEventListener((type: any), (listener: any), listenerOptions); + } + } + + _addDefaultHandlers(options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number }) { + const map = this._map; + const el = map.getCanvasContainer(); + this._add('mapEvent', new MapEventHandler(map, options)); + + const boxZoom = map.boxZoom = new BoxZoomHandler(map, options); + this._add('boxZoom', boxZoom); + + const tapZoom = new TapZoomHandler(); + const clickZoom = new ClickZoomHandler(); + map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom); + this._add('tapZoom', tapZoom); + this._add('clickZoom', clickZoom); + + const tapDragZoom = new TapDragZoomHandler(); + this._add('tapDragZoom', tapDragZoom); + + const touchPitch = map.touchPitch = new TouchPitchHandler(map); + this._add('touchPitch', touchPitch); + + const mouseRotate = new MouseRotateHandler(options); + const mousePitch = new MousePitchHandler(options); + map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch); + // $FlowFixMe[method-unbinding] + this._add('mouseRotate', mouseRotate, ['mousePitch']); + // $FlowFixMe[method-unbinding] + this._add('mousePitch', mousePitch, ['mouseRotate']); + + const mousePan = new MousePanHandler(options); + const touchPan = new TouchPanHandler(map, options); + map.dragPan = new DragPanHandler(el, mousePan, touchPan); + // $FlowFixMe[method-unbinding] + this._add('mousePan', mousePan); + this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); + + const touchRotate = new TouchRotateHandler(); + const touchZoom = new TouchZoomHandler(); + map.touchZoomRotate = new TouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom); + this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']); + this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']); + + this._add('blockableMapEvent', new BlockableMapEventHandler(map)); + + const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, this); + this._add('scrollZoom', scrollZoom, ['mousePan']); + + const keyboard = map.keyboard = new KeyboardHandler(); + this._add('keyboard', keyboard); + + for (const name of ['boxZoom', 'doubleClickZoom', 'tapDragZoom', 'touchPitch', 'dragRotate', 'dragPan', 'touchZoomRotate', 'scrollZoom', 'keyboard']) { + if (options.interactive && (options: any)[name]) { + (map: any)[name].enable((options: any)[name]); + } + } + } + + _add(handlerName: string, handler: Handler, allowed?: Array) { + this._handlers.push({handlerName, handler, allowed}); + this._handlersById[handlerName] = handler; + } + + stop(allowEndAnimation: boolean) { + // do nothing if this method was triggered by a gesture update + if (this._updatingCamera) return; + + for (const {handler} of this._handlers) { + handler.reset(); + } + this._inertia.clear(); + this._fireEvents({}, {}, allowEndAnimation); + this._changes = []; + } + + isActive(): boolean { + for (const {handler} of this._handlers) { + if (handler.isActive()) return true; + } + return false; + } + + isZooming(): boolean { + return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming(); + } + + isRotating(): boolean { + return !!this._eventsInProgress.rotate; + } + + isMoving(): boolean { + return !!isMoving(this._eventsInProgress) || this.isZooming(); + } + + _isDragging(): boolean { + return !!this._eventsInProgress.drag; + } + + _blockedByActive(activeHandlers: { [string]: Handler }, allowed: Array, myName: string): boolean { + for (const name in activeHandlers) { + if (name === myName) continue; + if (!allowed || allowed.indexOf(name) < 0) { + return true; + } + } + return false; + } + + handleWindowEvent: ((e: InputEvent) => void) = (e: InputEvent) => { + this.handleEvent(e, `${e.type}Window`); + } + + _getMapTouches(touches: TouchList): TouchList { + const mapTouches = []; + for (const t of touches) { + const target = ((t.target: any): Node); + if (this._el.contains(target)) { + mapTouches.push(t); + } + } + return ((mapTouches: any): TouchList); + } + + handleEvent: ((e: InputEvent | RenderFrameEvent, eventName?: string) => void) = (e: InputEvent | RenderFrameEvent, eventName?: string) => { + + this._updatingCamera = true; + assert(e.timeStamp !== undefined); + + const isRenderFrame = e.type === 'renderFrame'; + const inputEvent = isRenderFrame ? undefined : ((e: any): InputEvent); + + /* * We don't call e.preventDefault() for any events by default. * Handlers are responsible for calling it where necessary. */ - const mergedHandlerResult: HandlerResult = {needsRenderFrame: false}; - const eventsInProgress = {}; - const activeHandlers = {}; - - const mapTouches = e.touches ? - this._getMapTouches(((e: any): TouchEvent).touches) : - undefined; - const points = mapTouches ? - DOM.touchPos(this._el, mapTouches) : - isRenderFrame ? - undefined : // renderFrame event doesn't have any points - DOM.mousePos(this._el, ((e: any): MouseEvent)); - - for (const {handlerName, handler, allowed} of this._handlers) { - if (!handler.isEnabled()) continue; - - let data: ?HandlerResult; - if (this._blockedByActive(activeHandlers, allowed, handlerName)) { - handler.reset(); - } else { - if ((handler: any)[eventName || e.type]) { - data = (handler: any)[eventName || e.type](e, points, mapTouches); - this.mergeHandlerResult( - mergedHandlerResult, - eventsInProgress, - data, - handlerName, - inputEvent, - ); - if (data && data.needsRenderFrame) { - this._triggerRenderFrame(); - } - } - } - - if (data || handler.isActive()) { - activeHandlers[handlerName] = handler; - } - } - - const deactivatedHandlers = {}; - for (const name in this._previousActiveHandlers) { - if (!activeHandlers[name]) { - deactivatedHandlers[name] = inputEvent; - } - } - this._previousActiveHandlers = activeHandlers; - - if ( - Object.keys(deactivatedHandlers).length || hasChange(mergedHandlerResult) - ) { - this._changes.push( - [mergedHandlerResult, eventsInProgress, deactivatedHandlers], - ); - this._triggerRenderFrame(); - } - - if (Object.keys(activeHandlers).length || hasChange(mergedHandlerResult)) { - this._map._stop(true); - } - - this._updatingCamera = false; - - const {cameraAnimation} = mergedHandlerResult; - if (cameraAnimation) { - this._inertia.clear(); - this._fireEvents({}, {}, true); - this._changes = []; - cameraAnimation(this._map); - } - }; - - mergeHandlerResult( - mergedHandlerResult: HandlerResult, - eventsInProgress: Object, - handlerResult: HandlerResult, - name: string, - e?: InputEvent, - ) { - if (!handlerResult) return; - - extend(mergedHandlerResult, handlerResult); - - const eventData = { - handlerName: name, - originalEvent: handlerResult.originalEvent || e, - }; - - // track which handler changed which camera property - if (handlerResult.zoomDelta !== undefined) { - eventsInProgress.zoom = eventData; - } - if (handlerResult.panDelta !== undefined) { - eventsInProgress.drag = eventData; - } - if (handlerResult.pitchDelta !== undefined) { - eventsInProgress.pitch = eventData; - } - if (handlerResult.bearingDelta !== undefined) { - eventsInProgress.rotate = eventData; - } - } - - _applyChanges() { - const combined = {}; - const combinedEventsInProgress = {}; - const combinedDeactivatedHandlers = {}; - - for (const [change, eventsInProgress, deactivatedHandlers] of this._changes) { - if (change.panDelta) - combined.panDelta = (combined.panDelta || new Point(0, 0))._add( - change.panDelta, - ); - if (change.zoomDelta) - combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta; - if (change.bearingDelta) - combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; - if (change.pitchDelta) - combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; - if (change.around !== undefined) combined.around = change.around; - if (change.aroundCoord !== undefined) - combined.aroundCoord = change.aroundCoord; - if (change.pinchAround !== undefined) - combined.pinchAround = change.pinchAround; - if (change.noInertia) combined.noInertia = change.noInertia; - - extend(combinedEventsInProgress, eventsInProgress); - extend(combinedDeactivatedHandlers, deactivatedHandlers); - } - - this._updateMapTransform( - combined, - combinedEventsInProgress, - combinedDeactivatedHandlers, - ); - this._changes = []; - } - - _updateMapTransform( - combinedResult: any, - combinedEventsInProgress: Object, - deactivatedHandlers: Object, - ) { - const map = this._map; - const tr = map.transform; - - const eventStarted = (type => { - const newEvent = combinedEventsInProgress[type]; - return newEvent && !this._eventsInProgress[type]; - }); - - const eventEnded = (type => { - const event = this._eventsInProgress[type]; - return event && !this._handlersById[event.handlerName].isActive(); - }); - - const toVec3 = ((p: MercatorCoordinate): Vec3 => [p.x, p.y, p.z]); - - if (eventEnded("drag") && !hasChange(combinedResult)) { - const preZoom = tr.zoom; - tr.cameraElevationReference = "sea"; - tr.recenterOnTerrain(); - tr.cameraElevationReference = "ground"; - // Map zoom might change during the pan operation due to terrain elevation. - if (preZoom !== tr.zoom) this._map._update(true); - } - - // Catches double click and double tap zooms when camera is constrained over terrain - if (tr._isCameraConstrained) map._stop(true); - - if (!hasChange(combinedResult)) { - this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); - return; - } - - let { - panDelta, - zoomDelta, - bearingDelta, - pitchDelta, - around, - aroundCoord, - pinchAround - } = combinedResult; - - if (tr._isCameraConstrained) { - // Catches wheel zoom events when camera is constrained over terrain - if (zoomDelta > 0) zoomDelta = 0; - tr._isCameraConstrained = false; - } - - if (pinchAround !== undefined) { - around = pinchAround; - } - - if ((zoomDelta || eventStarted("drag")) && around) { - this._dragOrigin = toVec3(tr.pointCoordinate3D(around)); - // Construct the tracking ellipsoid every time user changes the zoom or drag origin. - // Direction of the ray will define size of the shape and hence defining the available range of movement - this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin); - } - - // All movement of the camera is done relative to the sea level - tr.cameraElevationReference = "sea"; - - // stop any ongoing camera animations (easeTo, flyTo) - map._stop(true); - - around = around || map.transform.centerPoint; - if (bearingDelta) tr.bearing += bearingDelta; - if (pitchDelta) tr.pitch += pitchDelta; - tr._updateCameraState(); - - // Compute Mercator 3D camera offset based on screenspace panDelta - const panVec = [0, 0, 0]; - if (panDelta) { - if (tr.projection.name === 'mercator') { - assert( - this._dragOrigin, - '_dragOrigin should have been setup with a previous dragstart', - ); - const startPoint = this._trackingEllipsoid.projectRay( - tr.screenPointToMercatorRay(around).dir, - ); - const endPoint = this._trackingEllipsoid.projectRay( - tr.screenPointToMercatorRay(around.sub(panDelta)).dir, - ); - panVec[0] = endPoint[0] - startPoint[0]; - panVec[1] = endPoint[1] - startPoint[1]; - } else { - const startPoint = tr.pointCoordinate(around); - if (tr.projection.name === 'globe') { - // Compute pan vector directly in pixel coordinates for the globe. - // Rotate the globe a bit faster when dragging near poles to compensate - // different pixel-per-meter ratios (ie. pixel-to-physical-rotation is lower) - panDelta = panDelta.rotate(-tr.angle); - const scale = tr._pixelsPerMercatorPixel / tr.worldSize; - panVec[0] = -panDelta.x * mercatorScale( - latFromMercatorY(startPoint.y), - ) * scale; - panVec[1] = -panDelta.y * mercatorScale(tr.center.lat) * scale; - } else { - const endPoint = tr.pointCoordinate(around.sub(panDelta)); - - if (startPoint && endPoint) { - panVec[0] = endPoint.x - startPoint.x; - panVec[1] = endPoint.y - startPoint.y; - } - } - } - } - - const originalZoom = tr.zoom; - // Compute Mercator 3D camera offset based on screenspace requested ZoomDelta - const zoomVec = [0, 0, 0]; - if (zoomDelta) { - // Zoom value has to be computed relative to a secondary map plane that is created from the terrain position below the cursor. - // This way the zoom interpolation can be kept linear and independent of the (possible) terrain elevation - const pickedPosition: Vec3 = aroundCoord ? - toVec3(aroundCoord) : - toVec3(tr.pointCoordinate3D(around)); - - const aroundRay = { - dir: vec3.normalize( - [], - vec3.sub([], pickedPosition, tr._camera.position), - ), - }; - if (aroundRay.dir[2] < 0) { - // Special handling is required if the ray created from the cursor is heading up. - // This scenario is possible if user is trying to zoom towards a feature like a hill or a mountain. - // Convert zoomDelta to a movement vector as if the camera would be orbiting around the picked point - const movement = tr.zoomDeltaToMovement(pickedPosition, zoomDelta); - vec3.scale(zoomVec, aroundRay.dir, movement); - } - } - - // Mutate camera state via CameraAPI - const translation = vec3.add(panVec, panVec, zoomVec); - tr._translateCameraConstrained(translation); - - if (zoomDelta && Math.abs(tr.zoom - originalZoom) > 0.0001) { - tr.recenterOnTerrain(); - } - - tr.cameraElevationReference = "ground"; - - this._map._update(); - if (!combinedResult.noInertia) this._inertia.record(combinedResult); - this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); - } - - _fireEvents( - newEventsInProgress: { [string]: Object }, - deactivatedHandlers: Object, - allowEndAnimation: boolean, - ) { - const wasMoving = isMoving(this._eventsInProgress); - const nowMoving = isMoving(newEventsInProgress); - - const startEvents = {}; - - for (const eventName in newEventsInProgress) { - const {originalEvent} = newEventsInProgress[eventName]; - if (!this._eventsInProgress[eventName]) { - startEvents[`${eventName}start`] = originalEvent; - } - this._eventsInProgress[eventName] = newEventsInProgress[eventName]; - } - - // fire start events only after this._eventsInProgress has been updated - if (!wasMoving && nowMoving) { - this._fireEvent('movestart', nowMoving.originalEvent); - } - - for (const name in startEvents) { - this._fireEvent(name, startEvents[name]); - } - - if (nowMoving) { - this._fireEvent('move', nowMoving.originalEvent); - } - - for (const eventName in newEventsInProgress) { - const {originalEvent} = newEventsInProgress[eventName]; - this._fireEvent(eventName, originalEvent); - } - - const endEvents = {}; - - let originalEndEvent; - for (const eventName in this._eventsInProgress) { - const {handlerName, originalEvent} = this._eventsInProgress[eventName]; - if (!this._handlersById[handlerName].isActive()) { - delete this._eventsInProgress[eventName]; - originalEndEvent = deactivatedHandlers[handlerName] || originalEvent; - endEvents[`${eventName}end`] = originalEndEvent; - } - } - - for (const name in endEvents) { - this._fireEvent(name, endEvents[name]); - } - - const stillMoving = isMoving(this._eventsInProgress); - if (allowEndAnimation && (wasMoving || nowMoving) && !stillMoving) { - this._updatingCamera = true; - const inertialEase = this._inertia._onMoveEnd( - this._map.dragPan._inertiaOptions, - ); - - const shouldSnapToNorth = (bearing => bearing !== 0 && - -this._bearingSnap < bearing && - bearing < this._bearingSnap); - - if (inertialEase) { - if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) { - inertialEase.bearing = 0; - } - this._map.easeTo(inertialEase, {originalEvent: originalEndEvent}); - } else { - this._map.fire( - new Event('moveend', {originalEvent: originalEndEvent}), - ); - if (shouldSnapToNorth(this._map.getBearing())) { - this._map.resetNorth(); - } - } - this._updatingCamera = false; - } - } - - _fireEvent(type: string, e: *) { - this._map.fire(new Event(type, e ? {originalEvent: e} : {})); - } - - _requestFrame(): number { - this._map.triggerRepaint(); - return this._map._renderTaskQueue.add( - timeStamp => { - this._frameId = undefined; - this.handleEvent(new RenderFrameEvent('renderFrame', {timeStamp})); - this._applyChanges(); - }, - ); - } - - _triggerRenderFrame() { - if (this._frameId === undefined) { - this._frameId = this._requestFrame(); - } - } + const mergedHandlerResult: HandlerResult = {needsRenderFrame: false}; + const eventsInProgress = {}; + const activeHandlers = {}; + + const mapTouches = e.touches ? this._getMapTouches(((e: any): TouchEvent).touches) : undefined; + const points = mapTouches ? DOM.touchPos(this._el, mapTouches) : + isRenderFrame ? undefined : // renderFrame event doesn't have any points + DOM.mousePos(this._el, ((e: any): MouseEvent)); + + for (const {handlerName, handler, allowed} of this._handlers) { + if (!handler.isEnabled()) continue; + + let data: ?HandlerResult; + if (this._blockedByActive(activeHandlers, allowed, handlerName)) { + handler.reset(); + + } else { + if ((handler: any)[eventName || e.type]) { + data = (handler: any)[eventName || e.type](e, points, mapTouches); + this.mergeHandlerResult(mergedHandlerResult, eventsInProgress, data, handlerName, inputEvent); + if (data && data.needsRenderFrame) { + this._triggerRenderFrame(); + } + } + } + + if (data || handler.isActive()) { + activeHandlers[handlerName] = handler; + } + } + + const deactivatedHandlers = {}; + for (const name in this._previousActiveHandlers) { + if (!activeHandlers[name]) { + deactivatedHandlers[name] = inputEvent; + } + } + this._previousActiveHandlers = activeHandlers; + + if (Object.keys(deactivatedHandlers).length || hasChange(mergedHandlerResult)) { + this._changes.push([mergedHandlerResult, eventsInProgress, deactivatedHandlers]); + this._triggerRenderFrame(); + } + + if (Object.keys(activeHandlers).length || hasChange(mergedHandlerResult)) { + this._map._stop(true); + } + + this._updatingCamera = false; + + const {cameraAnimation} = mergedHandlerResult; + if (cameraAnimation) { + this._inertia.clear(); + this._fireEvents({}, {}, true); + this._changes = []; + cameraAnimation(this._map); + } + } + + mergeHandlerResult(mergedHandlerResult: HandlerResult, eventsInProgress: Object, handlerResult: HandlerResult, name: string, e?: InputEvent) { + if (!handlerResult) return; + + extend(mergedHandlerResult, handlerResult); + + const eventData = {handlerName: name, originalEvent: handlerResult.originalEvent || e}; + + // track which handler changed which camera property + if (handlerResult.zoomDelta !== undefined) { + eventsInProgress.zoom = eventData; + } + if (handlerResult.panDelta !== undefined) { + eventsInProgress.drag = eventData; + } + if (handlerResult.pitchDelta !== undefined) { + eventsInProgress.pitch = eventData; + } + if (handlerResult.bearingDelta !== undefined) { + eventsInProgress.rotate = eventData; + } + } + + _applyChanges() { + const combined = {}; + const combinedEventsInProgress = {}; + const combinedDeactivatedHandlers = {}; + + for (const [change, eventsInProgress, deactivatedHandlers] of this._changes) { + + if (change.panDelta) combined.panDelta = (combined.panDelta || new Point(0, 0))._add(change.panDelta); + if (change.zoomDelta) combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta; + if (change.bearingDelta) combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; + if (change.pitchDelta) combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; + if (change.around !== undefined) combined.around = change.around; + if (change.aroundCoord !== undefined) combined.aroundCoord = change.aroundCoord; + if (change.pinchAround !== undefined) combined.pinchAround = change.pinchAround; + if (change.noInertia) combined.noInertia = change.noInertia; + + extend(combinedEventsInProgress, eventsInProgress); + extend(combinedDeactivatedHandlers, deactivatedHandlers); + } + + this._updateMapTransform(combined, combinedEventsInProgress, combinedDeactivatedHandlers); + this._changes = []; + } + + _updateMapTransform(combinedResult: any, combinedEventsInProgress: Object, deactivatedHandlers: Object) { + + const map = this._map; + const tr = map.transform; + + const eventStarted = (type) => { + const newEvent = combinedEventsInProgress[type]; + return newEvent && !this._eventsInProgress[type]; + }; + + const eventEnded = (type) => { + const event = this._eventsInProgress[type]; + return event && !this._handlersById[event.handlerName].isActive(); + }; + + const toVec3 = (p: MercatorCoordinate): Vec3 => [p.x, p.y, p.z]; + + if (eventEnded("drag") && !hasChange(combinedResult)) { + const preZoom = tr.zoom; + tr.cameraElevationReference = "sea"; + tr.recenterOnTerrain(); + tr.cameraElevationReference = "ground"; + // Map zoom might change during the pan operation due to terrain elevation. + if (preZoom !== tr.zoom) this._map._update(true); + } + + // Catches double click and double tap zooms when camera is constrained over terrain + if (tr._isCameraConstrained) map._stop(true); + + if (!hasChange(combinedResult)) { + this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); + return; + } + + let {panDelta, zoomDelta, bearingDelta, pitchDelta, around, aroundCoord, pinchAround} = combinedResult; + + if (tr._isCameraConstrained) { + // Catches wheel zoom events when camera is constrained over terrain + if (zoomDelta > 0) zoomDelta = 0; + tr._isCameraConstrained = false; + } + + if (pinchAround !== undefined) { + around = pinchAround; + } + + if ((zoomDelta || eventStarted("drag")) && around) { + this._dragOrigin = toVec3(tr.pointCoordinate3D(around)); + // Construct the tracking ellipsoid every time user changes the zoom or drag origin. + // Direction of the ray will define size of the shape and hence defining the available range of movement + this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin); + } + + // All movement of the camera is done relative to the sea level + tr.cameraElevationReference = "sea"; + + // stop any ongoing camera animations (easeTo, flyTo) + map._stop(true); + + around = around || map.transform.centerPoint; + if (bearingDelta) tr.bearing += bearingDelta; + if (pitchDelta) tr.pitch += pitchDelta; + tr._updateCameraState(); + + // Compute Mercator 3D camera offset based on screenspace panDelta + const panVec = [0, 0, 0]; + if (panDelta) { + if (tr.projection.name === 'mercator') { + assert(this._dragOrigin, '_dragOrigin should have been setup with a previous dragstart'); + const startPoint = this._trackingEllipsoid.projectRay(tr.screenPointToMercatorRay(around).dir); + const endPoint = this._trackingEllipsoid.projectRay(tr.screenPointToMercatorRay(around.sub(panDelta)).dir); + panVec[0] = endPoint[0] - startPoint[0]; + panVec[1] = endPoint[1] - startPoint[1]; + + } else { + const startPoint = tr.pointCoordinate(around); + if (tr.projection.name === 'globe') { + // Compute pan vector directly in pixel coordinates for the globe. + // Rotate the globe a bit faster when dragging near poles to compensate + // different pixel-per-meter ratios (ie. pixel-to-physical-rotation is lower) + panDelta = panDelta.rotate(-tr.angle); + const scale = tr._pixelsPerMercatorPixel / tr.worldSize; + panVec[0] = -panDelta.x * mercatorScale(latFromMercatorY(startPoint.y)) * scale; + panVec[1] = -panDelta.y * mercatorScale(tr.center.lat) * scale; + + } else { + const endPoint = tr.pointCoordinate(around.sub(panDelta)); + + if (startPoint && endPoint) { + panVec[0] = endPoint.x - startPoint.x; + panVec[1] = endPoint.y - startPoint.y; + } + } + } + } + + const originalZoom = tr.zoom; + // Compute Mercator 3D camera offset based on screenspace requested ZoomDelta + const zoomVec = [0, 0, 0]; + if (zoomDelta) { + // Zoom value has to be computed relative to a secondary map plane that is created from the terrain position below the cursor. + // This way the zoom interpolation can be kept linear and independent of the (possible) terrain elevation + const pickedPosition: Vec3 = aroundCoord ? toVec3(aroundCoord) : toVec3(tr.pointCoordinate3D(around)); + + const aroundRay = {dir: vec3.normalize([], vec3.sub([], pickedPosition, tr._camera.position))}; + if (aroundRay.dir[2] < 0) { + // Special handling is required if the ray created from the cursor is heading up. + // This scenario is possible if user is trying to zoom towards a feature like a hill or a mountain. + // Convert zoomDelta to a movement vector as if the camera would be orbiting around the picked point + const movement = tr.zoomDeltaToMovement(pickedPosition, zoomDelta); + vec3.scale(zoomVec, aroundRay.dir, movement); + } + } + + // Mutate camera state via CameraAPI + const translation = vec3.add(panVec, panVec, zoomVec); + tr._translateCameraConstrained(translation); + + if (zoomDelta && Math.abs(tr.zoom - originalZoom) > 0.0001) { + tr.recenterOnTerrain(); + } + + tr.cameraElevationReference = "ground"; + + this._map._update(); + if (!combinedResult.noInertia) this._inertia.record(combinedResult); + this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); + } + + _fireEvents(newEventsInProgress: { [string]: Object }, deactivatedHandlers: Object, allowEndAnimation: boolean) { + + const wasMoving = isMoving(this._eventsInProgress); + const nowMoving = isMoving(newEventsInProgress); + + const startEvents = {}; + + for (const eventName in newEventsInProgress) { + const {originalEvent} = newEventsInProgress[eventName]; + if (!this._eventsInProgress[eventName]) { + startEvents[`${eventName}start`] = originalEvent; + } + this._eventsInProgress[eventName] = newEventsInProgress[eventName]; + } + + // fire start events only after this._eventsInProgress has been updated + if (!wasMoving && nowMoving) { + this._fireEvent('movestart', nowMoving.originalEvent); + } + + for (const name in startEvents) { + this._fireEvent(name, startEvents[name]); + } + + if (nowMoving) { + this._fireEvent('move', nowMoving.originalEvent); + } + + for (const eventName in newEventsInProgress) { + const {originalEvent} = newEventsInProgress[eventName]; + this._fireEvent(eventName, originalEvent); + } + + const endEvents = {}; + + let originalEndEvent; + for (const eventName in this._eventsInProgress) { + const {handlerName, originalEvent} = this._eventsInProgress[eventName]; + if (!this._handlersById[handlerName].isActive()) { + delete this._eventsInProgress[eventName]; + originalEndEvent = deactivatedHandlers[handlerName] || originalEvent; + endEvents[`${eventName}end`] = originalEndEvent; + } + } + + for (const name in endEvents) { + this._fireEvent(name, endEvents[name]); + } + + const stillMoving = isMoving(this._eventsInProgress); + if (allowEndAnimation && (wasMoving || nowMoving) && !stillMoving) { + this._updatingCamera = true; + const inertialEase = this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions); + + const shouldSnapToNorth = bearing => bearing !== 0 && -this._bearingSnap < bearing && bearing < this._bearingSnap; + + if (inertialEase) { + if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) { + inertialEase.bearing = 0; + } + this._map.easeTo(inertialEase, {originalEvent: originalEndEvent}); + } else { + this._map.fire(new Event('moveend', {originalEvent: originalEndEvent})); + if (shouldSnapToNorth(this._map.getBearing())) { + this._map.resetNorth(); + } + } + this._updatingCamera = false; + } + + } + + _fireEvent(type: string, e: *) { + this._map.fire(new Event(type, e ? {originalEvent: e} : {})); + } + + _requestFrame(): number { + this._map.triggerRepaint(); + return this._map._renderTaskQueue.add(timeStamp => { + this._frameId = undefined; + this.handleEvent(new RenderFrameEvent('renderFrame', {timeStamp})); + this._applyChanges(); + }); + } + + _triggerRenderFrame() { + if (this._frameId === undefined) { + this._frameId = this._requestFrame(); + } + } } export default HandlerManager; From 18ae16928cc1c63b864b6394c9aa297e6c7604dd Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 14:37:49 +0200 Subject: [PATCH 52/72] fix formatting for rest of ui folder * hash.js * marker.js * popup.js * scroll_zoom.js --- src/ui/handler/scroll_zoom.js | 703 +++++++++---------- src/ui/hash.js | 211 +++--- src/ui/marker.js | 1235 ++++++++++++++++----------------- src/ui/popup.js | 689 +++++++++--------- 4 files changed, 1361 insertions(+), 1477 deletions(-) diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 2c073fb4e0f..1d4a4a2ec8f 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -32,54 +32,55 @@ const maxScalePerFrame = 2; * @see [Example: Disable scroll zoom](https://docs.mapbox.com/mapbox-gl-js/example/disable-scroll-zoom/) */ class ScrollZoomHandler { - _map: Map; - _el: HTMLElement; - _enabled: boolean; - _active: boolean; - _zooming: boolean; - _aroundCenter: boolean; - _aroundPoint: Point; - _aroundCoord: MercatorCoordinate; - _type: 'wheel' | 'trackpad' | null; - _lastValue: number; - _timeout: ?TimeoutID; // used for delayed-handling of a single wheel movement - _finishTimeout: ?TimeoutID; // used to delay final '{move,zoom}end' events - - _lastWheelEvent: any; - _lastWheelEventTime: number; - - _startZoom: ?number; - _targetZoom: ?number; - _delta: number; - _easing: ?((number) => number); - _prevEase: ?{ start: number, duration: number, easing: (_: number) => number }; - - _frameId: ?boolean; - _handler: HandlerManager; - - _defaultZoomRate: number; - _wheelZoomRate: number; - - _alertContainer: HTMLElement; // used to display the scroll zoom blocker alert - _alertTimer: TimeoutID; - - /** + _map: Map; + _el: HTMLElement; + _enabled: boolean; + _active: boolean; + _zooming: boolean; + _aroundCenter: boolean; + _aroundPoint: Point; + _aroundCoord: MercatorCoordinate; + _type: 'wheel' | 'trackpad' | null; + _lastValue: number; + _timeout: ?TimeoutID; // used for delayed-handling of a single wheel movement + _finishTimeout: ?TimeoutID; // used to delay final '{move,zoom}end' events + + _lastWheelEvent: any; + _lastWheelEventTime: number; + + _startZoom: ?number; + _targetZoom: ?number; + _delta: number; + _easing: ?((number) => number); + _prevEase: ?{start: number, duration: number, easing: (_: number) => number}; + + _frameId: ?boolean; + _handler: HandlerManager; + + _defaultZoomRate: number; + _wheelZoomRate: number; + + _alertContainer: HTMLElement; // used to display the scroll zoom blocker alert + _alertTimer: TimeoutID; + + /** * @private */ - constructor(map: Map, handler: HandlerManager) { - this._map = map; - this._el = map.getCanvasContainer(); - this._handler = handler; + constructor(map: Map, handler: HandlerManager) { + this._map = map; + this._el = map.getCanvasContainer(); + this._handler = handler; - this._delta = 0; + this._delta = 0; - this._defaultZoomRate = defaultZoomRate; - this._wheelZoomRate = wheelZoomRate; + this._defaultZoomRate = defaultZoomRate; + this._wheelZoomRate = wheelZoomRate; - bindAll(['_onTimeout', '_addScrollZoomBlocker', '_showBlockerAlert'], this); - } + bindAll(['_onTimeout', '_addScrollZoomBlocker', '_showBlockerAlert'], this); - /** + } + + /** * Sets the zoom rate of a trackpad. * * @param {number} [zoomRate=1/100] The rate used to scale trackpad movement to a zoom value. @@ -87,11 +88,11 @@ class ScrollZoomHandler { * // Speed up trackpad zoom * map.scrollZoom.setZoomRate(1 / 25); */ - setZoomRate(zoomRate: number) { - this._defaultZoomRate = zoomRate; - } + setZoomRate(zoomRate: number) { + this._defaultZoomRate = zoomRate; + } - /** + /** * Sets the zoom rate of a mouse wheel. * * @param {number} [wheelZoomRate=1/450] The rate used to scale mouse wheel movement to a zoom value. @@ -99,35 +100,35 @@ class ScrollZoomHandler { * // Slow down zoom of mouse wheel * map.scrollZoom.setWheelZoomRate(1 / 600); */ - setWheelZoomRate(wheelZoomRate: number) { - this._wheelZoomRate = wheelZoomRate; - } + setWheelZoomRate(wheelZoomRate: number) { + this._wheelZoomRate = wheelZoomRate; + } - /** + /** * Returns a Boolean indicating whether the "scroll to zoom" interaction is enabled. * * @returns {boolean} `true` if the "scroll to zoom" interaction is enabled. * @example * const isScrollZoomEnabled = map.scrollZoom.isEnabled(); */ - isEnabled(): boolean { - return !!this._enabled; - } + isEnabled(): boolean { + return !!this._enabled; + } - /* + /* * Active state is turned on and off with every scroll wheel event and is set back to false before the map * render is called, so _active is not a good candidate for determining if a scroll zoom animation is in * progress. */ - isActive(): boolean { - return this._active || this._finishTimeout !== undefined; - } + isActive(): boolean { + return this._active || this._finishTimeout !== undefined; + } - isZooming(): boolean { - return !!this._zooming; - } + isZooming(): boolean { + return !!this._zooming; + } - /** + /** * Enables the "scroll to zoom" interaction. * * @param {Object} [options] Options object. @@ -138,319 +139,285 @@ class ScrollZoomHandler { * @example * map.scrollZoom.enable({around: 'center'}); */ - enable(options: ?{ around?: 'center' }) { - if (this.isEnabled()) return; - this._enabled = true; - this._aroundCenter = !!options && options.around === 'center'; - if (this._map._cooperativeGestures) this._addScrollZoomBlocker(); - } - - /** + enable(options: ?{around?: 'center'}) { + if (this.isEnabled()) return; + this._enabled = true; + this._aroundCenter = !!options && options.around === 'center'; + if (this._map._cooperativeGestures) this._addScrollZoomBlocker(); + } + + /** * Disables the "scroll to zoom" interaction. * * @example * map.scrollZoom.disable(); */ - disable() { - if (!this.isEnabled()) return; - this._enabled = false; - if (this._map._cooperativeGestures) { - clearTimeout(this._alertTimer); - this._alertContainer.remove(); - } - } - - wheel: (e: WheelEvent) => void = (e) => { - if (!this.isEnabled()) return; - - if (this._map._cooperativeGestures) { - if (!e.ctrlKey && !e.metaKey && !this.isZooming() && !isFullscreen()) { - this._showBlockerAlert(); - return; - } else if (this._alertContainer.style.visibility !== 'hidden') { - // immediately hide alert if it is visible when ctrl or ⌘ is pressed while scroll zooming. - this._alertContainer.style.visibility = 'hidden'; - clearTimeout(this._alertTimer); - } - } - - // Remove `any` cast when https://github.com/facebook/flow/issues/4879 is fixed. - let value = e.deltaMode === (window.WheelEvent: any).DOM_DELTA_LINE ? - e.deltaY * 40 : - e.deltaY; - const now = browser.now(), - timeDelta = now - (this._lastWheelEventTime || 0); - - this._lastWheelEventTime = now; - - if (value !== 0 && value % wheelZoomDelta === 0) { - // This one is definitely a mouse wheel event. - this._type = 'wheel'; - } else if (value !== 0 && Math.abs(value) < 4) { - // This one is definitely a trackpad event because it is so small. - this._type = 'trackpad'; - } else if (timeDelta > 400) { - // This is likely a new scroll action. - this._type = null; - this._lastValue = value; - - // Start a timeout in case this was a singular event, and delay it by up to 40ms. - this._timeout = setTimeout(this._onTimeout, 40, e); - } else if (!this._type) { - // This is a repeating event, but we don't know the type of event just yet. - // If the delta per time is small, we assume it's a fast trackpad; otherwise we switch into wheel mode. - this._type = Math.abs(timeDelta * value) < 200 ? 'trackpad' : 'wheel'; - - // Make sure our delayed event isn't fired again, because we accumulate - // the previous event (which was less than 40ms ago) into this event. - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - value += this._lastValue; - } - } - - // Slow down zoom if shift key is held for more precise zooming - if (e.shiftKey && value) value = value / 4; - - // Only fire the callback if we actually know what type of scrolling device the user uses. - if (this._type) { - this._lastWheelEvent = e; - this._delta -= value; - if (!this._active) { - this._start(e); - } - } - - e.preventDefault(); - } - - _onTimeout: ((initialEvent: WheelEvent) => void) = (initialEvent: WheelEvent) => { - this._type = 'wheel'; - this._delta -= this._lastValue; - if (!this._active) { - this._start(initialEvent); - } - }; - - _start(e: WheelEvent) { - if (!this._delta) return; - - if (this._frameId) { - this._frameId = null; - } - - this._active = true; - if (!this.isZooming()) { - this._zooming = true; - } - - if (this._finishTimeout) { - clearTimeout(this._finishTimeout); - delete this._finishTimeout; - } - - const pos = DOM.mousePos(this._el, e); - this._aroundPoint = this._aroundCenter ? - this._map.transform.centerPoint : - pos; - this._aroundCoord = this._map.transform.pointCoordinate3D(this._aroundPoint); - this._targetZoom = undefined; - - if (!this._frameId) { - this._frameId = true; - this._handler._triggerRenderFrame(); - } - } - - renderFrame: () => ?HandlerResult = () => { - if (!this._frameId) return; - this._frameId = null; - - if (!this.isActive()) return; - - const tr = this._map.transform; - - // If projection wraps and center crosses the antimeridian, reset previous mouse scroll easing settings to resolve https://github.com/mapbox/mapbox-gl-js/issues/11910 - if ( - this._type === 'wheel' && tr.projection.wrap && - (tr._center.lng >= 180 || tr._center.lng <= -180) - ) { - this._prevEase = null; - this._easing = null; - this._lastWheelEvent = null; - this._lastWheelEventTime = 0; - } - - const startingZoom = (() => { - return tr._terrainEnabled() && this._aroundCoord ? - tr.computeZoomRelativeTo(this._aroundCoord) : - tr.zoom; - }); - - // if we've had scroll events since the last render frame, consume the - // accumulated delta, and update the target zoom level accordingly - if (this._delta !== 0) { - // For trackpad events and single mouse wheel ticks, use the default zoom rate - const zoomRate = this._type === 'wheel' && - Math.abs(this._delta) > wheelZoomDelta ? - this._wheelZoomRate : - this._defaultZoomRate; - // Scale by sigmoid of scroll wheel delta. - let scale = maxScalePerFrame / (1 + Math.exp( - -Math.abs(this._delta * zoomRate), - )); - - if (this._delta < 0 && scale !== 0) { - scale = 1 / scale; - } - - const startZoom = startingZoom(); - const startScale = Math.pow(2.0, startZoom); - - const fromScale = typeof this._targetZoom === 'number' ? - tr.zoomScale(this._targetZoom) : - startScale; - this._targetZoom = Math.min( - tr.maxZoom, - Math.max(tr.minZoom, tr.scaleZoom(fromScale * scale)), - ); - - // if this is a mouse wheel, refresh the starting zoom and easing - // function we're using to smooth out the zooming between wheel - // events - if (this._type === 'wheel') { - this._startZoom = startZoom; - this._easing = this._smoothOutEasing(200); - } - - this._delta = 0; - } - const targetZoom = typeof this._targetZoom === 'number' ? - this._targetZoom : - startingZoom(); - const startZoom = this._startZoom; - const easing = this._easing; - - let finished = false; - let zoom; - if (this._type === 'wheel' && startZoom && easing) { - assert(easing && typeof startZoom === 'number'); - - const t = Math.min((browser.now() - this._lastWheelEventTime) / 200, 1); - const k = easing(t); - zoom = interpolate(startZoom, targetZoom, k); - if (t < 1) { - if (!this._frameId) { - this._frameId = true; - } - } else { - finished = true; - } - } else { - zoom = targetZoom; - finished = true; - } - - this._active = true; - - if (finished) { - this._active = false; - this._finishTimeout = setTimeout( - () => { - this._zooming = false; - this._handler._triggerRenderFrame(); - delete this._targetZoom; + disable() { + if (!this.isEnabled()) return; + this._enabled = false; + if (this._map._cooperativeGestures) { + clearTimeout(this._alertTimer); + this._alertContainer.remove(); + } + } + + wheel: (e: WheelEvent) => void = (e) => { + if (!this.isEnabled()) return; + + if (this._map._cooperativeGestures) { + if (!e.ctrlKey && !e.metaKey && !this.isZooming() && !isFullscreen()) { + this._showBlockerAlert(); + return; + } else if (this._alertContainer.style.visibility !== 'hidden') { + // immediately hide alert if it is visible when ctrl or ⌘ is pressed while scroll zooming. + this._alertContainer.style.visibility = 'hidden'; + clearTimeout(this._alertTimer); + } + } + + // Remove `any` cast when https://github.com/facebook/flow/issues/4879 is fixed. + let value = e.deltaMode === (window.WheelEvent: any).DOM_DELTA_LINE ? e.deltaY * 40 : e.deltaY; + const now = browser.now(), + timeDelta = now - (this._lastWheelEventTime || 0); + + this._lastWheelEventTime = now; + + if (value !== 0 && (value % wheelZoomDelta) === 0) { + // This one is definitely a mouse wheel event. + this._type = 'wheel'; + + } else if (value !== 0 && Math.abs(value) < 4) { + // This one is definitely a trackpad event because it is so small. + this._type = 'trackpad'; + + } else if (timeDelta > 400) { + // This is likely a new scroll action. + this._type = null; + this._lastValue = value; + + // Start a timeout in case this was a singular event, and delay it by up to 40ms. + this._timeout = setTimeout(this._onTimeout, 40, e); + + } else if (!this._type) { + // This is a repeating event, but we don't know the type of event just yet. + // If the delta per time is small, we assume it's a fast trackpad; otherwise we switch into wheel mode. + this._type = (Math.abs(timeDelta * value) < 200) ? 'trackpad' : 'wheel'; + + // Make sure our delayed event isn't fired again, because we accumulate + // the previous event (which was less than 40ms ago) into this event. + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + value += this._lastValue; + } + } + + // Slow down zoom if shift key is held for more precise zooming + if (e.shiftKey && value) value = value / 4; + + // Only fire the callback if we actually know what type of scrolling device the user uses. + if (this._type) { + this._lastWheelEvent = e; + this._delta -= value; + if (!this._active) { + this._start(e); + } + } + + e.preventDefault(); + } + + _onTimeout: ((initialEvent: WheelEvent) => void) = (initialEvent: WheelEvent) => { + this._type = 'wheel'; + this._delta -= this._lastValue; + if (!this._active) { + this._start(initialEvent); + } + } + + _start(e: WheelEvent) { + if (!this._delta) return; + + if (this._frameId) { + this._frameId = null; + } + + this._active = true; + if (!this.isZooming()) { + this._zooming = true; + } + + if (this._finishTimeout) { + clearTimeout(this._finishTimeout); delete this._finishTimeout; - }, - 200, - ); - } - - return { - noInertia: true, - needsRenderFrame: !finished, - zoomDelta: zoom - startingZoom(), - around: this._aroundPoint, - aroundCoord: this._aroundCoord, - originalEvent: this._lastWheelEvent, - }; - } - - _smoothOutEasing(duration: number): (number) => number { - let easing = _ease; - - if (this._prevEase) { - const ease = this._prevEase, - t = (browser.now() - ease.start) / ease.duration, - speed = ease.easing(t + 0.01) - ease.easing(t), - // Quick hack to make new bezier that is continuous with last - x = 0.27 / Math.sqrt(speed * speed + 0.0001) * 0.01, - y = Math.sqrt(0.27 * 0.27 - x * x); - - easing = bezier(x, y, 0.25, 1); - } - - this._prevEase = { - start: browser.now(), - duration, - easing, - }; - - return easing; - } - - blur() { - this.reset(); - } - - reset() { - this._active = false; - } - - _addScrollZoomBlocker() { - if (this._map && !this._alertContainer) { - this._alertContainer = DOM.create( - 'div', - 'mapboxgl-scroll-zoom-blocker', - this._map._container, - ); - - if (/(Mac|iPad)/i.test(window.navigator.userAgent)) { - this._alertContainer.textContent = this._map._getUIString( - 'ScrollZoomBlocker.CmdMessage', - ); - } else { - this._alertContainer.textContent = this._map._getUIString( - 'ScrollZoomBlocker.CtrlMessage', - ); - } - - // dynamically set the font size of the scroll zoom blocker alert message - this._alertContainer.style.fontSize = `${Math.max( - 10, - Math.min(24, Math.floor(this._el.clientWidth * 0.05)), - )}px`; - } - } - - _showBlockerAlert() { - this._alertContainer.style.visibility = 'visible'; - this._alertContainer.classList.add('mapboxgl-scroll-zoom-blocker-show'); - this._alertContainer.setAttribute("role", "alert"); - - clearTimeout(this._alertTimer); - - this._alertTimer = setTimeout( - () => { - this._alertContainer.classList.remove( - 'mapboxgl-scroll-zoom-blocker-show', - ); - this._alertContainer.setAttribute("role", "null"); - }, - 200, - ); - } + } + + const pos = DOM.mousePos(this._el, e); + this._aroundPoint = this._aroundCenter ? this._map.transform.centerPoint : pos; + this._aroundCoord = this._map.transform.pointCoordinate3D(this._aroundPoint); + this._targetZoom = undefined; + + if (!this._frameId) { + this._frameId = true; + this._handler._triggerRenderFrame(); + } + } + + renderFrame: () => ?HandlerResult = () => { + if (!this._frameId) return; + this._frameId = null; + + if (!this.isActive()) return; + + const tr = this._map.transform; + + // If projection wraps and center crosses the antimeridian, reset previous mouse scroll easing settings to resolve https://github.com/mapbox/mapbox-gl-js/issues/11910 + if (this._type === 'wheel' && tr.projection.wrap && (tr._center.lng >= 180 || tr._center.lng <= -180)) { + this._prevEase = null; + this._easing = null; + this._lastWheelEvent = null; + this._lastWheelEventTime = 0; + } + + const startingZoom = () => { + return (tr._terrainEnabled() && this._aroundCoord) ? tr.computeZoomRelativeTo(this._aroundCoord) : tr.zoom; + }; + + // if we've had scroll events since the last render frame, consume the + // accumulated delta, and update the target zoom level accordingly + if (this._delta !== 0) { + // For trackpad events and single mouse wheel ticks, use the default zoom rate + const zoomRate = (this._type === 'wheel' && Math.abs(this._delta) > wheelZoomDelta) ? this._wheelZoomRate : this._defaultZoomRate; + // Scale by sigmoid of scroll wheel delta. + let scale = maxScalePerFrame / (1 + Math.exp(-Math.abs(this._delta * zoomRate))); + + if (this._delta < 0 && scale !== 0) { + scale = 1 / scale; + } + + const startZoom = startingZoom(); + const startScale = Math.pow(2.0, startZoom); + + const fromScale = typeof this._targetZoom === 'number' ? tr.zoomScale(this._targetZoom) : startScale; + this._targetZoom = Math.min(tr.maxZoom, Math.max(tr.minZoom, tr.scaleZoom(fromScale * scale))); + + // if this is a mouse wheel, refresh the starting zoom and easing + // function we're using to smooth out the zooming between wheel + // events + if (this._type === 'wheel') { + this._startZoom = startZoom; + this._easing = this._smoothOutEasing(200); + } + + this._delta = 0; + } + const targetZoom = typeof this._targetZoom === 'number' ? + this._targetZoom : startingZoom(); + const startZoom = this._startZoom; + const easing = this._easing; + + let finished = false; + let zoom; + if (this._type === 'wheel' && startZoom && easing) { + assert(easing && typeof startZoom === 'number'); + + const t = Math.min((browser.now() - this._lastWheelEventTime) / 200, 1); + const k = easing(t); + zoom = interpolate(startZoom, targetZoom, k); + if (t < 1) { + if (!this._frameId) { + this._frameId = true; + } + } else { + finished = true; + } + } else { + zoom = targetZoom; + finished = true; + } + + this._active = true; + + if (finished) { + this._active = false; + this._finishTimeout = setTimeout(() => { + this._zooming = false; + this._handler._triggerRenderFrame(); + delete this._targetZoom; + delete this._finishTimeout; + }, 200); + } + + return { + noInertia: true, + needsRenderFrame: !finished, + zoomDelta: zoom - startingZoom(), + around: this._aroundPoint, + aroundCoord: this._aroundCoord, + originalEvent: this._lastWheelEvent + }; + } + + _smoothOutEasing(duration: number): (number) => number { + let easing = _ease; + + if (this._prevEase) { + const ease = this._prevEase, + t = (browser.now() - ease.start) / ease.duration, + speed = ease.easing(t + 0.01) - ease.easing(t), + + // Quick hack to make new bezier that is continuous with last + x = 0.27 / Math.sqrt(speed * speed + 0.0001) * 0.01, + y = Math.sqrt(0.27 * 0.27 - x * x); + + easing = bezier(x, y, 0.25, 1); + } + + this._prevEase = { + start: browser.now(), + duration, + easing + }; + + return easing; + } + + blur() { + this.reset(); + } + + reset() { + this._active = false; + } + + _addScrollZoomBlocker() { + if (this._map && !this._alertContainer) { + this._alertContainer = DOM.create('div', 'mapboxgl-scroll-zoom-blocker', this._map._container); + + if (/(Mac|iPad)/i.test(window.navigator.userAgent)) { + this._alertContainer.textContent = this._map._getUIString('ScrollZoomBlocker.CmdMessage'); + } else { + this._alertContainer.textContent = this._map._getUIString('ScrollZoomBlocker.CtrlMessage'); + } + + // dynamically set the font size of the scroll zoom blocker alert message + this._alertContainer.style.fontSize = `${Math.max(10, Math.min(24, Math.floor(this._el.clientWidth * 0.05)))}px`; + } + } + + _showBlockerAlert() { + this._alertContainer.style.visibility = 'visible'; + this._alertContainer.classList.add('mapboxgl-scroll-zoom-blocker-show'); + this._alertContainer.setAttribute("role", "alert"); + + clearTimeout(this._alertTimer); + + this._alertTimer = setTimeout(() => { + this._alertContainer.classList.remove('mapboxgl-scroll-zoom-blocker-show'); + this._alertContainer.setAttribute("role", "null"); + }, 200); + } + } export default ScrollZoomHandler; diff --git a/src/ui/hash.js b/src/ui/hash.js index acccb4c2bb6..ce3a0dab0eb 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -13,126 +13,117 @@ import type Map from './map.js'; * @returns {Hash} `this` */ export default class Hash { - _map: ?Map; - _updateHash: () => ?TimeoutID; - _hashName: ?string; - - constructor(hashName: ?string) { - this._hashName = hashName && encodeURIComponent(hashName); - bindAll(['_getCurrentHash', '_onHashChange', '_updateHash'], this); - - // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. - this._updateHash = throttle( - this._updateHashUnthrottled.bind(this), - 30 * 1000 / 100, - ); - } - - /* + _map: ?Map; + _updateHash: () => ?TimeoutID; + _hashName: ?string; + + constructor(hashName: ?string) { + this._hashName = hashName && encodeURIComponent(hashName); + bindAll([ + '_getCurrentHash', + '_onHashChange', + '_updateHash' + ], this); + + // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. + this._updateHash = throttle(this._updateHashUnthrottled.bind(this), 30 * 1000 / 100); + } + + /* * Map element to listen for coordinate changes * * @param {Object} map * @returns {Hash} `this` */ - addTo(map: Map): this { - this._map = map; - window.addEventListener('hashchange', this._onHashChange, false); - map.on('moveend', this._updateHash); - return this; - } - - /* + addTo(map: Map): this { + this._map = map; + window.addEventListener('hashchange', this._onHashChange, false); + map.on('moveend', this._updateHash); + return this; + } + + /* * Removes hash * * @returns {Popup} `this` */ - remove(): this { - if (!this._map) return this; - - this._map.off('moveend', this._updateHash); - window.removeEventListener('hashchange', this._onHashChange, false); - clearTimeout(this._updateHash()); - - this._map = undefined; - return this; - } - - getHashString(): string { - const map = this._map; - if (!map) return ''; - - const hash = getHashString(map); - - if (this._hashName) { - const hashName = this._hashName; - let found = false; - const parts = window.location.hash.slice(1).split('&').map( - part => { - const key = part.split('=')[0]; - if (key === hashName) { - found = true; - return `${key}=${hash}`; + remove(): this { + if (!this._map) return this; + + this._map.off('moveend', this._updateHash); + window.removeEventListener('hashchange', this._onHashChange, false); + clearTimeout(this._updateHash()); + + this._map = undefined; + return this; + } + + getHashString(): string { + const map = this._map; + if (!map) return ''; + + const hash = getHashString(map); + + if (this._hashName) { + const hashName = this._hashName; + let found = false; + const parts = window.location.hash.slice(1).split('&').map(part => { + const key = part.split('=')[0]; + if (key === hashName) { + found = true; + return `${key}=${hash}`; + } + return part; + }).filter(a => a); + if (!found) { + parts.push(`${hashName}=${hash}`); } - return part; - }, - ).filter(a => a); - if (!found) { - parts.push(`${hashName}=${hash}`); - } - return `#${parts.join('&')}`; - } - - return `#${hash}`; - } - - _getCurrentHash(): Array { - // Get the current hash from location, stripped from its number sign - const hash = window.location.hash.replace('#', ''); - if (this._hashName) { - // Split the parameter-styled hash into parts and find the value we need - let keyval; - hash.split('&').map(part => part.split('=')).forEach( - part => { - if (part[0] === this._hashName) { - keyval = part; - } - }, - ); - return (keyval ? keyval[1] || '' : '').split('/'); - } - return hash.split('/'); - } - - _onHashChange: (() => boolean) = (): boolean => { - const map = this._map; - if (!map) return false; - const loc = this._getCurrentHash(); - if (loc.length >= 3 && !loc.some(v => isNaN(v))) { - const bearing = map.dragRotate.isEnabled() && - map.touchZoomRotate.isEnabled() ? - +(loc[3] || 0) : - map.getBearing(); - map.jumpTo( - { - center: [+loc[2], +loc[1]], - zoom: +loc[0], - bearing, - pitch: +(loc[4] || 0), - }, - ); - return true; - } - return false; - }; - - _updateHashUnthrottled: (() => void) = () => { - // Replace if already present, else append the updated hash string - const location = window.location.href.replace( - /(#.+)?$/, - this.getHashString(), - ); - window.history.replaceState(window.history.state, null, location); - }; + return `#${parts.join('&')}`; + } + + return `#${hash}`; + } + + _getCurrentHash(): Array { + // Get the current hash from location, stripped from its number sign + const hash = window.location.hash.replace('#', ''); + if (this._hashName) { + // Split the parameter-styled hash into parts and find the value we need + let keyval; + hash.split('&').map( + part => part.split('=') + ).forEach(part => { + if (part[0] === this._hashName) { + keyval = part; + } + }); + return (keyval ? keyval[1] || '' : '').split('/'); + } + return hash.split('/'); + } + + _onHashChange: (() => boolean) = (): boolean => { + const map = this._map; + if (!map) return false; + const loc = this._getCurrentHash(); + if (loc.length >= 3 && !loc.some(v => isNaN(v))) { + const bearing = map.dragRotate.isEnabled() && map.touchZoomRotate.isEnabled() ? +(loc[3] || 0) : map.getBearing(); + map.jumpTo({ + center: [+loc[2], +loc[1]], + zoom: +loc[0], + bearing, + pitch: +(loc[4] || 0) + }); + return true; + } + return false; + } + + _updateHashUnthrottled: (() => void) = () => { + // Replace if already present, else append the updated hash string + const location = window.location.href.replace(/(#.+)?$/, this.getHashString()); + window.history.replaceState(window.history.state, null, location); + } } export function getHashString(map: Map, mapFeedback?: boolean): string { diff --git a/src/ui/marker.js b/src/ui/marker.js index 89e47c85b6a..605c4b70419 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -61,135 +61,127 @@ type Options = { * @see [Example: Add custom icons with Markers](https://www.mapbox.com/mapbox-gl-js/example/custom-marker-icons/) * @see [Example: Create a draggable Marker](https://www.mapbox.com/mapbox-gl-js/example/drag-a-marker/) */ -export default class Marker - extends Evented { - _map: ?Map; - _anchor: Anchor; - _offset: Point; - _element: HTMLElement; - _popup: ?Popup; - _lngLat: LngLat; - _pos: ?Point; - _color: string; - _scale: number; - _defaultMarker: boolean; - _draggable: boolean; - _clickTolerance: number; - _isDragging: boolean; - _state: 'inactive' | 'pending' | 'active'; // used for handling drag events - _positionDelta: ?Point; - _pointerdownPos: ?Point; - _rotation: number; - _pitchAlignment: string; - _rotationAlignment: string; - _originalTabIndex: ?string; // original tabindex of _element - _fadeTimer: ?TimeoutID; - _updateFrameId: number; - _updateMoving: () => void; - _occludedOpacity: number; - - constructor(options?: Options, legacyOptions?: Options) { - super(); - // For backward compatibility -- the constructor used to accept the element as a - // required first argument, before it was made optional. - if (options instanceof window.HTMLElement || legacyOptions) { - options = extend({element: options}, legacyOptions); - } - - bindAll([ - '_update', - '_onMove', - '_onUp', - '_addDragHandler', - '_onMapClick', - '_onKeyPress', - '_clearFadeTimer' - ], this); - - this._anchor = (options && options.anchor) || 'center'; - this._color = (options && options.color) || '#3FB1CE'; - this._scale = (options && options.scale) || 1; - this._draggable = (options && options.draggable) || false; - this._clickTolerance = (options && options.clickTolerance) || 0; - this._isDragging = false; - this._state = 'inactive'; - this._rotation = (options && options.rotation) || 0; - this._rotationAlignment = (options && options.rotationAlignment) || 'auto'; - this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto'; - this._updateMoving = () => this._update(true); - this._occludedOpacity = (options && options.occludedOpacity) || 0.2; - - if (!options || !options.element) { - this._defaultMarker = true; - this._element = DOM.create('div'); - - // create default map marker SVG - - const DEFAULT_HEIGHT = 41; - const DEFAULT_WIDTH = 27; - - const svg = DOM.createSVG('svg', { - display: 'block', - height: `${DEFAULT_HEIGHT * this._scale}px`, - width: `${DEFAULT_WIDTH * this._scale}px`, - viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}` - }, this._element); - - const gradient = DOM.createSVG('radialGradient', {id: 'shadowGradient'}, DOM.createSVG('defs', {}, svg)); - DOM.createSVG('stop', {offset: '10%', 'stop-opacity': 0.4}, gradient); - DOM.createSVG('stop', {offset: '100%', 'stop-opacity': 0.05}, gradient); - DOM.createSVG('ellipse', {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, svg); // shadow - - DOM.createSVG('path', { // marker shape - fill: this._color, - d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z' - }, svg); - DOM.createSVG('path', { // border - opacity: 0.25, - d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z' - }, svg); - - DOM.createSVG('circle', {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, svg); // circle - - // if no element and no offset option given apply an offset for the default marker - // the -14 as the y value of the default marker offset was determined as follows - // - // the marker tip is at the center of the shadow ellipse from the default svg - // the y value of the center of the shadow ellipse relative to the svg top left is 34.8 - // offset to the svg center "height (41 / 2)" gives 34.8 - (41 / 2) and rounded for an integer pixel offset gives 14 - // negative is used to move the marker up from the center so the tip is at the Marker lngLat - this._offset = Point.convert((options && options.offset) || [0, -14]); - } else { - this._element = options.element; - this._offset = Point.convert((options && options.offset) || [0, 0]); - } - - if (!this._element.hasAttribute('aria-label')) - this._element.setAttribute('aria-label', 'Map marker'); - this._element.classList.add('mapboxgl-marker'); - this._element.addEventListener( - 'dragstart', - (e: DragEvent) => { - e.preventDefault(); - }, - ); - this._element.addEventListener( - 'mousedown', - (e: MouseEvent) => { - // prevent focusing on click - e.preventDefault(); - }, - ); - const classList = this._element.classList; - for (const key in anchorTranslate) { - classList.remove(`mapboxgl-marker-anchor-${key}`); - } - classList.add(`mapboxgl-marker-anchor-${this._anchor}`); - - this._popup = null; - } - - /** +export default class Marker extends Evented { + _map: ?Map; + _anchor: Anchor; + _offset: Point; + _element: HTMLElement; + _popup: ?Popup; + _lngLat: LngLat; + _pos: ?Point; + _color: string; + _scale: number; + _defaultMarker: boolean; + _draggable: boolean; + _clickTolerance: number; + _isDragging: boolean; + _state: 'inactive' | 'pending' | 'active'; // used for handling drag events + _positionDelta: ?Point; + _pointerdownPos: ?Point; + _rotation: number; + _pitchAlignment: string; + _rotationAlignment: string; + _originalTabIndex: ?string; // original tabindex of _element + _fadeTimer: ?TimeoutID; + _updateFrameId: number; + _updateMoving: () => void; + _occludedOpacity: number; + + constructor(options?: Options, legacyOptions?: Options) { + super(); + // For backward compatibility -- the constructor used to accept the element as a + // required first argument, before it was made optional. + if (options instanceof window.HTMLElement || legacyOptions) { + options = extend({element: options}, legacyOptions); + } + + bindAll([ + '_update', + '_onMove', + '_onUp', + '_addDragHandler', + '_onMapClick', + '_onKeyPress', + '_clearFadeTimer' + ], this); + + this._anchor = (options && options.anchor) || 'center'; + this._color = (options && options.color) || '#3FB1CE'; + this._scale = (options && options.scale) || 1; + this._draggable = (options && options.draggable) || false; + this._clickTolerance = (options && options.clickTolerance) || 0; + this._isDragging = false; + this._state = 'inactive'; + this._rotation = (options && options.rotation) || 0; + this._rotationAlignment = (options && options.rotationAlignment) || 'auto'; + this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto'; + this._updateMoving = () => this._update(true); + this._occludedOpacity = (options && options.occludedOpacity) || 0.2; + + if (!options || !options.element) { + this._defaultMarker = true; + this._element = DOM.create('div'); + + // create default map marker SVG + + const DEFAULT_HEIGHT = 41; + const DEFAULT_WIDTH = 27; + + const svg = DOM.createSVG('svg', { + display: 'block', + height: `${DEFAULT_HEIGHT * this._scale}px`, + width: `${DEFAULT_WIDTH * this._scale}px`, + viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}` + }, this._element); + + const gradient = DOM.createSVG('radialGradient', {id: 'shadowGradient'}, DOM.createSVG('defs', {}, svg)); + DOM.createSVG('stop', {offset: '10%', 'stop-opacity': 0.4}, gradient); + DOM.createSVG('stop', {offset: '100%', 'stop-opacity': 0.05}, gradient); + DOM.createSVG('ellipse', {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, svg); // shadow + + DOM.createSVG('path', { // marker shape + fill: this._color, + d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z' + }, svg); + DOM.createSVG('path', { // border + opacity: 0.25, + d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z' + }, svg); + + DOM.createSVG('circle', {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, svg); // circle + + // if no element and no offset option given apply an offset for the default marker + // the -14 as the y value of the default marker offset was determined as follows + // + // the marker tip is at the center of the shadow ellipse from the default svg + // the y value of the center of the shadow ellipse relative to the svg top left is 34.8 + // offset to the svg center "height (41 / 2)" gives 34.8 - (41 / 2) and rounded for an integer pixel offset gives 14 + // negative is used to move the marker up from the center so the tip is at the Marker lngLat + this._offset = Point.convert((options && options.offset) || [0, -14]); + } else { + this._element = options.element; + this._offset = Point.convert((options && options.offset) || [0, 0]); + } + + if (!this._element.hasAttribute('aria-label')) this._element.setAttribute('aria-label', 'Map marker'); + this._element.classList.add('mapboxgl-marker'); + this._element.addEventListener('dragstart', (e: DragEvent) => { + e.preventDefault(); + }); + this._element.addEventListener('mousedown', (e: MouseEvent) => { + // prevent focusing on click + e.preventDefault(); + }); + const classList = this._element.classList; + for (const key in anchorTranslate) { + classList.remove(`mapboxgl-marker-anchor-${key}`); + } + classList.add(`mapboxgl-marker-anchor-${this._anchor}`); + + this._popup = null; + } + + /** * Attaches the `Marker` to a `Map` object. * * @param {Map} map The Mapbox GL JS map to add the marker to. @@ -199,29 +191,29 @@ export default class Marker * .setLngLat([30.5, 50.5]) * .addTo(map); // add the marker to the map */ - addTo(map: Map): this { - if (map === this._map) { - return this; - } - this.remove(); - this._map = map; - map.getCanvasContainer().appendChild(this._element); - map.on('move', this._updateMoving); - map.on('moveend', this._update); - map.on('remove', this._clearFadeTimer); - map._addMarker(this); - this.setDraggable(this._draggable); - this._update(); - - // If we attached the `click` listener to the marker element, the popup - // would close once the event propogated to `map` due to the - // `Popup#_onClickClose` listener. - map.on('click', this._onMapClick); - - return this; - } - - /** + addTo(map: Map): this { + if (map === this._map) { + return this; + } + this.remove(); + this._map = map; + map.getCanvasContainer().appendChild(this._element); + map.on('move', this._updateMoving); + map.on('moveend', this._update); + map.on('remove', this._clearFadeTimer); + map._addMarker(this); + this.setDraggable(this._draggable); + this._update(); + + // If we attached the `click` listener to the marker element, the popup + // would close once the event propogated to `map` due to the + // `Popup#_onClickClose` listener. + map.on('click', this._onMapClick); + + return this; + } + + /** * Removes the marker from a map. * * @example @@ -229,29 +221,29 @@ export default class Marker * marker.remove(); * @returns {Marker} Returns itself to allow for method chaining. */ - remove(): this { - const map = this._map; - if (map) { - map.off('click', this._onMapClick); - map.off('move', this._updateMoving); - map.off('moveend', this._update); - map.off('mousedown', this._addDragHandler); - map.off('touchstart', this._addDragHandler); - map.off('mouseup', this._onUp); - map.off('touchend', this._onUp); - map.off('mousemove', this._onMove); - map.off('touchmove', this._onMove); - map.off('remove', this._clearFadeTimer); - map._removeMarker(this); - this._map = undefined; - } - this._clearFadeTimer(); - this._element.remove(); - if (this._popup) this._popup.remove(); - return this; - } - - /** + remove(): this { + const map = this._map; + if (map) { + map.off('click', this._onMapClick); + map.off('move', this._updateMoving); + map.off('moveend', this._update); + map.off('mousedown', this._addDragHandler); + map.off('touchstart', this._addDragHandler); + map.off('mouseup', this._onUp); + map.off('touchend', this._onUp); + map.off('mousemove', this._onMove); + map.off('touchmove', this._onMove); + map.off('remove', this._clearFadeTimer); + map._removeMarker(this); + this._map = undefined; + } + this._clearFadeTimer(); + this._element.remove(); + if (this._popup) this._popup.remove(); + return this; + } + + /** * Get the marker's geographical location. * * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously @@ -266,11 +258,11 @@ export default class Marker * console.log(`Longitude: ${lngLat.lng}, Latitude: ${lngLat.lat}`); * @see [Example: Create a draggable Marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/) */ - getLngLat(): LngLat { - return this._lngLat; - } + getLngLat(): LngLat { + return this._lngLat; + } - /** + /** * Set the marker's geographical position and move it. * * @param {LngLat} lnglat A {@link LngLat} describing where the marker should be located. @@ -284,26 +276,26 @@ export default class Marker * @see [Example: Create a draggable Marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/) * @see [Example: Add a marker using a place name](https://docs.mapbox.com/mapbox-gl-js/example/marker-from-geocode/) */ - setLngLat(lnglat: LngLatLike): this { - this._lngLat = LngLat.convert(lnglat); - this._pos = null; - if (this._popup) this._popup.setLngLat(this._lngLat); - this._update(true); - return this; - } - - /** + setLngLat(lnglat: LngLatLike): this { + this._lngLat = LngLat.convert(lnglat); + this._pos = null; + if (this._popup) this._popup.setLngLat(this._lngLat); + this._update(true); + return this; + } + + /** * Returns the `Marker`'s HTML element. * * @returns {HTMLElement} Returns the marker element. * @example * const element = marker.getElement(); */ - getElement(): HTMLElement { - return this._element; - } + getElement(): HTMLElement { + return this._element; + } - /** + /** * Binds a {@link Popup} to the {@link Marker}. * * @param {Popup | null} popup An instance of the {@link Popup} class. If undefined or null, any popup @@ -316,84 +308,72 @@ export default class Marker * .addTo(map); * @see [Example: Attach a popup to a marker instance](https://docs.mapbox.com/mapbox-gl-js/example/set-popup/) */ - setPopup(popup: ?Popup): this { - if (this._popup) { - this._popup.remove(); - this._popup = null; - this._element.removeAttribute('role'); - this._element.removeEventListener('keypress', this._onKeyPress); - - if (!this._originalTabIndex) { - this._element.removeAttribute('tabindex'); - } - } - - if (popup) { - if (!('offset' in popup.options)) { - const markerHeight = 41 - 5.8 / 2; - const markerRadius = 13.5; - const linearOffset = Math.sqrt(Math.pow(markerRadius, 2) / 2); - popup.options.offset = this._defaultMarker ? - { - 'top': [0, 0], - 'top-left': [0, 0], - 'top-right': [0, 0], - 'bottom': [0, -markerHeight], - 'bottom-left': [ - linearOffset, - (markerHeight - markerRadius + linearOffset) * -1, - ], - 'bottom-right': [ - -linearOffset, - (markerHeight - markerRadius + linearOffset) * -1, - ], - 'left': [markerRadius, (markerHeight - markerRadius) * -1], - 'right': [-markerRadius, (markerHeight - markerRadius) * -1], - } : - this._offset; - } - this._popup = popup; - popup._marker = this; - if (this._lngLat) this._popup.setLngLat(this._lngLat); - - this._element.setAttribute('role', 'button'); - this._originalTabIndex = this._element.getAttribute('tabindex'); - if (!this._originalTabIndex) { - this._element.setAttribute('tabindex', '0'); - } - this._element.addEventListener('keypress', this._onKeyPress); - this._element.setAttribute('aria-expanded', 'false'); - } - - return this; - } - - _onKeyPress: ((e: KeyboardEvent) => void) = (e: KeyboardEvent) => { - const code = e.code; - const legacyCode = e.charCode || e.keyCode; - - if ( - code === 'Space' || code === 'Enter' || legacyCode === 32 || - legacyCode === 13 // space or enter - - ) { - this.togglePopup(); - } - }; - - _onMapClick: ((e: MapMouseEvent) => void) = (e: MapMouseEvent) => { - const targetElement = e.originalEvent.target; - const element = this._element; - - if ( - this._popup && - (targetElement === element || element.contains((targetElement: any))) - ) { - this.togglePopup(); - } - }; - - /** + setPopup(popup: ?Popup): this { + if (this._popup) { + this._popup.remove(); + this._popup = null; + this._element.removeAttribute('role'); + this._element.removeEventListener('keypress', this._onKeyPress); + + if (!this._originalTabIndex) { + this._element.removeAttribute('tabindex'); + } + } + + if (popup) { + if (!('offset' in popup.options)) { + const markerHeight = 41 - (5.8 / 2); + const markerRadius = 13.5; + const linearOffset = Math.sqrt(Math.pow(markerRadius, 2) / 2); + popup.options.offset = this._defaultMarker ? { + 'top': [0, 0], + 'top-left': [0, 0], + 'top-right': [0, 0], + 'bottom': [0, -markerHeight], + 'bottom-left': [linearOffset, (markerHeight - markerRadius + linearOffset) * -1], + 'bottom-right': [-linearOffset, (markerHeight - markerRadius + linearOffset) * -1], + 'left': [markerRadius, (markerHeight - markerRadius) * -1], + 'right': [-markerRadius, (markerHeight - markerRadius) * -1] + } : this._offset; + } + this._popup = popup; + popup._marker = this; + if (this._lngLat) this._popup.setLngLat(this._lngLat); + + this._element.setAttribute('role', 'button'); + this._originalTabIndex = this._element.getAttribute('tabindex'); + if (!this._originalTabIndex) { + this._element.setAttribute('tabindex', '0'); + } + this._element.addEventListener('keypress', this._onKeyPress); + this._element.setAttribute('aria-expanded', 'false'); + } + + return this; + } + + _onKeyPress: ((e: KeyboardEvent) => void) = (e: KeyboardEvent) => { + const code = e.code; + const legacyCode = e.charCode || e.keyCode; + + if ( + (code === 'Space') || (code === 'Enter') || + (legacyCode === 32) || (legacyCode === 13) // space or enter + ) { + this.togglePopup(); + } + } + + _onMapClick: ((e: MapMouseEvent) => void) = (e: MapMouseEvent) => { + const targetElement = e.originalEvent.target; + const element = this._element; + + if (this._popup && (targetElement === element || element.contains((targetElement: any)))) { + this.togglePopup(); + } + } + + /** * Returns the {@link Popup} instance that is bound to the {@link Marker}. * * @returns {Popup} Returns the popup. @@ -405,11 +385,11 @@ export default class Marker * * console.log(marker.getPopup()); // return the popup instance */ - getPopup(): ?Popup { - return this._popup; - } + getPopup(): ?Popup { + return this._popup; + } - /** + /** * Opens or closes the {@link Popup} instance that is bound to the {@link Marker}, depending on the current state of the {@link Popup}. * * @returns {Marker} Returns itself to allow for method chaining. @@ -421,229 +401,200 @@ export default class Marker * * marker.togglePopup(); // toggle popup open or closed */ - togglePopup(): this { - const popup = this._popup; - if (!popup) { - return this; - } else if (popup.isOpen()) { - popup.remove(); - this._element.setAttribute('aria-expanded', 'false'); - } else if (this._map) { - popup.addTo(this._map); - this._element.setAttribute('aria-expanded', 'true'); - } - return this; - } - - _behindTerrain(): boolean { - const map = this._map; - const pos = this._pos; - if (!map || !pos) return false; - const unprojected = map.unproject(pos); - const camera = map.getFreeCameraOptions(); - if (!camera.position) return false; - const cameraLngLat = camera.position.toLngLat(); - const toClosestSurface = cameraLngLat.distanceTo(unprojected); - const toMarker = cameraLngLat.distanceTo(this._lngLat); - return toClosestSurface < toMarker * 0.9; - } - - _evaluateOpacity: (() => void) = () => { - const map = this._map; - if (!map) return; - - const pos = this._pos; - - if ( - !pos || pos.x < 0 || pos.x > map.transform.width || pos.y < 0 || - pos.y > map.transform.height - ) { - this._clearFadeTimer(); - return; - } - const mapLocation = map.unproject(pos); - let opacity; - if (map._showingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) { - opacity = 0; - } else { - opacity = 1 - map._queryFogOpacity(mapLocation); - if ( - map.transform._terrainEnabled() && map.getTerrain() && - this._behindTerrain() - ) { - opacity *= this._occludedOpacity; - } - } - - this._element.style.opacity = `${opacity}`; - this._element.style.pointerEvents = opacity > 0 ? 'auto' : 'none'; - if (this._popup) { - this._popup._setOpacity(opacity); - } - - this._fadeTimer = null; - }; - - _clearFadeTimer: (() => void) = () => { - if (this._fadeTimer) { - clearTimeout(this._fadeTimer); - this._fadeTimer = null; - } - }; - - _updateDOM() { - const pos = this._pos; - const map = this._map; - if (!pos || !map) { - return; - } - - const offset = this._offset.mult(this._scale); - - this._element.style.transform = ` + togglePopup(): this { + const popup = this._popup; + if (!popup) { + return this; + } else if (popup.isOpen()) { + popup.remove(); + this._element.setAttribute('aria-expanded', 'false'); + } else if (this._map) { + popup.addTo(this._map); + this._element.setAttribute('aria-expanded', 'true'); + } + return this; + } + + _behindTerrain(): boolean { + const map = this._map; + const pos = this._pos; + if (!map || !pos) return false; + const unprojected = map.unproject(pos); + const camera = map.getFreeCameraOptions(); + if (!camera.position) return false; + const cameraLngLat = camera.position.toLngLat(); + const toClosestSurface = cameraLngLat.distanceTo(unprojected); + const toMarker = cameraLngLat.distanceTo(this._lngLat); + return toClosestSurface < toMarker * 0.9; + + } + + _evaluateOpacity: (() => void) = () => { + const map = this._map; + if (!map) return; + + const pos = this._pos; + + if (!pos || pos.x < 0 || pos.x > map.transform.width || pos.y < 0 || pos.y > map.transform.height) { + this._clearFadeTimer(); + return; + } + const mapLocation = map.unproject(pos); + let opacity; + if (map._showingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) { + opacity = 0; + } else { + opacity = 1 - map._queryFogOpacity(mapLocation); + if (map.transform._terrainEnabled() && map.getTerrain() && this._behindTerrain()) { + opacity *= this._occludedOpacity; + } + } + + this._element.style.opacity = `${opacity}`; + this._element.style.pointerEvents = opacity > 0 ? 'auto' : 'none'; + if (this._popup) { + this._popup._setOpacity(opacity); + } + + this._fadeTimer = null; + } + + _clearFadeTimer: (() => void) = () => { + if (this._fadeTimer) { + clearTimeout(this._fadeTimer); + this._fadeTimer = null; + } + } + + _updateDOM() { + const pos = this._pos; + const map = this._map; + if (!pos || !map) { return; } + + const offset = this._offset.mult(this._scale); + + this._element.style.transform = ` translate(${pos.x}px,${pos.y}px) ${anchorTranslate[this._anchor]} ${this._calculateXYTransform()} ${this._calculateZTransform()} translate(${offset.x}px,${offset.y}px) `; - } - - _calculateXYTransform(): string { - const pos = this._pos; - const map = this._map; - const alignment = this.getPitchAlignment(); - - // `viewport', 'auto' and invalid arugments do no pitch transformation. - if (!map || !pos || alignment !== 'map') { - return ``; - } - // 'map' alignment on a flat map - if (!map._showingGlobe()) { - const pitch = map.getPitch(); - return pitch ? `rotateX(${pitch}deg)` : ''; - } - // 'map' alignment on globe - const tilt = radToDeg(globeTiltAtLngLat(map.transform, this._lngLat)); - const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform)); - const manhattanDistance = Math.abs(posFromCenter.x) + Math.abs( - posFromCenter.y, - ); - if (manhattanDistance === 0) { - return ''; - } - - const tiltOverDist = tilt / manhattanDistance; - const yTilt = posFromCenter.x * tiltOverDist; - const xTilt = -posFromCenter.y * tiltOverDist; - return `rotateX(${xTilt}deg) rotateY(${yTilt}deg)`; - } - - _calculateZTransform(): string { - const pos = this._pos; - const map = this._map; - if (!map || !pos) { - return ''; - } - - let rotation = 0; - const alignment = this.getRotationAlignment(); - if (alignment === 'map') { - if (map._showingGlobe()) { - const north = map.project( - new LngLat(this._lngLat.lng, this._lngLat.lat + .001), - ); - const south = map.project( - new LngLat(this._lngLat.lng, this._lngLat.lat - .001), - ); - const diff = south.sub(north); - rotation = radToDeg(Math.atan2(diff.y, diff.x)) - 90; - } else { - rotation = -map.getBearing(); - } - } else if (alignment === 'horizon') { - const ALIGN_TO_HORIZON_BELOW_ZOOM = 4; - const ALIGN_TO_SCREEN_ABOVE_ZOOM = 6; - assert( - ALIGN_TO_SCREEN_ABOVE_ZOOM <= GLOBE_ZOOM_THRESHOLD_MAX, - 'Horizon-oriented marker transition should be complete when globe switches to Mercator', - ); - assert(ALIGN_TO_HORIZON_BELOW_ZOOM <= ALIGN_TO_SCREEN_ABOVE_ZOOM); - - const smooth = smoothstep( - ALIGN_TO_HORIZON_BELOW_ZOOM, - ALIGN_TO_SCREEN_ABOVE_ZOOM, - map.getZoom(), - ); - - const centerPoint = globeCenterToScreenPoint(map.transform); - centerPoint.y += smooth * map.transform.height; - const rel = pos.sub(centerPoint); - const angle = radToDeg(Math.atan2(rel.y, rel.x)); - const up = angle > 90 ? angle - 270 : angle + 90; - rotation = up * (1 - smooth); - } - - rotation += this._rotation; - return rotation ? `rotateZ(${rotation}deg)` : ''; - } - - _update: ((delaySnap?: boolean) => void) = (delaySnap?: boolean) => { - window.cancelAnimationFrame(this._updateFrameId); - const map = this._map; - if (!map) return; - - if (map.transform.renderWorldCopies) { - this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); - } - - this._pos = map.project(this._lngLat); - - // because rounding the coordinates at every `move` event causes stuttered zooming - // we only round them when _update is called with `moveend` or when its called with - // no arguments (when the Marker is initialized or Marker#setLngLat is invoked). - if (delaySnap === true) { - this._updateFrameId = window.requestAnimationFrame( - () => { + } + + _calculateXYTransform(): string { + const pos = this._pos; + const map = this._map; + const alignment = this.getPitchAlignment(); + + // `viewport', 'auto' and invalid arugments do no pitch transformation. + if (!map || !pos || alignment !== 'map') { + return ``; + } + // 'map' alignment on a flat map + if (!map._showingGlobe()) { + const pitch = map.getPitch(); + return pitch ? `rotateX(${pitch}deg)` : ''; + } + // 'map' alignment on globe + const tilt = radToDeg(globeTiltAtLngLat(map.transform, this._lngLat)); + const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform)); + const manhattanDistance = (Math.abs(posFromCenter.x) + Math.abs(posFromCenter.y)); + if (manhattanDistance === 0) { return ''; } + + const tiltOverDist = tilt / manhattanDistance; + const yTilt = posFromCenter.x * tiltOverDist; + const xTilt = -posFromCenter.y * tiltOverDist; + return `rotateX(${xTilt}deg) rotateY(${yTilt}deg)`; + + } + + _calculateZTransform(): string { + + const pos = this._pos; + const map = this._map; + if (!map || !pos) { return ''; } + + let rotation = 0; + const alignment = this.getRotationAlignment(); + if (alignment === 'map') { + if (map._showingGlobe()) { + const north = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat + .001)); + const south = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat - .001)); + const diff = south.sub(north); + rotation = radToDeg(Math.atan2(diff.y, diff.x)) - 90; + } else { + rotation = -map.getBearing(); + } + } else if (alignment === 'horizon') { + const ALIGN_TO_HORIZON_BELOW_ZOOM = 4; + const ALIGN_TO_SCREEN_ABOVE_ZOOM = 6; + assert(ALIGN_TO_SCREEN_ABOVE_ZOOM <= GLOBE_ZOOM_THRESHOLD_MAX, 'Horizon-oriented marker transition should be complete when globe switches to Mercator'); + assert(ALIGN_TO_HORIZON_BELOW_ZOOM <= ALIGN_TO_SCREEN_ABOVE_ZOOM); + + const smooth = smoothstep(ALIGN_TO_HORIZON_BELOW_ZOOM, ALIGN_TO_SCREEN_ABOVE_ZOOM, map.getZoom()); + + const centerPoint = globeCenterToScreenPoint(map.transform); + centerPoint.y += smooth * map.transform.height; + const rel = pos.sub(centerPoint); + const angle = radToDeg(Math.atan2(rel.y, rel.x)); + const up = angle > 90 ? angle - 270 : angle + 90; + rotation = up * (1 - smooth); + } + + rotation += this._rotation; + return rotation ? `rotateZ(${rotation}deg)` : ''; + } + + _update: ((delaySnap?: boolean) => void) = (delaySnap?: boolean) => { + window.cancelAnimationFrame(this._updateFrameId); + const map = this._map; + if (!map) return; + + if (map.transform.renderWorldCopies) { + this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); + } + + this._pos = map.project(this._lngLat); + + // because rounding the coordinates at every `move` event causes stuttered zooming + // we only round them when _update is called with `moveend` or when its called with + // no arguments (when the Marker is initialized or Marker#setLngLat is invoked). + if (delaySnap === true) { + this._updateFrameId = window.requestAnimationFrame(() => { + if (this._element && this._pos && this._anchor) { + this._pos = this._pos.round(); + this._updateDOM(); + } + }); + } else { + this._pos = this._pos.round(); + } + + map._requestDomTask(() => { + if (!this._map) return; + if (this._element && this._pos && this._anchor) { - this._pos = this._pos.round(); this._updateDOM(); } - }, - ); - } else { - this._pos = this._pos.round(); - } - - map._requestDomTask( - () => { - if (!this._map) return; - - if (this._element && this._pos && this._anchor) { - this._updateDOM(); - } - - if ( - (map._showingGlobe() || map.getTerrain() || map.getFog()) && - !this._fadeTimer - ) { - this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60); - } - }, - ); - }; - - /** + + if ((map._showingGlobe() || map.getTerrain() || map.getFog()) && !this._fadeTimer) { + this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60); + } + }); + } + + /** * Get the marker's offset. * * @returns {Point} The marker's screen coordinates in pixels. * @example * const offset = marker.getOffset(); */ - getOffset(): Point { - return this._offset; - } + getOffset(): Point { + return this._offset; + } - /** + /** * Sets the offset of the marker. * * @param {PointLike} offset The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up. @@ -651,39 +602,39 @@ export default class Marker * @example * marker.setOffset([0, 1]); */ - setOffset(offset: PointLike): this { - this._offset = Point.convert(offset); - this._update(); - return this; - } - - _onMove: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { - const map = this._map; - if (!map) return; - - const startPos = this._pointerdownPos; - const posDelta = this._positionDelta; - if (!startPos || !posDelta) return; - - if (!this._isDragging) { - const clickTolerance = this._clickTolerance || map._clickTolerance; - if (e.point.dist(startPos) < clickTolerance) return; - this._isDragging = true; - } - - this._pos = e.point.sub(posDelta); - this._lngLat = map.unproject(this._pos); - this.setLngLat(this._lngLat); - // suppress click event so that popups don't toggle on drag - this._element.style.pointerEvents = 'none'; - - // make sure dragstart only fires on the first move event after mousedown. - // this can't be on mousedown because that event doesn't necessarily - // imply that a drag is about to happen. - if (this._state === 'pending') { - this._state = 'active'; - - /** + setOffset(offset: PointLike): this { + this._offset = Point.convert(offset); + this._update(); + return this; + } + + _onMove: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { + const map = this._map; + if (!map) return; + + const startPos = this._pointerdownPos; + const posDelta = this._positionDelta; + if (!startPos || !posDelta) return; + + if (!this._isDragging) { + const clickTolerance = this._clickTolerance || map._clickTolerance; + if (e.point.dist(startPos) < clickTolerance) return; + this._isDragging = true; + } + + this._pos = e.point.sub(posDelta); + this._lngLat = map.unproject(this._pos); + this.setLngLat(this._lngLat); + // suppress click event so that popups don't toggle on drag + this._element.style.pointerEvents = 'none'; + + // make sure dragstart only fires on the first move event after mousedown. + // this can't be on mousedown because that event doesn't necessarily + // imply that a drag is about to happen. + if (this._state === 'pending') { + this._state = 'active'; + + /** * Fired when dragging starts. * * @event dragstart @@ -692,10 +643,10 @@ export default class Marker * @type {Object} * @property {Marker} marker The object that is being dragged. */ - this.fire(new Event('dragstart')); - } + this.fire(new Event('dragstart')); + } - /** + /** * Fired while dragging. * * @event drag @@ -704,25 +655,25 @@ export default class Marker * @type {Object} * @property {Marker} marker The object that is being dragged. */ - this.fire(new Event('drag')); - }; - - _onUp: (() => void) = () => { - // revert to normal pointer event handling - this._element.style.pointerEvents = 'auto'; - this._positionDelta = null; - this._pointerdownPos = null; - this._isDragging = false; - - const map = this._map; - if (map) { - map.off('mousemove', this._onMove); - map.off('touchmove', this._onMove); - } - - // only fire dragend if it was preceded by at least one drag event - if (this._state === 'active') { - /** + this.fire(new Event('drag')); + } + + _onUp: (() => void) = () => { + // revert to normal pointer event handling + this._element.style.pointerEvents = 'auto'; + this._positionDelta = null; + this._pointerdownPos = null; + this._isDragging = false; + + const map = this._map; + if (map) { + map.off('mousemove', this._onMove); + map.off('touchmove', this._onMove); + } + + // only fire dragend if it was preceded by at least one drag event + if (this._state === 'active') { + /** * Fired when the marker is finished being dragged. * * @event dragend @@ -731,38 +682,38 @@ export default class Marker * @type {Object} * @property {Marker} marker The object that was dragged. */ - this.fire(new Event('dragend')); - } - - this._state = 'inactive'; - }; - - _addDragHandler: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { - const map = this._map; - const pos = this._pos; - if (!map || !pos) return; - - if (this._element.contains((e.originalEvent.target: any))) { - e.preventDefault(); - - // We need to calculate the pixel distance between the click point - // and the marker position, with the offset accounted for. Then we - // can subtract this distance from the mousemove event's position - // to calculate the new marker position. - // If we don't do this, the marker 'jumps' to the click position - // creating a jarring UX effect. - this._positionDelta = e.point.sub(pos); - this._pointerdownPos = e.point; - - this._state = 'pending'; - map.on('mousemove', this._onMove); - map.on('touchmove', this._onMove); - map.once('mouseup', this._onUp); - map.once('touchend', this._onUp); - } - }; - - /** + this.fire(new Event('dragend')); + } + + this._state = 'inactive'; + } + + _addDragHandler: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { + const map = this._map; + const pos = this._pos; + if (!map || !pos) return; + + if (this._element.contains((e.originalEvent.target: any))) { + e.preventDefault(); + + // We need to calculate the pixel distance between the click point + // and the marker position, with the offset accounted for. Then we + // can subtract this distance from the mousemove event's position + // to calculate the new marker position. + // If we don't do this, the marker 'jumps' to the click position + // creating a jarring UX effect. + this._positionDelta = e.point.sub(pos); + this._pointerdownPos = e.point; + + this._state = 'pending'; + map.on('mousemove', this._onMove); + map.on('touchmove', this._onMove); + map.once('mouseup', this._onUp); + map.once('touchend', this._onUp); + } + } + + /** * Sets the `draggable` property and functionality of the marker. * * @param {boolean} [shouldBeDraggable=false] Turns drag functionality on/off. @@ -770,37 +721,37 @@ export default class Marker * @example * marker.setDraggable(true); */ - setDraggable(shouldBeDraggable: boolean): this { - this._draggable = !!shouldBeDraggable; // convert possible undefined value to false - - // handle case where map may not exist yet - // for example, when setDraggable is called before addTo - const map = this._map; - if (map) { - if (shouldBeDraggable) { - map.on('mousedown', this._addDragHandler); - map.on('touchstart', this._addDragHandler); - } else { - map.off('mousedown', this._addDragHandler); - map.off('touchstart', this._addDragHandler); - } - } - - return this; - } - - /** + setDraggable(shouldBeDraggable: boolean): this { + this._draggable = !!shouldBeDraggable; // convert possible undefined value to false + + // handle case where map may not exist yet + // for example, when setDraggable is called before addTo + const map = this._map; + if (map) { + if (shouldBeDraggable) { + map.on('mousedown', this._addDragHandler); + map.on('touchstart', this._addDragHandler); + } else { + map.off('mousedown', this._addDragHandler); + map.off('touchstart', this._addDragHandler); + } + } + + return this; + } + + /** * Returns true if the marker can be dragged. * * @returns {boolean} True if the marker is draggable. * @example * const isMarkerDraggable = marker.isDraggable(); */ - isDraggable(): boolean { - return this._draggable; - } + isDraggable(): boolean { + return this._draggable; + } - /** + /** * Sets the `rotation` property of the marker. * * @param {number} [rotation=0] The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker#setRotationAlignment} setting. @@ -808,24 +759,24 @@ export default class Marker * @example * marker.setRotation(45); */ - setRotation(rotation: number): this { - this._rotation = rotation || 0; - this._update(); - return this; - } + setRotation(rotation: number): this { + this._rotation = rotation || 0; + this._update(); + return this; + } - /** + /** * Returns the current rotation angle of the marker (in degrees). * * @returns {number} The current rotation angle of the marker. * @example * const rotation = marker.getRotation(); */ - getRotation(): number { - return this._rotation; - } + getRotation(): number { + return this._rotation; + } - /** + /** * Sets the `rotationAlignment` property of the marker. * * @param {string} [alignment='auto'] Sets the `rotationAlignment` property of the marker. @@ -833,30 +784,28 @@ export default class Marker * @example * marker.setRotationAlignment('viewport'); */ - setRotationAlignment(alignment: string): this { - this._rotationAlignment = alignment || 'auto'; - this._update(); - return this; - } + setRotationAlignment(alignment: string): this { + this._rotationAlignment = alignment || 'auto'; + this._update(); + return this; + } - /** + /** * Returns the current `rotationAlignment` property of the marker. * * @returns {string} The current rotational alignment of the marker. * @example * const alignment = marker.getRotationAlignment(); */ - getRotationAlignment(): string { - if (this._rotationAlignment === 'auto') return 'viewport'; - if ( - this._rotationAlignment === 'horizon' && this._map && - !this._map._showingGlobe() - ) - return 'viewport'; - return this._rotationAlignment; - } - - /** + getRotationAlignment(): string { + if (this._rotationAlignment === 'auto') + return 'viewport'; + if (this._rotationAlignment === 'horizon' && this._map && !this._map._showingGlobe()) + return 'viewport'; + return this._rotationAlignment; + } + + /** * Sets the `pitchAlignment` property of the marker. * * @param {string} [alignment] Sets the `pitchAlignment` property of the marker. If alignment is 'auto', it will automatically match `rotationAlignment`. @@ -864,27 +813,27 @@ export default class Marker * @example * marker.setPitchAlignment('map'); */ - setPitchAlignment(alignment: string): this { - this._pitchAlignment = alignment || 'auto'; - this._update(); - return this; - } + setPitchAlignment(alignment: string): this { + this._pitchAlignment = alignment || 'auto'; + this._update(); + return this; + } - /** + /** * Returns the current `pitchAlignment` behavior of the marker. * * @returns {string} The current pitch alignment of the marker. * @example * const alignment = marker.getPitchAlignment(); */ - getPitchAlignment(): string { - if (this._pitchAlignment === 'auto') { - return this.getRotationAlignment(); - } - return this._pitchAlignment; - } - - /** + getPitchAlignment(): string { + if (this._pitchAlignment === 'auto') { + return this.getRotationAlignment(); + } + return this._pitchAlignment; + } + + /** * Sets the `occludedOpacity` property of the marker. * This opacity is used on the marker when the marker is occluded by terrain. * @@ -893,20 +842,20 @@ export default class Marker * @example * marker.setOccludedOpacity(0.3); */ - setOccludedOpacity(opacity: number): this { - this._occludedOpacity = opacity || 0.2; - this._update(); - return this; - } + setOccludedOpacity(opacity: number): this { + this._occludedOpacity = opacity || 0.2; + this._update(); + return this; + } - /** + /** * Returns the current `occludedOpacity` of the marker. * * @returns {number} The opacity of a terrain occluded marker. * @example * const opacity = marker.getOccludedOpacity(); */ - getOccludedOpacity(): number { - return this._occludedOpacity; - } + getOccludedOpacity(): number { + return this._occludedOpacity; + } } diff --git a/src/ui/popup.js b/src/ui/popup.js index acc505b22ea..16c5986c885 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -102,29 +102,28 @@ const focusQuerySelector = [ * @see [Example: Attach a popup to a marker instance](https://www.mapbox.com/mapbox-gl-js/example/set-popup/) */ export default class Popup extends Evented { - _map: ?Map; - options: PopupOptions; - _content: ?HTMLElement; - _container: ?HTMLElement; - _closeButton: ?HTMLElement; - _tip: ?HTMLElement; - _lngLat: LngLat; - _trackPointer: boolean; - _pos: ?Point; - _anchor: Anchor; - _classList: Set; - _marker: ?Marker; - - constructor(options: PopupOptions) { - super(); - this.options = extend(Object.create(defaultOptions), options); - bindAll(['_update', '_onClose', 'remove', '_onMouseEvent'], this); - this._classList = new Set( - options && options.className ? options.className.trim().split(/\s+/) : [], - ); - } - - /** + _map: ?Map; + options: PopupOptions; + _content: ?HTMLElement; + _container: ?HTMLElement; + _closeButton: ?HTMLElement; + _tip: ?HTMLElement; + _lngLat: LngLat; + _trackPointer: boolean; + _pos: ?Point; + _anchor: Anchor; + _classList: Set; + _marker: ?Marker; + + constructor(options: PopupOptions) { + super(); + this.options = extend(Object.create(defaultOptions), options); + bindAll(['_update', '_onClose', 'remove', '_onMouseEvent'], this); + this._classList = new Set(options && options.className ? + options.className.trim().split(/\s+/) : []); + } + + /** * Adds the popup to a map. * * @param {Map} map The Mapbox GL JS map to add the popup to. @@ -139,32 +138,32 @@ export default class Popup extends Evented { * @see [Example: Display a popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) * @see [Example: Show polygon information on click](https://docs.mapbox.com/mapbox-gl-js/example/polygon-popup-on-click/) */ - addTo(map: Map): this { - if (this._map) this.remove(); - - this._map = map; - if (this.options.closeOnClick) { - map.on('preclick', this._onClose); - } - - if (this.options.closeOnMove) { - map.on('move', this._onClose); - } - - map.on('remove', this.remove); - this._update(); - map._addPopup(this); - this._focusFirstElement(); - - if (this._trackPointer) { - map.on('mousemove', this._onMouseEvent); - map.on('mouseup', this._onMouseEvent); - map._canvasContainer.classList.add('mapboxgl-track-pointer'); - } else { - map.on('move', this._update); - } - - /** + addTo(map: Map): this { + if (this._map) this.remove(); + + this._map = map; + if (this.options.closeOnClick) { + map.on('preclick', this._onClose); + } + + if (this.options.closeOnMove) { + map.on('move', this._onClose); + } + + map.on('remove', this.remove); + this._update(); + map._addPopup(this); + this._focusFirstElement(); + + if (this._trackPointer) { + map.on('mousemove', this._onMouseEvent); + map.on('mouseup', this._onMouseEvent); + map._canvasContainer.classList.add('mapboxgl-track-pointer'); + } else { + map.on('move', this._update); + } + + /** * Fired when the popup is opened manually or programatically. * * @event open @@ -183,23 +182,23 @@ export default class Popup extends Evented { * }); * */ - this.fire(new Event('open')); + this.fire(new Event('open')); - return this; - } + return this; + } - /** + /** * Checks if a popup is open. * * @returns {boolean} `true` if the popup is open, `false` if it is closed. * @example * const isPopupOpen = popup.isOpen(); */ - isOpen(): boolean { - return !!this._map; - } + isOpen(): boolean { + return !!this._map; + } - /** + /** * Removes the popup from the map it has been added to. * * @example @@ -207,34 +206,34 @@ export default class Popup extends Evented { * popup.remove(); * @returns {Popup} Returns itself to allow for method chaining. */ - remove: () => Popup = () => { - if (this._content) { - this._content.remove(); - } - - if (this._container) { - this._container.remove(); - this._container = undefined; - } - - const map = this._map; - if (map) { - map.off('move', this._update); - map.off('move', this._onClose); - map.off('preclick', this._onClose); - map.off('click', this._onClose); - map.off('remove', this.remove); - map.off('mousemove', this._onMouseEvent); - map.off('mouseup', this._onMouseEvent); - map.off('drag', this._onMouseEvent); - if (map._canvasContainer) { - map._canvasContainer.classList.remove('mapboxgl-track-pointer'); - } - map._removePopup(this); - this._map = undefined; - } - - /** + remove: () => Popup = () => { + if (this._content) { + this._content.remove(); + } + + if (this._container) { + this._container.remove(); + this._container = undefined; + } + + const map = this._map; + if (map) { + map.off('move', this._update); + map.off('move', this._onClose); + map.off('preclick', this._onClose); + map.off('click', this._onClose); + map.off('remove', this.remove); + map.off('mousemove', this._onMouseEvent); + map.off('mouseup', this._onMouseEvent); + map.off('drag', this._onMouseEvent); + if (map._canvasContainer) { + map._canvasContainer.classList.remove('mapboxgl-track-pointer'); + } + map._removePopup(this); + this._map = undefined; + } + + /** * Fired when the popup is closed manually or programatically. * * @event close @@ -253,12 +252,12 @@ export default class Popup extends Evented { * }); * */ - this.fire(new Event('close')); + this.fire(new Event('close')); - return this; - }; + return this; + } - /** + /** * Returns the geographical location of the popup's anchor. * * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously @@ -269,11 +268,11 @@ export default class Popup extends Evented { * @example * const lngLat = popup.getLngLat(); */ - getLngLat(): LngLat { - return this._lngLat; - } + getLngLat(): LngLat { + return this._lngLat; + } - /** + /** * Sets the geographical location of the popup's anchor, and moves the popup to it. Replaces trackPointer() behavior. * * @param {LngLatLike} lnglat The geographical location to set as the popup's anchor. @@ -281,25 +280,25 @@ export default class Popup extends Evented { * @example * popup.setLngLat([-122.4194, 37.7749]); */ - setLngLat(lnglat: LngLatLike): this { - this._lngLat = LngLat.convert(lnglat); - this._pos = null; + setLngLat(lnglat: LngLatLike): this { + this._lngLat = LngLat.convert(lnglat); + this._pos = null; - this._trackPointer = false; + this._trackPointer = false; - this._update(); + this._update(); - const map = this._map; - if (map) { - map.on('move', this._update); - map.off('mousemove', this._onMouseEvent); - map._canvasContainer.classList.remove('mapboxgl-track-pointer'); - } + const map = this._map; + if (map) { + map.on('move', this._update); + map.off('mousemove', this._onMouseEvent); + map._canvasContainer.classList.remove('mapboxgl-track-pointer'); + } - return this; - } + return this; + } - /** + /** * Tracks the popup anchor to the cursor position on screens with a pointer device (it will be hidden on touchscreens). Replaces the `setLngLat` behavior. * For most use cases, set `closeOnClick` and `closeButton` to `false`. * @@ -310,22 +309,23 @@ export default class Popup extends Evented { * .addTo(map); * @returns {Popup} Returns itself to allow for method chaining. */ - trackPointer(): this { - this._trackPointer = true; - this._pos = null; - this._update(); - const map = this._map; - if (map) { - map.off('move', this._update); - map.on('mousemove', this._onMouseEvent); - map.on('drag', this._onMouseEvent); - map._canvasContainer.classList.add('mapboxgl-track-pointer'); - } - - return this; - } - - /** + trackPointer(): this { + this._trackPointer = true; + this._pos = null; + this._update(); + const map = this._map; + if (map) { + map.off('move', this._update); + map.on('mousemove', this._onMouseEvent); + map.on('drag', this._onMouseEvent); + map._canvasContainer.classList.add('mapboxgl-track-pointer'); + } + + return this; + + } + + /** * Returns the `Popup`'s HTML element. * * @example @@ -338,11 +338,11 @@ export default class Popup extends Evented { * popupElem.style.fontSize = "25px"; * @returns {HTMLElement} Returns container element. */ - getElement(): ?HTMLElement { - return this._container; - } + getElement(): ?HTMLElement { + return this._container; + } - /** + /** * Sets the popup's content to a string of text. * * This function creates a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node in the DOM, @@ -357,11 +357,11 @@ export default class Popup extends Evented { * .setText('Hello, world!') * .addTo(map); */ - setText(text: string): this { - return this.setDOMContent(window.document.createTextNode(text)); - } + setText(text: string): this { + return this.setDOMContent(window.document.createTextNode(text)); + } - /** + /** * Sets the popup's content to the HTML provided as a string. * * This method does not perform HTML filtering or sanitization, and must be @@ -380,32 +380,32 @@ export default class Popup extends Evented { * @see [Example: Display a popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) * @see [Example: Attach a popup to a marker instance](https://docs.mapbox.com/mapbox-gl-js/example/set-popup/) */ - setHTML(html: string): this { - const frag = window.document.createDocumentFragment(); - const temp = window.document.createElement('body'); - let child; - temp.innerHTML = html; - while (true) { - child = temp.firstChild; - if (!child) break; - frag.appendChild(child); - } - - return this.setDOMContent(frag); - } - - /** + setHTML(html: string): this { + const frag = window.document.createDocumentFragment(); + const temp = window.document.createElement('body'); + let child; + temp.innerHTML = html; + while (true) { + child = temp.firstChild; + if (!child) break; + frag.appendChild(child); + } + + return this.setDOMContent(frag); + } + + /** * Returns the popup's maximum width. * * @returns {string} The maximum width of the popup. * @example * const maxWidth = popup.getMaxWidth(); */ - getMaxWidth(): ?string { - return this._container && this._container.style.maxWidth; - } + getMaxWidth(): ?string { + return this._container && this._container.style.maxWidth; + } - /** + /** * Sets the popup's maximum width. This is setting the CSS property `max-width`. * Available values can be found here: https://developer.mozilla.org/en-US/docs/Web/CSS/max-width. * @@ -414,13 +414,13 @@ export default class Popup extends Evented { * @example * popup.setMaxWidth('50'); */ - setMaxWidth(maxWidth: string): this { - this.options.maxWidth = maxWidth; - this._update(); - return this; - } + setMaxWidth(maxWidth: string): this { + this.options.maxWidth = maxWidth; + this._update(); + return this; + } - /** + /** * Sets the popup's content to the element provided as a DOM node. * * @param {Element} htmlNode A DOM node to be used as content for the popup. @@ -434,44 +434,36 @@ export default class Popup extends Evented { * .setDOMContent(div) * .addTo(map); */ - setDOMContent(htmlNode: Node): this { - let content = this._content; - if (content) { - // Clear out children first. - while (content.hasChildNodes()) { - if (content.firstChild) { - content.removeChild(content.firstChild); - } - } - } else { - content = this._content = DOM.create( - 'div', - 'mapboxgl-popup-content', - this._container || undefined, - ); - } - - // The close button should be the last tabbable element inside the popup for a good keyboard UX. - content.appendChild(htmlNode); - - if (this.options.closeButton) { - const button = this._closeButton = DOM.create( - 'button', - 'mapboxgl-popup-close-button', - content, - ); - button.type = 'button'; - button.setAttribute('aria-label', 'Close popup'); - button.setAttribute('aria-hidden', 'true'); - button.innerHTML = '×'; - button.addEventListener('click', this._onClose); - } - this._update(); - this._focusFirstElement(); - return this; - } - - /** + setDOMContent(htmlNode: Node): this { + let content = this._content; + if (content) { + // Clear out children first. + while (content.hasChildNodes()) { + if (content.firstChild) { + content.removeChild(content.firstChild); + } + } + } else { + content = this._content = DOM.create('div', 'mapboxgl-popup-content', this._container || undefined); + } + + // The close button should be the last tabbable element inside the popup for a good keyboard UX. + content.appendChild(htmlNode); + + if (this.options.closeButton) { + const button = this._closeButton = DOM.create('button', 'mapboxgl-popup-close-button', content); + button.type = 'button'; + button.setAttribute('aria-label', 'Close popup'); + button.setAttribute('aria-hidden', 'true'); + button.innerHTML = '×'; + button.addEventListener('click', this._onClose); + } + this._update(); + this._focusFirstElement(); + return this; + } + + /** * Adds a CSS class to the popup container element. * * @param {string} className Non-empty string with CSS class name to add to popup container. @@ -481,13 +473,13 @@ export default class Popup extends Evented { * const popup = new mapboxgl.Popup(); * popup.addClassName('some-class'); */ - addClassName(className: string): this { - this._classList.add(className); - this._updateClassList(); - return this; - } + addClassName(className: string): this { + this._classList.add(className); + this._updateClassList(); + return this; + } - /** + /** * Removes a CSS class from the popup container element. * * @param {string} className Non-empty string with CSS class name to remove from popup container. @@ -497,13 +489,13 @@ export default class Popup extends Evented { * const popup = new mapboxgl.Popup({className: 'some classes'}); * popup.removeClassName('some'); */ - removeClassName(className: string): this { - this._classList.delete(className); - this._updateClassList(); - return this; - } + removeClassName(className: string): this { + this._classList.delete(className); + this._updateClassList(); + return this; + } - /** + /** * Sets the popup's offset. * * @param {number | PointLike | Object} offset Sets the popup's offset. The `Object` is of the following structure @@ -523,13 +515,13 @@ export default class Popup extends Evented { * @example * popup.setOffset(10); */ - setOffset(offset?: Offset): this { - this.options.offset = offset; - this._update(); - return this; - } + setOffset (offset?: Offset): this { + this.options.offset = offset; + this._update(); + return this; + } - /** + /** * Add or remove the given CSS class on the popup container, depending on whether the container currently has that class. * * @param {string} className Non-empty string with CSS class name to add/remove. @@ -540,150 +532,135 @@ export default class Popup extends Evented { * const popup = new mapboxgl.Popup(); * popup.toggleClassName('highlighted'); */ - toggleClassName(className: string): boolean { - let finalState: boolean; - if (this._classList.delete(className)) { - finalState = false; - } else { - this._classList.add(className); - finalState = true; - } - this._updateClassList(); - return finalState; - } - - _onMouseEvent: (event: MapMouseEvent) => void = (event: MapMouseEvent) => { - this._update(event.point); - }; - - _getAnchor(bottomY: number): Anchor { - if (this.options.anchor) { - return this.options.anchor; - } - - const map = this._map; - const container = this._container; - const pos = this._pos; - - if (!map || !container || !pos) return 'bottom'; - - const width = container.offsetWidth; - const height = container.offsetHeight; - - const isTop = pos.y + bottomY < height; - const isBottom = pos.y > map.transform.height - height; - const isLeft = pos.x < width / 2; - const isRight = pos.x > map.transform.width - width / 2; - - if (isTop) { - if (isLeft) return 'top-left'; - if (isRight) return 'top-right'; - return 'top'; - } - if (isBottom) { - if (isLeft) return 'bottom-left'; - if (isRight) return 'bottom-right'; - } - if (isLeft) return 'left'; - if (isRight) return 'right'; - - return 'bottom'; - } - - _updateClassList() { - const container = this._container; - if (!container) return; - - const classes = [...this._classList]; - classes.push('mapboxgl-popup'); - if (this._anchor) { - classes.push(`mapboxgl-popup-anchor-${this._anchor}`); - } - if (this._trackPointer) { - classes.push('mapboxgl-popup-track-pointer'); - } - container.className = classes.join(' '); - } - - _update: (cursor?: Point) => void = (cursor?: Point) => { - const hasPosition = this._lngLat || this._trackPointer; - const map = this._map; - const content = this._content; - - if (!map || !hasPosition || !content) { - return; - } - - let container = this._container; - - if (!container) { - container = this._container = DOM.create( - 'div', - 'mapboxgl-popup', - map.getContainer(), - ); - this._tip = DOM.create('div', 'mapboxgl-popup-tip', container); - container.appendChild(content); - } - - if ( - this.options.maxWidth && - container.style.maxWidth !== this.options.maxWidth - ) { - container.style.maxWidth = this.options.maxWidth; - } - - if (map.transform.renderWorldCopies && !this._trackPointer) { - this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); - } - - if (!this._trackPointer || cursor) { - const pos = this._pos = this._trackPointer && cursor ? - cursor : - map.project(this._lngLat); - - const offsetBottom = normalizeOffset(this.options.offset); - const anchor = this._anchor = this._getAnchor(offsetBottom.y); - const offset = normalizeOffset(this.options.offset, anchor); - - const offsetedPos = pos.add(offset).round(); - map._requestDomTask( - () => { - if (this._container && anchor) { - this._container.style.transform = `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`; - } - }, - ); - } - - if (!this._marker && map._showingGlobe()) { - const opacity = isLngLatBehindGlobe(map.transform, this._lngLat) ? 0 : 1; - this._setOpacity(opacity); - } - - this._updateClassList(); - }; - - _focusFirstElement() { - if (!this.options.focusAfterOpen || !this._container) return; - - const firstFocusable = this._container.querySelector(focusQuerySelector); - - if (firstFocusable) firstFocusable.focus(); - } - - _onClose: () => void = () => { - this.remove(); - }; - - _setOpacity(opacity: number) { - if (this._container) { - this._container.style.opacity = `${opacity}`; - } - if (this._content) { - this._content.style.pointerEvents = opacity ? 'auto' : 'none'; - } - } + toggleClassName(className: string): boolean { + let finalState: boolean; + if (this._classList.delete(className)) { + finalState = false; + } else { + this._classList.add(className); + finalState = true; + } + this._updateClassList(); + return finalState; + } + + _onMouseEvent: (event: MapMouseEvent) => void = (event: MapMouseEvent) => { + this._update(event.point); + } + + _getAnchor(bottomY: number): Anchor { + if (this.options.anchor) { return this.options.anchor; } + + const map = this._map; + const container = this._container; + const pos = this._pos; + + if (!map || !container || !pos) return 'bottom'; + + const width = container.offsetWidth; + const height = container.offsetHeight; + + const isTop = pos.y + bottomY < height; + const isBottom = pos.y > map.transform.height - height; + const isLeft = pos.x < width / 2; + const isRight = pos.x > map.transform.width - width / 2; + + if (isTop) { + if (isLeft) return 'top-left'; + if (isRight) return 'top-right'; + return 'top'; + } + if (isBottom) { + if (isLeft) return 'bottom-left'; + if (isRight) return 'bottom-right'; + } + if (isLeft) return 'left'; + if (isRight) return 'right'; + + return 'bottom'; + } + + _updateClassList() { + const container = this._container; + if (!container) return; + + const classes = [...this._classList]; + classes.push('mapboxgl-popup'); + if (this._anchor) { + classes.push(`mapboxgl-popup-anchor-${this._anchor}`); + } + if (this._trackPointer) { + classes.push('mapboxgl-popup-track-pointer'); + } + container.className = classes.join(' '); + } + + _update: (cursor?: Point) => void = (cursor?: Point) => { + const hasPosition = this._lngLat || this._trackPointer; + const map = this._map; + const content = this._content; + + if (!map || !hasPosition || !content) { return; } + + let container = this._container; + + if (!container) { + container = this._container = DOM.create('div', 'mapboxgl-popup', map.getContainer()); + this._tip = DOM.create('div', 'mapboxgl-popup-tip', container); + container.appendChild(content); + } + + if (this.options.maxWidth && container.style.maxWidth !== this.options.maxWidth) { + container.style.maxWidth = this.options.maxWidth; + } + + if (map.transform.renderWorldCopies && !this._trackPointer) { + this._lngLat = smartWrap(this._lngLat, this._pos, map.transform); + } + + if (!this._trackPointer || cursor) { + const pos = this._pos = this._trackPointer && cursor ? cursor : map.project(this._lngLat); + + const offsetBottom = normalizeOffset(this.options.offset); + const anchor = this._anchor = this._getAnchor(offsetBottom.y); + const offset = normalizeOffset(this.options.offset, anchor); + + const offsetedPos = pos.add(offset).round(); + map._requestDomTask(() => { + if (this._container && anchor) { + this._container.style.transform = `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`; + } + }); + } + + if (!this._marker && map._showingGlobe()) { + const opacity = isLngLatBehindGlobe(map.transform, this._lngLat) ? 0 : 1; + this._setOpacity(opacity); + } + + this._updateClassList(); + } + + _focusFirstElement() { + if (!this.options.focusAfterOpen || !this._container) return; + + const firstFocusable = this._container.querySelector(focusQuerySelector); + + if (firstFocusable) firstFocusable.focus(); + } + + _onClose: () => void = () => { + this.remove(); + } + + _setOpacity(opacity: number) { + if (this._container) { + this._container.style.opacity = `${opacity}`; + } + if (this._content) { + this._content.style.pointerEvents = opacity ? 'auto' : 'none'; + } + } } // returns a normalized offset for a given anchor From 2eec22b7bfa803b0ddbd6f9a6ab70dac14e2656a Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 15:16:05 +0200 Subject: [PATCH 53/72] fix formatting for terrain.js --- src/terrain/terrain.js | 2859 ++++++++++++++++++---------------------- 1 file changed, 1302 insertions(+), 1557 deletions(-) diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index abdf73aff2a..0e06b2a9d4d 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -191,1575 +191,1320 @@ class ProxiedTileID extends OverscaledTileID { type OverlapStencilType = false | 'Clip' | 'Mask'; type FBO = {fb: Framebuffer, tex: Texture, dirty: boolean}; -export class Terrain - extends Elevation { - terrainTileForTile: { [number | string]: Tile }; - prevTerrainTileForTile: { [number | string]: Tile }; - painter: Painter; - sourceCache: SourceCache; - gridBuffer: VertexBuffer; - gridIndexBuffer: IndexBuffer; - gridSegments: SegmentVector; - gridNoSkirtSegments: SegmentVector; - wireframeSegments: SegmentVector; - wireframeIndexBuffer: IndexBuffer; - proxiedCoords: { [string]: Array }; - proxyCoords: Array; - proxyToSource: { [number]: { [string]: Array } }; - proxySourceCache: ProxySourceCache; - renderingToTexture: boolean; - _style: Style; - _mockSourceCache: MockSourceCache; - orthoMatrix: Float32Array; - enabled: boolean; - renderMode: number; - - _visibleDemTiles: Array; - _sourceTilesOverlap: { [string]: boolean }; - _overlapStencilMode: StencilMode; - _overlapStencilType: OverlapStencilType; - _stencilRef: number; - - _exaggeration: number; - _depthFBO: ?Framebuffer; - _depthTexture: ?Texture; - _previousZoom: number; - _updateTimestamp: number; - _useVertexMorphing: boolean; - pool: Array; - renderedToTile: boolean; - _drapedRenderBatches: Array; - _sharedDepthStencil: ?WebGLRenderbuffer; - - _findCoveringTileCache: { [string]: { [number]: ?number } }; - - _tilesDirty: { [string]: { [number]: boolean } }; - _invalidateRenderCache: boolean; - - _emptyDepthBufferTexture: ?Texture; - _emptyDEMTexture: ?Texture; - _initializing: ?boolean; - _emptyDEMTextureDirty: ?boolean; - - constructor(painter: Painter, style: Style) { - super(); - this.painter = painter; - this.terrainTileForTile = {}; - this.prevTerrainTileForTile = {}; - - // Terrain rendering grid is 129x129 cell grid, made by 130x130 points. - // 130 vertices map to 128 DEM data + 1px padding on both sides. - // DEM texture is padded (1, 1, 1, 1) and padding pixels are backfilled - // by neighboring tile edges. This way we achieve tile stitching as - // edge vertices from neighboring tiles evaluate to the same 3D point. - const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid( - GRID_DIM + 1, - ); - const context = painter.context; - this.gridBuffer = context.createVertexBuffer( - triangleGridArray, - posAttributes.members, - ); - this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices); - this.gridSegments = SegmentVector.simpleSegment( - 0, - 0, - triangleGridArray.length, - triangleGridIndices.length, - ); - this.gridNoSkirtSegments = SegmentVector.simpleSegment( - 0, - 0, - triangleGridArray.length, - skirtIndicesOffset, - ); - this.proxyCoords = []; - this.proxiedCoords = {}; - this._visibleDemTiles = []; - this._drapedRenderBatches = []; - this._sourceTilesOverlap = {}; - this.proxySourceCache = new ProxySourceCache(style.map); - this.orthoMatrix = mat4.create(); - const epsilon = this.painter.transform.projection.name === 'globe' ? - .015 : - 0; // Experimentally the smallest value to avoid rendering artifacts (https://github.com/mapbox/mapbox-gl-js/issues/11975) - mat4.ortho(this.orthoMatrix, epsilon, EXTENT, 0, EXTENT, 0, 1); - const gl = context.gl; - this._overlapStencilMode = new StencilMode( - {func: gl.GEQUAL, mask: 0xFF}, - 0, - 0xFF, - gl.KEEP, - gl.KEEP, - gl.REPLACE, - ); - this._previousZoom = painter.transform.zoom; - this.pool = []; - this._findCoveringTileCache = {}; - this._tilesDirty = {}; - this.style = style; - this._useVertexMorphing = true; - this._exaggeration = 1; - this._mockSourceCache = new MockSourceCache(style.map); - } - - set style(style: Style) { - style.on('data', this._onStyleDataEvent.bind(this)); - style.on('neworder', this._checkRenderCacheEfficiency.bind(this)); - this._style = style; - this._checkRenderCacheEfficiency(); - this._style.map.on('moveend', () => { - this._clearLineLayersFromRenderCache(); - }); - } - - /* +export class Terrain extends Elevation { + terrainTileForTile: {[number | string]: Tile}; + prevTerrainTileForTile: {[number | string]: Tile}; + painter: Painter; + sourceCache: SourceCache; + gridBuffer: VertexBuffer; + gridIndexBuffer: IndexBuffer; + gridSegments: SegmentVector; + gridNoSkirtSegments: SegmentVector; + wireframeSegments: SegmentVector; + wireframeIndexBuffer: IndexBuffer; + proxiedCoords: {[string]: Array}; + proxyCoords: Array; + proxyToSource: {[number]: {[string]: Array}}; + proxySourceCache: ProxySourceCache; + renderingToTexture: boolean; + _style: Style; + _mockSourceCache: MockSourceCache; + orthoMatrix: Float32Array; + enabled: boolean; + renderMode: number; + + _visibleDemTiles: Array; + _sourceTilesOverlap: {[string]: boolean}; + _overlapStencilMode: StencilMode; + _overlapStencilType: OverlapStencilType; + _stencilRef: number; + + _exaggeration: number; + _depthFBO: ?Framebuffer; + _depthTexture: ?Texture; + _previousZoom: number; + _updateTimestamp: number; + _useVertexMorphing: boolean; + pool: Array; + renderedToTile: boolean; + _drapedRenderBatches: Array; + _sharedDepthStencil: ?WebGLRenderbuffer; + + _findCoveringTileCache: {[string]: {[number]: ?number}}; + + _tilesDirty: {[string]: {[number]: boolean}}; + _invalidateRenderCache: boolean; + + _emptyDepthBufferTexture: ?Texture; + _emptyDEMTexture: ?Texture; + _initializing: ?boolean; + _emptyDEMTextureDirty: ?boolean; + + constructor(painter: Painter, style: Style) { + super(); + this.painter = painter; + this.terrainTileForTile = {}; + this.prevTerrainTileForTile = {}; + + // Terrain rendering grid is 129x129 cell grid, made by 130x130 points. + // 130 vertices map to 128 DEM data + 1px padding on both sides. + // DEM texture is padded (1, 1, 1, 1) and padding pixels are backfilled + // by neighboring tile edges. This way we achieve tile stitching as + // edge vertices from neighboring tiles evaluate to the same 3D point. + const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid(GRID_DIM + 1); + const context = painter.context; + this.gridBuffer = context.createVertexBuffer(triangleGridArray, posAttributes.members); + this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices); + this.gridSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, triangleGridIndices.length); + this.gridNoSkirtSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, skirtIndicesOffset); + this.proxyCoords = []; + this.proxiedCoords = {}; + this._visibleDemTiles = []; + this._drapedRenderBatches = []; + this._sourceTilesOverlap = {}; + this.proxySourceCache = new ProxySourceCache(style.map); + this.orthoMatrix = mat4.create(); + const epsilon = this.painter.transform.projection.name === 'globe' ? .015 : 0; // Experimentally the smallest value to avoid rendering artifacts (https://github.com/mapbox/mapbox-gl-js/issues/11975) + mat4.ortho(this.orthoMatrix, epsilon, EXTENT, 0, EXTENT, 0, 1); + const gl = context.gl; + this._overlapStencilMode = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, 0, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); + this._previousZoom = painter.transform.zoom; + this.pool = []; + this._findCoveringTileCache = {}; + this._tilesDirty = {}; + this.style = style; + this._useVertexMorphing = true; + this._exaggeration = 1; + this._mockSourceCache = new MockSourceCache(style.map); + } + + set style(style: Style) { + style.on('data', this._onStyleDataEvent.bind(this)); + style.on('neworder', this._checkRenderCacheEfficiency.bind(this)); + this._style = style; + this._checkRenderCacheEfficiency(); + this._style.map.on('moveend', () => { + this._clearLineLayersFromRenderCache(); + }); + } + + /* * Validate terrain and update source cache used for elevation. * Explicitly pass transform to update elevation (Transform.updateElevation) * before using transform for source cache update. */ - update(style: Style, transform: Transform, adaptCameraAltitude: boolean) { - if (style && style.terrain) { - if (this._style !== style) { - this.style = style; - } - this.enabled = true; - const terrainProps = style.terrain.properties; - const isDrapeModeDeferred = style.terrain.drapeRenderMode === DrapeRenderMode.deferred; - this.sourceCache = isDrapeModeDeferred ? - this._mockSourceCache : - ((style._getSourceCache(terrainProps.get('source')): any): SourceCache); - this._exaggeration = terrainProps.get('exaggeration'); - - const updateSourceCache = (() => { - if (this.sourceCache.used) { - warnOnce(`Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + - 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.'); - } - // Lower tile zoom is sufficient for terrain, given the size of terrain grid. - const scaledDemTileSize = this.getScaledDemTileSize(); - // Dem tile needs to be parent or at least of the same zoom level as proxy tile. - // Tile cover roundZoom behavior is set to the same as for proxy (false) in SourceCache.update(). - this.sourceCache.update(transform, scaledDemTileSize, true); - // As a result of update, we get new set of tiles: reset lookup cache. - this.resetTileLookupCache(this.sourceCache.id); - }); - - if (!this.sourceCache.usedForTerrain) { - // Init cache entry. - this.resetTileLookupCache(this.sourceCache.id); - // When toggling terrain on/off load available terrain tiles from cache - // before reading elevation at center. - this.sourceCache.usedForTerrain = true; - updateSourceCache(); - this._initializing = true; - } - - updateSourceCache(); - // Camera gets constrained over terrain. Issue constrainCameraOverTerrain = true - // here to cover potential under terrain situation on data, style, or other camera changes. - transform.updateElevation(true, adaptCameraAltitude); - - // Reset tile lookup cache and update draped tiles coordinates. - this.resetTileLookupCache(this.proxySourceCache.id); - this.proxySourceCache.update(transform); - - this._emptyDEMTextureDirty = true; - } else { - this._disable(); - } - } - - resetTileLookupCache(sourceCacheID: string) { - this._findCoveringTileCache[sourceCacheID] = {}; - } - - getScaledDemTileSize(): number { - const demScale = this.sourceCache.getSource().tileSize / GRID_DIM; - const proxyTileSize = this.proxySourceCache.getSource().tileSize; - return demScale * proxyTileSize; - } - - _checkRenderCacheEfficiency: (() => void) = () => { - const renderCacheInfo = this.renderCacheEfficiency(this._style); - if (this._style.map._optimizeForTerrain) { - assert(renderCacheInfo.efficiency === 100); - } else if (renderCacheInfo.efficiency !== 100) { - warnOnce(`Terrain render cache efficiency is not optimal (${renderCacheInfo.efficiency}%) and performance + update(style: Style, transform: Transform, adaptCameraAltitude: boolean) { + if (style && style.terrain) { + if (this._style !== style) { + this.style = style; + } + this.enabled = true; + const terrainProps = style.terrain.properties; + const isDrapeModeDeferred = style.terrain.drapeRenderMode === DrapeRenderMode.deferred; + this.sourceCache = isDrapeModeDeferred ? this._mockSourceCache : + ((style._getSourceCache(terrainProps.get('source')): any): SourceCache); + this._exaggeration = terrainProps.get('exaggeration'); + + const updateSourceCache = () => { + if (this.sourceCache.used) { + warnOnce(`Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + + 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.'); + } + // Lower tile zoom is sufficient for terrain, given the size of terrain grid. + const scaledDemTileSize = this.getScaledDemTileSize(); + // Dem tile needs to be parent or at least of the same zoom level as proxy tile. + // Tile cover roundZoom behavior is set to the same as for proxy (false) in SourceCache.update(). + this.sourceCache.update(transform, scaledDemTileSize, true); + // As a result of update, we get new set of tiles: reset lookup cache. + this.resetTileLookupCache(this.sourceCache.id); + }; + + if (!this.sourceCache.usedForTerrain) { + // Init cache entry. + this.resetTileLookupCache(this.sourceCache.id); + // When toggling terrain on/off load available terrain tiles from cache + // before reading elevation at center. + this.sourceCache.usedForTerrain = true; + updateSourceCache(); + this._initializing = true; + } + + updateSourceCache(); + // Camera gets constrained over terrain. Issue constrainCameraOverTerrain = true + // here to cover potential under terrain situation on data, style, or other camera changes. + transform.updateElevation(true, adaptCameraAltitude); + + // Reset tile lookup cache and update draped tiles coordinates. + this.resetTileLookupCache(this.proxySourceCache.id); + this.proxySourceCache.update(transform); + + this._emptyDEMTextureDirty = true; + } else { + this._disable(); + } + } + + resetTileLookupCache(sourceCacheID: string) { + this._findCoveringTileCache[sourceCacheID] = {}; + } + + getScaledDemTileSize(): number { + const demScale = this.sourceCache.getSource().tileSize / GRID_DIM; + const proxyTileSize = this.proxySourceCache.getSource().tileSize; + return demScale * proxyTileSize; + } + + _checkRenderCacheEfficiency: (() => void) = () => { + const renderCacheInfo = this.renderCacheEfficiency(this._style); + if (this._style.map._optimizeForTerrain) { + assert(renderCacheInfo.efficiency === 100); + } else if (renderCacheInfo.efficiency !== 100) { + warnOnce(`Terrain render cache efficiency is not optimal (${renderCacheInfo.efficiency}%) and performance may be affected negatively, consider placing all background, fill and line layers before layer - with id '${renderCacheInfo.firstUndrapedLayer}' or create a map using optimizeForTerrain: true option.`,); - } - }; - - _onStyleDataEvent: ((event: any) => void) = (event: any) => { - if (event.coord && event.dataType === 'source') { - this._clearRenderCacheForTile(event.sourceCacheId, event.coord); - } else if (event.dataType === 'style') { - this._invalidateRenderCache = true; - } - }; - - // Terrain - _disable() { - if (!this.enabled) return; - this.enabled = false; - this._sharedDepthStencil = undefined; - this.proxySourceCache.deallocRenderCache(); - if (this._style) { - for (const id in this._style._sourceCaches) { - this._style._sourceCaches[id].usedForTerrain = false; - } - } - } - - destroy() { - this._disable(); - if (this._emptyDEMTexture) this._emptyDEMTexture.destroy(); - if (this._emptyDepthBufferTexture) this._emptyDepthBufferTexture.destroy(); - this.pool.forEach(fbo => fbo.fb.destroy()); - this.pool = []; - if (this._depthFBO) { - this._depthFBO.destroy(); - this._depthFBO = undefined; - this._depthTexture = undefined; - } - } - - // Implements Elevation::_source. - _source(): ?SourceCache { - return this.enabled ? this.sourceCache : null; - } - - isUsingMockSource(): boolean { - return this.sourceCache === this._mockSourceCache; - } - - // Implements Elevation::exaggeration. - exaggeration(): number { - return this._exaggeration; - } - - get visibleDemTiles(): Array { - return this._visibleDemTiles; - } - - get drapeBufferSize(): [number, number] { - const extent = this.proxySourceCache.getSource().tileSize * 2; // *2 is to avoid upscaling bitmap on zoom. - return [extent, extent]; - } - - set useVertexMorphing(enable: boolean) { - this._useVertexMorphing = enable; - } - - // For every renderable coordinate in every source cache, assign one proxy - // tile (see _setupProxiedCoordsForOrtho). Mapping of source tile to proxy - // tile is modeled by ProxiedTileID. In general case, source and proxy tile - // are of different zoom: ProxiedTileID.projMatrix models ortho, scale and - // translate from source to proxy. This matrix is used when rendering source - // tile to proxy tile's texture. - // One proxy tile can have multiple source tiles, or pieces of source tiles, - // that get rendered to it. - // For each proxy tile we assign one terrain tile (_assignTerrainTiles). The - // terrain tile provides elevation data when rendering (draping) proxy tile - // texture over terrain grid. - updateTileBinding(sourcesCoords: { [string]: Array }) { - if (!this.enabled) return; - this.prevTerrainTileForTile = this.terrainTileForTile; - - const psc = this.proxySourceCache; - const tr = this.painter.transform; - if (this._initializing) { - // Don't activate terrain until center tile gets loaded. - this._initializing = tr._centerAltitude === 0 && - this.getAtPointOrZero(MercatorCoordinate.fromLngLat(tr.center), -1) === -1; - this._emptyDEMTextureDirty = !this._initializing; - } - - const coords = this.proxyCoords = psc.getIds().map( - id => { - const tileID = psc.getTileByID(id).tileID; - tileID.projMatrix = tr.calculateProjMatrix(tileID.toUnwrapped()); - return tileID; - }, - ); - sortByDistanceToCamera(coords, this.painter); - this._previousZoom = tr.zoom; - - const previousProxyToSource = this.proxyToSource || {}; - this.proxyToSource = {}; - coords.forEach( - tileID => { - this.proxyToSource[tileID.key] = {}; - }, - ); - - this.terrainTileForTile = {}; - const sourceCaches = this._style._sourceCaches; - for (const id in sourceCaches) { - const sourceCache = sourceCaches[id]; - if (!sourceCache.used) continue; - if (sourceCache !== this.sourceCache) - this.resetTileLookupCache(sourceCache.id); - this._setupProxiedCoordsForOrtho( - sourceCache, - sourcesCoords[id], - previousProxyToSource, - ); - if (sourceCache.usedForTerrain) continue; - const coordinates = sourcesCoords[id]; - if (sourceCache.getSource().reparseOverscaled) { - // Do this for layers that are not rasterized to proxy tile. - this._assignTerrainTiles(coordinates); - } - } - - // Background has no source. Using proxy coords with 1-1 ortho (this.proxiedCoords[psc.id]) - // when rendering background to proxy tiles. - this.proxiedCoords[psc.id] = coords.map( - tileID => new ProxiedTileID(tileID, tileID.key, this.orthoMatrix), - ); - this._assignTerrainTiles(coords); - this._prepareDEMTextures(); - this._setupDrapedRenderBatches(); - this._initFBOPool(); - this._setupRenderCache(previousProxyToSource); - - this.renderingToTexture = false; - this._updateTimestamp = browser.now(); - - // Gather all dem tiles that are assigned to proxy tiles - const visibleKeys = {}; - this._visibleDemTiles = []; - - for (const id of this.proxyCoords) { - const demTile = this.terrainTileForTile[id.key]; - if (!demTile) continue; - const key = demTile.tileID.key; - if (key in visibleKeys) continue; - this._visibleDemTiles.push(demTile); - visibleKeys[key] = key; - } - } - - _assignTerrainTiles(coords: Array) { - if (this._initializing) return; - coords.forEach( - tileID => { - if (this.terrainTileForTile[tileID.key]) return; - const demTile = this._findTileCoveringTileID(tileID, this.sourceCache); - if (demTile) this.terrainTileForTile[tileID.key] = demTile; - }, - ); - } - - _prepareDEMTextures() { - const context = this.painter.context; - const gl = context.gl; - for (const key in this.terrainTileForTile) { - const tile = this.terrainTileForTile[key]; - const dem = tile.dem; - if (dem && (!tile.demTexture || tile.needsDEMTextureUpload)) { - context.activeTexture.set(gl.TEXTURE1); - prepareDEMTexture(this.painter, tile, dem); - } - } - } - - _prepareDemTileUniforms( - proxyTile: Tile, - demTile: ?Tile, - uniforms: UniformValues, - uniformSuffix: ?string, - ): boolean { - if (!demTile || demTile.demTexture == null) return false; - - assert(demTile.dem); - const proxyId = proxyTile.tileID.canonical; - const demId = demTile.tileID.canonical; - const demScaleBy = Math.pow(2, demId.z - proxyId.z); - const suffix = uniformSuffix || ""; - uniforms[`u_dem_tl${suffix}`] = [ - proxyId.x * demScaleBy % 1, - proxyId.y * demScaleBy % 1, - ]; - uniforms[`u_dem_scale${suffix}`] = demScaleBy; - return true; - } - - get emptyDEMTexture(): Texture { - return !this._emptyDEMTextureDirty && this._emptyDEMTexture ? - this._emptyDEMTexture : - this._updateEmptyDEMTexture(); - } - - get emptyDepthBufferTexture(): Texture { - const context = this.painter.context; - const gl = context.gl; - if (!this._emptyDepthBufferTexture) { - const image = new RGBAImage( - {width: 1, height: 1}, - Uint8Array.of(255, 255, 255, 255), - ); - this._emptyDepthBufferTexture = new Texture( - context, - image, - gl.RGBA, - {premultiply: false}, - ); - } - return this._emptyDepthBufferTexture; - } - - _getLoadedAreaMinimum(): number { - let nonzero = 0; - const min = this._visibleDemTiles.reduce( - (acc, tile) => { - if (!tile.dem) return acc; - const m = tile.dem.tree.minimums[0]; - acc += m; - if (m > 0) nonzero++; - return acc; - }, - 0, - ); - return nonzero ? min / nonzero : 0; - } - - _updateEmptyDEMTexture(): Texture { - const context = this.painter.context; - const gl = context.gl; - context.activeTexture.set(gl.TEXTURE2); - - const min = this._getLoadedAreaMinimum(); - const image = new RGBAImage( - {width: 1, height: 1}, - new Uint8Array( - DEMData.pack( - min, - ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding, - ), - ), - ); - - this._emptyDEMTextureDirty = false; - let texture = this._emptyDEMTexture; - if (!texture) { - texture = this._emptyDEMTexture = new Texture( - context, - image, - gl.RGBA, - {premultiply: false}, - ); - } else { - texture.update(image, {premultiply: false}); - } - return texture; - } - - // useDepthForOcclusion: Pre-rendered depth to texture (this._depthTexture) is - // used to hide (actually moves all object's vertices out of viewport). - // useMeterToDem: u_meter_to_dem uniform is not used for all terrain programs, - // optimization to avoid unnecessary computation and upload. - setupElevationDraw( - tile: Tile, - program: Program<*>, - options?: { - useDepthForOcclusion?: boolean, - useMeterToDem?: boolean, - labelPlaneMatrixInv?: ?Float32Array, - morphing?: { srcDemTile: Tile, dstDemTile: Tile, phase: number }, - useDenormalizedUpVectorScale?: boolean, - }, - ) { - const context = this.painter.context; - const gl = context.gl; - const uniforms = defaultTerrainUniforms( - ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding, - ); - uniforms['u_dem_size'] = this.sourceCache.getSource().tileSize; - uniforms['u_exaggeration'] = this.exaggeration(); - - let demTile = null; - let prevDemTile = null; - let morphingPhase = 1.0; - - if (options && options.morphing && this._useVertexMorphing) { - const srcTile = options.morphing.srcDemTile; - const dstTile = options.morphing.dstDemTile; - morphingPhase = options.morphing.phase; - - if (srcTile && dstTile) { - if (this._prepareDemTileUniforms(tile, srcTile, uniforms, "_prev")) - prevDemTile = srcTile; - if (this._prepareDemTileUniforms(tile, dstTile, uniforms)) - demTile = dstTile; - } - } - - if (prevDemTile && demTile) { - // Both DEM textures are expected to be correctly set if geomorphing is enabled - context.activeTexture.set(gl.TEXTURE2); - (demTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); - context.activeTexture.set(gl.TEXTURE4); - (prevDemTile.demTexture: any).bind( - gl.NEAREST, - gl.CLAMP_TO_EDGE, - gl.NEAREST, - ); - - uniforms["u_dem_lerp"] = morphingPhase; - } else { - demTile = this.terrainTileForTile[tile.tileID.key]; - context.activeTexture.set(gl.TEXTURE2); - const demTexture = this._prepareDemTileUniforms(tile, demTile, uniforms) ? - (demTile.demTexture: any) : - this.emptyDEMTexture; - demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - } - - context.activeTexture.set(gl.TEXTURE3); - if (options && options.useDepthForOcclusion) { - if (this._depthTexture) - this._depthTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - if (this._depthFBO) - uniforms['u_depth_size_inv'] = [ - 1 / this._depthFBO.width, - 1 / this._depthFBO.height, - ]; - } else { - this.emptyDepthBufferTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - uniforms['u_depth_size_inv'] = [1, 1]; - } - - if (options && options.useMeterToDem && demTile) { - const meterToDEM = (1 << demTile.tileID.canonical.z) * mercatorZfromAltitude( - 1, - this.painter.transform.center.lat, - ) * this.sourceCache.getSource().tileSize; - uniforms['u_meter_to_dem'] = meterToDEM; - } - if (options && options.labelPlaneMatrixInv) { - uniforms['u_label_plane_matrix_inv'] = options.labelPlaneMatrixInv; - } - program.setTerrainUniformValues(context, uniforms); - - if (this.painter.transform.projection.name === 'globe') { - const globeUniforms = this.globeUniformValues( - this.painter.transform, - tile.tileID.canonical, - options && options.useDenormalizedUpVectorScale, - ); - program.setGlobeUniformValues(context, globeUniforms); - } - } - - globeUniformValues(tr: Transform, id: CanonicalTileID, useDenormalizedUpVectorScale: ?boolean): UniformValues { - const projection = tr.projection; - return { - 'u_tile_tl_up': (projection.upVector(id, 0, 0): any), - 'u_tile_tr_up': (projection.upVector(id, EXTENT, 0): any), - 'u_tile_br_up': (projection.upVector(id, EXTENT, EXTENT): any), - 'u_tile_bl_up': (projection.upVector(id, 0, EXTENT): any), - 'u_tile_up_scale': (useDenormalizedUpVectorScale ? globeMetersToEcef(1) : projection.upVectorScale(id, tr.center.lat, tr.worldSize).metersToTile: any) - }; - } - - renderToBackBuffer(accumulatedDrapes: Array) { - const painter = this.painter; - const context = this.painter.context; - - if (accumulatedDrapes.length === 0) { - return; - } - - context.bindFramebuffer.set(null); - context.viewport.set([0, 0, painter.width, painter.height]); - - painter.gpuTimingDeferredRenderStart(); - - this.renderingToTexture = false; - drawTerrainRaster( - painter, - this, - this.proxySourceCache, - accumulatedDrapes, - this._updateTimestamp, - ); - this.renderingToTexture = true; - - painter.gpuTimingDeferredRenderEnd(); - - accumulatedDrapes.splice(0, accumulatedDrapes.length); - } - - // For each proxy tile, render all layers until the non-draped layer (and - // render the tile to the screen) before advancing to the next proxy tile. - // Returns the last drawn index that is used as a start - // layer for interleaved draped rendering. - // Apart to layer-by-layer rendering used in 2D, here we have proxy-tile-by-proxy-tile - // rendering. - renderBatch(startLayerIndex: number): number { - if (this._drapedRenderBatches.length === 0) { - return startLayerIndex + 1; - } - - this.renderingToTexture = true; - const painter = this.painter; - const context = this.painter.context; - const psc = this.proxySourceCache; - const proxies = this.proxiedCoords[psc.id]; - - // Consume batch of sequential drape layers and move next - const drapedLayerBatch = this._drapedRenderBatches.shift(); - assert(drapedLayerBatch.start === startLayerIndex); - - const accumulatedDrapes = []; - const layerIds = painter.style.order; - - let poolIndex = 0; - for (const proxy of proxies) { - // bind framebuffer and assign texture to the tile (texture used in drawTerrainRaster). - const tile = psc.getTileByID(proxy.proxyTileKey); - const renderCacheIndex = psc.proxyCachedFBO[proxy.key] ? - psc.proxyCachedFBO[proxy.key][startLayerIndex] : - undefined; - const fbo = renderCacheIndex !== undefined ? - psc.renderCache[renderCacheIndex] : - this.pool[poolIndex++]; - const useRenderCache = renderCacheIndex !== undefined; - - tile.texture = fbo.tex; - - if (useRenderCache && !fbo.dirty) { - // Use cached render from previous pass, no need to render again. - accumulatedDrapes.push(tile.tileID); - continue; - } - - context.bindFramebuffer.set(fbo.fb.framebuffer); - this.renderedToTile = false; // reset flag. - if (fbo.dirty) { - // Clear on start. - context.clear({color: Color.transparent, stencil: 0}); - fbo.dirty = false; - } - - let currentStencilSource; // There is no need to setup stencil for the same source for consecutive layers. - for (let j = drapedLayerBatch.start; j <= drapedLayerBatch.end; ++j) { - const layer = painter.style._layers[layerIds[j]]; - const hidden = layer.isHidden(painter.transform.zoom); - assert(this._style.isLayerDraped(layer) || hidden); - if (hidden) continue; - - const sourceCache = painter.style._getLayerSourceCache(layer); - const proxiedCoords = sourceCache ? - this.proxyToSource[proxy.key][sourceCache.id] : - [proxy]; - if (!proxiedCoords) - continue; // when tile is not loaded yet for the source cache. - - const coords = ((proxiedCoords: any): Array); - context.viewport.set([0, 0, fbo.fb.width, fbo.fb.height]); - if (currentStencilSource !== (sourceCache ? sourceCache.id : null)) { - this._setupStencil(fbo, proxiedCoords, layer, sourceCache); - currentStencilSource = sourceCache ? sourceCache.id : null; - } - painter.renderLayer(painter, sourceCache, layer, coords); - } - - if (this.renderedToTile) { - fbo.dirty = true; - accumulatedDrapes.push(tile.tileID); - } else if (!useRenderCache) { - --poolIndex; - assert(poolIndex >= 0); - } - if (poolIndex === FBO_POOL_SIZE) { - poolIndex = 0; - this.renderToBackBuffer(accumulatedDrapes); - } - } - - // Reset states and render last drapes - this.renderToBackBuffer(accumulatedDrapes); - this.renderingToTexture = false; - - context.bindFramebuffer.set(null); - context.viewport.set([0, 0, painter.width, painter.height]); - - return drapedLayerBatch.end + 1; - } - - postRender() { - // Make sure we consumed all the draped terrain batches at this point - assert(this._drapedRenderBatches.length === 0); - } - - renderCacheEfficiency(style: Style): Object { - const layerCount = style.order.length; - - if (layerCount === 0) { - return {efficiency: 100.0}; - } - - let uncacheableLayerCount = 0; - let drapedLayerCount = 0; - let reachedUndrapedLayer = false; - let firstUndrapedLayer; - - for (let i = 0; i < layerCount; ++i) { - const layer = style._layers[style.order[i]]; - if (!this._style.isLayerDraped(layer)) { - if (!reachedUndrapedLayer) { - reachedUndrapedLayer = true; - firstUndrapedLayer = layer.id; - } - } else { - if (reachedUndrapedLayer) { - ++uncacheableLayerCount; - } - ++drapedLayerCount; - } - } - - if (drapedLayerCount === 0) { - return {efficiency: 100.0}; - } - - return { - efficiency: (1.0 - uncacheableLayerCount / drapedLayerCount) * 100.0, - firstUndrapedLayer, - }; - } - - getMinElevationBelowMSL(): number { - let min = 0.0; - // The maximum DEM error in meters to be conservative (SRTM). - const maxDEMError = 30.0; - this._visibleDemTiles.filter(tile => tile.dem).forEach( - tile => { - const minMaxTree = (tile.dem: any).tree; - min = Math.min(min, minMaxTree.minimums[0]); - }, - ); - return min === 0.0 ? min : (min - maxDEMError) * this._exaggeration; - } - - // Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray. - // x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters. - raycast(pos: Vec3, dir: Vec3, exaggeration: number): ?number { - if (!this._visibleDemTiles) return null; - - // Perform initial raycasts against root nodes of the available dem tiles - // and use this information to sort them from closest to furthest. - const preparedTiles = this._visibleDemTiles.filter(tile => tile.dem).map( - tile => { - const id = tile.tileID; - const tiles = 1 << id.overscaledZ; - const {x, y} = id.canonical; - - // Compute tile boundaries in mercator coordinates - const minx = x / tiles; - const maxx = (x + 1) / tiles; - const miny = y / tiles; - const maxy = (y + 1) / tiles; - const tree = (tile.dem: any).tree; - - return { - minx, - miny, - maxx, - maxy, - t: tree.raycastRoot(minx, miny, maxx, maxy, pos, dir, exaggeration), - tile, - }; - }, - ); - - preparedTiles.sort( - (a, b) => { - const at = a.t !== null ? a.t : Number.MAX_VALUE; - const bt = b.t !== null ? b.t : Number.MAX_VALUE; - return at - bt; - }, - ); - - for (const obj of preparedTiles) { - if (obj.t == null) return null; - - // Perform more accurate raycast against the dem tree. First intersection is the closest on - // as all tiles are sorted from closest to furthest - const tree = (obj.tile.dem: any).tree; - const t = tree.raycast( - obj.minx, - obj.miny, - obj.maxx, - obj.maxy, - pos, - dir, - exaggeration, - ); - - if (t != null) return t; - } - - return null; - } - - _createFBO(): FBO { - const painter = this.painter; - const context = painter.context; - const gl = context.gl; - const bufferSize = this.drapeBufferSize; - context.activeTexture.set(gl.TEXTURE0); - const tex = new Texture( - context, - {width: bufferSize[0], height: bufferSize[1], data: null}, - gl.RGBA, - ); - tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); - const fb = context.createFramebuffer(bufferSize[0], bufferSize[1], false); - fb.colorAttachment.set(tex.texture); - fb.depthAttachment = new DepthStencilAttachment(context, fb.framebuffer); - - if (this._sharedDepthStencil === undefined) { - this._sharedDepthStencil = context.createRenderbuffer( - context.gl.DEPTH_STENCIL, - bufferSize[0], - bufferSize[1], - ); - this._stencilRef = 0; - fb.depthAttachment.set(this._sharedDepthStencil); - context.clear({stencil: 0}); - } else { - fb.depthAttachment.set(this._sharedDepthStencil); - } - - if ( - context.extTextureFilterAnisotropic && - !context.extTextureFilterAnisotropicForceOff - ) { - gl.texParameterf( - gl.TEXTURE_2D, - context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, - context.extTextureFilterAnisotropicMax, - ); - } - - return {fb, tex, dirty: false}; - } - - _initFBOPool() { - while (this.pool.length < Math.min(FBO_POOL_SIZE, this.proxyCoords.length)) { - this.pool.push(this._createFBO()); - } - } - - _shouldDisableRenderCache(): boolean { - // Disable render caches on dynamic events due to fading or transitioning. - if (this._style.light && this._style.light.hasTransition()) { - return true; - } - - for (const id in this._style._sourceCaches) { - if (this._style._sourceCaches[id].hasTransition()) { - return true; - } - } - - const isTransitioning = id => { - const layer = this._style._layers[id]; - const isHidden = layer.isHidden(this.painter.transform.zoom); - if (layer.type === 'custom') { - return !isHidden && ((layer: any): CustomStyleLayer).shouldRedrape(); - } - return !isHidden && layer.hasTransition(); - }; - return this._style.order.some(isTransitioning); - } - - _clearLineLayersFromRenderCache() { - let hasVectorSource = false; - for (const source of this._style._getSources()) { - if (source instanceof VectorTileSource) { - hasVectorSource = true; - break; - } - } - - if (!hasVectorSource) return; - - const clearSourceCaches = {}; - for (let i = 0; i < this._style.order.length; ++i) { - const layer = this._style._layers[this._style.order[i]]; - const sourceCache = this._style._getLayerSourceCache(layer); - if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; - - const isHidden = layer.isHidden(this.painter.transform.zoom); - if (isHidden || layer.type !== 'line') continue; - - // Check if layer has a zoom dependent "line-width" expression - const widthExpression = ((layer: any): LineStyleLayer).widthExpression(); - if (!(widthExpression instanceof ZoomDependentExpression)) continue; - - // Mark sourceCache as cleared - clearSourceCaches[sourceCache.id] = true; - for (const proxy of this.proxyCoords) { - const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; - const coords = ((proxiedCoords: any): Array); - if (!coords) continue; - - for (const coord of coords) { - this._clearRenderCacheForTile(sourceCache.id, coord); - } - } - } - } - - _clearRasterLayersFromRenderCache() { - let hasRasterSource = false; - for (const id in this._style._sourceCaches) { - if (this._style._sourceCaches[id]._source instanceof RasterTileSource) { - hasRasterSource = true; - break; - } - } - - if (!hasRasterSource) return; - - const clearSourceCaches = {}; - for (let i = 0; i < this._style.order.length; ++i) { - const layer = this._style._layers[this._style.order[i]]; - const sourceCache = this._style._getLayerSourceCache(layer); - if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; - - const isHidden = layer.isHidden(this.painter.transform.zoom); - if (isHidden || layer.type !== 'raster') continue; - - // Check if any raster tile is in a fading state - const fadeDuration = ((layer: any): RasterStyleLayer).paint.get('raster-fade-duration'); - for (const proxy of this.proxyCoords) { - const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; - const coords = ((proxiedCoords: any): Array); - if (!coords) continue; - - for (const coord of coords) { - const tile = sourceCache.getTile(coord); - const parent = sourceCache.findLoadedParent(coord, 0); - const fade = rasterFade( - tile, - parent, - sourceCache, - this.painter.transform, - fadeDuration, - ); - const isFading = fade.opacity !== 1 || fade.mix !== 0; - if (isFading) { - this._clearRenderCacheForTile(sourceCache.id, coord); - } - } - } - } - } - - _setupDrapedRenderBatches() { - const layerIds = this._style.order; - const layerCount = layerIds.length; - if (layerCount === 0) { - return; - } - - const batches = []; - - let currentLayer = 0; - let layer = this._style._layers[layerIds[currentLayer]]; - while ( - !this._style.isLayerDraped(layer) && - layer.isHidden(this.painter.transform.zoom) && - ++currentLayer < layerCount - ) { - layer = this._style._layers[layerIds[currentLayer]]; - } - - let batchStart; - for (; currentLayer < layerCount; ++currentLayer) { - const layer = this._style._layers[layerIds[currentLayer]]; - if (layer.isHidden(this.painter.transform.zoom)) { - continue; - } - if (!this._style.isLayerDraped(layer)) { - if (batchStart !== undefined) { - batches.push({start: batchStart, end: currentLayer - 1}); - batchStart = undefined; - } - continue; - } - if (batchStart === undefined) { - batchStart = currentLayer; - } - } - - if (batchStart !== undefined) { - batches.push({start: batchStart, end: currentLayer - 1}); - } - - if (this._style.map._optimizeForTerrain) { - // Draped first approach should result in a single or no batch - assert(batches.length === 1 || batches.length === 0); - } - - this._drapedRenderBatches = batches; - } - - _setupRenderCache(previousProxyToSource: {[number]: {[string]: Array}}) { - const psc = this.proxySourceCache; - if (this._shouldDisableRenderCache() || this._invalidateRenderCache) { - this._invalidateRenderCache = false; - if (psc.renderCache.length > psc.renderCachePool.length) { - const used = ((Object.values(psc.proxyCachedFBO): any): Array<{[string | number]: number}>); - psc.proxyCachedFBO = {}; - for (let i = 0; i < used.length; ++i) { - const fbos = ((Object.values(used[i]): any): Array); - psc.renderCachePool.push(...fbos); - } - assert(psc.renderCache.length === psc.renderCachePool.length); - } - return; - } - - this._clearRasterLayersFromRenderCache(); - - const coords = this.proxyCoords; - const dirty = this._tilesDirty; - for (let i = coords.length - 1; i >= 0; i--) { - const proxy = coords[i]; - const tile = psc.getTileByID(proxy.key); - - if (psc.proxyCachedFBO[proxy.key] !== undefined) { - assert(tile.texture); - const prev = previousProxyToSource[proxy.key]; - assert(prev); - // Reuse previous render from cache if there was no change of - // content that was used to render proxy tile. - const current = this.proxyToSource[proxy.key]; - let equal = 0; - for (const source in current) { - const tiles = current[source]; - const prevTiles = prev[source]; - if (!prevTiles || prevTiles.length !== tiles.length || - tiles.some((t, index) => - (t !== prevTiles[index] || - (dirty[source] && dirty[source].hasOwnProperty(t.key) - ))) - ) { - equal = -1; - break; - } - ++equal; - } - // dirty === false: doesn't need to be rendered to, just use cached render. - for (const proxyFBO in psc.proxyCachedFBO[proxy.key]) { - psc.renderCache[psc.proxyCachedFBO[proxy.key][proxyFBO]].dirty = equal < 0 || - equal !== Object.values(prev).length; - } - } - } - - const sortedRenderBatches = [...this._drapedRenderBatches]; - sortedRenderBatches.sort( - (batchA, batchB) => { - const batchASize = batchA.end - batchA.start; - const batchBSize = batchB.end - batchB.start; - return batchBSize - batchASize; - }, - ); - - for (const batch of sortedRenderBatches) { - for (const id of coords) { - if (psc.proxyCachedFBO[id.key]) { - continue; - } - - // Assign renderCache FBO if there are available FBOs in pool. - let index = psc.renderCachePool.pop(); - if ( - index === undefined && psc.renderCache.length < RENDER_CACHE_MAX_SIZE - ) { - index = psc.renderCache.length; - psc.renderCache.push(this._createFBO()); - } - if (index !== undefined) { - psc.proxyCachedFBO[id.key] = {}; - psc.proxyCachedFBO[id.key][batch.start] = index; - psc.renderCache[index].dirty = true; // needs to be rendered to. - - } - } - } - this._tilesDirty = {}; - } - - _setupStencil( - fbo: FBO, - proxiedCoords: Array, - layer: StyleLayer, - sourceCache?: SourceCache, - ) { - if (!sourceCache || !this._sourceTilesOverlap[sourceCache.id]) { - if (this._overlapStencilType) this._overlapStencilType = false; - return; - } - const context = this.painter.context; - const gl = context.gl; - - // If needed, setup stencilling. Don't bother to remove when there is no - // more need: in such case, if there is no overlap, stencilling is disabled. - if (proxiedCoords.length <= 1) { - this._overlapStencilType = false; - return; - } - - let stencilRange; - if (layer.isTileClipped()) { - stencilRange = proxiedCoords.length; - this._overlapStencilMode.test = {func: gl.EQUAL, mask: 0xFF}; - this._overlapStencilType = 'Clip'; - } else if ( - proxiedCoords[0].overscaledZ > proxiedCoords[proxiedCoords.length - 1].overscaledZ - ) { - stencilRange = 1; - this._overlapStencilMode.test = {func: gl.GREATER, mask: 0xFF}; - this._overlapStencilType = 'Mask'; - } else { - this._overlapStencilType = false; - return; - } - if (this._stencilRef + stencilRange > 255) { - context.clear({stencil: 0}); - this._stencilRef = 0; - } - this._stencilRef += stencilRange; - this._overlapStencilMode.ref = this._stencilRef; - if (layer.isTileClipped()) { - this._renderTileClippingMasks(proxiedCoords, this._overlapStencilMode.ref); - } - } - - clipOrMaskOverlapStencilType(): boolean { - return ( - this._overlapStencilType === 'Clip' || this._overlapStencilType === 'Mask' - ); - } - - stencilModeForRTTOverlap(id: OverscaledTileID): $ReadOnly { - if (!this.renderingToTexture || !this._overlapStencilType) { - return StencilMode.disabled; - } - // All source tiles contributing to the same proxy are processed in sequence, in zoom descending order. - // For raster / hillshade overlap masking, ref is based on zoom dif. - // For vector layer clipping, every tile gets dedicated stencil ref. - if (this._overlapStencilType === 'Clip') { - // In immediate 2D mode, we render rects to mark clipping area and handle behavior on tile borders. - // Here, there is no need for now for this: - // 1. overlap is handled by proxy render to texture tiles (there is no overlap there) - // 2. here we handle only brief zoom out semi-transparent color intensity flickering - // and that is avoided fine by stenciling primitives as part of drawing (instead of additional tile quad step). - this._overlapStencilMode.ref = this.painter._tileClippingMaskIDs[id.key]; - } // else this._overlapStencilMode.ref is set to a single value used per proxy tile, in _setupStencil. - return this._overlapStencilMode; - } - - _renderTileClippingMasks(proxiedCoords: Array, ref: number) { - const painter = this.painter; - const context = this.painter.context; - const gl = context.gl; - painter._tileClippingMaskIDs = {}; - context.setColorMode(ColorMode.disabled); - context.setDepthMode(DepthMode.disabled); - - const program = painter.useProgram('clippingMask'); - - for (const tileID of proxiedCoords) { - const id = painter._tileClippingMaskIDs[tileID.key] = --ref; - program.draw( - context, - gl.TRIANGLES, - DepthMode.disabled, - // Tests will always pass, and ref value will be written to stencil buffer. - new StencilMode( - {func: gl.ALWAYS, mask: 0}, - id, - 0xFF, - gl.KEEP, - gl.KEEP, - gl.REPLACE, - ), - ColorMode.disabled, - CullFaceMode.disabled, - clippingMaskUniformValues(tileID.projMatrix), - '$clipping', - painter.tileExtentBuffer, - painter.quadTriangleIndexBuffer, - painter.tileExtentSegments, - ); - } - } - - // Casts a ray from a point on screen and returns the intersection point with the terrain. - // The returned point contains the mercator coordinates in its first 3 components, and elevation - // in meter in its 4th coordinate. - pointCoordinate(screenPoint: Point): ?Vec4 { - const transform = this.painter.transform; - if ( - screenPoint.x < 0 || screenPoint.x > transform.width || screenPoint.y < 0 || - screenPoint.y > transform.height - ) { - return null; - } - - const far = [screenPoint.x, screenPoint.y, 1, 1]; - vec4.transformMat4(far, far, transform.pixelMatrixInverse); - vec4.scale(far, far, 1.0 / far[3]); - // x & y in pixel coordinates, z is altitude in meters - far[0] /= transform.worldSize; - far[1] /= transform.worldSize; - const camera = transform._camera.position; - const mercatorZScale = mercatorZfromAltitude(1, transform.center.lat); - const p = [camera[0], camera[1], camera[2] / mercatorZScale, 0.0]; - const dir = vec3.subtract([], far.slice(0, 3), p); - vec3.normalize(dir, dir); - - const exaggeration = this._exaggeration; - const distanceAlongRay = this.raycast(p, dir, exaggeration); - - if (distanceAlongRay === null || !distanceAlongRay) return null; - vec3.scaleAndAdd(p, p, dir, distanceAlongRay); - p[3] = p[2]; - p[2] *= mercatorZScale; - return p; - } - - drawDepth() { - const painter = this.painter; - const context = painter.context; - const psc = this.proxySourceCache; - - const width = Math.ceil(painter.width), - height = Math.ceil(painter.height); - if ( - this._depthFBO && - (this._depthFBO.width !== width || this._depthFBO.height !== height) - ) { - this._depthFBO.destroy(); - this._depthFBO = undefined; - this._depthTexture = undefined; - } - if (!this._depthFBO) { - const gl = context.gl; - const fbo = context.createFramebuffer(width, height, true); - context.activeTexture.set(gl.TEXTURE0); - const texture = new Texture( - context, - {width, height, data: null}, - gl.RGBA, - ); - texture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - fbo.colorAttachment.set(texture.texture); - const renderbuffer = context.createRenderbuffer( - context.gl.DEPTH_COMPONENT16, - width, - height, - ); - fbo.depthAttachment.set(renderbuffer); - this._depthFBO = fbo; - this._depthTexture = texture; - } - context.bindFramebuffer.set(this._depthFBO.framebuffer); - context.viewport.set([0, 0, width, height]); - - drawTerrainDepth(painter, this, psc, this.proxyCoords); - } - - _setupProxiedCoordsForOrtho( - sourceCache: SourceCache, - sourceCoords: Array, - previousProxyToSource: { [number]: { [string]: Array } }, - ): void { - if (sourceCache.getSource() instanceof ImageSource) { - return this._setupProxiedCoordsForImageSource( - sourceCache, - sourceCoords, - previousProxyToSource, - ); - } - this._findCoveringTileCache[sourceCache.id] = this._findCoveringTileCache[sourceCache.id] || - {}; - const coords = this.proxiedCoords[sourceCache.id] = []; - const proxys = this.proxyCoords; - for (let i = 0; i < proxys.length; i++) { - const proxyTileID = proxys[i]; - const proxied = this._findTileCoveringTileID(proxyTileID, sourceCache); - if (proxied) { - assert(proxied.hasData()); - const id = this._createProxiedId( - proxyTileID, - proxied, - previousProxyToSource[proxyTileID.key] && - previousProxyToSource[proxyTileID.key][sourceCache.id], - ); - coords.push(id); - this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; - } - } - let hasOverlap = false; - for (let i = 0; i < sourceCoords.length; i++) { - const tile = sourceCache.getTile(sourceCoords[i]); - if (!tile || !tile.hasData()) continue; - const proxy = this._findTileCoveringTileID( - tile.tileID, - this.proxySourceCache, - ); - // Don't add the tile if already added in loop above. - if (proxy && proxy.tileID.canonical.z !== tile.tileID.canonical.z) { - const array = this.proxyToSource[proxy.tileID.key][sourceCache.id]; - const id = this._createProxiedId( - proxy.tileID, - tile, - previousProxyToSource[proxy.tileID.key] && - previousProxyToSource[proxy.tileID.key][sourceCache.id], - ); - if (!array) { - this.proxyToSource[proxy.tileID.key][sourceCache.id] = [id]; - } else { - // The last element is parent added in loop above. This way we get - // a list in Z descending order which is needed for stencil masking. - array.splice(array.length - 1, 0, id); - } - coords.push(id); - hasOverlap = true; - } - } - this._sourceTilesOverlap[sourceCache.id] = hasOverlap; - } - - _setupProxiedCoordsForImageSource( - sourceCache: SourceCache, - sourceCoords: Array, - previousProxyToSource: { [number]: { [string]: Array } }, - ) { - if (!sourceCache.getSource().loaded()) return; - - const coords = this.proxiedCoords[sourceCache.id] = []; - const proxys = this.proxyCoords; - const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource); - - const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div( - 1 << imageSource.tileID.z, - ); - const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce( - (acc, coord) => { - acc.min.x = Math.min(acc.min.x, coord.x - anchor.x); - acc.min.y = Math.min(acc.min.y, coord.y - anchor.y); - acc.max.x = Math.max(acc.max.x, coord.x - anchor.x); - acc.max.y = Math.max(acc.max.y, coord.y - anchor.y); - return acc; - }, - { - min: new Point(Number.MAX_VALUE, Number.MAX_VALUE), - max: new Point(-Number.MAX_VALUE, -Number.MAX_VALUE), - }, - ); - - // Fast conservative check using aabb: content outside proxy tile gets clipped out by on render, anyway. - const tileOutsideImage = ((tileID, imageTileID) => { - const x = tileID.wrap + tileID.canonical.x / (1 << tileID.canonical.z); - const y = tileID.canonical.y / (1 << tileID.canonical.z); - const d = EXTENT / (1 << tileID.canonical.z); - - const ix = imageTileID.wrap + imageTileID.canonical.x / (1 << imageTileID.canonical.z); - const iy = imageTileID.canonical.y / (1 << imageTileID.canonical.z); - - return ( - x + d < ix + aabb.min.x || x > ix + aabb.max.x || - y + d < iy + aabb.min.y || - y > iy + aabb.max.y - ); - }); - - for (let i = 0; i < proxys.length; i++) { - const proxyTileID = proxys[i]; - for (let j = 0; j < sourceCoords.length; j++) { - const tile = sourceCache.getTile(sourceCoords[j]); - if (!tile || !tile.hasData()) continue; - - // Setup proxied -> proxy mapping only if image on given tile wrap intersects the proxy tile. - if (tileOutsideImage(proxyTileID, tile.tileID)) continue; - - const id = this._createProxiedId( - proxyTileID, - tile, - previousProxyToSource[proxyTileID.key] && - previousProxyToSource[proxyTileID.key][sourceCache.id], - ); - const array = this.proxyToSource[proxyTileID.key][sourceCache.id]; - if (!array) { - this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; - } else { - array.push(id); - } - coords.push(id); - } - } - } - - // recycle is previous pass content that likely contains proxied ID combining proxy and source tile. - _createProxiedId( - proxyTileID: OverscaledTileID, - tile: Tile, - recycle: Array, - ): ProxiedTileID { - let matrix = this.orthoMatrix; - if (recycle) { - const recycled = recycle.find(proxied => proxied.key === tile.tileID.key); - if (recycled) return recycled; - } - if (tile.tileID.key !== proxyTileID.key) { - const scale = proxyTileID.canonical.z - tile.tileID.canonical.z; - matrix = mat4.create(); - let size, xOffset, yOffset; - const wrap = tile.tileID.wrap - proxyTileID.wrap << proxyTileID.overscaledZ; - if (scale > 0) { - size = EXTENT >> scale; - xOffset = size * ((tile.tileID.canonical.x << scale) - proxyTileID.canonical.x + wrap); - yOffset = size * ((tile.tileID.canonical.y << scale) - proxyTileID.canonical.y); - } else { - size = EXTENT << -scale; - xOffset = EXTENT * (tile.tileID.canonical.x - (proxyTileID.canonical.x + wrap << -scale)); - yOffset = EXTENT * (tile.tileID.canonical.y - (proxyTileID.canonical.y << -scale)); - } - mat4.ortho(matrix, 0, size, 0, size, 0, 1); - mat4.translate(matrix, matrix, [xOffset, yOffset, 0]); - } - return new ProxiedTileID(tile.tileID, proxyTileID.key, matrix); - } - - // A variant of SourceCache.findLoadedParent that considers only visible - // tiles (and doesn't check SourceCache._cache). Another difference is in - // caching "not found" results along the lookup, to leave the lookup early. - // Not found is cached by this._findCoveringTileCache[key] = null; - _findTileCoveringTileID( - tileID: OverscaledTileID, - sourceCache: SourceCache, - ): ?Tile { - let tile = sourceCache.getTile(tileID); - if (tile && tile.hasData()) return tile; - - const lookup = this._findCoveringTileCache[sourceCache.id]; - const key = lookup[tileID.key]; - tile = key ? sourceCache.getTileByID(key) : null; - if ((tile && tile.hasData()) || key === null) return tile; - - assert(!key || tile); - - let sourceTileID = tile ? tile.tileID : tileID; - let z = sourceTileID.overscaledZ; - const minzoom = sourceCache.getSource().minzoom; - const path = []; - if (!key) { - const maxzoom = sourceCache.getSource().maxzoom; - if (tileID.canonical.z >= maxzoom) { - const downscale = tileID.canonical.z - maxzoom; - if (sourceCache.getSource().reparseOverscaled) { - z = Math.max(tileID.canonical.z + 2, sourceCache.transform.tileZoom); - sourceTileID = new OverscaledTileID( - z, - tileID.wrap, - maxzoom, - tileID.canonical.x >> downscale, - tileID.canonical.y >> downscale, - ); - } else if (downscale !== 0) { - z = maxzoom; - sourceTileID = new OverscaledTileID( - z, - tileID.wrap, - maxzoom, - tileID.canonical.x >> downscale, - tileID.canonical.y >> downscale, - ); - } - } - if (sourceTileID.key !== tileID.key) { - path.push(sourceTileID.key); - tile = sourceCache.getTile(sourceTileID); - } - } - - const pathToLookup = (key => { - path.forEach( - id => { - lookup[id] = key; - }, - ); - path.length = 0; - }); - - for (z = z - 1; z >= minzoom && !(tile && tile.hasData()); z--) { - if (tile) { - pathToLookup(tile.tileID.key); // Store lookup to parents not loaded (yet). - - } - const id = sourceTileID.calculateScaledKey(z); - tile = sourceCache.getTileByID(id); - if (tile && tile.hasData()) break; - const key = lookup[id]; - if (key === null) { - break; // There's no tile loaded and no point searching further. - - } else if (key !== undefined) { - tile = sourceCache.getTileByID(key); - assert(tile); - continue; - } - path.push(id); - } - - pathToLookup(tile ? tile.tileID.key : null); - return tile && tile.hasData() ? tile : null; - } - - findDEMTileFor(tileID: OverscaledTileID): ?Tile { - return this.enabled ? - this._findTileCoveringTileID(tileID, this.sourceCache) : - null; - } - - /* + with id '${renderCacheInfo.firstUndrapedLayer}' or create a map using optimizeForTerrain: true option.`); + } + } + + _onStyleDataEvent: ((event: any) => void) = (event: any) => { + if (event.coord && event.dataType === 'source') { + this._clearRenderCacheForTile(event.sourceCacheId, event.coord); + } else if (event.dataType === 'style') { + this._invalidateRenderCache = true; + } + } + + // Terrain + _disable() { + if (!this.enabled) return; + this.enabled = false; + this._sharedDepthStencil = undefined; + this.proxySourceCache.deallocRenderCache(); + if (this._style) { + for (const id in this._style._sourceCaches) { + this._style._sourceCaches[id].usedForTerrain = false; + } + } + } + + destroy() { + this._disable(); + if (this._emptyDEMTexture) this._emptyDEMTexture.destroy(); + if (this._emptyDepthBufferTexture) this._emptyDepthBufferTexture.destroy(); + this.pool.forEach(fbo => fbo.fb.destroy()); + this.pool = []; + if (this._depthFBO) { + this._depthFBO.destroy(); + this._depthFBO = undefined; + this._depthTexture = undefined; + } + } + + // Implements Elevation::_source. + _source(): ?SourceCache { + return this.enabled ? this.sourceCache : null; + } + + isUsingMockSource(): boolean { + return this.sourceCache === this._mockSourceCache; + } + + // Implements Elevation::exaggeration. + exaggeration(): number { + return this._exaggeration; + } + + get visibleDemTiles(): Array { + return this._visibleDemTiles; + } + + get drapeBufferSize(): [number, number] { + const extent = this.proxySourceCache.getSource().tileSize * 2; // *2 is to avoid upscaling bitmap on zoom. + return [extent, extent]; + } + + set useVertexMorphing(enable: boolean) { + this._useVertexMorphing = enable; + } + + // For every renderable coordinate in every source cache, assign one proxy + // tile (see _setupProxiedCoordsForOrtho). Mapping of source tile to proxy + // tile is modeled by ProxiedTileID. In general case, source and proxy tile + // are of different zoom: ProxiedTileID.projMatrix models ortho, scale and + // translate from source to proxy. This matrix is used when rendering source + // tile to proxy tile's texture. + // One proxy tile can have multiple source tiles, or pieces of source tiles, + // that get rendered to it. + // For each proxy tile we assign one terrain tile (_assignTerrainTiles). The + // terrain tile provides elevation data when rendering (draping) proxy tile + // texture over terrain grid. + updateTileBinding(sourcesCoords: {[string]: Array}) { + if (!this.enabled) return; + this.prevTerrainTileForTile = this.terrainTileForTile; + + const psc = this.proxySourceCache; + const tr = this.painter.transform; + if (this._initializing) { + // Don't activate terrain until center tile gets loaded. + this._initializing = tr._centerAltitude === 0 && this.getAtPointOrZero(MercatorCoordinate.fromLngLat(tr.center), -1) === -1; + this._emptyDEMTextureDirty = !this._initializing; + } + + const coords = this.proxyCoords = psc.getIds().map((id) => { + const tileID = psc.getTileByID(id).tileID; + tileID.projMatrix = tr.calculateProjMatrix(tileID.toUnwrapped()); + return tileID; + }); + sortByDistanceToCamera(coords, this.painter); + this._previousZoom = tr.zoom; + + const previousProxyToSource = this.proxyToSource || {}; + this.proxyToSource = {}; + coords.forEach((tileID) => { + this.proxyToSource[tileID.key] = {}; + }); + + this.terrainTileForTile = {}; + const sourceCaches = this._style._sourceCaches; + for (const id in sourceCaches) { + const sourceCache = sourceCaches[id]; + if (!sourceCache.used) continue; + if (sourceCache !== this.sourceCache) this.resetTileLookupCache(sourceCache.id); + this._setupProxiedCoordsForOrtho(sourceCache, sourcesCoords[id], previousProxyToSource); + if (sourceCache.usedForTerrain) continue; + const coordinates = sourcesCoords[id]; + if (sourceCache.getSource().reparseOverscaled) { + // Do this for layers that are not rasterized to proxy tile. + this._assignTerrainTiles(coordinates); + } + } + + // Background has no source. Using proxy coords with 1-1 ortho (this.proxiedCoords[psc.id]) + // when rendering background to proxy tiles. + this.proxiedCoords[psc.id] = coords.map(tileID => new ProxiedTileID(tileID, tileID.key, this.orthoMatrix)); + this._assignTerrainTiles(coords); + this._prepareDEMTextures(); + this._setupDrapedRenderBatches(); + this._initFBOPool(); + this._setupRenderCache(previousProxyToSource); + + this.renderingToTexture = false; + this._updateTimestamp = browser.now(); + + // Gather all dem tiles that are assigned to proxy tiles + const visibleKeys = {}; + this._visibleDemTiles = []; + + for (const id of this.proxyCoords) { + const demTile = this.terrainTileForTile[id.key]; + if (!demTile) + continue; + const key = demTile.tileID.key; + if (key in visibleKeys) + continue; + this._visibleDemTiles.push(demTile); + visibleKeys[key] = key; + } + + } + + _assignTerrainTiles(coords: Array) { + if (this._initializing) return; + coords.forEach((tileID) => { + if (this.terrainTileForTile[tileID.key]) return; + const demTile = this._findTileCoveringTileID(tileID, this.sourceCache); + if (demTile) this.terrainTileForTile[tileID.key] = demTile; + }); + } + + _prepareDEMTextures() { + const context = this.painter.context; + const gl = context.gl; + for (const key in this.terrainTileForTile) { + const tile = this.terrainTileForTile[key]; + const dem = tile.dem; + if (dem && (!tile.demTexture || tile.needsDEMTextureUpload)) { + context.activeTexture.set(gl.TEXTURE1); + prepareDEMTexture(this.painter, tile, dem); + } + } + } + + _prepareDemTileUniforms(proxyTile: Tile, demTile: ?Tile, uniforms: UniformValues, uniformSuffix: ?string): boolean { + if (!demTile || demTile.demTexture == null) + return false; + + assert(demTile.dem); + const proxyId = proxyTile.tileID.canonical; + const demId = demTile.tileID.canonical; + const demScaleBy = Math.pow(2, demId.z - proxyId.z); + const suffix = uniformSuffix || ""; + uniforms[`u_dem_tl${suffix}`] = [proxyId.x * demScaleBy % 1, proxyId.y * demScaleBy % 1]; + uniforms[`u_dem_scale${suffix}`] = demScaleBy; + return true; + } + + get emptyDEMTexture(): Texture { + return !this._emptyDEMTextureDirty && this._emptyDEMTexture ? + this._emptyDEMTexture : this._updateEmptyDEMTexture(); + } + + get emptyDepthBufferTexture(): Texture { + const context = this.painter.context; + const gl = context.gl; + if (!this._emptyDepthBufferTexture) { + const image = new RGBAImage({width: 1, height: 1}, Uint8Array.of(255, 255, 255, 255)); + this._emptyDepthBufferTexture = new Texture(context, image, gl.RGBA, {premultiply: false}); + } + return this._emptyDepthBufferTexture; + } + + _getLoadedAreaMinimum(): number { + let nonzero = 0; + const min = this._visibleDemTiles.reduce((acc, tile) => { + if (!tile.dem) return acc; + const m = tile.dem.tree.minimums[0]; + acc += m; + if (m > 0) nonzero++; + return acc; + }, 0); + return nonzero ? min / nonzero : 0; + } + + _updateEmptyDEMTexture(): Texture { + const context = this.painter.context; + const gl = context.gl; + context.activeTexture.set(gl.TEXTURE2); + + const min = this._getLoadedAreaMinimum(); + const image = new RGBAImage( + {width: 1, height: 1}, + new Uint8Array(DEMData.pack(min, ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding)) + ); + + this._emptyDEMTextureDirty = false; + let texture = this._emptyDEMTexture; + if (!texture) { + texture = this._emptyDEMTexture = new Texture(context, image, gl.RGBA, {premultiply: false}); + } else { + texture.update(image, {premultiply: false}); + } + return texture; + } + + // useDepthForOcclusion: Pre-rendered depth to texture (this._depthTexture) is + // used to hide (actually moves all object's vertices out of viewport). + // useMeterToDem: u_meter_to_dem uniform is not used for all terrain programs, + // optimization to avoid unnecessary computation and upload. + setupElevationDraw(tile: Tile, program: Program<*>, + options?: { + useDepthForOcclusion?: boolean, + useMeterToDem?: boolean, + labelPlaneMatrixInv?: ?Float32Array, + morphing?: { srcDemTile: Tile, dstDemTile: Tile, phase: number }, + useDenormalizedUpVectorScale?: boolean + }) { + const context = this.painter.context; + const gl = context.gl; + const uniforms = defaultTerrainUniforms(((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding); + uniforms['u_dem_size'] = this.sourceCache.getSource().tileSize; + uniforms['u_exaggeration'] = this.exaggeration(); + + let demTile = null; + let prevDemTile = null; + let morphingPhase = 1.0; + + if (options && options.morphing && this._useVertexMorphing) { + const srcTile = options.morphing.srcDemTile; + const dstTile = options.morphing.dstDemTile; + morphingPhase = options.morphing.phase; + + if (srcTile && dstTile) { + if (this._prepareDemTileUniforms(tile, srcTile, uniforms, "_prev")) + prevDemTile = srcTile; + if (this._prepareDemTileUniforms(tile, dstTile, uniforms)) + demTile = dstTile; + } + } + + if (prevDemTile && demTile) { + // Both DEM textures are expected to be correctly set if geomorphing is enabled + context.activeTexture.set(gl.TEXTURE2); + (demTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + context.activeTexture.set(gl.TEXTURE4); + (prevDemTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + + uniforms["u_dem_lerp"] = morphingPhase; + } else { + demTile = this.terrainTileForTile[tile.tileID.key]; + context.activeTexture.set(gl.TEXTURE2); + const demTexture = this._prepareDemTileUniforms(tile, demTile, uniforms) ? + (demTile.demTexture: any) : this.emptyDEMTexture; + demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + } + + context.activeTexture.set(gl.TEXTURE3); + if (options && options.useDepthForOcclusion) { + if (this._depthTexture) this._depthTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + if (this._depthFBO) uniforms['u_depth_size_inv'] = [1 / this._depthFBO.width, 1 / this._depthFBO.height]; + } else { + this.emptyDepthBufferTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + uniforms['u_depth_size_inv'] = [1, 1]; + } + + if (options && options.useMeterToDem && demTile) { + const meterToDEM = (1 << demTile.tileID.canonical.z) * mercatorZfromAltitude(1, this.painter.transform.center.lat) * this.sourceCache.getSource().tileSize; + uniforms['u_meter_to_dem'] = meterToDEM; + } + if (options && options.labelPlaneMatrixInv) { + uniforms['u_label_plane_matrix_inv'] = options.labelPlaneMatrixInv; + } + program.setTerrainUniformValues(context, uniforms); + + if (this.painter.transform.projection.name === 'globe') { + const globeUniforms = this.globeUniformValues(this.painter.transform, tile.tileID.canonical, options && options.useDenormalizedUpVectorScale); + program.setGlobeUniformValues(context, globeUniforms); + } + } + + globeUniformValues(tr: Transform, id: CanonicalTileID, useDenormalizedUpVectorScale: ?boolean): UniformValues { + const projection = tr.projection; + return { + 'u_tile_tl_up': (projection.upVector(id, 0, 0): any), + 'u_tile_tr_up': (projection.upVector(id, EXTENT, 0): any), + 'u_tile_br_up': (projection.upVector(id, EXTENT, EXTENT): any), + 'u_tile_bl_up': (projection.upVector(id, 0, EXTENT): any), + 'u_tile_up_scale': (useDenormalizedUpVectorScale ? globeMetersToEcef(1) : projection.upVectorScale(id, tr.center.lat, tr.worldSize).metersToTile: any) + }; + } + + renderToBackBuffer(accumulatedDrapes: Array) { + const painter = this.painter; + const context = this.painter.context; + + if (accumulatedDrapes.length === 0) { + return; + } + + context.bindFramebuffer.set(null); + context.viewport.set([0, 0, painter.width, painter.height]); + + painter.gpuTimingDeferredRenderStart(); + + this.renderingToTexture = false; + drawTerrainRaster(painter, this, this.proxySourceCache, accumulatedDrapes, this._updateTimestamp); + this.renderingToTexture = true; + + painter.gpuTimingDeferredRenderEnd(); + + accumulatedDrapes.splice(0, accumulatedDrapes.length); + } + + // For each proxy tile, render all layers until the non-draped layer (and + // render the tile to the screen) before advancing to the next proxy tile. + // Returns the last drawn index that is used as a start + // layer for interleaved draped rendering. + // Apart to layer-by-layer rendering used in 2D, here we have proxy-tile-by-proxy-tile + // rendering. + renderBatch(startLayerIndex: number): number { + if (this._drapedRenderBatches.length === 0) { + return startLayerIndex + 1; + } + + this.renderingToTexture = true; + const painter = this.painter; + const context = this.painter.context; + const psc = this.proxySourceCache; + const proxies = this.proxiedCoords[psc.id]; + + // Consume batch of sequential drape layers and move next + const drapedLayerBatch = this._drapedRenderBatches.shift(); + assert(drapedLayerBatch.start === startLayerIndex); + + const accumulatedDrapes = []; + const layerIds = painter.style.order; + + let poolIndex = 0; + for (const proxy of proxies) { + // bind framebuffer and assign texture to the tile (texture used in drawTerrainRaster). + const tile = psc.getTileByID(proxy.proxyTileKey); + const renderCacheIndex = psc.proxyCachedFBO[proxy.key] ? psc.proxyCachedFBO[proxy.key][startLayerIndex] : undefined; + const fbo = renderCacheIndex !== undefined ? psc.renderCache[renderCacheIndex] : this.pool[poolIndex++]; + const useRenderCache = renderCacheIndex !== undefined; + + tile.texture = fbo.tex; + + if (useRenderCache && !fbo.dirty) { + // Use cached render from previous pass, no need to render again. + accumulatedDrapes.push(tile.tileID); + continue; + } + + context.bindFramebuffer.set(fbo.fb.framebuffer); + this.renderedToTile = false; // reset flag. + if (fbo.dirty) { + // Clear on start. + context.clear({color: Color.transparent, stencil: 0}); + fbo.dirty = false; + } + + let currentStencilSource; // There is no need to setup stencil for the same source for consecutive layers. + for (let j = drapedLayerBatch.start; j <= drapedLayerBatch.end; ++j) { + const layer = painter.style._layers[layerIds[j]]; + const hidden = layer.isHidden(painter.transform.zoom); + assert(this._style.isLayerDraped(layer) || hidden); + if (hidden) continue; + + const sourceCache = painter.style._getLayerSourceCache(layer); + const proxiedCoords = sourceCache ? this.proxyToSource[proxy.key][sourceCache.id] : [proxy]; + if (!proxiedCoords) continue; // when tile is not loaded yet for the source cache. + + const coords = ((proxiedCoords: any): Array); + context.viewport.set([0, 0, fbo.fb.width, fbo.fb.height]); + if (currentStencilSource !== (sourceCache ? sourceCache.id : null)) { + this._setupStencil(fbo, proxiedCoords, layer, sourceCache); + currentStencilSource = sourceCache ? sourceCache.id : null; + } + painter.renderLayer(painter, sourceCache, layer, coords); + } + + if (this.renderedToTile) { + fbo.dirty = true; + accumulatedDrapes.push(tile.tileID); + } else if (!useRenderCache) { + --poolIndex; + assert(poolIndex >= 0); + } + if (poolIndex === FBO_POOL_SIZE) { + poolIndex = 0; + this.renderToBackBuffer(accumulatedDrapes); + } + } + + // Reset states and render last drapes + this.renderToBackBuffer(accumulatedDrapes); + this.renderingToTexture = false; + + context.bindFramebuffer.set(null); + context.viewport.set([0, 0, painter.width, painter.height]); + + return drapedLayerBatch.end + 1; + } + + postRender() { + // Make sure we consumed all the draped terrain batches at this point + assert(this._drapedRenderBatches.length === 0); + } + + renderCacheEfficiency(style: Style): Object { + const layerCount = style.order.length; + + if (layerCount === 0) { + return {efficiency: 100.0}; + } + + let uncacheableLayerCount = 0; + let drapedLayerCount = 0; + let reachedUndrapedLayer = false; + let firstUndrapedLayer; + + for (let i = 0; i < layerCount; ++i) { + const layer = style._layers[style.order[i]]; + if (!this._style.isLayerDraped(layer)) { + if (!reachedUndrapedLayer) { + reachedUndrapedLayer = true; + firstUndrapedLayer = layer.id; + } + } else { + if (reachedUndrapedLayer) { + ++uncacheableLayerCount; + } + ++drapedLayerCount; + } + } + + if (drapedLayerCount === 0) { + return {efficiency: 100.0}; + } + + return {efficiency: (1.0 - uncacheableLayerCount / drapedLayerCount) * 100.0, firstUndrapedLayer}; + } + + getMinElevationBelowMSL(): number { + let min = 0.0; + // The maximum DEM error in meters to be conservative (SRTM). + const maxDEMError = 30.0; + this._visibleDemTiles.filter(tile => tile.dem).forEach(tile => { + const minMaxTree = (tile.dem: any).tree; + min = Math.min(min, minMaxTree.minimums[0]); + }); + return min === 0.0 ? min : (min - maxDEMError) * this._exaggeration; + } + + // Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray. + // x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters. + raycast(pos: Vec3, dir: Vec3, exaggeration: number): ?number { + if (!this._visibleDemTiles) + return null; + + // Perform initial raycasts against root nodes of the available dem tiles + // and use this information to sort them from closest to furthest. + const preparedTiles = this._visibleDemTiles.filter(tile => tile.dem).map(tile => { + const id = tile.tileID; + const tiles = 1 << id.overscaledZ; + const {x, y} = id.canonical; + + // Compute tile boundaries in mercator coordinates + const minx = x / tiles; + const maxx = (x + 1) / tiles; + const miny = y / tiles; + const maxy = (y + 1) / tiles; + const tree = (tile.dem: any).tree; + + return { + minx, miny, maxx, maxy, + t: tree.raycastRoot(minx, miny, maxx, maxy, pos, dir, exaggeration), + tile + }; + }); + + preparedTiles.sort((a, b) => { + const at = a.t !== null ? a.t : Number.MAX_VALUE; + const bt = b.t !== null ? b.t : Number.MAX_VALUE; + return at - bt; + }); + + for (const obj of preparedTiles) { + if (obj.t == null) + return null; + + // Perform more accurate raycast against the dem tree. First intersection is the closest on + // as all tiles are sorted from closest to furthest + const tree = (obj.tile.dem: any).tree; + const t = tree.raycast(obj.minx, obj.miny, obj.maxx, obj.maxy, pos, dir, exaggeration); + + if (t != null) + return t; + } + + return null; + } + + _createFBO(): FBO { + const painter = this.painter; + const context = painter.context; + const gl = context.gl; + const bufferSize = this.drapeBufferSize; + context.activeTexture.set(gl.TEXTURE0); + const tex = new Texture(context, {width: bufferSize[0], height: bufferSize[1], data: null}, gl.RGBA); + tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + const fb = context.createFramebuffer(bufferSize[0], bufferSize[1], false); + fb.colorAttachment.set(tex.texture); + fb.depthAttachment = new DepthStencilAttachment(context, fb.framebuffer); + + if (this._sharedDepthStencil === undefined) { + this._sharedDepthStencil = context.createRenderbuffer(context.gl.DEPTH_STENCIL, bufferSize[0], bufferSize[1]); + this._stencilRef = 0; + fb.depthAttachment.set(this._sharedDepthStencil); + context.clear({stencil: 0}); + } else { + fb.depthAttachment.set(this._sharedDepthStencil); + } + + if (context.extTextureFilterAnisotropic && !context.extTextureFilterAnisotropicForceOff) { + gl.texParameterf(gl.TEXTURE_2D, + context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, + context.extTextureFilterAnisotropicMax); + } + + return {fb, tex, dirty: false}; + } + + _initFBOPool() { + while (this.pool.length < Math.min(FBO_POOL_SIZE, this.proxyCoords.length)) { + this.pool.push(this._createFBO()); + } + } + + _shouldDisableRenderCache(): boolean { + // Disable render caches on dynamic events due to fading or transitioning. + if (this._style.light && this._style.light.hasTransition()) { + return true; + } + + for (const id in this._style._sourceCaches) { + if (this._style._sourceCaches[id].hasTransition()) { + return true; + } + } + + const isTransitioning = id => { + const layer = this._style._layers[id]; + const isHidden = layer.isHidden(this.painter.transform.zoom); + if (layer.type === 'custom') { + return !isHidden && ((layer: any): CustomStyleLayer).shouldRedrape(); + } + return !isHidden && layer.hasTransition(); + }; + return this._style.order.some(isTransitioning); + } + + _clearLineLayersFromRenderCache() { + let hasVectorSource = false; + for (const source of this._style._getSources()) { + if (source instanceof VectorTileSource) { + hasVectorSource = true; + break; + } + } + + if (!hasVectorSource) return; + + const clearSourceCaches = {}; + for (let i = 0; i < this._style.order.length; ++i) { + const layer = this._style._layers[this._style.order[i]]; + const sourceCache = this._style._getLayerSourceCache(layer); + if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; + + const isHidden = layer.isHidden(this.painter.transform.zoom); + if (isHidden || layer.type !== 'line') continue; + + // Check if layer has a zoom dependent "line-width" expression + const widthExpression = ((layer: any): LineStyleLayer).widthExpression(); + if (!(widthExpression instanceof ZoomDependentExpression)) continue; + + // Mark sourceCache as cleared + clearSourceCaches[sourceCache.id] = true; + for (const proxy of this.proxyCoords) { + const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; + const coords = ((proxiedCoords: any): Array); + if (!coords) continue; + + for (const coord of coords) { + this._clearRenderCacheForTile(sourceCache.id, coord); + } + } + } + } + + _clearRasterLayersFromRenderCache() { + let hasRasterSource = false; + for (const id in this._style._sourceCaches) { + if (this._style._sourceCaches[id]._source instanceof RasterTileSource) { + hasRasterSource = true; + break; + } + } + + if (!hasRasterSource) return; + + const clearSourceCaches = {}; + for (let i = 0; i < this._style.order.length; ++i) { + const layer = this._style._layers[this._style.order[i]]; + const sourceCache = this._style._getLayerSourceCache(layer); + if (!sourceCache || clearSourceCaches[sourceCache.id]) continue; + + const isHidden = layer.isHidden(this.painter.transform.zoom); + if (isHidden || layer.type !== 'raster') continue; + + // Check if any raster tile is in a fading state + const fadeDuration = ((layer: any): RasterStyleLayer).paint.get('raster-fade-duration'); + for (const proxy of this.proxyCoords) { + const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; + const coords = ((proxiedCoords: any): Array); + if (!coords) continue; + + for (const coord of coords) { + const tile = sourceCache.getTile(coord); + const parent = sourceCache.findLoadedParent(coord, 0); + const fade = rasterFade(tile, parent, sourceCache, this.painter.transform, fadeDuration); + const isFading = fade.opacity !== 1 || fade.mix !== 0; + if (isFading) { + this._clearRenderCacheForTile(sourceCache.id, coord); + } + } + } + } + } + + _setupDrapedRenderBatches() { + const layerIds = this._style.order; + const layerCount = layerIds.length; + if (layerCount === 0) { + return; + } + + const batches = []; + + let currentLayer = 0; + let layer = this._style._layers[layerIds[currentLayer]]; + while (!this._style.isLayerDraped(layer) && layer.isHidden(this.painter.transform.zoom) && ++currentLayer < layerCount) { + layer = this._style._layers[layerIds[currentLayer]]; + } + + let batchStart; + for (; currentLayer < layerCount; ++currentLayer) { + const layer = this._style._layers[layerIds[currentLayer]]; + if (layer.isHidden(this.painter.transform.zoom)) { + continue; + } + if (!this._style.isLayerDraped(layer)) { + if (batchStart !== undefined) { + batches.push({start: batchStart, end: currentLayer - 1}); + batchStart = undefined; + } + continue; + } + if (batchStart === undefined) { + batchStart = currentLayer; + } + } + + if (batchStart !== undefined) { + batches.push({start: batchStart, end: currentLayer - 1}); + } + + if (this._style.map._optimizeForTerrain) { + // Draped first approach should result in a single or no batch + assert(batches.length === 1 || batches.length === 0); + } + + this._drapedRenderBatches = batches; + } + + _setupRenderCache(previousProxyToSource: {[number]: {[string]: Array}}) { + const psc = this.proxySourceCache; + if (this._shouldDisableRenderCache() || this._invalidateRenderCache) { + this._invalidateRenderCache = false; + if (psc.renderCache.length > psc.renderCachePool.length) { + const used = ((Object.values(psc.proxyCachedFBO): any): Array<{[string | number]: number}>); + psc.proxyCachedFBO = {}; + for (let i = 0; i < used.length; ++i) { + const fbos = ((Object.values(used[i]): any): Array); + psc.renderCachePool.push(...fbos); + } + assert(psc.renderCache.length === psc.renderCachePool.length); + } + return; + } + + this._clearRasterLayersFromRenderCache(); + + const coords = this.proxyCoords; + const dirty = this._tilesDirty; + for (let i = coords.length - 1; i >= 0; i--) { + const proxy = coords[i]; + const tile = psc.getTileByID(proxy.key); + + if (psc.proxyCachedFBO[proxy.key] !== undefined) { + assert(tile.texture); + const prev = previousProxyToSource[proxy.key]; + assert(prev); + // Reuse previous render from cache if there was no change of + // content that was used to render proxy tile. + const current = this.proxyToSource[proxy.key]; + let equal = 0; + for (const source in current) { + const tiles = current[source]; + const prevTiles = prev[source]; + if (!prevTiles || prevTiles.length !== tiles.length || + tiles.some((t, index) => + (t !== prevTiles[index] || + (dirty[source] && dirty[source].hasOwnProperty(t.key) + ))) + ) { + equal = -1; + break; + } + ++equal; + } + // dirty === false: doesn't need to be rendered to, just use cached render. + for (const proxyFBO in psc.proxyCachedFBO[proxy.key]) { + psc.renderCache[psc.proxyCachedFBO[proxy.key][proxyFBO]].dirty = equal < 0 || equal !== Object.values(prev).length; + } + } + } + + const sortedRenderBatches = [...this._drapedRenderBatches]; + sortedRenderBatches.sort((batchA, batchB) => { + const batchASize = batchA.end - batchA.start; + const batchBSize = batchB.end - batchB.start; + return batchBSize - batchASize; + }); + + for (const batch of sortedRenderBatches) { + for (const id of coords) { + if (psc.proxyCachedFBO[id.key]) { + continue; + } + + // Assign renderCache FBO if there are available FBOs in pool. + let index = psc.renderCachePool.pop(); + if (index === undefined && psc.renderCache.length < RENDER_CACHE_MAX_SIZE) { + index = psc.renderCache.length; + psc.renderCache.push(this._createFBO()); + } + if (index !== undefined) { + psc.proxyCachedFBO[id.key] = {}; + psc.proxyCachedFBO[id.key][batch.start] = index; + psc.renderCache[index].dirty = true; // needs to be rendered to. + } + } + } + this._tilesDirty = {}; + } + + _setupStencil(fbo: FBO, proxiedCoords: Array, layer: StyleLayer, sourceCache?: SourceCache) { + if (!sourceCache || !this._sourceTilesOverlap[sourceCache.id]) { + if (this._overlapStencilType) this._overlapStencilType = false; + return; + } + const context = this.painter.context; + const gl = context.gl; + + // If needed, setup stencilling. Don't bother to remove when there is no + // more need: in such case, if there is no overlap, stencilling is disabled. + if (proxiedCoords.length <= 1) { this._overlapStencilType = false; return; } + + let stencilRange; + if (layer.isTileClipped()) { + stencilRange = proxiedCoords.length; + this._overlapStencilMode.test = {func: gl.EQUAL, mask: 0xFF}; + this._overlapStencilType = 'Clip'; + } else if (proxiedCoords[0].overscaledZ > proxiedCoords[proxiedCoords.length - 1].overscaledZ) { + stencilRange = 1; + this._overlapStencilMode.test = {func: gl.GREATER, mask: 0xFF}; + this._overlapStencilType = 'Mask'; + } else { + this._overlapStencilType = false; + return; + } + if (this._stencilRef + stencilRange > 255) { + context.clear({stencil: 0}); + this._stencilRef = 0; + } + this._stencilRef += stencilRange; + this._overlapStencilMode.ref = this._stencilRef; + if (layer.isTileClipped()) { + this._renderTileClippingMasks(proxiedCoords, this._overlapStencilMode.ref); + } + } + + clipOrMaskOverlapStencilType(): boolean { + return this._overlapStencilType === 'Clip' || this._overlapStencilType === 'Mask'; + } + + stencilModeForRTTOverlap(id: OverscaledTileID): $ReadOnly { + if (!this.renderingToTexture || !this._overlapStencilType) { + return StencilMode.disabled; + } + // All source tiles contributing to the same proxy are processed in sequence, in zoom descending order. + // For raster / hillshade overlap masking, ref is based on zoom dif. + // For vector layer clipping, every tile gets dedicated stencil ref. + if (this._overlapStencilType === 'Clip') { + // In immediate 2D mode, we render rects to mark clipping area and handle behavior on tile borders. + // Here, there is no need for now for this: + // 1. overlap is handled by proxy render to texture tiles (there is no overlap there) + // 2. here we handle only brief zoom out semi-transparent color intensity flickering + // and that is avoided fine by stenciling primitives as part of drawing (instead of additional tile quad step). + this._overlapStencilMode.ref = this.painter._tileClippingMaskIDs[id.key]; + } // else this._overlapStencilMode.ref is set to a single value used per proxy tile, in _setupStencil. + return this._overlapStencilMode; + } + + _renderTileClippingMasks(proxiedCoords: Array, ref: number) { + const painter = this.painter; + const context = this.painter.context; + const gl = context.gl; + painter._tileClippingMaskIDs = {}; + context.setColorMode(ColorMode.disabled); + context.setDepthMode(DepthMode.disabled); + + const program = painter.useProgram('clippingMask'); + + for (const tileID of proxiedCoords) { + const id = painter._tileClippingMaskIDs[tileID.key] = --ref; + program.draw(context, gl.TRIANGLES, DepthMode.disabled, + // Tests will always pass, and ref value will be written to stencil buffer. + new StencilMode({func: gl.ALWAYS, mask: 0}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE), + ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.projMatrix), + '$clipping', painter.tileExtentBuffer, + painter.quadTriangleIndexBuffer, painter.tileExtentSegments); + } + } + + // Casts a ray from a point on screen and returns the intersection point with the terrain. + // The returned point contains the mercator coordinates in its first 3 components, and elevation + // in meter in its 4th coordinate. + pointCoordinate(screenPoint: Point): ?Vec4 { + const transform = this.painter.transform; + if (screenPoint.x < 0 || screenPoint.x > transform.width || + screenPoint.y < 0 || screenPoint.y > transform.height) { + return null; + } + + const far = [screenPoint.x, screenPoint.y, 1, 1]; + vec4.transformMat4(far, far, transform.pixelMatrixInverse); + vec4.scale(far, far, 1.0 / far[3]); + // x & y in pixel coordinates, z is altitude in meters + far[0] /= transform.worldSize; + far[1] /= transform.worldSize; + const camera = transform._camera.position; + const mercatorZScale = mercatorZfromAltitude(1, transform.center.lat); + const p = [camera[0], camera[1], camera[2] / mercatorZScale, 0.0]; + const dir = vec3.subtract([], far.slice(0, 3), p); + vec3.normalize(dir, dir); + + const exaggeration = this._exaggeration; + const distanceAlongRay = this.raycast(p, dir, exaggeration); + + if (distanceAlongRay === null || !distanceAlongRay) return null; + vec3.scaleAndAdd(p, p, dir, distanceAlongRay); + p[3] = p[2]; + p[2] *= mercatorZScale; + return p; + } + + drawDepth() { + const painter = this.painter; + const context = painter.context; + const psc = this.proxySourceCache; + + const width = Math.ceil(painter.width), height = Math.ceil(painter.height); + if (this._depthFBO && (this._depthFBO.width !== width || this._depthFBO.height !== height)) { + this._depthFBO.destroy(); + this._depthFBO = undefined; + this._depthTexture = undefined; + } + if (!this._depthFBO) { + const gl = context.gl; + const fbo = context.createFramebuffer(width, height, true); + context.activeTexture.set(gl.TEXTURE0); + const texture = new Texture(context, {width, height, data: null}, gl.RGBA); + texture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + fbo.colorAttachment.set(texture.texture); + const renderbuffer = context.createRenderbuffer(context.gl.DEPTH_COMPONENT16, width, height); + fbo.depthAttachment.set(renderbuffer); + this._depthFBO = fbo; + this._depthTexture = texture; + } + context.bindFramebuffer.set(this._depthFBO.framebuffer); + context.viewport.set([0, 0, width, height]); + + drawTerrainDepth(painter, this, psc, this.proxyCoords); + } + + _setupProxiedCoordsForOrtho(sourceCache: SourceCache, sourceCoords: Array, previousProxyToSource: {[number]: {[string]: Array}}): void { + if (sourceCache.getSource() instanceof ImageSource) { + return this._setupProxiedCoordsForImageSource(sourceCache, sourceCoords, previousProxyToSource); + } + this._findCoveringTileCache[sourceCache.id] = this._findCoveringTileCache[sourceCache.id] || {}; + const coords = this.proxiedCoords[sourceCache.id] = []; + const proxys = this.proxyCoords; + for (let i = 0; i < proxys.length; i++) { + const proxyTileID = proxys[i]; + const proxied = this._findTileCoveringTileID(proxyTileID, sourceCache); + if (proxied) { + assert(proxied.hasData()); + const id = this._createProxiedId(proxyTileID, proxied, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]); + coords.push(id); + this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; + } + } + let hasOverlap = false; + for (let i = 0; i < sourceCoords.length; i++) { + const tile = sourceCache.getTile(sourceCoords[i]); + if (!tile || !tile.hasData()) continue; + const proxy = this._findTileCoveringTileID(tile.tileID, this.proxySourceCache); + // Don't add the tile if already added in loop above. + if (proxy && proxy.tileID.canonical.z !== tile.tileID.canonical.z) { + const array = this.proxyToSource[proxy.tileID.key][sourceCache.id]; + const id = this._createProxiedId(proxy.tileID, tile, previousProxyToSource[proxy.tileID.key] && previousProxyToSource[proxy.tileID.key][sourceCache.id]); + if (!array) { + this.proxyToSource[proxy.tileID.key][sourceCache.id] = [id]; + } else { + // The last element is parent added in loop above. This way we get + // a list in Z descending order which is needed for stencil masking. + array.splice(array.length - 1, 0, id); + } + coords.push(id); + hasOverlap = true; + } + } + this._sourceTilesOverlap[sourceCache.id] = hasOverlap; + } + + _setupProxiedCoordsForImageSource(sourceCache: SourceCache, sourceCoords: Array, previousProxyToSource: {[number]: {[string]: Array}}) { + if (!sourceCache.getSource().loaded()) return; + + const coords = this.proxiedCoords[sourceCache.id] = []; + const proxys = this.proxyCoords; + const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource); + + const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div(1 << imageSource.tileID.z); + const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce((acc, coord) => { + acc.min.x = Math.min(acc.min.x, coord.x - anchor.x); + acc.min.y = Math.min(acc.min.y, coord.y - anchor.y); + acc.max.x = Math.max(acc.max.x, coord.x - anchor.x); + acc.max.y = Math.max(acc.max.y, coord.y - anchor.y); + return acc; + }, {min: new Point(Number.MAX_VALUE, Number.MAX_VALUE), max: new Point(-Number.MAX_VALUE, -Number.MAX_VALUE)}); + + // Fast conservative check using aabb: content outside proxy tile gets clipped out by on render, anyway. + const tileOutsideImage = (tileID, imageTileID) => { + const x = tileID.wrap + tileID.canonical.x / (1 << tileID.canonical.z); + const y = tileID.canonical.y / (1 << tileID.canonical.z); + const d = EXTENT / (1 << tileID.canonical.z); + + const ix = imageTileID.wrap + imageTileID.canonical.x / (1 << imageTileID.canonical.z); + const iy = imageTileID.canonical.y / (1 << imageTileID.canonical.z); + + return x + d < ix + aabb.min.x || x > ix + aabb.max.x || y + d < iy + aabb.min.y || y > iy + aabb.max.y; + }; + + for (let i = 0; i < proxys.length; i++) { + const proxyTileID = proxys[i]; + for (let j = 0; j < sourceCoords.length; j++) { + const tile = sourceCache.getTile(sourceCoords[j]); + if (!tile || !tile.hasData()) continue; + + // Setup proxied -> proxy mapping only if image on given tile wrap intersects the proxy tile. + if (tileOutsideImage(proxyTileID, tile.tileID)) continue; + + const id = this._createProxiedId(proxyTileID, tile, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]); + const array = this.proxyToSource[proxyTileID.key][sourceCache.id]; + if (!array) { + this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; + } else { + array.push(id); + } + coords.push(id); + } + } + } + + // recycle is previous pass content that likely contains proxied ID combining proxy and source tile. + _createProxiedId(proxyTileID: OverscaledTileID, tile: Tile, recycle: Array): ProxiedTileID { + let matrix = this.orthoMatrix; + if (recycle) { + const recycled = recycle.find(proxied => (proxied.key === tile.tileID.key)); + if (recycled) return recycled; + } + if (tile.tileID.key !== proxyTileID.key) { + const scale = proxyTileID.canonical.z - tile.tileID.canonical.z; + matrix = mat4.create(); + let size, xOffset, yOffset; + const wrap = (tile.tileID.wrap - proxyTileID.wrap) << proxyTileID.overscaledZ; + if (scale > 0) { + size = EXTENT >> scale; + xOffset = size * ((tile.tileID.canonical.x << scale) - proxyTileID.canonical.x + wrap); + yOffset = size * ((tile.tileID.canonical.y << scale) - proxyTileID.canonical.y); + } else { + size = EXTENT << -scale; + xOffset = EXTENT * (tile.tileID.canonical.x - ((proxyTileID.canonical.x + wrap) << -scale)); + yOffset = EXTENT * (tile.tileID.canonical.y - (proxyTileID.canonical.y << -scale)); + } + mat4.ortho(matrix, 0, size, 0, size, 0, 1); + mat4.translate(matrix, matrix, [xOffset, yOffset, 0]); + } + return new ProxiedTileID(tile.tileID, proxyTileID.key, matrix); + } + + // A variant of SourceCache.findLoadedParent that considers only visible + // tiles (and doesn't check SourceCache._cache). Another difference is in + // caching "not found" results along the lookup, to leave the lookup early. + // Not found is cached by this._findCoveringTileCache[key] = null; + _findTileCoveringTileID(tileID: OverscaledTileID, sourceCache: SourceCache): ?Tile { + let tile = sourceCache.getTile(tileID); + if (tile && tile.hasData()) return tile; + + const lookup = this._findCoveringTileCache[sourceCache.id]; + const key = lookup[tileID.key]; + tile = key ? sourceCache.getTileByID(key) : null; + if ((tile && tile.hasData()) || key === null) return tile; + + assert(!key || tile); + + let sourceTileID = tile ? tile.tileID : tileID; + let z = sourceTileID.overscaledZ; + const minzoom = sourceCache.getSource().minzoom; + const path = []; + if (!key) { + const maxzoom = sourceCache.getSource().maxzoom; + if (tileID.canonical.z >= maxzoom) { + const downscale = tileID.canonical.z - maxzoom; + if (sourceCache.getSource().reparseOverscaled) { + z = Math.max(tileID.canonical.z + 2, sourceCache.transform.tileZoom); + sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom, + tileID.canonical.x >> downscale, tileID.canonical.y >> downscale); + } else if (downscale !== 0) { + z = maxzoom; + sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom, + tileID.canonical.x >> downscale, tileID.canonical.y >> downscale); + } + } + if (sourceTileID.key !== tileID.key) { + path.push(sourceTileID.key); + tile = sourceCache.getTile(sourceTileID); + } + } + + const pathToLookup = (key) => { + path.forEach(id => { lookup[id] = key; }); + path.length = 0; + }; + + for (z = z - 1; z >= minzoom && !(tile && tile.hasData()); z--) { + if (tile) { + pathToLookup(tile.tileID.key); // Store lookup to parents not loaded (yet). + } + const id = sourceTileID.calculateScaledKey(z); + tile = sourceCache.getTileByID(id); + if (tile && tile.hasData()) break; + const key = lookup[id]; + if (key === null) { + break; // There's no tile loaded and no point searching further. + } else if (key !== undefined) { + tile = sourceCache.getTileByID(key); + assert(tile); + continue; + } + path.push(id); + } + + pathToLookup(tile ? tile.tileID.key : null); + return tile && tile.hasData() ? tile : null; + } + + findDEMTileFor(tileID: OverscaledTileID): ?Tile { + return this.enabled ? this._findTileCoveringTileID(tileID, this.sourceCache) : null; + } + + /* * Bookkeeping if something gets rendered to the tile. */ - prepareDrawTile() { - this.renderedToTile = true; - } + prepareDrawTile() { + this.renderedToTile = true; + } - _clearRenderCacheForTile(source: string, coord: OverscaledTileID) { - let sourceTiles = this._tilesDirty[source]; - if (!sourceTiles) sourceTiles = this._tilesDirty[source] = {}; - sourceTiles[coord.key] = true; - } + _clearRenderCacheForTile(source: string, coord: OverscaledTileID) { + let sourceTiles = this._tilesDirty[source]; + if (!sourceTiles) sourceTiles = this._tilesDirty[source] = {}; + sourceTiles[coord.key] = true; + } - /* + /* * Lazily instantiate the wireframe index buffer and segment vector so that we don't * allocate the geometry for rendering a debug wireframe until it's needed. */ - getWirefameBuffer(): [IndexBuffer, SegmentVector] { - if (!this.wireframeSegments) { - const wireframeGridIndices = createWireframeGrid(GRID_DIM + 1); - this.wireframeIndexBuffer = this.painter.context.createIndexBuffer(wireframeGridIndices); - this.wireframeSegments = SegmentVector.simpleSegment(0, 0, this.gridBuffer.length, wireframeGridIndices.length); - } - return [this.wireframeIndexBuffer, this.wireframeSegments]; - } + getWirefameBuffer(): [IndexBuffer, SegmentVector] { + if (!this.wireframeSegments) { + const wireframeGridIndices = createWireframeGrid(GRID_DIM + 1); + this.wireframeIndexBuffer = this.painter.context.createIndexBuffer(wireframeGridIndices); + this.wireframeSegments = SegmentVector.simpleSegment(0, 0, this.gridBuffer.length, wireframeGridIndices.length); + } + return [this.wireframeIndexBuffer, this.wireframeSegments]; + } + } function sortByDistanceToCamera(tileIDs, painter) { From 665ef89b4a04cb40754b69f600bf78ba7a3f25cb Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 15:31:55 +0200 Subject: [PATCH 54/72] fix formatting for style.js --- src/style/style.js | 3697 ++++++++++++++++++++------------------------ 1 file changed, 1664 insertions(+), 2033 deletions(-) diff --git a/src/style/style.js b/src/style/style.js index 9425dafd9b6..44900f06867 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -130,566 +130,503 @@ const drapedLayers = {'fill': true, 'line': true, 'background': true, "hillshade /** * @private */ -class Style - extends Evented { - map: Map; - stylesheet: StyleSpecification; - dispatcher: Dispatcher; - imageManager: ImageManager; - glyphManager: GlyphManager; - light: Light; - terrain: ?Terrain; - fog: ?Fog; - - _request: ?Cancelable; - _spriteRequest: ?Cancelable; - _layers: { [_: string]: StyleLayer }; - _num3DLayers: number; - _numSymbolLayers: number; - _numCircleLayers: number; - _serializedLayers: { [_: string]: Object }; - _order: Array; - _drapedFirstOrder: Array; - _sourceCaches: { [_: string]: SourceCache }; - _otherSourceCaches: { [_: string]: SourceCache }; - _symbolSourceCaches: { [_: string]: SourceCache }; - _loaded: boolean; - _rtlTextPluginCallback: Function; - _changed: boolean; - _updatedSources: { [_: string]: 'clear' | 'reload' }; - _updatedLayers: { [_: string]: true }; - _removedLayers: { [_: string]: StyleLayer }; - _changedImages: { [_: string]: true }; - _updatedPaintProps: { [layer: string]: true }; - _layerOrderChanged: boolean; - _availableImages: Array; - _markersNeedUpdate: boolean; - - crossTileSymbolIndex: CrossTileSymbolIndex; - pauseablePlacement: PauseablePlacement; - placement: Placement; - z: number; - - // exposed to allow stubbing by unit tests - static getSourceType: typeof getSourceType; - static setSourceType: typeof setSourceType; - static registerForPluginStateChange: typeof registerForPluginStateChange; - - constructor(map: Map, options: StyleOptions = {}) { - super(); - - this.map = map; - this.dispatcher = new Dispatcher(getWorkerPool(), this); - this.imageManager = new ImageManager(); - this.imageManager.setEventedParent(this); - this.glyphManager = new GlyphManager( - map._requestManager, - options.localFontFamily ? - LocalGlyphMode.all : - options.localIdeographFontFamily ? - LocalGlyphMode.ideographs : - LocalGlyphMode.none, - options.localFontFamily || options.localIdeographFontFamily, - ); - this.crossTileSymbolIndex = new CrossTileSymbolIndex(); - - this._layers = {}; - this._num3DLayers = 0; - this._numSymbolLayers = 0; - this._numCircleLayers = 0; - this._serializedLayers = {}; - this._sourceCaches = {}; - this._otherSourceCaches = {}; - this._symbolSourceCaches = {}; - this._loaded = false; - this._availableImages = []; - this._order = []; - this._drapedFirstOrder = []; - this._markersNeedUpdate = false; - - this._resetUpdates(); - - this.dispatcher.broadcast('setReferrer', getReferrer()); - - const self = this; - this._rtlTextPluginCallback = Style.registerForPluginStateChange( - event => { - const state = { - pluginStatus: event.pluginStatus, - pluginURL: event.pluginURL, - }; - self.dispatcher.broadcast( - 'syncRTLPluginState', - state, - (err, results) => { - triggerPluginCompletionEvent(err); - if (results) { - const allComplete = results.every(elem => elem); - if (allComplete) { - for (const id in self._sourceCaches) { - const sourceCache = self._sourceCaches[id]; - const sourceCacheType = sourceCache.getSource().type; - if ( - sourceCacheType === 'vector' || - sourceCacheType === 'geojson' - ) { - sourceCache.reload(); // Should be a no-op if the plugin loads before any tiles load - - } - } - } - } - }, - ); - }, - ); - - this.on( - 'data', - event => { - if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') { - return; - } - - const source = this.getSource(event.sourceId); - if (!source || !source.vectorLayerIds) { - return; - } - - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.source === source.id) { - this._validateLayer(layer); - } - } - }, - ); - } - - loadURL( - url: string, - options: { - validate?: boolean, - accessToken?: string, - } = {}, - ) { - this.fire(new Event('dataloading', {dataType: 'style'})); - - const validate = typeof options.validate === 'boolean' ? - options.validate : - !isMapboxURL(url); - - url = this.map._requestManager.normalizeStyleURL(url, options.accessToken); - const request = this.map._requestManager.transformRequest( - url, - ResourceType.Style, - ); - this._request = getJSON( - request, - (error: ?Error, json: ?Object) => { - this._request = null; - if (error) { - this.fire(new ErrorEvent(error)); - } else if (json) { - this._load(json, validate); - } - }, - ); - } - - loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}) { - this.fire(new Event('dataloading', {dataType: 'style'})); - - this._request = browser.frame( - () => { - this._request = null; - this._load(json, options.validate !== false); - }, - ); - } - - loadEmpty() { - this.fire(new Event('dataloading', {dataType: 'style'})); - this._load(empty, false); - } - - _updateLayerCount(layer: StyleLayer, add: boolean) { - // Typed layer bookkeeping - const count = add ? 1 : -1; - if (layer.is3D()) { - this._num3DLayers += count; - } - if (layer.type === 'circle') { - this._numCircleLayers += count; - } - if (layer.type === 'symbol') { - this._numSymbolLayers += count; - } - } - - _load(json: StyleSpecification, validate: boolean) { - if (validate && emitValidationErrors(this, validateStyle(json))) { - return; - } - - this._loaded = true; - this.stylesheet = clone(json); - this._updateMapProjection(); - - for (const id in json.sources) { - this.addSource(id, json.sources[id], {validate: false}); - } - this._changed = false; // avoid triggering redundant style update after adding initial sources - if (json.sprite) { - this._loadSprite(json.sprite); - } else { - this.imageManager.setLoaded(true); - this.dispatcher.broadcast('spriteLoaded', true); - } - - this.glyphManager.setURL(json.glyphs); - - const layers = deref(this.stylesheet.layers); - - this._order = layers.map(layer => layer.id); - - this._layers = {}; - this._serializedLayers = {}; - for (let layer of layers) { - layer = createStyleLayer(layer); - layer.setEventedParent(this, {layer: {id: layer.id}}); - this._layers[layer.id] = layer; - this._serializedLayers[layer.id] = layer.serialize(); - this._updateLayerCount(layer, true); - } - - this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); - - this.light = new Light(this.stylesheet.light); - if (this.stylesheet.terrain && !this.terrainSetForDrapingOnly()) { - this._createTerrain(this.stylesheet.terrain, DrapeRenderMode.elevated); - } - if (this.stylesheet.fog) { - this._createFog(this.stylesheet.fog); - } - this._updateDrapeFirstLayers(); - - this.fire(new Event('data', {dataType: 'style'})); - this.fire(new Event('style.load')); - } - - terrainSetForDrapingOnly(): boolean { - return ( - !!this.terrain && - this.terrain.drapeRenderMode === DrapeRenderMode.deferred - ); - } - - setProjection(projection?: ?ProjectionSpecification) { - if (projection) { - this.stylesheet.projection = projection; - } else { - delete this.stylesheet.projection; - } - this._updateMapProjection(); - } - - applyProjectionUpdate() { - if (!this._loaded) return; - this.dispatcher.broadcast( - 'setProjection', - this.map.transform.projectionOptions, - ); - - if (this.map.transform.projection.requiresDraping) { - const hasTerrain = this.getTerrain() || this.stylesheet.terrain; - if (!hasTerrain) { - this.setTerrainForDraping(); - } - } else if (this.terrainSetForDrapingOnly()) { - this.setTerrain(null); - } - } - - _updateMapProjection() { - if (!this.map._useExplicitProjection) { - // Update the visible projection if map's is null - this.map._prioritizeAndUpdateProjection(null, this.stylesheet.projection); - } else { - // Ensure that style is consistent with current projection on style load - this.applyProjectionUpdate(); - } - } - - _loadSprite(url: string) { - this._spriteRequest = loadSprite( - url, - this.map._requestManager, - (err, images) => { - this._spriteRequest = null; - if (err) { - this.fire(new ErrorEvent(err)); - } else if (images) { - for (const id in images) { - this.imageManager.addImage(id, images[id]); - } - } - - this.imageManager.setLoaded(true); - this._availableImages = this.imageManager.listImages(); - this.dispatcher.broadcast('setImages', this._availableImages); - this.dispatcher.broadcast('spriteLoaded', true); - this.fire(new Event('data', {dataType: 'style'})); - }, - ); - } - - _validateLayer(layer: StyleLayer) { - const source = this.getSource(layer.source); - if (!source) { - return; - } - - const sourceLayer = layer.sourceLayer; - if (!sourceLayer) { - return; - } - - if ( - source.type === 'geojson' || - ( - source.vectorLayerIds && - source.vectorLayerIds.indexOf(sourceLayer) === -1 - ) - ) { - this.fire( - new ErrorEvent(new Error( +class Style extends Evented { + map: Map; + stylesheet: StyleSpecification; + dispatcher: Dispatcher; + imageManager: ImageManager; + glyphManager: GlyphManager; + light: Light; + terrain: ?Terrain; + fog: ?Fog; + + _request: ?Cancelable; + _spriteRequest: ?Cancelable; + _layers: {[_: string]: StyleLayer}; + _num3DLayers: number; + _numSymbolLayers: number; + _numCircleLayers: number; + _serializedLayers: {[_: string]: Object}; + _order: Array; + _drapedFirstOrder: Array; + _sourceCaches: {[_: string]: SourceCache}; + _otherSourceCaches: {[_: string]: SourceCache}; + _symbolSourceCaches: {[_: string]: SourceCache}; + _loaded: boolean; + _rtlTextPluginCallback: Function; + _changed: boolean; + _updatedSources: {[_: string]: 'clear' | 'reload'}; + _updatedLayers: {[_: string]: true}; + _removedLayers: {[_: string]: StyleLayer}; + _changedImages: {[_: string]: true}; + _updatedPaintProps: {[layer: string]: true}; + _layerOrderChanged: boolean; + _availableImages: Array; + _markersNeedUpdate: boolean; + + crossTileSymbolIndex: CrossTileSymbolIndex; + pauseablePlacement: PauseablePlacement; + placement: Placement; + z: number; + + // exposed to allow stubbing by unit tests + static getSourceType: typeof getSourceType; + static setSourceType: typeof setSourceType; + static registerForPluginStateChange: typeof registerForPluginStateChange; + + constructor(map: Map, options: StyleOptions = {}) { + super(); + + this.map = map; + this.dispatcher = new Dispatcher(getWorkerPool(), this); + this.imageManager = new ImageManager(); + this.imageManager.setEventedParent(this); + this.glyphManager = new GlyphManager(map._requestManager, + options.localFontFamily ? + LocalGlyphMode.all : + (options.localIdeographFontFamily ? LocalGlyphMode.ideographs : LocalGlyphMode.none), + options.localFontFamily || options.localIdeographFontFamily); + this.crossTileSymbolIndex = new CrossTileSymbolIndex(); + + this._layers = {}; + this._num3DLayers = 0; + this._numSymbolLayers = 0; + this._numCircleLayers = 0; + this._serializedLayers = {}; + this._sourceCaches = {}; + this._otherSourceCaches = {}; + this._symbolSourceCaches = {}; + this._loaded = false; + this._availableImages = []; + this._order = []; + this._drapedFirstOrder = []; + this._markersNeedUpdate = false; + + this._resetUpdates(); + + this.dispatcher.broadcast('setReferrer', getReferrer()); + + const self = this; + this._rtlTextPluginCallback = Style.registerForPluginStateChange((event) => { + const state = { + pluginStatus: event.pluginStatus, + pluginURL: event.pluginURL + }; + self.dispatcher.broadcast('syncRTLPluginState', state, (err, results) => { + triggerPluginCompletionEvent(err); + if (results) { + const allComplete = results.every((elem) => elem); + if (allComplete) { + for (const id in self._sourceCaches) { + const sourceCache = self._sourceCaches[id]; + const sourceCacheType = sourceCache.getSource().type; + if (sourceCacheType === 'vector' || sourceCacheType === 'geojson') { + sourceCache.reload(); // Should be a no-op if the plugin loads before any tiles load + } + } + } + } + + }); + }); + + this.on('data', (event) => { + if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') { + return; + } + + const source = this.getSource(event.sourceId); + if (!source || !source.vectorLayerIds) { + return; + } + + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.source === source.id) { + this._validateLayer(layer); + } + } + }); + } + + loadURL(url: string, options: { + validate?: boolean, + accessToken?: string + } = {}) { + this.fire(new Event('dataloading', {dataType: 'style'})); + + const validate = typeof options.validate === 'boolean' ? + options.validate : !isMapboxURL(url); + + url = this.map._requestManager.normalizeStyleURL(url, options.accessToken); + const request = this.map._requestManager.transformRequest(url, ResourceType.Style); + this._request = getJSON(request, (error: ?Error, json: ?Object) => { + this._request = null; + if (error) { + this.fire(new ErrorEvent(error)); + } else if (json) { + this._load(json, validate); + } + }); + } + + loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}) { + this.fire(new Event('dataloading', {dataType: 'style'})); + + this._request = browser.frame(() => { + this._request = null; + this._load(json, options.validate !== false); + }); + } + + loadEmpty() { + this.fire(new Event('dataloading', {dataType: 'style'})); + this._load(empty, false); + } + + _updateLayerCount(layer: StyleLayer, add: boolean) { + // Typed layer bookkeeping + const count = add ? 1 : -1; + if (layer.is3D()) { + this._num3DLayers += count; + } + if (layer.type === 'circle') { + this._numCircleLayers += count; + } + if (layer.type === 'symbol') { + this._numSymbolLayers += count; + } + } + + _load(json: StyleSpecification, validate: boolean) { + if (validate && emitValidationErrors(this, validateStyle(json))) { + return; + } + + this._loaded = true; + this.stylesheet = clone(json); + this._updateMapProjection(); + + for (const id in json.sources) { + this.addSource(id, json.sources[id], {validate: false}); + } + this._changed = false; // avoid triggering redundant style update after adding initial sources + if (json.sprite) { + this._loadSprite(json.sprite); + } else { + this.imageManager.setLoaded(true); + this.dispatcher.broadcast('spriteLoaded', true); + } + + this.glyphManager.setURL(json.glyphs); + + const layers = deref(this.stylesheet.layers); + + this._order = layers.map((layer) => layer.id); + + this._layers = {}; + this._serializedLayers = {}; + for (let layer of layers) { + layer = createStyleLayer(layer); + layer.setEventedParent(this, {layer: {id: layer.id}}); + this._layers[layer.id] = layer; + this._serializedLayers[layer.id] = layer.serialize(); + this._updateLayerCount(layer, true); + } + + this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); + + this.light = new Light(this.stylesheet.light); + if (this.stylesheet.terrain && !this.terrainSetForDrapingOnly()) { + this._createTerrain(this.stylesheet.terrain, DrapeRenderMode.elevated); + } + if (this.stylesheet.fog) { + this._createFog(this.stylesheet.fog); + } + this._updateDrapeFirstLayers(); + + this.fire(new Event('data', {dataType: 'style'})); + this.fire(new Event('style.load')); + } + + terrainSetForDrapingOnly(): boolean { + return !!this.terrain && this.terrain.drapeRenderMode === DrapeRenderMode.deferred; + } + + setProjection(projection?: ?ProjectionSpecification) { + if (projection) { + this.stylesheet.projection = projection; + } else { + delete this.stylesheet.projection; + } + this._updateMapProjection(); + } + + applyProjectionUpdate() { + if (!this._loaded) return; + this.dispatcher.broadcast('setProjection', this.map.transform.projectionOptions); + + if (this.map.transform.projection.requiresDraping) { + const hasTerrain = this.getTerrain() || this.stylesheet.terrain; + if (!hasTerrain) { + this.setTerrainForDraping(); + } + } else if (this.terrainSetForDrapingOnly()) { + this.setTerrain(null); + } + } + + _updateMapProjection() { + if (!this.map._useExplicitProjection) { // Update the visible projection if map's is null + this.map._prioritizeAndUpdateProjection(null, this.stylesheet.projection); + } else { // Ensure that style is consistent with current projection on style load + this.applyProjectionUpdate(); + } + } + + _loadSprite(url: string) { + this._spriteRequest = loadSprite(url, this.map._requestManager, (err, images) => { + this._spriteRequest = null; + if (err) { + this.fire(new ErrorEvent(err)); + } else if (images) { + for (const id in images) { + this.imageManager.addImage(id, images[id]); + } + } + + this.imageManager.setLoaded(true); + this._availableImages = this.imageManager.listImages(); + this.dispatcher.broadcast('setImages', this._availableImages); + this.dispatcher.broadcast('spriteLoaded', true); + this.fire(new Event('data', {dataType: 'style'})); + }); + } + + _validateLayer(layer: StyleLayer) { + const source = this.getSource(layer.source); + if (!source) { + return; + } + + const sourceLayer = layer.sourceLayer; + if (!sourceLayer) { + return; + } + + if (source.type === 'geojson' || (source.vectorLayerIds && source.vectorLayerIds.indexOf(sourceLayer) === -1)) { + this.fire(new ErrorEvent(new Error( `Source layer "${sourceLayer}" ` + `does not exist on source "${source.id}" ` + - `as specified by style layer "${layer.id}"`, - ), - ), - ); - } - } - - loaded(): boolean { - if (!this._loaded) return false; - - if (Object.keys(this._updatedSources).length) return false; - - for (const id in this._sourceCaches) - if (!this._sourceCaches[id].loaded()) return false; - - if (!this.imageManager.isLoaded()) return false; - - return true; - } - - _serializeLayers(ids: Array): Array { - const serializedLayers = []; - for (const id of ids) { - const layer = this._layers[id]; - if (layer.type !== 'custom') { - serializedLayers.push(layer.serialize()); - } - } - return serializedLayers; - } - - hasTransitions(): boolean { - if (this.light && this.light.hasTransition()) { - return true; - } - - if (this.fog && this.fog.hasTransition()) { - return true; - } - - for (const id in this._sourceCaches) { - if (this._sourceCaches[id].hasTransition()) { - return true; - } - } - - for (const id in this._layers) { - if (this._layers[id].hasTransition()) { - return true; - } - } - - return false; - } - - get order(): Array { - if (this.map._optimizeForTerrain && this.terrain) { - assert(this._drapedFirstOrder.length === this._order.length); - return this._drapedFirstOrder; - } - return this._order; - } - - isLayerDraped(layer: StyleLayer): boolean { - if (!this.terrain) return false; - // $FlowFixMe[prop-missing] - // $FlowFixMe[incompatible-use] - if (typeof layer.isLayerDraped === 'function') return layer.isLayerDraped(); - return drapedLayers[layer.type]; - } - - _checkLoaded() { - if (!this._loaded) { - throw new Error('Style is not done loading'); - } - } - - /** + `as specified by style layer "${layer.id}"` + ))); + } + } + + loaded(): boolean { + if (!this._loaded) + return false; + + if (Object.keys(this._updatedSources).length) + return false; + + for (const id in this._sourceCaches) + if (!this._sourceCaches[id].loaded()) + return false; + + if (!this.imageManager.isLoaded()) + return false; + + return true; + } + + _serializeLayers(ids: Array): Array { + const serializedLayers = []; + for (const id of ids) { + const layer = this._layers[id]; + if (layer.type !== 'custom') { + serializedLayers.push(layer.serialize()); + } + } + return serializedLayers; + } + + hasTransitions(): boolean { + if (this.light && this.light.hasTransition()) { + return true; + } + + if (this.fog && this.fog.hasTransition()) { + return true; + } + + for (const id in this._sourceCaches) { + if (this._sourceCaches[id].hasTransition()) { + return true; + } + } + + for (const id in this._layers) { + if (this._layers[id].hasTransition()) { + return true; + } + } + + return false; + } + + get order(): Array { + if (this.map._optimizeForTerrain && this.terrain) { + assert(this._drapedFirstOrder.length === this._order.length); + return this._drapedFirstOrder; + } + return this._order; + } + + isLayerDraped(layer: StyleLayer): boolean { + if (!this.terrain) return false; + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-use] + if (typeof layer.isLayerDraped === 'function') return layer.isLayerDraped(); + return drapedLayers[layer.type]; + } + + _checkLoaded() { + if (!this._loaded) { + throw new Error('Style is not done loading'); + } + } + + /** * Apply queued style updates in a batch and recalculate zoom-dependent paint properties. * @private */ - update(parameters: EvaluationParameters) { - if (!this._loaded) { - return; - } - - const changed = this._changed; - if (this._changed) { - const updatedIds = Object.keys(this._updatedLayers); - const removedIds = Object.keys(this._removedLayers); - - if (updatedIds.length || removedIds.length) { - this._updateWorkerLayers(updatedIds, removedIds); - } - for (const id in this._updatedSources) { - const action = this._updatedSources[id]; - assert(action === 'reload' || action === 'clear'); - if (action === 'reload') { - this._reloadSource(id); - } else if (action === 'clear') { - this._clearSource(id); - } - } - - this._updateTilesForChangedImages(); - - for (const id in this._updatedPaintProps) { - this._layers[id].updateTransitions(parameters); - } - - this.light.updateTransitions(parameters); - if (this.fog) { - this.fog.updateTransitions(parameters); - } - - this._resetUpdates(); - } - - const sourcesUsedBefore = {}; - - for (const sourceId in this._sourceCaches) { - const sourceCache = this._sourceCaches[sourceId]; - sourcesUsedBefore[sourceId] = sourceCache.used; - sourceCache.used = false; - } - - for (const layerId of this._order) { - const layer = this._layers[layerId]; - - layer.recalculate(parameters, this._availableImages); - if (!layer.isHidden(parameters.zoom)) { - const sourceCache = this._getLayerSourceCache(layer); - if (sourceCache) sourceCache.used = true; - } - - const painter = this.map.painter; - if (painter) { - const programIds = layer.getProgramIds(); - if (!programIds) continue; - - const programConfiguration = layer.getProgramConfiguration( - parameters.zoom, - ); - - for (const programId of programIds) { - painter.useProgram(programId, programConfiguration); - } - } - } - - for (const sourceId in sourcesUsedBefore) { - const sourceCache = this._sourceCaches[sourceId]; - if (sourcesUsedBefore[sourceId] !== sourceCache.used) { - sourceCache.getSource().fire( - new Event( - 'data', - { - sourceDataType: 'visibility', - dataType: 'source', - sourceId: sourceCache.getSource().id, - }, - ), - ); - } - } - - this.light.recalculate(parameters); - if (this.terrain) { - this.terrain.recalculate(parameters); - } - if (this.fog) { - this.fog.recalculate(parameters); - } - this.z = parameters.zoom; - - if (this._markersNeedUpdate) { - this._updateMarkersOpacity(); - this._markersNeedUpdate = false; - } - - if (changed) { - this.fire(new Event('data', {dataType: 'style'})); - } - } - - /* + update(parameters: EvaluationParameters) { + if (!this._loaded) { + return; + } + + const changed = this._changed; + if (this._changed) { + const updatedIds = Object.keys(this._updatedLayers); + const removedIds = Object.keys(this._removedLayers); + + if (updatedIds.length || removedIds.length) { + this._updateWorkerLayers(updatedIds, removedIds); + } + for (const id in this._updatedSources) { + const action = this._updatedSources[id]; + assert(action === 'reload' || action === 'clear'); + if (action === 'reload') { + this._reloadSource(id); + } else if (action === 'clear') { + this._clearSource(id); + } + } + + this._updateTilesForChangedImages(); + + for (const id in this._updatedPaintProps) { + this._layers[id].updateTransitions(parameters); + } + + this.light.updateTransitions(parameters); + if (this.fog) { + this.fog.updateTransitions(parameters); + } + + this._resetUpdates(); + } + + const sourcesUsedBefore = {}; + + for (const sourceId in this._sourceCaches) { + const sourceCache = this._sourceCaches[sourceId]; + sourcesUsedBefore[sourceId] = sourceCache.used; + sourceCache.used = false; + } + + for (const layerId of this._order) { + const layer = this._layers[layerId]; + + layer.recalculate(parameters, this._availableImages); + if (!layer.isHidden(parameters.zoom)) { + const sourceCache = this._getLayerSourceCache(layer); + if (sourceCache) sourceCache.used = true; + } + + const painter = this.map.painter; + if (painter) { + const programIds = layer.getProgramIds(); + if (!programIds) continue; + + const programConfiguration = layer.getProgramConfiguration(parameters.zoom); + + for (const programId of programIds) { + painter.useProgram(programId, programConfiguration); + } + } + } + + for (const sourceId in sourcesUsedBefore) { + const sourceCache = this._sourceCaches[sourceId]; + if (sourcesUsedBefore[sourceId] !== sourceCache.used) { + sourceCache.getSource().fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId: sourceCache.getSource().id})); + } + } + + this.light.recalculate(parameters); + if (this.terrain) { + this.terrain.recalculate(parameters); + } + if (this.fog) { + this.fog.recalculate(parameters); + } + this.z = parameters.zoom; + + if (this._markersNeedUpdate) { + this._updateMarkersOpacity(); + this._markersNeedUpdate = false; + } + + if (changed) { + this.fire(new Event('data', {dataType: 'style'})); + } + } + + /* * Apply any queued image changes. */ - _updateTilesForChangedImages() { - const changedImages = Object.keys(this._changedImages); - if (changedImages.length) { - for (const name in this._sourceCaches) { - this._sourceCaches[name].reloadTilesForDependencies( - ['icons', 'patterns'], - changedImages, - ); - } - this._changedImages = {}; - } - } - - _updateWorkerLayers(updatedIds: Array, removedIds: Array) { - this.dispatcher.broadcast( - 'updateLayers', - { - layers: this._serializeLayers(updatedIds), - removedIds, - }, - ); - } - - _resetUpdates() { - this._changed = false; - - this._updatedLayers = {}; - this._removedLayers = {}; - - this._updatedSources = {}; - this._updatedPaintProps = {}; - - this._changedImages = {}; - } - - /** + _updateTilesForChangedImages() { + const changedImages = Object.keys(this._changedImages); + if (changedImages.length) { + for (const name in this._sourceCaches) { + this._sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], changedImages); + } + this._changedImages = {}; + } + } + + _updateWorkerLayers(updatedIds: Array, removedIds: Array) { + this.dispatcher.broadcast('updateLayers', { + layers: this._serializeLayers(updatedIds), + removedIds + }); + } + + _resetUpdates() { + this._changed = false; + + this._updatedLayers = {}; + this._removedLayers = {}; + + this._updatedSources = {}; + this._updatedPaintProps = {}; + + this._changedImages = {}; + } + + /** * Update this style's state to match the given style JSON, performing only * the necessary mutations. * @@ -699,252 +636,202 @@ class Style * @returns {boolean} true if any changes were made; false otherwise * @private */ - setState(nextState: StyleSpecification): boolean { - this._checkLoaded(); - - if (emitValidationErrors(this, validateStyle(nextState))) return false; - - nextState = clone(nextState); - nextState.layers = deref(nextState.layers); - - const changes = diffStyles(this.serialize(), nextState).filter( - op => !(op.command in ignoredDiffOperations), - ); - - if (changes.length === 0) { - return false; - } - - const unimplementedOps = changes.filter( - op => !(op.command in supportedDiffOperations), - ); - if (unimplementedOps.length > 0) { - throw ( - new Error(`Unimplemented: ${unimplementedOps.map(op => op.command).join( - ', ', - )}.`,) - ); - } - - changes.forEach( - op => { - if (op.command === 'setTransition' || op.command === 'setProjection') { - // `transition` and `projection` are always read directly from - // `this.stylesheet`, which we update below - return; - } - (this: any)[op.command].apply(this, op.args); - }, - ); - - this.stylesheet = nextState; - this._updateMapProjection(); - - return true; - } - - addImage(id: string, image: StyleImage): this { - if (this.getImage(id)) { - return this.fire( - new ErrorEvent(new Error('An image with this name already exists.')), - ); - } - this.imageManager.addImage(id, image); - this._afterImageUpdated(id); - return this; - } - - updateImage(id: string, image: StyleImage) { - this.imageManager.updateImage(id, image); - } - - getImage(id: string): ?StyleImage { - return this.imageManager.getImage(id); - } - - removeImage(id: string): this { - if (!this.getImage(id)) { - return this.fire( - new ErrorEvent(new Error('No image with this name exists.')), - ); - } - this.imageManager.removeImage(id); - this._afterImageUpdated(id); - return this; - } - - _afterImageUpdated(id: string) { - this._availableImages = this.imageManager.listImages(); - this._changedImages[id] = true; - this._changed = true; - this.dispatcher.broadcast('setImages', this._availableImages); - this.fire(new Event('data', {dataType: 'style'})); - } - - listImages(): Array { - this._checkLoaded(); - return this._availableImages.slice(); - } - - addSource( - id: string, - source: SourceSpecification, - options: StyleSetterOptions = {}, - ) { - this._checkLoaded(); - - if (this.getSource(id) !== undefined) { - throw new Error('There is already a source with this ID'); - } - - if (!source.type) { - throw ( - new Error(`The type property must be defined, but only the following properties were given: ${Object.keys( - source, - ).join(', ')}.`,) - ); - } - - const builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; - const shouldValidate = builtIns.indexOf(source.type) >= 0; - if ( - shouldValidate && - this._validate(validateSource, `sources.${id}`, source, null, options) - ) - return; - - if (this.map && this.map._collectResourceTiming) - (source: any).collectResourceTiming = true; - - const sourceInstance = createSource(id, source, this.dispatcher, this); - - sourceInstance.setEventedParent( - this, - () => ({ - isSourceLoaded: this._isSourceCacheLoaded(id), - source: sourceInstance.serialize(), - sourceId: id, - }), - ); - - const addSourceCache = (onlySymbols => { - const sourceCacheId = (onlySymbols ? 'symbol:' : 'other:') + id; - const sourceCache = this._sourceCaches[sourceCacheId] = new SourceCache( - sourceCacheId, - sourceInstance, - onlySymbols, - ); - (onlySymbols ? this._symbolSourceCaches : this._otherSourceCaches)[id] = sourceCache; - sourceCache.style = this; - - sourceCache.onAdd(this.map); - }); - - addSourceCache(false); - if (source.type === 'vector' || source.type === 'geojson') { - addSourceCache(true); - } - - if (sourceInstance.onAdd) sourceInstance.onAdd(this.map); - - this._changed = true; - } - - /** + setState(nextState: StyleSpecification): boolean { + this._checkLoaded(); + + if (emitValidationErrors(this, validateStyle(nextState))) return false; + + nextState = clone(nextState); + nextState.layers = deref(nextState.layers); + + const changes = diffStyles(this.serialize(), nextState) + .filter(op => !(op.command in ignoredDiffOperations)); + + if (changes.length === 0) { + return false; + } + + const unimplementedOps = changes.filter(op => !(op.command in supportedDiffOperations)); + if (unimplementedOps.length > 0) { + throw new Error(`Unimplemented: ${unimplementedOps.map(op => op.command).join(', ')}.`); + } + + changes.forEach((op) => { + if (op.command === 'setTransition' || op.command === 'setProjection') { + // `transition` and `projection` are always read directly from + // `this.stylesheet`, which we update below + return; + } + (this: any)[op.command].apply(this, op.args); + }); + + this.stylesheet = nextState; + this._updateMapProjection(); + + return true; + } + + addImage(id: string, image: StyleImage): this { + if (this.getImage(id)) { + return this.fire(new ErrorEvent(new Error('An image with this name already exists.'))); + } + this.imageManager.addImage(id, image); + this._afterImageUpdated(id); + return this; + } + + updateImage(id: string, image: StyleImage) { + this.imageManager.updateImage(id, image); + } + + getImage(id: string): ?StyleImage { + return this.imageManager.getImage(id); + } + + removeImage(id: string): this { + if (!this.getImage(id)) { + return this.fire(new ErrorEvent(new Error('No image with this name exists.'))); + } + this.imageManager.removeImage(id); + this._afterImageUpdated(id); + return this; + } + + _afterImageUpdated(id: string) { + this._availableImages = this.imageManager.listImages(); + this._changedImages[id] = true; + this._changed = true; + this.dispatcher.broadcast('setImages', this._availableImages); + this.fire(new Event('data', {dataType: 'style'})); + } + + listImages(): Array { + this._checkLoaded(); + return this._availableImages.slice(); + } + + addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + if (this.getSource(id) !== undefined) { + throw new Error('There is already a source with this ID'); + } + + if (!source.type) { + throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(source).join(', ')}.`); + } + + const builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; + const shouldValidate = builtIns.indexOf(source.type) >= 0; + if (shouldValidate && this._validate(validateSource, `sources.${id}`, source, null, options)) return; + + if (this.map && this.map._collectResourceTiming) (source: any).collectResourceTiming = true; + + const sourceInstance = createSource(id, source, this.dispatcher, this); + + sourceInstance.setEventedParent(this, () => ({ + isSourceLoaded: this._isSourceCacheLoaded(id), + source: sourceInstance.serialize(), + sourceId: id + })); + + const addSourceCache = (onlySymbols) => { + const sourceCacheId = (onlySymbols ? 'symbol:' : 'other:') + id; + const sourceCache = this._sourceCaches[sourceCacheId] = new SourceCache(sourceCacheId, sourceInstance, onlySymbols); + (onlySymbols ? this._symbolSourceCaches : this._otherSourceCaches)[id] = sourceCache; + sourceCache.style = this; + + sourceCache.onAdd(this.map); + }; + + addSourceCache(false); + if (source.type === 'vector' || source.type === 'geojson') { + addSourceCache(true); + } + + if (sourceInstance.onAdd) sourceInstance.onAdd(this.map); + + this._changed = true; + } + + /** * Remove a source from this stylesheet, given its ID. * @param {string} id ID of the source to remove. * @throws {Error} If no source is found with the given ID. * @returns {Map} The {@link Map} object. */ - removeSource(id: string): this { - this._checkLoaded(); - - const source = this.getSource(id); - if (!source) { - throw new Error('There is no source with this ID'); - } - for (const layerId in this._layers) { - if (this._layers[layerId].source === id) { - return this.fire( - new ErrorEvent( - new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`,), - ), - ); - } - } - if (this.terrain && this.terrain.get().source === id) { - return this.fire( - new ErrorEvent( - new Error(`Source "${id}" cannot be removed while terrain is using it.`,), - ), - ); - } - - const sourceCaches = this._getSourceCaches(id); - for (const sourceCache of sourceCaches) { - delete this._sourceCaches[sourceCache.id]; - delete this._updatedSources[sourceCache.id]; - sourceCache.fire( - new Event( - 'data', - { - sourceDataType: 'metadata', - dataType: 'source', - sourceId: sourceCache.getSource().id, - }, - ), - ); - sourceCache.setEventedParent(null); - sourceCache.clearTiles(); - } - delete this._otherSourceCaches[id]; - delete this._symbolSourceCaches[id]; - - source.setEventedParent(null); - if (source.onRemove) { - source.onRemove(this.map); - } - this._changed = true; - return this; - } - - /** + removeSource(id: string): this { + this._checkLoaded(); + + const source = this.getSource(id); + if (!source) { + throw new Error('There is no source with this ID'); + } + for (const layerId in this._layers) { + if (this._layers[layerId].source === id) { + return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); + } + } + if (this.terrain && this.terrain.get().source === id) { + return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while terrain is using it.`))); + } + + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + delete this._sourceCaches[sourceCache.id]; + delete this._updatedSources[sourceCache.id]; + sourceCache.fire(new Event('data', {sourceDataType: 'metadata', dataType:'source', sourceId: sourceCache.getSource().id})); + sourceCache.setEventedParent(null); + sourceCache.clearTiles(); + } + delete this._otherSourceCaches[id]; + delete this._symbolSourceCaches[id]; + + source.setEventedParent(null); + if (source.onRemove) { + source.onRemove(this.map); + } + this._changed = true; + return this; + } + + /** * Set the data of a GeoJSON source, given its ID. * @param {string} id ID of the source. * @param {GeoJSON|string} data GeoJSON source. */ - setGeoJSONSourceData(id: string, data: GeoJSON | string) { - this._checkLoaded(); + setGeoJSONSourceData(id: string, data: GeoJSON | string) { + this._checkLoaded(); - assert(this.getSource(id) !== undefined, 'There is no source with this ID'); - const geojsonSource: GeoJSONSource = (this.getSource(id): any); - assert(geojsonSource.type === 'geojson'); + assert(this.getSource(id) !== undefined, 'There is no source with this ID'); + const geojsonSource: GeoJSONSource = (this.getSource(id): any); + assert(geojsonSource.type === 'geojson'); - geojsonSource.setData(data); - this._changed = true; - } + geojsonSource.setData(data); + this._changed = true; + } - /** + /** * Get a source by ID. * @param {string} id ID of the desired source. * @returns {?Source} The source object. */ - getSource(id: string): ?Source { - const sourceCache = this._getSourceCache(id); - return sourceCache && sourceCache.getSource(); - } - - _getSources(): Array { - const sources = []; - for (const id in this._otherSourceCaches) { - const sourceCache = this._getSourceCache(id); - if (sourceCache) sources.push(sourceCache.getSource()); - } - - return sources; - } - - /** + getSource(id: string): ?Source { + const sourceCache = this._getSourceCache(id); + return sourceCache && sourceCache.getSource(); + } + + _getSources(): Source[] { + const sources = []; + for (const id in this._otherSourceCaches) { + const sourceCache = this._getSourceCache(id); + if (sourceCache) sources.push(sourceCache.getSource()); + } + + return sources; + } + + /** * Add a layer to the map style. The layer will be inserted before the layer with * ID `before`, or appended if `before` is omitted. * @param {Object | CustomLayerInterface} layerObject The style layer to add. @@ -952,146 +839,116 @@ class Style * @param {Object} options Style setter options. * @returns {Map} The {@link Map} object. */ - addLayer( - layerObject: LayerSpecification | CustomLayerInterface, - before?: string, - options: StyleSetterOptions = {}, - ) { - this._checkLoaded(); - - const id = layerObject.id; - - if (this.getLayer(id)) { - this.fire( - new ErrorEvent( - new Error(`Layer with id "${id}" already exists on this map`), - ), - ); - return; - } - - let layer; - if (layerObject.type === 'custom') { - if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) - return; - - layer = createStyleLayer(layerObject); - } else { - if (typeof layerObject.source === 'object') { - this.addSource(id, layerObject.source); - layerObject = clone(layerObject); - layerObject = (extend(layerObject, {source: id}): any); - } - - // this layer is not in the style.layers array, so we pass an impossible array index - if ( - this._validate( - validateLayer, - `layers.${id}`, - layerObject, - {arrayIndex: -1}, - options, - ) - ) - return; - - layer = createStyleLayer(layerObject); - this._validateLayer(layer); - - layer.setEventedParent(this, {layer: {id}}); - this._serializedLayers[layer.id] = layer.serialize(); - this._updateLayerCount(layer, true); - } - - const index = before ? this._order.indexOf(before) : this._order.length; - if (before && index === -1) { - this.fire( - new ErrorEvent( - new Error(`Layer with id "${before}" does not exist on this map.`), - ), - ); - return; - } - - this._order.splice(index, 0, id); - this._layerOrderChanged = true; - - this._layers[id] = layer; - - const sourceCache = this._getLayerSourceCache(layer); - if ( - this._removedLayers[id] && layer.source && sourceCache && - layer.type !== 'custom' - ) { - // If, in the current batch, we have already removed this layer - // and we are now re-adding it with a different `type`, then we - // need to clear (rather than just reload) the underyling source's - // tiles. Otherwise, tiles marked 'reloading' will have buckets / - // buffers that are set up for the _previous_ version of this - // layer, causing, e.g.: - // https://github.com/mapbox/mapbox-gl-js/issues/3633 - const removed = this._removedLayers[id]; - delete this._removedLayers[id]; - if (removed.type !== layer.type) { - this._updatedSources[layer.source] = 'clear'; - } else { - this._updatedSources[layer.source] = 'reload'; - sourceCache.pause(); - } - } - this._updateLayer(layer); - - if (layer.onAdd) { - layer.onAdd(this.map); - } - - this._updateDrapeFirstLayers(); - } - - /** + addLayer(layerObject: LayerSpecification | CustomLayerInterface, before?: string, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + const id = layerObject.id; + + if (this.getLayer(id)) { + this.fire(new ErrorEvent(new Error(`Layer with id "${id}" already exists on this map`))); + return; + } + + let layer; + if (layerObject.type === 'custom') { + + if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return; + + layer = createStyleLayer(layerObject); + + } else { + if (typeof layerObject.source === 'object') { + this.addSource(id, layerObject.source); + layerObject = clone(layerObject); + layerObject = (extend(layerObject, {source: id}): any); + } + + // this layer is not in the style.layers array, so we pass an impossible array index + if (this._validate(validateLayer, + `layers.${id}`, layerObject, {arrayIndex: -1}, options)) return; + + layer = createStyleLayer(layerObject); + this._validateLayer(layer); + + layer.setEventedParent(this, {layer: {id}}); + this._serializedLayers[layer.id] = layer.serialize(); + this._updateLayerCount(layer, true); + } + + const index = before ? this._order.indexOf(before) : this._order.length; + if (before && index === -1) { + this.fire(new ErrorEvent(new Error(`Layer with id "${before}" does not exist on this map.`))); + return; + } + + this._order.splice(index, 0, id); + this._layerOrderChanged = true; + + this._layers[id] = layer; + + const sourceCache = this._getLayerSourceCache(layer); + if (this._removedLayers[id] && layer.source && sourceCache && layer.type !== 'custom') { + // If, in the current batch, we have already removed this layer + // and we are now re-adding it with a different `type`, then we + // need to clear (rather than just reload) the underyling source's + // tiles. Otherwise, tiles marked 'reloading' will have buckets / + // buffers that are set up for the _previous_ version of this + // layer, causing, e.g.: + // https://github.com/mapbox/mapbox-gl-js/issues/3633 + const removed = this._removedLayers[id]; + delete this._removedLayers[id]; + if (removed.type !== layer.type) { + this._updatedSources[layer.source] = 'clear'; + } else { + this._updatedSources[layer.source] = 'reload'; + sourceCache.pause(); + } + } + this._updateLayer(layer); + + if (layer.onAdd) { + layer.onAdd(this.map); + } + + this._updateDrapeFirstLayers(); + } + + /** * Moves a layer to a different z-position. The layer will be inserted before the layer with * ID `before`, or appended if `before` is omitted. * @param {string} id ID of the layer to move. * @param {string} [before] ID of an existing layer to insert before. */ - moveLayer(id: string, before?: string) { - this._checkLoaded(); - this._changed = true; - - const layer = this._layers[id]; - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`,), - ), - ); - return; - } - - if (id === before) { - return; - } - - const index = this._order.indexOf(id); - this._order.splice(index, 1); - - const newIndex = before ? this._order.indexOf(before) : this._order.length; - if (before && newIndex === -1) { - this.fire( - new ErrorEvent( - new Error(`Layer with id "${before}" does not exist on this map.`), - ), - ); - return; - } - this._order.splice(newIndex, 0, id); - - this._layerOrderChanged = true; - - this._updateDrapeFirstLayers(); - } - - /** + moveLayer(id: string, before?: string) { + this._checkLoaded(); + this._changed = true; + + const layer = this._layers[id]; + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`))); + return; + } + + if (id === before) { + return; + } + + const index = this._order.indexOf(id); + this._order.splice(index, 1); + + const newIndex = before ? this._order.indexOf(before) : this._order.length; + if (before && newIndex === -1) { + this.fire(new ErrorEvent(new Error(`Layer with id "${before}" does not exist on this map.`))); + return; + } + this._order.splice(newIndex, 0, id); + + this._layerOrderChanged = true; + + this._updateDrapeFirstLayers(); + } + + /** * Remove the layer with the given id from the style. * * If no such layer exists, an `error` event is fired. @@ -1099,1147 +956,921 @@ class Style * @param {string} id ID of the layer to remove. * @fires Map.event:error */ - removeLayer(id: string) { - this._checkLoaded(); - - const layer = this._layers[id]; - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${id}' does not exist in the map's style and cannot be removed.`,), - ), - ); - return; - } - - layer.setEventedParent(null); - - this._updateLayerCount(layer, false); - - const index = this._order.indexOf(id); - this._order.splice(index, 1); - - this._layerOrderChanged = true; - this._changed = true; - this._removedLayers[id] = layer; - delete this._layers[id]; - delete this._serializedLayers[id]; - delete this._updatedLayers[id]; - delete this._updatedPaintProps[id]; - - if (layer.onRemove) { - layer.onRemove(this.map); - } - - this._updateDrapeFirstLayers(); - } - - /** + removeLayer(id: string) { + this._checkLoaded(); + + const layer = this._layers[id]; + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be removed.`))); + return; + } + + layer.setEventedParent(null); + + this._updateLayerCount(layer, false); + + const index = this._order.indexOf(id); + this._order.splice(index, 1); + + this._layerOrderChanged = true; + this._changed = true; + this._removedLayers[id] = layer; + delete this._layers[id]; + delete this._serializedLayers[id]; + delete this._updatedLayers[id]; + delete this._updatedPaintProps[id]; + + if (layer.onRemove) { + layer.onRemove(this.map); + } + + this._updateDrapeFirstLayers(); + } + + /** * Return the style layer object with the given `id`. * * @param {string} id ID of the desired layer. * @returns {?StyleLayer} A layer, if one with the given `id` exists. */ - getLayer(id: string): ?StyleLayer { - return this._layers[id]; - } + getLayer(id: string): ?StyleLayer { + return this._layers[id]; + } - /** + /** * Checks if a specific layer is present within the style. * * @param {string} id ID of the desired layer. * @returns {boolean} A boolean specifying if the given layer is present. */ - hasLayer(id: string): boolean { - return id in this._layers; - } + hasLayer(id: string): boolean { + return id in this._layers; + } - /** + /** * Checks if a specific layer type is present within the style. * * @param {string} type Type of the desired layer. * @returns {boolean} A boolean specifying if the given layer type is present. */ - hasLayerType(type: string): boolean { - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.type === type) { - return true; - } - } - return false; - } - - setLayerZoomRange(layerId: string, minzoom: ?number, maxzoom: ?number) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${layerId}' does not exist in the map's style and cannot have zoom extent.`,), - ), - ); - return; - } - - if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; - - if (minzoom != null) { - layer.minzoom = minzoom; - } - if (maxzoom != null) { - layer.maxzoom = maxzoom; - } - this._updateLayer(layer); - } - - setFilter( - layerId: string, - filter: ?FilterSpecification, - options: StyleSetterOptions = {}, - ) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${layerId}' does not exist in the map's style and cannot be filtered.`,), - ), - ); - return; - } - - if (deepEqual(layer.filter, filter)) { - return; - } - - if (filter === null || filter === undefined) { - layer.filter = undefined; - this._updateLayer(layer); - return; - } - - if ( - this._validate( - validateFilter, - `layers.${layer.id}.filter`, - filter, - {layerType: layer.type}, - options, - ) - ) { - return; - } - - layer.filter = clone(filter); - this._updateLayer(layer); - } - - /** + hasLayerType(type: string): boolean { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === type) { + return true; + } + } + return false; + } + + setLayerZoomRange(layerId: string, minzoom: ?number, maxzoom: ?number) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot have zoom extent.`))); + return; + } + + if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; + + if (minzoom != null) { + layer.minzoom = minzoom; + } + if (maxzoom != null) { + layer.maxzoom = maxzoom; + } + this._updateLayer(layer); + } + + setFilter(layerId: string, filter: ?FilterSpecification, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be filtered.`))); + return; + } + + if (deepEqual(layer.filter, filter)) { + return; + } + + if (filter === null || filter === undefined) { + layer.filter = undefined; + this._updateLayer(layer); + return; + } + + if (this._validate(validateFilter, `layers.${layer.id}.filter`, filter, {layerType: layer.type}, options)) { + return; + } + + layer.filter = clone(filter); + this._updateLayer(layer); + } + + /** * Get a layer's filter object. * @param {string} layerId The layer to inspect. * @returns {*} The layer's filter, if any. */ - getFilter(layerId: string): ?FilterSpecification { - const layer = this.getLayer(layerId); - return layer && clone(layer.filter); - } - - setLayoutProperty( - layerId: string, - name: string, - value: any, - options: StyleSetterOptions = {}, - ) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`,), - ), - ); - return; - } - - if (deepEqual(layer.getLayoutProperty(name), value)) return; - - layer.setLayoutProperty(name, value, options); - this._updateLayer(layer); - } - - /** + getFilter(layerId: string): ?FilterSpecification { + const layer = this.getLayer(layerId); + return layer && clone(layer.filter); + } + + setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`))); + return; + } + + if (deepEqual(layer.getLayoutProperty(name), value)) return; + + layer.setLayoutProperty(name, value, options); + this._updateLayer(layer); + } + + /** * Get a layout property's value from a given layer. * @param {string} layerId The layer to inspect. * @param {string} name The name of the layout property. * @returns {*} The property value. */ - getLayoutProperty( - layerId: string, - name: string, - ): ?PropertyValueSpecification { - const layer = this.getLayer(layerId); - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${layerId}' does not exist in the map's style.`), - ), - ); - return; - } - - return layer.getLayoutProperty(name); - } - - setPaintProperty( - layerId: string, - name: string, - value: any, - options: StyleSetterOptions = {}, - ) { - this._checkLoaded(); - - const layer = this.getLayer(layerId); - if (!layer) { - this.fire( - new ErrorEvent( - new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`,), - ), - ); - return; - } - - if (deepEqual(layer.getPaintProperty(name), value)) return; - - const requiresRelayout = layer.setPaintProperty(name, value, options); - if (requiresRelayout) { - this._updateLayer(layer); - } - - this._changed = true; - this._updatedPaintProps[layerId] = true; - } - - getPaintProperty( - layerId: string, - name: string, - ): void | TransitionSpecification | PropertyValueSpecification { - const layer = this.getLayer(layerId); - return layer && layer.getPaintProperty(name); - } - - setFeatureState( - target: { source: string, sourceLayer?: string, id: string | number }, - state: Object, - ) { - this._checkLoaded(); - const sourceId = target.source; - const sourceLayer = target.sourceLayer; - const source = this.getSource(sourceId); - - if (!source) { - this.fire( - new ErrorEvent( - new Error(`The source '${sourceId}' does not exist in the map's style.`,), - ), - ); - return; - } - const sourceType = source.type; - if (sourceType === 'geojson' && sourceLayer) { - this.fire( - new ErrorEvent( - new Error(`GeoJSON sources cannot have a sourceLayer parameter.`), - ), - ); - return; - } - if (sourceType === 'vector' && !sourceLayer) { - this.fire( - new ErrorEvent( - new Error(`The sourceLayer parameter must be provided for vector source types.`,), - ), - ); - return; - } - if (target.id === undefined) { - this.fire( - new ErrorEvent(new Error(`The feature id parameter must be provided.`)), - ); - } - - const sourceCaches = this._getSourceCaches(sourceId); - for (const sourceCache of sourceCaches) { - sourceCache.setFeatureState(sourceLayer, target.id, state); - } - } - - removeFeatureState( - target: { source: string, sourceLayer?: string, id?: string | number }, - key?: string, - ) { - this._checkLoaded(); - const sourceId = target.source; - const source = this.getSource(sourceId); - - if (!source) { - this.fire( - new ErrorEvent( - new Error(`The source '${sourceId}' does not exist in the map's style.`,), - ), - ); - return; - } - - const sourceType = source.type; - const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; - - if (sourceType === 'vector' && !sourceLayer) { - this.fire( - new ErrorEvent( - new Error(`The sourceLayer parameter must be provided for vector source types.`,), - ), - ); - return; - } - - if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) { - this.fire( - new ErrorEvent( - new Error(`A feature id is required to remove its specific state property.`,), - ), - ); - return; - } - - const sourceCaches = this._getSourceCaches(sourceId); - for (const sourceCache of sourceCaches) { - sourceCache.removeFeatureState(sourceLayer, target.id, key); - } - } - - getFeatureState( - target: { source: string, sourceLayer?: string, id: string | number }, - ): ?FeatureStates { - this._checkLoaded(); - const sourceId = target.source; - const sourceLayer = target.sourceLayer; - const source = this.getSource(sourceId); - - if (!source) { - this.fire( - new ErrorEvent( - new Error(`The source '${sourceId}' does not exist in the map's style.`,), - ), - ); - return; - } - const sourceType = source.type; - if (sourceType === 'vector' && !sourceLayer) { - this.fire( - new ErrorEvent( - new Error(`The sourceLayer parameter must be provided for vector source types.`,), - ), - ); - return; - } - if (target.id === undefined) { - this.fire( - new ErrorEvent(new Error(`The feature id parameter must be provided.`)), - ); - } - - const sourceCaches = this._getSourceCaches(sourceId); - return sourceCaches[0].getFeatureState(sourceLayer, target.id); - } - - getTransition(): TransitionSpecification { - return extend( - {duration: 300, delay: 0}, - this.stylesheet && this.stylesheet.transition, - ); - } - - serialize(): StyleSpecification { - const sources = {}; - for (const cacheId in this._sourceCaches) { - const source = this._sourceCaches[cacheId].getSource(); - if (!sources[source.id]) { - sources[source.id] = source.serialize(); - } - } - - return filterObject( - { - version: this.stylesheet.version, - name: this.stylesheet.name, - metadata: this.stylesheet.metadata, - light: this.stylesheet.light, - terrain: this.getTerrain() || undefined, - fog: this.stylesheet.fog, - center: this.stylesheet.center, - zoom: this.stylesheet.zoom, - bearing: this.stylesheet.bearing, - pitch: this.stylesheet.pitch, - sprite: this.stylesheet.sprite, - glyphs: this.stylesheet.glyphs, - transition: this.stylesheet.transition, - projection: this.stylesheet.projection, - sources, - layers: this._serializeLayers(this._order), - }, - value => { - return value !== undefined; - }, - ); - } - - _updateLayer(layer: StyleLayer) { - this._updatedLayers[layer.id] = true; - const sourceCache = this._getLayerSourceCache(layer); - if ( - layer.source && !this._updatedSources[layer.source] && - //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865) - sourceCache && - sourceCache.getSource().type !== 'raster' - ) { - this._updatedSources[layer.source] = 'reload'; - sourceCache.pause(); - } - this._changed = true; - layer.invalidateCompiledFilter(); - } - - _flattenAndSortRenderedFeatures(sourceResults: Array): Array { - // Feature order is complicated. - // The order between features in two 2D layers is always determined by layer order. - // The order between features in two 3D layers is always determined by depth. - // The order between a feature in a 2D layer and a 3D layer is tricky: - // Most often layer order determines the feature order in this case. If - // a line layer is above a extrusion layer the line feature will be rendered - // above the extrusion. If the line layer is below the extrusion layer, - // it will be rendered below it. - // - // There is a weird case though. - // You have layers in this order: extrusion_layer_a, line_layer, extrusion_layer_b - // Each layer has a feature that overlaps the other features. - // The feature in extrusion_layer_a is closer than the feature in extrusion_layer_b so it is rendered above. - // The feature in line_layer is rendered above extrusion_layer_a. - // This means that that the line_layer feature is above the extrusion_layer_b feature despite - // it being in an earlier layer. - - const isLayer3D = (layerId => this._layers[layerId].type === 'fill-extrusion'); - - const layerIndex = {}; - const features3D = []; - for (let l = this._order.length - 1; l >= 0; l--) { - const layerId = this._order[l]; - if (isLayer3D(layerId)) { - layerIndex[layerId] = l; - for (const sourceResult of sourceResults) { - const layerFeatures = sourceResult[layerId]; - if (layerFeatures) { - for (const featureWrapper of layerFeatures) { - features3D.push(featureWrapper); - } - } - } - } - } - - features3D.sort( - (a, b) => { - return b.intersectionZ - a.intersectionZ; - }, - ); - - const features = []; - for (let l = this._order.length - 1; l >= 0; l--) { - const layerId = this._order[l]; - - if (isLayer3D(layerId)) { - // add all 3D features that are in or above the current layer - for (let i = features3D.length - 1; i >= 0; i--) { - const topmost3D = features3D[i].feature; - if (layerIndex[topmost3D.layer.id] < l) break; - features.push(topmost3D); - features3D.pop(); - } - } else { - for (const sourceResult of sourceResults) { - const layerFeatures = sourceResult[layerId]; - if (layerFeatures) { - for (const featureWrapper of layerFeatures) { - features.push(featureWrapper.feature); - } - } - } - } - } - - return features; - } - - queryRenderedFeatures( - queryGeometry: PointLike | [PointLike, PointLike], - params: any, - transform: Transform, - ): Array { - if (params && params.filter) { - this._validate( - validateFilter, - 'queryRenderedFeatures.filter', - params.filter, - null, - params, - ); - } - - const includedSources = {}; - if (params && params.layers) { - if (!Array.isArray(params.layers)) { - this.fire( - new ErrorEvent(new Error('parameters.layers must be an Array.')), - ); - return []; - } - for (const layerId of params.layers) { - const layer = this._layers[layerId]; - if (!layer) { - // this layer is not in the style.layers array - this.fire( - new ErrorEvent( - new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`,), - ), - ); - return []; - } - includedSources[layer.source] = true; - } - } - - const sourceResults: Array = []; - - params.availableImages = this._availableImages; - - const has3DLayer = params && params.layers ? - params.layers.some( - layerId => { - const layer = this.getLayer(layerId); - return layer && layer.is3D(); - }, - ) : - this.has3DLayers(); - const queryGeometryStruct = QueryGeometry.createFromScreenPoints( - queryGeometry, - transform, - ); - - for (const id in this._sourceCaches) { - const sourceId = this._sourceCaches[id].getSource().id; - if (params.layers && !includedSources[sourceId]) continue; - sourceResults.push( - queryRenderedFeatures( - this._sourceCaches[id], - this._layers, - this._serializedLayers, - queryGeometryStruct, - params, - transform, - has3DLayer, - !!this.map._showQueryGeometry, - ), - ); - } - - if (this.placement) { - // If a placement has run, query against its CollisionIndex - // for symbol results, and treat it as an extra source to merge - sourceResults.push( - queryRenderedSymbols( - this._layers, - this._serializedLayers, - this._getLayerSourceCache.bind(this), - queryGeometryStruct.screenGeometry, - params, - this.placement.collisionIndex, - this.placement.retainedQueryData, - ), - ); - } - - return (this._flattenAndSortRenderedFeatures(sourceResults): any); - } - - querySourceFeatures( - sourceID: string, - params: ?{ sourceLayer: ?string, filter: ?Array, validate?: boolean }, - ): Array { - if (params && params.filter) { - this._validate( - validateFilter, - 'querySourceFeatures.filter', - params.filter, - null, - params, - ); - } - const sourceCaches = this._getSourceCaches(sourceID); - let results = []; - for (const sourceCache of sourceCaches) { - results = results.concat(querySourceFeatures(sourceCache, params)); - } - return results; - } - - addSourceType( - name: string, - SourceType: SourceClass, - callback: Callback, - ): void { - if (Style.getSourceType(name)) { - return callback( - new Error(`A source type called "${name}" already exists.`), - ); - } - - Style.setSourceType(name, SourceType); - - if (!SourceType.workerSourceURL) { - return callback(null, null); - } - - this.dispatcher.broadcast( - 'loadWorkerSource', - { - name, - url: SourceType.workerSourceURL, - }, - callback, - ); - } - - getLight(): LightSpecification { - return this.light.getLight(); - } - - setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) { - this._checkLoaded(); - - const light = this.light.getLight(); - let _update = false; - for (const key in lightOptions) { - if (!deepEqual(lightOptions[key], light[key])) { - _update = true; - break; - } - } - if (!_update) return; - - const parameters = this._setTransitionParameters( - {duration: 300, delay: 0}, - ); - - this.light.setLight(lightOptions, options); - this.light.updateTransitions(parameters); - } - - getTerrain(): ?TerrainSpecification { - return this.terrain && - this.terrain.drapeRenderMode === DrapeRenderMode.elevated ? - this.terrain.get() : - null; - } - - setTerrainForDraping() { - const mockTerrainOptions = {source: '', exaggeration: 0}; - this.setTerrain(mockTerrainOptions, DrapeRenderMode.deferred); - } - - // eslint-disable-next-line no-warning-comments - // TODO: generic approach for root level property: light, terrain, skybox. - // It is not done here to prevent rebasing issues. - setTerrain( - terrainOptions: ?TerrainSpecification, - drapeRenderMode: number = DrapeRenderMode.elevated, - ) { - this._checkLoaded(); - - // Disabling - if (!terrainOptions) { - delete this.terrain; - delete this.stylesheet.terrain; - this.dispatcher.broadcast('enableTerrain', false); - this._force3DLayerUpdate(); - this._markersNeedUpdate = true; - return; - } - - if (drapeRenderMode === DrapeRenderMode.elevated) { - // Input validation and source object unrolling - if (typeof terrainOptions.source === 'object') { - const id = 'terrain-dem-src'; - this.addSource(id, (terrainOptions.source: any)); - terrainOptions = clone(terrainOptions); - terrainOptions = (extend(terrainOptions, {source: id}): any); - } - - if (this._validate(validateTerrain, 'terrain', terrainOptions)) { - return; - } - } - - // Enabling - if (!this.terrain || (this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode)) { - this._createTerrain(terrainOptions, drapeRenderMode); - } else { // Updating - const terrain = this.terrain; - const currSpec = terrain.get(); - - for (const name of Object.keys(styleSpec.terrain)) { - // Fallback to use default style specification when the properties wasn't set - if ( - !terrainOptions.hasOwnProperty(name) && - !!styleSpec.terrain[name].default - ) { - terrainOptions[name] = styleSpec.terrain[name].default; - } - } - for (const key in terrainOptions) { - if (!deepEqual(terrainOptions[key], currSpec[key])) { - terrain.set(terrainOptions); - this.stylesheet.terrain = terrainOptions; - const parameters = this._setTransitionParameters({duration: 0}); - terrain.updateTransitions(parameters); - break; - } - } - } - - this._updateDrapeFirstLayers(); - this._markersNeedUpdate = true; - } - - _createFog(fogOptions: FogSpecification) { - const fog = this.fog = new Fog(fogOptions, this.map.transform); - this.stylesheet.fog = fogOptions; - const parameters = this._setTransitionParameters({duration: 0}); - fog.updateTransitions(parameters); - } - - _updateMarkersOpacity() { - if (this.map._markers.length === 0) { - return; - } - this.map._requestDomTask( - () => { - for (const marker of this.map._markers) { - marker._evaluateOpacity(); - } - }, - ); - } - - getFog(): ?FogSpecification { - return this.fog ? this.fog.get() : null; - } - - setFog(fogOptions: FogSpecification) { - this._checkLoaded(); - - if (!fogOptions) { - // Remove fog - delete this.fog; - delete this.stylesheet.fog; - this._markersNeedUpdate = true; - return; - } - - if (!this.fog) { - // Initialize Fog - this._createFog(fogOptions); - } else { - // Updating fog - const fog = this.fog; - const currSpec = fog.get(); - - // empty object should pass through to set default values - if (Object.keys(fogOptions).length === 0) fog.set(fogOptions); - - for (const key in fogOptions) { - if (!deepEqual(fogOptions[key], currSpec[key])) { - fog.set(fogOptions); - this.stylesheet.fog = fogOptions; - const parameters = this._setTransitionParameters({duration: 0}); - fog.updateTransitions(parameters); - break; - } - } - } - - this._markersNeedUpdate = true; - } - - _setTransitionParameters(transitionOptions: Object): TransitionParameters { - return { - now: browser.now(), - transition: extend(transitionOptions, this.stylesheet.transition), - }; - } - - _updateDrapeFirstLayers() { - if (!this.map._optimizeForTerrain || !this.terrain) { - return; - } - - const draped = this._order.filter( - id => { - return this.isLayerDraped(this._layers[id]); - }, - ); - - const nonDraped = this._order.filter( - id => { - return !this.isLayerDraped(this._layers[id]); - }, - ); - this._drapedFirstOrder = []; - this._drapedFirstOrder.push(...draped); - this._drapedFirstOrder.push(...nonDraped); - } - - _createTerrain(terrainOptions: TerrainSpecification, drapeRenderMode: number) { - const terrain = this.terrain = new Terrain(terrainOptions, drapeRenderMode); - this.stylesheet.terrain = terrainOptions; - this.dispatcher.broadcast('enableTerrain', !this.terrainSetForDrapingOnly()); - this._force3DLayerUpdate(); - const parameters = this._setTransitionParameters({duration: 0}); - terrain.updateTransitions(parameters); - } - - _force3DLayerUpdate() { - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.type === 'fill-extrusion') { - this._updateLayer(layer); - } - } - } - - _forceSymbolLayerUpdate() { - for (const layerId in this._layers) { - const layer = this._layers[layerId]; - if (layer.type === 'symbol') { - this._updateLayer(layer); - } - } - } - - _validate( - validate: Validator, - key: string, - value: any, - props: any, - options: { validate?: boolean } = {}, - ): boolean { - if (options && options.validate === false) { - return false; - } - return emitValidationErrors( - this, - validate.call( - validateStyle, - extend( - { - key, - style: this.serialize(), - value, - styleSpec, - }, - props, - ), - ), - ); - } - - _remove() { - if (this._request) { - this._request.cancel(); - this._request = null; - } - if (this._spriteRequest) { - this._spriteRequest.cancel(); - this._spriteRequest = null; - } - rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback); - for (const layerId in this._layers) { - const layer: StyleLayer = this._layers[layerId]; - layer.setEventedParent(null); - } - for (const id in this._sourceCaches) { - this._sourceCaches[id].clearTiles(); - this._sourceCaches[id].setEventedParent(null); - } - this.imageManager.setEventedParent(null); - this.setEventedParent(null); - this.dispatcher.remove(); - } - - _clearSource(id: string) { - const sourceCaches = this._getSourceCaches(id); - for (const sourceCache of sourceCaches) { - sourceCache.clearTiles(); - } - } - - _reloadSource(id: string) { - const sourceCaches = this._getSourceCaches(id); - for (const sourceCache of sourceCaches) { - sourceCache.resume(); - sourceCache.reload(); - } - } - - _reloadSources() { - for (const source of this._getSources()) { - if (source.reload) { - source.reload(); - } - } - } - - _updateSources(transform: Transform) { - for (const id in this._sourceCaches) { - this._sourceCaches[id].update(transform); - } - } - - _generateCollisionBoxes() { - for (const id in this._sourceCaches) { - const sourceCache = this._sourceCaches[id]; - sourceCache.resume(); - sourceCache.reload(); - } - } - - _updatePlacement( - transform: Transform, - showCollisionBoxes: boolean, - fadeDuration: number, - crossSourceCollisions: boolean, - forceFullPlacement: boolean = false, - ): boolean { - let symbolBucketsChanged = false; - let placementCommitted = false; - - const layerTiles = {}; - - for (const layerID of this._order) { - const styleLayer = this._layers[layerID]; - if (styleLayer.type !== 'symbol') continue; - - if (!layerTiles[styleLayer.source]) { - const sourceCache = this._getLayerSourceCache(styleLayer); - if (!sourceCache) continue; - layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true).map( - id => sourceCache.getTileByID(id), - ).sort( - (a, b) => b.tileID.overscaledZ - a.tileID.overscaledZ || - (a.tileID.isLessThan(b.tileID) ? -1 : 1), - ); - } - - const layerBucketsChanged = this.crossTileSymbolIndex.addLayer( - styleLayer, - layerTiles[styleLayer.source], - transform.center.lng, - transform.projection, - ); - symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged; - } - this.crossTileSymbolIndex.pruneUnusedLayers(this._order); - - // Anything that changes our "in progress" layer and tile indices requires us - // to start over. When we start over, we do a full placement instead of incremental - // to prevent starvation. - // We need to restart placement to keep layer indices in sync. - // Also force full placement when fadeDuration === 0 to ensure that newly loaded - // tiles will fully display symbols in their first frame - forceFullPlacement = forceFullPlacement || this._layerOrderChanged || - fadeDuration === 0; - - if (this._layerOrderChanged) { - this.fire(new Event('neworder')); - } - - if ( - forceFullPlacement || !this.pauseablePlacement || - (this.pauseablePlacement.isDone() && - !this.placement.stillRecent(browser.now(), transform.zoom)) - ) { - const fogState = this.fog && transform.projection.supportsFog ? this.fog.state : null; - this.pauseablePlacement = new PauseablePlacement( - transform, - this._order, - forceFullPlacement, - showCollisionBoxes, - fadeDuration, - crossSourceCollisions, - this.placement, - fogState - ); - this._layerOrderChanged = false; - } - - if (this.pauseablePlacement.isDone()) { - // the last placement finished running, but the next one hasn’t - // started yet because of the `stillRecent` check immediately - // above, so mark it stale to ensure that we request another - // render frame - this.placement.setStale(); - } else { - this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); - - if (this.pauseablePlacement.isDone()) { - this.placement = this.pauseablePlacement.commit(browser.now()); - placementCommitted = true; - } - - if (symbolBucketsChanged) { - // since the placement gets split over multiple frames it is possible - // these buckets were processed before they were changed and so the - // placement is already stale while it is in progress - this.pauseablePlacement.placement.setStale(); - } - } - - if (placementCommitted || symbolBucketsChanged) { - for (const layerID of this._order) { - const styleLayer = this._layers[layerID]; - if (styleLayer.type !== 'symbol') continue; - this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]); - } - } - - // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols - const needsRerender = !this.pauseablePlacement.isDone() || - this.placement.hasTransitions(browser.now()); - return needsRerender; - } - - _releaseSymbolFadeTiles() { - for (const id in this._sourceCaches) { - this._sourceCaches[id].releaseSymbolFadeTiles(); - } - } - - // Callbacks from web workers - - getImages( - mapId: string, - params: { - icons: Array, - source: string, - tileID: OverscaledTileID, - type: string, - }, - callback: Callback<{ [_: string]: StyleImage }>, - ) { - this.imageManager.getImages(params.icons, callback); - - // Apply queued image changes before setting the tile's dependencies so that the tile - // is not reloaded unecessarily. Without this forced update the reload could happen in cases - // like this one: - // - icons contains "my-image" - // - imageManager.getImages(...) triggers `onstyleimagemissing` - // - the user adds "my-image" within the callback - // - addImage adds "my-image" to this._changedImages - // - the next frame triggers a reload of this tile even though it already has the latest version - this._updateTilesForChangedImages(); - - const setDependencies = ((sourceCache: SourceCache) => { - if (sourceCache) { - sourceCache.setDependencies( - params.tileID.key, - params.type, - params.icons, - ); - } - }); - setDependencies(this._otherSourceCaches[params.source]); - setDependencies(this._symbolSourceCaches[params.source]); - } - - getGlyphs(mapId: string, params: {stacks: {[_: string]: Array}}, callback: Callback<{[_: string]: {glyphs: {[_: number]: ?StyleGlyph}, ascender?: number, descender?: number}}>) { - this.glyphManager.getGlyphs(params.stacks, callback); - } - - getResource( - mapId: string, - params: RequestParameters, - callback: ResponseCallback, - ): Cancelable { - return makeRequest(params, callback); - } - - _getSourceCache(source: string): SourceCache | void { - return this._otherSourceCaches[source]; - } - - _getLayerSourceCache: (layer: StyleLayer) => SourceCache | void = (layer) => { - return layer.type === 'symbol' ? - this._symbolSourceCaches[layer.source] : - this._otherSourceCaches[layer.source]; - }; - - _getSourceCaches(source: string): Array { - const sourceCaches = []; - if (this._otherSourceCaches[source]) { - sourceCaches.push(this._otherSourceCaches[source]); - } - if (this._symbolSourceCaches[source]) { - sourceCaches.push(this._symbolSourceCaches[source]); - } - return sourceCaches; - } - - _isSourceCacheLoaded(source: string): boolean { - const sourceCaches = this._getSourceCaches(source); - if (sourceCaches.length === 0) { - this.fire( - new ErrorEvent(new Error(`There is no source with ID '${source}'`)), - ); - return false; - } - return sourceCaches.every(sc => sc.loaded()); - } - - has3DLayers(): boolean { - return this._num3DLayers > 0; - } - - hasSymbolLayers(): boolean { - return this._numSymbolLayers > 0; - } - - hasCircleLayers(): boolean { - return this._numCircleLayers > 0; - } - - _clearWorkerCaches() { - this.dispatcher.broadcast('clearCaches'); - } - - destroy() { - this._clearWorkerCaches(); - if (this.terrainSetForDrapingOnly()) { - delete this.terrain; - delete this.stylesheet.terrain; - } - } + getLayoutProperty(layerId: string, name: string): ?PropertyValueSpecification { + const layer = this.getLayer(layerId); + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style.`))); + return; + } + + return layer.getLayoutProperty(name); + } + + setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + const layer = this.getLayer(layerId); + if (!layer) { + this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be styled.`))); + return; + } + + if (deepEqual(layer.getPaintProperty(name), value)) return; + + const requiresRelayout = layer.setPaintProperty(name, value, options); + if (requiresRelayout) { + this._updateLayer(layer); + } + + this._changed = true; + this._updatedPaintProps[layerId] = true; + } + + getPaintProperty(layerId: string, name: string): void | TransitionSpecification | PropertyValueSpecification { + const layer = this.getLayer(layerId); + return layer && layer.getPaintProperty(name); + } + + setFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }, state: Object) { + this._checkLoaded(); + const sourceId = target.source; + const sourceLayer = target.sourceLayer; + const source = this.getSource(sourceId); + + if (!source) { + this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); + return; + } + const sourceType = source.type; + if (sourceType === 'geojson' && sourceLayer) { + this.fire(new ErrorEvent(new Error(`GeoJSON sources cannot have a sourceLayer parameter.`))); + return; + } + if (sourceType === 'vector' && !sourceLayer) { + this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); + return; + } + if (target.id === undefined) { + this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); + } + + const sourceCaches = this._getSourceCaches(sourceId); + for (const sourceCache of sourceCaches) { + sourceCache.setFeatureState(sourceLayer, target.id, state); + } + } + + removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { + this._checkLoaded(); + const sourceId = target.source; + const source = this.getSource(sourceId); + + if (!source) { + this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); + return; + } + + const sourceType = source.type; + const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; + + if (sourceType === 'vector' && !sourceLayer) { + this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); + return; + } + + if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) { + this.fire(new ErrorEvent(new Error(`A feature id is required to remove its specific state property.`))); + return; + } + + const sourceCaches = this._getSourceCaches(sourceId); + for (const sourceCache of sourceCaches) { + sourceCache.removeFeatureState(sourceLayer, target.id, key); + } + } + + getFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }): ?FeatureStates { + this._checkLoaded(); + const sourceId = target.source; + const sourceLayer = target.sourceLayer; + const source = this.getSource(sourceId); + + if (!source) { + this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); + return; + } + const sourceType = source.type; + if (sourceType === 'vector' && !sourceLayer) { + this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); + return; + } + if (target.id === undefined) { + this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); + } + + const sourceCaches = this._getSourceCaches(sourceId); + return sourceCaches[0].getFeatureState(sourceLayer, target.id); + } + + getTransition(): TransitionSpecification { + return extend({duration: 300, delay: 0}, this.stylesheet && this.stylesheet.transition); + } + + serialize(): StyleSpecification { + const sources = {}; + for (const cacheId in this._sourceCaches) { + const source = this._sourceCaches[cacheId].getSource(); + if (!sources[source.id]) { + sources[source.id] = source.serialize(); + } + } + + return filterObject({ + version: this.stylesheet.version, + name: this.stylesheet.name, + metadata: this.stylesheet.metadata, + light: this.stylesheet.light, + terrain: this.getTerrain() || undefined, + fog: this.stylesheet.fog, + center: this.stylesheet.center, + zoom: this.stylesheet.zoom, + bearing: this.stylesheet.bearing, + pitch: this.stylesheet.pitch, + sprite: this.stylesheet.sprite, + glyphs: this.stylesheet.glyphs, + transition: this.stylesheet.transition, + projection: this.stylesheet.projection, + sources, + layers: this._serializeLayers(this._order) + }, (value) => { return value !== undefined; }); + } + + _updateLayer(layer: StyleLayer) { + this._updatedLayers[layer.id] = true; + const sourceCache = this._getLayerSourceCache(layer); + if (layer.source && !this._updatedSources[layer.source] && + //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865) + sourceCache && + sourceCache.getSource().type !== 'raster') { + this._updatedSources[layer.source] = 'reload'; + sourceCache.pause(); + } + this._changed = true; + layer.invalidateCompiledFilter(); + + } + + _flattenAndSortRenderedFeatures(sourceResults: Array): Array { + // Feature order is complicated. + // The order between features in two 2D layers is always determined by layer order. + // The order between features in two 3D layers is always determined by depth. + // The order between a feature in a 2D layer and a 3D layer is tricky: + // Most often layer order determines the feature order in this case. If + // a line layer is above a extrusion layer the line feature will be rendered + // above the extrusion. If the line layer is below the extrusion layer, + // it will be rendered below it. + // + // There is a weird case though. + // You have layers in this order: extrusion_layer_a, line_layer, extrusion_layer_b + // Each layer has a feature that overlaps the other features. + // The feature in extrusion_layer_a is closer than the feature in extrusion_layer_b so it is rendered above. + // The feature in line_layer is rendered above extrusion_layer_a. + // This means that that the line_layer feature is above the extrusion_layer_b feature despite + // it being in an earlier layer. + + const isLayer3D = layerId => this._layers[layerId].type === 'fill-extrusion'; + + const layerIndex = {}; + const features3D = []; + for (let l = this._order.length - 1; l >= 0; l--) { + const layerId = this._order[l]; + if (isLayer3D(layerId)) { + layerIndex[layerId] = l; + for (const sourceResult of sourceResults) { + const layerFeatures = sourceResult[layerId]; + if (layerFeatures) { + for (const featureWrapper of layerFeatures) { + features3D.push(featureWrapper); + } + } + } + } + } + + features3D.sort((a, b) => { + return b.intersectionZ - a.intersectionZ; + }); + + const features = []; + for (let l = this._order.length - 1; l >= 0; l--) { + const layerId = this._order[l]; + + if (isLayer3D(layerId)) { + // add all 3D features that are in or above the current layer + for (let i = features3D.length - 1; i >= 0; i--) { + const topmost3D = features3D[i].feature; + if (layerIndex[topmost3D.layer.id] < l) break; + features.push(topmost3D); + features3D.pop(); + } + } else { + for (const sourceResult of sourceResults) { + const layerFeatures = sourceResult[layerId]; + if (layerFeatures) { + for (const featureWrapper of layerFeatures) { + features.push(featureWrapper.feature); + } + } + } + } + } + + return features; + } + + queryRenderedFeatures(queryGeometry: PointLike | [PointLike, PointLike], params: any, transform: Transform): Array { + if (params && params.filter) { + this._validate(validateFilter, 'queryRenderedFeatures.filter', params.filter, null, params); + } + + const includedSources = {}; + if (params && params.layers) { + if (!Array.isArray(params.layers)) { + this.fire(new ErrorEvent(new Error('parameters.layers must be an Array.'))); + return []; + } + for (const layerId of params.layers) { + const layer = this._layers[layerId]; + if (!layer) { + // this layer is not in the style.layers array + this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`))); + return []; + } + includedSources[layer.source] = true; + } + } + + const sourceResults: Array = []; + + params.availableImages = this._availableImages; + + const has3DLayer = (params && params.layers) ? + params.layers.some((layerId) => { + const layer = this.getLayer(layerId); + return layer && layer.is3D(); + }) : + this.has3DLayers(); + const queryGeometryStruct = QueryGeometry.createFromScreenPoints(queryGeometry, transform); + + for (const id in this._sourceCaches) { + const sourceId = this._sourceCaches[id].getSource().id; + if (params.layers && !includedSources[sourceId]) continue; + sourceResults.push( + queryRenderedFeatures( + this._sourceCaches[id], + this._layers, + this._serializedLayers, + queryGeometryStruct, + params, + transform, + has3DLayer, + !!this.map._showQueryGeometry) + ); + } + + if (this.placement) { + // If a placement has run, query against its CollisionIndex + // for symbol results, and treat it as an extra source to merge + sourceResults.push( + queryRenderedSymbols( + this._layers, + this._serializedLayers, + this._getLayerSourceCache.bind(this), + queryGeometryStruct.screenGeometry, + params, + this.placement.collisionIndex, + this.placement.retainedQueryData) + ); + } + + return (this._flattenAndSortRenderedFeatures(sourceResults): any); + } + + querySourceFeatures(sourceID: string, params: ?{sourceLayer: ?string, filter: ?Array, validate?: boolean}): Array { + if (params && params.filter) { + this._validate(validateFilter, 'querySourceFeatures.filter', params.filter, null, params); + } + const sourceCaches = this._getSourceCaches(sourceID); + let results = []; + for (const sourceCache of sourceCaches) { + results = results.concat(querySourceFeatures(sourceCache, params)); + } + return results; + } + + addSourceType(name: string, SourceType: SourceClass, callback: Callback): void { + if (Style.getSourceType(name)) { + return callback(new Error(`A source type called "${name}" already exists.`)); + } + + Style.setSourceType(name, SourceType); + + if (!SourceType.workerSourceURL) { + return callback(null, null); + } + + this.dispatcher.broadcast('loadWorkerSource', { + name, + url: SourceType.workerSourceURL + }, callback); + } + + getLight(): LightSpecification { + return this.light.getLight(); + } + + setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) { + this._checkLoaded(); + + const light = this.light.getLight(); + let _update = false; + for (const key in lightOptions) { + if (!deepEqual(lightOptions[key], light[key])) { + _update = true; + break; + } + } + if (!_update) return; + + const parameters = this._setTransitionParameters({duration: 300, delay: 0}); + + this.light.setLight(lightOptions, options); + this.light.updateTransitions(parameters); + } + + getTerrain(): ?TerrainSpecification { + return this.terrain && this.terrain.drapeRenderMode === DrapeRenderMode.elevated ? this.terrain.get() : null; + } + + setTerrainForDraping() { + const mockTerrainOptions = {source: '', exaggeration: 0}; + this.setTerrain(mockTerrainOptions, DrapeRenderMode.deferred); + } + + // eslint-disable-next-line no-warning-comments + // TODO: generic approach for root level property: light, terrain, skybox. + // It is not done here to prevent rebasing issues. + setTerrain(terrainOptions: ?TerrainSpecification, drapeRenderMode: number = DrapeRenderMode.elevated) { + this._checkLoaded(); + + // Disabling + if (!terrainOptions) { + delete this.terrain; + delete this.stylesheet.terrain; + this.dispatcher.broadcast('enableTerrain', false); + this._force3DLayerUpdate(); + this._markersNeedUpdate = true; + return; + } + + if (drapeRenderMode === DrapeRenderMode.elevated) { + // Input validation and source object unrolling + if (typeof terrainOptions.source === 'object') { + const id = 'terrain-dem-src'; + this.addSource(id, ((terrainOptions.source): any)); + terrainOptions = clone(terrainOptions); + terrainOptions = (extend(terrainOptions, {source: id}): any); + } + + if (this._validate(validateTerrain, 'terrain', terrainOptions)) { + return; + } + } + + // Enabling + if (!this.terrain || (this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode)) { + this._createTerrain(terrainOptions, drapeRenderMode); + } else { // Updating + const terrain = this.terrain; + const currSpec = terrain.get(); + + for (const name of Object.keys(styleSpec.terrain)) { + // Fallback to use default style specification when the properties wasn't set + if (!terrainOptions.hasOwnProperty(name) && !!styleSpec.terrain[name].default) { + terrainOptions[name] = styleSpec.terrain[name].default; + } + } + for (const key in terrainOptions) { + if (!deepEqual(terrainOptions[key], currSpec[key])) { + terrain.set(terrainOptions); + this.stylesheet.terrain = terrainOptions; + const parameters = this._setTransitionParameters({duration: 0}); + terrain.updateTransitions(parameters); + break; + } + } + } + + this._updateDrapeFirstLayers(); + this._markersNeedUpdate = true; + } + + _createFog(fogOptions: FogSpecification) { + const fog = this.fog = new Fog(fogOptions, this.map.transform); + this.stylesheet.fog = fogOptions; + const parameters = this._setTransitionParameters({duration: 0}); + fog.updateTransitions(parameters); + } + + _updateMarkersOpacity() { + if (this.map._markers.length === 0) { + return; + } + this.map._requestDomTask(() => { + for (const marker of this.map._markers) { + marker._evaluateOpacity(); + } + }); + } + + getFog(): ?FogSpecification { + return this.fog ? this.fog.get() : null; + } + + setFog(fogOptions: FogSpecification) { + this._checkLoaded(); + + if (!fogOptions) { + // Remove fog + delete this.fog; + delete this.stylesheet.fog; + this._markersNeedUpdate = true; + return; + } + + if (!this.fog) { + // Initialize Fog + this._createFog(fogOptions); + } else { + // Updating fog + const fog = this.fog; + const currSpec = fog.get(); + + // empty object should pass through to set default values + if (Object.keys(fogOptions).length === 0) fog.set(fogOptions); + + for (const key in fogOptions) { + if (!deepEqual(fogOptions[key], currSpec[key])) { + fog.set(fogOptions); + this.stylesheet.fog = fogOptions; + const parameters = this._setTransitionParameters({duration: 0}); + fog.updateTransitions(parameters); + break; + } + } + } + + this._markersNeedUpdate = true; + } + + _setTransitionParameters(transitionOptions: Object): TransitionParameters { + return { + now: browser.now(), + transition: extend( + transitionOptions, + this.stylesheet.transition) + }; + } + + _updateDrapeFirstLayers() { + if (!this.map._optimizeForTerrain || !this.terrain) { + return; + } + + const draped = this._order.filter((id) => { + return this.isLayerDraped(this._layers[id]); + }); + + const nonDraped = this._order.filter((id) => { + return !this.isLayerDraped(this._layers[id]); + }); + this._drapedFirstOrder = []; + this._drapedFirstOrder.push(...draped); + this._drapedFirstOrder.push(...nonDraped); + } + + _createTerrain(terrainOptions: TerrainSpecification, drapeRenderMode: number) { + const terrain = this.terrain = new Terrain(terrainOptions, drapeRenderMode); + this.stylesheet.terrain = terrainOptions; + this.dispatcher.broadcast('enableTerrain', !this.terrainSetForDrapingOnly()); + this._force3DLayerUpdate(); + const parameters = this._setTransitionParameters({duration: 0}); + terrain.updateTransitions(parameters); + } + + _force3DLayerUpdate() { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === 'fill-extrusion') { + this._updateLayer(layer); + } + } + } + + _forceSymbolLayerUpdate() { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === 'symbol') { + this._updateLayer(layer); + } + } + } + + _validate(validate: Validator, key: string, value: any, props: any, options: { validate?: boolean } = {}): boolean { + if (options && options.validate === false) { + return false; + } + return emitValidationErrors(this, validate.call(validateStyle, extend({ + key, + style: this.serialize(), + value, + styleSpec + }, props))); + } + + _remove() { + if (this._request) { + this._request.cancel(); + this._request = null; + } + if (this._spriteRequest) { + this._spriteRequest.cancel(); + this._spriteRequest = null; + } + rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback); + for (const layerId in this._layers) { + const layer: StyleLayer = this._layers[layerId]; + layer.setEventedParent(null); + } + for (const id in this._sourceCaches) { + this._sourceCaches[id].clearTiles(); + this._sourceCaches[id].setEventedParent(null); + } + this.imageManager.setEventedParent(null); + this.setEventedParent(null); + this.dispatcher.remove(); + } + + _clearSource(id: string) { + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + sourceCache.clearTiles(); + } + } + + _reloadSource(id: string) { + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + sourceCache.resume(); + sourceCache.reload(); + } + } + + _reloadSources() { + for (const source of this._getSources()) { + if (source.reload) { + source.reload(); + } + } + } + + _updateSources(transform: Transform) { + for (const id in this._sourceCaches) { + this._sourceCaches[id].update(transform); + } + } + + _generateCollisionBoxes() { + for (const id in this._sourceCaches) { + const sourceCache = this._sourceCaches[id]; + sourceCache.resume(); + sourceCache.reload(); + } + } + + _updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number, crossSourceCollisions: boolean, forceFullPlacement: boolean = false): boolean { + let symbolBucketsChanged = false; + let placementCommitted = false; + + const layerTiles = {}; + + for (const layerID of this._order) { + const styleLayer = this._layers[layerID]; + if (styleLayer.type !== 'symbol') continue; + + if (!layerTiles[styleLayer.source]) { + const sourceCache = this._getLayerSourceCache(styleLayer); + if (!sourceCache) continue; + layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true) + .map((id) => sourceCache.getTileByID(id)) + .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1)); + } + + const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source], transform.center.lng, transform.projection); + symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged; + } + this.crossTileSymbolIndex.pruneUnusedLayers(this._order); + + // Anything that changes our "in progress" layer and tile indices requires us + // to start over. When we start over, we do a full placement instead of incremental + // to prevent starvation. + // We need to restart placement to keep layer indices in sync. + // Also force full placement when fadeDuration === 0 to ensure that newly loaded + // tiles will fully display symbols in their first frame + forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0; + + if (this._layerOrderChanged) { + this.fire(new Event('neworder')); + } + + if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) { + const fogState = this.fog && transform.projection.supportsFog ? this.fog.state : null; + this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement, fogState); + this._layerOrderChanged = false; + } + + if (this.pauseablePlacement.isDone()) { + // the last placement finished running, but the next one hasn’t + // started yet because of the `stillRecent` check immediately + // above, so mark it stale to ensure that we request another + // render frame + this.placement.setStale(); + } else { + this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); + + if (this.pauseablePlacement.isDone()) { + this.placement = this.pauseablePlacement.commit(browser.now()); + placementCommitted = true; + } + + if (symbolBucketsChanged) { + // since the placement gets split over multiple frames it is possible + // these buckets were processed before they were changed and so the + // placement is already stale while it is in progress + this.pauseablePlacement.placement.setStale(); + } + } + + if (placementCommitted || symbolBucketsChanged) { + for (const layerID of this._order) { + const styleLayer = this._layers[layerID]; + if (styleLayer.type !== 'symbol') continue; + this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]); + } + } + + // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols + const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(browser.now()); + return needsRerender; + } + + _releaseSymbolFadeTiles() { + for (const id in this._sourceCaches) { + this._sourceCaches[id].releaseSymbolFadeTiles(); + } + } + + // Callbacks from web workers + + getImages(mapId: string, params: {icons: Array, source: string, tileID: OverscaledTileID, type: string}, callback: Callback<{[_: string]: StyleImage}>) { + + this.imageManager.getImages(params.icons, callback); + + // Apply queued image changes before setting the tile's dependencies so that the tile + // is not reloaded unecessarily. Without this forced update the reload could happen in cases + // like this one: + // - icons contains "my-image" + // - imageManager.getImages(...) triggers `onstyleimagemissing` + // - the user adds "my-image" within the callback + // - addImage adds "my-image" to this._changedImages + // - the next frame triggers a reload of this tile even though it already has the latest version + this._updateTilesForChangedImages(); + + const setDependencies = (sourceCache: SourceCache) => { + if (sourceCache) { + sourceCache.setDependencies(params.tileID.key, params.type, params.icons); + } + }; + setDependencies(this._otherSourceCaches[params.source]); + setDependencies(this._symbolSourceCaches[params.source]); + } + + getGlyphs(mapId: string, params: {stacks: {[_: string]: Array}}, callback: Callback<{[_: string]: {glyphs: {[_: number]: ?StyleGlyph}, ascender?: number, descender?: number}}>) { + this.glyphManager.getGlyphs(params.stacks, callback); + } + + getResource(mapId: string, params: RequestParameters, callback: ResponseCallback): Cancelable { + return makeRequest(params, callback); + } + + _getSourceCache(source: string): SourceCache | void { + return this._otherSourceCaches[source]; + } + + _getLayerSourceCache: (layer: StyleLayer) => SourceCache | void = (layer) => { + return layer.type === 'symbol' ? + this._symbolSourceCaches[layer.source] : + this._otherSourceCaches[layer.source]; + } + + _getSourceCaches(source: string): Array { + const sourceCaches = []; + if (this._otherSourceCaches[source]) { + sourceCaches.push(this._otherSourceCaches[source]); + } + if (this._symbolSourceCaches[source]) { + sourceCaches.push(this._symbolSourceCaches[source]); + } + return sourceCaches; + } + + _isSourceCacheLoaded(source: string): boolean { + const sourceCaches = this._getSourceCaches(source); + if (sourceCaches.length === 0) { + this.fire(new ErrorEvent(new Error(`There is no source with ID '${source}'`))); + return false; + } + return sourceCaches.every(sc => sc.loaded()); + } + + has3DLayers(): boolean { + return this._num3DLayers > 0; + } + + hasSymbolLayers(): boolean { + return this._numSymbolLayers > 0; + } + + hasCircleLayers(): boolean { + return this._numCircleLayers > 0; + } + + _clearWorkerCaches() { + this.dispatcher.broadcast('clearCaches'); + } + + destroy() { + this._clearWorkerCaches(); + if (this.terrainSetForDrapingOnly()) { + delete this.terrain; + delete this.stylesheet.terrain; + } + } } Style.getSourceType = getSourceType; From c84eef7d61e9f539d50e7cc703b6010a47ed8111 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 15:38:13 +0200 Subject: [PATCH 55/72] fix formatting for custom_style_layer.js --- src/style/style_layer/custom_style_layer.js | 94 ++++++++++----------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index c90a0ae43cc..a83454d947b 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -189,53 +189,53 @@ export function validateCustomStyleLayer(layerObject: CustomLayerInterface): Val return errors; } -class CustomStyleLayer - extends StyleLayer { - implementation: CustomLayerInterface; - - constructor(implementation: CustomLayerInterface) { - super(implementation, {}); - this.implementation = implementation; - } - - is3D(): boolean { - return this.implementation.renderingMode === '3d'; - } - - hasOffscreenPass(): boolean { - return this.implementation.prerender !== undefined; - } - - isLayerDraped(): boolean { - return this.implementation.renderToTile !== undefined; - } - - shouldRedrape(): boolean { - return !!this.implementation.shouldRerenderTiles && this.implementation.shouldRerenderTiles(); - } - - recalculate() {} - updateTransitions() {} - hasTransition(): boolean { - return false; - } - - // $FlowFixMe[incompatible-extend] - CustomStyleLayer is not serializable - serialize() { - assert(false, "Custom layers cannot be serialized"); - } - - onAdd: ((map: Map) => void) = (map: Map) => { - if (this.implementation.onAdd) { - this.implementation.onAdd(map, map.painter.context.gl); - } - }; - - onRemove: (map: Map) => void = (map: Map) => { - if (this.implementation.onRemove) { - this.implementation.onRemove(map, map.painter.context.gl); - } - } +class CustomStyleLayer extends StyleLayer { + + implementation: CustomLayerInterface; + + constructor(implementation: CustomLayerInterface) { + super(implementation, {}); + this.implementation = implementation; + } + + is3D(): boolean { + return this.implementation.renderingMode === '3d'; + } + + hasOffscreenPass(): boolean { + return this.implementation.prerender !== undefined; + } + + isLayerDraped(): boolean { + return this.implementation.renderToTile !== undefined; + } + + shouldRedrape(): boolean { + return !!this.implementation.shouldRerenderTiles && this.implementation.shouldRerenderTiles(); + } + + recalculate() {} + updateTransitions() {} + hasTransition(): boolean { + return false; + } + + // $FlowFixMe[incompatible-extend] - CustomStyleLayer is not serializable + serialize() { + assert(false, "Custom layers cannot be serialized"); + } + + onAdd: ((map: Map) => void) = (map: Map) => { + if (this.implementation.onAdd) { + this.implementation.onAdd(map, map.painter.context.gl); + } + } + + onRemove: (map: Map) => void = (map: Map) => { + if (this.implementation.onRemove) { + this.implementation.onRemove(map, map.painter.context.gl); + } + } } export default CustomStyleLayer; From 6a10d00c6c3b839f649d76be09061fed1a5cba39 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 15:58:33 +0200 Subject: [PATCH 56/72] fix formatting for source_cache.js --- src/source/source_cache.js | 1976 +++++++++++++++++------------------- 1 file changed, 908 insertions(+), 1068 deletions(-) diff --git a/src/source/source_cache.js b/src/source/source_cache.js index eb992c68e0c..d109c6808ed 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -33,435 +33,386 @@ import type {QueryGeometry, TilespaceQueryGeometry} from '../style/query_geometr * * @private */ -class SourceCache - extends Evented { - id: string; - map: MapboxMap; - style: Style; - - _source: Source; - _sourceLoaded: boolean; - _sourceErrored: boolean; - _tiles: { [_: string | number]: Tile }; - _prevLng: number | void; - _cache: TileCache; - _timers: { [_: any]: TimeoutID }; - _cacheTimers: { [_: any]: TimeoutID }; - _minTileCacheSize: ?number; - _maxTileCacheSize: ?number; - _paused: boolean; - _isRaster: boolean; - _shouldReloadOnResume: boolean; - _coveredTiles: { [_: number | string]: boolean }; - transform: Transform; - used: boolean; - usedForTerrain: boolean; - _state: SourceFeatureState; - _loadedParentTiles: { [_: number | string]: ?Tile }; - _onlySymbols: ?boolean; - - static maxUnderzooming: number; - static maxOverzooming: number; - - constructor(id: string, source: Source, onlySymbols?: boolean) { - super(); - this.id = id; - this._onlySymbols = onlySymbols; - - source.on( - 'data', - e => { - // this._sourceLoaded signifies that the TileJSON is loaded if applicable. - // if the source type does not come with a TileJSON, the flag signifies the - // source data has loaded (in other words, GeoJSON has been tiled on the worker and is ready) - if (e.dataType === 'source' && e.sourceDataType === 'metadata') - this._sourceLoaded = true; - - // for sources with mutable data, this event fires when the underlying data - // to a source is changed (for example, using [GeoJSONSource#setData](https://docs.mapbox.com/mapbox-gl-js/api/sources/#geojsonsource#setdata) or [ImageSource#setCoordinates](https://docs.mapbox.com/mapbox-gl-js/api/sources/#imagesource#setcoordinates)) - if ( - this._sourceLoaded && !this._paused && e.dataType === "source" && - e.sourceDataType === 'content' - ) { - this.reload(); - if (this.transform) { - this.update(this.transform); - } - } - }, - ); - - source.on( - 'error', - () => { - this._sourceErrored = true; - }, - ); - - this._source = source; - this._tiles = {}; - this._cache = new TileCache(0, this._unloadTile.bind(this)); - this._timers = {}; - this._cacheTimers = {}; - this._minTileCacheSize = source.minTileCacheSize; - this._maxTileCacheSize = source.maxTileCacheSize; - this._loadedParentTiles = {}; - - this._coveredTiles = {}; - this._state = new SourceFeatureState(); - this._isRaster = - this._source.type === 'raster' || - this._source.type === 'raster-dem' || - // $FlowFixMe[prop-missing] - (this._source.type === 'custom' && this._source._dataType === 'raster'); - } - - onAdd(map: MapboxMap) { - this.map = map; - this._minTileCacheSize = this._minTileCacheSize === undefined && map ? - map._minTileCacheSize : - this._minTileCacheSize; - this._maxTileCacheSize = this._maxTileCacheSize === undefined && map ? - map._maxTileCacheSize : - this._maxTileCacheSize; - } - - /** +class SourceCache extends Evented { + id: string; + map: MapboxMap; + style: Style; + + _source: Source; + _sourceLoaded: boolean; + _sourceErrored: boolean; + _tiles: {[_: string | number]: Tile}; + _prevLng: number | void; + _cache: TileCache; + _timers: {[_: any]: TimeoutID}; + _cacheTimers: {[_: any]: TimeoutID}; + _minTileCacheSize: ?number; + _maxTileCacheSize: ?number; + _paused: boolean; + _isRaster: boolean; + _shouldReloadOnResume: boolean; + _coveredTiles: {[_: number | string]: boolean}; + transform: Transform; + used: boolean; + usedForTerrain: boolean; + _state: SourceFeatureState; + _loadedParentTiles: {[_: number | string]: ?Tile}; + _onlySymbols: ?boolean; + + static maxUnderzooming: number; + static maxOverzooming: number; + + constructor(id: string, source: Source, onlySymbols?: boolean) { + super(); + this.id = id; + this._onlySymbols = onlySymbols; + + source.on('data', (e) => { + // this._sourceLoaded signifies that the TileJSON is loaded if applicable. + // if the source type does not come with a TileJSON, the flag signifies the + // source data has loaded (in other words, GeoJSON has been tiled on the worker and is ready) + if (e.dataType === 'source' && e.sourceDataType === 'metadata') this._sourceLoaded = true; + + // for sources with mutable data, this event fires when the underlying data + // to a source is changed (for example, using [GeoJSONSource#setData](https://docs.mapbox.com/mapbox-gl-js/api/sources/#geojsonsource#setdata) or [ImageSource#setCoordinates](https://docs.mapbox.com/mapbox-gl-js/api/sources/#imagesource#setcoordinates)) + if (this._sourceLoaded && !this._paused && e.dataType === "source" && e.sourceDataType === 'content') { + this.reload(); + if (this.transform) { + this.update(this.transform); + } + } + }); + + source.on('error', () => { + this._sourceErrored = true; + }); + + this._source = source; + this._tiles = {}; + this._cache = new TileCache(0, this._unloadTile.bind(this)); + this._timers = {}; + this._cacheTimers = {}; + this._minTileCacheSize = source.minTileCacheSize; + this._maxTileCacheSize = source.maxTileCacheSize; + this._loadedParentTiles = {}; + + this._coveredTiles = {}; + this._state = new SourceFeatureState(); + this._isRaster = + this._source.type === 'raster' || + this._source.type === 'raster-dem' || + // $FlowFixMe[prop-missing] + (this._source.type === 'custom' && this._source._dataType === 'raster'); + } + + onAdd(map: MapboxMap) { + this.map = map; + this._minTileCacheSize = this._minTileCacheSize === undefined && map ? map._minTileCacheSize : this._minTileCacheSize; + this._maxTileCacheSize = this._maxTileCacheSize === undefined && map ? map._maxTileCacheSize : this._maxTileCacheSize; + } + + /** * Return true if no tile data is pending, tiles will not change unless * an additional API call is received. * @private */ - loaded(): boolean { - if (this._sourceErrored) { - return true; - } - if (!this._sourceLoaded) { - return false; - } - if (!this._source.loaded()) { - return false; - } - for (const t in this._tiles) { - const tile = this._tiles[t]; - if (tile.state !== 'loaded' && tile.state !== 'errored') return false; - } - return true; - } - - getSource(): Source { - return this._source; - } - - pause() { - this._paused = true; - } - - resume() { - if (!this._paused) return; - const shouldReload = this._shouldReloadOnResume; - this._paused = false; - this._shouldReloadOnResume = false; - if (shouldReload) this.reload(); - if (this.transform) this.update(this.transform); - } - - _loadTile(tile: Tile, callback: Callback): void { - tile.isSymbolTile = this._onlySymbols; - return this._source.loadTile(tile, callback); - } - - _unloadTile: ((tile: Tile) => void) = (tile: Tile): void => { - if (this._source.unloadTile) return this._source.unloadTile(tile, () => {}); - }; - - _abortTile(tile: Tile): void { - if (this._source.abortTile) return this._source.abortTile(tile, () => {}); - } - - serialize(): SourceSpecification { - return this._source.serialize(); - } - - prepare(context: Context) { - if (this._source.prepare) { - this._source.prepare(); - } - - this._state.coalesceChanges(this._tiles, this.map ? this.map.painter : null); - - for (const i in this._tiles) { - const tile = this._tiles[i]; - tile.upload(context); - tile.prepare(this.map.style.imageManager); - } - } - - /** + loaded(): boolean { + if (this._sourceErrored) { return true; } + if (!this._sourceLoaded) { return false; } + if (!this._source.loaded()) { return false; } + for (const t in this._tiles) { + const tile = this._tiles[t]; + if (tile.state !== 'loaded' && tile.state !== 'errored') + return false; + } + return true; + } + + getSource(): Source { + return this._source; + } + + pause() { + this._paused = true; + } + + resume() { + if (!this._paused) return; + const shouldReload = this._shouldReloadOnResume; + this._paused = false; + this._shouldReloadOnResume = false; + if (shouldReload) this.reload(); + if (this.transform) this.update(this.transform); + } + + _loadTile(tile: Tile, callback: Callback): void { + tile.isSymbolTile = this._onlySymbols; + return this._source.loadTile(tile, callback); + } + + _unloadTile: ((tile: Tile) => void) = (tile: Tile): void => { + if (this._source.unloadTile) + return this._source.unloadTile(tile, () => {}); + } + + _abortTile(tile: Tile): void { + if (this._source.abortTile) + return this._source.abortTile(tile, () => {}); + } + + serialize(): SourceSpecification { + return this._source.serialize(); + } + + prepare(context: Context) { + if (this._source.prepare) { + this._source.prepare(); + } + + this._state.coalesceChanges(this._tiles, this.map ? this.map.painter : null); + + for (const i in this._tiles) { + const tile = this._tiles[i]; + tile.upload(context); + tile.prepare(this.map.style.imageManager); + } + } + + /** * Return all tile ids ordered with z-order, and cast to numbers * @private */ - getIds(): Array { - return values((this._tiles: any)).map((tile: Tile) => tile.tileID).sort( - compareTileId, - ).map(id => id.key); - } - - getRenderableIds(symbolLayer?: boolean): Array { - const renderables: Array = []; - for (const id in this._tiles) { - if (this._isIdRenderable(+id, symbolLayer)) - renderables.push(this._tiles[id]); - } - if (symbolLayer) { - return renderables.sort( - (a_: Tile, b_: Tile) => { - const a = a_.tileID; - const b = b_.tileID; - const rotatedA = new Point(a.canonical.x, a.canonical.y)._rotate( - this.transform.angle, - ); - const rotatedB = new Point(b.canonical.x, b.canonical.y)._rotate( - this.transform.angle, - ); - return ( - a.overscaledZ - b.overscaledZ || rotatedB.y - rotatedA.y || - rotatedB.x - rotatedA.x - ); - }, - ).map(tile => tile.tileID.key); - } - return renderables.map(tile => tile.tileID).sort(compareTileId).map( - id => id.key, - ); - } - - hasRenderableParent(tileID: OverscaledTileID): boolean { - const parentTile = this.findLoadedParent(tileID, 0); - if (parentTile) { - return this._isIdRenderable(parentTile.tileID.key); - } - return false; - } - - _isIdRenderable(id: number, symbolLayer?: boolean): boolean { - return ( - this._tiles[id] && this._tiles[id].hasData() && !this._coveredTiles[id] && - (symbolLayer || !this._tiles[id].holdingForFade()) - ); - } - - reload() { - if (this._paused) { - this._shouldReloadOnResume = true; - return; - } - - this._cache.reset(); - - for (const i in this._tiles) { - if (this._tiles[i].state !== "errored") this._reloadTile(+i, 'reloading'); - } - } - - _reloadTile(id: number, state: TileState) { - const tile = this._tiles[id]; - - // this potentially does not address all underlying - // issues https://github.com/mapbox/mapbox-gl-js/issues/4252 - // - hard to tell without repro steps - if (!tile) return; - - // The difference between "loading" tiles and "reloading" or "expired" - // tiles is that "reloading"/"expired" tiles are "renderable". - // Therefore, a "loading" tile cannot become a "reloading" tile without - // first becoming a "loaded" tile. - if (tile.state !== 'loading') { - tile.state = state; - } - - this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); - } - - _tileLoaded: ((tile: Tile, id: number, previousState: TileState, err: ?Error) => void) = (tile: Tile, id: number, previousState: TileState, err: ?Error) => { - if (err) { - tile.state = 'errored'; - if ((err: any).status !== 404) - this._source.fire(new ErrorEvent(err, {tile})); else { - // continue to try loading parent/children tiles if a tile doesn't exist (404) - const updateForTerrain = this._source.type === 'raster-dem' && - this.usedForTerrain; - if (updateForTerrain && this.map.painter.terrain) { - const terrain = this.map.painter.terrain; - this.update(this.transform, terrain.getScaledDemTileSize(), true); - terrain.resetTileLookupCache(this.id); - } else { - this.update(this.transform); - } - } - return; - } - - tile.timeAdded = browser.now(); - if (previousState === 'expired') tile.refreshedUponExpiration = true; - this._setTileReloadTimer(id, tile); - if (this._source.type === 'raster-dem' && tile.dem) this._backfillDEM(tile); - this._state.initializeTileState(tile, this.map ? this.map.painter : null); - - this._source.fire( - new Event( - 'data', - { - dataType: 'source', - tile, - coord: tile.tileID, - 'sourceCacheId': this.id, - }, - ), - ); - }; - - /** + getIds(): Array { + return values((this._tiles: any)).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); + } + + getRenderableIds(symbolLayer?: boolean): Array { + const renderables: Array = []; + for (const id in this._tiles) { + if (this._isIdRenderable(+id, symbolLayer)) renderables.push(this._tiles[id]); + } + if (symbolLayer) { + return renderables.sort((a_: Tile, b_: Tile) => { + const a = a_.tileID; + const b = b_.tileID; + const rotatedA = (new Point(a.canonical.x, a.canonical.y))._rotate(this.transform.angle); + const rotatedB = (new Point(b.canonical.x, b.canonical.y))._rotate(this.transform.angle); + return a.overscaledZ - b.overscaledZ || rotatedB.y - rotatedA.y || rotatedB.x - rotatedA.x; + }).map(tile => tile.tileID.key); + } + return renderables.map(tile => tile.tileID).sort(compareTileId).map(id => id.key); + } + + hasRenderableParent(tileID: OverscaledTileID): boolean { + const parentTile = this.findLoadedParent(tileID, 0); + if (parentTile) { + return this._isIdRenderable(parentTile.tileID.key); + } + return false; + } + + _isIdRenderable(id: number, symbolLayer?: boolean): boolean { + return this._tiles[id] && this._tiles[id].hasData() && + !this._coveredTiles[id] && (symbolLayer || !this._tiles[id].holdingForFade()); + } + + reload() { + if (this._paused) { + this._shouldReloadOnResume = true; + return; + } + + this._cache.reset(); + + for (const i in this._tiles) { + if (this._tiles[i].state !== "errored") this._reloadTile(+i, 'reloading'); + } + } + + _reloadTile(id: number, state: TileState) { + const tile = this._tiles[id]; + + // this potentially does not address all underlying + // issues https://github.com/mapbox/mapbox-gl-js/issues/4252 + // - hard to tell without repro steps + if (!tile) return; + + // The difference between "loading" tiles and "reloading" or "expired" + // tiles is that "reloading"/"expired" tiles are "renderable". + // Therefore, a "loading" tile cannot become a "reloading" tile without + // first becoming a "loaded" tile. + if (tile.state !== 'loading') { + tile.state = state; + } + + this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); + } + + _tileLoaded: ((tile: Tile, id: number, previousState: TileState, err: ?Error) => void) = (tile: Tile, id: number, previousState: TileState, err: ?Error) => { + if (err) { + tile.state = 'errored'; + if ((err: any).status !== 404) this._source.fire(new ErrorEvent(err, {tile})); + else { + // continue to try loading parent/children tiles if a tile doesn't exist (404) + const updateForTerrain = this._source.type === 'raster-dem' && this.usedForTerrain; + if (updateForTerrain && this.map.painter.terrain) { + const terrain = this.map.painter.terrain; + this.update(this.transform, terrain.getScaledDemTileSize(), true); + terrain.resetTileLookupCache(this.id); + } else { + this.update(this.transform); + } + } + return; + } + + tile.timeAdded = browser.now(); + if (previousState === 'expired') tile.refreshedUponExpiration = true; + this._setTileReloadTimer(id, tile); + if (this._source.type === 'raster-dem' && tile.dem) this._backfillDEM(tile); + this._state.initializeTileState(tile, this.map ? this.map.painter : null); + + this._source.fire(new Event('data', {dataType: 'source', tile, coord: tile.tileID, 'sourceCacheId': this.id})); + } + + /** * For raster terrain source, backfill DEM to eliminate visible tile boundaries * @private */ - _backfillDEM(tile: Tile) { - const renderables = this.getRenderableIds(); - for (let i = 0; i < renderables.length; i++) { - const borderId = renderables[i]; - if (tile.neighboringTiles && tile.neighboringTiles[borderId]) { - const borderTile = this.getTileByID(borderId); - fillBorder(tile, borderTile); - fillBorder(borderTile, tile); - } - } - - function fillBorder(tile, borderTile) { - if (!tile.dem || tile.dem.borderReady) return; - tile.needsHillshadePrepare = true; - tile.needsDEMTextureUpload = true; - let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x; - const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y; - const dim = Math.pow(2, tile.tileID.canonical.z); - const borderId = borderTile.tileID.key; - if (dx === 0 && dy === 0) return; - - if (Math.abs(dy) > 1) { - return; - } - if (Math.abs(dx) > 1) { - // Adjust the delta coordinate for world wraparound. - if (Math.abs(dx + dim) === 1) { - dx += dim; - } else if (Math.abs(dx - dim) === 1) { - dx -= dim; - } - } - if (!borderTile.dem || !tile.dem) return; - tile.dem.backfillBorder(borderTile.dem, dx, dy); - if (tile.neighboringTiles && tile.neighboringTiles[borderId]) - tile.neighboringTiles[borderId].backfilled = true; - } - } - /** + _backfillDEM(tile: Tile) { + const renderables = this.getRenderableIds(); + for (let i = 0; i < renderables.length; i++) { + const borderId = renderables[i]; + if (tile.neighboringTiles && tile.neighboringTiles[borderId]) { + const borderTile = this.getTileByID(borderId); + fillBorder(tile, borderTile); + fillBorder(borderTile, tile); + } + } + + function fillBorder(tile, borderTile) { + if (!tile.dem || tile.dem.borderReady) return; + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x; + const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y; + const dim = Math.pow(2, tile.tileID.canonical.z); + const borderId = borderTile.tileID.key; + if (dx === 0 && dy === 0) return; + + if (Math.abs(dy) > 1) { + return; + } + if (Math.abs(dx) > 1) { + // Adjust the delta coordinate for world wraparound. + if (Math.abs(dx + dim) === 1) { + dx += dim; + } else if (Math.abs(dx - dim) === 1) { + dx -= dim; + } + } + if (!borderTile.dem || !tile.dem) return; + tile.dem.backfillBorder(borderTile.dem, dx, dy); + if (tile.neighboringTiles && tile.neighboringTiles[borderId]) + tile.neighboringTiles[borderId].backfilled = true; + } + } + /** * Get a specific tile by TileID * @private */ - getTile(tileID: OverscaledTileID): Tile { - return this.getTileByID(tileID.key); - } + getTile(tileID: OverscaledTileID): Tile { + return this.getTileByID(tileID.key); + } - /** + /** * Get a specific tile by id * @private */ - getTileByID(id: number): Tile { - return this._tiles[id]; - } + getTileByID(id: number): Tile { + return this._tiles[id]; + } - /** + /** * For a given set of tiles, retain children that are loaded and have a zoom * between `zoom` (exclusive) and `maxCoveringZoom` (inclusive) * @private */ - _retainLoadedChildren( - idealTiles: { [number | string]: OverscaledTileID }, - zoom: number, - maxCoveringZoom: number, - retain: { [number | string]: OverscaledTileID }, - ) { - for (const id in this._tiles) { - let tile = this._tiles[id]; - - // only consider renderable tiles up to maxCoveringZoom - if ( - retain[id] || !tile.hasData() || tile.tileID.overscaledZ <= zoom || - tile.tileID.overscaledZ > maxCoveringZoom - ) - continue; - - // loop through parents and retain the topmost loaded one if found - let topmostLoadedID = tile.tileID; - while (tile && tile.tileID.overscaledZ > zoom + 1) { - const parentID = tile.tileID.scaledTo(tile.tileID.overscaledZ - 1); - - tile = this._tiles[parentID.key]; - - if (tile && tile.hasData()) { - topmostLoadedID = parentID; - } - } - - // loop through ancestors of the topmost loaded child to see if there's one that needed it - let tileID = topmostLoadedID; - while (tileID.overscaledZ > zoom) { - tileID = tileID.scaledTo(tileID.overscaledZ - 1); - - if (idealTiles[tileID.key]) { - // found a parent that needed a loaded child; retain that child - retain[topmostLoadedID.key] = topmostLoadedID; - break; - } - } - } - } - - /** + _retainLoadedChildren( + idealTiles: {[number | string]: OverscaledTileID}, + zoom: number, + maxCoveringZoom: number, + retain: {[number | string]: OverscaledTileID} + ) { + for (const id in this._tiles) { + let tile = this._tiles[id]; + + // only consider renderable tiles up to maxCoveringZoom + if (retain[id] || + !tile.hasData() || + tile.tileID.overscaledZ <= zoom || + tile.tileID.overscaledZ > maxCoveringZoom + ) continue; + + // loop through parents and retain the topmost loaded one if found + let topmostLoadedID = tile.tileID; + while (tile && tile.tileID.overscaledZ > zoom + 1) { + const parentID = tile.tileID.scaledTo(tile.tileID.overscaledZ - 1); + + tile = this._tiles[parentID.key]; + + if (tile && tile.hasData()) { + topmostLoadedID = parentID; + } + } + + // loop through ancestors of the topmost loaded child to see if there's one that needed it + let tileID = topmostLoadedID; + while (tileID.overscaledZ > zoom) { + tileID = tileID.scaledTo(tileID.overscaledZ - 1); + + if (idealTiles[tileID.key]) { + // found a parent that needed a loaded child; retain that child + retain[topmostLoadedID.key] = topmostLoadedID; + break; + } + } + } + } + + /** * Find a loaded parent of the given tile (up to minCoveringZoom) * @private */ - findLoadedParent(tileID: OverscaledTileID, minCoveringZoom: number): ?Tile { - if (tileID.key in this._loadedParentTiles) { - const parent = this._loadedParentTiles[tileID.key]; - if (parent && parent.tileID.overscaledZ >= minCoveringZoom) { - return parent; - } else { - return null; - } - } - for (let z = tileID.overscaledZ - 1; z >= minCoveringZoom; z--) { - const parentTileID = tileID.scaledTo(z); - const tile = this._getLoadedTile(parentTileID); - if (tile) { - return tile; - } - } - } - - _getLoadedTile(tileID: OverscaledTileID): ?Tile { - const tile = this._tiles[tileID.key]; - if (tile && tile.hasData()) { - return tile; - } - // TileCache ignores wrap in lookup. - const cachedTile = this._cache.getByKey( - this._source.reparseOverscaled ? - tileID.wrapped().key : - tileID.canonical.key, - ); - return cachedTile; - } - - /** + findLoadedParent(tileID: OverscaledTileID, minCoveringZoom: number): ?Tile { + if (tileID.key in this._loadedParentTiles) { + const parent = this._loadedParentTiles[tileID.key]; + if (parent && parent.tileID.overscaledZ >= minCoveringZoom) { + return parent; + } else { + return null; + } + } + for (let z = tileID.overscaledZ - 1; z >= minCoveringZoom; z--) { + const parentTileID = tileID.scaledTo(z); + const tile = this._getLoadedTile(parentTileID); + if (tile) { + return tile; + } + } + } + + _getLoadedTile(tileID: OverscaledTileID): ?Tile { + const tile = this._tiles[tileID.key]; + if (tile && tile.hasData()) { + return tile; + } + // TileCache ignores wrap in lookup. + const cachedTile = this._cache.getByKey(this._source.reparseOverscaled ? tileID.wrapped().key : tileID.canonical.key); + return cachedTile; + } + + /** * Resizes the tile cache based on the current viewport's size * or the minTileCacheSize and maxTileCacheSize options passed during map creation * @@ -470,68 +421,64 @@ class SourceCache * the map is more important. * @private */ - updateCacheSize(transform: Transform, tileSize?: number) { - tileSize = tileSize || this._source.tileSize; - const widthInTiles = Math.ceil(transform.width / tileSize) + 1; - const heightInTiles = Math.ceil(transform.height / tileSize) + 1; - const approxTilesInView = widthInTiles * heightInTiles; - const commonZoomRange = 5; - - const viewDependentMaxSize = Math.floor(approxTilesInView * commonZoomRange); - const minSize = typeof this._minTileCacheSize === 'number' ? - Math.max(this._minTileCacheSize, viewDependentMaxSize) : - viewDependentMaxSize; - const maxSize = typeof this._maxTileCacheSize === 'number' ? - Math.min(this._maxTileCacheSize, minSize) : - minSize; - - this._cache.setMaxSize(maxSize); - } - - handleWrapJump(lng: number) { - // On top of the regular z/x/y values, TileIDs have a `wrap` value that specify - // which copy of the world the tile belongs to. For example, at `lng: 10` you - // might render z/x/y/0 while at `lng: 370` you would render z/x/y/1. - // - // When lng values get wrapped (going from `lng: 370` to `long: 10`) you expect - // to see the same thing on the screen (370 degrees and 10 degrees is the same - // place in the world) but all the TileIDs will have different wrap values. - // - // In order to make this transition seamless, we calculate the rounded difference of - // "worlds" between the last frame and the current frame. If the map panned by - // a world, then we can assign all the tiles new TileIDs with updated wrap values. - // For example, assign z/x/y/1 a new id: z/x/y/0. It is the same tile, just rendered - // in a different position. - // - // This enables us to reuse the tiles at more ideal locations and prevent flickering. - const prevLng = this._prevLng === undefined ? lng : this._prevLng; - const lngDifference = lng - prevLng; - const worldDifference = lngDifference / 360; - const wrapDelta = Math.round(worldDifference); - this._prevLng = lng; - - if (wrapDelta) { - const tiles: { [_: string | number]: Tile } = {}; - for (const key in this._tiles) { - const tile = this._tiles[key]; - tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); - tiles[tile.tileID.key] = tile; - } - this._tiles = tiles; - - // Reset tile reload timers - for (const id in this._timers) { - clearTimeout(this._timers[id]); - delete this._timers[id]; - } - for (const id in this._tiles) { - const tile = this._tiles[id]; - this._setTileReloadTimer(+id, tile); - } - } - } - - /** + updateCacheSize(transform: Transform, tileSize?: number) { + tileSize = tileSize || this._source.tileSize; + const widthInTiles = Math.ceil(transform.width / tileSize) + 1; + const heightInTiles = Math.ceil(transform.height / tileSize) + 1; + const approxTilesInView = widthInTiles * heightInTiles; + const commonZoomRange = 5; + + const viewDependentMaxSize = Math.floor(approxTilesInView * commonZoomRange); + const minSize = typeof this._minTileCacheSize === 'number' ? Math.max(this._minTileCacheSize, viewDependentMaxSize) : viewDependentMaxSize; + const maxSize = typeof this._maxTileCacheSize === 'number' ? Math.min(this._maxTileCacheSize, minSize) : minSize; + + this._cache.setMaxSize(maxSize); + } + + handleWrapJump(lng: number) { + // On top of the regular z/x/y values, TileIDs have a `wrap` value that specify + // which copy of the world the tile belongs to. For example, at `lng: 10` you + // might render z/x/y/0 while at `lng: 370` you would render z/x/y/1. + // + // When lng values get wrapped (going from `lng: 370` to `long: 10`) you expect + // to see the same thing on the screen (370 degrees and 10 degrees is the same + // place in the world) but all the TileIDs will have different wrap values. + // + // In order to make this transition seamless, we calculate the rounded difference of + // "worlds" between the last frame and the current frame. If the map panned by + // a world, then we can assign all the tiles new TileIDs with updated wrap values. + // For example, assign z/x/y/1 a new id: z/x/y/0. It is the same tile, just rendered + // in a different position. + // + // This enables us to reuse the tiles at more ideal locations and prevent flickering. + const prevLng = this._prevLng === undefined ? lng : this._prevLng; + const lngDifference = lng - prevLng; + const worldDifference = lngDifference / 360; + const wrapDelta = Math.round(worldDifference); + this._prevLng = lng; + + if (wrapDelta) { + const tiles: {[_: string | number]: Tile} = {}; + for (const key in this._tiles) { + const tile = this._tiles[key]; + tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); + tiles[tile.tileID.key] = tile; + } + this._tiles = tiles; + + // Reset tile reload timers + for (const id in this._timers) { + clearTimeout(this._timers[id]); + delete this._timers[id]; + } + for (const id in this._tiles) { + const tile = this._tiles[id]; + this._setTileReloadTimer(+id, tile); + } + } + } + + /** * Removes tiles that are outside the viewport and adds new tiles that * are inside the viewport. * @private @@ -540,418 +487,358 @@ class SourceCache * @param {tileSize} tileSize If needed to get lower resolution ideal cover, * override source.tileSize used in tile cover calculation. */ - update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { - this.transform = transform; - if ( - !this._sourceLoaded || this._paused || this.transform.freezeTileCoverage - ) { - return; - } - assert(!(updateForTerrain && !this.usedForTerrain)); - if (this.usedForTerrain && !updateForTerrain) { - // If source is used for both terrain and hillshade, don't update it twice. - return; - } - - this.updateCacheSize(transform, tileSize); - if (this.transform.projection.name !== 'globe') { - this.handleWrapJump(this.transform.center.lng); - } - - // Covered is a list of retained tiles who's areas are fully covered by other, - // better, retained tiles. They are not drawn separately. - this._coveredTiles = {}; - - let idealTileIDs; - if (!this.used && !this.usedForTerrain) { - idealTileIDs = []; - } else if (this._source.tileID) { - idealTileIDs = transform.getVisibleUnwrappedCoordinates( - this._source.tileID, - ).map( - unwrapped => new OverscaledTileID( - unwrapped.canonical.z, - unwrapped.wrap, - unwrapped.canonical.z, - unwrapped.canonical.x, - unwrapped.canonical.y, - ), - ); - } else { - idealTileIDs = transform.coveringTiles( - { - tileSize: tileSize || this._source.tileSize, - minzoom: this._source.minzoom, - maxzoom: this._source.maxzoom, - roundZoom: this._source.roundZoom && !updateForTerrain, - reparseOverscaled: this._source.reparseOverscaled, - isTerrainDEM: this.usedForTerrain, - }, - ); - - if (this._source.hasTile) { - idealTileIDs = idealTileIDs.filter( - coord => (this._source.hasTile: any)(coord), - ); - } - } - - // Retain is a list of tiles that we shouldn't delete, even if they are not - // the most ideal tile for the current viewport. This may include tiles like - // parent or child tiles that are *already* loaded. - const retain = this._updateRetainedTiles(idealTileIDs); - - if (isRasterType(this._source.type) && idealTileIDs.length !== 0) { - const parentsForFading: { [_: string | number]: OverscaledTileID } = {}; - const fadingTiles = {}; - const ids = Object.keys(retain); - for (const id of ids) { - const tileID = retain[id]; - assert(tileID.key === +id); - - const tile = this._tiles[id]; - if (!tile || (tile.fadeEndTime && tile.fadeEndTime <= browser.now())) - continue; - - // if the tile is loaded but still fading in, find parents to cross-fade with it - const parentTile = this.findLoadedParent( - tileID, - Math.max( - tileID.overscaledZ - SourceCache.maxOverzooming, - this._source.minzoom, - ), - ); - if (parentTile) { - this._addTile(parentTile.tileID); - parentsForFading[parentTile.tileID.key] = parentTile.tileID; - } - - fadingTiles[id] = tileID; - } - - // for children tiles with parent tiles still fading in, - // retain the children so the parent can fade on top - const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; - for (const id in this._tiles) { - const childTile = this._tiles[id]; - if (retain[id] || !childTile.hasData()) { - continue; - } - - let parentID = childTile.tileID; - while (parentID.overscaledZ > minZoom) { - parentID = parentID.scaledTo(parentID.overscaledZ - 1); - const tile = this._tiles[parentID.key]; - if (tile && tile.hasData() && fadingTiles[parentID.key]) { - retain[id] = childTile.tileID; - break; - } - } - } - - for (const id in parentsForFading) { - if (!retain[id]) { - // If a tile is only needed for fading, mark it as covered so that it isn't rendered on it's own. - this._coveredTiles[id] = true; - retain[id] = parentsForFading[id]; - } - } - } - - for (const retainedId in retain) { - // Make sure retained tiles always clear any existing fade holds - // so that if they're removed again their fade timer starts fresh. - this._tiles[retainedId].clearFadeHold(); - } - - // Remove the tiles we don't need anymore. - const remove = keysDifference((this._tiles: any), (retain: any)); - for (const tileID of remove) { - const tile = this._tiles[tileID]; - if (tile.hasSymbolBuckets && !tile.holdingForFade()) { - tile.setHoldDuration(this.map._fadeDuration); - } else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) { - this._removeTile(+tileID); - } - } - - // Construct a cache of loaded parents - this._updateLoadedParentTileCache(); - - if (this._onlySymbols && this._source.afterUpdate) { - this._source.afterUpdate(); - } - } - - releaseSymbolFadeTiles() { - for (const id in this._tiles) { - if (this._tiles[id].holdingForFade()) { - this._removeTile(+id); - } - } - } - - _updateRetainedTiles( - idealTileIDs: Array, - ): { [_: number | string]: OverscaledTileID } { - const retain: { [_: number | string]: OverscaledTileID } = {}; - if (idealTileIDs.length === 0) { - return retain; - } - - const checked: { [_: number | string]: boolean } = {}; - const minZoom = idealTileIDs.reduce( - (min, id) => Math.min(min, id.overscaledZ), - Infinity, - ); - const maxZoom = idealTileIDs[0].overscaledZ; - assert(minZoom <= maxZoom); - const minCoveringZoom = Math.max( - maxZoom - SourceCache.maxOverzooming, - this._source.minzoom, - ); - const maxCoveringZoom = Math.max( - maxZoom + SourceCache.maxUnderzooming, - this._source.minzoom, - ); - - const missingTiles = {}; - for (const tileID of idealTileIDs) { - const tile = this._addTile(tileID); - - // retain the tile even if it's not loaded because it's an ideal tile. - retain[tileID.key] = tileID; - - if (tile.hasData()) continue; - - if (minZoom < this._source.maxzoom) { - // save missing tiles that potentially have loaded children - missingTiles[tileID.key] = tileID; - } - } - - // retain any loaded children of ideal tiles up to maxCoveringZoom - this._retainLoadedChildren(missingTiles, minZoom, maxCoveringZoom, retain); - - for (const tileID of idealTileIDs) { - let tile = this._tiles[tileID.key]; - - if (tile.hasData()) continue; - - // The tile we require is not yet loaded or does not exist; - // Attempt to find children that fully cover it. - - if (tileID.canonical.z >= this._source.maxzoom) { - // We're looking for an overzoomed child tile. - const childCoord = tileID.children(this._source.maxzoom)[0]; - const childTile = this.getTile(childCoord); - if (!!childTile && childTile.hasData()) { - retain[childCoord.key] = childCoord; - continue; // tile is covered by overzoomed child - - } - } else { - // Check if all 4 immediate children are loaded (in other words, the missing ideal tile is covered) - const children = tileID.children(this._source.maxzoom); - - if ( - retain[children[0].key] && retain[children[1].key] && - retain[children[2].key] && - retain[children[3].key] - ) - continue; // tile is covered by children - - } - - // We couldn't find child tiles that entirely cover the ideal tile; look for parents now. - - // As we ascend up the tile pyramid of the ideal tile, we check whether the parent - // tile has been previously requested (and errored because we only loop over tiles with no data) - // in order to determine if we need to request its parent. - let parentWasRequested = tile.wasRequested(); - - for ( - let overscaledZ = tileID.overscaledZ - 1; - overscaledZ >= minCoveringZoom; - --overscaledZ - ) { - const parentId = tileID.scaledTo(overscaledZ); - - // Break parent tile ascent if this route has been previously checked by another child. - if (checked[parentId.key]) break; - checked[parentId.key] = true; - - tile = this.getTile(parentId); - if (!tile && parentWasRequested) { - tile = this._addTile(parentId); - } - if (tile) { - retain[parentId.key] = parentId; - // Save the current values, since they're the parent of the next iteration - // of the parent tile ascent loop. - parentWasRequested = tile.wasRequested(); - if (tile.hasData()) break; - } - } - } - - return retain; - } - - _updateLoadedParentTileCache() { - this._loadedParentTiles = {}; - - for (const tileKey in this._tiles) { - const path = []; - let parentTile: ?Tile; - let currentId = this._tiles[tileKey].tileID; - - // Find the closest loaded ancestor by traversing the tile tree towards the root and - // caching results along the way - while (currentId.overscaledZ > 0) { - // Do we have a cached result from previous traversals? - if (currentId.key in this._loadedParentTiles) { - parentTile = this._loadedParentTiles[currentId.key]; - break; - } - - path.push(currentId.key); - - // Is the parent loaded? - const parentId = currentId.scaledTo(currentId.overscaledZ - 1); - parentTile = this._getLoadedTile(parentId); - if (parentTile) { - break; - } - - currentId = parentId; - } - - // Cache the result of this traversal to all newly visited tiles - for (const key of path) { - this._loadedParentTiles[key] = parentTile; - } - } - } - - /** + update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { + this.transform = transform; + if (!this._sourceLoaded || this._paused || this.transform.freezeTileCoverage) { return; } + assert(!(updateForTerrain && !this.usedForTerrain)); + if (this.usedForTerrain && !updateForTerrain) { + // If source is used for both terrain and hillshade, don't update it twice. + return; + } + + this.updateCacheSize(transform, tileSize); + if (this.transform.projection.name !== 'globe') { + this.handleWrapJump(this.transform.center.lng); + } + + // Covered is a list of retained tiles who's areas are fully covered by other, + // better, retained tiles. They are not drawn separately. + this._coveredTiles = {}; + + let idealTileIDs; + if (!this.used && !this.usedForTerrain) { + idealTileIDs = []; + } else if (this._source.tileID) { + idealTileIDs = transform.getVisibleUnwrappedCoordinates(this._source.tileID) + .map((unwrapped) => new OverscaledTileID(unwrapped.canonical.z, unwrapped.wrap, unwrapped.canonical.z, unwrapped.canonical.x, unwrapped.canonical.y)); + } else { + idealTileIDs = transform.coveringTiles({ + tileSize: tileSize || this._source.tileSize, + minzoom: this._source.minzoom, + maxzoom: this._source.maxzoom, + roundZoom: this._source.roundZoom && !updateForTerrain, + reparseOverscaled: this._source.reparseOverscaled, + isTerrainDEM: this.usedForTerrain + }); + + if (this._source.hasTile) { + idealTileIDs = idealTileIDs.filter((coord) => (this._source.hasTile: any)(coord)); + } + } + + // Retain is a list of tiles that we shouldn't delete, even if they are not + // the most ideal tile for the current viewport. This may include tiles like + // parent or child tiles that are *already* loaded. + const retain = this._updateRetainedTiles(idealTileIDs); + + if (isRasterType(this._source.type) && idealTileIDs.length !== 0) { + const parentsForFading: {[_: string | number]: OverscaledTileID} = {}; + const fadingTiles = {}; + const ids = Object.keys(retain); + for (const id of ids) { + const tileID = retain[id]; + assert(tileID.key === +id); + + const tile = this._tiles[id]; + if (!tile || (tile.fadeEndTime && tile.fadeEndTime <= browser.now())) continue; + + // if the tile is loaded but still fading in, find parents to cross-fade with it + const parentTile = this.findLoadedParent(tileID, Math.max(tileID.overscaledZ - SourceCache.maxOverzooming, this._source.minzoom)); + if (parentTile) { + this._addTile(parentTile.tileID); + parentsForFading[parentTile.tileID.key] = parentTile.tileID; + } + + fadingTiles[id] = tileID; + } + + // for children tiles with parent tiles still fading in, + // retain the children so the parent can fade on top + const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; + for (const id in this._tiles) { + const childTile = this._tiles[id]; + if (retain[id] || !childTile.hasData()) { + continue; + } + + let parentID = childTile.tileID; + while (parentID.overscaledZ > minZoom) { + parentID = parentID.scaledTo(parentID.overscaledZ - 1); + const tile = this._tiles[parentID.key]; + if (tile && tile.hasData() && fadingTiles[parentID.key]) { + retain[id] = childTile.tileID; + break; + } + } + } + + for (const id in parentsForFading) { + if (!retain[id]) { + // If a tile is only needed for fading, mark it as covered so that it isn't rendered on it's own. + this._coveredTiles[id] = true; + retain[id] = parentsForFading[id]; + } + } + } + + for (const retainedId in retain) { + // Make sure retained tiles always clear any existing fade holds + // so that if they're removed again their fade timer starts fresh. + this._tiles[retainedId].clearFadeHold(); + } + + // Remove the tiles we don't need anymore. + const remove = keysDifference((this._tiles: any), (retain: any)); + for (const tileID of remove) { + const tile = this._tiles[tileID]; + if (tile.hasSymbolBuckets && !tile.holdingForFade()) { + tile.setHoldDuration(this.map._fadeDuration); + } else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) { + this._removeTile(+tileID); + } + } + + // Construct a cache of loaded parents + this._updateLoadedParentTileCache(); + + if (this._onlySymbols && this._source.afterUpdate) { + this._source.afterUpdate(); + } + } + + releaseSymbolFadeTiles() { + for (const id in this._tiles) { + if (this._tiles[id].holdingForFade()) { + this._removeTile(+id); + } + } + } + + _updateRetainedTiles(idealTileIDs: Array): {[_: number | string]: OverscaledTileID} { + const retain: {[_: number | string]: OverscaledTileID} = {}; + if (idealTileIDs.length === 0) { return retain; } + + const checked: {[_: number | string]: boolean } = {}; + const minZoom = idealTileIDs.reduce((min, id) => Math.min(min, id.overscaledZ), Infinity); + const maxZoom = idealTileIDs[0].overscaledZ; + assert(minZoom <= maxZoom); + const minCoveringZoom = Math.max(maxZoom - SourceCache.maxOverzooming, this._source.minzoom); + const maxCoveringZoom = Math.max(maxZoom + SourceCache.maxUnderzooming, this._source.minzoom); + + const missingTiles = {}; + for (const tileID of idealTileIDs) { + const tile = this._addTile(tileID); + + // retain the tile even if it's not loaded because it's an ideal tile. + retain[tileID.key] = tileID; + + if (tile.hasData()) continue; + + if (minZoom < this._source.maxzoom) { + // save missing tiles that potentially have loaded children + missingTiles[tileID.key] = tileID; + } + } + + // retain any loaded children of ideal tiles up to maxCoveringZoom + this._retainLoadedChildren(missingTiles, minZoom, maxCoveringZoom, retain); + + for (const tileID of idealTileIDs) { + let tile = this._tiles[tileID.key]; + + if (tile.hasData()) continue; + + // The tile we require is not yet loaded or does not exist; + // Attempt to find children that fully cover it. + + if (tileID.canonical.z >= this._source.maxzoom) { + // We're looking for an overzoomed child tile. + const childCoord = tileID.children(this._source.maxzoom)[0]; + const childTile = this.getTile(childCoord); + if (!!childTile && childTile.hasData()) { + retain[childCoord.key] = childCoord; + continue; // tile is covered by overzoomed child + } + } else { + // Check if all 4 immediate children are loaded (in other words, the missing ideal tile is covered) + const children = tileID.children(this._source.maxzoom); + + if (retain[children[0].key] && + retain[children[1].key] && + retain[children[2].key] && + retain[children[3].key]) continue; // tile is covered by children + } + + // We couldn't find child tiles that entirely cover the ideal tile; look for parents now. + + // As we ascend up the tile pyramid of the ideal tile, we check whether the parent + // tile has been previously requested (and errored because we only loop over tiles with no data) + // in order to determine if we need to request its parent. + let parentWasRequested = tile.wasRequested(); + + for (let overscaledZ = tileID.overscaledZ - 1; overscaledZ >= minCoveringZoom; --overscaledZ) { + const parentId = tileID.scaledTo(overscaledZ); + + // Break parent tile ascent if this route has been previously checked by another child. + if (checked[parentId.key]) break; + checked[parentId.key] = true; + + tile = this.getTile(parentId); + if (!tile && parentWasRequested) { + tile = this._addTile(parentId); + } + if (tile) { + retain[parentId.key] = parentId; + // Save the current values, since they're the parent of the next iteration + // of the parent tile ascent loop. + parentWasRequested = tile.wasRequested(); + if (tile.hasData()) break; + } + } + } + + return retain; + } + + _updateLoadedParentTileCache() { + this._loadedParentTiles = {}; + + for (const tileKey in this._tiles) { + const path = []; + let parentTile: ?Tile; + let currentId = this._tiles[tileKey].tileID; + + // Find the closest loaded ancestor by traversing the tile tree towards the root and + // caching results along the way + while (currentId.overscaledZ > 0) { + + // Do we have a cached result from previous traversals? + if (currentId.key in this._loadedParentTiles) { + parentTile = this._loadedParentTiles[currentId.key]; + break; + } + + path.push(currentId.key); + + // Is the parent loaded? + const parentId = currentId.scaledTo(currentId.overscaledZ - 1); + parentTile = this._getLoadedTile(parentId); + if (parentTile) { + break; + } + + currentId = parentId; + } + + // Cache the result of this traversal to all newly visited tiles + for (const key of path) { + this._loadedParentTiles[key] = parentTile; + } + } + } + + /** * Add a tile, given its coordinate, to the pyramid. * @private */ - _addTile(tileID: OverscaledTileID): Tile { - let tile = this._tiles[tileID.key]; - if (tile) return tile; - - tile = this._cache.getAndRemove(tileID); - if (tile) { - this._setTileReloadTimer(tileID.key, tile); - // set the tileID because the cached tile could have had a different wrap value - tile.tileID = tileID; - this._state.initializeTileState(tile, this.map ? this.map.painter : null); - if (this._cacheTimers[tileID.key]) { - clearTimeout(this._cacheTimers[tileID.key]); - delete this._cacheTimers[tileID.key]; - this._setTileReloadTimer(tileID.key, tile); - } - } - - const cached = Boolean(tile); - if (!cached) { - const painter = this.map ? this.map.painter : null; - tile = new Tile( - tileID, - this._source.tileSize * tileID.overscaleFactor(), - this.transform.tileZoom, - painter, - this._isRaster, - ); - this._loadTile( - tile, - this._tileLoaded.bind(this, tile, tileID.key, tile.state), - ); - } - - // Impossible, but silence flow. - if (!tile) return (null: any); - - tile.uses++; - this._tiles[tileID.key] = tile; - if (!cached) - this._source.fire( - new Event( - 'dataloading', - {tile, coord: tile.tileID, dataType: 'source'}, - ), - ); - - return tile; - } - - _setTileReloadTimer(id: number, tile: Tile) { - if (id in this._timers) { - clearTimeout(this._timers[id]); - delete this._timers[id]; - } - - const expiryTimeout = tile.getExpiryTimeout(); - if (expiryTimeout) { - this._timers[id] = setTimeout( - () => { - this._reloadTile(id, 'expired'); + _addTile(tileID: OverscaledTileID): Tile { + let tile = this._tiles[tileID.key]; + if (tile) return tile; + + tile = this._cache.getAndRemove(tileID); + if (tile) { + this._setTileReloadTimer(tileID.key, tile); + // set the tileID because the cached tile could have had a different wrap value + tile.tileID = tileID; + this._state.initializeTileState(tile, this.map ? this.map.painter : null); + if (this._cacheTimers[tileID.key]) { + clearTimeout(this._cacheTimers[tileID.key]); + delete this._cacheTimers[tileID.key]; + this._setTileReloadTimer(tileID.key, tile); + } + } + + const cached = Boolean(tile); + if (!cached) { + const painter = this.map ? this.map.painter : null; + tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, painter, this._isRaster); + this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state)); + } + + // Impossible, but silence flow. + if (!tile) return (null: any); + + tile.uses++; + this._tiles[tileID.key] = tile; + if (!cached) this._source.fire(new Event('dataloading', {tile, coord: tile.tileID, dataType: 'source'})); + + return tile; + } + + _setTileReloadTimer(id: number, tile: Tile) { + if (id in this._timers) { + clearTimeout(this._timers[id]); delete this._timers[id]; - }, - expiryTimeout, - ); - } - } - - /** + } + + const expiryTimeout = tile.getExpiryTimeout(); + if (expiryTimeout) { + this._timers[id] = setTimeout(() => { + this._reloadTile(id, 'expired'); + delete this._timers[id]; + }, expiryTimeout); + } + } + + /** * Remove a tile, given its id, from the pyramid * @private */ - _removeTile(id: number) { - const tile = this._tiles[id]; - if (!tile) return; - - tile.uses--; - delete this._tiles[id]; - if (this._timers[id]) { - clearTimeout(this._timers[id]); - delete this._timers[id]; - } - - if (tile.uses > 0) return; - - if (tile.hasData() && tile.state !== 'reloading') { - this._cache.add(tile.tileID, tile, tile.getExpiryTimeout()); - } else { - tile.aborted = true; - this._abortTile(tile); - this._unloadTile(tile); - } - } - - /** + _removeTile(id: number) { + const tile = this._tiles[id]; + if (!tile) + return; + + tile.uses--; + delete this._tiles[id]; + if (this._timers[id]) { + clearTimeout(this._timers[id]); + delete this._timers[id]; + } + + if (tile.uses > 0) + return; + + if (tile.hasData() && tile.state !== 'reloading') { + this._cache.add(tile.tileID, tile, tile.getExpiryTimeout()); + } else { + tile.aborted = true; + this._abortTile(tile); + this._unloadTile(tile); + } + } + + /** * Remove all tiles from this pyramid. * @private */ - clearTiles() { - this._shouldReloadOnResume = false; - this._paused = false; + clearTiles() { + this._shouldReloadOnResume = false; + this._paused = false; - for (const id in this._tiles) - this._removeTile(+id); + for (const id in this._tiles) + this._removeTile(+id); - if (this._source._clear) this._source._clear(); + if (this._source._clear) this._source._clear(); - this._cache.reset(); + this._cache.reset(); - if (this.map && this.usedForTerrain && this.map.painter.terrain) { - this.map.painter.terrain.resetTileLookupCache(this.id); - } - } + if (this.map && this.usedForTerrain && this.map.painter.terrain) { + this.map.painter.terrain.resetTileLookupCache(this.id); + } + } - /** + /** * Search through our current tiles and attempt to find the tiles that cover the given `queryGeometry`. * * @param {QueryGeometry} queryGeometry @@ -960,232 +847,185 @@ class SourceCache * @returns * @private */ - tilesIn( - queryGeometry: QueryGeometry, - use3DQuery: boolean, - visualizeQueryGeometry: boolean, - ): Array { - const tileResults = []; - - const transform = this.transform; - if (!transform) return tileResults; - - const isGlobe = transform.projection.name === 'globe'; - const centerX = mercatorXfromLng(transform.center.lng); - - for (const tileID in this._tiles) { - const tile = this._tiles[tileID]; - if (visualizeQueryGeometry) { - tile.clearQueryDebugViz(); - } - if (tile.holdingForFade()) { - // Tiles held for fading are covered by tiles that are closer to ideal - continue; - } - - // An array of wrap values for the tile [-1, 0, 1]. The default value is 0 but -1 or 1 wrapping - // might be required in globe view due to globe's surface being continuous. - let tilesToCheck; - - if (isGlobe) { - // Compare distances to copies of the tile to see if a wrapped one should be used. - const id = tile.tileID.canonical; - assert(tile.tileID.wrap === 0); - - if (id.z === 0) { - // Render the zoom level 0 tile twice as the query polygon might span over the antimeridian - const distances = [ - Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), - Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX), - ]; - - tilesToCheck = [0, distances.indexOf(Math.min(...distances)) * 2 - 1]; - } else { - const distances = [ - Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), - Math.abs(clamp(centerX, ...tileBoundsX(id, 0)) - centerX), - Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX), - ]; - - tilesToCheck = [distances.indexOf(Math.min(...distances)) - 1]; - } - } else { - tilesToCheck = [0]; - } - - for (const wrap of tilesToCheck) { - const tileResult = queryGeometry.containsTile( - tile, - transform, - use3DQuery, - wrap, - ); - if (tileResult) { - tileResults.push(tileResult); - } - } - } - return tileResults; - } - - getVisibleCoordinates(symbolLayer?: boolean): Array { - const coords = this.getRenderableIds(symbolLayer).map( - id => this._tiles[id].tileID, - ); - for (const coord of coords) { - coord.projMatrix = this.transform.calculateProjMatrix(coord.toUnwrapped()); - } - return coords; - } - - hasTransition(): boolean { - if (this._source.hasTransition()) { - return true; - } - - if (isRasterType(this._source.type)) { - for (const id in this._tiles) { - const tile = this._tiles[id]; - if (tile.fadeEndTime !== undefined && tile.fadeEndTime >= browser.now()) { - return true; - } - } - } - - return false; - } - - /** + tilesIn(queryGeometry: QueryGeometry, use3DQuery: boolean, visualizeQueryGeometry: boolean): TilespaceQueryGeometry[] { + const tileResults = []; + + const transform = this.transform; + if (!transform) return tileResults; + + const isGlobe = transform.projection.name === 'globe'; + const centerX = mercatorXfromLng(transform.center.lng); + + for (const tileID in this._tiles) { + const tile = this._tiles[tileID]; + if (visualizeQueryGeometry) { + tile.clearQueryDebugViz(); + } + if (tile.holdingForFade()) { + // Tiles held for fading are covered by tiles that are closer to ideal + continue; + } + + // An array of wrap values for the tile [-1, 0, 1]. The default value is 0 but -1 or 1 wrapping + // might be required in globe view due to globe's surface being continuous. + let tilesToCheck; + + if (isGlobe) { + // Compare distances to copies of the tile to see if a wrapped one should be used. + const id = tile.tileID.canonical; + assert(tile.tileID.wrap === 0); + + if (id.z === 0) { + // Render the zoom level 0 tile twice as the query polygon might span over the antimeridian + const distances = [ + Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), + Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX) + ]; + + tilesToCheck = [0, distances.indexOf(Math.min(...distances)) * 2 - 1]; + } else { + const distances = [ + Math.abs(clamp(centerX, ...tileBoundsX(id, -1)) - centerX), + Math.abs(clamp(centerX, ...tileBoundsX(id, 0)) - centerX), + Math.abs(clamp(centerX, ...tileBoundsX(id, 1)) - centerX) + ]; + + tilesToCheck = [distances.indexOf(Math.min(...distances)) - 1]; + } + } else { + tilesToCheck = [0]; + } + + for (const wrap of tilesToCheck) { + const tileResult = queryGeometry.containsTile(tile, transform, use3DQuery, wrap); + if (tileResult) { + tileResults.push(tileResult); + } + } + } + return tileResults; + } + + getVisibleCoordinates(symbolLayer?: boolean): Array { + const coords = this.getRenderableIds(symbolLayer).map((id) => this._tiles[id].tileID); + for (const coord of coords) { + coord.projMatrix = this.transform.calculateProjMatrix(coord.toUnwrapped()); + } + return coords; + } + + hasTransition(): boolean { + if (this._source.hasTransition()) { + return true; + } + + if (isRasterType(this._source.type)) { + for (const id in this._tiles) { + const tile = this._tiles[id]; + if (tile.fadeEndTime !== undefined && tile.fadeEndTime >= browser.now()) { + return true; + } + } + } + + return false; + } + + /** * Set the value of a particular state for a feature * @private */ - setFeatureState( - sourceLayer?: string, - featureId: number | string, - state: Object, - ) { - sourceLayer = sourceLayer || '_geojsonTileLayer'; - this._state.updateState(sourceLayer, featureId, state); - } - - /** + setFeatureState(sourceLayer?: string, featureId: number | string, state: Object) { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + this._state.updateState(sourceLayer, featureId, state); + } + + /** * Resets the value of a particular state key for a feature * @private */ - removeFeatureState( - sourceLayer?: string, - featureId?: number | string, - key?: string, - ) { - sourceLayer = sourceLayer || '_geojsonTileLayer'; - this._state.removeFeatureState(sourceLayer, featureId, key); - } - - /** + removeFeatureState(sourceLayer?: string, featureId?: number | string, key?: string) { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + this._state.removeFeatureState(sourceLayer, featureId, key); + } + + /** * Get the entire state object for a feature * @private */ - getFeatureState( - sourceLayer?: string, - featureId: number | string, - ): FeatureStates { - sourceLayer = sourceLayer || '_geojsonTileLayer'; - return this._state.getState(sourceLayer, featureId); - } - - /** + getFeatureState(sourceLayer?: string, featureId: number | string): FeatureStates { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + return this._state.getState(sourceLayer, featureId); + } + + /** * Sets the set of keys that the tile depends on. This allows tiles to * be reloaded when their dependencies change. * @private */ - setDependencies( - tileKey: number, - namespace: string, - dependencies: Array, - ) { - const tile = this._tiles[tileKey]; - if (tile) { - tile.setDependencies(namespace, dependencies); - } - } - - /** + setDependencies(tileKey: number, namespace: string, dependencies: Array) { + const tile = this._tiles[tileKey]; + if (tile) { + tile.setDependencies(namespace, dependencies); + } + } + + /** * Reloads all tiles that depend on the given keys. * @private */ - reloadTilesForDependencies(namespaces: Array, keys: Array) { - for (const id in this._tiles) { - const tile = this._tiles[id]; - if (tile.hasDependency(namespaces, keys)) { - this._reloadTile(+id, 'reloading'); - } - } - this._cache.filter(tile => !tile.hasDependency(namespaces, keys)); - } - - /** + reloadTilesForDependencies(namespaces: Array, keys: Array) { + for (const id in this._tiles) { + const tile = this._tiles[id]; + if (tile.hasDependency(namespaces, keys)) { + this._reloadTile(+id, 'reloading'); + } + } + this._cache.filter(tile => !tile.hasDependency(namespaces, keys)); + } + + /** * Preloads all tiles that will be requested for one or a series of transformations * * @private * @returns {Object} Returns `this` | Promise. */ - _preloadTiles( - transform: Transform | Array, - callback: Callback, - ) { - const coveringTilesIDs: Map = new Map(); - const transforms = Array.isArray(transform) ? transform : [transform]; - - const terrain = this.map.painter.terrain; - const tileSize = this.usedForTerrain && terrain ? - terrain.getScaledDemTileSize() : - this._source.tileSize; - - for (const tr of transforms) { - const tileIDs = tr.coveringTiles( - { - tileSize, - minzoom: this._source.minzoom, - maxzoom: this._source.maxzoom, - roundZoom: this._source.roundZoom && !this.usedForTerrain, - reparseOverscaled: this._source.reparseOverscaled, - isTerrainDEM: this.usedForTerrain, - }, - ); - - for (const tileID of tileIDs) { - coveringTilesIDs.set(tileID.key, tileID); - } - - if (this.usedForTerrain) { - tr.updateElevation(false); - } - } - - const tileIDs = Array.from(coveringTilesIDs.values()); - - asyncAll( - tileIDs, - (tileID, done) => { - const tile = new Tile( - tileID, - this._source.tileSize * tileID.overscaleFactor(), - this.transform.tileZoom, - this.map.painter, - this._isRaster, - ); - this._loadTile( - tile, - err => { - if (this._source.type === 'raster-dem' && tile.dem) - this._backfillDEM(tile); - done(err, tile); - }, - ); - }, - callback, - ); - } + _preloadTiles(transform: Transform | Array, callback: Callback) { + const coveringTilesIDs: Map = new Map(); + const transforms = Array.isArray(transform) ? transform : [transform]; + + const terrain = this.map.painter.terrain; + const tileSize = this.usedForTerrain && terrain ? terrain.getScaledDemTileSize() : this._source.tileSize; + + for (const tr of transforms) { + const tileIDs = tr.coveringTiles({ + tileSize, + minzoom: this._source.minzoom, + maxzoom: this._source.maxzoom, + roundZoom: this._source.roundZoom && !this.usedForTerrain, + reparseOverscaled: this._source.reparseOverscaled, + isTerrainDEM: this.usedForTerrain + }); + + for (const tileID of tileIDs) { + coveringTilesIDs.set(tileID.key, tileID); + } + + if (this.usedForTerrain) { + tr.updateElevation(false); + } + } + + const tileIDs = Array.from(coveringTilesIDs.values()); + + asyncAll(tileIDs, (tileID, done) => { + const tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, this.map.painter, this._isRaster); + this._loadTile(tile, (err) => { + if (this._source.type === 'raster-dem' && tile.dem) this._backfillDEM(tile); + done(err, tile); + }); + }, callback); + } } SourceCache.maxOverzooming = 10; From a63ed326c193fbc904a2e590a3ecf4cd108e00d1 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 16:06:09 +0200 Subject: [PATCH 57/72] fix formatting for grid_index.js --- src/symbol/grid_index.js | 773 +++++++++++++++++---------------------- 1 file changed, 337 insertions(+), 436 deletions(-) diff --git a/src/symbol/grid_index.js b/src/symbol/grid_index.js index c30ce2ae2e0..60cb3c6336f 100644 --- a/src/symbol/grid_index.js +++ b/src/symbol/grid_index.js @@ -22,485 +22,386 @@ type GridItem = { * @private */ class GridIndex { - circleKeys: Array; - boxKeys: Array; - boxCells: Array>; - circleCells: Array>; - bboxes: Array; - circles: Array; - xCellCount: number; - yCellCount: number; - width: number; - height: number; - xScale: number; - yScale: number; - boxUid: number; - circleUid: number; + circleKeys: Array; + boxKeys: Array; + boxCells: Array>; + circleCells: Array>; + bboxes: Array; + circles: Array; + xCellCount: number; + yCellCount: number; + width: number; + height: number; + xScale: number; + yScale: number; + boxUid: number; + circleUid: number; - constructor(width: number, height: number, cellSize: number) { - const boxCells = this.boxCells = []; - const circleCells = this.circleCells = []; + constructor (width: number, height: number, cellSize: number) { + const boxCells = this.boxCells = []; + const circleCells = this.circleCells = []; - // More cells -> fewer geometries to check per cell, but items tend - // to be split across more cells. - // Sweet spot allows most small items to fit in one cell - this.xCellCount = Math.ceil(width / cellSize); - this.yCellCount = Math.ceil(height / cellSize); + // More cells -> fewer geometries to check per cell, but items tend + // to be split across more cells. + // Sweet spot allows most small items to fit in one cell + this.xCellCount = Math.ceil(width / cellSize); + this.yCellCount = Math.ceil(height / cellSize); - for (let i = 0; i < this.xCellCount * this.yCellCount; i++) { - boxCells.push([]); - circleCells.push([]); - } - this.circleKeys = []; - this.boxKeys = []; - this.bboxes = []; - this.circles = []; + for (let i = 0; i < this.xCellCount * this.yCellCount; i++) { + boxCells.push([]); + circleCells.push([]); + } + this.circleKeys = []; + this.boxKeys = []; + this.bboxes = []; + this.circles = []; - this.width = width; - this.height = height; - this.xScale = this.xCellCount / width; - this.yScale = this.yCellCount / height; - this.boxUid = 0; - this.circleUid = 0; - } + this.width = width; + this.height = height; + this.xScale = this.xCellCount / width; + this.yScale = this.yCellCount / height; + this.boxUid = 0; + this.circleUid = 0; + } - keysLength(): number { - return this.boxKeys.length + this.circleKeys.length; - } + keysLength(): number { + return this.boxKeys.length + this.circleKeys.length; + } - insert(key: any, x1: number, y1: number, x2: number, y2: number) { - this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++); - this.boxKeys.push(key); - this.bboxes.push(x1); - this.bboxes.push(y1); - this.bboxes.push(x2); - this.bboxes.push(y2); - } + insert(key: any, x1: number, y1: number, x2: number, y2: number) { + this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++); + this.boxKeys.push(key); + this.bboxes.push(x1); + this.bboxes.push(y1); + this.bboxes.push(x2); + this.bboxes.push(y2); + } - insertCircle(key: any, x: number, y: number, radius: number) { - // Insert circle into grid for all cells in the circumscribing square - // It's more than necessary (by a factor of 4/PI), but fast to insert - this._forEachCell( - x - radius, - y - radius, - x + radius, - y + radius, - this._insertCircleCell, - this.circleUid++, - ); - this.circleKeys.push(key); - this.circles.push(x); - this.circles.push(y); - this.circles.push(radius); - } + insertCircle(key: any, x: number, y: number, radius: number) { + // Insert circle into grid for all cells in the circumscribing square + // It's more than necessary (by a factor of 4/PI), but fast to insert + this._forEachCell(x - radius, y - radius, x + radius, y + radius, this._insertCircleCell, this.circleUid++); + this.circleKeys.push(key); + this.circles.push(x); + this.circles.push(y); + this.circles.push(radius); + } - _insertBoxCell: (( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number -) => void) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number, -) => { - this.boxCells[cellIndex].push(uid); -}; + _insertBoxCell: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number + ) => void) = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number + ) => { + this.boxCells[cellIndex].push(uid); + }; - _insertCircleCell: (( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number -) => void) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number, -) => { - this.circleCells[cellIndex].push(uid); -}; + _insertCircleCell: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number + ) => void) = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number + ) => { + this.circleCells[cellIndex].push(uid); + } - _query( - x1: number, - y1: number, - x2: number, - y2: number, - hitTest: boolean, - predicate?: any, - ): boolean | Array { - if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { - return hitTest ? false : []; - } - const result = []; - if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) { - if (hitTest) { - return true; - } - for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) { - result.push( - { - key: this.boxKeys[boxUid], - x1: this.bboxes[boxUid * 4], - y1: this.bboxes[boxUid * 4 + 1], - x2: this.bboxes[boxUid * 4 + 2], - y2: this.bboxes[boxUid * 4 + 3], - }, - ); - } - for (let circleUid = 0; circleUid < this.circleKeys.length; circleUid++) { - const x = this.circles[circleUid * 3]; - const y = this.circles[circleUid * 3 + 1]; - const radius = this.circles[circleUid * 3 + 2]; - result.push( - { - key: this.circleKeys[circleUid], - x1: x - radius, - y1: y - radius, - x2: x + radius, - y2: y + radius, - }, - ); - } - return predicate ? result.filter(predicate) : result; - } else { - const queryArgs = { - hitTest, - seenUids: {box: {}, circle: {}}, - }; - this._forEachCell( - x1, - y1, - x2, - y2, - this._queryCell, - result, - queryArgs, - predicate, - ); - return hitTest ? result.length > 0 : result; - } - } + _query(x1: number, y1: number, x2: number, y2: number, hitTest: boolean, predicate?: any): boolean | Array { + if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { + return hitTest ? false : []; + } + const result = []; + if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) { + if (hitTest) { + return true; + } + for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) { + result.push({ + key: this.boxKeys[boxUid], + x1: this.bboxes[boxUid * 4], + y1: this.bboxes[boxUid * 4 + 1], + x2: this.bboxes[boxUid * 4 + 2], + y2: this.bboxes[boxUid * 4 + 3] + }); + } + for (let circleUid = 0; circleUid < this.circleKeys.length; circleUid++) { + const x = this.circles[circleUid * 3]; + const y = this.circles[circleUid * 3 + 1]; + const radius = this.circles[circleUid * 3 + 2]; + result.push({ + key: this.circleKeys[circleUid], + x1: x - radius, + y1: y - radius, + x2: x + radius, + y2: y + radius + }); + } + return predicate ? result.filter(predicate) : result; + } else { + const queryArgs = { + hitTest, + seenUids: {box: {}, circle: {}} + }; + this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate); + return hitTest ? result.length > 0 : result; + } + } - _queryCircle( - x: number, - y: number, - radius: number, - hitTest: boolean, - predicate?: any, - ): boolean | Array { - // Insert circle into grid for all cells in the circumscribing square - // It's more than necessary (by a factor of 4/PI), but fast to insert - const x1 = x - radius; - const x2 = x + radius; - const y1 = y - radius; - const y2 = y + radius; - if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { - return hitTest ? false : []; - } + _queryCircle(x: number, y: number, radius: number, hitTest: boolean, predicate?: any): boolean | Array { + // Insert circle into grid for all cells in the circumscribing square + // It's more than necessary (by a factor of 4/PI), but fast to insert + const x1 = x - radius; + const x2 = x + radius; + const y1 = y - radius; + const y2 = y + radius; + if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { + return hitTest ? false : []; + } - // Box query early exits if the bounding box is larger than the grid, but we don't do - // the equivalent calculation for circle queries because early exit is less likely - // and the calculation is more expensive - const result = []; - const queryArgs = { - hitTest, - circle: {x, y, radius}, - seenUids: {box: {}, circle: {}}, - }; - this._forEachCell( - x1, - y1, - x2, - y2, - this._queryCellCircle, - result, - queryArgs, - predicate, - ); - return hitTest ? result.length > 0 : result; - } + // Box query early exits if the bounding box is larger than the grid, but we don't do + // the equivalent calculation for circle queries because early exit is less likely + // and the calculation is more expensive + const result = []; + const queryArgs = { + hitTest, + circle: {x, y, radius}, + seenUids: {box: {}, circle: {}} + }; + this._forEachCell(x1, y1, x2, y2, this._queryCellCircle, result, queryArgs, predicate); + return hitTest ? result.length > 0 : result; + } - query( - x1: number, - y1: number, - x2: number, - y2: number, - predicate?: any, - ): Array { - return (this._query(x1, y1, x2, y2, false, predicate): any); - } + query(x1: number, y1: number, x2: number, y2: number, predicate?: any): Array { + return (this._query(x1, y1, x2, y2, false, predicate): any); + } - hitTest( - x1: number, - y1: number, - x2: number, - y2: number, - predicate?: any, - ): boolean { - return (this._query(x1, y1, x2, y2, true, predicate): any); - } + hitTest(x1: number, y1: number, x2: number, y2: number, predicate?: any): boolean { + return (this._query(x1, y1, x2, y2, true, predicate): any); + } - hitTestCircle(x: number, y: number, radius: number, predicate?: any): boolean { - return (this._queryCircle(x, y, radius, true, predicate): any); - } + hitTestCircle(x: number, y: number, radius: number, predicate?: any): boolean { + return (this._queryCircle(x, y, radius, true, predicate): any); + } - _queryCell: (( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any -) => void | boolean) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any, -): void | boolean => { - const seenUids = queryArgs.seenUids; - const boxCell = this.boxCells[cellIndex]; - if (boxCell !== null) { - const bboxes = this.bboxes; - for (const boxUid of boxCell) { - if (!seenUids.box[boxUid]) { - seenUids.box[boxUid] = true; - const offset = boxUid * 4; - if ( - x1 <= bboxes[offset + 2] && y1 <= bboxes[offset + 3] && - x2 >= bboxes[offset + 0] && - y2 >= bboxes[offset + 1] && - (!predicate || predicate(this.boxKeys[boxUid])) - ) { - if (queryArgs.hitTest) { - result.push(true); - return true; - } else { - result.push( - { - key: this.boxKeys[boxUid], - x1: bboxes[offset], - y1: bboxes[offset + 1], - x2: bboxes[offset + 2], - y2: bboxes[offset + 3], - }, - ); + _queryCell: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any + ) => void | boolean) = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any, + ): void | boolean => { + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if ((x1 <= bboxes[offset + 2]) && + (y1 <= bboxes[offset + 3]) && + (x2 >= bboxes[offset + 0]) && + (y2 >= bboxes[offset + 1]) && + (!predicate || predicate(this.boxKeys[boxUid]))) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } else { + result.push({ + key: this.boxKeys[boxUid], + x1: bboxes[offset], + y1: bboxes[offset + 1], + x2: bboxes[offset + 2], + y2: bboxes[offset + 3] + }); + } } } } } - } - const circleCell = this.circleCells[cellIndex]; - if (circleCell !== null) { - const circles = this.circles; - for (const circleUid of circleCell) { - if (!seenUids.circle[circleUid]) { - seenUids.circle[circleUid] = true; - const offset = circleUid * 3; - if ( - this._circleAndRectCollide( - circles[offset], - circles[offset + 1], - circles[offset + 2], - x1, - y1, - x2, - y2, - ) && - (!predicate || predicate(this.circleKeys[circleUid])) - ) { - if (queryArgs.hitTest) { - result.push(true); - return true; - } else { - const x = circles[offset]; - const y = circles[offset + 1]; - const radius = circles[offset + 2]; - result.push( - { - key: this.circleKeys[circleUid], - x1: x - radius, - y1: y - radius, - x2: x + radius, - y2: y + radius, - }, - ); + const circleCell = this.circleCells[cellIndex]; + if (circleCell !== null) { + const circles = this.circles; + for (const circleUid of circleCell) { + if (!seenUids.circle[circleUid]) { + seenUids.circle[circleUid] = true; + const offset = circleUid * 3; + if (this._circleAndRectCollide( + circles[offset], + circles[offset + 1], + circles[offset + 2], + x1, + y1, + x2, + y2) && + (!predicate || predicate(this.circleKeys[circleUid]))) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } else { + const x = circles[offset]; + const y = circles[offset + 1]; + const radius = circles[offset + 2]; + result.push({ + key: this.circleKeys[circleUid], + x1: x - radius, + y1: y - radius, + x2: x + radius, + y2: y + radius + }); + } } } } } } -}; - _queryCellCircle: (( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any -) => void | boolean) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any, -): void | boolean => { - const circle = queryArgs.circle; - const seenUids = queryArgs.seenUids; - const boxCell = this.boxCells[cellIndex]; - if (boxCell !== null) { - const bboxes = this.bboxes; - for (const boxUid of boxCell) { - if (!seenUids.box[boxUid]) { - seenUids.box[boxUid] = true; - const offset = boxUid * 4; - if ( - this._circleAndRectCollide( - circle.x, - circle.y, - circle.radius, - bboxes[offset + 0], - bboxes[offset + 1], - bboxes[offset + 2], - bboxes[offset + 3], - ) && - (!predicate || predicate(this.boxKeys[boxUid])) - ) { - result.push(true); - return true; + _queryCellCircle: (( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any + ) => void | boolean) = ( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any, + queryArgs: any, + predicate?: any, + ): void | boolean => { + const circle = queryArgs.circle; + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if (this._circleAndRectCollide( + circle.x, + circle.y, + circle.radius, + bboxes[offset + 0], + bboxes[offset + 1], + bboxes[offset + 2], + bboxes[offset + 3]) && + (!predicate || predicate(this.boxKeys[boxUid]))) { + result.push(true); + return true; + } } } } - } - const circleCell = this.circleCells[cellIndex]; - if (circleCell !== null) { - const circles = this.circles; - for (const circleUid of circleCell) { - if (!seenUids.circle[circleUid]) { - seenUids.circle[circleUid] = true; - const offset = circleUid * 3; - if ( - this._circlesCollide( - circles[offset], - circles[offset + 1], - circles[offset + 2], - circle.x, - circle.y, - circle.radius, - ) && - (!predicate || predicate(this.circleKeys[circleUid])) - ) { - result.push(true); - return true; + const circleCell = this.circleCells[cellIndex]; + if (circleCell !== null) { + const circles = this.circles; + for (const circleUid of circleCell) { + if (!seenUids.circle[circleUid]) { + seenUids.circle[circleUid] = true; + const offset = circleUid * 3; + if (this._circlesCollide( + circles[offset], + circles[offset + 1], + circles[offset + 2], + circle.x, + circle.y, + circle.radius) && + (!predicate || predicate(this.circleKeys[circleUid]))) { + result.push(true); + return true; + } } } } } -}; - _forEachCell( - x1: number, - y1: number, - x2: number, - y2: number, - fn: any, - arg1: any, - arg2?: any, - predicate?: any, - ) { - const cx1 = this._convertToXCellCoord(x1); - const cy1 = this._convertToYCellCoord(y1); - const cx2 = this._convertToXCellCoord(x2); - const cy2 = this._convertToYCellCoord(y2); + _forEachCell(x1: number, y1: number, x2: number, y2: number, fn: any, arg1: any, arg2?: any, predicate?: any) { + const cx1 = this._convertToXCellCoord(x1); + const cy1 = this._convertToYCellCoord(y1); + const cx2 = this._convertToXCellCoord(x2); + const cy2 = this._convertToYCellCoord(y2); - for (let x = cx1; x <= cx2; x++) { - for (let y = cy1; y <= cy2; y++) { - const cellIndex = this.xCellCount * y + x; - if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) - return; - } - } - } + for (let x = cx1; x <= cx2; x++) { + for (let y = cy1; y <= cy2; y++) { + const cellIndex = this.xCellCount * y + x; + if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) return; + } + } + } - _convertToXCellCoord(x: number): number { - return Math.max( - 0, - Math.min(this.xCellCount - 1, Math.floor(x * this.xScale)), - ); - } + _convertToXCellCoord(x: number): number { + return Math.max(0, Math.min(this.xCellCount - 1, Math.floor(x * this.xScale))); + } - _convertToYCellCoord(y: number): number { - return Math.max( - 0, - Math.min(this.yCellCount - 1, Math.floor(y * this.yScale)), - ); - } + _convertToYCellCoord(y: number): number { + return Math.max(0, Math.min(this.yCellCount - 1, Math.floor(y * this.yScale))); + } - _circlesCollide( - x1: number, - y1: number, - r1: number, - x2: number, - y2: number, - r2: number, - ): boolean { - const dx = x2 - x1; - const dy = y2 - y1; - const bothRadii = r1 + r2; - return bothRadii * bothRadii > dx * dx + dy * dy; - } + _circlesCollide(x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): boolean { + const dx = x2 - x1; + const dy = y2 - y1; + const bothRadii = r1 + r2; + return (bothRadii * bothRadii) > (dx * dx + dy * dy); + } - _circleAndRectCollide( - circleX: number, - circleY: number, - radius: number, - x1: number, - y1: number, - x2: number, - y2: number, - ): boolean { - const halfRectWidth = (x2 - x1) / 2; - const distX = Math.abs(circleX - (x1 + halfRectWidth)); - if (distX > halfRectWidth + radius) { - return false; - } + _circleAndRectCollide(circleX: number, circleY: number, radius: number, x1: number, y1: number, x2: number, y2: number): boolean { + const halfRectWidth = (x2 - x1) / 2; + const distX = Math.abs(circleX - (x1 + halfRectWidth)); + if (distX > (halfRectWidth + radius)) { + return false; + } - const halfRectHeight = (y2 - y1) / 2; - const distY = Math.abs(circleY - (y1 + halfRectHeight)); - if (distY > halfRectHeight + radius) { - return false; - } + const halfRectHeight = (y2 - y1) / 2; + const distY = Math.abs(circleY - (y1 + halfRectHeight)); + if (distY > (halfRectHeight + radius)) { + return false; + } - if (distX <= halfRectWidth || distY <= halfRectHeight) { - return true; - } + if (distX <= halfRectWidth || distY <= halfRectHeight) { + return true; + } - const dx = distX - halfRectWidth; - const dy = distY - halfRectHeight; - return dx * dx + dy * dy <= radius * radius; - } + const dx = distX - halfRectWidth; + const dy = distY - halfRectHeight; + return (dx * dx + dy * dy <= (radius * radius)); + } } export default GridIndex; From cd999397930525234e71c1aefb593b040db37b41 Mon Sep 17 00:00:00 2001 From: Dzianis Sheka Date: Fri, 24 Feb 2023 16:13:23 +0200 Subject: [PATCH 58/72] fix formatting for expression/index.js --- src/style-spec/expression/index.js | 39 ++++++++---------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 6ff48054028..15152150a0f 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -193,41 +193,22 @@ export class ZoomDependentExpression { } export type ConstantExpression = interface { - kind: 'constant', - +evaluate: ( - globals: GlobalProperties, - feature?: Feature, - featureState?: FeatureState, - canonical?: CanonicalTileID, - availableImages?: Array - ) => any, + kind: 'constant', + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, } export type SourceExpression = interface { - kind: 'source', - isStateDependent: boolean, - +evaluate: ( - globals: GlobalProperties, - feature?: Feature, - featureState?: FeatureState, - canonical?: CanonicalTileID, - availableImages?: Array, - formattedSection?: FormattedSection - ) => any, + kind: 'source', + isStateDependent: boolean, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, }; export type CameraExpression = interface { - kind: 'camera', - +evaluate: ( - globals: GlobalProperties, - feature?: Feature, - featureState?: FeatureState, - canonical?: CanonicalTileID, - availableImages?: Array - ) => any, - +interpolationFactor: (input: number, lower: number, upper: number) => number, - zoomStops: Array, - interpolationType: ?InterpolationType, + kind: 'camera', + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, + zoomStops: Array, + interpolationType: ?InterpolationType }; export interface CompositeExpression { From be873dd09df0d8d1278f085a892358e59c3ebbf1 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 16:49:10 +0200 Subject: [PATCH 59/72] Enable `constrain_writes` inference mode See https://flow.org/en/docs/config/options/#toc-inference-mode-classic-constrain-writes --- .flowconfig | 1 + src/data/feature_index.js | 3 +- src/geo/transform.js | 4 +- src/render/draw_fill_extrusion.js | 4 +- src/render/draw_symbol.js | 2 +- src/source/rtl_text_plugin.js | 2 +- src/source/source_cache.js | 2 +- src/source/worker.js | 1 + src/source/worker_tile.js | 6 +-- src/style-spec/diff.js | 7 ++-- .../expression/definitions/match.js | 2 +- src/style-spec/validate/validate_function.js | 2 +- src/style-spec/validate/validate_property.js | 5 ++- src/style/load_sprite.js | 4 +- src/style/style.js | 40 ++++++++++--------- src/symbol/check_max_angle.js | 2 +- src/symbol/collision_index.js | 2 +- src/symbol/placement.js | 34 ++++++++++------ src/symbol/projection.js | 4 +- src/terrain/elevation.js | 2 +- src/terrain/terrain.js | 6 +-- src/ui/camera.js | 2 +- src/ui/map.js | 2 +- src/util/browser.js | 2 +- src/util/global_worker_pool.js | 2 +- 25 files changed, 79 insertions(+), 64 deletions(-) diff --git a/.flowconfig b/.flowconfig index 73c818edc76..ade6637dfad 100644 --- a/.flowconfig +++ b/.flowconfig @@ -33,6 +33,7 @@ 0.184.0 [options] +inference_mode=constrain_writes [strict] nonstrict-import diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 713b1e7f17b..8aba77cc407 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -305,8 +305,9 @@ class FeatureIndex { let id = feature.id; if (this.promoteId) { const propName = typeof this.promoteId === 'string' ? this.promoteId : this.promoteId[sourceLayerId]; + // $FlowFixMe[incompatible-type] - Flow can't narrow the id type from IVectorTileFeature.id if (propName != null) id = feature.properties[propName]; - if (typeof id === 'boolean') id = Number(id); + if (typeof id === 'boolean') id = Number(id); } return id; } diff --git a/src/geo/transform.js b/src/geo/transform.js index bf99f565989..e8014e29e43 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -81,7 +81,7 @@ class Transform { mercatorFogMatrix: Float32Array; // Projection from world coordinates (mercator scaled by worldSize) to clip coordinates - projMatrix: Array; + projMatrix: Float32Array | Float64Array; invProjMatrix: Float64Array; // Same as projMatrix, pixel-aligned to avoid fractional pixels for raster tiles @@ -1874,7 +1874,7 @@ class Transform { cameraToClip[8] = -offset.x * 2 / this.width; cameraToClip[9] = offset.y * 2 / this.height; - let m = mat4.mul([], cameraToClip, worldToCamera); + let m: Float32Array | Float64Array = mat4.mul(new Float64Array([]), cameraToClip, worldToCamera); if (this.projection.isReprojectedInTileSpace) { // Projections undistort as you zoom in (shear, scale, rotate). diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index fc8361cecab..2daf225b03c 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -145,7 +145,7 @@ function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMo function flatRoofsUpdate(context, source, coord, bucket, layer, terrain) { // For all four borders: 0 - left, 1, right, 2 - top, 3 - bottom const neighborCoord = [ - coord => { + (coord: OverscaledTileID) => { let x = coord.canonical.x - 1; let w = coord.wrap; if (x < 0) { @@ -154,7 +154,7 @@ function flatRoofsUpdate(context, source, coord, bucket, layer, terrain) { } return new OverscaledTileID(coord.overscaledZ, w, coord.canonical.z, x, coord.canonical.y); }, - coord => { + (coord: OverscaledTileID) => { let x = coord.canonical.x + 1; let w = coord.wrap; if (x === 1 << coord.canonical.z) { diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index 75f058d7333..52ee6942f92 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -295,7 +295,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const mercatorCameraUp = [0, -1, 0]; - let globeCameraUp = mercatorCameraUp; + let globeCameraUp: [number, number, number] = mercatorCameraUp; if ((isGlobeProjection || tr.mercatorFromTransition) && !rotateWithMap) { // Each symbol rotating with the viewport requires per-instance information about // how to align with the viewport. In 2D case rotation is shared between all of the symbols and diff --git a/src/source/rtl_text_plugin.js b/src/source/rtl_text_plugin.js index fbe4ae244d0..ead7f58de70 100644 --- a/src/source/rtl_text_plugin.js +++ b/src/source/rtl_text_plugin.js @@ -25,7 +25,7 @@ let _completionCallback = null; //Variables defining the current state of the plugin let pluginStatus = status.unavailable; -let pluginURL = null; +let pluginURL: ?string = null; export const triggerPluginCompletionEvent = function(error: ?Error) { // NetworkError's are not correctly reflected by the plugin status which prevents reloading plugin diff --git a/src/source/source_cache.js b/src/source/source_cache.js index d109c6808ed..28c178f9c2d 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -742,7 +742,7 @@ class SourceCache extends Evented { * @private */ _addTile(tileID: OverscaledTileID): Tile { - let tile = this._tiles[tileID.key]; + let tile: ?Tile = this._tiles[tileID.key]; if (tile) return tile; tile = this._cache.getAndRemove(tileID); diff --git a/src/source/worker.js b/src/source/worker.js index f0437586a9a..3e561a5151b 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -291,5 +291,6 @@ export default class Worker { if (typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope) { + // $FlowFixMe[prop-missing] self.worker = new Worker(self); } diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 010756c236e..eb8b25ce467 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -178,9 +178,9 @@ class WorkerTile { lineAtlas.trim(); let error: ?Error; - let glyphMap: ?{[_: string]: {glyphs: {[_: number]: ?StyleGlyph}, ascender?: number, descender?: number}}; - let iconMap: ?{[_: string]: StyleImage}; - let patternMap: ?{[_: string]: StyleImage}; + let glyphMap: {[_: string]: {glyphs: {[_: number]: ?StyleGlyph}, ascender?: number, descender?: number}}; + let iconMap: {[_: string]: StyleImage}; + let patternMap: {[_: string]: StyleImage}; const taskMetadata = {type: 'maybePrepare', isSymbolTile: this.isSymbolTile, zoom: this.zoom}; const stacks = mapObject(options.glyphDependencies, (glyphs) => Object.keys(glyphs).map(Number)); diff --git a/src/style-spec/diff.js b/src/style-spec/diff.js index 1d6be4ca7ba..f25d31c3b8f 100644 --- a/src/style-spec/diff.js +++ b/src/style-spec/diff.js @@ -170,11 +170,12 @@ function diffSources(before, after, commands, sourcesRemoved) { // look for sources to add/update for (sourceId in after) { if (!after.hasOwnProperty(sourceId)) continue; + const source = after[sourceId]; if (!before.hasOwnProperty(sourceId)) { addSource(sourceId, after, commands); - } else if (!isEqual(before[sourceId], after[sourceId])) { - if (before[sourceId].type === 'geojson' && after[sourceId].type === 'geojson' && canUpdateGeoJSON(before, after, sourceId)) { - commands.push({command: operations.setGeoJSONSourceData, args: [sourceId, after[sourceId].data]}); + } else if (!isEqual(before[sourceId], source)) { + if (before[sourceId].type === 'geojson' && source.type === 'geojson' && canUpdateGeoJSON(before, after, sourceId)) { + commands.push({command: operations.setGeoJSONSourceData, args: [sourceId, source.data]}); } else { // no update command, must remove then add updateSource(sourceId, after, commands, sourcesRemoved); diff --git a/src/style-spec/expression/definitions/match.js b/src/style-spec/expression/definitions/match.js index 8f87b07ab31..c4403102c96 100644 --- a/src/style-spec/expression/definitions/match.js +++ b/src/style-spec/expression/definitions/match.js @@ -37,7 +37,7 @@ class Match implements Expression { return context.error(`Expected an even number of arguments.`); let inputType; - let outputType; + let outputType: ?Type; if (context.expectedType && context.expectedType.kind !== 'value') { outputType = context.expectedType; } diff --git a/src/style-spec/validate/validate_function.js b/src/style-spec/validate/validate_function.js index b51a79e2850..e86e3af4e54 100644 --- a/src/style-spec/validate/validate_function.js +++ b/src/style-spec/validate/validate_function.js @@ -21,7 +21,7 @@ export default function validateFunction(options: ValidationOptions): any { const functionType = unbundle(options.value.type); let stopKeyType; let stopDomainValues: {[string | number]: boolean} = {}; - let previousStopDomainValue; + let previousStopDomainValue: ?mixed; let previousStopDomainZoom; const isZoomFunction = functionType !== 'categorical' && options.value.property === undefined; diff --git a/src/style-spec/validate/validate_property.js b/src/style-spec/validate/validate_property.js index c09690ca746..99d9ae8941e 100644 --- a/src/style-spec/validate/validate_property.js +++ b/src/style-spec/validate/validate_property.js @@ -40,12 +40,13 @@ export default function validateProperty(options: PropertyValidationOptions, pro return [new ValidationError(key, value, `unknown property "${propertyKey}"`)]; } - let tokenMatch; + let tokenMatch: ?RegExp$matchResult; if (getType(value) === 'string' && supportsPropertyExpression(valueSpec) && !valueSpec.tokens && (tokenMatch = /^{([^}]+)}$/.exec(value))) { + const example = `\`{ "type": "identity", "property": ${tokenMatch ? JSON.stringify(tokenMatch[1]) : '"_"'} }\``; return [new ValidationError( key, value, `"${propertyKey}" does not support interpolation syntax\n` + - `Use an identity property function instead: \`{ "type": "identity", "property": ${JSON.stringify(tokenMatch[1])} }\`.`)]; + `Use an identity property function instead: ${example}.`)]; } const errors = []; diff --git a/src/style/load_sprite.js b/src/style/load_sprite.js index 1ca924fce0e..871970afadd 100644 --- a/src/style/load_sprite.js +++ b/src/style/load_sprite.js @@ -16,7 +16,7 @@ export default function(baseURL: string, let json: any, image, error; const format = browser.devicePixelRatio > 1 ? '@2x' : ''; - let jsonRequest = getJSON(requestManager.transformRequest(requestManager.normalizeSpriteURL(baseURL, format, '.json'), ResourceType.SpriteJSON), (err: ?Error, data: ?Object) => { + let jsonRequest: ?Cancelable = getJSON(requestManager.transformRequest(requestManager.normalizeSpriteURL(baseURL, format, '.json'), ResourceType.SpriteJSON), (err: ?Error, data: ?Object) => { jsonRequest = null; if (!error) { error = err; @@ -25,7 +25,7 @@ export default function(baseURL: string, } }); - let imageRequest = getImage(requestManager.transformRequest(requestManager.normalizeSpriteURL(baseURL, format, '.png'), ResourceType.SpriteImage), (err, img) => { + let imageRequest: ?Cancelable = getImage(requestManager.transformRequest(requestManager.normalizeSpriteURL(baseURL, format, '.png'), ResourceType.SpriteImage), (err, img) => { imageRequest = null; if (!error) { error = err; diff --git a/src/style/style.js b/src/style/style.js index 44900f06867..5855a5b6abb 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -320,18 +320,18 @@ class Style extends Evented { this.glyphManager.setURL(json.glyphs); - const layers = deref(this.stylesheet.layers); + const layers: Array = deref(this.stylesheet.layers); this._order = layers.map((layer) => layer.id); this._layers = {}; this._serializedLayers = {}; - for (let layer of layers) { - layer = createStyleLayer(layer); - layer.setEventedParent(this, {layer: {id: layer.id}}); - this._layers[layer.id] = layer; - this._serializedLayers[layer.id] = layer.serialize(); - this._updateLayerCount(layer, true); + for (const layer of layers) { + const styleLayer = createStyleLayer(layer); + styleLayer.setEventedParent(this, {layer: {id: styleLayer.id}}); + this._layers[styleLayer.id] = styleLayer; + this._serializedLayers[styleLayer.id] = styleLayer.serialize(); + this._updateLayerCount(styleLayer, true); } this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); @@ -1478,37 +1478,39 @@ class Style extends Evented { return; } + let options: TerrainSpecification = terrainOptions; if (drapeRenderMode === DrapeRenderMode.elevated) { // Input validation and source object unrolling - if (typeof terrainOptions.source === 'object') { + if (typeof options.source === 'object') { const id = 'terrain-dem-src'; - this.addSource(id, ((terrainOptions.source): any)); - terrainOptions = clone(terrainOptions); - terrainOptions = (extend(terrainOptions, {source: id}): any); + this.addSource(id, options.source); + options = clone(options); + options = extend(options, {source: id}); } - if (this._validate(validateTerrain, 'terrain', terrainOptions)) { + if (this._validate(validateTerrain, 'terrain', options)) { return; } } // Enabling if (!this.terrain || (this.terrain && drapeRenderMode !== this.terrain.drapeRenderMode)) { - this._createTerrain(terrainOptions, drapeRenderMode); + if (!options) return; + this._createTerrain(options, drapeRenderMode); } else { // Updating const terrain = this.terrain; const currSpec = terrain.get(); for (const name of Object.keys(styleSpec.terrain)) { // Fallback to use default style specification when the properties wasn't set - if (!terrainOptions.hasOwnProperty(name) && !!styleSpec.terrain[name].default) { - terrainOptions[name] = styleSpec.terrain[name].default; + if (!options.hasOwnProperty(name) && !!styleSpec.terrain[name].default) { + options[name] = styleSpec.terrain[name].default; } } - for (const key in terrainOptions) { - if (!deepEqual(terrainOptions[key], currSpec[key])) { - terrain.set(terrainOptions); - this.stylesheet.terrain = terrainOptions; + for (const key in options) { + if (!deepEqual(options[key], currSpec[key])) { + terrain.set(options); + this.stylesheet.terrain = options; const parameters = this._setTransitionParameters({duration: 0}); terrain.updateTransitions(parameters); break; diff --git a/src/symbol/check_max_angle.js b/src/symbol/check_max_angle.js index c71a42c9d05..f5fa17a8c96 100644 --- a/src/symbol/check_max_angle.js +++ b/src/symbol/check_max_angle.js @@ -23,7 +23,7 @@ function checkMaxAngle(line: Array, anchor: Anchor, labelLength: number, // horizontal labels always pass if (anchor.segment === undefined) return true; - let p = anchor; + let p: Point = anchor; let index = anchor.segment + 1; let anchorDistance = 0; diff --git a/src/symbol/collision_index.js b/src/symbol/collision_index.js index f9878c19dfd..2e827b8f209 100644 --- a/src/symbol/collision_index.js +++ b/src/symbol/collision_index.js @@ -19,7 +19,7 @@ import type {GlyphOffsetArray, SymbolLineVertexArray, PlacedSymbol} from '../dat import type {FogState} from '../style/fog_helpers.js'; import type {Vec3, Mat4} from 'gl-matrix'; -type PlacedCollisionBox = {| +export type PlacedCollisionBox = {| box: Array, offscreen: boolean, occluded: boolean diff --git a/src/symbol/placement.js b/src/symbol/placement.js index a2f4d3791f5..a9e83fd188f 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -21,6 +21,10 @@ import type {OverscaledTileID} from '../source/tile_id.js'; import type {TextAnchor} from './symbol_layout.js'; import type {FogState} from '../style/fog_helpers.js'; import type {Mat4} from 'gl-matrix'; +import type {PlacedCollisionBox} from './collision_index.js'; + +// PlacedCollisionBox with all fields optional +type PartialPlacedCollisionBox = $ObjMap() => ?V>; class OpacityState { opacity: number; @@ -336,7 +340,7 @@ export class Placement { textScale: number, rotateWithMap: boolean, pitchWithMap: boolean, textPixelRatio: number, posMatrix: Mat4, collisionGroup: CollisionGroup, textAllowOverlap: boolean, symbolInstance: SymbolInstance, boxIndex: number, bucket: SymbolBucket, - orientation: number, iconBox: ?SingleCollisionBox, textSize: any, iconSize: any): ?{ shift: Point, placedGlyphBoxes: { box: Array, offscreen: boolean, occluded: boolean } } { + orientation: number, iconBox: ?SingleCollisionBox, textSize: any, iconSize: any): ?{ shift: Point, placedGlyphBoxes: PlacedCollisionBox } { const {textOffset0, textOffset1, crossTileID} = symbolInstance; const textOffset = [textOffset0, textOffset1]; @@ -478,15 +482,15 @@ export class Placement { this.placements[crossTileID] = new JointPlacement(false, false, false); return; } - let placeText = false; - let placeIcon = false; - let offscreen = true; - let textOccluded = false; + let placeText: ?boolean = false; + let placeIcon: ?boolean = false; + let offscreen: ?boolean = true; + let textOccluded: ?boolean = false; let iconOccluded = false; let shift = null; - let placed = {box: null, offscreen: null, occluded: null}; - let placedVerticalText = {box: null, offscreen: null, occluded: null}; + let placed: PartialPlacedCollisionBox = {box: null, offscreen: null, occluded: null}; + let placedVerticalText: PartialPlacedCollisionBox = {box: null, offscreen: null, occluded: null}; let placedGlyphBoxes = null; let placedGlyphCircles = null; @@ -527,7 +531,7 @@ export class Placement { return previousOrientation; }; - const placeTextForPlacementModes = (placeHorizontalFn, placeVerticalFn) => { + const placeTextForPlacementModes = (placeHorizontalFn: () => PartialPlacedCollisionBox, placeVerticalFn: () => PartialPlacedCollisionBox) => { if (bucket.allowVerticalPlacement && numVerticalGlyphVertices > 0 && collisionArrays.verticalTextBox) { for (const placementMode of bucket.writingModes) { if (placementMode === WritingMode.vertical) { @@ -555,11 +559,11 @@ export class Placement { return placedFeature; }; - const placeHorizontal = () => { + const placeHorizontal: () => PlacedCollisionBox = () => { return placeBox(textBox, WritingMode.horizontal); }; - const placeVertical = () => { + const placeVertical: () => PlacedCollisionBox | PartialPlacedCollisionBox = () => { const verticalTextBox = collisionArrays.verticalTextBox; if (bucket.allowVerticalPlacement && numVerticalGlyphVertices > 0 && verticalTextBox) { updateBoxData(verticalTextBox); @@ -568,7 +572,11 @@ export class Placement { return {box: null, offscreen: null, occluded: null}; }; - placeTextForPlacementModes(placeHorizontal, placeVertical); + placeTextForPlacementModes( + ((placeHorizontal: any): () => PartialPlacedCollisionBox), + ((placeVertical: any): () => PartialPlacedCollisionBox), + ); + updatePreviousOrientationIfNotPlaced(placed && placed.box && placed.box.length); } else { @@ -593,7 +601,7 @@ export class Placement { const variableIconBox = hasIconTextFit && !iconAllowOverlap ? collisionIconBox : null; if (variableIconBox) updateBoxData(variableIconBox); - let placedBox: ?{ box: Array, offscreen: boolean, occluded: boolean } = {box: [], offscreen: false, occluded: false}; + let placedBox: PartialPlacedCollisionBox = {box: [], offscreen: false, occluded: false}; const placementAttempts = textAllowOverlap ? anchors.length * 2 : anchors.length; for (let i = 0; i < placementAttempts; ++i) { const anchor = anchors[i % anchors.length]; @@ -605,7 +613,7 @@ export class Placement { partiallyEvaluatedTextSize, partiallyEvaluatedIconSize); if (result) { - placedBox = result.placedGlyphBoxes; + placedBox = ((result.placedGlyphBoxes: any): PartialPlacedCollisionBox); if (placedBox && placedBox.box && placedBox.box.length) { placeText = true; shift = result.shift; diff --git a/src/symbol/projection.js b/src/symbol/projection.js index ea3cce3fab9..72f9d3f7801 100644 --- a/src/symbol/projection.js +++ b/src/symbol/projection.js @@ -269,7 +269,7 @@ function updateLineLabels(bucket: SymbolBucket, const aspectRatio = painter.transform.width / painter.transform.height; - let useVertical = false; + let useVertical: ?boolean = false; let prevWritingMode; for (let s = 0; s < placedSymbols.length; s++) { @@ -628,7 +628,7 @@ function placeGlyphAlongLine( const prevToCurrent = vec3.sub([], current, prev); const labelPlanePoint = vec3.scaleAndAdd([], prev, prevToCurrent, segmentInterpolationT); - let axisZ = [0, 0, 1]; + let axisZ: Vec3 = [0, 0, 1]; let diffX = prevToCurrent[0]; let diffY = prevToCurrent[1]; diff --git a/src/terrain/elevation.js b/src/terrain/elevation.js index 3e1ace442aa..92a4878c754 100644 --- a/src/terrain/elevation.js +++ b/src/terrain/elevation.js @@ -73,7 +73,7 @@ export class Elevation { * point elevation, returns `defaultIfNotLoaded`. * Doesn't invoke network request to fetch the data. */ - getAtPoint(point: MercatorCoordinate, defaultIfNotLoaded: ?number, exaggerated: boolean = true): number | null { + getAtPoint(point: MercatorCoordinate, defaultIfNotLoaded: ?number, exaggerated: boolean = true): ?number { if (this.isUsingMockSource()) { return null; } diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index 0e06b2a9d4d..d1b9a9b5cc6 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -1033,7 +1033,7 @@ export class Terrain extends Elevation { return; } - const batches = []; + const batches: Array = []; let currentLayer = 0; let layer = this._style._layers[layerIds[currentLayer]]; @@ -1041,7 +1041,7 @@ export class Terrain extends Elevation { layer = this._style._layers[layerIds[currentLayer]]; } - let batchStart; + let batchStart: number | void; for (; currentLayer < layerCount; ++currentLayer) { const layer = this._style._layers[layerIds[currentLayer]]; if (layer.isHidden(this.painter.transform.zoom)) { @@ -1414,7 +1414,7 @@ export class Terrain extends Elevation { // caching "not found" results along the lookup, to leave the lookup early. // Not found is cached by this._findCoveringTileCache[key] = null; _findTileCoveringTileID(tileID: OverscaledTileID, sourceCache: SourceCache): ?Tile { - let tile = sourceCache.getTile(tileID); + let tile: ?Tile = sourceCache.getTile(tileID); if (tile && tile.hasData()) return tile; const lookup = this._findCoveringTileCache[sourceCache.id]; diff --git a/src/ui/camera.js b/src/ui/camera.js index 4cfc1cc53db..d3fba62e513 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -762,7 +762,7 @@ class Camera extends Evented { * const elevation = map.queryTerrainElevation(coordinate); * @see [Example: Query terrain elevation](https://docs.mapbox.com/mapbox-gl-js/example/query-terrain-elevation/) */ - queryTerrainElevation(lnglat: LngLatLike, options: ?ElevationQueryOptions): number | null { + queryTerrainElevation(lnglat: LngLatLike, options: ?ElevationQueryOptions): ?number { const elevation = this.transform.elevation; if (elevation) { options = extend({}, {exaggerated: true}, options); diff --git a/src/ui/map.js b/src/ui/map.js index 1a3f3c1a835..762851bd012 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -2870,7 +2870,7 @@ class Map extends Camera { let transformValues; let transformScaleWidth; let transformScaleHeight; - let el = this._container; + let el: ?Element = this._container; while (el && (!transformScaleWidth || !transformScaleHeight)) { const transformMatrix = window.getComputedStyle(el).transform; if (transformMatrix && transformMatrix !== 'none') { diff --git a/src/util/browser.js b/src/util/browser.js index 742a161fcf4..4a0a70e903e 100755 --- a/src/util/browser.js +++ b/src/util/browser.js @@ -7,7 +7,7 @@ let linkEl; let reducedMotionQuery: MediaQueryList; -let stubTime; +let stubTime: number | void; let canvas; diff --git a/src/util/global_worker_pool.js b/src/util/global_worker_pool.js index a7eee422ebf..898b87e623f 100644 --- a/src/util/global_worker_pool.js +++ b/src/util/global_worker_pool.js @@ -2,7 +2,7 @@ import WorkerPool, {PRELOAD_POOL_ID} from './worker_pool.js'; -let globalWorkerPool; +let globalWorkerPool: ?WorkerPool; /** * Creates (if necessary) and returns the single, global WorkerPool instance From 5070eddc5a60e4e03b534ea49e19c9b976c8c7a7 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 17:09:42 +0200 Subject: [PATCH 60/72] Upgrade Flow to v0.185.0 --- .flowconfig | 3 +-- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.flowconfig b/.flowconfig index ade6637dfad..6e7cdc0f3f8 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,10 +30,9 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.184.0 +0.185.0 [options] -inference_mode=constrain_writes [strict] nonstrict-import diff --git a/package.json b/package.json index d02bc7fbd52..69184f23462 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.184.0", + "flow-bin": "0.185.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 0665dcd0e51..39006fac161 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.184.0: - version "0.184.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.184.0.tgz#0256b3c302ce465b60d0f0296273840d38d3f9e6" - integrity sha512-HiHuxhO06dqhV7YabluSswm3ZgxVi2L+aArcuIJMON/CRzqkGQrRjIVNbKllMs95rFk6aeuFR3FdVCCUa0SbGw== +flow-bin@0.185.0: + version "0.185.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.185.0.tgz#5110cd9ac6cb6716ec864acafe54007e0a13aeff" + integrity sha512-Kl6QdphjpAhD0ieohtmiOttB1Y4J8t7ucJepeovuFt7xFwTRhBA3j3XOST4jSpuEx7ijhP64A/T3bU6W810iYw== follow-redirects@^1.0.0: version "1.15.1" From 4f8f733c7bff53f4885fd127f854a8ec0d31f0e9 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 17:10:21 +0200 Subject: [PATCH 61/72] Upgrade Flow to v0.186.0 --- .flowconfig | 2 +- package.json | 2 +- src/util/config.js | 6 +++--- yarn.lock | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.flowconfig b/.flowconfig index 6e7cdc0f3f8..eb5f54c8629 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.185.0 +0.186.0 [options] diff --git a/package.json b/package.json index 69184f23462..796af75b969 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.185.0", + "flow-bin": "0.186.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/util/config.js b/src/util/config.js index 6413bb2e778..fb27b49d9e7 100644 --- a/src/util/config.js +++ b/src/util/config.js @@ -54,10 +54,10 @@ const config: Config = { return /^((https?:)?\/\/)?api\.mapbox\.c(n|om)(\/mapbox-gl-js\/)(.*$)/i; }, get EVENTS_URL() { - if (!this.API_URL) { return null; } - if (this.API_URL.indexOf('https://api.mapbox.cn') === 0) { + if (!config.API_URL) { return null; } + if (config.API_URL.indexOf('https://api.mapbox.cn') === 0) { return 'https://events.mapbox.cn/events/v2'; - } else if (this.API_URL.indexOf('https://api.mapbox.com') === 0) { + } else if (config.API_URL.indexOf('https://api.mapbox.com') === 0) { return 'https://events.mapbox.com/events/v2'; } else { return null; diff --git a/yarn.lock b/yarn.lock index 39006fac161..273ea6f62a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.185.0: - version "0.185.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.185.0.tgz#5110cd9ac6cb6716ec864acafe54007e0a13aeff" - integrity sha512-Kl6QdphjpAhD0ieohtmiOttB1Y4J8t7ucJepeovuFt7xFwTRhBA3j3XOST4jSpuEx7ijhP64A/T3bU6W810iYw== +flow-bin@0.186.0: + version "0.186.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.186.0.tgz#b60b720dea977516db8e77fdd3911f66e86c6378" + integrity sha512-p5g03TsAipO6Wu22rpphRZQ9EYuhVWANN698MRLWRyP7ZtkEyCpFHDfmzRHM4Ym/tbCQCMdLJ9JX/GmuiDsB2w== follow-redirects@^1.0.0: version "1.15.1" From 234501ca4c5cd7dba3d19f6262418f230de4673a Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 17:11:27 +0200 Subject: [PATCH 62/72] Upgrade Flow to v0.187.0 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index eb5f54c8629..9314b9db6ef 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.186.0 +0.187.0 [options] diff --git a/package.json b/package.json index 796af75b969..38353d92d32 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.186.0", + "flow-bin": "0.187.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 273ea6f62a6..2b5bbe3f408 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.186.0: - version "0.186.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.186.0.tgz#b60b720dea977516db8e77fdd3911f66e86c6378" - integrity sha512-p5g03TsAipO6Wu22rpphRZQ9EYuhVWANN698MRLWRyP7ZtkEyCpFHDfmzRHM4Ym/tbCQCMdLJ9JX/GmuiDsB2w== +flow-bin@0.187.0: + version "0.187.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.187.0.tgz#3a2a0331f926b9ce285d726108f8a9af9158e13e" + integrity sha512-f0c3e7D33+xYT7comdqPtckBeSxlaQq+NtckDarGM0LU3ofgDQfMzt0XlyWjL9/8y3QzwwaUdM0JJbI7hTiscA== follow-redirects@^1.0.0: version "1.15.1" From ce6bece02fee2553ccff1869d65b25c45a35a9e1 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 17:22:54 +0200 Subject: [PATCH 63/72] Upgrade Flow to v0.188.0 --- .flowconfig | 2 +- package.json | 2 +- src/geo/transform.js | 1 + src/render/painter.js | 1 + src/style-spec/expression/definitions/assertion.js | 4 ++-- src/style/style.js | 1 + src/ui/camera.js | 1 + src/ui/control/attribution_control.js | 1 + yarn.lock | 8 ++++---- 9 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.flowconfig b/.flowconfig index 9314b9db6ef..582663c86cc 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,7 +30,7 @@ .*/test/build/downstream-flow-fixture/.* [version] -0.187.0 +0.188.0 [options] diff --git a/package.json b/package.json index 38353d92d32..73e5e197fab 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-html": "^7.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "flow-bin": "0.187.0", + "flow-bin": "0.188.0", "gl": "6.0.2", "glob": "^8.0.3", "is-builtin-module": "^3.2.0", diff --git a/src/geo/transform.js b/src/geo/transform.js index e8014e29e43..2bb2f678a1f 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -580,6 +580,7 @@ class Transform { let changed = false; if (options.orientation && !quat.exactEquals(options.orientation, this._camera.orientation)) { + // $FlowFixMe[incompatible-call] - Flow can't infer that orientation is not null changed = this._setCameraOrientation(options.orientation); } diff --git a/src/render/painter.js b/src/render/painter.js index 81f4cb436ec..33065b58ef0 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -555,6 +555,7 @@ class Painter { // With terrain on, renders the depth buffer into a texture. // This texture is used for occlusion testing (labels) if (this.terrain && (this.style.hasSymbolLayers() || this.style.hasCircleLayers())) { + // $FlowFixMe[incompatible-use] - Flow can't infer that terrain is not null this.terrain.drawDepth(); } diff --git a/src/style-spec/expression/definitions/assertion.js b/src/style-spec/expression/definitions/assertion.js index 0c4a66ce5ae..98651af8571 100644 --- a/src/style-spec/expression/definitions/assertion.js +++ b/src/style-spec/expression/definitions/assertion.js @@ -56,7 +56,7 @@ class Assertion implements Expression { itemType = ValueType; } - let N; + let N: ?number; if (args.length > 3) { if (args[2] !== null && (typeof args[2] !== 'number' || @@ -65,7 +65,7 @@ class Assertion implements Expression { ) { return context.error('The length argument to "array" must be a positive integer literal', 2); } - N = args[2]; + N = ((args[2]: any): number); i++; } diff --git a/src/style/style.js b/src/style/style.js index 5855a5b6abb..53069b4b4a2 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -338,6 +338,7 @@ class Style extends Evented { this.light = new Light(this.stylesheet.light); if (this.stylesheet.terrain && !this.terrainSetForDrapingOnly()) { + // $FlowFixMe[incompatible-call] - Flow can't infer that terrain is not undefined this._createTerrain(this.stylesheet.terrain, DrapeRenderMode.elevated); } if (this.stylesheet.fog) { diff --git a/src/ui/camera.js b/src/ui/camera.js index d3fba62e513..16da35a91da 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -1066,6 +1066,7 @@ class Camera extends Evented { } if (options.padding != null && !tr.isPaddingEqual(options.padding)) { + // $FlowFixMe[incompatible-type] - Flow can't infer that padding is not null here tr.padding = options.padding; } diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 3fb0092d8ff..750cdf97cc9 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -161,6 +161,7 @@ class AttributionControl { if (sourceCache.used) { const source = sourceCache.getSource(); if (source.attribution && attributions.indexOf(source.attribution) < 0) { + // $FlowFixMe[incompatible-call] - Flow can't infer that attribution is a string attributions.push(source.attribution); } } diff --git a/yarn.lock b/yarn.lock index 2b5bbe3f408..a79fba72b17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,10 +3462,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@0.187.0: - version "0.187.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.187.0.tgz#3a2a0331f926b9ce285d726108f8a9af9158e13e" - integrity sha512-f0c3e7D33+xYT7comdqPtckBeSxlaQq+NtckDarGM0LU3ofgDQfMzt0XlyWjL9/8y3QzwwaUdM0JJbI7hTiscA== +flow-bin@0.188.0: + version "0.188.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.188.0.tgz#5d6e2c918fdf1d1d1d083f739ad8812ecd6f0b46" + integrity sha512-VGCvufWjCQgbM+S2inUeq4mpU/cSAVKUrJFn0iQ+68dHnQdXUdDKd0Mtt18mToPgu3Dsxgi20529V5vlyzYKEQ== follow-redirects@^1.0.0: version "1.15.1" From 1f789b38e2182ed2e6831805c7a95e78789742c2 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 17:34:40 +0200 Subject: [PATCH 64/72] Fix src/geo/transform.js --- src/geo/transform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/geo/transform.js b/src/geo/transform.js index 2bb2f678a1f..15f42587ab3 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -81,7 +81,7 @@ class Transform { mercatorFogMatrix: Float32Array; // Projection from world coordinates (mercator scaled by worldSize) to clip coordinates - projMatrix: Float32Array | Float64Array; + projMatrix: Array | Float32Array | Float64Array; invProjMatrix: Float64Array; // Same as projMatrix, pixel-aligned to avoid fractional pixels for raster tiles @@ -1875,7 +1875,7 @@ class Transform { cameraToClip[8] = -offset.x * 2 / this.width; cameraToClip[9] = offset.y * 2 / this.height; - let m: Float32Array | Float64Array = mat4.mul(new Float64Array([]), cameraToClip, worldToCamera); + let m: Array | Float32Array | Float64Array = mat4.mul([], cameraToClip, worldToCamera); if (this.projection.isReprojectedInTileSpace) { // Projections undistort as you zoom in (shear, scale, rotate). From cb52c7173899b103ed82be42e4e6c9322b1b1708 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 17:53:39 +0200 Subject: [PATCH 65/72] Cleanup --- src/render/uniform_binding.js | 2 +- src/source/custom_source.js | 6 +- src/source/geojson_source.js | 2 +- src/source/source_cache.js | 4 +- src/source/video_source.js | 2 +- src/source/worker.js | 2 +- src/style-spec/validate_style.min.js | 6 +- src/style/properties.js | 4 +- src/style/style_layer/circle_style_layer.js | 32 +++--- src/style/style_layer/custom_style_layer.js | 4 +- .../style_layer/fill_extrusion_style_layer.js | 108 +++++++++--------- src/style/style_layer/fill_style_layer.js | 29 +++-- src/style/style_layer/heatmap_style_layer.js | 27 +++-- src/style/style_layer/line_style_layer.js | 40 ++++--- 14 files changed, 148 insertions(+), 120 deletions(-) diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index cfb4605ee2f..67d1d5a93aa 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -17,7 +17,7 @@ class Uniform { this.initialized = false; } - fetchUniformLocation: ((program: WebGLProgram, name: string) => boolean) = (program: WebGLProgram, name: string): boolean => { + fetchUniformLocation: (program: WebGLProgram, name: string) => boolean = (program, name) => { if (!this.location && !this.initialized) { this.location = this.gl.getUniformLocation(program, name); this.initialized = true; diff --git a/src/source/custom_source.js b/src/source/custom_source.js index f96e201731d..2edfe59fde3 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -335,7 +335,7 @@ class CustomSource extends Evented implements Source { return false; } - _coveringTiles: (() => Array<{ x: number, y: number, z: number, ... }>) = (): Array<{ z: number, x: number, y: number }> => { + _coveringTiles: () => { z: number, x: number, y: number }[] = () => { const tileIDs = this._map.transform.coveringTiles({ tileSize: this.tileSize, minzoom: this.minzoom, @@ -346,11 +346,11 @@ class CustomSource extends Evented implements Source { return tileIDs.map(tileID => ({x: tileID.canonical.x, y: tileID.canonical.y, z: tileID.canonical.z})); } - _clearTiles: (() => void) = () => { + _clearTiles: () => void = () => { this._map.style._clearSource(this.id); } - _update: (() => void) = () => { + _update: () => void = () => { this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); } } diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index f86a013ecc6..362feb27de8 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -148,7 +148,7 @@ class GeoJSONSource extends Evented implements Source { }, options.workerOptions); } - onAdd: (map: Map) => void = (map: Map) => { + onAdd: (map: Map) => void = (map) => { this.map = map; this.setData(this._data); } diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 28c178f9c2d..571395bfc19 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -150,7 +150,7 @@ class SourceCache extends Evented { return this._source.loadTile(tile, callback); } - _unloadTile: ((tile: Tile) => void) = (tile: Tile): void => { + _unloadTile: (tile: Tile) => void = (tile) => { if (this._source.unloadTile) return this._source.unloadTile(tile, () => {}); } @@ -248,7 +248,7 @@ class SourceCache extends Evented { this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); } - _tileLoaded: ((tile: Tile, id: number, previousState: TileState, err: ?Error) => void) = (tile: Tile, id: number, previousState: TileState, err: ?Error) => { + _tileLoaded: (tile: Tile, id: number, previousState: TileState, err: ?Error) => void = (tile, id, previousState, err) => { if (err) { tile.state = 'errored'; if ((err: any).status !== 404) this._source.fire(new ErrorEvent(err, {tile})); diff --git a/src/source/video_source.js b/src/source/video_source.js index f1203bc4399..49cbecde6f9 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -154,7 +154,7 @@ class VideoSource extends ImageSource { return this.video; } - onAdd: (map: Map) => void = (map: Map) => { + onAdd: (map: Map) => void = (map) => { if (this.map) return; this.map = map; this.load(); diff --git a/src/source/worker.js b/src/source/worker.js index 3e561a5151b..6691678424c 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -126,7 +126,7 @@ export default class Worker { callback(); } - enableTerrain: ((mapId: string, enable: boolean, callback: WorkerTileCallback) => void) = (mapId: string, enable: boolean, callback: WorkerTileCallback) => { + enableTerrain: (mapId: string, enable: boolean, callback: WorkerTileCallback) => void = (mapId, enable, callback) => { this.terrain = enable; callback(); }; diff --git a/src/style-spec/validate_style.min.js b/src/style-spec/validate_style.min.js index 28cc1eef01f..a1ab583635d 100644 --- a/src/style-spec/validate_style.min.js +++ b/src/style-spec/validate_style.min.js @@ -15,9 +15,9 @@ import _validateLayoutProperty from './validate/validate_layout_property.js'; import type {StyleSpecification} from './types.js'; export type ValidationError = interface { - message: string, - identifier?: ?string, - line?: ?number, + message: string, + identifier?: ?string, + line?: ?number, }; export type ValidationErrors = $ReadOnlyArray; export type Validator = (Object) => ValidationErrors; diff --git a/src/style/properties.js b/src/style/properties.js index a9f34a1a1c7..c3b931be004 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -107,8 +107,8 @@ export class PropertyValue { // ------- Transitionable ------- export type TransitionParameters = interface { - now: TimePoint, - transition: TransitionSpecification, + now: TimePoint, + transition: TransitionSpecification, }; /** diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index b56ee236d2d..c0873916e46 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -24,6 +24,17 @@ import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {DEMSampler} from '../../terrain/elevation.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; +type QueryIntersectsFeatureFn = ( + queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform, + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler +) => boolean; + class CircleStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; layout: PossiblyEvaluated; @@ -47,27 +58,20 @@ class CircleStyleLayer extends StyleLayer { translateDistance(this.paint.get('circle-translate')); } - queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform, - pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { - - const translation = tilespaceTranslate( + queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { + const translation = tilespaceTranslate( this.paint.get('circle-translate'), this.paint.get('circle-translate-anchor'), - transform.angle, queryGeometry.pixelToTileUnitsFactor); + transform.angle, queryGeometry.pixelToTileUnitsFactor + ); - const size = this.paint.get('circle-radius').evaluate(feature, featureState) + + const size = this.paint.get('circle-radius').evaluate(feature, featureState) + this.paint.get('circle-stroke-width').evaluate(feature, featureState); - return queryIntersectsCircle(queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, + return queryIntersectsCircle(queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, this.paint.get('circle-pitch-alignment') === 'map', this.paint.get('circle-pitch-scale') === 'map', translation, size); - } + } getProgramIds(): Array { return ['circle']; diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index a83454d947b..cdad752a51a 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -225,13 +225,13 @@ class CustomStyleLayer extends StyleLayer { assert(false, "Custom layers cannot be serialized"); } - onAdd: ((map: Map) => void) = (map: Map) => { + onAdd: (map: Map) => void = (map) => { if (this.implementation.onAdd) { this.implementation.onAdd(map, map.painter.context.gl); } } - onRemove: (map: Map) => void = (map: Map) => { + onRemove: (map: Map) => void = (map) => { if (this.implementation.onRemove) { this.implementation.onRemove(map, map.painter.context.gl); } diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index 5fc209ff029..566e122664e 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -22,6 +22,18 @@ import type {DEMSampler} from '../../terrain/elevation.js'; import type {Vec2, Vec4} from 'gl-matrix'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; +type QueryIntersectsFeatureFn = ( + queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform, + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler, + layoutVertexArrayOffset: number +) => boolean | number; + class Point3D extends Point { z: number; @@ -63,57 +75,51 @@ class FillExtrusionStyleLayer extends StyleLayer { return new ProgramConfiguration(this, zoom); } - queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform, - pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler, - layoutVertexArrayOffset: number) => boolean | number = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper, layoutVertexArrayOffset) => { - - const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), - this.paint.get('fill-extrusion-translate-anchor'), - transform.angle, - queryGeometry.pixelToTileUnitsFactor); - const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); - const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); - - const centroid = [0, 0]; - const terrainVisible = elevationHelper && transform.elevation; - const exaggeration = transform.elevation ? transform.elevation.exaggeration() : 1; - const bucket = queryGeometry.tile.getBucket(this); - if (terrainVisible && bucket instanceof FillExtrusionBucket) { - const centroidVertexArray = bucket.centroidVertexArray; - - // See FillExtrusionBucket#encodeCentroid(), centroid is inserted at vertexOffset + 1 - const centroidOffset = layoutVertexArrayOffset + 1; - if (centroidOffset < centroidVertexArray.length) { - centroid[0] = centroidVertexArray.geta_centroid_pos0(centroidOffset); - centroid[1] = centroidVertexArray.geta_centroid_pos1(centroidOffset); - } - } - - // Early exit if fill extrusion is still hidden while waiting for backfill - const isHidden = centroid[0] === 0 && centroid[1] === 1; - if (isHidden) return false; - - if (transform.projection.name === 'globe') { - // Fill extrusion geometry has to be resampled so that large planar polygons - // can be rendered on the curved surface - const bounds = [new Point(0, 0), new Point(EXTENT, EXTENT)]; - const resampledGeometry = resampleFillExtrusionPolygonsForGlobe([geometry], bounds, queryGeometry.tileID.canonical); - geometry = resampledGeometry.map(clipped => clipped.polygon).flat(); - } - - const demSampler = terrainVisible ? elevationHelper : null; - const [projectedBase, projectedTop] = projectExtrusion(transform, geometry, base, height, translation, pixelPosMatrix, demSampler, centroid, exaggeration, transform.center.lat, queryGeometry.tileID.canonical); - - const screenQuery = queryGeometry.queryGeometry; - const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry; - return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry); - } + queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper, layoutVertexArrayOffset) => { + const translation = tilespaceTranslate( + this.paint.get('fill-extrusion-translate'), + this.paint.get('fill-extrusion-translate-anchor'), + transform.angle, + queryGeometry.pixelToTileUnitsFactor + ); + + const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); + const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); + + const centroid = [0, 0]; + const terrainVisible = elevationHelper && transform.elevation; + const exaggeration = transform.elevation ? transform.elevation.exaggeration() : 1; + const bucket = queryGeometry.tile.getBucket(this); + if (terrainVisible && bucket instanceof FillExtrusionBucket) { + const centroidVertexArray = bucket.centroidVertexArray; + + // See FillExtrusionBucket#encodeCentroid(), centroid is inserted at vertexOffset + 1 + const centroidOffset = layoutVertexArrayOffset + 1; + if (centroidOffset < centroidVertexArray.length) { + centroid[0] = centroidVertexArray.geta_centroid_pos0(centroidOffset); + centroid[1] = centroidVertexArray.geta_centroid_pos1(centroidOffset); + } + } + + // Early exit if fill extrusion is still hidden while waiting for backfill + const isHidden = centroid[0] === 0 && centroid[1] === 1; + if (isHidden) return false; + + if (transform.projection.name === 'globe') { + // Fill extrusion geometry has to be resampled so that large planar polygons + // can be rendered on the curved surface + const bounds = [new Point(0, 0), new Point(EXTENT, EXTENT)]; + const resampledGeometry = resampleFillExtrusionPolygonsForGlobe([geometry], bounds, queryGeometry.tileID.canonical); + geometry = resampledGeometry.map(clipped => clipped.polygon).flat(); + } + + const demSampler = terrainVisible ? elevationHelper : null; + const [projectedBase, projectedTop] = projectExtrusion(transform, geometry, base, height, translation, pixelPosMatrix, demSampler, centroid, exaggeration, transform.center.lat, queryGeometry.tileID.canonical); + + const screenQuery = queryGeometry.queryGeometry; + const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry; + return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry); + } } function dot(a, b) { diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index d70a44ede1e..3dd08434bf4 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -19,6 +19,15 @@ import type {LayerSpecification} from '../../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; +type QueryIntersectsFeatureFn = ( + queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform +) => boolean; + class FillStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; layout: PossiblyEvaluated; @@ -65,20 +74,18 @@ class FillStyleLayer extends StyleLayer { return translateDistance(this.paint.get('fill-translate')); } - queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform) => { - if (queryGeometry.queryGeometry.isAboveHorizon) return false; + queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform) => { + if (queryGeometry.queryGeometry.isAboveHorizon) return false; - const translatedPolygon = translate(queryGeometry.tilespaceGeometry, + const translatedPolygon = translate( + queryGeometry.tilespaceGeometry, this.paint.get('fill-translate'), this.paint.get('fill-translate-anchor'), - transform.angle, queryGeometry.pixelToTileUnitsFactor); - return polygonIntersectsMultiPolygon(translatedPolygon, geometry); - } + transform.angle, queryGeometry.pixelToTileUnitsFactor + ); + + return polygonIntersectsMultiPolygon(translatedPolygon, geometry); + } isTileClipped(): boolean { return true; diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index 97c6f6cdf9e..c300ba58364 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -24,6 +24,17 @@ import type Transform from '../../geo/transform.js'; import type CircleBucket from '../../data/bucket/circle_bucket.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; +type QueryIntersectsFeatureFn = ( + queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform, + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler +) => boolean; + class HeatmapStyleLayer extends StyleLayer { heatmapFbo: ?Framebuffer; @@ -72,20 +83,12 @@ class HeatmapStyleLayer extends StyleLayer { return getMaximumPaintValue('heatmap-radius', this, ((bucket: any): CircleBucket<*>)); } - queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform, - pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { - - const size = this.paint.get('heatmap-radius').evaluate(feature, featureState); - return queryIntersectsCircle( + queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { + const size = this.paint.get('heatmap-radius').evaluate(feature, featureState); + return queryIntersectsCircle( queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, true, true, new Point(0, 0), size); - } + } hasOffscreenPass(): boolean { return this.paint.get('heatmap-opacity') !== 0 && this.visibility !== 'none'; diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 1d0ac273cc4..f4644142421 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -21,6 +21,15 @@ import type {LayerSpecification} from '../../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; +type QueryIntersectsFeatureFn = ( + queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform +) => boolean; + class LineFloorwidthProperty extends DataDrivenProperty { useIntegerZoom = true; @@ -105,28 +114,27 @@ class LineStyleLayer extends StyleLayer { return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate')); } - queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform) => boolean = (queryGeometry, feature, featureState, geometry, zoom, transform) => { - if (queryGeometry.queryGeometry.isAboveHorizon) return false; + queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform) => { + if (queryGeometry.queryGeometry.isAboveHorizon) return false; - const translatedPolygon = translate(queryGeometry.tilespaceGeometry, + const translatedPolygon = translate( + queryGeometry.tilespaceGeometry, this.paint.get('line-translate'), this.paint.get('line-translate-anchor'), - transform.angle, queryGeometry.pixelToTileUnitsFactor); - const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( + transform.angle, queryGeometry.pixelToTileUnitsFactor + ); + + const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( this.paint.get('line-width').evaluate(feature, featureState), this.paint.get('line-gap-width').evaluate(feature, featureState)); - const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState); - if (lineOffset) { - geometry = offsetLine(geometry, lineOffset * queryGeometry.pixelToTileUnitsFactor); - } - return polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth); - } + const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState); + if (lineOffset) { + geometry = offsetLine(geometry, lineOffset * queryGeometry.pixelToTileUnitsFactor); + } + + return polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth); + } isTileClipped(): boolean { return true; From 17eef44e30ea1fac2769155a2fbc95d603fa6b19 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 18:04:46 +0200 Subject: [PATCH 66/72] Cleanup formatting --- src/style/style_layer/circle_style_layer.js | 3 +-- src/style/style_layer/fill_extrusion_style_layer.js | 10 ++++------ src/style/style_layer/fill_style_layer.js | 6 ++---- src/style/style_layer/line_style_layer.js | 7 ++----- src/terrain/terrain.js | 4 ++-- src/ui/camera.js | 2 +- src/ui/control/attribution_control.js | 6 +++--- src/ui/control/fullscreen_control.js | 4 ++-- src/ui/control/geolocate_control.js | 6 +++--- src/ui/control/logo_control.js | 2 +- src/ui/control/navigation_control.js | 6 +++--- src/ui/hash.js | 2 +- src/ui/marker.js | 6 +++--- src/util/scheduler.js | 2 +- 14 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index c0873916e46..07b7fbb81d4 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -62,8 +62,7 @@ class CircleStyleLayer extends StyleLayer { const translation = tilespaceTranslate( this.paint.get('circle-translate'), this.paint.get('circle-translate-anchor'), - transform.angle, queryGeometry.pixelToTileUnitsFactor - ); + transform.angle, queryGeometry.pixelToTileUnitsFactor); const size = this.paint.get('circle-radius').evaluate(feature, featureState) + this.paint.get('circle-stroke-width').evaluate(feature, featureState); diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index 566e122664e..d662604ff65 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -76,12 +76,10 @@ class FillExtrusionStyleLayer extends StyleLayer { } queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper, layoutVertexArrayOffset) => { - const translation = tilespaceTranslate( - this.paint.get('fill-extrusion-translate'), - this.paint.get('fill-extrusion-translate-anchor'), - transform.angle, - queryGeometry.pixelToTileUnitsFactor - ); + const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), + this.paint.get('fill-extrusion-translate-anchor'), + transform.angle, + queryGeometry.pixelToTileUnitsFactor); const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index 3dd08434bf4..36b322b03bd 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -77,12 +77,10 @@ class FillStyleLayer extends StyleLayer { queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform) => { if (queryGeometry.queryGeometry.isAboveHorizon) return false; - const translatedPolygon = translate( - queryGeometry.tilespaceGeometry, + const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('fill-translate'), this.paint.get('fill-translate-anchor'), - transform.angle, queryGeometry.pixelToTileUnitsFactor - ); + transform.angle, queryGeometry.pixelToTileUnitsFactor); return polygonIntersectsMultiPolygon(translatedPolygon, geometry); } diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index f4644142421..7533cf91910 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -117,17 +117,14 @@ class LineStyleLayer extends StyleLayer { queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform) => { if (queryGeometry.queryGeometry.isAboveHorizon) return false; - const translatedPolygon = translate( - queryGeometry.tilespaceGeometry, + const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('line-translate'), this.paint.get('line-translate-anchor'), - transform.angle, queryGeometry.pixelToTileUnitsFactor - ); + transform.angle, queryGeometry.pixelToTileUnitsFactor); const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( this.paint.get('line-width').evaluate(feature, featureState), this.paint.get('line-gap-width').evaluate(feature, featureState)); - const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState); if (lineOffset) { geometry = offsetLine(geometry, lineOffset * queryGeometry.pixelToTileUnitsFactor); diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index d1b9a9b5cc6..391041dba2e 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -354,7 +354,7 @@ export class Terrain extends Elevation { return demScale * proxyTileSize; } - _checkRenderCacheEfficiency: (() => void) = () => { + _checkRenderCacheEfficiency: () => void = () => { const renderCacheInfo = this.renderCacheEfficiency(this._style); if (this._style.map._optimizeForTerrain) { assert(renderCacheInfo.efficiency === 100); @@ -365,7 +365,7 @@ export class Terrain extends Elevation { } } - _onStyleDataEvent: ((event: any) => void) = (event: any) => { + _onStyleDataEvent: (event: any) => void = (event) => { if (event.coord && event.dataType === 'source') { this._clearRenderCacheForTile(event.sourceCacheId, event.coord); } else if (event.dataType === 'style') { diff --git a/src/ui/camera.js b/src/ui/camera.js index 16da35a91da..fc4e7546b07 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -1724,7 +1724,7 @@ class Camera extends Evented { } // Callback for map._requestRenderFrame - _renderFrameCallback: (() => void) = () => { + _renderFrameCallback: () => void = () => { const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1); const frame = this._onEaseFrame; if (frame) frame(this._easeOptions.easing(t)); diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 750cdf97cc9..0e7e668cd89 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -103,7 +103,7 @@ class AttributionControl { if (element.firstElementChild) element.firstElementChild.setAttribute('title', str); } - _toggleAttribution: (() => void) = () => { + _toggleAttribution: () => void = () => { if (this._container.classList.contains('mapboxgl-compact-show')) { this._container.classList.remove('mapboxgl-compact-show'); this._compactButton.setAttribute('aria-expanded', 'false'); @@ -113,7 +113,7 @@ class AttributionControl { } }; - _updateEditLink: (() => void) = () => { + _updateEditLink: () => void = () => { let editLink = this._editLink; if (!editLink) { editLink = this._editLink = (this._container.querySelector('.mapbox-improve-map'): any); @@ -201,7 +201,7 @@ class AttributionControl { this._editLink = null; } - _updateCompact: (() => void) = () => { + _updateCompact: () => void = () => { if (this._map.getCanvasContainer().offsetWidth <= 640) { this._container.classList.add('mapboxgl-compact'); } else { diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index 8121d268eb7..4406c6d225b 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -101,7 +101,7 @@ class FullscreenControl { return this._fullscreen; } - _changeIcon: (() => void) = () => { + _changeIcon: () => void = () => { const fullscreenElement = window.document.fullscreenElement || (window.document: any).webkitFullscreenElement; @@ -114,7 +114,7 @@ class FullscreenControl { } }; - _onClickFullscreen: (() => void) = () => { + _onClickFullscreen: () => void = () => { if (this._isFullscreen()) { if (window.document.exitFullscreen) { (window.document: any).exitFullscreen(); diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 0aaffbbb6e6..09eaaf14e9d 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -347,7 +347,7 @@ class GeolocateControl extends Evented { this._circleElement.style.height = `${circleDiameter}px`; } - _onZoom: (() => void) = () => { + _onZoom: () => void = () => { if (this.options.showUserLocation && this.options.showAccuracyCircle) { this._updateCircleRadius(); } @@ -358,7 +358,7 @@ class GeolocateControl extends Evented { * * @private */ - _updateMarkerRotation: (() => void) = () => { + _updateMarkerRotation: () => void = () => { if (this._userLocationDotMarker && typeof this._heading === 'number') { this._userLocationDotMarker.setRotation(this._heading); this._dotElement.classList.add('mapboxgl-user-location-show-heading'); @@ -411,7 +411,7 @@ class GeolocateControl extends Evented { this._finish(); } - _finish: (() => void) = () => { + _finish: () => void = () => { if (this._timeoutId) { clearTimeout(this._timeoutId); } this._timeoutId = undefined; }; diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index faa7e591e1c..d53bf1f5e90 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -74,7 +74,7 @@ class LogoControl { return true; } - _updateCompact: (() => void) = () => { + _updateCompact: () => void = () => { const containerChildren = this._container.children; if (containerChildren.length) { const anchor = containerChildren[0]; diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index 8b8897e4e70..f3823d100fd 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -84,7 +84,7 @@ class NavigationControl { } } - _updateZoomButtons: (() => void) = () => { + _updateZoomButtons: () => void = () => { const map = this._map; if (!map) return; @@ -97,7 +97,7 @@ class NavigationControl { this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); }; - _rotateCompassArrow: (() => void) = () => { + _rotateCompassArrow: () => void = () => { const map = this._map; if (!map) return; @@ -268,7 +268,7 @@ class MouseRotateWrapper { this.reset(); }; - reset: (() => void) = () => { + reset: () => void = () => { this.mouseRotate.reset(); if (this.mousePitch) this.mousePitch.reset(); delete this._startPos; diff --git a/src/ui/hash.js b/src/ui/hash.js index ce3a0dab0eb..563af94a6b0 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -119,7 +119,7 @@ export default class Hash { return false; } - _updateHashUnthrottled: (() => void) = () => { + _updateHashUnthrottled: () => void = () => { // Replace if already present, else append the updated hash string const location = window.location.href.replace(/(#.+)?$/, this.getHashString()); window.history.replaceState(window.history.state, null, location); diff --git a/src/ui/marker.js b/src/ui/marker.js index 605c4b70419..2c8c3f08f79 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -429,7 +429,7 @@ export default class Marker extends Evented { } - _evaluateOpacity: (() => void) = () => { + _evaluateOpacity: () => void = () => { const map = this._map; if (!map) return; @@ -459,7 +459,7 @@ export default class Marker extends Evented { this._fadeTimer = null; } - _clearFadeTimer: (() => void) = () => { + _clearFadeTimer: () => void = () => { if (this._fadeTimer) { clearTimeout(this._fadeTimer); this._fadeTimer = null; @@ -658,7 +658,7 @@ export default class Marker extends Evented { this.fire(new Event('drag')); } - _onUp: (() => void) = () => { + _onUp: () => void = () => { // revert to normal pointer event handling this._element.style.pointerEvents = 'auto'; this._positionDelta = null; diff --git a/src/util/scheduler.js b/src/util/scheduler.js index 862ed9eb937..a65827bdd81 100644 --- a/src/util/scheduler.js +++ b/src/util/scheduler.js @@ -64,7 +64,7 @@ class Scheduler { }; } - process: (() => void) = () => { + process: () => void = () => { const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; try { this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); From a055f35f1ad3f290296cc983be26b9dc4d93c22d Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 24 Feb 2023 18:22:24 +0200 Subject: [PATCH 67/72] Fix layoutType in program_configuration.js --- src/data/program_configuration.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 6e6ec3c71ff..22d627f8bbf 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -669,9 +669,8 @@ const defaultLayouts = { }; function layoutType(property, type, binderType) { - assert(type === 'color' || type === 'number', `Unknown layout type: ${type}`); const layoutException = propertyExceptions[property]; - // $FlowFixMe[prop-missing] - assert above ensures that type is a valid key + // $FlowFixMe[prop-missing] - we don't cover all types in defaultLayouts for some reason return (layoutException && layoutException[binderType]) || defaultLayouts[type][binderType]; } From 64bdec6546aab25766fe2014dbcd0f4bdd6da3fe Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Mon, 27 Feb 2023 14:50:59 +0200 Subject: [PATCH 68/72] Fix ProgramConfiguration constructor --- src/data/program_configuration.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 22d627f8bbf..e404891a3d1 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -398,24 +398,21 @@ export default class ProgramConfiguration { keys.push(`/u_${property}`); } else if (expression.kind === 'source' || sourceException || isPattern) { - assert(expression.kind === 'composite' || expression.kind === 'source', `Unexpected expression kind ${expression.kind} in program configuration`); const StructArrayLayout = layoutType(property, type, 'source'); this.binders[property] = isPattern ? // $FlowFixMe[prop-missing] - // $FlowFixMe[incompatible-call] - assert should refine kind + // $FlowFixMe[incompatible-call] - expression can be a `composite` or `constant` kind expression new PatternCompositeBinder(expression, names, type, StructArrayLayout, layer.id) : // $FlowFixMe[prop-missing] - // $FlowFixMe[incompatible-call] - assert should refine kind + // $FlowFixMe[incompatible-call] - expression can be a `composite` or `constant` kind expression new SourceExpressionBinder(expression, names, type, StructArrayLayout); keys.push(`/a_${property}`); } else { const StructArrayLayout = layoutType(property, type, 'composite'); - - assert(expression.kind === 'composite', `Unexpected expression kind ${expression.kind} in program configuration`); // $FlowFixMe[prop-missing] - // $FlowFixMe[incompatible-call] - assert should refine kind + // $FlowFixMe[incompatible-call] — expression can be a `constant` kind expression this.binders[property] = new CompositeExpressionBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout); keys.push(`/z_${property}`); } From 3a389625b3ff3f8b3f7a68abe0145c2be313c18e Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Mon, 27 Feb 2023 16:09:47 +0200 Subject: [PATCH 69/72] Fix EVENTS_URL sanitization --- src/util/config.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/util/config.js b/src/util/config.js index fb27b49d9e7..1286120f5fd 100644 --- a/src/util/config.js +++ b/src/util/config.js @@ -55,11 +55,16 @@ const config: Config = { }, get EVENTS_URL() { if (!config.API_URL) { return null; } - if (config.API_URL.indexOf('https://api.mapbox.cn') === 0) { - return 'https://events.mapbox.cn/events/v2'; - } else if (config.API_URL.indexOf('https://api.mapbox.com') === 0) { - return 'https://events.mapbox.com/events/v2'; - } else { + try { + const url = new URL(config.API_URL); + if (url.hostname === 'api.mapbox.cn') { + return 'https://events.mapbox.cn/events/v2'; + } else if (url.hostname === 'api.mapbox.com') { + return 'https://events.mapbox.com/events/v2'; + } else { + return null; + } + } catch (e) { return null; } }, From 3a85820658212b2865bb8ea097bfe7211807ee14 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Mon, 27 Feb 2023 17:21:26 +0200 Subject: [PATCH 70/72] Cleanup --- src/source/source.js | 3 +- src/style/evaluation_parameters.js | 2 +- src/style/properties.js | 2 +- src/style/style_layer/symbol_style_layer.js | 2 +- src/symbol/grid_index.js | 48 ++++----------------- src/ui/camera.js | 5 ++- src/ui/control/attribution_control.js | 2 +- src/ui/control/geolocate_control.js | 8 ++-- src/ui/control/logo_control.js | 2 +- src/ui/control/navigation_control.js | 12 +++--- src/ui/events.js | 27 ++++++++++++ src/ui/handler/box_zoom.js | 2 +- src/ui/handler/scroll_zoom.js | 2 +- src/ui/handler_manager.js | 6 +-- src/ui/hash.js | 2 +- src/ui/map.js | 6 +-- src/ui/marker.js | 10 ++--- src/util/actor.js | 2 +- src/util/evented.js | 7 +-- src/util/image.js | 4 +- src/util/web_worker.js | 4 +- 21 files changed, 78 insertions(+), 80 deletions(-) diff --git a/src/source/source.js b/src/source/source.js index ed1f9f94d41..a3a1e92a424 100644 --- a/src/source/source.js +++ b/src/source/source.js @@ -8,6 +8,7 @@ import type Map from '../ui/map.js'; import type Tile from './tile.js'; import type {OverscaledTileID} from './tile_id.js'; import type {Callback} from '../types/callback.js'; +import type {MapEvent} from '../ui/events.js'; import {CanonicalTileID} from './tile_id.js'; /** @@ -58,7 +59,7 @@ export interface Source { loaded(): boolean; fire(event: Event): mixed; - on(type: *, listener: (Object) => any): Evented; + on(type: MapEvent, listener: (Object) => any): Evented; setEventedParent(parent: ?Evented, data?: Object | () => Object): Evented; +onAdd?: (map: Map) => void; diff --git a/src/style/evaluation_parameters.js b/src/style/evaluation_parameters.js index 6584faaa00f..2f7f10547a5 100644 --- a/src/style/evaluation_parameters.js +++ b/src/style/evaluation_parameters.js @@ -13,7 +13,7 @@ class EvaluationParameters { transition: TransitionSpecification; // "options" may also be another EvaluationParameters to copy - constructor(zoom: number, options?: *) { + constructor(zoom: number, options?: any) { this.zoom = zoom; if (options) { diff --git a/src/style/properties.js b/src/style/properties.js index c3b931be004..c16e9c34884 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -376,7 +376,7 @@ export class Layout { return clone(this._values[name].value); } - setValue(name: S, value: *) { + setValue(name: S, value: any) { this._values[name] = new PropertyValue(this._values[name].property, value === null ? undefined : clone(value)); } diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 91d3fde249c..3bca5107599 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -96,7 +96,7 @@ class SymbolStyleLayer extends StyleLayer { this._setPaintOverrides(); } - getValueAndResolveTokens(name: *, feature: Feature, canonical: CanonicalTileID, availableImages: Array): string { + getValueAndResolveTokens(name: any, feature: Feature, canonical: CanonicalTileID, availableImages: Array): string { const value = this.layout.get(name).evaluate(feature, {}, canonical, availableImages); const unevaluated = this._unevaluatedLayout._values[name]; if (!unevaluated.isDataDriven() && !isExpression(unevaluated.value) && value) { diff --git a/src/symbol/grid_index.js b/src/symbol/grid_index.js index 60cb3c6336f..91b64fe1633 100644 --- a/src/symbol/grid_index.js +++ b/src/symbol/grid_index.js @@ -87,39 +87,25 @@ class GridIndex { this.circles.push(radius); } - _insertBoxCell: (( + _insertBoxCell: ( x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number - ) => void) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number - ) => { + ) => void = (x1, y1, x2, y2, cellIndex, uid) => { this.boxCells[cellIndex].push(uid); }; - _insertCircleCell: (( + _insertCircleCell: ( x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number - ) => void) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number - ) => { + ) => void = (x1, y1, x2, y2, cellIndex, uid) => { this.circleCells[cellIndex].push(uid); } @@ -200,7 +186,7 @@ class GridIndex { return (this._queryCircle(x, y, radius, true, predicate): any); } - _queryCell: (( + _queryCell: ( x1: number, y1: number, x2: number, @@ -209,16 +195,7 @@ class GridIndex { result: any, queryArgs: any, predicate?: any - ) => void | boolean) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any, - ): void | boolean => { + ) => void | boolean = (x1, y1, x2, y2, cellIndex, result, queryArgs, predicate?) => { const seenUids = queryArgs.seenUids; const boxCell = this.boxCells[cellIndex]; if (boxCell !== null) { @@ -285,7 +262,7 @@ class GridIndex { } } - _queryCellCircle: (( + _queryCellCircle: ( x1: number, y1: number, x2: number, @@ -294,16 +271,7 @@ class GridIndex { result: any, queryArgs: any, predicate?: any - ) => void | boolean) = ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any, - ): void | boolean => { + ) => void | boolean = (x1, y1, x2, y2, cellIndex, result, queryArgs, predicate?) => { const circle = queryArgs.circle; const seenUids = queryArgs.seenUids; const boxCell = this.boxCells[cellIndex]; diff --git a/src/ui/camera.js b/src/ui/camera.js index fc4e7546b07..58d029ed6d6 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -43,6 +43,7 @@ import type {Callback} from '../types/callback.js'; import type {PointLike} from '@mapbox/point-geometry'; import {Aabb} from '../util/primitives.js'; import type {PaddingOptions} from '../geo/edge_insets.js'; +import type {MapEvent} from './events.js'; /** * A helper type: converts all Object type values to non-maybe types. @@ -1782,7 +1783,7 @@ function addAssertions(camera: Camera) { //eslint-disable-line ['drag', 'zoom', 'rotate', 'pitch', 'move'].forEach(name => { inProgress[name] = false; - camera.on(`${name}start`, () => { + camera.on(((`${name}start`: any): MapEvent), () => { assert(!inProgress[name], `"${name}start" fired twice without a "${name}end"`); inProgress[name] = true; assert(inProgress.move); @@ -1793,7 +1794,7 @@ function addAssertions(camera: Camera) { //eslint-disable-line assert(inProgress.move); }); - camera.on(`${name}end`, () => { + camera.on(((`${name}end`: any): MapEvent), () => { assert(inProgress.move); assert(inProgress[name]); inProgress[name] = false; diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 0e7e668cd89..c5062c3d675 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -138,7 +138,7 @@ class AttributionControl { } }; - _updateData: ((e: any) => void) = (e: any) => { + _updateData: (e: any) => void = (e) => { if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style')) { this._updateAttributions(); this._updateEditLink(); diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 09eaaf14e9d..44f75b2f324 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -234,7 +234,7 @@ class GeolocateControl extends Evented { * @param {Position} position the Geolocation API Position * @private */ - _onSuccess: ((position: Position) => void) = (position: Position) => { + _onSuccess: (position: Position) => void = (position) => { if (!this._map) { // control has since been removed return; @@ -368,7 +368,7 @@ class GeolocateControl extends Evented { } }; - _onError: ((error: PositionError) => void) = (error: PositionError) => { + _onError: (error: PositionError) => void = (error) => { if (!this._map) { // control has since been removed return; @@ -416,7 +416,7 @@ class GeolocateControl extends Evented { this._timeoutId = undefined; }; - _setupUI: ((supported: boolean) => void) = (supported: boolean) => { + _setupUI: (supported: boolean) => void = (supported) => { if (this._map === undefined) { // This control was removed from the map before geolocation // support was determined. @@ -508,7 +508,7 @@ class GeolocateControl extends Evented { * geolocate.trigger(); * }); */ - _onDeviceOrientation: ((deviceOrientationEvent: DeviceOrientationEvent) => void) = (deviceOrientationEvent: DeviceOrientationEvent) => { + _onDeviceOrientation: (deviceOrientationEvent: DeviceOrientationEvent) => void = (deviceOrientationEvent) => { // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. if (this._userLocationDotMarker) { if (deviceOrientationEvent.webkitCompassHeading) { diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index d53bf1f5e90..9627f5e2eab 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -54,7 +54,7 @@ class LogoControl { return 'bottom-left'; }; - _updateLogo: ((e: any) => void) = (e: any) => { + _updateLogo: (e: any) => void = (e) => { if (!e || e.sourceDataType === 'metadata') { this._container.style.display = this._logoRequired() ? 'block' : 'none'; } diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index f3823d100fd..2f1eb5f9ecc 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -224,23 +224,23 @@ class MouseRotateWrapper { window.removeEventListener('mouseup', this.mouseup); } - mousedown: ((e: MouseEvent) => void) = (e: MouseEvent) => { + mousedown: (e: MouseEvent) => void = (e) => { this.down(extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), DOM.mousePos(this.element, e)); window.addEventListener('mousemove', this.mousemove); window.addEventListener('mouseup', this.mouseup); }; - mousemove: ((e: MouseEvent) => void) = (e: MouseEvent) => { + mousemove: (e: MouseEvent) => void = (e) => { this.move(e, DOM.mousePos(this.element, e)); }; - mouseup: ((e: MouseEvent) => void) = (e: MouseEvent) => { + mouseup: (e: MouseEvent) => void = (e) => { this.mouseRotate.mouseupWindow(e); if (this.mousePitch) this.mousePitch.mouseupWindow(e); this.offTemp(); }; - touchstart: ((e: TouchEvent) => void) = (e: TouchEvent) => { + touchstart: (e: TouchEvent) => void = (e) => { if (e.targetTouches.length !== 1) { this.reset(); } else { @@ -249,7 +249,7 @@ class MouseRotateWrapper { } }; - touchmove: ((e: TouchEvent) => void) = (e: TouchEvent) => { + touchmove: (e: TouchEvent) => void = (e) => { if (e.targetTouches.length !== 1) { this.reset(); } else { @@ -258,7 +258,7 @@ class MouseRotateWrapper { } }; - touchend: ((e: TouchEvent) => void) = (e: TouchEvent) => { + touchend: (e: TouchEvent) => void = (e) => { if (e.targetTouches.length === 0 && this._startPos && this._lastPos && diff --git a/src/ui/events.js b/src/ui/events.js index 51bd9f8ae75..e310208cfb5 100644 --- a/src/ui/events.js +++ b/src/ui/events.js @@ -1521,4 +1521,31 @@ export type MapEvent = * }); */ | 'speedindexcompleted' + + /** + * Fired after RTL text plugin state changes. + * + * @event pluginStateChange + * @instance + * @private + */ + | 'pluginStateChange' + + /** + * Fired in worker.js after sprite loaded. + * + * @event pluginStateChange + * @instance + * @private + */ + | 'isSpriteLoaded' + + /** + * Fired in style.js after layer order changed. + * + * @event pluginStateChange + * @instance + * @private + */ + | 'neworder' ; diff --git a/src/ui/handler/box_zoom.js b/src/ui/handler/box_zoom.js index 9b2bc437e0d..406b0a22c6f 100644 --- a/src/ui/handler/box_zoom.js +++ b/src/ui/handler/box_zoom.js @@ -175,7 +175,7 @@ class BoxZoomHandler { delete this._lastPos; } - _fireEvent(type: string, e: *): Map { + _fireEvent(type: string, e: any): Map { return this._map.fire(new Event(type, {originalEvent: e})); } } diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 1d4a4a2ec8f..9c4b54f4ee6 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -227,7 +227,7 @@ class ScrollZoomHandler { e.preventDefault(); } - _onTimeout: ((initialEvent: WheelEvent) => void) = (initialEvent: WheelEvent) => { + _onTimeout: (initialEvent: WheelEvent) => void = (initialEvent) => { this._type = 'wheel'; this._delta -= this._lastValue; if (!this._active) { diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 372fa18a07b..c0264f351bd 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -339,7 +339,7 @@ class HandlerManager { return false; } - handleWindowEvent: ((e: InputEvent) => void) = (e: InputEvent) => { + handleWindowEvent: (e: InputEvent) => void = (e) => { this.handleEvent(e, `${e.type}Window`); } @@ -354,7 +354,7 @@ class HandlerManager { return ((mapTouches: any): TouchList); } - handleEvent: ((e: InputEvent | RenderFrameEvent, eventName?: string) => void) = (e: InputEvent | RenderFrameEvent, eventName?: string) => { + handleEvent: (e: InputEvent | RenderFrameEvent, eventName?: string) => void = (e, eventName) => { this._updatingCamera = true; assert(e.timeStamp !== undefined); @@ -673,7 +673,7 @@ class HandlerManager { } - _fireEvent(type: string, e: *) { + _fireEvent(type: string, e: any) { this._map.fire(new Event(type, e ? {originalEvent: e} : {})); } diff --git a/src/ui/hash.js b/src/ui/hash.js index 563af94a6b0..607739937bd 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -102,7 +102,7 @@ export default class Hash { return hash.split('/'); } - _onHashChange: (() => boolean) = (): boolean => { + _onHashChange: () => boolean = () => { const map = this._map; if (!map) return false; const loc = this._getCurrentHash(); diff --git a/src/ui/map.js b/src/ui/map.js index 762851bd012..4e62f2b7181 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -2993,7 +2993,7 @@ class Map extends Camera { webpSupported.testSupport(gl); } - _contextLost: (event: *) => void = (event: *) => { + _contextLost: (event: any) => void = (event) => { event.preventDefault(); if (this._frame) { this._frame.cancel(); @@ -3002,14 +3002,14 @@ class Map extends Camera { this.fire(new Event('webglcontextlost', {originalEvent: event})); } - _contextRestored: (event: *) => void = (event: *) => { + _contextRestored: (event: any) => void = (event) => { this._setupPainter(); this.resize(); this._update(); this.fire(new Event('webglcontextrestored', {originalEvent: event})); } - _onMapScroll: (event: *) => ?boolean = (event: *) => { + _onMapScroll: (event: any) => ?boolean = (event) => { if (event.target !== this._container) return; // Revert any scroll which would move the canvas outside of the view diff --git a/src/ui/marker.js b/src/ui/marker.js index 2c8c3f08f79..d3bfe400d82 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -352,7 +352,7 @@ export default class Marker extends Evented { return this; } - _onKeyPress: ((e: KeyboardEvent) => void) = (e: KeyboardEvent) => { + _onKeyPress: (e: KeyboardEvent) => void = (e) => { const code = e.code; const legacyCode = e.charCode || e.keyCode; @@ -364,7 +364,7 @@ export default class Marker extends Evented { } } - _onMapClick: ((e: MapMouseEvent) => void) = (e: MapMouseEvent) => { + _onMapClick: (e: MapMouseEvent) => void = (e) => { const targetElement = e.originalEvent.target; const element = this._element; @@ -545,7 +545,7 @@ export default class Marker extends Evented { return rotation ? `rotateZ(${rotation}deg)` : ''; } - _update: ((delaySnap?: boolean) => void) = (delaySnap?: boolean) => { + _update: (delaySnap?: boolean) => void = (delaySnap) => { window.cancelAnimationFrame(this._updateFrameId); const map = this._map; if (!map) return; @@ -608,7 +608,7 @@ export default class Marker extends Evented { return this; } - _onMove: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { + _onMove: (e: MapMouseEvent | MapTouchEvent) => void = (e) => { const map = this._map; if (!map) return; @@ -688,7 +688,7 @@ export default class Marker extends Evented { this._state = 'inactive'; } - _addDragHandler: ((e: MapMouseEvent | MapTouchEvent) => void) = (e: MapMouseEvent | MapTouchEvent) => { + _addDragHandler: (e: MapMouseEvent | MapTouchEvent) => void = (e) => { const map = this._map; const pos = this._pos; if (!map || !pos) return; diff --git a/src/util/actor.js b/src/util/actor.js index b981c2c0f5a..255101ff332 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -85,7 +85,7 @@ class Actor { }; } - receive: ((message: any) => void) = (message: Object) => { + receive: (message: any) => void = (message) => { const data = message.data, id = data.id; diff --git a/src/util/evented.js b/src/util/evented.js index 76579900211..19ec3dcd154 100644 --- a/src/util/evented.js +++ b/src/util/evented.js @@ -1,6 +1,7 @@ // @flow import {extend} from './util.js'; +import type {MapEvent} from '../ui/events.js'; type Listener = (Object) => any; type Listeners = {[_: string]: Array }; @@ -67,7 +68,7 @@ export class Evented { * extended with `target` and `type` properties. * @returns {Object} Returns itself to allow for method chaining. */ - on(type: *, listener: Listener): this { + on(type: MapEvent, listener: Listener): this { this._listeners = this._listeners || {}; _addEventListener(type, listener, this._listeners); @@ -81,7 +82,7 @@ export class Evented { * @param {Function} listener The listener function to remove. * @returns {Object} Returns itself to allow for method chaining. */ - off(type: *, listener: Listener): this { + off(type: MapEvent, listener: Listener): this { _removeEventListener(type, listener, this._listeners); _removeEventListener(type, listener, this._oneTimeListeners); @@ -98,7 +99,7 @@ export class Evented { * If not provided, returns a Promise that will be resolved when the event is fired once. * @returns {Object} Returns `this` | Promise. */ - once(type: *, listener?: Listener): this | Promise { + once(type: MapEvent, listener?: Listener): this | Promise { if (!listener) { return new Promise(resolve => this.once(type, resolve)); } diff --git a/src/util/image.js b/src/util/image.js index 673d98ccaad..dffc642484a 100644 --- a/src/util/image.js +++ b/src/util/image.js @@ -5,8 +5,8 @@ import assert from 'assert'; import {register} from './web_worker_transfer.js'; export type Size = interface { - width: number, - height: number, + width: number, + height: number, }; export interface SpritePosition { diff --git a/src/util/web_worker.js b/src/util/web_worker.js index 44c3532a67f..1c69db2bdb0 100644 --- a/src/util/web_worker.js +++ b/src/util/web_worker.js @@ -32,8 +32,8 @@ class MessageBus implements WorkerInterface, WorkerGlobalScopeInterface { addListeners: Array; postListeners: Array; target: MessageBus; - registerWorkerSource: *; - registerRTLTextPlugin: *; + registerWorkerSource: any; + registerRTLTextPlugin: any; constructor(addListeners: Array, postListeners: Array) { this.addListeners = addListeners; From 4ca5ac4818d1a1392db3cb5db5d795edf043bcd1 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Fri, 31 Mar 2023 15:35:45 +0300 Subject: [PATCH 71/72] Revert flow method-unbinding fixes and supress errors --- src/data/bucket/circle_bucket.js | 1 + src/data/bucket/fill_bucket.js | 1 + src/data/bucket/fill_extrusion_bucket.js | 1 + src/data/bucket/line_bucket.js | 1 + src/data/bucket/symbol_bucket.js | 1 + src/data/feature_index.js | 2 + src/data/program_configuration.js | 8 +++ src/geo/mercator_coordinate.js | 2 +- src/render/uniform_binding.js | 31 ++++++---- src/source/canvas_source.js | 9 ++- src/source/custom_source.js | 24 +++++--- src/source/geojson_source.js | 12 ++-- src/source/image_source.js | 13 +++-- src/source/raster_dem_tile_source.js | 4 +- src/source/raster_tile_source.js | 22 +++++--- src/source/source_cache.js | 7 ++- src/source/tile.js | 2 + src/source/vector_tile_source.js | 21 ++++--- src/source/video_source.js | 4 +- src/source/worker.js | 7 ++- src/style/evaluation_parameters.js | 2 +- src/style/light.js | 1 + src/style/properties.js | 4 ++ src/style/style.js | 4 +- src/style/style_layer/circle_style_layer.js | 25 ++++----- src/style/style_layer/custom_style_layer.js | 6 +- .../style_layer/fill_extrusion_style_layer.js | 28 +++++----- src/style/style_layer/fill_style_layer.js | 21 +++---- src/style/style_layer/heatmap_style_layer.js | 25 ++++----- src/style/style_layer/line_style_layer.js | 21 +++---- src/style/style_layer/symbol_style_layer.js | 6 +- src/symbol/grid_index.js | 46 +++------------ src/symbol/symbol_size.js | 3 + src/terrain/terrain.js | 7 ++- src/ui/camera.js | 6 +- src/ui/control/attribution_control.js | 35 +++++++----- src/ui/control/fullscreen_control.js | 11 ++-- src/ui/control/geolocate_control.js | 41 ++++++++------ src/ui/control/logo_control.js | 22 +++++--- src/ui/control/navigation_control.js | 56 +++++++++++++------ src/ui/control/scale_control.js | 6 +- src/ui/handler/box_zoom.js | 4 +- src/ui/handler/click_zoom.js | 2 +- src/ui/handler/keyboard.js | 2 +- src/ui/handler/map_event.js | 32 +++++------ src/ui/handler/mouse.js | 8 +-- src/ui/handler/scroll_zoom.js | 7 ++- src/ui/handler/tap_drag_zoom.js | 8 +-- src/ui/handler/tap_zoom.js | 8 +-- src/ui/handler/touch_pan.js | 8 +-- src/ui/handler/touch_zoom_rotate.js | 12 ++-- src/ui/handler_manager.js | 18 +++++- src/ui/hash.js | 7 ++- src/ui/map.js | 40 ++++++++++--- src/ui/marker.js | 41 +++++++++++--- src/ui/popup.js | 28 ++++++++-- src/util/actor.js | 4 +- src/util/mapbox.js | 12 ++-- src/util/scheduler.js | 3 +- 59 files changed, 490 insertions(+), 303 deletions(-) diff --git a/src/data/bucket/circle_bucket.js b/src/data/bucket/circle_bucket.js index 100af3bad75..16a25a23f4b 100644 --- a/src/data/bucket/circle_bucket.js +++ b/src/data/bucket/circle_bucket.js @@ -107,6 +107,7 @@ class CircleBucket implements Bucke const needGeometry = this.layers[0]._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); + // $FlowFixMe[method-unbinding] if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; const sortKey = circleSortKey ? diff --git a/src/data/bucket/fill_bucket.js b/src/data/bucket/fill_bucket.js index 56d5c7dc88a..a05087889b6 100644 --- a/src/data/bucket/fill_bucket.js +++ b/src/data/bucket/fill_bucket.js @@ -89,6 +89,7 @@ class FillBucket implements Bucket { const needGeometry = this.layers[0]._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); + // $FlowFixMe[method-unbinding] if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; const sortKey = fillSortKey ? diff --git a/src/data/bucket/fill_extrusion_bucket.js b/src/data/bucket/fill_extrusion_bucket.js index 42767ae51c3..28bf7b4ec62 100644 --- a/src/data/bucket/fill_extrusion_bucket.js +++ b/src/data/bucket/fill_extrusion_bucket.js @@ -243,6 +243,7 @@ class FillExtrusionBucket implements Bucket { const needGeometry = this.layers[0]._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); + // $FlowFixMe[method-unbinding] if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; const bucketFeature: BucketFeature = { diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index d333af932e0..2483efed328 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -146,6 +146,7 @@ class LineBucket implements Bucket { const needGeometry = this.layers[0]._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); + // $FlowFixMe[method-unbinding] if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; const sortKey = lineSortKey ? diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 797e8d5506a..334d8f10e6c 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -506,6 +506,7 @@ class SymbolBucket implements Bucket { const needGeometry = layer._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); + // $FlowFixMe[method-unbinding] if (!layer._featureFilter.filter(globalProperties, evaluationFeature, canonical)) { continue; } diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 8aba77cc407..95c09def69d 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -192,9 +192,11 @@ class FeatureIndex { if (filter.needGeometry) { const evaluationFeature = toEvaluationFeature(feature, true); + // $FlowFixMe[method-unbinding] if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), evaluationFeature, this.tileID.canonical)) { return; } + // $FlowFixMe[method-unbinding] } else if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) { return; } diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index e404891a3d1..b8c6d7be30c 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -106,7 +106,9 @@ class ConstantBinder implements UniformBinder { uniform.set(program, uniformName, currentValue.constantOr(this.value)); } + // $FlowFixMe[method-unbinding] getBinding(context: Context, _: string): $Shape> { + // $FlowFixMe[method-unbinding] return (this.type === 'color') ? new UniformColor(context) : new Uniform1f(context); @@ -136,7 +138,9 @@ class PatternConstantBinder implements UniformBinder { if (pos) uniform.set(program, uniformName, pos); } + // $FlowFixMe[method-unbinding] getBinding(context: Context, name: string): $Shape> { + // $FlowFixMe[method-unbinding] return name === 'u_pattern' || name === 'u_dash' ? new Uniform4f(context) : new Uniform1f(context); @@ -168,6 +172,7 @@ class SourceExpressionBinder implements AttributeBinder { populatePaintArray(newLength: number, feature: Feature, imagePositions: SpritePositions, availableImages: Array, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { const start = this.paintVertexArray.length; assert(Array.isArray(availableImages)); + // $FlowFixMe[method-unbinding] const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, canonical, availableImages, formattedSection); this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, value); @@ -238,7 +243,9 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder { } populatePaintArray(newLength: number, feature: Feature, imagePositions: SpritePositions, availableImages: Array, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { + // $FlowFixMe[method-unbinding] const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}, canonical, availableImages, formattedSection); + // $FlowFixMe[method-unbinding] const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}, canonical, availableImages, formattedSection); const start = this.paintVertexArray.length; this.paintVertexArray.resize(newLength); @@ -288,6 +295,7 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder { uniform.set(program, uniformName, factor); } + // $FlowFixMe[method-unbinding] getBinding(context: Context, _: string): Uniform1f { return new Uniform1f(context); } diff --git a/src/geo/mercator_coordinate.js b/src/geo/mercator_coordinate.js index 705cc63c6af..2e8e6669eca 100644 --- a/src/geo/mercator_coordinate.js +++ b/src/geo/mercator_coordinate.js @@ -94,7 +94,7 @@ class MercatorCoordinate { * const coord = mapboxgl.MercatorCoordinate.fromLngLat({lng: 0, lat: 0}, 0); * console.log(coord); // MercatorCoordinate(0.5, 0.5, 0) */ - static fromLngLat: (lngLatLike: LngLatLike, altitude?: number) => MercatorCoordinate = (lngLatLike, altitude = 0) => { + static fromLngLat(lngLatLike: LngLatLike, altitude: number = 0): MercatorCoordinate { const lngLat = LngLat.convert(lngLatLike); return new MercatorCoordinate( diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index 67d1d5a93aa..4a311124d9a 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -17,13 +17,13 @@ class Uniform { this.initialized = false; } - fetchUniformLocation: (program: WebGLProgram, name: string) => boolean = (program, name) => { + fetchUniformLocation(program: WebGLProgram, name: string): boolean { if (!this.location && !this.initialized) { this.location = this.gl.getUniformLocation(program, name); this.initialized = true; } return !!this.location; - }; + } +set: (program: WebGLProgram, name: string, v: T) => void; } @@ -34,7 +34,8 @@ class Uniform1i extends Uniform { this.current = 0; } - set: (program: WebGLProgram, name: string, v: number) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: number): void { if (!this.fetchUniformLocation(program, name)) return; if (this.current !== v) { this.current = v; @@ -49,7 +50,8 @@ class Uniform1f extends Uniform { this.current = 0; } - set: (program: WebGLProgram, name: string, v: number) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: number): void { if (!this.fetchUniformLocation(program, name)) return; if (this.current !== v) { this.current = v; @@ -64,7 +66,8 @@ class Uniform2f extends Uniform<[number, number]> { this.current = [0, 0]; } - set: (program: WebGLProgram, name: string, v: [number, number]) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: [number, number]): void { if (!this.fetchUniformLocation(program, name)) return; if (v[0] !== this.current[0] || v[1] !== this.current[1]) { this.current = v; @@ -79,7 +82,8 @@ class Uniform3f extends Uniform<[number, number, number]> { this.current = [0, 0, 0]; } - set: (program: WebGLProgram, name: string, v: [number, number, number]) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: [number, number, number]): void { if (!this.fetchUniformLocation(program, name)) return; if (v[0] !== this.current[0] || v[1] !== this.current[1] || v[2] !== this.current[2]) { this.current = v; @@ -94,7 +98,8 @@ class Uniform4f extends Uniform<[number, number, number, number]> { this.current = [0, 0, 0, 0]; } - set: (program: WebGLProgram, name: string, v: [number, number, number, number]) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: [number, number, number, number]): void { if (!this.fetchUniformLocation(program, name)) return; if (v[0] !== this.current[0] || v[1] !== this.current[1] || v[2] !== this.current[2] || v[3] !== this.current[3]) { @@ -110,7 +115,8 @@ class UniformColor extends Uniform { this.current = Color.transparent; } - set: (program: WebGLProgram, name: string, v: Color) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: Color): void { if (!this.fetchUniformLocation(program, name)) return; if (v.r !== this.current.r || v.g !== this.current.g || v.b !== this.current.b || v.a !== this.current.a) { @@ -127,7 +133,8 @@ class UniformMatrix4f extends Uniform { this.current = emptyMat4; } - set: (program: WebGLProgram, name: string, v: Float32Array) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: Float32Array): void { if (!this.fetchUniformLocation(program, name)) return; // The vast majority of matrix comparisons that will trip this set // happen at i=12 or i=0, so we check those first to avoid lots of @@ -154,7 +161,8 @@ class UniformMatrix3f extends Uniform { this.current = emptyMat3; } - set: (program: WebGLProgram, name: string, v: Float32Array) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: Float32Array): void { if (!this.fetchUniformLocation(program, name)) return; for (let i = 0; i < 9; i++) { if (v[i] !== this.current[i]) { @@ -173,7 +181,8 @@ class UniformMatrix2f extends Uniform { this.current = emptyMat2; } - set: (program: WebGLProgram, name: string, v: Float32Array) => void = (program, name, v) => { + // $FlowFixMe[method-unbinding] + set(program: WebGLProgram, name: string, v: Float32Array): void { if (!this.fetchUniformLocation(program, name)) return; for (let i = 0; i < 4; i++) { if (v[i] !== this.current[i]) { diff --git a/src/source/canvas_source.js b/src/source/canvas_source.js index a28313bc11d..1617a63cf3b 100644 --- a/src/source/canvas_source.js +++ b/src/source/canvas_source.js @@ -162,7 +162,8 @@ class CanvasSource extends ImageSource { return this.canvas; } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map) { this.map = map; this.load(); if (this.canvas) { @@ -170,7 +171,8 @@ class CanvasSource extends ImageSource { } } - onRemove: () => void = () => { + // $FlowFixMe[method-unbinding] + onRemove() { this.pause(); } @@ -189,7 +191,8 @@ class CanvasSource extends ImageSource { // setCoordinates inherited from ImageSource - prepare: () => void = () => { + // $FlowFixMe[method-unbinding] + prepare() { let resize = false; if (this.canvas.width !== this.width) { this.width = this.canvas.width; diff --git a/src/source/custom_source.js b/src/source/custom_source.js index 2edfe59fde3..7803e75bf77 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -198,12 +198,15 @@ class CustomSource extends Evented implements Source { } // $FlowFixMe[prop-missing] + // $FlowFixMe[method-unbinding] implementation.update = this._update.bind(this); // $FlowFixMe[prop-missing] + // $FlowFixMe[method-unbinding] implementation.clearTiles = this._clearTiles.bind(this); // $FlowFixMe[prop-missing] + // $FlowFixMe[method-unbinding] implementation.coveringTiles = this._coveringTiles.bind(this); extend(this, pick(implementation, ['dataType', 'scheme', 'minzoom', 'maxzoom', 'tileSize', 'attribution', 'minTileCacheSize', 'maxTileCacheSize'])); @@ -223,7 +226,8 @@ class CustomSource extends Evented implements Source { return this._loaded; } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map): void { this._map = map; this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); @@ -231,13 +235,15 @@ class CustomSource extends Evented implements Source { this.load(); } - onRemove: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onRemove(map: Map): void { if (this._implementation.onRemove) { this._implementation.onRemove(map); } } - hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { + // $FlowFixMe[method-unbinding] + hasTile(tileID: OverscaledTileID): boolean { if (this._implementation.hasTile) { const {x, y, z} = tileID.canonical; return this._implementation.hasTile({x, y, z}); @@ -312,7 +318,8 @@ class CustomSource extends Evented implements Source { RasterTileSource.unloadTileData(tile, this._map.painter); } - unloadTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { + // $FlowFixMe[method-unbinding] + unloadTile(tile: Tile, callback: Callback): void { this.unloadTileData(tile); if (this._implementation.unloadTile) { const {x, y, z} = tile.tileID.canonical; @@ -322,7 +329,8 @@ class CustomSource extends Evented implements Source { callback(); } - abortTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { + // $FlowFixMe[method-unbinding] + abortTile(tile: Tile, callback: Callback): void { if (tile.request && tile.request.cancel) { tile.request.cancel(); delete tile.request; @@ -335,7 +343,7 @@ class CustomSource extends Evented implements Source { return false; } - _coveringTiles: () => { z: number, x: number, y: number }[] = () => { + _coveringTiles(): { z: number, x: number, y: number }[] { const tileIDs = this._map.transform.coveringTiles({ tileSize: this.tileSize, minzoom: this.minzoom, @@ -346,11 +354,11 @@ class CustomSource extends Evented implements Source { return tileIDs.map(tileID => ({x: tileID.canonical.x, y: tileID.canonical.y, z: tileID.canonical.z})); } - _clearTiles: () => void = () => { + _clearTiles() { this._map.style._clearSource(this.id); } - _update: () => void = () => { + _update() { this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); } } diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index 362feb27de8..8a6c4857ecb 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -148,7 +148,8 @@ class GeoJSONSource extends Evented implements Source { }, options.workerOptions); } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map) { this.map = map; this.setData(this._data); } @@ -375,7 +376,8 @@ class GeoJSONSource extends Evented implements Source { }, undefined, message === 'loadTile'); } - abortTile: (tile: Tile) => void = (tile) => { + // $FlowFixMe[method-unbinding] + abortTile(tile: Tile) { if (tile.request) { tile.request.cancel(); delete tile.request; @@ -383,12 +385,14 @@ class GeoJSONSource extends Evented implements Source { tile.aborted = true; } - unloadTile: (tile: Tile) => void = (tile) => { + // $FlowFixMe[method-unbinding] + unloadTile(tile: Tile) { tile.unloadVectorData(); this.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id}); } - onRemove: () => void = () => { + // $FlowFixMe[method-unbinding] + onRemove() { if (this._pendingLoad) { this._pendingLoad.cancel(); } diff --git a/src/source/image_source.js b/src/source/image_source.js index 1f3db51fb55..a6b68daa00c 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -228,12 +228,14 @@ class ImageSource extends Evented implements Source { } } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map) { this.map = map; this.load(); } - onRemove: () => void = () => { + // $FlowFixMe[method-unbinding] + onRemove() { if (this._imageRequest) { this._imageRequest.cancel(); this._imageRequest = null; @@ -278,6 +280,7 @@ class ImageSource extends Evented implements Source { // may be outside the tile, because raster tiles aren't clipped when rendering. // transform the geo coordinates into (zoom 0) tile space coordinates + // $FlowFixMe[method-unbinding] const cornerCoords = coordinates.map(MercatorCoordinate.fromLngLat); // Compute the coordinates of the tile we'll use to hold this image's @@ -293,7 +296,8 @@ class ImageSource extends Evented implements Source { return this; } - _clear: () => void = () => { + // $FlowFixMe[method-unbinding] + _clear() { this._boundsArray = undefined; } @@ -332,7 +336,8 @@ class ImageSource extends Evented implements Source { this.boundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); } - prepare: () => void = () => { + // $FlowFixMe[method-unbinding] + prepare() { if (Object.keys(this.tiles).length === 0 || !this.image) return; const context = this.map.painter.context; diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index 762fb88b122..f56b7d4fd2a 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -17,6 +17,7 @@ import type Tile from './tile.js'; import type {Callback} from '../types/callback.js'; import type {RasterDEMSourceSpecification} from '../style-spec/types.js'; +// $FlowFixMe[method-unbinding] class RasterDEMTileSource extends RasterTileSource implements Source { encoding: "mapbox" | "terrarium"; @@ -116,7 +117,8 @@ class RasterDEMTileSource extends RasterTileSource implements Source { return neighboringTiles; } - unloadTile: (tile: Tile, callback: Callback) => void = (tile) => { + // $FlowFixMe[method-unbinding] + unloadTile(tile: Tile) { if (tile.demTexture) this.map.painter.saveTileTexture(tile.demTexture); if (tile.fbo) { tile.fbo.destroy(); diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index d883b7ec93b..dccf8c21888 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -113,7 +113,8 @@ class RasterTileSource extends Evented implements Source { return this._loaded; } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map) { this.map = map; this.load(); } @@ -124,7 +125,8 @@ class RasterTileSource extends Evented implements Source { * @example * map.getSource('source-id').reload(); */ - reload: () => void = () => { + // $FlowFixMe[method-unbinding] + reload() { this.cancelTileJSONRequest(); this.load(() => this.map.style._clearSource(this.id)); } @@ -173,17 +175,19 @@ class RasterTileSource extends Evented implements Source { return this; } - onRemove: () => void = () => { + // $FlowFixMe[method-unbinding] + onRemove() { this.cancelTileJSONRequest(); - }; + } serialize(): RasterSourceSpecification | RasterDEMSourceSpecification { return extend({}, this._options); } - hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { + // $FlowFixMe[method-unbinding] + hasTile(tileID: OverscaledTileID): boolean { return !this.tileBounds || this.tileBounds.contains(tileID.canonical); - }; + } loadTile(tile: Tile, callback: Callback) { const use2x = browser.devicePixelRatio >= 2; @@ -222,7 +226,8 @@ class RasterTileSource extends Evented implements Source { } } - abortTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { + // $FlowFixMe[method-unbinding] + abortTile(tile: Tile, callback: Callback) { if (tile.request) { tile.request.cancel(); delete tile.request; @@ -230,7 +235,8 @@ class RasterTileSource extends Evented implements Source { callback(); } - unloadTile: (tile: Tile, callback: Callback) => void = (tile, callback) => { + // $FlowFixMe[method-unbinding] + unloadTile(tile: Tile, callback: Callback) { if (tile.texture) this.map.painter.saveTileTexture(tile.texture); callback(); } diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 571395bfc19..a1afffb650c 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -89,6 +89,7 @@ class SourceCache extends Evented { this._source = source; this._tiles = {}; + // $FlowFixMe[method-unbinding] this._cache = new TileCache(0, this._unloadTile.bind(this)); this._timers = {}; this._cacheTimers = {}; @@ -150,7 +151,7 @@ class SourceCache extends Evented { return this._source.loadTile(tile, callback); } - _unloadTile: (tile: Tile) => void = (tile) => { + _unloadTile(tile: Tile): void { if (this._source.unloadTile) return this._source.unloadTile(tile, () => {}); } @@ -245,10 +246,11 @@ class SourceCache extends Evented { tile.state = state; } + // $FlowFixMe[method-unbinding] this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); } - _tileLoaded: (tile: Tile, id: number, previousState: TileState, err: ?Error) => void = (tile, id, previousState, err) => { + _tileLoaded(tile: Tile, id: number, previousState: TileState, err: ?Error) { if (err) { tile.state = 'errored'; if ((err: any).status !== 404) this._source.fire(new ErrorEvent(err, {tile})); @@ -762,6 +764,7 @@ class SourceCache extends Evented { if (!cached) { const painter = this.map ? this.map.painter : null; tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, painter, this._isRaster); + // $FlowFixMe[method-unbinding] this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state)); } diff --git a/src/source/tile.js b/src/source/tile.js index 64899600da4..bcafa476bba 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -463,7 +463,9 @@ class Tile { const feature = layer.feature(i); if (filter.needGeometry) { const evaluationFeature = toEvaluationFeature(feature, true); + // $FlowFixMe[method-unbinding] if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), evaluationFeature, this.tileID.canonical)) continue; + // $FlowFixMe[method-unbinding] } else if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) { continue; } diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index af5ef0d4191..70c23497a20 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -134,11 +134,13 @@ class VectorTileSource extends Evented implements Source { return this._loaded; } - hasTile: (tileID: OverscaledTileID) => boolean = (tileID) => { + // $FlowFixMe[method-unbinding] + hasTile(tileID: OverscaledTileID): boolean { return !this.tileBounds || this.tileBounds.contains(tileID.canonical); } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map) { this.map = map; this.load(); } @@ -149,7 +151,8 @@ class VectorTileSource extends Evented implements Source { * @example * map.getSource('source-id').reload(); */ - reload: () => void = () => { + // $FlowFixMe[method-unbinding] + reload() { this.cancelTileJSONRequest(); this.load(() => this.map.style._clearSource(this.id)); } @@ -199,7 +202,8 @@ class VectorTileSource extends Evented implements Source { return this; } - onRemove: () => void = () => { + // $FlowFixMe[method-unbinding] + onRemove() { this.cancelTileJSONRequest(); } @@ -288,7 +292,8 @@ class VectorTileSource extends Evented implements Source { } } - abortTile: (tile: Tile) => void = (tile) => { + // $FlowFixMe[method-unbinding] + abortTile(tile: Tile) { if (tile.request) { tile.request.cancel(); delete tile.request; @@ -298,7 +303,8 @@ class VectorTileSource extends Evented implements Source { } } - unloadTile: (tile: Tile) => void = (tile) => { + // $FlowFixMe[method-unbinding] + unloadTile(tile: Tile) { tile.unloadVectorData(); if (tile.actor) { tile.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id}); @@ -309,7 +315,8 @@ class VectorTileSource extends Evented implements Source { return false; } - afterUpdate: () => void = () => { + // $FlowFixMe[method-unbinding] + afterUpdate() { this._tileWorkers = {}; } diff --git a/src/source/video_source.js b/src/source/video_source.js index 49cbecde6f9..9c5682a077c 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -154,7 +154,7 @@ class VideoSource extends ImageSource { return this.video; } - onAdd: (map: Map) => void = (map) => { + onAdd(map: Map) { if (this.map) return; this.map = map; this.load(); @@ -198,7 +198,7 @@ class VideoSource extends ImageSource { */ // setCoordinates inherited from ImageSource - prepare: () => void = () => { + prepare() { if (Object.keys(this.tiles).length === 0 || this.video.readyState < 2) { return; // not enough data for current position } diff --git a/src/source/worker.js b/src/source/worker.js index 6691678424c..4f9994457c3 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -126,10 +126,10 @@ export default class Worker { callback(); } - enableTerrain: (mapId: string, enable: boolean, callback: WorkerTileCallback) => void = (mapId, enable, callback) => { + enableTerrain(mapId: string, enable: boolean, callback: WorkerTileCallback) { this.terrain = enable; callback(); - }; + } setProjection(mapId: string, config: ProjectionSpecification) { this.projections[mapId] = getProjection(config); @@ -147,18 +147,21 @@ export default class Worker { loadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { assert(params.type); + // $FlowFixMe[method-unbinding] const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; p.projection = this.projections[mapId] || this.defaultProjection; this.getWorkerSource(mapId, params.type, params.source).loadTile(p, callback); } loadDEMTile(mapId: string, params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { + // $FlowFixMe[method-unbinding] const p = this.enableTerrain ? extend({buildQuadTree: this.terrain}, params) : params; this.getDEMWorkerSource(mapId, params.source).loadTile(p, callback); } reloadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { assert(params.type); + // $FlowFixMe[method-unbinding] const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; p.projection = this.projections[mapId] || this.defaultProjection; this.getWorkerSource(mapId, params.type, params.source).reloadTile(p, callback); diff --git a/src/style/evaluation_parameters.js b/src/style/evaluation_parameters.js index 2f7f10547a5..fec5d2de14c 100644 --- a/src/style/evaluation_parameters.js +++ b/src/style/evaluation_parameters.js @@ -29,7 +29,7 @@ class EvaluationParameters { } } - isSupportedScript: (str: string) => boolean = (str) => { + isSupportedScript(str: string): boolean { return isStringInSupportedScript(str, rtlTextPlugin.isLoaded()); } } diff --git a/src/style/light.js b/src/style/light.js index 5daab4fd11a..e5aeaf1fb6e 100644 --- a/src/style/light.js +++ b/src/style/light.js @@ -60,6 +60,7 @@ class LightPositionProperty implements Property<[number, number, number], LightP } possiblyEvaluate(value: PropertyValue<[number, number, number], LightPosition>, parameters: EvaluationParameters): LightPosition { + // $FlowFixMe[method-unbinding] return sphericalToCartesian(value.expression.evaluate(parameters)); } diff --git a/src/style/properties.js b/src/style/properties.js index c16e9c34884..a19fb7b5c6e 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -517,6 +517,7 @@ export class DataConstantProperty implements Property { possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters): T { assert(!value.isDataDriven()); + // $FlowFixMe[method-unbinding] return value.expression.evaluate(parameters); } @@ -549,6 +550,7 @@ export class DataDrivenProperty implements Property>, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): PossiblyEvaluatedPropertyValue { if (value.expression.kind === 'constant' || value.expression.kind === 'camera') { + // $FlowFixMe[method-unbinding] return new PossiblyEvaluatedPropertyValue(this, {kind: 'constant', value: value.expression.evaluate(parameters, (null: any), {}, canonical, availableImages)}, parameters); } else { return new PossiblyEvaluatedPropertyValue(this, value.expression, parameters); @@ -586,6 +588,7 @@ export class DataDrivenProperty implements Property { } possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): boolean { + // $FlowFixMe[method-unbinding] return !!value.expression.evaluate(parameters, (null: any), {}, canonical, availableImages); } diff --git a/src/style/style.js b/src/style/style.js index 53069b4b4a2..056f9239e74 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -907,6 +907,7 @@ class Style extends Evented { } this._updateLayer(layer); + // $FlowFixMe[method-unbinding] if (layer.onAdd) { layer.onAdd(this.map); } @@ -1391,6 +1392,7 @@ class Style extends Evented { queryRenderedSymbols( this._layers, this._serializedLayers, + // $FlowFixMe[method-unbinding] this._getLayerSourceCache.bind(this), queryGeometryStruct.screenGeometry, params, @@ -1825,7 +1827,7 @@ class Style extends Evented { return this._otherSourceCaches[source]; } - _getLayerSourceCache: (layer: StyleLayer) => SourceCache | void = (layer) => { + _getLayerSourceCache(layer: StyleLayer): SourceCache | void { return layer.type === 'symbol' ? this._symbolSourceCaches[layer.source] : this._otherSourceCaches[layer.source]; diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index 07b7fbb81d4..23e155e3791 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -24,17 +24,6 @@ import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {DEMSampler} from '../../terrain/elevation.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; -type QueryIntersectsFeatureFn = ( - queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform, - pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler -) => boolean; - class CircleStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; layout: PossiblyEvaluated; @@ -51,14 +40,24 @@ class CircleStyleLayer extends StyleLayer { return new CircleBucket(parameters); } - queryRadius: (bucket: Bucket) => number = (bucket: Bucket) => { + // $FlowFixMe[method-unbinding] + queryRadius(bucket: Bucket): number { const circleBucket: CircleBucket = (bucket: any); return getMaximumPaintValue('circle-radius', this, circleBucket) + getMaximumPaintValue('circle-stroke-width', this, circleBucket) + translateDistance(this.paint.get('circle-translate')); } - queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { + // $FlowFixMe[method-unbinding] + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform, + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler): boolean { + const translation = tilespaceTranslate( this.paint.get('circle-translate'), this.paint.get('circle-translate-anchor'), diff --git a/src/style/style_layer/custom_style_layer.js b/src/style/style_layer/custom_style_layer.js index cdad752a51a..03fa20bbe84 100644 --- a/src/style/style_layer/custom_style_layer.js +++ b/src/style/style_layer/custom_style_layer.js @@ -225,13 +225,15 @@ class CustomStyleLayer extends StyleLayer { assert(false, "Custom layers cannot be serialized"); } - onAdd: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onAdd(map: Map) { if (this.implementation.onAdd) { this.implementation.onAdd(map, map.painter.context.gl); } } - onRemove: (map: Map) => void = (map) => { + // $FlowFixMe[method-unbinding] + onRemove(map: Map) { if (this.implementation.onRemove) { this.implementation.onRemove(map, map.painter.context.gl); } diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index d662604ff65..a874e8d3163 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -22,18 +22,6 @@ import type {DEMSampler} from '../../terrain/elevation.js'; import type {Vec2, Vec4} from 'gl-matrix'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; -type QueryIntersectsFeatureFn = ( - queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform, - pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler, - layoutVertexArrayOffset: number -) => boolean | number; - class Point3D extends Point { z: number; @@ -57,7 +45,8 @@ class FillExtrusionStyleLayer extends StyleLayer { return new FillExtrusionBucket(parameters); } - queryRadius: () => number = () => { + // $FlowFixMe[method-unbinding] + queryRadius(): number { return translateDistance(this.paint.get('fill-extrusion-translate')); } @@ -75,12 +64,21 @@ class FillExtrusionStyleLayer extends StyleLayer { return new ProgramConfiguration(this, zoom); } - queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper, layoutVertexArrayOffset) => { + // $FlowFixMe[method-unbinding] + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform, + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler, + layoutVertexArrayOffset: number): boolean | number { + const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), this.paint.get('fill-extrusion-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index 36b322b03bd..def0f40617f 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -19,15 +19,6 @@ import type {LayerSpecification} from '../../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; -type QueryIntersectsFeatureFn = ( - queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform -) => boolean; - class FillStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; layout: PossiblyEvaluated; @@ -70,18 +61,24 @@ class FillStyleLayer extends StyleLayer { return new FillBucket(parameters); } - queryRadius: () => number = () => { + // $FlowFixMe[method-unbinding] + queryRadius(): number { return translateDistance(this.paint.get('fill-translate')); } - queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform) => { + // $FlowFixMe[method-unbinding] + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform): boolean { if (queryGeometry.queryGeometry.isAboveHorizon) return false; const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('fill-translate'), this.paint.get('fill-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - return polygonIntersectsMultiPolygon(translatedPolygon, geometry); } diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index c300ba58364..25ea385f6b7 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -24,17 +24,6 @@ import type Transform from '../../geo/transform.js'; import type CircleBucket from '../../data/bucket/circle_bucket.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; -type QueryIntersectsFeatureFn = ( - queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform, - pixelPosMatrix: Float32Array, - elevationHelper: ?DEMSampler -) => boolean; - class HeatmapStyleLayer extends StyleLayer { heatmapFbo: ?Framebuffer; @@ -79,11 +68,21 @@ class HeatmapStyleLayer extends StyleLayer { } } - queryRadius: (bucket: Bucket) => number = (bucket) => { + // $FlowFixMe[method-unbinding] + queryRadius(bucket: Bucket): number { return getMaximumPaintValue('heatmap-radius', this, ((bucket: any): CircleBucket<*>)); } - queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform, pixelPosMatrix, elevationHelper) => { + // $FlowFixMe[method-unbinding] + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform, + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler): boolean { + const size = this.paint.get('heatmap-radius').evaluate(feature, featureState); return queryIntersectsCircle( queryGeometry, geometry, transform, pixelPosMatrix, elevationHelper, diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 7533cf91910..12e61cd04fe 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -21,15 +21,6 @@ import type {LayerSpecification} from '../../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../query_geometry.js'; import type {IVectorTileFeature} from '@mapbox/vector-tile'; -type QueryIntersectsFeatureFn = ( - queryGeometry: TilespaceQueryGeometry, - feature: IVectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: Transform -) => boolean; - class LineFloorwidthProperty extends DataDrivenProperty { useIntegerZoom = true; @@ -105,7 +96,8 @@ class LineStyleLayer extends StyleLayer { return new ProgramConfiguration(this, zoom); } - queryRadius: (bucket: Bucket) => number = (bucket) => { + // $FlowFixMe[method-unbinding] + queryRadius(bucket: Bucket): number { const lineBucket: LineBucket = (bucket: any); const width = getLineWidth( getMaximumPaintValue('line-width', this, lineBucket), @@ -114,14 +106,19 @@ class LineStyleLayer extends StyleLayer { return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate')); } - queryIntersectsFeature: QueryIntersectsFeatureFn = (queryGeometry, feature, featureState, geometry, zoom, transform) => { + // $FlowFixMe[method-unbinding] + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, + feature: IVectorTileFeature, + featureState: FeatureState, + geometry: Array>, + zoom: number, + transform: Transform): boolean { if (queryGeometry.queryGeometry.isAboveHorizon) return false; const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('line-translate'), this.paint.get('line-translate-anchor'), transform.angle, queryGeometry.pixelToTileUnitsFactor); - const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( this.paint.get('line-width').evaluate(feature, featureState), this.paint.get('line-gap-width').evaluate(feature, featureState)); diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 3bca5107599..8eac1810527 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -110,11 +110,13 @@ class SymbolStyleLayer extends StyleLayer { return new SymbolBucket(parameters); } - queryRadius: () => number = () => { + // $FlowFixMe[method-unbinding] + queryRadius(): number { return 0; } - queryIntersectsFeature: () => boolean = () => { + // $FlowFixMe[method-unbinding] + queryIntersectsFeature(): boolean { assert(false); // Should take a different path in FeatureIndex return false; } diff --git a/src/symbol/grid_index.js b/src/symbol/grid_index.js index 91b64fe1633..fbd626393bc 100644 --- a/src/symbol/grid_index.js +++ b/src/symbol/grid_index.js @@ -69,6 +69,7 @@ class GridIndex { } insert(key: any, x1: number, y1: number, x2: number, y2: number) { + // $FlowFixMe[method-unbinding] this._forEachCell(x1, y1, x2, y2, this._insertBoxCell, this.boxUid++); this.boxKeys.push(key); this.bboxes.push(x1); @@ -80,6 +81,7 @@ class GridIndex { insertCircle(key: any, x: number, y: number, radius: number) { // Insert circle into grid for all cells in the circumscribing square // It's more than necessary (by a factor of 4/PI), but fast to insert + // $FlowFixMe[method-unbinding] this._forEachCell(x - radius, y - radius, x + radius, y + radius, this._insertCircleCell, this.circleUid++); this.circleKeys.push(key); this.circles.push(x); @@ -87,25 +89,11 @@ class GridIndex { this.circles.push(radius); } - _insertBoxCell: ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number - ) => void = (x1, y1, x2, y2, cellIndex, uid) => { + _insertBoxCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number) { this.boxCells[cellIndex].push(uid); - }; + } - _insertCircleCell: ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - uid: number - ) => void = (x1, y1, x2, y2, cellIndex, uid) => { + _insertCircleCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, uid: number) { this.circleCells[cellIndex].push(uid); } @@ -145,6 +133,7 @@ class GridIndex { hitTest, seenUids: {box: {}, circle: {}} }; + // $FlowFixMe[method-unbinding] this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate); return hitTest ? result.length > 0 : result; } @@ -170,6 +159,7 @@ class GridIndex { circle: {x, y, radius}, seenUids: {box: {}, circle: {}} }; + // $FlowFixMe[method-unbinding] this._forEachCell(x1, y1, x2, y2, this._queryCellCircle, result, queryArgs, predicate); return hitTest ? result.length > 0 : result; } @@ -186,16 +176,7 @@ class GridIndex { return (this._queryCircle(x, y, radius, true, predicate): any); } - _queryCell: ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any - ) => void | boolean = (x1, y1, x2, y2, cellIndex, result, queryArgs, predicate?) => { + _queryCell(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any, predicate?: any): void | boolean { const seenUids = queryArgs.seenUids; const boxCell = this.boxCells[cellIndex]; if (boxCell !== null) { @@ -262,16 +243,7 @@ class GridIndex { } } - _queryCellCircle: ( - x1: number, - y1: number, - x2: number, - y2: number, - cellIndex: number, - result: any, - queryArgs: any, - predicate?: any - ) => void | boolean = (x1, y1, x2, y2, cellIndex, result, queryArgs, predicate?) => { + _queryCellCircle(x1: number, y1: number, x2: number, y2: number, cellIndex: number, result: any, queryArgs: any, predicate?: any): void | boolean { const circle = queryArgs.circle; const seenUids = queryArgs.seenUids; const boxCell = this.boxCells[cellIndex]; diff --git a/src/symbol/symbol_size.js b/src/symbol/symbol_size.js index 98bac3ebffc..8b5f0093312 100644 --- a/src/symbol/symbol_size.js +++ b/src/symbol/symbol_size.js @@ -42,6 +42,7 @@ function getSizeData(tileZoom: number, value: PropertyValue void = () => { + _checkRenderCacheEfficiency() { const renderCacheInfo = this.renderCacheEfficiency(this._style); if (this._style.map._optimizeForTerrain) { assert(renderCacheInfo.efficiency === 100); @@ -365,7 +367,7 @@ export class Terrain extends Elevation { } } - _onStyleDataEvent: (event: any) => void = (event) => { + _onStyleDataEvent(event: any) { if (event.coord && event.dataType === 'source') { this._clearRenderCacheForTile(event.sourceCacheId, event.coord); } else if (event.dataType === 'style') { @@ -1341,6 +1343,7 @@ export class Terrain extends Elevation { const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource); const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div(1 << imageSource.tileID.z); + // $FlowFixMe[method-unbinding] const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce((acc, coord) => { acc.min.x = Math.min(acc.min.x, coord.x - anchor.x); acc.min.y = Math.min(acc.min.y, coord.y - anchor.y); diff --git a/src/ui/camera.js b/src/ui/camera.js index 58d029ed6d6..c05ece75eff 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -1720,21 +1720,23 @@ class Camera extends Evented { this._easeOptions = options; this._onEaseFrame = frame; this._onEaseEnd = finish; + // $FlowFixMe[method-unbinding] this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); } } // Callback for map._requestRenderFrame - _renderFrameCallback: () => void = () => { + _renderFrameCallback() { const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1); const frame = this._onEaseFrame; if (frame) frame(this._easeOptions.easing(t)); if (t < 1) { + // $FlowFixMe[method-unbinding] this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback); } else { this.stop(); } - }; + } // convert bearing so that it's numerically close to the current one so that it interpolates properly _normalizeBearing(bearing: number, currentBearing: number): number { diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index c5062c3d675..53a22565897 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -48,11 +48,11 @@ class AttributionControl { ], this); } - getDefaultPosition: () => ControlPosition = () => { + getDefaultPosition(): ControlPosition { return 'bottom-right'; - }; + } - onAdd: (map: Map) => HTMLElement = (map) => { + onAdd(map: Map): HTMLElement { const compact = this.options && this.options.compact; this._map = map; @@ -60,6 +60,7 @@ class AttributionControl { this._compactButton = DOM.create('button', 'mapboxgl-ctrl-attrib-button', this._container); DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute('aria-hidden', 'true'); this._compactButton.type = 'button'; + // $FlowFixMe[method-unbinding] this._compactButton.addEventListener('click', this._toggleAttribution); this._setElementTitle(this._compactButton, 'ToggleAttribution'); this._innerContainer = DOM.create('div', 'mapboxgl-ctrl-attrib-inner', this._container); @@ -72,29 +73,37 @@ class AttributionControl { this._updateAttributions(); this._updateEditLink(); + // $FlowFixMe[method-unbinding] this._map.on('styledata', this._updateData); + // $FlowFixMe[method-unbinding] this._map.on('sourcedata', this._updateData); + // $FlowFixMe[method-unbinding] this._map.on('moveend', this._updateEditLink); if (compact === undefined) { + // $FlowFixMe[method-unbinding] this._map.on('resize', this._updateCompact); this._updateCompact(); } return this._container; - }; + } - onRemove: () => void = () => { + onRemove() { this._container.remove(); + // $FlowFixMe[method-unbinding] this._map.off('styledata', this._updateData); + // $FlowFixMe[method-unbinding] this._map.off('sourcedata', this._updateData); + // $FlowFixMe[method-unbinding] this._map.off('moveend', this._updateEditLink); + // $FlowFixMe[method-unbinding] this._map.off('resize', this._updateCompact); this._map = (undefined: any); this._attribHTML = (undefined: any); - }; + } _setElementTitle(element: HTMLElement, title: string) { const str = this._map._getUIString(`AttributionControl.${title}`); @@ -103,7 +112,7 @@ class AttributionControl { if (element.firstElementChild) element.firstElementChild.setAttribute('title', str); } - _toggleAttribution: () => void = () => { + _toggleAttribution() { if (this._container.classList.contains('mapboxgl-compact-show')) { this._container.classList.remove('mapboxgl-compact-show'); this._compactButton.setAttribute('aria-expanded', 'false'); @@ -111,9 +120,9 @@ class AttributionControl { this._container.classList.add('mapboxgl-compact-show'); this._compactButton.setAttribute('aria-expanded', 'true'); } - }; + } - _updateEditLink: () => void = () => { + _updateEditLink() { let editLink = this._editLink; if (!editLink) { editLink = this._editLink = (this._container.querySelector('.mapbox-improve-map'): any); @@ -136,14 +145,14 @@ class AttributionControl { editLink.rel = 'noopener nofollow'; this._setElementTitle(editLink, 'MapFeedback'); } - }; + } - _updateData: (e: any) => void = (e) => { + _updateData(e: any) { if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style')) { this._updateAttributions(); this._updateEditLink(); } - }; + } _updateAttributions() { if (!this._map.style) return; @@ -201,7 +210,7 @@ class AttributionControl { this._editLink = null; } - _updateCompact: () => void = () => { + _updateCompact() { if (this._map.getCanvasContainer().offsetWidth <= 640) { this._container.classList.add('mapboxgl-compact'); } else { diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index 4406c6d225b..a7104905a9c 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -68,6 +68,7 @@ class FullscreenControl { onRemove() { this._controlContainer.remove(); this._map = (null: any); + // $FlowFixMe[method-unbinding] window.document.removeEventListener(this._fullscreenchange, this._changeIcon); } @@ -83,7 +84,9 @@ class FullscreenControl { DOM.create('span', `mapboxgl-ctrl-icon`, button).setAttribute('aria-hidden', 'true'); button.type = 'button'; this._updateTitle(); + // $FlowFixMe[method-unbinding] this._fullscreenButton.addEventListener('click', this._onClickFullscreen); + // $FlowFixMe[method-unbinding] window.document.addEventListener(this._fullscreenchange, this._changeIcon); } @@ -101,7 +104,7 @@ class FullscreenControl { return this._fullscreen; } - _changeIcon: () => void = () => { + _changeIcon() { const fullscreenElement = window.document.fullscreenElement || (window.document: any).webkitFullscreenElement; @@ -112,9 +115,9 @@ class FullscreenControl { this._fullscreenButton.classList.toggle(`mapboxgl-ctrl-fullscreen`); this._updateTitle(); } - }; + } - _onClickFullscreen: () => void = () => { + _onClickFullscreen() { if (this._isFullscreen()) { if (window.document.exitFullscreen) { (window.document: any).exitFullscreen(); @@ -127,7 +130,7 @@ class FullscreenControl { } else if ((this._container: any).webkitRequestFullscreen) { (this._container: any).webkitRequestFullscreen(); } - }; + } } export default FullscreenControl; diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 44f75b2f324..623f6c72c27 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -126,6 +126,7 @@ class GeolocateControl extends Evented { '_onDeviceOrientation' ], this); + // $FlowFixMe[method-unbinding] this._updateMarkerRotationThrottled = throttle(this._updateMarkerRotation, 20); this._numberOfWatches = 0; } @@ -133,6 +134,7 @@ class GeolocateControl extends Evented { onAdd(map: Map): HTMLElement { this._map = map; this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); + // $FlowFixMe[method-unbinding] this._checkGeolocationSupport(this._setupUI); return this._container; } @@ -153,6 +155,7 @@ class GeolocateControl extends Evented { } this._container.remove(); + // $FlowFixMe[method-unbinding] this._map.off('zoom', this._onZoom); this._map = (undefined: any); this._numberOfWatches = 0; @@ -234,7 +237,7 @@ class GeolocateControl extends Evented { * @param {Position} position the Geolocation API Position * @private */ - _onSuccess: (position: Position) => void = (position) => { + _onSuccess(position: Position) { if (!this._map) { // control has since been removed return; @@ -294,7 +297,7 @@ class GeolocateControl extends Evented { this.fire(new Event('geolocate', position)); this._finish(); - }; + } /** * Update the camera location to center on the current position @@ -347,7 +350,7 @@ class GeolocateControl extends Evented { this._circleElement.style.height = `${circleDiameter}px`; } - _onZoom: () => void = () => { + _onZoom() { if (this.options.showUserLocation && this.options.showAccuracyCircle) { this._updateCircleRadius(); } @@ -358,7 +361,7 @@ class GeolocateControl extends Evented { * * @private */ - _updateMarkerRotation: () => void = () => { + _updateMarkerRotation() { if (this._userLocationDotMarker && typeof this._heading === 'number') { this._userLocationDotMarker.setRotation(this._heading); this._dotElement.classList.add('mapboxgl-user-location-show-heading'); @@ -366,9 +369,9 @@ class GeolocateControl extends Evented { this._dotElement.classList.remove('mapboxgl-user-location-show-heading'); this._userLocationDotMarker.setRotation(0); } - }; + } - _onError: (error: PositionError) => void = (error) => { + _onError(error: PositionError) { if (!this._map) { // control has since been removed return; @@ -411,12 +414,12 @@ class GeolocateControl extends Evented { this._finish(); } - _finish: () => void = () => { + _finish() { if (this._timeoutId) { clearTimeout(this._timeoutId); } this._timeoutId = undefined; - }; + } - _setupUI: (supported: boolean) => void = (supported) => { + _setupUI(supported: boolean) { if (this._map === undefined) { // This control was removed from the map before geolocation // support was determined. @@ -462,11 +465,12 @@ class GeolocateControl extends Evented { if (this.options.trackUserLocation) this._watchState = 'OFF'; + // $FlowFixMe[method-unbinding] this._map.on('zoom', this._onZoom); } - this._geolocateButton.addEventListener('click', - this.trigger.bind(this)); + // $FlowFixMe[method-unbinding] + this._geolocateButton.addEventListener('click', this.trigger.bind(this)); this._setup = true; @@ -484,7 +488,7 @@ class GeolocateControl extends Evented { } }); } - }; + } /** * Programmatically request and move the map to the user's location. @@ -508,7 +512,7 @@ class GeolocateControl extends Evented { * geolocate.trigger(); * }); */ - _onDeviceOrientation: (deviceOrientationEvent: DeviceOrientationEvent) => void = (deviceOrientationEvent) => { + _onDeviceOrientation(deviceOrientationEvent: DeviceOrientationEvent) { // absolute is true if the orientation data is provided as the difference between the Earth's coordinate frame and the device's coordinate frame, or false if the orientation data is being provided in reference to some arbitrary, device-determined coordinate frame. if (this._userLocationDotMarker) { if (deviceOrientationEvent.webkitCompassHeading) { @@ -540,7 +544,7 @@ class GeolocateControl extends Evented { * }); * @returns {boolean} Returns `false` if called before control was added to a map, otherwise returns `true`. */ - trigger: (() => boolean) = (): boolean => { + trigger(): boolean { if (!this._setup) { warnOnce('Geolocate control triggered before added to a map'); return false; @@ -636,11 +640,12 @@ class GeolocateControl extends Evented { } } } else { - this.options.geolocation.getCurrentPosition( - this._onSuccess, this._onError, this.options.positionOptions); + // $FlowFixMe[method-unbinding] + this.options.geolocation.getCurrentPosition(this._onSuccess, this._onError, this.options.positionOptions); // This timeout ensures that we still call finish() even if // the user declines to share their location in Firefox + // $FlowFixMe[method-unbinding] this._timeoutId = setTimeout(this._finish, 10000 /* 10sec */); } @@ -650,8 +655,10 @@ class GeolocateControl extends Evented { _addDeviceOrientationListener() { const addListener = () => { if ('ondeviceorientationabsolute' in window) { + // $FlowFixMe[method-unbinding] window.addEventListener('deviceorientationabsolute', this._onDeviceOrientation); } else { + // $FlowFixMe[method-unbinding] window.addEventListener('deviceorientation', this._onDeviceOrientation); } }; @@ -674,7 +681,9 @@ class GeolocateControl extends Evented { _clearWatch() { this.options.geolocation.clearWatch(this._geolocationWatchID); + // $FlowFixMe[method-unbinding] window.removeEventListener('deviceorientation', this._onDeviceOrientation); + // $FlowFixMe[method-unbinding] window.removeEventListener('deviceorientationabsolute', this._onDeviceOrientation); this._geolocationWatchID = (undefined: any); diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index 9627f5e2eab..6ee16ee3724 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -23,7 +23,7 @@ class LogoControl { bindAll(['_updateLogo', '_updateCompact'], this); } - onAdd: (map: Map) => HTMLElement = (map) => { + onAdd(map: Map): HTMLElement { this._map = map; this._container = DOM.create('div', 'mapboxgl-ctrl'); const anchor = DOM.create('a', 'mapboxgl-ctrl-logo'); @@ -35,30 +35,34 @@ class LogoControl { this._container.appendChild(anchor); this._container.style.display = 'none'; + // $FlowFixMe[method-unbinding] this._map.on('sourcedata', this._updateLogo); this._updateLogo(); + // $FlowFixMe[method-unbinding] this._map.on('resize', this._updateCompact); this._updateCompact(); return this._container; } - onRemove: () => void = () => { + onRemove() { this._container.remove(); + // $FlowFixMe[method-unbinding] this._map.off('sourcedata', this._updateLogo); + // $FlowFixMe[method-unbinding] this._map.off('resize', this._updateCompact); - }; + } - getDefaultPosition: () => ControlPosition = () => { + getDefaultPosition(): ControlPosition { return 'bottom-left'; - }; + } - _updateLogo: (e: any) => void = (e) => { + _updateLogo(e: any) { if (!e || e.sourceDataType === 'metadata') { this._container.style.display = this._logoRequired() ? 'block' : 'none'; } - }; + } _logoRequired(): boolean { if (!this._map.style) return true; @@ -74,7 +78,7 @@ class LogoControl { return true; } - _updateCompact: () => void = () => { + _updateCompact() { const containerChildren = this._container.children; if (containerChildren.length) { const anchor = containerChildren[0]; @@ -84,7 +88,7 @@ class LogoControl { anchor.classList.remove('mapboxgl-compact'); } } - }; + } } diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index 2f1eb5f9ecc..b768fa4bbe7 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -84,7 +84,7 @@ class NavigationControl { } } - _updateZoomButtons: () => void = () => { + _updateZoomButtons() { const map = this._map; if (!map) return; @@ -95,9 +95,9 @@ class NavigationControl { this._zoomOutButton.disabled = isMin; this._zoomInButton.setAttribute('aria-disabled', isMax.toString()); this._zoomOutButton.setAttribute('aria-disabled', isMin.toString()); - }; + } - _rotateCompassArrow: () => void = () => { + _rotateCompassArrow() { const map = this._map; if (!map) return; @@ -110,21 +110,24 @@ class NavigationControl { this._compassIcon.style.transform = rotate; } }); - }; + } onAdd(map: Map): HTMLElement { this._map = map; if (this.options.showZoom) { this._setButtonTitle(this._zoomInButton, 'ZoomIn'); this._setButtonTitle(this._zoomOutButton, 'ZoomOut'); + // $FlowFixMe[method-unbinding] map.on('zoom', this._updateZoomButtons); this._updateZoomButtons(); } if (this.options.showCompass) { this._setButtonTitle(this._compass, 'ResetBearing'); if (this.options.visualizePitch) { + // $FlowFixMe[method-unbinding] map.on('pitch', this._rotateCompassArrow); } + // $FlowFixMe[method-unbinding] map.on('rotate', this._rotateCompassArrow); this._rotateCompassArrow(); this._handler = new MouseRotateWrapper(map, this._compass, this.options.visualizePitch); @@ -137,12 +140,15 @@ class NavigationControl { if (!map) return; this._container.remove(); if (this.options.showZoom) { + // $FlowFixMe[method-unbinding] map.off('zoom', this._updateZoomButtons); } if (this.options.showCompass) { if (this.options.visualizePitch) { + // $FlowFixMe[method-unbinding] map.off('pitch', this._rotateCompassArrow); } + // $FlowFixMe[method-unbinding] map.off('rotate', this._rotateCompassArrow); if (this._handler) this._handler.off(); this._handler = undefined; @@ -183,10 +189,15 @@ class MouseRotateWrapper { if (pitch) this.mousePitch = new MousePitchHandler({clickTolerance: map.dragRotate._mousePitch._clickTolerance}); bindAll(['mousedown', 'mousemove', 'mouseup', 'touchstart', 'touchmove', 'touchend', 'reset'], this); + // $FlowFixMe[method-unbinding] element.addEventListener('mousedown', this.mousedown); + // $FlowFixMe[method-unbinding] element.addEventListener('touchstart', this.touchstart, {passive: false}); + // $FlowFixMe[method-unbinding] element.addEventListener('touchmove', this.touchmove); + // $FlowFixMe[method-unbinding] element.addEventListener('touchend', this.touchend); + // $FlowFixMe[method-unbinding] element.addEventListener('touchcancel', this.reset); } @@ -210,55 +221,64 @@ class MouseRotateWrapper { off() { const element = this.element; + // $FlowFixMe[method-unbinding] element.removeEventListener('mousedown', this.mousedown); + // $FlowFixMe[method-unbinding] element.removeEventListener('touchstart', this.touchstart, {passive: false}); + // $FlowFixMe[method-unbinding] element.removeEventListener('touchmove', this.touchmove); + // $FlowFixMe[method-unbinding] element.removeEventListener('touchend', this.touchend); + // $FlowFixMe[method-unbinding] element.removeEventListener('touchcancel', this.reset); this.offTemp(); } offTemp() { DOM.enableDrag(); + // $FlowFixMe[method-unbinding] window.removeEventListener('mousemove', this.mousemove); + // $FlowFixMe[method-unbinding] window.removeEventListener('mouseup', this.mouseup); } - mousedown: (e: MouseEvent) => void = (e) => { + mousedown(e: MouseEvent) { this.down(extend({}, e, {ctrlKey: true, preventDefault: () => e.preventDefault()}), DOM.mousePos(this.element, e)); + // $FlowFixMe[method-unbinding] window.addEventListener('mousemove', this.mousemove); + // $FlowFixMe[method-unbinding] window.addEventListener('mouseup', this.mouseup); - }; + } - mousemove: (e: MouseEvent) => void = (e) => { + mousemove(e: MouseEvent) { this.move(e, DOM.mousePos(this.element, e)); - }; + } - mouseup: (e: MouseEvent) => void = (e) => { + mouseup(e: MouseEvent) { this.mouseRotate.mouseupWindow(e); if (this.mousePitch) this.mousePitch.mouseupWindow(e); this.offTemp(); - }; + } - touchstart: (e: TouchEvent) => void = (e) => { + touchstart(e: TouchEvent) { if (e.targetTouches.length !== 1) { this.reset(); } else { this._startPos = this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; this.down((({type: 'mousedown', button: 0, ctrlKey: true, preventDefault: () => e.preventDefault()}: any): MouseEvent), this._startPos); } - }; + } - touchmove: (e: TouchEvent) => void = (e) => { + touchmove(e: TouchEvent) { if (e.targetTouches.length !== 1) { this.reset(); } else { this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0]; this.move((({preventDefault: () => e.preventDefault()}: any): MouseEvent), this._lastPos); } - }; + } - touchend: (e: TouchEvent) => void = (e) => { + touchend(e: TouchEvent) { if (e.targetTouches.length === 0 && this._startPos && this._lastPos && @@ -266,15 +286,15 @@ class MouseRotateWrapper { this.element.click(); } this.reset(); - }; + } - reset: () => void = () => { + reset() { this.mouseRotate.reset(); if (this.mousePitch) this.mousePitch.reset(); delete this._startPos; delete this._lastPos; this.offTemp(); - }; + } } export default NavigationControl; diff --git a/src/ui/control/scale_control.js b/src/ui/control/scale_control.js index d36d43f7be0..fae79eb28dd 100644 --- a/src/ui/control/scale_control.js +++ b/src/ui/control/scale_control.js @@ -61,7 +61,7 @@ class ScaleControl { return 'bottom-left'; } - _update: () => void = () => { + _update() { // A horizontal scale is imagined to be present at center of the map // container with maximum length (Default) as 100px. // Using spherical law of cosines approximation, the real distance is @@ -93,7 +93,7 @@ class ScaleControl { } else { this._setScale(maxWidth, maxMeters, 'meter'); } - }; + } _setScale(maxWidth: number, maxDistance: number, unit: string) { const distance = getRoundNum(maxDistance); @@ -120,6 +120,7 @@ class ScaleControl { this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-scale', map.getContainer()); this._container.dir = 'auto'; + // $FlowFixMe[method-unbinding] this._map.on('move', this._update); this._update(); @@ -128,6 +129,7 @@ class ScaleControl { onRemove() { this._container.remove(); + // $FlowFixMe[method-unbinding] this._map.off('move', this._update); this._map = (undefined: any); } diff --git a/src/ui/handler/box_zoom.js b/src/ui/handler/box_zoom.js index 406b0a22c6f..29386050569 100644 --- a/src/ui/handler/box_zoom.js +++ b/src/ui/handler/box_zoom.js @@ -82,7 +82,7 @@ class BoxZoomHandler { this._enabled = false; } - mousedown: (e: MouseEvent, point: Point) => void = (e, point) => { + mousedown(e: MouseEvent, point: Point) { if (!this.isEnabled()) return; if (!(e.shiftKey && e.button === 0)) return; @@ -146,7 +146,7 @@ class BoxZoomHandler { } } - keydown: (e: KeyboardEvent) => void = (e) => { + keydown(e: KeyboardEvent) { if (!this._active) return; if (e.keyCode === 27) { diff --git a/src/ui/handler/click_zoom.js b/src/ui/handler/click_zoom.js index c2ff1e259a7..9010c5b4fbf 100644 --- a/src/ui/handler/click_zoom.js +++ b/src/ui/handler/click_zoom.js @@ -21,7 +21,7 @@ export default class ClickZoomHandler { this.reset(); } - dblclick: (e: MouseEvent, point: Point) => HandlerResult = (e, point) => { + dblclick(e: MouseEvent, point: Point): HandlerResult { e.preventDefault(); return { cameraAnimation: (map: Map) => { diff --git a/src/ui/handler/keyboard.js b/src/ui/handler/keyboard.js index a1a267f0014..f47b2b1eb85 100644 --- a/src/ui/handler/keyboard.js +++ b/src/ui/handler/keyboard.js @@ -54,7 +54,7 @@ class KeyboardHandler { this._active = false; } - keydown: (e: KeyboardEvent) => ?HandlerResult = (e) => { + keydown(e: KeyboardEvent): ?HandlerResult { if (e.altKey || e.ctrlKey || e.metaKey) return; let zoomDir = 0; diff --git a/src/ui/handler/map_event.js b/src/ui/handler/map_event.js index a2f74a3d9c9..a611fa0cc92 100644 --- a/src/ui/handler/map_event.js +++ b/src/ui/handler/map_event.js @@ -22,13 +22,13 @@ export class MapEventHandler { this._mousedownPos = undefined; } - wheel: (e: WheelEvent) => ?HandlerResult = (e) => { + wheel(e: WheelEvent): ?HandlerResult { // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - ScrollZoom return this._firePreventable(new MapWheelEvent(e.type, this._map, e)); } - mousedown: (e: MouseEvent, point: Point) => ?HandlerResult = (e, point) => { + mousedown(e: MouseEvent, point: Point): ?HandlerResult { this._mousedownPos = point; // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - MousePan @@ -36,9 +36,9 @@ export class MapEventHandler { // - MousePitch // - DblclickHandler return this._firePreventable(new MapMouseEvent(e.type, this._map, e)); - }; + } - mouseup: (e: MouseEvent) => void = (e) => { + mouseup(e: MouseEvent) { this._map.fire(new MapMouseEvent(e.type, this._map, e)); } @@ -48,27 +48,27 @@ export class MapEventHandler { this._map.fire(new MapMouseEvent(synth.type, this._map, synth)); } - click: (e: MouseEvent, point: Point) => void = (e, point) => { + click(e: MouseEvent, point: Point) { if (this._mousedownPos && this._mousedownPos.dist(point) >= this._clickTolerance) return; this.preclick(e); this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - dblclick: (e: MouseEvent) => ?HandlerResult = (e) => { + dblclick(e: MouseEvent): ?HandlerResult { // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - DblClickZoom return this._firePreventable(new MapMouseEvent(e.type, this._map, e)); } - mouseover: (e: MouseEvent) => void = (e) => { + mouseover(e: MouseEvent) { this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - mouseout: (e: MouseEvent) => void = (e) => { + mouseout(e: MouseEvent) { this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - touchstart: (e: TouchEvent) => ?HandlerResult = (e) => { + touchstart(e: TouchEvent): ?HandlerResult { // If mapEvent.preventDefault() is called by the user, prevent handlers such as: // - TouchPan // - TouchZoom @@ -79,15 +79,15 @@ export class MapEventHandler { return this._firePreventable(new MapTouchEvent(e.type, this._map, e)); } - touchmove: (e: TouchEvent) => void = (e) => { + touchmove(e: TouchEvent) { this._map.fire(new MapTouchEvent(e.type, this._map, e)); } - touchend: (e: TouchEvent) => void = (e) => { + touchend(e: TouchEvent) { this._map.fire(new MapTouchEvent(e.type, this._map, e)); } - touchcancel: (e: TouchEvent) => void = (e) => { + touchcancel(e: TouchEvent) { this._map.fire(new MapTouchEvent(e.type, this._map, e)); } @@ -124,23 +124,23 @@ export class BlockableMapEventHandler { this._contextMenuEvent = undefined; } - mousemove: (e: MouseEvent) => void = (e) => { + mousemove(e: MouseEvent) { // mousemove map events should not be fired when interaction handlers (pan, rotate, etc) are active this._map.fire(new MapMouseEvent(e.type, this._map, e)); } - mousedown: () => void = () => { + mousedown() { this._delayContextMenu = true; } - mouseup: () => void = () => { + mouseup() { this._delayContextMenu = false; if (this._contextMenuEvent) { this._map.fire(new MapMouseEvent('contextmenu', this._map, this._contextMenuEvent)); delete this._contextMenuEvent; } } - contextmenu: (e: MouseEvent) => void = (e) => { + contextmenu(e: MouseEvent) { if (this._delayContextMenu) { // Mac: contextmenu fired on mousedown; we save it until mouseup for consistency's sake this._contextMenuEvent = e; diff --git a/src/ui/handler/mouse.js b/src/ui/handler/mouse.js index f85e50f251f..df64d23075a 100644 --- a/src/ui/handler/mouse.js +++ b/src/ui/handler/mouse.js @@ -61,7 +61,7 @@ class MouseHandler { this._eventButton = eventButton; } - mousemoveWindow: (e: MouseEvent, point: Point) => ?HandlerResult = (e, point) => { + mousemoveWindow(e: MouseEvent, point: Point): ?HandlerResult { const lastPoint = this._lastPoint; if (!lastPoint) return; e.preventDefault(); @@ -85,7 +85,7 @@ class MouseHandler { return this._move(lastPoint, point); } - mouseupWindow: (e: MouseEvent) => void = (e) => { + mouseupWindow(e: MouseEvent) { if (!this._lastPoint) return; const eventButton = DOM.mouseButton(e); if (eventButton !== this._eventButton) return; @@ -143,7 +143,7 @@ export class MouseRotateHandler extends MouseHandler { } } - contextmenu: (e: MouseEvent) => void = (e) => { + contextmenu(e: MouseEvent) { // prevent browser context menu when necessary; we don't allow it with rotation // because we can't discern rotation gesture start from contextmenu on Mac e.preventDefault(); @@ -164,7 +164,7 @@ export class MousePitchHandler extends MouseHandler { } } - contextmenu: (e: MouseEvent) => void = (e) => { + contextmenu(e: MouseEvent) { // prevent browser context menu when necessary; we don't allow it with rotation // because we can't discern rotation gesture start from contextmenu on Mac e.preventDefault(); diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 9c4b54f4ee6..e9f1b941f57 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -161,7 +161,7 @@ class ScrollZoomHandler { } } - wheel: (e: WheelEvent) => void = (e) => { + wheel(e: WheelEvent) { if (!this.isEnabled()) return; if (this._map._cooperativeGestures) { @@ -196,6 +196,7 @@ class ScrollZoomHandler { this._lastValue = value; // Start a timeout in case this was a singular event, and delay it by up to 40ms. + // $FlowFixMe[method-unbinding] this._timeout = setTimeout(this._onTimeout, 40, e); } else if (!this._type) { @@ -227,7 +228,7 @@ class ScrollZoomHandler { e.preventDefault(); } - _onTimeout: (initialEvent: WheelEvent) => void = (initialEvent) => { + _onTimeout(initialEvent: WheelEvent) { this._type = 'wheel'; this._delta -= this._lastValue; if (!this._active) { @@ -263,7 +264,7 @@ class ScrollZoomHandler { } } - renderFrame: () => ?HandlerResult = () => { + renderFrame(): ?HandlerResult { if (!this._frameId) return; this._frameId = null; diff --git a/src/ui/handler/tap_drag_zoom.js b/src/ui/handler/tap_drag_zoom.js index 4fa6e8b224c..0953fd5d6c4 100644 --- a/src/ui/handler/tap_drag_zoom.js +++ b/src/ui/handler/tap_drag_zoom.js @@ -31,7 +31,7 @@ export default class TapDragZoomHandler { this._tap.reset(); } - touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchstart(e: TouchEvent, points: Array, mapTouches: Array) { if (this._swipePoint) return; if (this._tapTime && e.timeStamp - this._tapTime > MAX_TAP_INTERVAL) { @@ -47,7 +47,7 @@ export default class TapDragZoomHandler { } - touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { + touchmove(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { if (!this._tapTime) { this._tap.touchmove(e, points, mapTouches); } else if (this._swipePoint) { @@ -68,7 +68,7 @@ export default class TapDragZoomHandler { } } - touchend: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchend(e: TouchEvent, points: Array, mapTouches: Array) { if (!this._tapTime) { const point = this._tap.touchend(e, points, mapTouches); if (point) { @@ -81,7 +81,7 @@ export default class TapDragZoomHandler { } } - touchcancel: () => void = () => { + touchcancel() { this.reset(); } diff --git a/src/ui/handler/tap_zoom.js b/src/ui/handler/tap_zoom.js index 62e1b250a4e..afeb7a48760 100644 --- a/src/ui/handler/tap_zoom.js +++ b/src/ui/handler/tap_zoom.js @@ -32,17 +32,17 @@ export default class TapZoomHandler { this._zoomOut.reset(); } - touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchstart(e: TouchEvent, points: Array, mapTouches: Array) { this._zoomIn.touchstart(e, points, mapTouches); this._zoomOut.touchstart(e, points, mapTouches); } - touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchmove(e: TouchEvent, points: Array, mapTouches: Array) { this._zoomIn.touchmove(e, points, mapTouches); this._zoomOut.touchmove(e, points, mapTouches); } - touchend: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { + touchend(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { const zoomInPoint = this._zoomIn.touchend(e, points, mapTouches); const zoomOutPoint = this._zoomOut.touchend(e, points, mapTouches); @@ -71,7 +71,7 @@ export default class TapZoomHandler { } } - touchcancel: () => void = () => { + touchcancel() { this.reset(); } diff --git a/src/ui/handler/touch_pan.js b/src/ui/handler/touch_pan.js index ed9789b8bbd..62a6d5a80d2 100644 --- a/src/ui/handler/touch_pan.js +++ b/src/ui/handler/touch_pan.js @@ -35,11 +35,11 @@ export default class TouchPanHandler { this._sum = new Point(0, 0); } - touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { + touchstart(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { return this._calculateTransform(e, points, mapTouches); } - touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { + touchmove(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { if (!this._active || mapTouches.length < this._minTouches) return; // if cooperative gesture handling is set to true, require two fingers to touch pan @@ -61,7 +61,7 @@ export default class TouchPanHandler { return this._calculateTransform(e, points, mapTouches); } - touchend: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchend(e: TouchEvent, points: Array, mapTouches: Array) { this._calculateTransform(e, points, mapTouches); if (this._active && mapTouches.length < this._minTouches) { @@ -69,7 +69,7 @@ export default class TouchPanHandler { } } - touchcancel: () => void = () => { + touchcancel() { this.reset(); } diff --git a/src/ui/handler/touch_zoom_rotate.js b/src/ui/handler/touch_zoom_rotate.js index f39ce6052be..d59cf6411c8 100644 --- a/src/ui/handler/touch_zoom_rotate.js +++ b/src/ui/handler/touch_zoom_rotate.js @@ -27,7 +27,7 @@ class TwoTouchHandler { _start(points: [Point, Point]) {} //eslint-disable-line _move(points: [Point, Point], pinchAround: ?Point, e: TouchEvent): ?HandlerResult { return {}; } //eslint-disable-line - touchstart: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchstart(e: TouchEvent, points: Array, mapTouches: Array) { //console.log(e.target, e.targetTouches.length ? e.targetTouches[0].target : null); //log('touchstart', points, e.target.innerHTML, e.targetTouches.length ? e.targetTouches[0].target.innerHTML: undefined); if (this._firstTwoTouches || mapTouches.length < 2) return; @@ -41,7 +41,7 @@ class TwoTouchHandler { this._start([points[0], points[1]]); } - touchmove: (e: TouchEvent, points: Array, mapTouches: Array) => ?HandlerResult = (e, points, mapTouches) => { + touchmove(e: TouchEvent, points: Array, mapTouches: Array): ?HandlerResult { const firstTouches = this._firstTwoTouches; if (!firstTouches) return; @@ -58,7 +58,7 @@ class TwoTouchHandler { } - touchend: (e: TouchEvent, points: Array, mapTouches: Array) => void = (e, points, mapTouches) => { + touchend(e: TouchEvent, points: Array, mapTouches: Array) { if (!this._firstTwoTouches) return; const [idA, idB] = this._firstTwoTouches; @@ -71,7 +71,7 @@ class TwoTouchHandler { this.reset(); } - touchcancel: () => void = () => { + touchcancel() { this.reset(); } @@ -139,8 +139,7 @@ export class TouchZoomHandler extends TwoTouchHandler { const ROTATION_THRESHOLD = 25; // pixels along circumference of touch circle -function getBearingDelta(a, b) { - if (!a) throw new Error('Point in `getBearingDelta` is undefined'); +function getBearingDelta(a: Point, b: Point) { return a.angleWith(b) * 180 / Math.PI; } @@ -167,6 +166,7 @@ export class TouchRotateHandler extends TwoTouchHandler { this._active = true; return { + // $FlowFixMe[incompatible-call] - Flow doesn't infer that this._vectoris not null bearingDelta: getBearingDelta(this._vector, lastVector), pinchAround }; diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index c0264f351bd..c9a1f1a83a9 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -221,6 +221,7 @@ class HandlerManager { ]; for (const [target, type, listenerOptions] of this._listeners) { + // $FlowFixMe[method-unbinding] const listener = target === window.document ? this.handleWindowEvent : this.handleEvent; target.addEventListener((type: any), (listener: any), listenerOptions); } @@ -228,6 +229,7 @@ class HandlerManager { destroy() { for (const [target, type, listenerOptions] of this._listeners) { + // $FlowFixMe[method-unbinding] const listener = target === window.document ? this.handleWindowEvent : this.handleEvent; target.removeEventListener((type: any), (listener: any), listenerOptions); } @@ -236,21 +238,27 @@ class HandlerManager { _addDefaultHandlers(options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number }) { const map = this._map; const el = map.getCanvasContainer(); + // $FlowFixMe[method-unbinding] this._add('mapEvent', new MapEventHandler(map, options)); const boxZoom = map.boxZoom = new BoxZoomHandler(map, options); + // $FlowFixMe[method-unbinding] this._add('boxZoom', boxZoom); const tapZoom = new TapZoomHandler(); const clickZoom = new ClickZoomHandler(); map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom); + // $FlowFixMe[method-unbinding] this._add('tapZoom', tapZoom); + // $FlowFixMe[method-unbinding] this._add('clickZoom', clickZoom); const tapDragZoom = new TapDragZoomHandler(); + // $FlowFixMe[method-unbinding] this._add('tapDragZoom', tapDragZoom); const touchPitch = map.touchPitch = new TouchPitchHandler(map); + // $FlowFixMe[method-unbinding] this._add('touchPitch', touchPitch); const mouseRotate = new MouseRotateHandler(options); @@ -266,20 +274,26 @@ class HandlerManager { map.dragPan = new DragPanHandler(el, mousePan, touchPan); // $FlowFixMe[method-unbinding] this._add('mousePan', mousePan); + // $FlowFixMe[method-unbinding] this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); const touchRotate = new TouchRotateHandler(); const touchZoom = new TouchZoomHandler(); map.touchZoomRotate = new TouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom); + // $FlowFixMe[method-unbinding] this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']); + // $FlowFixMe[method-unbinding] this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']); + // $FlowFixMe[method-unbinding] this._add('blockableMapEvent', new BlockableMapEventHandler(map)); const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, this); + // $FlowFixMe[method-unbinding] this._add('scrollZoom', scrollZoom, ['mousePan']); const keyboard = map.keyboard = new KeyboardHandler(); + // $FlowFixMe[method-unbinding] this._add('keyboard', keyboard); for (const name of ['boxZoom', 'doubleClickZoom', 'tapDragZoom', 'touchPitch', 'dragRotate', 'dragPan', 'touchZoomRotate', 'scrollZoom', 'keyboard']) { @@ -339,7 +353,7 @@ class HandlerManager { return false; } - handleWindowEvent: (e: InputEvent) => void = (e) => { + handleWindowEvent(e: InputEvent) { this.handleEvent(e, `${e.type}Window`); } @@ -354,7 +368,7 @@ class HandlerManager { return ((mapTouches: any): TouchList); } - handleEvent: (e: InputEvent | RenderFrameEvent, eventName?: string) => void = (e, eventName) => { + handleEvent(e: InputEvent | RenderFrameEvent, eventName?: string) { this._updatingCamera = true; assert(e.timeStamp !== undefined); diff --git a/src/ui/hash.js b/src/ui/hash.js index 607739937bd..4d1829a5639 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -26,6 +26,7 @@ export default class Hash { ], this); // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. + // $FlowFixMe[method-unbinding] this._updateHash = throttle(this._updateHashUnthrottled.bind(this), 30 * 1000 / 100); } @@ -37,6 +38,7 @@ export default class Hash { */ addTo(map: Map): this { this._map = map; + // $FlowFixMe[method-unbinding] window.addEventListener('hashchange', this._onHashChange, false); map.on('moveend', this._updateHash); return this; @@ -51,6 +53,7 @@ export default class Hash { if (!this._map) return this; this._map.off('moveend', this._updateHash); + // $FlowFixMe[method-unbinding] window.removeEventListener('hashchange', this._onHashChange, false); clearTimeout(this._updateHash()); @@ -102,7 +105,7 @@ export default class Hash { return hash.split('/'); } - _onHashChange: () => boolean = () => { + _onHashChange(): boolean { const map = this._map; if (!map) return false; const loc = this._getCurrentHash(); @@ -119,7 +122,7 @@ export default class Hash { return false; } - _updateHashUnthrottled: () => void = () => { + _updateHashUnthrottled() { // Replace if already present, else append the updated hash string const location = window.location.href.replace(/(#.+)?$/, this.getHashString()); window.history.replaceState(window.history.state, null, location); diff --git a/src/ui/map.js b/src/ui/map.js index 4e62f2b7181..d989aa5064a 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -563,10 +563,15 @@ class Map extends Camera { this.on('zoom', () => this._update(true)); if (typeof window !== 'undefined') { + // $FlowFixMe[method-unbinding] window.addEventListener('online', this._onWindowOnline, false); + // $FlowFixMe[method-unbinding] window.addEventListener('resize', this._onWindowResize, false); + // $FlowFixMe[method-unbinding] window.addEventListener('orientationchange', this._onWindowResize, false); + // $FlowFixMe[method-unbinding] window.addEventListener('webkitfullscreenchange', this._onWindowResize, false); + // $FlowFixMe[method-unbinding] window.addEventListener('visibilitychange', this._onVisibilityChange, false); } @@ -603,9 +608,12 @@ class Map extends Camera { this.resize(); if (options.attributionControl) + // $FlowFixMe[method-unbinding] this.addControl(new AttributionControl({customAttribution: options.customAttribution})); + // $FlowFixMe[method-unbinding] this._logoControl = new LogoControl(); + // $FlowFixMe[method-unbinding] this.addControl(this._logoControl, options.logoPosition); this.on('style.load', () => { @@ -2909,7 +2917,9 @@ class Map extends Camera { } this._canvas = DOM.create('canvas', 'mapboxgl-canvas', canvasContainer); + // $FlowFixMe[method-unbinding] this._canvas.addEventListener('webglcontextlost', this._contextLost, false); + // $FlowFixMe[method-unbinding] this._canvas.addEventListener('webglcontextrestored', this._contextRestored, false); this._canvas.setAttribute('tabindex', '0'); this._canvas.setAttribute('aria-label', this._getUIString('Map.Title')); @@ -2924,6 +2934,7 @@ class Map extends Camera { positions[positionName] = DOM.create('div', `mapboxgl-ctrl-${positionName}`, controlContainer); }); + // $FlowFixMe[method-unbinding] this._container.addEventListener('scroll', this._onMapScroll, false); } @@ -2993,7 +3004,7 @@ class Map extends Camera { webpSupported.testSupport(gl); } - _contextLost: (event: any) => void = (event) => { + _contextLost(event: any) { event.preventDefault(); if (this._frame) { this._frame.cancel(); @@ -3002,14 +3013,14 @@ class Map extends Camera { this.fire(new Event('webglcontextlost', {originalEvent: event})); } - _contextRestored: (event: any) => void = (event) => { + _contextRestored(event: any) { this._setupPainter(); this.resize(); this._update(); this.fire(new Event('webglcontextrestored', {originalEvent: event})); } - _onMapScroll: (event: any) => ?boolean = (event) => { + _onMapScroll(event: any): ?boolean { if (event.target !== this._container) return; // Revert any scroll which would move the canvas outside of the view @@ -3059,12 +3070,14 @@ class Map extends Camera { * @returns An id that can be used to cancel the callback * @private */ - _requestRenderFrame: (callback: () => void) => TaskID = (callback) => { + // $FlowFixMe[method-unbinding] + _requestRenderFrame(callback: () => void): TaskID { this._update(); return this._renderTaskQueue.add(callback); } - _cancelRenderFrame: (id: TaskID) => void = (id) => { + // $FlowFixMe[method-unbinding] + _cancelRenderFrame(id: TaskID) { this._renderTaskQueue.remove(id); } @@ -3502,17 +3515,24 @@ class Map extends Camera { this.setStyle(null); if (typeof window !== 'undefined') { + // $FlowFixMe[method-unbinding] window.removeEventListener('resize', this._onWindowResize, false); + // $FlowFixMe[method-unbinding] window.removeEventListener('orientationchange', this._onWindowResize, false); + // $FlowFixMe[method-unbinding] window.removeEventListener('webkitfullscreenchange', this._onWindowResize, false); + // $FlowFixMe[method-unbinding] window.removeEventListener('online', this._onWindowOnline, false); + // $FlowFixMe[method-unbinding] window.removeEventListener('visibilitychange', this._onVisibilityChange, false); } const extension = this.painter.context.gl.getExtension('WEBGL_lose_context'); if (extension) extension.loseContext(); + // $FlowFixMe[method-unbinding] this._canvas.removeEventListener('webglcontextlost', this._contextLost, false); + // $FlowFixMe[method-unbinding] this._canvas.removeEventListener('webglcontextrestored', this._contextRestored, false); this._canvasContainer.remove(); @@ -3525,6 +3545,7 @@ class Map extends Camera { this._missingCSSCanary = (undefined: any); this._container.classList.remove('mapboxgl-map'); + // $FlowFixMe[method-unbinding] this._container.removeEventListener('scroll', this._onMapScroll, false); PerformanceUtils.clearMetrics(); @@ -3569,7 +3590,8 @@ class Map extends Camera { * @private * @returns {Object} Returns `this` | Promise. */ - _preloadTiles: (transform: Transform | Array) => Map = (transform) => { + // $FlowFixMe[method-unbinding] + _preloadTiles(transform: Transform | Array): this { const sources: Array = this.style ? (Object.values(this.style._sourceCaches): any) : []; asyncAll(sources, (source, done) => source._preloadTiles(transform, done), () => { this.triggerRepaint(); @@ -3578,17 +3600,17 @@ class Map extends Camera { return this; } - _onWindowOnline: () => void = () => { + _onWindowOnline() { this._update(); } - _onWindowResize: (event: Event) => void = (event: Event) => { + _onWindowResize(event: Event) { if (this._trackResize) { this.resize({originalEvent: event})._update(); } } - _onVisibilityChange: () => void = () => { + _onVisibilityChange() { if (window.document.visibilityState === 'hidden') { this._visibilityHidden++; } diff --git a/src/ui/marker.js b/src/ui/marker.js index d3bfe400d82..cdd556465b1 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -199,7 +199,9 @@ export default class Marker extends Evented { this._map = map; map.getCanvasContainer().appendChild(this._element); map.on('move', this._updateMoving); + // $FlowFixMe[method-unbinding] map.on('moveend', this._update); + // $FlowFixMe[method-unbinding] map.on('remove', this._clearFadeTimer); map._addMarker(this); this.setDraggable(this._draggable); @@ -208,6 +210,7 @@ export default class Marker extends Evented { // If we attached the `click` listener to the marker element, the popup // would close once the event propogated to `map` due to the // `Popup#_onClickClose` listener. + // $FlowFixMe[method-unbinding] map.on('click', this._onMapClick); return this; @@ -224,15 +227,24 @@ export default class Marker extends Evented { remove(): this { const map = this._map; if (map) { + // $FlowFixMe[method-unbinding] map.off('click', this._onMapClick); map.off('move', this._updateMoving); + // $FlowFixMe[method-unbinding] map.off('moveend', this._update); + // $FlowFixMe[method-unbinding] map.off('mousedown', this._addDragHandler); + // $FlowFixMe[method-unbinding] map.off('touchstart', this._addDragHandler); + // $FlowFixMe[method-unbinding] map.off('mouseup', this._onUp); + // $FlowFixMe[method-unbinding] map.off('touchend', this._onUp); + // $FlowFixMe[method-unbinding] map.off('mousemove', this._onMove); + // $FlowFixMe[method-unbinding] map.off('touchmove', this._onMove); + // $FlowFixMe[method-unbinding] map.off('remove', this._clearFadeTimer); map._removeMarker(this); this._map = undefined; @@ -313,6 +325,7 @@ export default class Marker extends Evented { this._popup.remove(); this._popup = null; this._element.removeAttribute('role'); + // $FlowFixMe[method-unbinding] this._element.removeEventListener('keypress', this._onKeyPress); if (!this._originalTabIndex) { @@ -345,6 +358,7 @@ export default class Marker extends Evented { if (!this._originalTabIndex) { this._element.setAttribute('tabindex', '0'); } + // $FlowFixMe[method-unbinding] this._element.addEventListener('keypress', this._onKeyPress); this._element.setAttribute('aria-expanded', 'false'); } @@ -352,7 +366,7 @@ export default class Marker extends Evented { return this; } - _onKeyPress: (e: KeyboardEvent) => void = (e) => { + _onKeyPress(e: KeyboardEvent) { const code = e.code; const legacyCode = e.charCode || e.keyCode; @@ -364,7 +378,7 @@ export default class Marker extends Evented { } } - _onMapClick: (e: MapMouseEvent) => void = (e) => { + _onMapClick(e: MapMouseEvent) { const targetElement = e.originalEvent.target; const element = this._element; @@ -429,7 +443,7 @@ export default class Marker extends Evented { } - _evaluateOpacity: () => void = () => { + _evaluateOpacity() { const map = this._map; if (!map) return; @@ -459,7 +473,7 @@ export default class Marker extends Evented { this._fadeTimer = null; } - _clearFadeTimer: () => void = () => { + _clearFadeTimer() { if (this._fadeTimer) { clearTimeout(this._fadeTimer); this._fadeTimer = null; @@ -545,7 +559,7 @@ export default class Marker extends Evented { return rotation ? `rotateZ(${rotation}deg)` : ''; } - _update: (delaySnap?: boolean) => void = (delaySnap) => { + _update(delaySnap?: boolean) { window.cancelAnimationFrame(this._updateFrameId); const map = this._map; if (!map) return; @@ -578,6 +592,7 @@ export default class Marker extends Evented { } if ((map._showingGlobe() || map.getTerrain() || map.getFog()) && !this._fadeTimer) { + // $FlowFixMe[method-unbinding] this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60); } }); @@ -608,7 +623,7 @@ export default class Marker extends Evented { return this; } - _onMove: (e: MapMouseEvent | MapTouchEvent) => void = (e) => { + _onMove(e: MapMouseEvent | MapTouchEvent) { const map = this._map; if (!map) return; @@ -658,7 +673,7 @@ export default class Marker extends Evented { this.fire(new Event('drag')); } - _onUp: () => void = () => { + _onUp() { // revert to normal pointer event handling this._element.style.pointerEvents = 'auto'; this._positionDelta = null; @@ -667,7 +682,9 @@ export default class Marker extends Evented { const map = this._map; if (map) { + // $FlowFixMe[method-unbinding] map.off('mousemove', this._onMove); + // $FlowFixMe[method-unbinding] map.off('touchmove', this._onMove); } @@ -688,7 +705,7 @@ export default class Marker extends Evented { this._state = 'inactive'; } - _addDragHandler: (e: MapMouseEvent | MapTouchEvent) => void = (e) => { + _addDragHandler(e: MapMouseEvent | MapTouchEvent) { const map = this._map; const pos = this._pos; if (!map || !pos) return; @@ -706,9 +723,13 @@ export default class Marker extends Evented { this._pointerdownPos = e.point; this._state = 'pending'; + // $FlowFixMe[method-unbinding] map.on('mousemove', this._onMove); + // $FlowFixMe[method-unbinding] map.on('touchmove', this._onMove); + // $FlowFixMe[method-unbinding] map.once('mouseup', this._onUp); + // $FlowFixMe[method-unbinding] map.once('touchend', this._onUp); } } @@ -729,10 +750,14 @@ export default class Marker extends Evented { const map = this._map; if (map) { if (shouldBeDraggable) { + // $FlowFixMe[method-unbinding] map.on('mousedown', this._addDragHandler); + // $FlowFixMe[method-unbinding] map.on('touchstart', this._addDragHandler); } else { + // $FlowFixMe[method-unbinding] map.off('mousedown', this._addDragHandler); + // $FlowFixMe[method-unbinding] map.off('touchstart', this._addDragHandler); } } diff --git a/src/ui/popup.js b/src/ui/popup.js index 16c5986c885..c41e0cb90f6 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -143,23 +143,29 @@ export default class Popup extends Evented { this._map = map; if (this.options.closeOnClick) { + // $FlowFixMe[method-unbinding] map.on('preclick', this._onClose); } if (this.options.closeOnMove) { + // $FlowFixMe[method-unbinding] map.on('move', this._onClose); } + // $FlowFixMe[method-unbinding] map.on('remove', this.remove); this._update(); map._addPopup(this); this._focusFirstElement(); if (this._trackPointer) { + // $FlowFixMe[method-unbinding] map.on('mousemove', this._onMouseEvent); + // $FlowFixMe[method-unbinding] map.on('mouseup', this._onMouseEvent); map._canvasContainer.classList.add('mapboxgl-track-pointer'); } else { + // $FlowFixMe[method-unbinding] map.on('move', this._update); } @@ -206,7 +212,7 @@ export default class Popup extends Evented { * popup.remove(); * @returns {Popup} Returns itself to allow for method chaining. */ - remove: () => Popup = () => { + remove(): this { if (this._content) { this._content.remove(); } @@ -218,13 +224,21 @@ export default class Popup extends Evented { const map = this._map; if (map) { + // $FlowFixMe[method-unbinding] map.off('move', this._update); + // $FlowFixMe[method-unbinding] map.off('move', this._onClose); + // $FlowFixMe[method-unbinding] map.off('preclick', this._onClose); + // $FlowFixMe[method-unbinding] map.off('click', this._onClose); + // $FlowFixMe[method-unbinding] map.off('remove', this.remove); + // $FlowFixMe[method-unbinding] map.off('mousemove', this._onMouseEvent); + // $FlowFixMe[method-unbinding] map.off('mouseup', this._onMouseEvent); + // $FlowFixMe[method-unbinding] map.off('drag', this._onMouseEvent); if (map._canvasContainer) { map._canvasContainer.classList.remove('mapboxgl-track-pointer'); @@ -290,7 +304,9 @@ export default class Popup extends Evented { const map = this._map; if (map) { + // $FlowFixMe[method-unbinding] map.on('move', this._update); + // $FlowFixMe[method-unbinding] map.off('mousemove', this._onMouseEvent); map._canvasContainer.classList.remove('mapboxgl-track-pointer'); } @@ -315,8 +331,11 @@ export default class Popup extends Evented { this._update(); const map = this._map; if (map) { + // $FlowFixMe[method-unbinding] map.off('move', this._update); + // $FlowFixMe[method-unbinding] map.on('mousemove', this._onMouseEvent); + // $FlowFixMe[method-unbinding] map.on('drag', this._onMouseEvent); map._canvasContainer.classList.add('mapboxgl-track-pointer'); } @@ -456,6 +475,7 @@ export default class Popup extends Evented { button.setAttribute('aria-label', 'Close popup'); button.setAttribute('aria-hidden', 'true'); button.innerHTML = '×'; + // $FlowFixMe[method-unbinding] button.addEventListener('click', this._onClose); } this._update(); @@ -544,7 +564,7 @@ export default class Popup extends Evented { return finalState; } - _onMouseEvent: (event: MapMouseEvent) => void = (event: MapMouseEvent) => { + _onMouseEvent(event: MapMouseEvent) { this._update(event.point); } @@ -595,7 +615,7 @@ export default class Popup extends Evented { container.className = classes.join(' '); } - _update: (cursor?: Point) => void = (cursor?: Point) => { + _update(cursor?: Point) { const hasPosition = this._lngLat || this._trackPointer; const map = this._map; const content = this._content; @@ -649,7 +669,7 @@ export default class Popup extends Evented { if (firstFocusable) firstFocusable.focus(); } - _onClose: () => void = () => { + _onClose() { this.remove(); } diff --git a/src/util/actor.js b/src/util/actor.js index 255101ff332..7c709c06959 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -36,6 +36,7 @@ class Actor { this.callbacks = {}; this.cancelCallbacks = {}; bindAll(['receive'], this); + // $FlowFixMe[method-unbinding] this.target.addEventListener('message', this.receive, false); this.globalScope = isWorker() ? target : window; this.scheduler = new Scheduler(); @@ -85,7 +86,7 @@ class Actor { }; } - receive: (message: any) => void = (message) => { + receive(message: Object) { const data = message.data, id = data.id; @@ -170,6 +171,7 @@ class Actor { remove() { this.scheduler.remove(); + // $FlowFixMe[method-unbinding] this.target.removeEventListener('message', this.receive, false); } } diff --git a/src/util/mapbox.js b/src/util/mapbox.js index d735c7c8b9b..1910ee9b424 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -418,7 +418,7 @@ export class PerformanceEvent extends TelemetryEvent { super('gljs.performance'); } - postPerformanceEvent: (customAccessToken: ?string, performanceData: LivePerformanceData) => void = (customAccessToken, performanceData) => { + postPerformanceEvent(customAccessToken: ?string, performanceData: LivePerformanceData) { if (config.EVENTS_URL) { if (customAccessToken || config.ACCESS_TOKEN) { this.queueRequest({timestamp: Date.now(), performanceData}, customAccessToken); @@ -461,7 +461,7 @@ export class MapLoadEvent extends TelemetryEvent { this.skuToken = ''; } - postMapLoadEvent: (mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) => void = (mapId, skuToken, customAccessToken, callback) => { + postMapLoadEvent(mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) { this.skuToken = skuToken; this.errorCb = callback; @@ -540,7 +540,7 @@ export class MapSessionAPI extends TelemetryEvent { }); } - getSessionAPI: (mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) => void = (mapId, skuToken, customAccessToken, callback) => { + getSessionAPI(mapId: number, skuToken: string, customAccessToken: ?string, callback: EventCallback) { this.skuToken = skuToken; this.errorCb = callback; @@ -576,7 +576,7 @@ export class TurnstileEvent extends TelemetryEvent { this._customAccessToken = customAccessToken; } - postTurnstileEvent: (tileUrls: Array, customAccessToken?: ?string) => void = (tileUrls, customAccessToken) => { + postTurnstileEvent(tileUrls: Array, customAccessToken?: ?string) { //Enabled only when Mapbox Access Token is set and a source uses // mapbox tiles. if (config.EVENTS_URL && @@ -641,15 +641,19 @@ export class TurnstileEvent extends TelemetryEvent { } const turnstileEvent_ = new TurnstileEvent(); +// $FlowFixMe[method-unbinding] export const postTurnstileEvent: (tileUrls: Array, customAccessToken?: ?string) => void = turnstileEvent_.postTurnstileEvent.bind(turnstileEvent_); const mapLoadEvent_ = new MapLoadEvent(); +// $FlowFixMe[method-unbinding] export const postMapLoadEvent: (number, string, ?string, EventCallback) => void = mapLoadEvent_.postMapLoadEvent.bind(mapLoadEvent_); export const performanceEvent_: PerformanceEvent = new PerformanceEvent(); +// $FlowFixMe[method-unbinding] export const postPerformanceEvent: (?string, LivePerformanceData) => void = performanceEvent_.postPerformanceEvent.bind(performanceEvent_); const mapSessionAPI_ = new MapSessionAPI(); +// $FlowFixMe[method-unbinding] export const getMapSessionAPI: (number, string, ?string, EventCallback) => void = mapSessionAPI_.getSessionAPI.bind(mapSessionAPI_); const authenticatedMaps = new Set(); diff --git a/src/util/scheduler.js b/src/util/scheduler.js index a65827bdd81..1b603d59431 100644 --- a/src/util/scheduler.js +++ b/src/util/scheduler.js @@ -32,6 +32,7 @@ class Scheduler { this.tasks = {}; this.taskQueue = []; bindAll(['process'], this); + // $FlowFixMe[method-unbinding] this.invoker = new ThrottledInvoker(this.process); this.nextId = 0; @@ -64,7 +65,7 @@ class Scheduler { }; } - process: () => void = () => { + process() { const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; try { this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); From c6302c3ceac388624126588d029b2f1ed1996b25 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Mon, 3 Apr 2023 15:53:53 +0300 Subject: [PATCH 72/72] Fix painter.js --- src/render/painter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/render/painter.js b/src/render/painter.js index 33065b58ef0..d9e3c9a4d1b 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -554,9 +554,9 @@ class Painter { // Terrain depth offscreen render pass ========================== // With terrain on, renders the depth buffer into a texture. // This texture is used for occlusion testing (labels) - if (this.terrain && (this.style.hasSymbolLayers() || this.style.hasCircleLayers())) { - // $FlowFixMe[incompatible-use] - Flow can't infer that terrain is not null - this.terrain.drawDepth(); + const terrain = this.terrain; + if (terrain && (this.style.hasSymbolLayers() || this.style.hasCircleLayers())) { + terrain.drawDepth(); } // Rebind the main framebuffer now that all offscreen layers have been rendered: