import { INTERNAL_LAYER_PREFIX } from '../core/Constants';
import {
now,
extend,
IS_NODE,
isNil,
isString,
isFunction,
sign,
UID,
b64toBlob,
isNumber,
isObject,
isArrayHasData
} from '../core/util';
import Class from '../core/Class';
import Browser from '../core/Browser';
import Eventable from '../core/Eventable';
import Handlerable from '../handler/Handlerable';
import Point from '../geo/Point';
import Size from '../geo/Size';
import PointExtent from '../geo/PointExtent';
import Extent, { ExtentLike } from '../geo/Extent';
import Coordinate from '../geo/Coordinate';
import Layer from '../layer/Layer';
import Renderable from '../renderer/Renderable';
import SpatialReference, { type SpatialReferenceType } from './spatial-reference/SpatialReference';
import { computeDomPosition, MOUSEMOVE_THROTTLE_TIME } from '../core/util/dom';
import EPSG9807, { type EPSG9807ProjectionType } from '../geo/projection/Projection.EPSG9807.js';
import { AnimationOptionsType, EasingType } from '../core/Animation';
import { BBOX, bboxInBBOX, getDefaultBBOX, pointsBBOX } from '../core/util/bbox';
import { Attribution } from '../control';
import { AttributionOptionsType } from '../control/Control.Attribution';
import { intersectsBox } from 'frustum-intersects';
const TEMP_COORD = new Coordinate(0, 0);
const TEMP_POINT = new Point(0, 0);
const REDRAW_OPTIONS_PROPERTIES = ['centerCross', 'fog', 'fogColor', 'debugSky'];
const SKYBOX3 = [[0, 0, 0], [0, 0, 0]];
/**
* @property {Object} options - map's options, options must be updated by config method:
map.config('zoomAnimation', false);
* @property {Boolean} [options.centerCross=false] - Display a red cross in the center of map
* @property {Boolean} [options.seamlessZoom=true] - whether to use seamless zooming mode
* @property {Boolean} [options.zoomInCenter=false] - whether to fix in the center when zooming
* @property {Number} [options.zoomOrigin=null] - zoom origin in container point, e.g. [400, 300]
* @property {Boolean} [options.zoomAnimation=true] - enable zooming animation
* @property {Number} [options.zoomAnimationDuration=330] - zoom animation duration.
* @property {Boolean} [options.panAnimation=true] - continue to animate panning when dragging or touching ended.
* @property {Boolean} [options.panAnimationDuration=600] - duration of pan animation.
* @property {Boolean} [options.rotateAnimation=true] - continue to animate rotating when dragging or touching rotation ended.
* @property {Boolean} [options.rotateAnimationDuration=800] - duration of rotate animation.
* @property {Boolean} [options.zoomable=true] - whether to enable map zooming.
* @property {Boolean} [options.enableInfoWindow=true] - whether to enable infowindow on this map.
* @property {Boolean} [options.hitDetect=true] - whether to enable hit detecting of layers for cursor style on this map, disable it to improve performance.
* @property {Boolean} [options.hitDetectLimit=5] - the maximum number of layers to perform hit detect.
* @property {Boolean} [options.fpsOnInteracting=25] - fps when map is interacting, some slow layers will not be drawn on interacting when fps is low. Set to 0 to disable it.
* @property {Boolean} [options.layerCanvasLimitOnInteracting=-1] - limit of layer canvas to draw on map when interacting, set it to improve perf.
* @property {Number} [options.maxZoom=null] - the maximum zoom the map can be zooming to.
* @property {Number} [options.minZoom=null] - the minimum zoom the map can be zooming to.
* @property {Extent} [options.maxExtent=null] - when maxExtent is set, map will be restricted to the give max extent and bouncing back when user trying to pan ouside the extent.
* @property {Boolean} [options.fixCenterOnResize=true] - whether to fix map center when map is resized
*
* @property {Number} [options.maxPitch=80] - max pitch
* @property {Number} [options.maxVisualPitch=70] - the max pitch to be visual
*
* @property {Extent} [options.viewHistory=true] - whether to record view history
* @property {Extent} [options.viewHistoryCount=10] - the count of view history record.
*
* @property {Boolean} [options.draggable=true] - disable the map dragging if set to false.
* @property {Boolean} [options.dragPan=true] - if true, map can be dragged to pan.
* @property {Boolean} [options.dragRotate=true] - default true. If true, map can be dragged to rotate by right click or ctrl + left click.
* @property {Boolean} [options.dragPitch=true] - default true. If true, map can be dragged to pitch by right click or ctrl + left click.
* @property {Boolean} [options.dragRotatePitch=true] - if true, map is dragged to pitch and rotate at the same time.
* @property {Number} [options.switchDragButton=false] - switch to use left click (or touch on mobile) to rotate map and right click to move map.
* @property {Boolean} [options.touchGesture=true] - whether to allow map to zoom/rotate/tilt by two finger touch gestures.
* @property {Boolean} [options.touchZoom=true] - whether to allow map to zoom by touch pinch.
* @property {Boolean} [options.touchRotate=true] - whether to allow map to rotate by touch pinch.
* @property {Boolean} [options.touchPitch=true] - whether to allow map to pitch by touch pinch.
* @property {Boolean} [options.touchZoomRotate=false] - if true, map is to zoom and rotate at the same time by touch pinch.
* @property {Boolean} [options.doubleClickZoom=true] - whether to allow map to zoom by double click events.
* @property {Boolean} [options.scrollWheelZoom=true] - whether to allow map to zoom by scroll wheel events.
* @property {Boolean} [options.geometryEvents=true] - enable/disable firing geometry events
* @property {Number} [options.clickTimeThreshold=280] - time threshold between mousedown(touchstart) and mouseup(touchend) to determine if it's a click event
*
* @property {Boolean} [options.control=true] - whether allow map to add controls.
* @property {Boolean|Object} [options.attribution=true] - whether to display the attribution control on the map. if true, attribution display maptalks info; if object, you can specify positon or your base content, and both;
* @property {Boolean|Object} [options.zoomControl=false] - display the zoom control on the map if set to true or a object as the control construct option.
* @property {Boolean|Object} [options.scaleControl=false] - display the scale control on the map if set to true or a object as the control construct option.
* @property {Boolean|Object} [options.overviewControl=false] - display the overview control on the map if set to true or a object as the control construct option.
*
* @property {Boolean} [options.fog=true] - whether to draw fog in far distance.
* @property {Number[]} [options.fogColor=[233, 233, 233]] - color of fog: [r, g, b]
*
* @property {String | String[]} [options.renderer=['canvas', 'gl', 'gpu']] - renderer type. Don't change it if you are not sure about it. About renderer, see [TODO]{@link tutorial.renderer}.
* @property {Number} [options.devicePixelRatio=null] - device pixel ratio to override device's default one
* @property {Number} [options.heightFactor=1] - the factor for height/altitude calculation,This affects the height calculation of all layers(vectortilelayer/gllayer/threelayer/3dtilelayer)
* @property {Boolean} [options.stopRenderOnOffscreen=true] - whether to stop map rendering when container is offscreen
* @property {Boolean} [options.originLatitudeForAltitude=40] - default latitude for map.altitudeToPoint method
* @property {Boolean} [options.mousemoveThrottleEnable=true] - enable map mousemove event throttling
* @property {Number} [options.mousemoveThrottleTime=48] - mousemove event interval time(ms)
* @property {Number} [options.maxFPS=0] - 0 means no frame is locked, otherwise the frame is locked
* @property {Number} [options.cameraFarUndergroundInMeter=2000] - camera far distance from underground in meter
* @property {Boolean} [options.queryTerrainInMapEvents=false] - whether to query terrain in map's event
* @memberOf Map
* @instance
*/
const options: MapOptionsType = {
'maxVisualPitch': 70,
'maxPitch': 80,
'centerCross': false,
'zoomable': true,
'zoomInCenter': false,
'zoomOrigin': null,
'zoomAnimation': (function () {
return !IS_NODE;
})(),
'zoomAnimationDuration': 330,
'tileBackgroundLimitPerFrame': 3,
'panAnimation': (function () {
return !IS_NODE;
})(),
//default pan animation duration
'panAnimationDuration': 600,
'rotateAnimation': (function () {
return !IS_NODE;
})(),
'enableInfoWindow': true,
'hitDetect': (function () {
return !Browser.mobile;
})(),
'hitDetectLimit': 5,
'fpsOnInteracting': 25,
'layerCanvasLimitOnInteracting': -1,
'maxZoom': null,
'minZoom': null,
'maxExtent': null,
'limitExtentOnMaxExtent': false,
'fixCenterOnResize': true,
'checkSize': true,
'checkSizeInterval': 1000,
'renderer': ['canvas', 'gl', 'gpu'],
'cascadePitches': [10, 60],
'renderable': true,
'clickTimeThreshold': 280,
'stopRenderOnOffscreen': true,
'preventWheelScroll': true,
'preventTouch': true,
//for plugin layer,such as threelayer
'supportPluginEvent': true,
'switchDragButton': false,
'mousemoveThrottleTime': MOUSEMOVE_THROTTLE_TIME,
'mousemoveThrottleEnable': true,
'maxFPS': 0,
'debug': false,
'cameraFarUndergroundInMeter': 2000,
'cameraNearScale': 1,
'onlyWebGL1': false,
'forceRedrawPerFrame': false,
'preserveDrawingBuffer': true,
'extensions': [],
'optionalExtensions': [
'ANGLE_instanced_arrays',
'OES_element_index_uint',
'OES_standard_derivatives',
'OES_vertex_array_object',
'OES_texture_half_float',
'OES_texture_half_float_linear',
'OES_texture_float',
'OES_texture_float_linear',
'WEBGL_depth_texture',
'EXT_shader_texture_lod',
'EXT_frag_depth',
'EXT_texture_filter_anisotropic',
// compressed textures
'WEBGL_compressed_texture_astc',
'WEBGL_compressed_texture_etc',
'WEBGL_compressed_texture_etc1',
'WEBGL_compressed_texture_pvrtc',
'WEBGL_compressed_texture_s3tc',
'WEBGL_compressed_texture_s3tc_srgb'
],
};
/**
* The central class of the library, to create a map on a container.
*
* @category map
*
* @mixes Eventable
* @mixes Handlerable
* @mixes ui.Menuable
* @mixes Renderable
*
* @example
* var map = new maptalks.Map("map",{
* center: [180,0],
* zoom: 4,
* baseLayer : new maptalks.TileLayer("base",{
* urlTemplate:'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
* subdomains:['a','b','c']
* }),
* layers : [
* new maptalks.VectorLayer('v', [new maptalks.Marker([180, 0])])
* ]
* });
*/
export class Map extends Handlerable(Eventable(Renderable(Class))) {
VERSION: string;
//@internal
_loaded: boolean;
//@internal
_panels: Record;
//@internal
_baseLayer: Layer;
//@internal
_layers: Array;
//@internal
_zoomLevel: number;
//@internal
_center: Coordinate;
//@internal
_mapViewPoint: Point;
isMap: boolean = true;
//@internal
_containerDOM: HTMLDivElement | HTMLCanvasElement;
//@internal
_spatialReference: SpatialReference;
//@internal
_originLng: number;
//@internal
_altitudeOriginDirty: boolean;
//@internal
_glScale: number;
//@internal
_cursor: string;
//@internal
_prjCenter: Coordinate;
centerAltitude: number;
width: number;
height: number;
//@internal
_prjMaxExtent: PointExtent;
//@internal
_glRes: number;
//@internal
_zooming: boolean;
//@internal
_layerCache: { [key: string]: Layer };
//@internal
_mapViewCoord: Coordinate;
//@internal
_eventSilence: boolean;
//@internal
_moving: boolean;
//@internal
_originCenter: Coordinate;
//@internal
_suppressRecenter: boolean;
//@internal
_dragRotating: boolean;
CanvasClass: any;
//@internal
_priorityCursor: string;
//@internal
_initTime: number;
//@internal
_renderer: any;
//@internal
_containerDomContentRect: DOMRect;
//@internal
_mapRes: number;
//@internal
_onLoadHooks: Array<(...args) => void>;
cameraCenterDistance: number;
//@internal
_limitMaxExtenting: boolean;
//@internal
_currentViewGLInfo: any;
options: MapOptionsType;
static VERSION: string;
JSON_VERSION: '1.0';
attributionControl?: Attribution;
id: number;
/**
* @param {(string|HTMLElement|object)} container - The container to create the map on, can be:
* 1. A HTMLElement container.
* 2. ID of a HTMLElement container.
* 3. Any canvas compatible container
* @param {Object} options - construct options
* @param {(Number[]|Coordinate)} options.center - initial center of the map.
* @param {Number} options.zoom - initial zoom of the map.
* @param {Object} [options.spatialReference=null] - map's spatial reference, default is using projection EPSG:3857 with resolutions used by google map/osm.
* @param {Layer} [options.baseLayer=null] - base layer that will be set to map initially.
* @param {Layer[]} [options.layers=null] - layers that will be added to map initially.
* @param {*} options.* - any other option defined in [Map.options]{@link Map#options} [description]
*/
constructor(container: MapContainerType,
options: MapCreateOptionsType) {
if (!options) {
throw new Error('Invalid options when creating map.');
}
if (!options['center']) {
throw new Error('Invalid center when creating map.');
}
// prepare options
const opts = extend({} as any, options) as MapOptionsType;
const zoom = opts['zoom'];
delete opts['zoom'];
const center = new Coordinate(opts['center']);
delete opts['center'];
const baseLayer = opts['baseLayer'];
delete opts['baseLayer'];
const layers = opts['layers'];
delete opts['layers'];
super(opts);
/**
* @property {String} - Version of library
* @constant
* @static
*/
this.VERSION = Map.VERSION;
Object.defineProperty(this, 'id', {
value: UID(),
writable: false
});
this._loaded = false;
this._initContainer(container);
this._panels = {};
//Layers
this._baseLayer = null;
this._layers = [];
this._zoomLevel = zoom;
this._center = center;
this.setSpatialReference(opts['spatialReference'] || opts['view']);
this._mapViewPoint = new Point(0, 0);
this._initRenderer();
this._updateMapSize(this._getContainerDomSize());
if (baseLayer) {
this.setBaseLayer(baseLayer);
}
if (layers) {
this.addLayer(layers);
}
this.setMaxExtent(opts['maxExtent']);
this._Load();
this.proxyOptions()
}
/**
* Add hooks for additional codes when map's loading complete, useful for plugin developping.
* Note that it can only be called before the map is created.
* @param {Function | any} fn
* @returns {Map}
*/
static addOnLoadHook(fn: string | ((...args) => void), ...args) { // (Function) || (String, args...)
// const args = Array.prototype.slice.call(arguments, 1);
const onload = typeof fn === 'function' ? fn : function () {
this[fn].call(this, ...args);
};
this.prototype._onLoadHooks = this.prototype._onLoadHooks || [];
this.prototype._onLoadHooks.push(onload);
return this;
}
/**
* Whether the map is loaded or not.
* @return {Boolean}
*/
isLoaded() {
return !!this._loaded;
}
/**
* Get map's container
* @returns {HTMLElement}
*/
getContainer() {
return this._containerDOM;
}
/**
* Get the spatial reference of the Map.
* @return {SpatialReference} map's spatial reference
*/
getSpatialReference() {
return this._spatialReference;
}
/**
* Change the spatial reference of the map.
* A SpatialReference is a series of settings to decide the map presentation:
* 1. the projection.
* 2. zoom levels and resolutions.
* 3. full extent.
* There are some [predefined spatial references]{@link http://www.foo.com}, and surely you can [define a custom one.]{@link http://www.foo.com}.
* SpatialReference can also be updated by map.config('spatialReference', spatialReference);
* @param {SpatialReference} spatialReference - spatial reference
* @returns {Map} this
* @fires Map#spatialreferencechange
* @example
* map.setSpatialReference({
projection:'EPSG:4326',
resolutions: (function() {
const resolutions = [];
for (let i=0; i < 19; i++) {
resolutions[i] = 180/(Math.pow(2, i)*128);
}
return resolutions;
})()
* });
@example
* map.config('spatialReference', {
projection:'EPSG:4326',
resolutions: (function() {
const resolutions = [];
for (let i=0; i < 19; i++) {
resolutions[i] = 180/(Math.pow(2, i)*128);
}
return resolutions;
})()
});
*/
setSpatialReference(ref: SpatialReferenceType) {
const oldRef = this.options['spatialReference'];
if (this._loaded && SpatialReference.equals(oldRef, ref)) {
return this;
}
this._updateSpatialReference(ref, oldRef);
return this;
}
//@internal
_updateSpatialReference(ref: SpatialReferenceType, oldRef) {
if (isString(ref)) {
ref = SpatialReference.getPreset(ref);
}
ref = extend({}, ref);
this._center = this.getCenter();
this.options['spatialReference'] = ref;
this._spatialReference = new SpatialReference(ref);
const projection = this._spatialReference.getProjection();
if (this.options['spatialReference'] && isFunction(this.options['spatialReference']['projection'])) {
//save projection code for map profiling (toJSON/fromJSON)
this.options['spatialReference']['projection'] = projection['code'];
}
this._resetMapStatus();
if (EPSG9807.is(projection.code)) {
this._originLng = (projection as EPSG9807ProjectionType).centralMeridian;
this._altitudeOriginDirty = true;
}
/**
* spatialreferencechange event, fired when map's spatial reference is updated.
*
* @event Map#spatialreferencechange
* @type {Object}
* @property {String} type - spatialreferencechange
* @property {Map} target - map
* @property {Map} old - the old spatial reference
* @property {Map} new - the new spatial reference changed to
*/
this._fireEvent('spatialreferencechange', {
'old': oldRef,
'new': extend({} as any, this.options['spatialReference'])
});
return this;
}
// _syncWorld() {
// const projection = this.getProjection();
// if (!projection) {
// return false;
// }
// const pcenter = this._getPrjCenter();
// if (projection.isOutSphere(pcenter)) {
// const wrapped = projection.wrapCoord(pcenter);
// this._setPrjCenter(wrapped);
// this._fireEvent('syncworld', { 'old' : pcenter.toArray(), 'new' : wrapped.toArray() });
// return true;
// }
// return false;
// }
/**
* Callback when any option is updated
* @param {Object} conf - options to update
* @return {Map} this
*/
onConfig(conf: { [key: string]: any }) {
const ref = conf['spatialReference'] || conf['view'];
if (!isNil(ref)) {
this._updateSpatialReference(ref, null);
}
if (this.options.renderer === 'canvas') {
let needUpdate = false;
for (let i = 0, len = REDRAW_OPTIONS_PROPERTIES.length; i < len; i++) {
const key = REDRAW_OPTIONS_PROPERTIES[i];
if (!isNil(conf[key])) {
needUpdate = true;
break;
}
}
if (!needUpdate) {
return this;
}
}
const renderer = this.getRenderer();
if (renderer) {
renderer.setToRedraw();
}
return this;
}
/**
* Get the projection of the map.
* Projection is an algorithm for map projection, e.g. well-known [Mercator Projection]{@link https://en.wikipedia.org/wiki/Mercator_projection}
* A projection must have 2 methods:
* 1. project(coordinate) - project the input coordinate
* 2. unproject(coordinate) - unproject the input coordinate
* Projection also contains measuring method usually extended from a measurer:
* 1. measureLength(coord1, coord2) - compute length between 2 coordinates.
* 2. measureArea(coords[]) - compute area of the input coordinates.
* 3. locate(coord, distx, disty) - compute the coordinate from the coord with xdist on axis x and ydist on axis y.
* @return {Object}
*/
getProjection() {
if (!this._spatialReference) {
return null;
}
return this._spatialReference.getProjection();
}
/**
* Get map's full extent, which is defined in map's spatial reference.
* eg: {'left': -180, 'right' : 180, 'top' : 90, 'bottom' : -90}
* @return {Extent}
*/
getFullExtent() {
if (!this._spatialReference) {
return null;
}
return this._spatialReference.getFullExtent();
}
/**
* Set map's cursor style, cursor style is same with CSS.
* @param {String} cursor - cursor style
* @returns {Map} this
* @example
* map.setCursor('url(cursor.png) 4 12, auto');
*/
setCursor(cursor: string) {
delete this._cursor;
this._trySetCursor(cursor);
this._cursor = cursor;
return this;
}
/**
* Reset map's cursor style.
* @return {Map} this
* @example
* map.resetCursor();
*/
resetCursor() {
return this.setCursor(null);
}
/**
* Get center of the map.
* @return {Coordinate}
*/
getCenter(): Coordinate {
if (!this._loaded || !this._prjCenter) {
return this._center;
}
const projection = this.getProjection();
const center = projection.unproject(this._prjCenter);
center.x = Math.round(center.x * 1E8) / 1E8;
center.y = Math.round(center.y * 1E8) / 1E8;
if (this.centerAltitude) {
center.z = this.centerAltitude;
}
return center;
}
/**
* Set a new center to the map.
* @param {Coordinate} center
* @param {Object} [padding]
* @param {Number} [padding.paddingLeft] - Sets the amount of padding in the left of a map container
* @param {Number} [padding.paddingTop] - Sets the amount of padding in the top of a map container
* @param {Number} [padding.paddingRight] - Sets the amount of padding in the right of a map container
* @param {Number} [padding.paddingBottom] - Sets the amount of padding in the bottom of a map container
* @return {Map} this
*/
setCenter(center: Coordinate, padding?: MapPaddingType) {
if (!center) {
return this;
}
center = new Coordinate(center);
if (padding) {
center = this._getCenterByPadding(center, this.getZoom(), padding);
}
const projection = this.getProjection();
const pcenter = projection.project(center);
if (!this._verifyExtent(pcenter) && !this.options.limitExtentOnMaxExtent) {
return this;
}
if (!this._loaded) {
this._center = center;
return this;
}
this.onMoveStart();
this._setPrjCenter(pcenter);
this.onMoveEnd(this._parseEventFromCoord(this.getCenter()));
return this;
}
/**
* Get map's size (width and height) in pixel.
* @return {Size}
*/
getSize(): Size {
if (isNil(this.width) || isNil(this.height)) {
return this._getContainerDomSize();
}
return new Size(this.width, this.height);
}
/**
* Get container extent of the map
* @return {PointExtent}
*/
getContainerExtent() {
let visualHeight = this.height;
const pitch = this.getPitch(),
maxVisualPitch = this.options['maxVisualPitch'];
if (maxVisualPitch && pitch > maxVisualPitch) {
visualHeight = this._getVisualHeight(maxVisualPitch);
}
return new PointExtent(0, this.height - visualHeight, this.width, this.height);
}
/**
* get Ground Extent for sky ,line ,polygon 2d render clip etc
* @return {PointExtent}
*/
getGroundExtent() {
const extent = this.getContainerExtent();
const { ymin } = extent;
if (ymin > 0 || this.getPitch() > 60) {
const halfh = this.height / 2;
const halfw = this.width / 2;
const box = SKYBOX3;
const p1 = new Point(halfw, ymin);
const p2 = p1.copy();
const glRes = this.getGLRes();
const scale = this.getScale();
const calBox = () => {
const p3 = this['_containerPointToPointAtRes'](p1, glRes);
const p4 = this['_containerPointToPointAtRes'](p2, glRes);
box[0][0] = Math.min(p3.x, p4.x);
box[0][1] = Math.min(p3.y, p4.y);
box[1][0] = Math.max(p3.x, p4.x);
box[1][1] = Math.max(p3.y, p4.y);
}
if (scale > 15) {
for (let y = ymin; y <= halfh; y++) {
p1.y = y;
p2.y = y + 1;
calBox();
if (intersectsBox(this.projViewMatrix, box)) {
extent.ymin = y;
break;
}
}
} else {
for (let y = ymin; y >= 0; y--) {
p1.y = y;
p2.y = y - 1;
calBox();
if (!intersectsBox(this.projViewMatrix, box)) {
extent.ymin = y;
break;
}
}
}
}
return extent;
}
//@internal
_getVisualHeight(visualPitch) {
// const pitch = this.getPitch();
// const visualDistance = this.height / 2 * Math.tan(visualPitch * Math.PI / 180);
// return this.height / 2 + visualDistance * Math.tan((90 - pitch) * Math.PI / 180);
visualPitch = visualPitch || 1E-2;
const pitch = (90 - this.getPitch()) * Math.PI / 180;
const fov = this.getFov() * Math.PI / 180;
visualPitch *= Math.PI / 180;
const cameraToCenter = this.cameraCenterDistance / this.getGLScale();
const tanB = Math.tan(fov / 2);
const tanP = Math.tan(visualPitch);
const visualDistance = (cameraToCenter * tanB) / (1 / tanP - tanB) / Math.sin(visualPitch);
const x = cameraToCenter * (Math.sin(pitch) * visualDistance / (cameraToCenter + Math.cos(pitch) * visualDistance));
return this.height / 2 + x;
}
/**
* Get the geographical extent of map's current view extent.
*
* @return {Extent}
*/
getExtent() {
return this.pointToExtent(this.get2DExtent());
}
/**
* Get the projected geographical extent of map's current view extent.
*
* @return {Extent}
*/
getProjExtent() {
const extent2D = this.get2DExtent();
return new Extent(
this._pointToPrj(extent2D.getMin()),
this._pointToPrj(extent2D.getMax())
);
}
/**
* Alias for getProjExtent
*
* @return {Extent}
*/
getPrjExtent() {
return this.getProjExtent();
}
/**
* Get the max extent that the map is restricted to.
* @return {Extent}
*/
getMaxExtent() {
if (!this.options['maxExtent']) {
return null;
}
return new Extent(this.options['maxExtent'], this.getProjection());
}
/**
* Sets the max extent that the map is restricted to.
* @param {Extent}
* @return {Map} this
* @example
* map.setMaxExtent(map.getExtent());
*/
setMaxExtent(extent: Extent) {
if (extent) {
const maxExt = new Extent(extent, this.getProjection());
this.options['maxExtent'] = maxExt;
const projection = this.getProjection();
this._prjMaxExtent = maxExt.convertTo(c => projection.project(c));
if (!this._verifyExtent(this._getPrjCenter())) {
if (this._loaded) {
this._panTo(this._prjMaxExtent.getCenter());
} else {
this._center = projection.unproject(this._prjMaxExtent.getCenter());
}
}
} else {
delete this.options['maxExtent'];
delete this._prjMaxExtent;
}
return this;
}
/**
* Get map's current zoom.
* @return {Number}
*/
getZoom() {
return this._zoomLevel;
}
/**
* Caculate the target zoom if scaling from "fromZoom" by "scale"
* @param {Number} scale
* @param {Number} fromZoom
* @param {Boolean} isFraction - can return fractional zoom
* @return {Number} zoom fit for scale starting from fromZoom
*/
getZoomForScale(scale: number, fromZoom?: number, isFraction?: boolean) {
const zoom = this.getZoom();
if (isNil(fromZoom)) {
fromZoom = zoom;
}
if (scale === 1 && fromZoom === zoom) {
return zoom;
}
const res = this._getResolution(fromZoom),
targetRes = res / scale;
const scaleZoom = this.getZoomFromRes(targetRes);
if (isFraction) {
return scaleZoom;
} else {
const delta = 1E-6; //avoid precision
return this.getSpatialReference().getZoomDirection() < 0 ?
Math.ceil(scaleZoom - delta) : Math.floor(scaleZoom + delta);
}
}
getZoomFromRes(res: number): number {
const resolutions = this._getResolutions(),
minRes = this._getResolution(this.getMinZoom()),
maxRes = this._getResolution(this.getMaxZoom());
if (minRes <= maxRes) {
if (res <= minRes) {
return this.getMinZoom();
} else if (res >= maxRes) {
return this.getMaxZoom();
}
} else if (res >= minRes) {
return this.getMinZoom();
} else if (res <= maxRes) {
return this.getMaxZoom();
}
const l = resolutions.length;
for (let i = 0; i < l - 1; i++) {
if (!resolutions[i]) {
continue;
}
const gap = resolutions[i + 1] - resolutions[i];
const test = res - resolutions[i];
if (sign(gap) === sign(test) && Math.abs(gap) >= Math.abs(test)) {
return i + test / gap;
}
}
return l - 1;
}
/**
* Sets zoom of the map
* @param {Number} zoom
* @param {Object} [options=null] options
* @param {Boolean} [options.animation=true] whether zoom is animation, true by default
* @returns {Map} this
*/
setZoom(zoom: number, options = { 'animation': true }) {
if (isNaN(zoom) || isNil(zoom)) {
return this;
}
zoom = +zoom;
if (this._loaded && this.options['zoomAnimation'] && options['animation']) {
this._zoomAnimation(zoom);
} else {
this._zoom(zoom);
}
return this;
}
/**
* Get the max zoom that the map can be zoom to.
* @return {Number}
*/
getMaxZoom(): number {
if (!isNil(this.options['maxZoom'])) {
return this.options['maxZoom'];
}
return this.getMaxNativeZoom();
}
/**
* Sets the max zoom that the map can be zoom to.
* @param {Number} maxZoom
* @returns {Map} this
*/
setMaxZoom(maxZoom: number) {
const viewMaxZoom = this.getMaxNativeZoom();
if (maxZoom > viewMaxZoom) {
maxZoom = viewMaxZoom;
}
if (maxZoom !== null && maxZoom < this._zoomLevel) {
this.setZoom(maxZoom);
maxZoom = +maxZoom;
}
this.options['maxZoom'] = maxZoom;
return this;
}
/**
* Get the min zoom that the map can be zoom to.
* @return {Number}
*/
getMinZoom(): number {
if (!isNil(this.options['minZoom'])) {
return this.options['minZoom'];
}
return this._spatialReference.getMinZoom();
}
/**
* Sets the min zoom that the map can be zoom to.
* @param {Number} minZoom
* @return {Map} this
*/
setMinZoom(minZoom: number) {
if (minZoom !== null) {
minZoom = +minZoom;
const viewMinZoom = this._spatialReference.getMinZoom();
if (minZoom < viewMinZoom) {
minZoom = viewMinZoom;
}
if (minZoom > this._zoomLevel) {
this.setZoom(minZoom);
}
}
this.options['minZoom'] = minZoom;
return this;
}
/**
* Maximum zoom the map has
* @return {Number}
*/
getMaxNativeZoom(): number {
const ref = this.getSpatialReference();
if (!ref) {
return null;
}
return ref.getMaxZoom();
}
/**
* Resolution for world point in WebGL context
* @returns {Number}
*/
getGLRes(): number {
if (this._glRes) {
return this._glRes;
}
const fullExtent = this.getSpatialReference().getFullExtent();
this._glRes = (fullExtent.right - fullExtent.left) / Math.pow(2, 19);
return this._glRes;
// return this._getResolution(14);
// return this._getResolution(this.getMaxNativeZoom() / 2);
}
/**
* Caculate scale from gl zoom to given zoom (default by current zoom)
* @param {Number} [zoom=undefined] target zoom, current zoom by default
* @returns {Number}
* @examples
* const point = map.coordToPoint(map.getCenter());
* // convert to point in gl zoom
* const glPoint = point.multi(this.getGLScale());
*/
getGLScale(zoom?: number): number {
if (isNil(zoom)) {
zoom = this.getZoom();
}
return this._getResolution(zoom) / this.getGLRes();
}
/**
* zoom in
* @return {Map} this
*/
zoomIn() {
return this.setZoom(this.getZoom() + 1);
}
/**
* zoom out
* @return {Map} this
*/
zoomOut() {
return this.setZoom(this.getZoom() - 1);
}
/**
* Whether the map is zooming
* @return {Boolean}
*/
isZooming() {
return !!this._zooming;
}
/**
* Whether the map is being interacted
* @return {Boolean}
*/
isInteracting() {
return this.isZooming() || this.isMoving() || this.isRotating();
}
/**
* Sets the center and zoom at the same time.
* @param {Coordinate} center
* @param {Number} zoom
* @return {Map} this
*/
setCenterAndZoom(center: Coordinate, zoom?: number) {
if (!isNil(zoom) && this._zoomLevel !== zoom) {
this.setCenter(center);
this.setZoom(zoom, { animation: false });
} else {
this.setCenter(center);
}
return this;
}
/**
* Get the padding Size
* @param {Object} options
* @param {Number} [options.paddingLeft] - Sets the amount of padding in the left of a map container
* @param {Number} [options.paddingTop] - Sets the amount of padding in the top of a map container
* @param {Number} [options.paddingRight] - Sets the amount of padding in the right of a map container
* @param {Number} [options.paddingBottom] - Sets the amount of padding in the bottom of a map container
* @returns {Object|null}
*/
//@internal
_getPaddingSize(options = {}) {
if (options['paddingLeft'] || options['paddingTop'] || options['paddingRight'] || options['paddingBottom']) {
return {
width: (options['paddingLeft'] || 0) + (options['paddingRight'] || 0),
height: (options['paddingTop'] || 0) + (options['paddingBottom'] || 0)
};
}
return null;
}
/**
* Caculate the zoom level that contains the given extent with the maximum zoom level possible.
* @param {Extent} extent
* @param {Boolean} [isFraction] - can return fractional zoom
* @param {Object} [padding] [padding] - padding
* @param {Object} [padding.paddingLeft] - Sets the amount of padding in the left of a map container
* @param {Object} [padding.paddingTop] - Sets the amount of padding in the top of a map container
* @param {Object} [padding.paddingRight] - Sets the amount of padding in the right of a map container
* @param {Object} [padding.paddingBottom] - Sets the amount of padding in the bottom of a map container
* @return {Number} zoom fit for scale starting from fromZoom
*/
getFitZoom(extent: Extent, isFraction?: boolean, padding?: MapPaddingType) {
if (!extent || !(extent instanceof Extent)) {
return this._zoomLevel;
}
//It's a point
if (extent['xmin'] === extent['xmax'] && extent['ymin'] === extent['ymax']) {
return this.getMaxZoom();
}
let size = this.getSize();
const paddingSize = this._getPaddingSize(padding);
if (paddingSize) {
const rect = {
width: size.width - (paddingSize.width || 0),
height: size.height - (paddingSize.height || 0)
};
size = new Size(rect.width, rect.height);
}
const containerExtent = extent.convertTo(p => this.coordToPoint(p));
const w = containerExtent.getWidth(),
h = containerExtent.getHeight();
const scaleX = size['width'] / w,
scaleY = size['height'] / h;
const scale = this.getSpatialReference().getZoomDirection() < 0 ?
Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
const zoom = this.getZoomForScale(scale, null, isFraction);
return zoom;
}
/**
* Get map's current view (center/zoom/pitch/bearing)
* @return {Object} { center : *, zoom : *, pitch : *, bearing : * }
*/
getView(): MapViewType {
return {
'center': this.getCenter().toArray() as any,
'zoom': this.getZoom(),
'pitch': this.getPitch(),
'bearing': this.getBearing()
};
}
stringifyView() {
return JSON.stringify(this.getView());
}
//@internal
_validateView(view: MapViewType) {
if (!view || !isObject(view)) {
return;
}
if (isNumber(view.bearing)) {
let bearing = view.bearing;
//周期性
bearing = bearing % 360;
//自动转换为负的值
if (bearing > 180) {
bearing = -180 + Math.abs(bearing - 180);
}
//自动转换为正的值
if (bearing < -180) {
bearing = 180 - Math.abs(bearing + 180);
}
view.bearing = bearing;
// view.bearing = Math.max(-180, view.bearing);
// view.bearing = Math.min(180, view.bearing);
}
if (isNumber(view.pitch)) {
view.pitch = Math.max(0, view.pitch);
view.pitch = Math.min(this.options.maxPitch, view.pitch);
}
const maxZoom = this.getMaxZoom();
if (isNumber(view.zoom)) {
view.zoom = Math.max(0, view.zoom);
view.zoom = Math.min(maxZoom, view.zoom);
}
return;
}
/**
* Set map's center/zoom/pitch/bearing at one time
* @param {Object} view - a object containing center/zoom/pitch/bearing
* return {Map} this
*/
setView(view: MapViewType) {
if (!view) {
return this;
}
this._validateView(view);
if (view['center']) {
this.setCenter(view['center'] as Coordinate);
}
if (view['zoom'] !== null && !isNaN(+view['zoom'])) {
this.setZoom(+view['zoom'], { 'animation': false });
}
if (view['pitch'] !== null && !isNaN(+view['pitch'])) {
this.setPitch(+view['pitch']);
}
if (view['bearing'] !== null && !isNaN(+view['bearing'])) {
this.setBearing(+view['bearing']);
}
return this;
}
/**
* Get map's resolution
* @param {Number} zoom - zoom or current zoom if not given
* @return {Number} resolution
*/
getResolution(zoom?: number) {
return this._getResolution(zoom);
}
/**
* Get scale of resolutions from zoom to max zoom
* @param {Number} zoom - zoom or current zoom if not given
* @return {Number} scale
*/
getScale(zoom?: number) {
const z = (isNil(zoom) ? this.getZoom() : zoom);
const max = this._getResolution(this.getMaxNativeZoom()),
res = this._getResolution(z);
return res / max;
}
/**
* Get center by the padding.
* @private
* @param {Coordinate} center
* @param {Number} zoom
* @param {Object} padding
* @param {Number} [padding.paddingLeft] - Sets the amount of padding in the left of a map container
* @param {Number} [padding.paddingTop] - Sets the amount of padding in the top of a map container
* @param {Number} [padding.paddingRight] - Sets the amount of padding in the right of a map container
* @param {Number} [padding.paddingBottom] - Sets the amount of padding in the bottom of a map container
* @return {Coordinate}
*/
//@internal
_getCenterByPadding(center: Coordinate, zoom?: number, padding?: MapPaddingType) {
const point = this.coordinateToPoint(center, zoom);
const { paddingLeft = 0, paddingRight = 0, paddingTop = 0, paddingBottom = 0 } = padding || {};
let pX = 0;
let pY = 0;
if (paddingLeft || paddingRight) {
pX = (paddingRight - paddingLeft) / 2;
}
if (paddingTop || paddingBottom) {
pY = (paddingTop - paddingBottom) / 2;
}
const newPoint = new Point({
x: point.x + pX,
y: point.y + pY
});
return this.pointToCoordinate(newPoint, zoom);
}
/**
* Set the map to be fit for the given extent with the max zoom level possible.
* @param {ExtentLike} extent - extent
* @param {Number} zoomOffset - zoom offset
* @param {Object} [options={}] - options
* @param {Object} [options.animation]
* @param {Object} [options.duration]
* @param {Object} [options.zoomAnimationDuration]
* @param {Object} [options.easing='out']
* @param {Number} [options.paddingLeft] - Sets the amount of padding in the left of a map container
* @param {Number} [options.paddingTop] - Sets the amount of padding in the top of a map container
* @param {Number} [options.paddingRight] - Sets the amount of padding in the right of a map container
* @param {Number} [options.paddingBottom] - Sets the amount of padding in the bottom of a map container
* @param {Boolean} [options.isFraction=false] - can locate to fractional zoom
* @param {Function} step - step function for animation
* @return {Map | player} - this
*/
fitExtent(extent: ExtentLike, zoomOffset?: number, options?: MapFitType, step?: (frame) => void) {
options = (options || {} as any);
if (!extent) {
return this;
}
const syncExtent = new Extent(extent);
const zoom = this.getFitZoom(syncExtent, options.isFraction || false, options) + (zoomOffset || 0);
const containerExtent = syncExtent.convertTo(p => this.coordToPoint(p));
let center = this.pointToCoord(containerExtent.getCenter() as Point);
if (this._getPaddingSize(options)) {
center = this._getCenterByPadding(center, zoom, options);
}
const maxAltitude = (extent as Extent).zmax;
if (isNumber(maxAltitude) && maxAltitude !== 0) {
const currentCenterZ = this.getCenter().z || 0;
if (maxAltitude > currentCenterZ) {
center.z = maxAltitude;
}
}
if (typeof (options['animation']) === 'undefined' || options['animation'])
return this._animateTo({
center,
zoom
}, {
'duration': options['duration'] || this.options['zoomAnimationDuration'],
'easing': options['easing'] || 'out',
}, step);
else
return this.setCenterAndZoom(center, zoom);
}
/**
* Get the base layer of the map.
* @return {Layer}
*/
getBaseLayer() {
return this._baseLayer;
}
/**
* Sets a new base layer to the map.
* Some events will be thrown such as baselayerchangestart, baselayerload, baselayerchangeend.
* @param {Layer} baseLayer - new base layer
* @return {Map} this
* @fires Map#setbaselayer
* @fires Map#baselayerchangestart
* @fires Map#baselayerchangeend
*/
setBaseLayer(baseLayer: Layer) {
let isChange = false;
if (this._baseLayer) {
isChange = true;
/**
* baselayerchangestart event, fired when base layer is changed.
*
* @event Map#baselayerchangestart
* @type {Object}
* @property {String} type - baselayerchangestart
* @property {Map} target - map
*/
this._fireEvent('baselayerchangestart');
this._baseLayer.remove();
}
if (!baseLayer) {
delete this._baseLayer;
/**
* baselayerchangeend event, fired when base layer is changed.
*
* @event Map#baselayerchangeend
* @type {Object}
* @property {String} type - baselayerchangeend
* @property {Map} target - map
*/
this._fireEvent('baselayerchangeend');
/**
* setbaselayer event, fired when base layer is set.
*
* @event Map#setbaselayer
* @type {Object}
* @property {String} type - setbaselayer
* @property {Map} target - map
*/
this._fireEvent('setbaselayer');
return this;
}
this._baseLayer = baseLayer;
baseLayer._bindMap(this, -1);
function onbaseLayerload() {
/**
* baselayerload event, fired when base layer is loaded.
*
* @event Map#baselayerload
* @type {Object}
* @property {String} type - baselayerload
* @property {Map} target - map
*/
this._fireEvent('baselayerload');
if (isChange) {
isChange = false;
this._fireEvent('baselayerchangeend');
}
}
this._baseLayer.on('layerload', onbaseLayerload, this);
if (this._loaded) {
this._baseLayer.load();
}
this._fireEvent('setbaselayer');
return this;
}
/**
* Remove the base layer from the map
* @return {Map} this
* @fires Map#baselayerremove
*/
removeBaseLayer() {
if (this._baseLayer) {
this._baseLayer.remove();
delete this._baseLayer;
/**
* baselayerremove event, fired when base layer is removed.
*
* @event Map#baselayerremove
* @type {Object}
* @property {String} type - baselayerremove
* @property {Map} target - map
*/
this._fireEvent('baselayerremove');
}
return this;
}
/**
* Get the layers of the map, except base layer (which should be by getBaseLayer).
* A filter function can be given to filter layers, e.g. exclude all the VectorLayers.
* @param {Function} [filter=undefined] - a filter function of layers, return false to exclude the given layer.
* @return {Layer[]}
* @example
* var vectorLayers = map.getLayers(function (layer) {
* return (layer instanceof VectorLayer);
* });
*/
getLayers(filter?: (layer: Layer) => boolean): Array {
return this._getLayers(function (layer) {
if (layer === this._baseLayer || layer.getId().indexOf(INTERNAL_LAYER_PREFIX) >= 0) {
return false;
}
if (filter) {
return filter(layer);
}
return true;
});
}
/**
* Get the layer with the given id.
* @param {String} id - layer id
* @return {Layer}
*/
getLayer(id: string): Layer | null {
if (!id) {
return null;
}
const layer = this._layerCache ? this._layerCache[id] : null;
if (layer) {
return layer;
}
const baseLayer = this.getBaseLayer();
if (baseLayer && baseLayer.getId() === id) {
return baseLayer;
}
return null;
}
/**
* Add a new layer on the top of the map.
* @param {Layer|Layer[]} layer - one or more layers to add
* @return {Map} this
* @fires Map#addlayer
*/
addLayer(layers: Layer | Array, ...otherLayers: Array): this {
if (!layers) {
return this;
}
if (!Array.isArray(layers)) {
layers = [layers];
// layers = Array.prototype.slice.call(arguments, 0);
// return this.addLayer(layers);
}
if (otherLayers && otherLayers.length) {
layers = layers.concat(otherLayers);
}
if (!this._layerCache) {
this._layerCache = {};
}
const mapLayers = this._layers;
for (let i = 0, len = layers.length; i < len; i++) {
const layer = layers[i];
const id = layer.getId();
if (isNil(id)) {
throw new Error('Invalid id for the layer: ' + id);
}
if (layer.getMap() === this) {
continue;
}
if (this._layerCache[id]) {
throw new Error('Duplicate layer id in the map: ' + id);
}
this._layerCache[id] = layer;
layer._bindMap(this);
mapLayers.push(layer);
if (this._loaded) {
layer.load();
}
}
this._sortLayersByZIndex();
/**
* addlayer event, fired when adding layers.
*
* @event Map#addlayer
* @type {Object}
* @property {String} type - addlayer
* @property {Map} target - map
* @property {Layer[]} layers - layers to add
*/
this._fireEvent('addlayer', {
'layers': layers
});
return this;
}
/**
* Remove a layer from the map
* @param {String|String[]|Layer|Layer[]} layer - one or more layers or layer ids
* @return {Map} this
* @fires Map#removelayer
*/
removeLayer(layers: Layer | Array): this {
if (!layers) {
return this;
}
if (!Array.isArray(layers)) {
return this.removeLayer([layers]);
}
const removed = [];
for (let i = 0, len = layers.length; i < len; i++) {
let layer = layers[i];
if (!(layer instanceof Layer)) {
layer = this.getLayer(layer);
}
if (!layer) {
continue;
}
const map = layer.getMap();
if (!map || (map as any) !== this) {
continue;
}
removed.push(layer);
this._removeLayer(layer, this._layers);
if (this._loaded) {
layer._doRemove();
}
const id = layer.getId();
if (this._layerCache) {
delete this._layerCache[id];
}
}
if (removed.length > 0) {
const renderer = this.getRenderer();
if (renderer) {
if (this.options.renderer === 'canvas') {
renderer.setLayerCanvasUpdated();
} else {
renderer.setToRedraw();
}
}
this.once('frameend', () => {
removed.forEach(layer => {
layer.fire('remove');
});
});
}
/**
* removelayer event, fired when removing layers.
*
* @event Map#removelayer
* @type {Object}
* @property {String} type - removelayer
* @property {Map} target - map
* @property {Layer[]} layers - layers to remove
*/
this._fireEvent('removelayer', {
'layers': layers
});
return this;
}
//@internal
_findTerrainLayer() {
if (isTerrainLayer(this._baseLayer)) {
return this._baseLayer;
}
const layers = this._getLayers() || [];
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
if (isTerrainLayer(layer)) {
return layer;
}
}
return null;
}
/**
* Sort layers according to the order provided, the last will be on the top.
* @param {string[]|Layer[]} layers - layers or layer ids to sort
* @return {Map} this
* @example
* map.addLayer([layer1, layer2, layer3]);
* map.sortLayers([layer2, layer3, layer1]);
* map.sortLayers(['3', '2', '1']); // sort by layer ids.
*/
sortLayers(layers: Array) {
if (!layers || !Array.isArray(layers)) {
return this;
}
const layersToOrder = [];
let minZ = Number.MAX_VALUE;
for (let i = 0, l = layers.length; i < l; i++) {
let layer = layers[i];
if (isString(layers[i])) {
layer = this.getLayer(layer as any);
}
if (!(layer instanceof Layer) || !layer.getMap() || layer.getMap() as any !== this) {
throw new Error('It must be a layer added to this map to order.');
}
if (layer.getZIndex() < minZ) {
minZ = layer.getZIndex();
}
layersToOrder.push(layer);
}
for (let i = 0, l = layersToOrder.length; i < l; i++) {
layersToOrder[i].setZIndex(minZ + i);
}
return this;
}
/**
* Exports image from the map's canvas.
* @param {Object} [options=undefined] - options
* @param {String} [options.mimeType=image/png] - mime type of the image: image/png, image/jpeg, image/webp
* @param {String} [options.quality=0.92] - A Number between 0 and 1 indicating the image quality to use for image formats that use lossy compression such as image/jpeg and image/webp.
* @param {Boolean} [options.save=false] - whether pop a file save dialog to save the export image.
* @param {String} [options.fileName=export] - specify the file name, if options.save is true.
* @return {String} image of base64 format.
*/
toDataURL(options?: MapDataURLType): string | null {
if (!options) {
options = {};
}
let mimeType = options['mimeType'];
if (!mimeType) {
mimeType = 'image/png';
}
const save = options['save'];
const renderer = this._getRenderer();
if (renderer && renderer.toDataURL) {
let file = options['fileName'];
if (!file) {
file = 'export';
}
const dataURL = renderer.toDataURL(mimeType, options.quality || 0.92);
if (save && dataURL) {
let imgURL;
if (typeof Blob !== 'undefined' && typeof atob !== 'undefined') {
const blob = b64toBlob(dataURL.replace(/^data:image\/(png|jpeg|jpg|webp);base64,/, ''), mimeType);
imgURL = URL.createObjectURL(blob);
} else {
imgURL = dataURL;
}
const dlLink = document.createElement('a');
dlLink.download = file;
dlLink.href = imgURL;
document.body.appendChild(dlLink);
dlLink.click();
document.body.removeChild(dlLink);
}
return dataURL;
}
return null;
}
/**
* shorter alias for coordinateToPoint
*/
coordToPoint(coordinate: Coordinate, zoom?: number, out?: Point) {
return this.coordinateToPoint(coordinate, zoom, out);
}
/**
* shorter alias for coordinateToPointAtRes
*/
coordToPointAtRes(coordinate: Coordinate, res?: number, out?: Point) {
return this.coordinateToPointAtRes(coordinate, res, out);
}
/**
* shorter alias for pointToCoordinate
*/
pointToCoord(point: Point, zoom?: number, out?: Coordinate) {
return this.pointToCoordinate(point, zoom, out);
}
/**
* shorter alias for pointAtResToCoordinate
*/
pointAtResToCoord(point: Point, res?: number, out?: Coordinate) {
return this.pointAtResToCoordinate(point, res, out);
}
/**
* shorter alias for coordinateToViewPoint
*/
coordToViewPoint(coordinate: Coordinate, out?: Point, altitude?: number) {
return this.coordinateToViewPoint(coordinate, out, altitude);
}
/**
* shorter alias for viewPointToCoordinate
*/
viewPointToCoord(viewPoint: Point, out?: Coordinate) {
return this.viewPointToCoordinate(viewPoint, out);
}
/**
* shorter alias for coordinateToContainerPoint
*/
coordToContainerPoint(coordinate: Coordinate, zoom?: number, out?: Point) {
return this.coordinateToContainerPoint(coordinate, zoom, out);
}
/**
* shorter alias for containerPointToCoordinate
*/
containerPointToCoord(containerPoint: Point, out?: Coordinate) {
return this.containerPointToCoordinate(containerPoint, out);
}
/**
* Converts a container point to the view point.
* Usually used in plugin development.
* @param {Point}
* @param {Point} [out=undefined] - optional point to receive result
* @returns {Point}
*/
containerPointToViewPoint(containerPoint: Point, out?: Point) {
if (out) {
out.set(containerPoint.x, containerPoint.y);
} else {
out = containerPoint.copy() as Point;
}
return out._sub(this.getViewPoint());
}
/**
* Converts a view point to the container point.
* Usually used in plugin development.
* @param {Point}
* @param {Point} [out=undefined] - optional point to receive result
* @returns {Point}
*/
viewPointToContainerPoint(viewPoint: Point, out?: Point) {
if (out) {
out.set(viewPoint.x, viewPoint.y);
} else {
out = viewPoint.copy() as Point;
}
return out._add(this.getViewPoint());
}
/**
* Checks if the map container size changed and updates the map if so.
* @return {Map} this
* @fires Map#resize
*/
checkSize(force?: boolean) {
const justStart = ((now() - this._initTime) < 1500) && this.width === 0 || this.height === 0;
const watched = this._getContainerDomSize(),
oldHeight = this.height,
oldWidth = this.width;
if (!force && watched['width'] === oldWidth && watched['height'] === oldHeight) {
return this;
}
// refresh map's dom position
computeDomPosition(this._containerDOM);
const center = this.getCenter();
if (!this.options['fixCenterOnResize']) {
// fix northwest's geo coordinate
const vh = this._getVisualHeight(this.getPitch());
const nwCP = new Point(0, this.height - vh);
const nwCoord = this._containerPointToPrj(nwCP);
this._updateMapSize(watched);
const vhAfter = this._getVisualHeight(this.getPitch());
const nwCPAfter = new Point(0, this.height - vhAfter);
this._setPrjCoordAtContainerPoint(nwCoord, nwCPAfter);
// when size changed, center is updated but panel's offset remains.
this._mapViewCoord = this._getPrjCenter();
} else {
this._updateMapSize(watched);
}
const hided = (watched['width'] === 0 || watched['height'] === 0 || oldWidth === 0 || oldHeight === 0);
if (justStart || hided) {
this._eventSilence = true;
this.setCenter(center);
delete this._eventSilence;
}
/**
* resize event when map container's size changes
* @event Map#resize
* @type {Object}
* @property {String} type - resize
* @property {Map} target - map fires the event
*/
this._fireEvent('resize');
return this;
}
/**
* Computes the coordinate from the given meter distance.
* @param {Coordinate} coordinate - source coordinate
* @param {Number} dx - meter distance on X axis
* @param {Number} dy - meter distance on Y axis
* @return {Coordinate} Result coordinate
*/
locate(coordinate: Coordinate, dx: number, dy: number): Coordinate {
return (this.getProjection() as any)._locate(new Coordinate(coordinate), dx, dy);
}
/**
* Return map's main panel
* @returns {HTMLElement}
*/
getMainPanel(): HTMLDivElement | null {
const renderer = this._getRenderer();
if (!renderer) {
return null;
}
return renderer.getMainPanel();
}
/**
* Returns map panels.
* @return {Object}
*/
getPanels() {
return this._panels;
}
/**
* Remove the map
* @return {Map} this
*/
remove() {
if (this.isRemoved()) {
return this;
}
this._fireEvent('removestart');
//remove animation when map removed
const animationPlayerList = [this._animPlayer, this._mapAnimPlayer];
animationPlayerList.forEach(player => {
if (player && player.finish) {
this._stopAnim(player);
}
});
this._removeDomEvents();
this._clearHandlers();
this.removeBaseLayer();
const layers = this.getLayers();
for (let i = 0; i < layers.length; i++) {
layers[i].remove();
}
if (this._getRenderer()) {
this._getRenderer().remove();
}
if (this._containerDOM.childNodes && this._containerDOM.childNodes.length > 0) {
Array.prototype.slice.call(this._containerDOM.childNodes, 0)
.filter(node => node.className === 'maptalks-wrapper')
.forEach(node => this._containerDOM.removeChild(node));
}
delete this._panels;
delete this._containerDOM;
delete this._renderer;
this._fireEvent('removeend');
this._clearAllListeners();
return this;
}
/**
* whether the map is removed
* @return {Boolean}
*/
isRemoved() {
return !this._containerDOM;
}
/**
* Whether the map is moving
* @return {Boolean}
*/
isMoving() {
return !!this._moving;
}
/**
* The callback function when move started
* @private
* @fires Map#movestart
*/
onMoveStart(param?: any) {
if (this._mapAnimPlayer) {
this._stopAnim(this._mapAnimPlayer);
}
const prjCenter = this._getPrjCenter();
if (!this._originCenter || this._verifyExtent(prjCenter)) {
this._originCenter = prjCenter;
}
this._moving = true;
this._trySetCursor('move');
/**
* movestart event
* @event Map#movestart
* @type {Object}
* @property {String} type - movestart
* @property {Map} target - map fires the event
* @property {Coordinate} coordinate - coordinate of the event
* @property {Point} containerPoint - container point of the event
* @property {Point} viewPoint - view point of the event
* @property {Event} domEvent - dom event
*/
this._fireEvent('movestart', this._parseEvent(param ? param['domEvent'] : null, 'movestart'));
}
onMoving(param) {
/**
* moving event
* @event Map#moving
* @type {Object}
* @property {String} type - moving
* @property {Map} target - map fires the event
* @property {Coordinate} coordinate - coordinate of the event
* @property {Point} containerPoint - container point of the event
* @property {Point} viewPoint - view point of the event
* @property {Event} domEvent - dom event
*/
this._fireEvent('moving', this._parseEvent(param ? param['domEvent'] : null, 'moving'));
this._limitMaxExtent();
}
onMoveEnd(param) {
this._moving = false;
if (!this._suppressRecenter) {
this._recenterOnTerrain();
}
this._trySetCursor('default');
/**
* moveend event
* @event Map#moveend
* @type {Object}
* @property {String} type - moveend
* @property {Map} target - map fires the event
* @property {Coordinate} coordinate - coordinate of the event
* @property {Point} containerPoint - container point of the event
* @property {Point} viewPoint - view point of the event
* @property {Event} domEvent - dom event
*/
this._fireEvent('moveend', (param && param['domEvent']) ? this._parseEvent(param['domEvent'], 'moveend') : param);
if (!this._verifyExtent(this._getPrjCenter()) && this._originCenter && !this.options.limitExtentOnMaxExtent) {
const moveTo = this._originCenter;
this._panTo(moveTo);
}
this._limitMaxExtent();
}
onDragRotateStart(param) {
this._dragRotating = true;
/**
* dragrotatestart event
* @event Map#dragrotatestart
* @type {Object}
* @property {String} type - dragrotatestart
* @property {Map} target - map fires the event
* @property {Coordinate} coordinate - coordinate of the event
* @property {Point} containerPoint - container point of the event
* @property {Point} viewPoint - view point of the event
* @property {Event} domEvent - dom event
*/
this._fireEvent('dragrotatestart', this._parseEvent(param ? param['domEvent'] : null, 'dragrotatestart'));
}
onDragRotating(param) {
/**
* dragrotating event
* @event Map#dragrotating
* @type {Object}
* @property {String} type - dragrotating
* @property {Map} target - map fires the event
* @property {Coordinate} coordinate - coordinate of the event
* @property {Point} containerPoint - container point of the event
* @property {Point} viewPoint - view point of the event
* @property {Event} domEvent - dom event
*/
this._fireEvent('dragrotating', this._parseEvent(param ? param['domEvent'] : null, 'dragrotating'));
}
onDragRotateEnd(param) {
this._dragRotating = false;
/**
* dragrotateend event
* @event Map#dragrotateend
* @type {Object}
* @property {String} type - dragrotateend
* @property {Map} target - map fires the event
* @property {Coordinate} coordinate - coordinate of the event
* @property {Point} containerPoint - container point of the event
* @property {Point} viewPoint - view point of the event
* @property {Event} domEvent - dom event
*/
this._fireEvent('dragrotateend', this._parseEvent(param ? param['domEvent'] : null, 'dragrotateend'));
}
isDragRotating() {
return !!this._dragRotating;
}
/**
* Test if given box is out of current screen
* @param {Number[] | PointExtent} box - [minx, miny, maxx, maxy]
* @param {Number} padding - test padding
* @returns {Boolean}
*/
isOffscreen(box: PointExtent | Array, viewportPadding = 0) {
const { width, height } = this;
const screenRightBoundary = width + viewportPadding;
const screenBottomBoundary = height + viewportPadding;
let { xmin, ymin, xmax, ymax } = (box as PointExtent);
if (Array.isArray(box)) {
[xmin, ymin, xmax, ymax] = box;
}
return xmax < viewportPadding || xmin >= screenRightBoundary || ymax < viewportPadding || ymin > screenBottomBoundary;
}
getRenderer() {
return this._getRenderer();
}
/**
* Get map's devicePixelRatio, you can override it by setting devicePixelRatio in options.
* @returns {Number}
*/
getDevicePixelRatio(): number {
return this.options['devicePixelRatio'] || Browser.devicePixelRatio || 1;
}
/**
* Set map's devicePixelRatio
* @param {Number} dpr
* @returns {Map} this
*/
setDevicePixelRatio(dpr: number) {
if (isNumber(dpr) && dpr > 0 && dpr !== this.options['devicePixelRatio']) {
this.options['devicePixelRatio'] = dpr;
this.checkSize(true);
}
return this;
}
//-----------------------------------------------------------
//@internal
_initContainer(container: MapContainerType) {
if (isString(container)) {
this._containerDOM = document.getElementById(container) as HTMLDivElement;
if (!this._containerDOM) {
throw new Error('Invalid container when creating map: \'' + container + '\'');
}
} else {
this._containerDOM = container as HTMLDivElement;
if (IS_NODE) {
//Reserve container's constructor in node for canvas creating.
this.CanvasClass = this._containerDOM.constructor;
}
}
if (this._containerDOM?.childNodes?.length > 0) {
const firstChild = this._containerDOM.childNodes[0];
if (firstChild instanceof HTMLElement && firstChild.classList.contains('maptalks-wrapper')) {
throw new Error('Container is already loaded with another map instance, use map.remove() to clear it.');
}
}
}
/**
* try to change cursor when map is not setCursored
* @private
* @param {String} cursor css cursor
*/
//@internal
_trySetCursor(cursor: string) {
if (!this._cursor && !this._priorityCursor) {
if (!cursor) {
cursor = 'default';
}
this._setCursorToPanel(cursor);
}
return this;
}
//@internal
_setPriorityCursor(cursor: string) {
if (!cursor) {
let hasCursor = false;
if (this._priorityCursor) {
hasCursor = true;
}
delete this._priorityCursor;
if (hasCursor) {
this.setCursor(this._cursor);
}
} else {
this._priorityCursor = cursor;
this._setCursorToPanel(cursor);
}
return this;
}
//@internal
_setCursorToPanel(cursor: string) {
const panel = this.getMainPanel();
if (panel && panel.style && panel.style.cursor !== cursor) {
panel.style.cursor = cursor;
}
}
//remove a layer from the layerList
//@internal
_removeLayer(layer: Layer, layerList: Array) {
if (!layer || !layerList) {
return;
}
const index = layerList.indexOf(layer);
if (index > -1) {
layerList.splice(index, 1);
}
}
//@internal
_sortLayersByZIndex() {
if (!this._layers) {
return;
}
for (let i = 0, l = this._layers.length; i < l; i++) {
// this._layers[i]._order = i;
const layer = this._layers[i] as any;
layer._order = i;
if (layer.sortLayersByZIndex) {
layer.sortLayersByZIndex();
}
}
this._layers.sort(function (a, b) {
const c = a.getZIndex() - b.getZIndex();
if (c === 0) {
return (a as any)._order - (b as any)._order;
}
return c;
});
}
//@internal
_fireEvent(eventName: string, param?: { [key: string]: any }) {
if (this._eventSilence) {
return;
}
//fire internal events at first
const underline = '_';
if (eventName[0] !== underline) {
this.fire(underline + eventName, param);
}
this.fire(eventName, param);
}
//@internal
_Load() {
this._resetMapStatus();
if (this.options['pitch']) {
this.setPitch(this.options['pitch']);
delete this.options['pitch'];
}
if (this.options['bearing']) {
this.setBearing(this.options['bearing']);
delete this.options['bearing'];
}
delete this._glRes;
this._loadAllLayers();
this._getRenderer().onLoad();
this._loaded = true;
this._callOnLoadHooks();
this._initTime = now();
}
//@internal
_initRenderer() {
let renderer = this.options['renderer'];
if (!Array.isArray(renderer)) {
renderer = [renderer];
}
for (let i = 0; i < renderer.length; i++) {
const clazz = Map.getRendererClass(renderer[i]) as any;
if (clazz) {
this._renderer = new clazz(this);
this._renderer.load();
break;
}
}
if (!this._renderer) {
throw new Error('Invalid map.options.renderer: ' + this.options['renderer']);
}
}
//@internal
_getRenderer() {
return this._renderer;
}
//@internal
_loadAllLayers() {
function loadLayer(layer) {
if (layer) {
layer.load();
}
}
if (this._baseLayer) {
this._baseLayer.load();
}
this._eachLayer(loadLayer, this.getLayers());
}
/**
* Gets layers that fits for the filter
* @param {fn} filter - filter function
* @return {Layer[]}
* @private
*/
//@internal
_getLayers(filter?: (layer: Layer) => boolean) {
const layers = this._baseLayer ? [this._baseLayer].concat(this._layers) : this._layers;
const result = [];
for (let i = 0; i < layers.length; i++) {
if (!filter || filter.call(this, layers[i])) {
result.push(layers[i]);
}
}
return result;
}
//@internal
_eachLayer(fn, ...layerLists) {
if (arguments.length < 2) {
return;
}
// let layerLists = Array.prototype.slice.call(arguments, 1);
if (layerLists && !Array.isArray(layerLists)) {
layerLists = [layerLists];
}
let layers = [];
for (let i = 0, len = layerLists.length; i < len; i++) {
layers = layers.concat(layerLists[i]);
}
for (let j = 0, jlen = layers.length; j < jlen; j++) {
fn.call(fn, layers[j]);
}
}
//@internal
_onLayerEvent(param) {
if (!param) {
return;
}
if (param['type'] === 'idchange') {
delete this._layerCache[param['old']];
this._layerCache[param['new']] = param['target'];
}
}
//Check and reset map's status when map's spatial reference is changed.
//@internal
_resetMapStatus() {
let maxZoom = this.getMaxZoom(),
minZoom = this.getMinZoom();
const viewMaxZoom = this._spatialReference.getMaxZoom(),
viewMinZoom = this._spatialReference.getMinZoom();
if (isNil(maxZoom) || maxZoom === -1 || maxZoom > viewMaxZoom) {
this.setMaxZoom(viewMaxZoom);
}
if (isNil(minZoom) || minZoom === -1 || minZoom < viewMinZoom) {
this.setMinZoom(viewMinZoom);
}
maxZoom = this.getMaxZoom();
minZoom = this.getMinZoom();
if (maxZoom < minZoom) {
this.setMaxZoom(minZoom);
}
if (isNil(this._zoomLevel) || this._zoomLevel > maxZoom) {
this._zoomLevel = maxZoom;
}
if (this._zoomLevel < minZoom) {
this._zoomLevel = minZoom;
}
delete this._prjCenter;
delete this._glRes;
const projection = this.getProjection();
this._prjCenter = projection.project(this._center);
this._prjCenter.z = this._center.z;
this._calcMatrices();
const renderer = this._getRenderer();
if (renderer) {
renderer.resetContainer();
}
}
setContainerDomRect(domRect: DOMRect) {
this._containerDomContentRect = domRect;
}
//@internal
_getContainerDomSize(): Size | null {
if (!this._containerDOM) {
return null;
}
const containerDOM = this._containerDOM;
let width, height;
if (this._containerDomContentRect) {
width = this._containerDomContentRect.width;
height = this._containerDomContentRect.height;
return new Size(width, height);
}
//is Canvas
const canvasDom = containerDOM as HTMLCanvasElement;
if (!isNil(canvasDom.width) && !isNil(canvasDom.height)) {
width = canvasDom.width;
height = canvasDom.height;
const dpr = this.getDevicePixelRatio();
if (dpr !== 1 && containerDOM['layer']) {
//is a canvas tile of CanvasTileLayer
width /= dpr;
height /= dpr;
}
} else if (!isNil(containerDOM.clientWidth) && !isNil(containerDOM.clientHeight)) {
width = parseInt(containerDOM.clientWidth + '', 0);
height = parseInt(containerDOM.clientHeight + '', 0);
} else {
throw new Error('can not get size of container');
}
return new Size(width, height);
}
//@internal
_updateMapSize(mSize: Size) {
this.width = mSize['width'];
this.height = mSize['height'];
const renderer = this._getRenderer();
if (renderer) {
renderer.updateMapSize(mSize);
}
this._calcMatrices();
return this;
}
/**
* Gets projected center of the map
* @return {Coordinate}
* @private
*/
//@internal
_getPrjCenter() {
return this._prjCenter;
}
//@internal
_setPrjCenter(pcenter: Coordinate) {
if (pcenter && this._prjCenter) {
//Respect the current altitude
// https://github.com/maptalks/maptalks.js/issues/2724
// https://github.com/maptalks/issues/issues/913
if (!isNumber(pcenter.z) && isNumber(this._prjCenter.z)) {
const altitude = this._prjCenter.z;
pcenter.z = altitude;
}
}
this._prjCenter = pcenter;
if (this.isInteracting() && !this.isMoving()) {
// when map is not moving, map's center is updated but map platform won't
// mapViewCoord needs to be synced
this._mapViewCoord = pcenter;
}
this._calcMatrices();
}
//@internal
_setPrjCoordAtContainerPoint(coordinate: Coordinate, point: Point) {
if (!this.centerAltitude && point.x === this.width / 2 && point.y === this.height / 2) {
return this;
}
const p = this._containerPointToPoint(point);
const t = p._sub(this._prjToPoint(this._getPrjCenter()));
const pcenter = this._pointToPrj(this._prjToPoint(coordinate)._sub(t));
this._setPrjCenter(pcenter);
return this;
}
//@internal
_setPrjCoordAtOffsetToCenter(prjCoord: Coordinate, offset: Point) {
const pcenter = this._pointToPrj(this._prjToPoint(prjCoord)._sub(offset));
this._setPrjCenter(pcenter);
return this;
}
//@internal
_verifyExtent(prjCenter: Coordinate) {
if (!prjCenter) {
return false;
}
const maxExt = this._prjMaxExtent;
if (!maxExt) {
return true;
}
return maxExt.contains(prjCenter);
}
//@internal
_limitMaxExtent() {
if (this._limitMaxExtenting || !this.options.limitExtentOnMaxExtent) {
return this;
}
const maxPrjExtent = this._prjMaxExtent;
const maxExtent = this.getMaxExtent();
if (!maxPrjExtent || !maxExtent) {
return this;
}
const prjCoords = maxPrjExtent.toArray();
const points = prjCoords.map(prjCoord => {
return this.prjToContainerPoint(prjCoord);
})
//屏幕坐标包围盒
const maxExtentBBOX = getDefaultBBOX();
pointsBBOX(points, maxExtentBBOX);
const { width, height } = this.getSize();
const mapBBOX = [0, 0, width, height] as BBOX;
if (bboxInBBOX(mapBBOX, maxExtentBBOX)) {
return this;
}
//maxExtent完全在当前视野内
if (bboxInBBOX(maxExtentBBOX, mapBBOX)) {
return this;
}
let translateX = 0, translateY = 0;
let offsetleft = 0, offsetright = 0, offsettop = 0, offsetbottom = 0;
const abs = Math.abs;
const [left, top, right, bottom] = maxExtentBBOX;
//left overflow
if (left > 0 && right > width) {
translateX = offsetleft = abs(left);
}
if (left < 0 && right < width) {
translateX = offsetleft = -abs(left);
}
//right overflow
if (left < 0 && right < width) {
translateX = offsetright = - abs(width - right);
}
if (left > 0 && right > width) {
translateX = offsetright = abs(width - right);
}
//top overflow
if (top > 0 && bottom > height) {
translateY = offsettop = abs(top);
}
if (top < 0 && bottom < height) {
translateY = offsettop = -abs(top);
}
//bottom overflow
if (top < 0 && bottom < height) {
translateY = offsetbottom = -abs(height - bottom);
}
if (top > 0 && bottom > height) {
translateY = offsetbottom = abs(height - bottom);
}
//同时溢出取最小的值,四周最近距离吸附
if (offsetleft !== 0 && offsetright !== 0) {
translateX = offsetleft;
if (abs(offsetright) < abs(offsetleft)) {
translateX = offsetright;
}
}
if (offsettop !== 0 && offsetbottom !== 0) {
translateY = offsettop;
if (abs(offsetbottom) < abs(offsettop)) {
translateY = offsetbottom;
}
}
if (translateX !== 0 || translateY !== 0) {
const point = new Point(width / 2 + translateX, height / 2 + translateY);
const center = this.containerPointToCoord(point);
this._limitMaxExtenting = true;
this.setCenter(center);
this._limitMaxExtenting = false;
}
return this;
}
/**
* Move map's center by pixels.
* @param {Point} pixel - pixels to move, the relation between value and direction is as:
* -1,1 | 1,1
* ------------
*-1,-1 | 1,-1
* @
* @returns {Coordinate} the new projected center.
*/
//@internal
_offsetCenterByPixel(pixel: Point) {
const pos = TEMP_POINT.set(this.width / 2 - pixel.x, this.height / 2 - pixel.y);
const coord = this._containerPointToPrj(pos, TEMP_COORD);
const containerCenter = TEMP_POINT.set(this.width / 2, this.height / 2);
this._setPrjCoordAtContainerPoint(coord, containerCenter);
}
/**
* offset map panels.
*
* @param {Point} offset - offset in pixel to move
* @return {Map} this
*/
/**
* Gets map panel's current view point.
* @return {Point}
*/
offsetPlatform(offset?: Point): Point {
if (!offset) {
return this._mapViewPoint;
} else {
this._getRenderer().offsetPlatform(offset);
this._mapViewCoord = this._getPrjCenter();
this._mapViewPoint = this._mapViewPoint.add(offset) as Point;
return this._mapViewPoint;
}
}
/**
* Get map's view point, adding in frame offset
* @return {Point} map view point
*/
getViewPoint(): Point {
const offset = this.getViewPointFrameOffset();
let panelOffset = this.offsetPlatform();
if (offset) {
panelOffset = (panelOffset as any).add(offset);
}
return panelOffset;
}
//@internal
_resetMapViewPoint() {
this._mapViewPoint = new Point(0, 0);
// mapViewCoord is the proj coordinate of current view point
this._mapViewCoord = this._getPrjCenter();
}
/**
* Get map's current resolution
* @return {Number} resolution
* @private
*/
//@internal
_getResolution(zoom?: number) {
if ((zoom === undefined || zoom === this._zoomLevel) && this._mapRes !== undefined) {
return this._mapRes;
}
if (isNil(zoom)) {
zoom = this._zoomLevel;
}
return this._spatialReference.getResolution(zoom);
}
//@internal
_getResolutions() {
return this._spatialReference.getResolutions();
}
/**
* Converts the projected coordinate to a 2D point in the specific zoom
* @param {Coordinate} pCoord - projected Coordinate
* @param {Number} zoom - point's zoom level
* @return {Point} 2D point
* @private
*/
//@internal
_prjToPoint(pCoord, zoom?: number, out?: Point) {
zoom = (isNil(zoom) ? this.getZoom() : zoom);
const res = this._getResolution(zoom);
return this._prjToPointAtRes(pCoord, res, out);
}
//@internal
_prjToPointAtRes(pCoord: Coordinate, res?: number, out?: Point): Point {
return this._spatialReference.getTransformation().transform(pCoord, res, out);
}
/**
* Converts the projected coordinate to a 2D point in the specific resolution
* @param {Coordinate} pCoord - projected Coordinate
* @param {Number} res - point's resolution
* @return {Point} 2D point
* @private
* @internal
*/
_prjsToPointsAtRes(pCoords: Array, res?: number, resultPoints = []): Array {
const transformation = this._spatialReference.getTransformation();
const pts = [];
for (let i = 0, len = pCoords.length; i < len; i++) {
const pt = transformation.transform(pCoords[i], res, resultPoints[i]);
pts.push(pt);
}
return pts;
}
/**
* Converts the 2D point to projected coordinate
* @param {Point} point - 2D point
* @param {Number} zoom - point's zoom level
* @return {Coordinate} projected coordinate
* @private
*/
//@internal
_pointToPrj(point: Point, zoom?: number, out?: Coordinate): Coordinate {
zoom = (isNil(zoom) ? this.getZoom() : zoom);
const res = this._getResolution(zoom);
return this._pointToPrjAtRes(point, res, out);
}
//@internal
_pointToPrjAtRes(point: Point, res?: number, out?: Coordinate): Coordinate {
return this._spatialReference.getTransformation().untransform(point, res, out);
}
/**
* Convert point at zoom to point at current zoom
* @param {Point} point point
* @param {Number} zoom point's zoom
* @return {Point} point at current zoom
* @private
*/
//@internal
_pointToPoint(point: Point, zoom?: number, out?: Point): Point {
if (!isNil(zoom)) {
return this._pointAtResToPoint(point, this._getResolution(zoom), out);
}
if (out) {
out.x = point.x;
out.y = point.y;
} else {
out = point.copy() as Point;
}
return out;
}
//@internal
_pointAtResToPoint(point: Point, res?: number, out?: Point): Point {
if (out) {
out.x = point.x;
out.y = point.y;
} else {
out = point.copy() as Point;
}
return out._multi(res / this._getResolution());
}
/**
* Convert point at current zoom to point at target res
* @param {Point} point point
* @param {Number} res target res
* @return {Point} point at target res
* @private
*/
//@internal
_pointToPointAtRes(point: Point, res?: number, out?: Point): Point {
if (out) {
out.x = point.x;
out.y = point.y;
} else {
out = point.copy() as Point;
}
return out._multi(this._getResolution() / res);
}
/**
* transform container point to geographical projected coordinate
*
* @param {Point} containerPoint
* @return {Coordinate}
* @private
*/
//@internal
_containerPointToPrj(containerPoint: Point, out?: Coordinate) {
return this._pointToPrj(this._containerPointToPoint(containerPoint, undefined, out as Point), undefined, out);
}
/* eslint no-extend-native: 0 */
//@internal
_callOnLoadHooks() {
const proto = Map.prototype;
if (!proto._onLoadHooks) {
return;
}
for (let i = 0, l = proto._onLoadHooks.length; i < l; i++) {
proto._onLoadHooks[i].call(this);
}
}
//fix prj value when current view is world wide
//@internal
_fixPrjOnWorldWide(prjCoord: Coordinate) {
const projection = this.getProjection() as any;
if (projection && projection.fullExtent && prjCoord) {
const { left, bottom, top, right } = projection.fullExtent || {};
if (isNumber(left)) {
prjCoord.x = Math.max(left, prjCoord.x);
}
if (isNumber(right)) {
prjCoord.x = Math.min(right, prjCoord.x);
}
if (isNumber(bottom)) {
prjCoord.y = Math.max(bottom, prjCoord.y);
}
if (isNumber(top)) {
prjCoord.y = Math.min(top, prjCoord.y);
}
}
return this;
}
/**
* Export the map's json, a snapshot of the map in JSON format.
* It can be used to reproduce the instance by [fromJSON]{@link Map#fromJSON} method
* @param {Object} [options=null] - export options
* @param {Boolean|Object} [options.baseLayer=null] - whether to export base layer's JSON, if yes, it will be used as layer's toJSON options.
* @param {Boolean|Extent} [options.clipExtent=null] - if set with an extent instance, only the geometries intersectes with the extent will be exported.
* If set to true, map's current extent will be used.
* @param {Boolean|Object|Object[]} [options.layers=null] - whether to export other layers' JSON, if yes, it will be used as layer's toJSON options.
* It can also be an array of layer export options with a "id" attribute to filter the layers to export.
* @return {Object} layer's JSON
*/
toJSON(options?: MapOptionsType): { [key: string]: any } {
if (!options) {
options = {};
}
const json = {
'jsonVersion': this['JSON_VERSION'],
'version': this.VERSION,
'extent': this.getExtent().toJSON()
};
json['options'] = this.config();
json['options']['center'] = this.getCenter();
json['options']['zoom'] = this.getZoom();
json['options']['bearing'] = this.getBearing();
json['options']['pitch'] = this.getPitch();
const baseLayer = this.getBaseLayer();
if ((isNil(options['baseLayer']) || options['baseLayer']) && baseLayer) {
json['baseLayer'] = baseLayer.toJSON(options['baseLayer']);
}
const extraLayerOptions = {};
if (options['clipExtent']) {
//if clipExtent is set, only geometries intersecting with extent will be exported.
//clipExtent's value can be an extent or true (map's current extent)
if (options['clipExtent'] === true) {
extraLayerOptions['clipExtent'] = this.getExtent();
} else {
extraLayerOptions['clipExtent'] = options['clipExtent'];
}
}
const layersJSON = [];
if (isNil(options['layers']) || (options['layers'] && !Array.isArray(options['layers']))) {
const layers = this.getLayers();
for (let i = 0, len = layers.length; i < len; i++) {
if (!layers[i].toJSON) {
continue;
}
const opts = extend({}, isObject(options['layers']) ? options['layers'] : {}, extraLayerOptions);
layersJSON.push(layers[i].toJSON(opts));
}
json['layers'] = layersJSON;
} else if (isArrayHasData(options['layers'])) {
const layers = options['layers'];
for (let i = 0; i < layers.length; i++) {
const exportOption = layers[i];
const layer = this.getLayer(exportOption['id']);
if (!layer.toJSON) {
continue;
}
const opts = extend({}, exportOption['options'], extraLayerOptions);
layersJSON.push(layer.toJSON(opts));
}
json['layers'] = layersJSON;
} else {
json['layers'] = [];
}
return json;
}
/**
* Reproduce a map from map's profile JSON.
* @param {(string|HTMLElement|object)} container - The container to create the map on, can be:
* 1. A HTMLElement container.
* 2. ID of a HTMLElement container.
* 3. A canvas compatible container in node,
* e.g. [node-canvas]{@link https://github.com/Automattic/node-canvas},
* [canvas2svg]{@link https://github.com/gliffy/canvas2svg}
* @param {Object} mapJSON - map's profile JSON
* @param {Object} [options=null] - options
* @param {Object} [options.baseLayer=null] - whether to import the baseLayer
* @param {Object} [options.layers=null] - whether to import the layers
* @return {Map}
* @static
* @function
* @example
* var map = Map.fromJSON('map', mapProfile);
*/
static fromJSON(container: MapContainerType, profile: { [key: string]: any }, options?: MapOptionsType) {
if (!container || !profile) {
return null;
}
if (!options) {
options = {};
}
const map = new Map(container, profile['options']);
if (isNil(options['baseLayer']) || options['baseLayer']) {
const baseLayer = Layer.fromJSON(profile['baseLayer']);
if (baseLayer) {
map.setBaseLayer(baseLayer);
}
}
if (isNil(options['layers']) || options['layers']) {
const layers = [];
const layerJSONs = profile['layers'];
for (let i = 0; i < layerJSONs.length; i++) {
const layer = Layer.fromJSON(layerJSONs[i]);
layers.push(layer);
}
map.addLayer(layers);
}
return map;
}
}
Map.mergeOptions(options);
export default Map;
export type MapRendererType = 'canvas' | 'gl' | 'gpu';
export type MapOptionsType = {
// center: Array | Coordinate;
// zoom: number;
pitch?: number;
bearing?: number;
baseLayer?: Layer;
layers?: Array;
draggable?: boolean;
dragPan?: boolean;
dragPanEasing?: EasingType;
dragRotate?: boolean;
dragPitch?: boolean;
dragRotatePitch?: boolean;
touchGesture?: boolean;
touchZoom?: boolean;
touchRotate?: boolean;
touchPitch?: boolean;
touchZoomRotate?: boolean;
doubleClickZoom?: boolean;
scrollWheelZoom?: boolean;
geometryEvents?: boolean;
control?: boolean;
attribution?: boolean | AttributionOptionsType;
zoomControl?: boolean;
scaleControl?: boolean;
overviewControl?: boolean;
fog?: boolean;
fogColor?: any; // fixme 确认类型
devicePixelRatio?: number;
heightFactor?: number;
originLatitudeForAltitude?: number;
viewHistory?: boolean;
viewHistoryCount?: number;
seamlessZoom?: boolean;
maxVisualPitch?: number;
maxPitch?: number;
centerCross?: boolean;
zoomable?: boolean;
zoomInCenter?: boolean;
zoomOrigin?: Array;
zoomAnimation?: boolean;
zoomAnimationDuration?: number;
tileBackgroundLimitPerFrame?: number;
panAnimation?: boolean;
panAnimationDuration?: number;
rotateAnimation?: boolean;
rotateAnimationDuration?: number;
enableInfoWindow?: boolean;
hitDetect?: boolean;
hitDetectLimit?: number;
fpsOnInteracting?: number;
layerCanvasLimitOnInteracting?: number;
maxZoom?: number;
minZoom?: number;
maxExtent?: Extent;
limitExtentOnMaxExtent?: boolean;
fixCenterOnResize?: boolean;
checkSize?: boolean;
checkSizeInterval?: number;
renderer?: MapRendererType | MapRendererType[];
cascadePitches?: Array;
renderable?: boolean;
clickTimeThreshold?: number;
stopRenderOnOffscreen?: boolean;
preventWheelScroll?: boolean;
preventTouch?: boolean;
supportPluginEvent?: boolean;
switchDragButton?: boolean;
mousemoveThrottleTime?: number;
mousemoveThrottleEnable?: boolean;
maxFPS?: number;
debug?: boolean;
spatialReference?: SpatialReferenceType,
autoPanAtEdge?: boolean;
boxZoom?: boolean;
boxZoomSymbol?: {
'markerType': string;
'markerLineWidth': number;
'markerLineColor': string;
'markerLineDasharray': Array;
'markerFillOpacity': number;
'markerFill': string;
'markerWidth': number;
'markerHeight': number;
};
onlyVisibleGeometryEvents?: boolean;
compassControl?: boolean;
layerSwitcherControl?: boolean;
navControl?: boolean;
resetControl?: boolean;
cameraFarUndergroundInMeter?: number;
onlyWebGL1?: boolean;
preserveDrawingBuffer?: boolean;
forceRedrawPerFrame?: boolean;
extensions?: string[];
optionalExtensions?: string[];
cameraNearScale?: number;
}
export type MapCreateOptionsType = {
center: Array | Coordinate;
zoom: number
} & MapOptionsType;
export type MapPaddingType = {
paddingLeft: number;
paddingRight: number;
paddingTop: number;
paddingBottom: number;
}
export type MapViewType = {
center?: Array | Coordinate,
zoom?: number;
pitch?: number;
bearing?: number;
height?: number;
around?: Point;
prjCenter?: Array | Coordinate,
}
export type MapFitType = {
isFraction?: boolean;
animation?: boolean;
duration?: number;
easing?: EasingType
} & MapPaddingType;
export type MapDataURLType = {
mimeType?: string;
fileName?: string;
quality?: number;
save?: boolean;
}
export type MapAnimationOptionsType = AnimationOptionsType & { counterclockwise?: boolean, continueOnViewChanged?: boolean, wheelZoom?: boolean }
export type MapIdentifyOptionsType = {
tolerance?: number;
eventTypes?: Array;
layers?: Array;
count?: number;
includeInvisible?: boolean;
includeInternals?: boolean;
}
export type MapContainerType = string | HTMLDivElement | HTMLCanvasElement | { [key: string]: any };
export type PanelDom = (HTMLDivElement | HTMLElement) & { layerDOM: HTMLElement; uiDOM: HTMLElement; }
function isTerrainLayer(layer: any): boolean {
return layer && layer.queryTerrainAtPoint && layer.getTerrainLayer && layer.getTerrainLayer();
}
export function mapViewEqual(view1: MapViewType, view2: MapViewType, strict?: boolean): boolean {
if (view1 === view2) {
return true;
}
if (!view1 || !view2) {
return false;
}
try {
const str1 = JSON.stringify(view1);
const str2 = JSON.stringify(view2);
if (str1 === str2) {
return true;
}
} catch (e) {
console.error('stringify map view error', e);
return false;
}
if (strict) {
return false;
}
// Precision after decimal point
const pow = Math.pow(10, 7);
const roundValue = (v: number) => {
v = v || 0;
return Math.round(v * pow);
}
let zoom1 = view1.zoom, zoom2 = view2.zoom;
let bearing1 = view1.bearing, bearing2 = view2.bearing;
let pitch1 = view1.pitch, pitch2 = view2.pitch;
let height1 = view1.height || 0, height2 = view2.height || 0;
zoom1 = roundValue(zoom1);
zoom2 = roundValue(zoom2);
bearing1 = roundValue(bearing1);
bearing2 = roundValue(bearing2);
pitch1 = roundValue(pitch1);
pitch2 = roundValue(pitch2);
height1 = roundValue(height1);
height2 = roundValue(height2);
if (zoom1 !== zoom2) {
return false;
}
if (bearing1 !== bearing2) {
return false;
}
if (pitch1 !== pitch2) {
return false;
}
if (height1 !== height2) {
return false;
}
const centerEqual = (p1, p2) => {
if (p1 === p2) {
return true;
}
if (!p1 || !p2) {
return false;
}
const c1 = new Coordinate(p1 as Coordinate);
const c2 = new Coordinate(p2 as Coordinate);
if (c1.equals(c2)) {
return true;
}
let x1 = c1.x, y1 = c1.y, z1 = c1.z || 0;
let x2 = c2.x, y2 = c2.y, z2 = c2.z || 0;
x1 = roundValue(x1);
y1 = roundValue(y1);
z1 = roundValue(z1);
x2 = roundValue(x2);
y2 = roundValue(y2);
z2 = roundValue(z2);
return (x1 === x2 && y1 === y2 && z1 === z2)
}
if (!centerEqual(view1.center, view2.center)) {
return false;
}
if (!centerEqual(view1.prjCenter, view2.prjCenter)) {
return false;
}
return true;
}