UNPKG

@orca-fe/x-map

Version:
494 lines (493 loc) 20.2 kB
import { EventManager } from 'mjolnir.js'; import ResizeObserver from 'resize-observer-polyfill'; import { alignNumber, clamp, debounce, isSame, toFixedNumber } from '../utils/private'; import { getMapViewport } from '../utils/WebViewport'; import MapControllerEvent from './MapControllerEvent'; import { mixObj } from '../utils/transition'; const defaultBounds = [ [-180, -85], [180, 85], ]; const defaultViewport = { lng: 121, lat: 31, zoom: 8, pitch: 0, rotate: 0, }; export default class MapController extends MapControllerEvent { constructor(dom, options = {}) { super(); this.timer = -1; this.domSize = [100, 100]; /** 标记是否右键点击可用,若不可用则需要用到contextmenu事件 */ this.canRightButtonDown = false; this.rightButtonDown = false; this.startPos = null; this.startRotation = null; this.startPitch = null; /** 记录触摸按下的数量 */ this.pointerdown = 0; this.moving = false; this.lastPos = { x: Infinity, y: Infinity }; this.handlePointerMoveOrigin = (e) => { const x = e.clientX; const y = e.clientY; if (this.startPos == null) { return; } if (this.lastPos.x === x && this.lastPos.y === y) { return; } this.lastPos = { x, y }; if (!this.moving) { this.emit('move-start'); this.moving = true; } const [lng, lat] = this.startPos; const vpp = this.getViewportWithProjection(); const [centerLng, centerLat] = vpp.getMapCenterByLngLatPosition({ lngLat: [lng, lat], pos: [x, y], }); this.triggerViewportChange({ lng: Number(centerLng.toFixed(6)), lat: Number(centerLat.toFixed(6)), }); }; this.wheelDelta = 0; this.zoomStart = null; this.wheelTimer = 0; this.pinchstartCoor = null; this.pinchOrRotate = null; this.triggerViewportChangePause = debounce((viewport) => { this.emit('viewport-change-pause', viewport); }, 300); this.animTriggerTimer = -1; const eventManager = new EventManager(dom); // dom.addEventListener('mousemove', this.handlePointerMoveOrigin); eventManager.on({ pointerdown: this.handlePointerDown.bind(this), pointermove: this.handlePointerMove.bind(this), pointerup: this.handlePointerUp.bind(this), wheel: this.handleWheel.bind(this), contextmenu: this.handleContextmenu.bind(this), pinchstart: this.handlePinchStart.bind(this), pinchmove: this.handlePinchMove.bind(this), pinchend: this.handlePinchEnd.bind(this), rotatestart: this.handleRotateStart.bind(this), rotatemove: this.handleRotateMove.bind(this), rotateend: this.handleRotateEnd.bind(this), }); const ro = new ResizeObserver((entries, observer) => { this.computeSize(); this.triggerViewportChange(); }); ro.observe(dom); this.ro = ro; this.on('destroy', () => { eventManager.destroy(); dom.removeEventListener('mousemove', this.handlePointerMoveOrigin); this.ro.disconnect(); this.removeAllListeners(); }); const { viewport = defaultViewport, canPitch = true, canRotate = true, projection = 'mercator', maxZoom = 19, minZoom = 2, maxPitch = 60, disableMove = false, disableZoom = false, disableRotate = false, rotateDelay = 6, zoomDelay = 0.1, motion = false, limit = defaultBounds, } = options; this.viewport = viewport; this.realViewport = viewport; this.motion = motion; this.canPitch = canPitch; this.canRotate = canRotate; this.projection = projection; this.maxZoom = maxZoom; this.minZoom = minZoom; this.maxPitch = maxPitch; this.disableMove = disableMove; this.disableZoom = disableZoom; this.disableRotate = disableRotate; this.rotateDelay = rotateDelay; this.zoomDelay = zoomDelay; this.limit = limit; this.dom = dom; this.computeSize(); let time = undefined; const startAnim = () => { this.timer = requestAnimationFrame((ctime) => { const timeDiff = ctime - (time !== null && time !== void 0 ? time : ctime); this.animViewport(timeDiff); time = ctime; if (this.motion) { startAnim(); } }); }; startAnim(); this.on('destroy', () => { cancelAnimationFrame(this.timer); }); } computeSize() { if (this.dom) { this.domSize = [this.dom.clientWidth, this.dom.clientHeight]; } } destroy() { this.emit('destroy'); } setViewport(viewport) { this.realViewport = viewport; this.viewport = viewport; } getViewport(real = false) { const [width, height] = this.domSize; return Object.assign(Object.assign({}, (real ? this.realViewport : this.viewport)), { width, height }); } getViewportWithProjection() { const wmvp = getMapViewport(this.getViewport(), this.projection); return wmvp; } getRealViewportWithProjection() { const wmvp = getMapViewport(this.getViewport(true), this.projection); return wmvp; } getLimitViewport(viewport, limitBounds = defaultBounds) { const wmvp = getMapViewport(Object.assign(Object.assign({}, viewport), { pitch: 0, rotate: 0 }), this.projection); const { longitude, latitude, width, height } = wmvp; // 整个地图 左下角的像素 const minMapPixel = wmvp.project(defaultBounds[0]); // 整个地图 右上角像素 const maxMapPixel = wmvp.project(defaultBounds[1]); const mapSize = [maxMapPixel[0] - minMapPixel[0], minMapPixel[1] - maxMapPixel[1]]; // limit 左下角的像素 const minBoundsPixel = wmvp.project(limitBounds[0]); // limit 右上角像素 const maxBoundsPixel = wmvp.project(limitBounds[1]); const boundsSize = [maxBoundsPixel[0] - minBoundsPixel[0], minBoundsPixel[1] - maxBoundsPixel[1]]; let lng = longitude; let lat = latitude; const centerOffset = [0, 0]; if (minBoundsPixel[0] > 0) { // eslint-disable-next-line prefer-destructuring centerOffset[0] = minBoundsPixel[0]; } if (maxBoundsPixel[0] < width) { centerOffset[0] = maxBoundsPixel[0] - width; } if (maxBoundsPixel[1] > 0) { // eslint-disable-next-line prefer-destructuring centerOffset[1] = maxBoundsPixel[1]; } if (minBoundsPixel[1] < height) { centerOffset[1] = minBoundsPixel[1] - height; } if (boundsSize[0] < width) { const xPosition = minBoundsPixel[0] + 0.5 * (maxBoundsPixel[0] - minBoundsPixel[0]); centerOffset[0] = xPosition - width / 2; } if (boundsSize[1] < height) { const yPosition = maxBoundsPixel[1] + 0.5 * (minBoundsPixel[1] - maxBoundsPixel[1]); centerOffset[1] = yPosition - height / 2; } centerOffset[0] = Math.max(centerOffset[0], minMapPixel[0]); centerOffset[0] = Math.min(centerOffset[0], maxMapPixel[0] - width); centerOffset[1] = Math.max(centerOffset[1], maxMapPixel[1]); centerOffset[1] = Math.min(centerOffset[1], minMapPixel[1] - height); if (centerOffset[0] !== 0 || centerOffset[1] !== 0) { [lng, lat] = wmvp.unproject([width / 2 + centerOffset[0], height / 2 + centerOffset[1]]); } if (mapSize[0] < width) { lng = 0; } if (mapSize[1] < height) { lat = 0; } const newViewport = Object.assign(Object.assign({}, viewport), { lng: toFixedNumber(lng, 6), lat: toFixedNumber(lat, 6) }); return newViewport; } handlePointerDown(e) { if (!this.controllable(e, 'move')) { return; } if (e.rightButton) { this.canRightButtonDown = true; this.rightButtonDown = true; const viewport = this.getViewport(); this.startPitch = viewport.pitch; this.startRotation = viewport.rotate; } /* 如果超过2指,则取消平移效果 */ if (e.pointers.length > 1) { this.handlePointerUp(); } else { this.lastPos = { x: Infinity, y: Infinity }; } const { x, y } = e.offsetCenter; // 根据 点击的坐标 记录起始坐标 const vpp = this.getViewportWithProjection(); const [lng, lat] = vpp.unproject([x, y]); this.startPos = [lng, lat]; } handleContextmenu(e) { if (!this.controllable(e, 'move')) { return; } if (!this.canRightButtonDown) { // 无法通过 pointerdown 事件捕获右键点击的情况 const { x, y } = e.offsetCenter; const vpp = this.getViewportWithProjection(); const [lng, lat] = vpp.unproject([x, y]); this.startPos = [lng, lat]; } e.preventDefault(); } handlePointerMove(e) { const { x, y } = e.offsetCenter; if (this.startPos == null) { return; } if (this.lastPos.x === x && this.lastPos.y === y) { return; } this.lastPos = { x, y }; if (e.leftButton || e.rightButton || e.pointerType === 'touch') { if (!this.moving) { this.emit('move-start'); this.moving = true; } } if (e.rightButton) { if (this.startPitch != null && this.startRotation != null) { this.triggerViewportChange({ pitch: this.canPitch ? clamp(this.startPitch - e.deltaY / 2, 0, this.maxPitch) : 0, rotate: this.canRotate ? this.startRotation + e.deltaX / 2 : 0, }); } } else if (e.leftButton || (e.pointerType === 'touch' && e.pointers.length === 1)) { if (this.rightButtonDown) { this.handlePointerUp(); return; } const [lng, lat] = this.startPos; const vpp = this.getViewportWithProjection(); const [centerLng, centerLat] = vpp.getMapCenterByLngLatPosition({ lngLat: [lng, lat], pos: [x, y], }); this.triggerViewportChange({ lng: Number(centerLng.toFixed(6)), lat: Number(centerLat.toFixed(6)), }); } } handlePointerUp() { this.startPos = null; this.startRotation = null; this.startPitch = null; this.rightButtonDown = false; this.pointerdown -= 1; this.pointerdown = Math.max(0, this.pointerdown); if (this.moving) { this.moving = false; this.emit('move-end'); } } handleWheel(e) { const { maxZoom, minZoom } = this; const viewport = this.getViewport(); if (!this.controllable(e, 'zoom')) { return; } const { x, y } = e.offsetCenter; if (Math.sign(this.wheelDelta) !== Math.sign(e.delta)) { this.wheelDelta = 0; this.zoomStart = null; } if (this.zoomStart == null) { this.zoomStart = viewport.zoom; } this.wheelDelta += clamp(e.delta, -20, 20); const newZoom = Number(clamp(this.zoomStart + (this.wheelDelta || 0) * 0.01, minZoom, maxZoom).toFixed(2)); // origin center const [lng, lat] = getMapViewport(viewport).unproject([x, y]); const vpp = getMapViewport(Object.assign(Object.assign({}, viewport), { zoom: newZoom }), this.projection); const [centerLng, centerLat] = vpp.getMapCenterByLngLatPosition({ lngLat: [lng, lat], pos: [x, y], }); const newViewport = Object.assign(Object.assign({}, viewport), { zoom: newZoom, lng: centerLng, lat: centerLat }); this.triggerViewportChange(newViewport, { x, y }); clearTimeout(this.wheelTimer); this.wheelTimer = setTimeout(() => { this.wheelDelta = 0; this.zoomStart = null; }, 300); e.preventDefault(); } handlePinchStart(e) { if (!this.controllable(e, 'zoom')) { return; } const viewport = this.getViewport(); const { x, y } = e.offsetCenter; this.viewport = viewport; const wmvp = getMapViewport(viewport, this.projection); const [lng, lat] = wmvp.unproject([x, y]); this.pinchstartCoor = [lng, lat]; this.pinchOrRotate = null; } handlePinchMove(e) { if (this.pinchstartCoor) { const [lng, lat] = this.pinchstartCoor; const { x, y } = e.offsetCenter; const zoomDiff = Math.log2(e.scale); if (this.pinchOrRotate == null) { if (Math.abs(zoomDiff) < this.zoomDelay) { return; } this.pinchOrRotate = 'pinch'; } else if (this.pinchOrRotate !== 'pinch') { return; } const viewport = this.getViewport(); const newZoom = clamp(viewport.zoom + zoomDiff, 3, 18); const wmvp = this.getViewportWithProjection(); const [centerLng, centerLat] = wmvp.getMapCenterByLngLatPosition({ lngLat: [lng, lat], pos: [x, y], }); this.triggerViewportChange({ lng: Number(centerLng.toFixed(6)), lat: Number(centerLat.toFixed(6)), zoom: newZoom, }); } } handlePinchEnd() { this.pinchstartCoor = null; } handleRotateStart(e) { if (!this.controllable(e, 'rotate')) { return; } this.startRotation = e.rotation; /* 延迟旋转 */ this.pinchOrRotate = null; } handleRotateMove(e) { const { disableRotate, rotateDelay } = this; if (!disableRotate && this.startRotation != null) { const rotationDiff = e.rotation - this.startRotation; if (this.pinchOrRotate == null) { if (Math.abs(rotationDiff) < rotateDelay) { return; } this.pinchOrRotate = 'rotate'; } else if (this.pinchOrRotate !== 'rotate') { return; } const { rotate } = this.getViewport(); this.triggerViewportChange({ rotate: rotate - rotationDiff, }); } } handleRotateEnd() { this.startRotation = null; } triggerViewportChange(changedViewport = {}, mousePoint) { const viewport = this.getViewport(); const newViewport = this.getLimitViewport(Object.assign(Object.assign({}, viewport), changedViewport), this.limit); newViewport.zoom = Number(newViewport.zoom.toFixed(6)); this.viewport = newViewport; this.mousePoint = mousePoint; const isResize = Object.keys(changedViewport).length === 0; // triggerEvents if (!this.motion || isResize) { this.realViewport = this.viewport; if (viewport.zoom !== newViewport.zoom) { this.emit('zoom', newViewport.zoom); } if (viewport.rotate !== newViewport.rotate) { this.emit('rotate', newViewport.rotate); } if (viewport.pitch !== newViewport.pitch) { this.emit('pitch', newViewport.pitch); } this.emit('viewport-change', newViewport); this.triggerViewportChangePause(newViewport); } } animViewport(time) { const targetViewport = this.getViewport(); const viewport = this.realViewport; if (this.viewport === this.realViewport || (isSame(viewport.zoom, targetViewport.zoom, 0.01) && isSame(viewport.lng, targetViewport.lng, 0.000001) && isSame(viewport.lat, targetViewport.lat, 0.000001) && isSame(viewport.pitch, targetViewport.pitch, 0.1) && isSame(viewport.rotate, targetViewport.rotate, 0.1))) { return; } const rate = typeof this.motion === 'number' ? this.motion : 0.9; let r = 1; for (let i = 0; i < time / 16; i++) { r *= rate; } let newViewport = mixObj(viewport, targetViewport, 1 - r); // newViewport.zoom = Math.log2(mix(2 ** viewport.zoom, 2 ** targetViewport.zoom, 0.05)); newViewport.zoom = toFixedNumber(alignNumber(newViewport.zoom, targetViewport.zoom, 0.005), 3); if (this.mousePoint) { const vp = Object.assign(Object.assign({}, this.getViewport()), viewport); const { x, y } = this.mousePoint; const [lng, lat] = getMapViewport(vp).unproject([x, y]); const vpp = getMapViewport(Object.assign(Object.assign({}, vp), { zoom: newViewport.zoom }), this.projection); const [centerLng, centerLat] = vpp.getMapCenterByLngLatPosition({ lngLat: [lng, lat], pos: [x, y], }); newViewport.lng = centerLng; newViewport.lat = centerLat; if (newViewport.zoom === targetViewport.zoom) { this.viewport.lng = centerLng; this.viewport.lat = centerLat; } } newViewport.lng = toFixedNumber(alignNumber(newViewport.lng, targetViewport.lng, 0.000001), 6); newViewport.lat = toFixedNumber(alignNumber(newViewport.lat, targetViewport.lat, 0.000001), 6); newViewport.rotate = toFixedNumber(alignNumber(newViewport.rotate, targetViewport.rotate, 0.1), 2); newViewport.pitch = toFixedNumber(alignNumber(newViewport.pitch, targetViewport.pitch, 0.1), 2); newViewport = this.getLimitViewport(newViewport, this.limit); this.realViewport = newViewport; // if (this.animTriggerTimer) { // clearTimeout(this.animTriggerTimer); // } // this.animTriggerTimer = setTimeout(() => { // this.animTriggerTimer = 0; if (viewport.zoom !== newViewport.zoom) { this.emit('zoom', newViewport.zoom); } if (viewport.rotate !== newViewport.rotate) { this.emit('rotate', newViewport.rotate); } if (viewport.pitch !== newViewport.pitch) { this.emit('pitch', newViewport.pitch); } this.emit('viewport-change', newViewport); this.triggerViewportChangePause(newViewport); // }); } controllable(e, type) { const { disableMove, disableZoom, disableRotate } = this; if (disableMove && type === 'move') return false; if (disableZoom && type === 'zoom') return false; if (disableRotate && type === 'rotate') return false; return true; } }