diff --git a/src/core/math/Plane.ts b/src/core/math/Plane.ts new file mode 100644 index 0000000000..6db9bb37b4 --- /dev/null +++ b/src/core/math/Plane.ts @@ -0,0 +1,16 @@ +import { Vector3 } from "../util/mat4"; +import * as vec3 from '../util/vec3'; + +export default class Plane { + normal: Vector3; + constant: number; + + constructor(normal: Vector3, constant: number) { + this.normal = normal; + this.constant = constant; + } + + distanceToPoint( point ) { + return vec3.dot(this.normal, point) + this.constant; + } +} diff --git a/src/core/math/Ray.ts b/src/core/math/Ray.ts new file mode 100644 index 0000000000..183333fb79 --- /dev/null +++ b/src/core/math/Ray.ts @@ -0,0 +1,226 @@ +import { Vector3 } from '../util/mat4'; +import * as vec3 from '../util/vec3'; +import Plane from './Plane'; + +const scaledDir: [number, number, number] = [0, 0, 0]; +const _edge1: [number, number, number] = [0, 0, 0]; +const _edge2: [number, number, number] = [0, 0, 0]; +const _normal: [number, number, number] = [0, 0, 0]; +const _diff: [number, number, number] = [0, 0, 0]; + +const Ground = new Plane([0, 0, 1], 0); + +// from Vector3.js in three.js +// https://github.com/mrdoob/three.js +// MIT license +export default class Ray { + origin: [number, number, number]; + direction: [number, number, number]; + + constructor(from: Vector3, to: Vector3) { + this.setFromTo(from, to); + } + + + + setFromTo(from: Vector3, to: Vector3) { + this.origin = from; + const direction = this.direction || [0, 0, 0] as [number, number, number]; + this.direction = vec3.normalize(direction, vec3.sub(direction, to, from)); + } + + distanceToPlane( plane: Plane ) { + + const denominator = vec3.dot(plane.normal, this.direction); + + if ( denominator === 0 ) { + + // line is coplanar, return origin + if ( plane.distanceToPoint( this.origin ) === 0 ) { + + return 0; + + } + + // Null is preferable to undefined since undefined means.... it is undefined + + return null; + + } + + const t = - (vec3.dot(this.origin, plane.normal) + plane.constant) / denominator; + + // Return if the ray never intersects the plane + + return t >= 0 ? t : null; + + } + + intersectGround(target: Vector3) { + return this.intersectPlane(Ground, target); + } + + distanceToGround() { + return this.distanceToPlane(Ground); + } + + intersectPlane( plane: Plane, target: Vector3 ) { + + const t = this.distanceToPlane( plane ); + + if ( t === null ) { + + return null; + + } + + return this.at( t, target ); + + } + + intersectBox( box, target: Vector3 ) { + + let tmin, tmax, tymin, tymax, tzmin, tzmax; + + const invdirx = 1 / this.direction[0], + invdiry = 1 / this.direction[1], + invdirz = 1 / this.direction[2]; + + const origin = this.origin; + + if ( invdirx >= 0 ) { + + tmin = ( box[0][0] - origin[0] ) * invdirx; + tmax = ( box[1][0] - origin[0] ) * invdirx; + + } else { + + tmin = ( box[1][0] - origin[0] ) * invdirx; + tmax = ( box[0][0] - origin[0] ) * invdirx; + + } + + if ( invdiry >= 0 ) { + + tymin = ( box[0][1] - origin[1] ) * invdiry; + tymax = ( box[1][1] - origin[1] ) * invdiry; + + } else { + + tymin = ( box[1][1] - origin[1] ) * invdiry; + tymax = ( box[0][1] - origin[1] ) * invdiry; + + } + + if ( ( tmin > tymax ) || ( tymin > tmax ) ) return null; + + if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin; + + if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax; + + if ( invdirz >= 0 ) { + + tzmin = ( box[0][2] - origin[2] ) * invdirz; + tzmax = ( box[1][2] - origin[2] ) * invdirz; + + } else { + + tzmin = ( box[1][2] - origin[2] ) * invdirz; + tzmax = ( box[0][2] - origin[2] ) * invdirz; + + } + + if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null; + + if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin; + + if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax; + + //return point closest to the ray (positive side) + + if ( tmax < 0 ) return null; + + return this.at( tmin >= 0 ? tmin : tmax, target ); + + } + + intersectTriangle( a, b, c, backfaceCulling, target ) { + + // Compute the offset origin, edges, and normal. + + // from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h + vec3.sub(_edge1, b, a); + vec3.sub(_edge2, c, a); + vec3.cross(_normal, _edge1, _edge2); + + // Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction, + // E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by + // |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2)) + // |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q)) + // |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N) + let DdN = vec3.dot(this.direction, _normal); + let sign; + + if ( DdN > 0 ) { + + if ( backfaceCulling ) return null; + sign = 1; + + } else if ( DdN < 0 ) { + + sign = - 1; + DdN = - DdN; + + } else { + + return null; + + } + vec3.sub(_diff, this.origin, a); + + vec3.cross(_edge2, _diff, _edge2); + const DdQxE2 = sign * vec3.dot(this.direction, _edge2); + + // b1 < 0, no intersection + if ( DdQxE2 < 0 ) { + + return null; + + } + vec3.cross(_edge1, _edge1, _diff); + const DdE1xQ = sign * vec3.dot(this.direction, _edge1); + + // b2 < 0, no intersection + if ( DdE1xQ < 0 ) { + + return null; + + } + + // b1+b2 > 1, no intersection + if ( DdQxE2 + DdE1xQ > DdN ) { + + return null; + + } + + // Line intersects triangle, check if ray does. + const QdN = - sign * vec3.dot(_diff, _normal); + + // t < 0, no intersection + if ( QdN < 0 ) { + + return null; + + } + + // Ray intersects triangle. + return this.at( QdN / DdN, target ); + + } + + at( t, target ) { + vec3.copy(target, this.origin); + return vec3.add(target, target, vec3.scale(scaledDir, this.direction, t)); + } +} diff --git a/src/core/util/dom.ts b/src/core/util/dom.ts index 9d62b9a5cc..4b3c1f1f89 100644 --- a/src/core/util/dom.ts +++ b/src/core/util/dom.ts @@ -322,7 +322,7 @@ export function computeDomPosition(dom: HTMLElement): number[] { * @param ev event * @return */ -export function getEventContainerPoint(ev: MouseEvent, dom: HTMLElement) { +export function getEventContainerPoint(ev: MouseEvent | TouchEvent, dom: HTMLElement) { if (!ev) { // @ts-expect-error ev = window.event; @@ -342,6 +342,7 @@ export function getEventContainerPoint(ev: MouseEvent, dom: HTMLElement) { if (toucheEvent.changedTouches && toucheEvent.changedTouches.length) { ev = toucheEvent.changedTouches[0] as unknown as MouseEvent; } + ev = ev as MouseEvent; // div by scaleX, scaleY to fix #450 return new Point( (ev.clientX - domPos[0] - dom.clientLeft) / domPos[2], diff --git a/src/core/util/vec3.ts b/src/core/util/vec3.ts index 035996bf76..8ae51c85b0 100644 --- a/src/core/util/vec3.ts +++ b/src/core/util/vec3.ts @@ -46,6 +46,17 @@ export function subtract(out: Vec3, a: Vec3, b: Vec3) { return out; } +export function sub(out: Vec3, a: Vec3, b: Vec3) { + return subtract(out, a, b); +} + +export function copy(out: Vec3, a: Vec3) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + return out; +} + /** * Calculates the length of a vec3 * @ignore diff --git a/src/map/Map.Camera.ts b/src/map/Map.Camera.ts index e7695adc75..e8119e6457 100644 --- a/src/map/Map.Camera.ts +++ b/src/map/Map.Camera.ts @@ -3,7 +3,7 @@ import Point from '../geo/Point'; import Coordinate from '../geo/Coordinate'; import * as mat4 from '../core/util/mat4'; import { subtract, add, scale, normalize, dot, set, distance, angle, cross } from '../core/util/vec3'; -import { clamp, interpolate, isNumber, isNil, wrap, toDegree, toRadian, Matrix4 } from '../core/util'; +import { clamp, interpolate, isNumber, isNil, wrap, toDegree, toRadian, Matrix4, Vector3 } from '../core/util'; import { applyMatrix, matrixToQuaternion, quaternionToMatrix, lookAt, setPosition } from '../core/util/math'; import Browser from '../core/Browser'; @@ -26,7 +26,7 @@ declare module "./Map" { //@internal _calcMatrices(): void; //@internal - _containerPointToPoint(p: Point, zoom?: number, out?: Point): Point; + _containerPointToPoint(p: Point, zoom?: number, out?: Point, height?: number): Point; //@internal _recenterOnTerrain(): void; setCameraMovements(frameOptions: Array, option?: { autoRotate: boolean }); @@ -49,6 +49,14 @@ declare module "./Map" { _pointToContainerPoint(point: Point, zoom?: number, out?: Point): Point; //@internal _pointsAtResToContainerPoints(point: Point[], res?: number, altitude?: number[], out?: Point[]): Point[]; + //@internal + getContainerPointRay(from: Vector3, to: Vector3, containerPoint: Point, near: number, far: number); + //@internal + _query3DTilesInfo(containerPoint: Point); + //@internal + _queryTerrainInfo(containerPoint: Point); + //@internal + queryPrjCoordAtContainerPoint(containerPoint: Point); } } @@ -598,20 +606,11 @@ Map.include(/** @lends Map.prototype */{ //@internal _containerPointToPointAtRes: function () { - const cp = [0, 0, 0], - coord0 = [0, 0, 0, 1], - coord1 = [0, 0, 0, 1]; + const coord0 = [0, 0, 0], + coord1 = [0, 0, 0]; return function (p, res, out, height) { if (this.isTransforming()) { - const w2 = this.width / 2 || 1, h2 = this.height / 2 || 1; - set(cp as any, (p.x - w2) / w2, (h2 - p.y) / h2, 0); - - set(coord0 as any, cp[0], cp[1], 0); - set(coord1 as any, cp[0], cp[1], 1); - coord0[3] = coord1[3] = 1; - - applyMatrix(coord0, coord0 as any, this.projViewMatrixInverse); - applyMatrix(coord1, coord1 as any, this.projViewMatrixInverse); + this.getContainerPointRay(coord0, coord1, p); const x0 = coord0[0]; const x1 = coord1[0]; const y0 = coord0[1]; @@ -641,6 +640,23 @@ Map.include(/** @lends Map.prototype */{ }; }(), + getContainerPointRay: function () { + const cp = [0, 0, 0], + coord0 = [0, 0, 0, 1], + coord1 = [0, 0, 0, 1]; + return function (from: Vector3, to: Vector3, containerPoint: Point, near = 0, far = 1) { + const w2 = this.width / 2 || 1, + h2 = this.height / 2 || 1; + const p = containerPoint; + set(cp as Vector3, (p.x - w2) / w2, (h2 - p.y) / h2, 0); + set(coord0 as Vector3, cp[0], cp[1], near); + set(coord1 as Vector3, cp[0], cp[1], far); + coord0[3] = coord1[3] = 1; + applyMatrix(from, coord0 as Vector3, this.projViewMatrixInverse); + applyMatrix(to, coord1 as Vector3, this.projViewMatrixInverse); + } + }(), + /** * GL Matrices in maptalks (based on THREE): * //based on point at map's gl world space, by map.coordToPointAtRes(coord, map.getGLRes())) @@ -1008,6 +1024,43 @@ Map.include(/** @lends Map.prototype */{ return null; }, + //@internal + _query3DTilesInfo(containerPoint) { + const layers = this._getLayers() || []; + for (let i = 0; i < layers.length; i++) { + const layer = layers[i]; + if (containerPoint && layer && layer.query3DTilesAtPoint) { + const coordinate = layer.query3DTilesAtPoint(containerPoint); + if (coordinate) { + return { + coordinate, + altitude: coordinate.z + }; + } else { + break; + } + } + } + return null; + }, + + //@internal + queryPrjCoordAtContainerPoint(p) { + let queryCoord = this._query3DTilesInfo(p) + if (!queryCoord) { + queryCoord = this._queryTerrainInfo(p); + } + if (queryCoord) { + const prjCoord = this.getProjection().project(queryCoord.coordinate); + prjCoord.z = queryCoord.altitude; + return prjCoord; + } + if (this._isContainerPointOutOfMap(p)) { + p = new Point(this.width / 2, this.height / 2); + } + return this._containerPointToPrj(p); + }, + //@internal _getFovRatio() { const fov = this.getFov(); diff --git a/src/map/Map.DomEvents.ts b/src/map/Map.DomEvents.ts index 52433a80d0..e2b1480f95 100644 --- a/src/map/Map.DomEvents.ts +++ b/src/map/Map.DomEvents.ts @@ -1,5 +1,5 @@ import GlobalConfig from '../GlobalConfig'; -import { now, extend } from '../core/util'; +import { now, extend, Vector3 } from '../core/util'; import { addDomEvent, removeDomEvent, @@ -10,6 +10,8 @@ import { } from '../core/util/dom'; import Map from './Map'; import { Coordinate, Point } from '../geo'; +import Ray from '../core/math/Ray'; +import * as vec3 from '../core/util/vec3'; declare module "./Map" { interface Map { @@ -27,10 +29,15 @@ declare module "./Map" { _fireDOMEvent(target: any, e: MapEventDomType, type: string); //@internal _getEventParams(e: MapEventDomType): MapEventDataType; - + //@internal + _isContainerPointOutOfMap(containerPoint: Point): boolean; + //@internal + _getActualEvent(e: MapEventDomType): MapEventDomType; } } +const PITCH_TO_CHECK = [60, 120]; + export type MapEventDomType = MouseEvent | TouchEvent | DragEvent; export type MapEventDataType = { coordinate?: Coordinate; @@ -280,7 +287,7 @@ Map.include(/** @lends Map.prototype */ { } else { this._fireDOMEvent(this, e, 'dom:' + e.type); } - if (this._ignoreEvent(e) || this._isEventOutMap(e)) { + if (this._ignoreEvent(e)) { return; } let mimicClick = false; @@ -322,7 +329,7 @@ Map.include(/** @lends Map.prototype */ { this._fireDOMEvent(this, e, 'dom:click'); } } - if (this._ignoreEvent(e) || this._isEventOutMap(e)) { + if (this._ignoreEvent(e)) { return; } this._fireDOMEvent(this, e, type); @@ -358,15 +365,44 @@ Map.include(/** @lends Map.prototype */ { //@internal _isEventOutMap(domEvent: MapEventDomType) { - if (this.getPitch() > this.options['maxVisualPitch']) { - const actualEvent = this._getActualEvent(domEvent); - const eventPos = getEventContainerPoint(actualEvent, this._containerDOM); - if (!this.getContainerExtent().contains(eventPos)) { - return true; + const actualEvent = this._getActualEvent(domEvent); + const cp = getEventContainerPoint(actualEvent, this._containerDOM); + return this._isContainerPointOutOfMap(cp); + }, + + _isContainerPointOutOfMap: function () { + const from: Vector3 = [0, 0, 0]; + const to: Vector3 = [0, 0, 0]; + const ray = new Ray(from, to); + const target: Vector3 = [0, 0, 0]; + const fromGround : Vector3 = [0, 0, 0]; + const rayOnGround: Vector3 = [0, 0, 0]; + + return function (containerPoint: Point) { + const pitch = this.getPitch(); + if (pitch > PITCH_TO_CHECK[0] && pitch < PITCH_TO_CHECK[1]) { + this.getContainerPointRay(from, to, containerPoint, 0, 0.5); + ray.setFromTo(from, to); + const intersection = ray.intersectGround(target); + if (intersection === null) { + return true; + } + const t = ray.distanceToGround(); + if (t <= 0) { + return true; + } + const dir = ray.direction; + vec3.set(fromGround, from[0], from[1], 0); + vec3.sub(rayOnGround, fromGround, rayOnGround); + vec3.normalize(rayOnGround, rayOnGround); + const dot = vec3.dot(dir, rayOnGround); + const angle = Math.abs(Math.acos(dot)); + // 如果鼠标射线与地面的角度小于设定度数,则认为 + return angle <= 30 * Math.PI / 180; } + return false; } - return false; - }, + }(), //@internal _wrapTerrainData(eventParam: MapEventDataType) { @@ -391,9 +427,8 @@ Map.include(/** @lends Map.prototype */ { 'containerPoint': containerPoint, 'viewPoint': this.containerPointToViewPoint(containerPoint) }); - const maxVisualPitch = this.options['maxVisualPitch']; // ignore coorindate out of visual extent - if (this.getPitch() <= maxVisualPitch || containerPoint.y >= (this.height - this._getVisualHeight(maxVisualPitch))) { + if (!this._isContainerPointOutOfMap(containerPoint)) { eventParam = extend(eventParam, { 'coordinate': this.containerPointToCoord(containerPoint), 'point2d': this._containerPointToPoint(containerPoint) diff --git a/src/map/Map.Zoom.ts b/src/map/Map.Zoom.ts index f35e4c5799..22e3cffd49 100644 --- a/src/map/Map.Zoom.ts +++ b/src/map/Map.Zoom.ts @@ -52,7 +52,7 @@ Map.include(/** @lends Map.prototype */{ //@internal _checkZoomOrigin(origin?: Point) { - if (!origin || this.options['zoomInCenter']) { + if (!origin || this.options['zoomInCenter'] || this._isContainerPointOutOfMap(origin)) { origin = new Point(this.width / 2, this.height / 2); } if (this.options['zoomOrigin']) { @@ -86,7 +86,7 @@ Map.include(/** @lends Map.prototype */{ delete this.cameraZenithDistance; this._zooming = true; this._startZoomVal = this.getZoom(); - this._startZoomCoord = origin && this._containerPointToPrj(origin); + this._startZoomCoord = origin && this.queryPrjCoordAtContainerPoint(origin); /** * zoomstart event * @event Map#zoomstart @@ -174,8 +174,12 @@ Map.include(/** @lends Map.prototype */{ this._zoomLevel = nextZoom; this._calcMatrices(); if (origin && this._startZoomCoord) { - const p = this._containerPointToPoint(origin); - const offset = p._sub(this._prjToPoint(this._getPrjCenter())); + // const p = this._containerPointToPoint(origin); + // const offset = p._sub(this._prjToPoint(this._getPrjCenter())); + // this._setPrjCoordAtOffsetToCenter(this._startZoomCoord, offset); + const movingPoint = this._containerPointToPoint(origin, undefined, undefined, this._startZoomCoord.z); + const point = this._prjToPoint(this._pointToPrj(movingPoint)); + const offset = point._sub(this._prjToPoint(this._getPrjCenter())); this._setPrjCoordAtOffsetToCenter(this._startZoomCoord, offset); } }, diff --git a/src/map/handler/Map.Drag.ts b/src/map/handler/Map.Drag.ts index 11353c506c..c74fd97483 100644 --- a/src/map/handler/Map.Drag.ts +++ b/src/map/handler/Map.Drag.ts @@ -4,6 +4,7 @@ import Handler from '../../handler/Handler'; import DragHandler from '../../handler/Drag'; import Map from '../Map'; import { type Param } from './CommonType'; +import Point from '../../geo/Point'; class MapDragHandler extends Handler { @@ -26,6 +27,8 @@ class MapDragHandler extends Handler { _rotateMode: 'rotate_pitch' | 'rotate' | 'pitch' //@internal _db: number + //@internal + private startContainerPoint: Point // TODO:等待补充Map类型定义 // target: Map @@ -71,7 +74,7 @@ class MapDragHandler extends Handler { if (param.domEvent) { param = param.domEvent; } - return this.target._ignoreEvent(param) || this.target._isEventOutMap(param); + return this.target._ignoreEvent(param); } //@internal @@ -105,10 +108,6 @@ class MapDragHandler extends Handler { //@internal _onDragging(param) { - const map = this.target; - if (map._isEventOutMap(param['domEvent'])) { - return; - } if (this._mode === 'move') { this._moving(param); } else if (this._mode === 'rotatePitch') { @@ -138,11 +137,17 @@ class MapDragHandler extends Handler { //@internal _moveStart(param) { + delete this.startContainerPoint; this._start(param); - const map = this.target; + const map = this.target as Map; map.onMoveStart(param); const p = getEventContainerPoint(map._getActualEvent(param.domEvent), map.getContainer()); - this.startPrjCoord = this._containerPointToPrj(p); + this.startPrjCoord = map.queryPrjCoordAtContainerPoint(p); + // 如果 startPrjCoord.z 不为 undefined,说明是3dtiles或terrain上的查询结果 + if (map._isContainerPointOutOfMap(p) && this.startPrjCoord.z === undefined) { + // containerPoint的射线不与地图相交,则以中心点作为拖拽基准 + this.startContainerPoint = p; + } } //@internal @@ -150,34 +155,26 @@ class MapDragHandler extends Handler { if (!this.startDragTime) { return; } - const map = this.target; + const map = this.target as Map; const p = getEventContainerPoint(map._getActualEvent(param.domEvent), map.getContainer()); + if (this.startContainerPoint) { + const offset = p._sub(this.startContainerPoint); + p.set(map.width / 2 + offset.x, map.height / 2 + offset.y); + } + // 如果point的位置比相机高,地图的移动方向会相反 const movingPoint = map._containerPointToPoint(p, undefined, undefined, this.startPrjCoord.z); - const point = map._prjToPoint(map._pointToPrj(movingPoint)); - const offset = point._sub(map._prjToPoint(map._getPrjCenter())); + const offset = movingPoint._sub(map._prjToPoint(map._getPrjCenter())); map._setPrjCoordAtOffsetToCenter(this.startPrjCoord, offset); map.onMoving(param); } - //@internal - _containerPointToPrj(p) { - const map = this.target; - const queryCoord = map._queryTerrainInfo(p); - if (queryCoord) { - const prjCoord = map.getProjection().project(queryCoord.coordinate); - prjCoord.z = queryCoord.altitude; - return prjCoord; - } - return map._containerPointToPrj(p); - } - //@internal _moveEnd(param: Param) { if (!this.startDragTime) { return; } const isTouch = param.domEvent.type === 'touchend'; - const map = this.target; + const map = this.target as Map; let t = now() - this.startDragTime; const mx = param['mousePos'].x, my = param['mousePos'].y; diff --git a/src/map/handler/Map.ScrollWheelZoom.ts b/src/map/handler/Map.ScrollWheelZoom.ts index 360af77722..c1cc1bd723 100644 --- a/src/map/handler/Map.ScrollWheelZoom.ts +++ b/src/map/handler/Map.ScrollWheelZoom.ts @@ -73,7 +73,7 @@ class MapScrollWheelZoomHandler extends Handler { stopPropagation(evt); } - if (map._ignoreEvent(evt) || map._isEventOutMap(evt) || !map.options['zoomable']) { + if (map._ignoreEvent(evt) || !map.options['zoomable']) { return false; } const container = map.getContainer(); diff --git a/test/map/MapEventSpec.js b/test/map/MapEventSpec.js index 77ec7c1dad..c6c56c0a05 100644 --- a/test/map/MapEventSpec.js +++ b/test/map/MapEventSpec.js @@ -137,8 +137,8 @@ describe('Map.Event', function () { }); expect(spy.called).not.to.be.ok(); }); - - it('ignore events out of container extent', function () { + // #2419 增加了对地图范围外的鼠标交互支持,该用例不再需要 + it.skip('ignore events out of container extent', function () { var domPosition = GET_PAGE_POSITION(container); var x = domPosition.x + 2; var y = domPosition.y + 2; diff --git a/test/map/MapScrollZoomSpec.js b/test/map/MapScrollZoomSpec.js index 498c76ec34..f9545f4aa2 100644 --- a/test/map/MapScrollZoomSpec.js +++ b/test/map/MapScrollZoomSpec.js @@ -68,8 +68,8 @@ describe('Map.ScrollZoom', function () { } map.on('zoomend', function () { const center = map.getCenter(); - expect(center.x.toFixed(4)).to.eql(118.8474); - expect(center.y.toFixed(4)).to.eql(32.0460); + expect(center.x.toFixed(4)).to.eql(118.8464); + expect(center.y.toFixed(4)).to.eql(31.8479); done(); }); scrollMap(100, 10, 10);