mirror of https://gitee.com/antv-l7/antv-l7
commit
915728c39a
docs/api/district
packages
component/src
core/src/services
l7/demo
layers/src/point/models
map
README.md
__tests__
package.jsonsrc
camera.ts
tsconfig.build.jsoncss
geo
handler
IHandler.tsblockable_map_event.tsbox_zoom.tsclick_zoom.ts
hash.tsindex.tsinterface.tsmap.tsutil.tsevents
handler_inertia.tshandler_manager.tshandler_util.tskeyboard.tsmap_event.tsmouse
scroll_zoom.tsshim
tap
touch
utils
maps/src
react/src/component
renderer/src/regl
scene/__tests__
stories
3D_Model/Components
Components/components
District/Layer
Layers/components
React/components
world_ncov.tsxworld_ncov_bubble.tsxworld_ncov_bubble_animate.tsxworld_ncov_column.tsxworld_ncov_fill.tsx
customMap
|
@ -55,7 +55,8 @@ order: 2
|
|||
- strokeWidth 填充描边宽度 `ProvinceLayer, CityLayer, CountyLayer`
|
||||
- autoFit 是否自动缩放到图层范围 `boolean`
|
||||
- popup 信息窗口
|
||||
|
||||
- openTriggerEvent 触发事件 例如 'mousemove' | 'click';
|
||||
- closeTriggerEvent 触发事件 例如 'mousemove' | 'click';
|
||||
- enable 是否开启 `boolean`
|
||||
- triggerEvent 触发事件 例如 'mousemove' | 'click';
|
||||
- Html popup html 字符串,支持回调函数 (properties: any) => string;
|
||||
|
|
|
@ -62,7 +62,8 @@ order: 2
|
|||
- popup 信息窗口
|
||||
|
||||
- enable 是否开启 `boolean`
|
||||
- triggerEvent 触发事件 例如 'mousemove' | 'click';
|
||||
- openTriggerEvent 触发事件 例如 'mousemove' | 'click';
|
||||
- closeTriggerEvent 触发事件 例如 'mousemove' | 'click';
|
||||
- Html popup html 字符串,支持回调函数 (properties: any) => string;
|
||||
|
||||
- chinaNationalStroke 中国国界线颜色 `CountryLayer`
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
IPoint,
|
||||
IPopup,
|
||||
IPopupOption,
|
||||
ISceneService,
|
||||
TYPES,
|
||||
} from '@antv/l7-core';
|
||||
import {
|
||||
|
@ -21,6 +22,7 @@ import { Container } from 'inversify';
|
|||
export default class Popup extends EventEmitter implements IPopup {
|
||||
private popupOption: IPopupOption;
|
||||
private mapsService: IMapService<unknown>;
|
||||
private sceneSerive: ISceneService;
|
||||
private lngLat: ILngLat;
|
||||
private content: HTMLElement;
|
||||
private closeButton: HTMLElement;
|
||||
|
@ -40,6 +42,7 @@ export default class Popup extends EventEmitter implements IPopup {
|
|||
|
||||
public addTo(scene: Container) {
|
||||
this.mapsService = scene.get<IMapService>(TYPES.IMapService);
|
||||
this.sceneSerive = scene.get<ISceneService>(TYPES.ISceneService);
|
||||
this.mapsService.on('camerachange', this.update);
|
||||
this.scene = scene;
|
||||
this.update();
|
||||
|
@ -177,6 +180,7 @@ export default class Popup extends EventEmitter implements IPopup {
|
|||
offsets: [0, 0],
|
||||
anchor: anchorType.BOTTOM,
|
||||
className: '',
|
||||
stopPropagation: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -193,12 +197,12 @@ export default class Popup extends EventEmitter implements IPopup {
|
|||
if (!this.mapsService || !hasPosition || !this.content) {
|
||||
return;
|
||||
}
|
||||
const markerContainer = this.mapsService.getMarkerContainer();
|
||||
if (!this.container && markerContainer) {
|
||||
const popupContainer = this.sceneSerive.getSceneContainer();
|
||||
if (!this.container && popupContainer) {
|
||||
this.container = this.creatDom(
|
||||
'div',
|
||||
'l7-popup',
|
||||
markerContainer.parentNode as HTMLElement,
|
||||
popupContainer as HTMLElement,
|
||||
);
|
||||
|
||||
this.tip = this.creatDom('div', 'l7-popup-tip', this.container);
|
||||
|
@ -208,11 +212,12 @@ export default class Popup extends EventEmitter implements IPopup {
|
|||
.split(' ')
|
||||
.forEach((name) => this.container.classList.add(name));
|
||||
}
|
||||
['mousemove', 'mousedown', 'mouseup', 'click'].forEach((type) => {
|
||||
this.container.addEventListener(type, (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
// ['mousemove', 'mousedown', 'mouseup', 'click'].forEach((type) => {
|
||||
// this.container.addEventListener(type, (e) => {
|
||||
// e.stopPropagation();
|
||||
// });
|
||||
// });
|
||||
}
|
||||
if (maxWidth && this.container.style.maxWidth !== maxWidth) {
|
||||
this.container.style.maxWidth = maxWidth;
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface IPopupOption {
|
|||
anchor: anchorType[any];
|
||||
className: string;
|
||||
offsets: number[];
|
||||
stopPropagation: boolean;
|
||||
}
|
||||
export interface IPopup {
|
||||
addTo(scene: Container): this;
|
||||
|
|
|
@ -308,7 +308,7 @@ export default class StyleAttributeService implements IStyleAttributeService {
|
|||
}
|
||||
});
|
||||
|
||||
this.attributesAndIndices.elements.destroy();
|
||||
this.attributesAndIndices?.elements.destroy();
|
||||
this.attributes = [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,8 @@ export interface ITexture2DInitializationOptions {
|
|||
|
||||
export interface ITexture2D {
|
||||
get(): unknown;
|
||||
update(): void;
|
||||
update(options: any): void;
|
||||
bind(): void;
|
||||
resize(options: { width: number; height: number }): void;
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>L7 IE</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
cursor:pointer;
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
href="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.8.3/polyfill.min.js"></script>
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script>
|
||||
|
||||
<script src="../dist/l7-dev.js"></script>
|
||||
<script>
|
||||
console.log(L7);
|
||||
const scene = new L7.Scene({
|
||||
id: "map",
|
||||
map: new L7.Mapbox({
|
||||
style: "blank", // 样式URL
|
||||
center: [108.6167, 19.1000],
|
||||
pitch: 0,
|
||||
zoom: 6
|
||||
})
|
||||
});
|
||||
|
||||
const data = {"list":[{"w":19.1000,"t":24.6000,"s":"海南","l":11,"m":"东方","j":108.6167,"h":"59838"},{"w":20.0000,"t":23.8000,"s":"海南","l":11,"m":"海口","j":110.2500,"h":"59758"},{"w":22.2750,"t":23.6000,"s":"广东","l":12,"m":"珠海","j":113.5669,"h":"59488"},{"w":20.3372,"t":23.4000,"s":"广东","l":12,"m":"徐闻","j":110.1794,"h":"59754"},{"w":19.2089,"t":23.2000,"s":"海南","l":12,"m":"琼海","j":110.4819,"h":"59855"},{"w":21.7358,"t":23.2000,"s":"广东","l":11,"m":"上川岛","j":112.7731,"h":"59673"},{"w":23.3853,"t":23.0000,"s":"广东","l":11,"m":"汕头","j":116.6792,"h":"59316"},{"w":22.5417,"t":23.0000,"s":"广东","l":12,"m":"深圳","j":114.0033,"h":"59493"},{"w":19.5167,"t":22.9000,"s":"海南","l":12,"m":"儋州","j":109.5833,"h":"59845"},{"w":21.1547,"t":22.7000,"s":"广东","l":12,"m":"湛江","j":110.3022,"h":"59658"},{"w":21.4500,"t":22.7000,"s":"广西","l":12,"m":"北海","j":109.1333,"h":"59644"},{"w":22.5000,"t":22.6000,"s":"广东","l":12,"m":"中山","j":113.4000,"h":"59485"},{"w":21.8453,"t":22.6000,"s":"广东","l":12,"m":"阳江","j":111.9783,"h":"59663"},{"w":22.3469,"t":22.6000,"s":"广东","l":12,"m":"信宜","j":110.9250,"h":"59456"},{"w":22.8000,"t":22.5000,"s":"广东","l":12,"m":"汕尾","j":115.3667,"h":"59501"},{"w":23.4275,"t":22.3000,"s":"广东","l":12,"m":"南澳","j":117.0292,"h":"59324"},{"w":22.7100,"t":22.3000,"s":"广东","l":12,"m":"罗定","j":111.6000,"h":"59462"},{"w":19.0333,"t":22.3000,"s":"海南","l":12,"m":"琼中","j":109.8333,"h":"59849"},{"w":21.5458,"t":22.2000,"s":"广东","l":11,"m":"电白","j":110.9886,"h":"59664"},{"w":22.9661,"t":21.9000,"s":"广东","l":12,"m":"东莞","j":113.7389,"h":"59289"},{"w":22.2472,"t":21.8000,"s":"广东","l":12,"m":"台山","j":112.7858,"h":"59478"},{"w":22.9836,"t":21.6000,"s":"广东","l":12,"m":"惠来","j":116.3014,"h":"59317"},{"w":22.9906,"t":21.0000,"s":"广东","l":12,"m":"高要","j":112.4786,"h":"59278"},{"w":23.9000,"t":20.8000,"s":"广西","l":12,"m":"百色","j":106.6000,"h":"59211"},{"w":23.0711,"t":20.4000,"s":"广东","l":12,"m":"惠阳","j":114.3744,"h":"59298"},{"w":23.4497,"t":20.4000,"s":"广东","l":12,"m":"揭西","j":115.8492,"h":"59306"},{"w":23.3353,"t":20.4000,"s":"广东","l":11,"m":"增城","j":113.8275,"h":"59294"},{"w":23.4167,"t":19.9000,"s":"广西","l":12,"m":"那坡","j":105.8333,"h":"59209"},{"w":24.9000,"t":19.7000,"s":"福建","l":11,"m":"崇武","j":118.9167,"h":"59133"},{"w":24.4833,"t":19.7000,"s":"福建","l":12,"m":"厦门","j":118.0667,"h":"59134"},{"w":23.7936,"t":19.6000,"s":"广东","l":12,"m":"河源","j":114.7297,"h":"59293"},{"w":23.7106,"t":19.4000,"s":"广东","l":12,"m":"清远","j":113.0850,"h":"59280"},{"w":23.1000,"t":19.4000,"s":"广西","l":12,"m":"靖西","j":106.4500,"h":"59218"},{"w":23.6000,"t":19.4000,"s":"广西","l":13,"m":"田东","j":107.1167,"h":"59224"},{"w":25.5167,"t":19.2000,"s":"福建","l":12,"m":"平潭","j":119.7833,"h":"58944"},{"w":25.0500,"t":19.2000,"s":"福建","l":12,"m":"龙岩","j":117.0167,"h":"58927"},{"w":23.2100,"t":19.0000,"s":"广东","l":12,"m":"广州","j":113.4822,"h":"59287"},{"w":21.9833,"t":18.9000,"s":"广西","l":12,"m":"钦州","j":108.6000,"h":"59632"}]}
|
||||
|
||||
scene.on('loaded',function(){
|
||||
scene.addImage(
|
||||
'00',
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/604b5e7f-309e-40db-b95b-4fac746c5153.svg'
|
||||
);
|
||||
scene.addImage(
|
||||
'01',
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/30580bc9-506f-4438-8c1a-744e082054ec.svg'
|
||||
);
|
||||
scene.addImage(
|
||||
'02',
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/7aa1f460-9f9f-499f-afdf-13424aa26bbf.svg'
|
||||
);
|
||||
const pointLayer = new L7.PointLayer({})
|
||||
.source(data.list, {
|
||||
parser: {
|
||||
type: "json",
|
||||
x: "j",
|
||||
y: "w"
|
||||
}
|
||||
})
|
||||
|
||||
.shape('m', [ '00', '01', '02' ])
|
||||
.size(20)
|
||||
.active(true)
|
||||
.style({
|
||||
opacity: 1.0
|
||||
});
|
||||
|
||||
scene.addLayer(pointLayer);
|
||||
pointLayer.on('inited',function(){
|
||||
console.log('加载完成');
|
||||
})
|
||||
scene.render();
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -21,7 +21,7 @@ export default class ImageModel extends BaseModel {
|
|||
public getUninforms(): IModelUniform {
|
||||
const { opacity } = this.layer.getLayerConfig() as IImageLayerStyleOptions;
|
||||
if (this.rendererService.getDirty()) {
|
||||
this.texture.update();
|
||||
this.texture.bind();
|
||||
}
|
||||
return {
|
||||
u_opacity: opacity || 1.0,
|
||||
|
@ -112,7 +112,11 @@ export default class ImageModel extends BaseModel {
|
|||
private updateTexture = () => {
|
||||
const { createTexture2D } = this.rendererService;
|
||||
if (this.texture) {
|
||||
this.texture.destroy();
|
||||
this.texture.update({
|
||||
data: this.iconService.getCanvas(),
|
||||
});
|
||||
this.layer.render();
|
||||
return;
|
||||
}
|
||||
this.texture = createTexture2D({
|
||||
data: this.iconService.getCanvas(),
|
||||
|
@ -121,6 +125,5 @@ export default class ImageModel extends BaseModel {
|
|||
width: 1024,
|
||||
height: this.iconService.canvasHeight || 128,
|
||||
});
|
||||
this.layer.render();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# `map`
|
||||
|
||||
> TODO: description
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
const map = require('map');
|
||||
|
||||
// TODO: DEMONSTRATE API
|
||||
```
|
|
@ -0,0 +1,14 @@
|
|||
import { Map } from '../src/map';
|
||||
describe('Map', () => {
|
||||
const el = document.createElement('div');
|
||||
el.id = 'test-div-id';
|
||||
// el.style.width = '500px';
|
||||
// el.style.height = '500px';
|
||||
el.style.background = '#aaa';
|
||||
document.querySelector('body')?.appendChild(el);
|
||||
it('init Map', () => {
|
||||
const map = new Map({
|
||||
container: el,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "@antv/l7-map",
|
||||
"version": "2.2.21",
|
||||
"description": "l7 map",
|
||||
"keywords": [],
|
||||
"author": "thinkinggis <lzx199065@gmail.com>",
|
||||
"license": "ISC",
|
||||
"main": "lib/index.js",
|
||||
"module": "es/index.js",
|
||||
"unpkg": "dist/l7-map.js",
|
||||
"types": "es/index.d.ts",
|
||||
"sideEffects": true,
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"es",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/antvis/L7.git"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc --project tsconfig.build.json",
|
||||
"clean": "rimraf dist; rimraf es; rimraf lib;",
|
||||
"build": "run-p build:*",
|
||||
"build:cjs": "BABEL_ENV=cjs babel src --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
|
||||
"build:esm": "BABEL_ENV=esm babel src --root-mode upward --out-dir es --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
|
||||
"watch": "BABEL_ENV=cjs babel src --watch --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
|
||||
"test": "jest"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/antvis/L7/issues"
|
||||
},
|
||||
"homepage": "https://github.com/antvis/L7#readme",
|
||||
"dependencies": {
|
||||
"@antv/l7-utils": "2.2.14",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
"@mapbox/unitbezier": "^0.0.0",
|
||||
"eventemitter3": "^4.0.4",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,889 @@
|
|||
// @ts-ignore
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { merge } from 'lodash';
|
||||
import { IPaddingOptions } from './geo/edge_insets';
|
||||
import LngLat, { LngLatLike } from './geo/lng_lat';
|
||||
import LngLatBounds, { LngLatBoundsLike } from './geo/lng_lat_bounds';
|
||||
import Point, { PointLike } from './geo/point';
|
||||
import Transform from './geo/transform';
|
||||
import { Event } from './handler/events/event';
|
||||
import { IMapOptions } from './interface';
|
||||
type CallBack = (_: number) => void;
|
||||
import {
|
||||
cancel,
|
||||
clamp,
|
||||
ease as defaultEasing,
|
||||
interpolate,
|
||||
now,
|
||||
pick,
|
||||
prefersReducedMotion,
|
||||
raf,
|
||||
wrap,
|
||||
} from './util';
|
||||
|
||||
export interface ICameraOptions {
|
||||
center?: LngLatLike;
|
||||
zoom?: number;
|
||||
bearing?: number;
|
||||
pitch?: number;
|
||||
around?: LngLatLike;
|
||||
padding?: IPaddingOptions;
|
||||
}
|
||||
|
||||
export interface IAnimationOptions {
|
||||
duration?: number;
|
||||
easing?: (_: number) => number;
|
||||
offset?: PointLike;
|
||||
animate?: boolean;
|
||||
essential?: boolean;
|
||||
linear?: boolean;
|
||||
}
|
||||
|
||||
export default class Camera extends EventEmitter {
|
||||
public transform: Transform;
|
||||
// public requestRenderFrame: (_: any) => number;
|
||||
// public cancelRenderFrame: (_: number) => void;
|
||||
protected options: IMapOptions;
|
||||
protected moving: boolean;
|
||||
protected zooming: boolean;
|
||||
protected rotating: boolean;
|
||||
protected pitching: boolean;
|
||||
protected padding: boolean;
|
||||
|
||||
private bearingSnap: number;
|
||||
private easeEndTimeoutID: number;
|
||||
private easeStart: number;
|
||||
private easeOptions: {
|
||||
duration: number;
|
||||
easing: (_: number) => number;
|
||||
};
|
||||
private easeId: string | void;
|
||||
private onEaseFrame: (_: number) => void;
|
||||
private onEaseEnd: (easeId?: string) => void;
|
||||
private easeFrameId: number;
|
||||
|
||||
constructor(options: IMapOptions) {
|
||||
super();
|
||||
this.options = options;
|
||||
const { minZoom, maxZoom, minPitch, maxPitch, renderWorldCopies } = options;
|
||||
this.moving = false;
|
||||
this.zooming = false;
|
||||
this.bearingSnap = options.bearingSnap;
|
||||
this.transform = new Transform(
|
||||
minZoom,
|
||||
maxZoom,
|
||||
minPitch,
|
||||
maxPitch,
|
||||
renderWorldCopies,
|
||||
);
|
||||
}
|
||||
public requestRenderFrame(cb: CallBack): number {
|
||||
return 0;
|
||||
}
|
||||
public cancelRenderFrame(_: number): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public getCenter() {
|
||||
const { lng, lat } = this.transform.center;
|
||||
return new LngLat(lng, lat);
|
||||
}
|
||||
|
||||
public getZoom(): number {
|
||||
return this.transform.zoom;
|
||||
}
|
||||
|
||||
public getPitch(): number {
|
||||
return this.transform.pitch;
|
||||
}
|
||||
|
||||
public setCenter(center: LngLatLike, eventData?: any) {
|
||||
return this.jumpTo({ center }, eventData);
|
||||
}
|
||||
|
||||
public setPitch(pitch: number, eventData?: any) {
|
||||
this.jumpTo({ pitch }, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public getBearing(): number {
|
||||
return this.transform.bearing;
|
||||
}
|
||||
|
||||
public panTo(
|
||||
lnglat: LngLatLike,
|
||||
options?: IAnimationOptions,
|
||||
eventData?: any,
|
||||
) {
|
||||
return this.easeTo(
|
||||
merge(
|
||||
{
|
||||
center: lnglat,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
|
||||
public zoomOut(options?: IAnimationOptions, eventData?: any) {
|
||||
this.zoomTo(this.getZoom() - 1, options, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setBearing(bearing: number, eventData?: any) {
|
||||
this.jumpTo({ bearing }, eventData);
|
||||
return this;
|
||||
}
|
||||
public setZoom(zoom: number, eventData?: any) {
|
||||
this.jumpTo({ zoom }, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public zoomIn(options?: IAnimationOptions, eventData?: any) {
|
||||
this.zoomTo(this.getZoom() + 1, options, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public zoomTo(zoom: number, options?: IAnimationOptions, eventData?: any) {
|
||||
return this.easeTo(
|
||||
merge(
|
||||
{
|
||||
zoom,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
|
||||
public getPadding(): IPaddingOptions {
|
||||
return this.transform.padding;
|
||||
}
|
||||
|
||||
public setPadding(padding: IPaddingOptions, eventData?: any) {
|
||||
this.jumpTo({ padding }, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public rotateTo(
|
||||
bearing: number,
|
||||
options?: IAnimationOptions,
|
||||
eventData?: any,
|
||||
) {
|
||||
return this.easeTo(
|
||||
merge(
|
||||
{
|
||||
bearing,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
|
||||
public resetNorth(options?: IAnimationOptions, eventData?: any) {
|
||||
this.rotateTo(0, merge({ duration: 1000 }, options), eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public resetNorthPitch(options?: IAnimationOptions, eventData?: any) {
|
||||
this.easeTo(
|
||||
merge(
|
||||
{
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
duration: 1000,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
public fitBounds(
|
||||
bounds: LngLatBoundsLike,
|
||||
options?: IAnimationOptions & ICameraOptions,
|
||||
eventData?: any,
|
||||
) {
|
||||
return this.fitInternal(
|
||||
// @ts-ignore
|
||||
this.cameraForBounds(bounds, options),
|
||||
options,
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
public cameraForBounds(
|
||||
bounds: LngLatBoundsLike,
|
||||
options?: ICameraOptions,
|
||||
): void | (ICameraOptions & IAnimationOptions) {
|
||||
bounds = LngLatBounds.convert(bounds);
|
||||
return this.cameraForBoxAndBearing(
|
||||
bounds.getNorthWest(),
|
||||
bounds.getSouthEast(),
|
||||
0,
|
||||
// @ts-ignore
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public snapToNorth(options?: IAnimationOptions, eventData?: any) {
|
||||
if (Math.abs(this.getBearing()) < this.bearingSnap) {
|
||||
return this.resetNorth(options, eventData);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public jumpTo(options: ICameraOptions = {}, eventData?: any) {
|
||||
this.stop();
|
||||
|
||||
const tr = this.transform;
|
||||
let zoomChanged = false;
|
||||
let bearingChanged = false;
|
||||
let pitchChanged = false;
|
||||
|
||||
if (options.zoom && tr.zoom !== +options.zoom) {
|
||||
zoomChanged = true;
|
||||
tr.zoom = +options.zoom;
|
||||
}
|
||||
|
||||
if (options.center !== undefined) {
|
||||
tr.center = LngLat.convert(options.center);
|
||||
}
|
||||
|
||||
if (options.bearing && tr.bearing !== +options.bearing) {
|
||||
bearingChanged = true;
|
||||
tr.bearing = +options.bearing;
|
||||
}
|
||||
|
||||
if (options.pitch && tr.pitch !== +options.pitch) {
|
||||
pitchChanged = true;
|
||||
tr.pitch = +options.pitch;
|
||||
}
|
||||
|
||||
if (options.padding != null && !tr.isPaddingEqual(options.padding)) {
|
||||
tr.padding = options.padding;
|
||||
}
|
||||
|
||||
this.emit('movestart', new Event('movestart', eventData));
|
||||
this.emit('move', new Event('move', eventData));
|
||||
|
||||
if (zoomChanged) {
|
||||
this.emit('zoomstart', new Event('zoomstart', eventData));
|
||||
this.emit('zoom', new Event('zoom', eventData));
|
||||
this.emit('zoomend', new Event('zoomend', eventData));
|
||||
}
|
||||
|
||||
if (bearingChanged) {
|
||||
this.emit('rotatestart', new Event('rotatestart', eventData));
|
||||
this.emit('rotate', new Event('rotate', eventData));
|
||||
this.emit('rotateend', new Event('rotateend', eventData));
|
||||
}
|
||||
|
||||
if (pitchChanged) {
|
||||
this.emit('pitchstart', new Event('pitchstart', eventData));
|
||||
this.emit('pitch', new Event('pitch', eventData));
|
||||
this.emit('pitchend', new Event('pitchend', eventData));
|
||||
}
|
||||
|
||||
return this.emit('moveend', new Event('moveend', eventData));
|
||||
}
|
||||
|
||||
public easeTo(
|
||||
options: ICameraOptions &
|
||||
IAnimationOptions & { easeId?: string; noMoveStart?: boolean } = {},
|
||||
eventData?: any,
|
||||
) {
|
||||
options = merge(
|
||||
{
|
||||
offset: [0, 0],
|
||||
duration: 500,
|
||||
easing: defaultEasing,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (
|
||||
options.animate === false ||
|
||||
(!options.essential && prefersReducedMotion())
|
||||
) {
|
||||
options.duration = 0;
|
||||
}
|
||||
|
||||
const tr = this.transform;
|
||||
const startZoom = this.getZoom();
|
||||
const startBearing = this.getBearing();
|
||||
const startPitch = this.getPitch();
|
||||
const startPadding = this.getPadding();
|
||||
|
||||
const zoom = options.zoom ? +options.zoom : startZoom;
|
||||
const bearing = options.bearing
|
||||
? this.normalizeBearing(options.bearing, startBearing)
|
||||
: startBearing;
|
||||
const pitch = options.pitch ? +options.pitch : startPitch;
|
||||
const padding = options.padding ? options.padding : tr.padding;
|
||||
|
||||
const offsetAsPoint = Point.convert(options.offset);
|
||||
let pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
||||
const locationAtOffset = tr.pointLocation(pointAtOffset);
|
||||
const center = LngLat.convert(options.center || locationAtOffset);
|
||||
this.normalizeCenter(center);
|
||||
|
||||
const from = tr.project(locationAtOffset);
|
||||
const delta = tr.project(center).sub(from);
|
||||
const finalScale = tr.zoomScale(zoom - startZoom);
|
||||
|
||||
let around: LngLat;
|
||||
let aroundPoint: Point;
|
||||
|
||||
if (options.around) {
|
||||
around = LngLat.convert(options.around);
|
||||
aroundPoint = tr.locationPoint(around);
|
||||
}
|
||||
|
||||
const currently = {
|
||||
moving: this.moving,
|
||||
zooming: this.zooming,
|
||||
rotating: this.rotating,
|
||||
pitching: this.pitching,
|
||||
};
|
||||
|
||||
this.zooming = this.zooming || zoom !== startZoom;
|
||||
this.rotating = this.rotating || startBearing !== bearing;
|
||||
this.pitching = this.pitching || pitch !== startPitch;
|
||||
this.padding = !tr.isPaddingEqual(padding);
|
||||
|
||||
this.easeId = options.easeId;
|
||||
this.prepareEase(eventData, options.noMoveStart, currently);
|
||||
|
||||
clearTimeout(this.easeEndTimeoutID);
|
||||
|
||||
this.ease(
|
||||
(k) => {
|
||||
if (this.zooming) {
|
||||
tr.zoom = interpolate(startZoom, zoom, k);
|
||||
}
|
||||
if (this.rotating) {
|
||||
tr.bearing = interpolate(startBearing, bearing, k);
|
||||
}
|
||||
if (this.pitching) {
|
||||
tr.pitch = interpolate(startPitch, pitch, k);
|
||||
}
|
||||
if (this.padding) {
|
||||
tr.interpolatePadding(startPadding, padding, k);
|
||||
// When padding is being applied, Transform#centerPoint is changing continously,
|
||||
// thus we need to recalculate offsetPoint every fra,e
|
||||
pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
||||
}
|
||||
|
||||
if (around) {
|
||||
tr.setLocationAtPoint(around, aroundPoint);
|
||||
} else {
|
||||
const scale = tr.zoomScale(tr.zoom - startZoom);
|
||||
const base =
|
||||
zoom > startZoom
|
||||
? Math.min(2, finalScale)
|
||||
: Math.max(0.5, finalScale);
|
||||
const speedup = Math.pow(base, 1 - k);
|
||||
const newCenter = tr.unproject(
|
||||
from.add(delta.mult(k * speedup)).mult(scale),
|
||||
);
|
||||
tr.setLocationAtPoint(
|
||||
tr.renderWorldCopies ? newCenter.wrap() : newCenter,
|
||||
pointAtOffset,
|
||||
);
|
||||
}
|
||||
|
||||
this.fireMoveEvents(eventData);
|
||||
},
|
||||
(interruptingEaseId?: string) => {
|
||||
this.afterEase(eventData, interruptingEaseId);
|
||||
},
|
||||
// @ts-ignore
|
||||
options,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
public flyTo(options: any = {}, eventData?: any) {
|
||||
// Fall through to jumpTo if user has set prefers-reduced-motion
|
||||
if (!options.essential && prefersReducedMotion()) {
|
||||
const coercedOptions = pick(options, [
|
||||
'center',
|
||||
'zoom',
|
||||
'bearing',
|
||||
'pitch',
|
||||
'around',
|
||||
]) as ICameraOptions;
|
||||
return this.jumpTo(coercedOptions, eventData);
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
options = merge(
|
||||
{
|
||||
offset: [0, 0],
|
||||
speed: 1.2,
|
||||
curve: 1.42,
|
||||
easing: defaultEasing,
|
||||
},
|
||||
options,
|
||||
);
|
||||
const tr = this.transform;
|
||||
const startZoom = this.getZoom();
|
||||
const startBearing = this.getBearing();
|
||||
const startPitch = this.getPitch();
|
||||
const startPadding = this.getPadding();
|
||||
|
||||
const zoom = options.zoom
|
||||
? clamp(+options.zoom, tr.minZoom, tr.maxZoom)
|
||||
: startZoom;
|
||||
const bearing = options.bearing
|
||||
? this.normalizeBearing(options.bearing, startBearing)
|
||||
: startBearing;
|
||||
const pitch = options.pitch ? +options.pitch : startPitch;
|
||||
const padding = 'padding' in options ? options.padding : tr.padding;
|
||||
|
||||
const scale = tr.zoomScale(zoom - startZoom);
|
||||
const offsetAsPoint = Point.convert(options.offset);
|
||||
let pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
||||
const locationAtOffset = tr.pointLocation(pointAtOffset);
|
||||
const center = LngLat.convert(options.center || locationAtOffset);
|
||||
this.normalizeCenter(center);
|
||||
|
||||
const from = tr.project(locationAtOffset);
|
||||
const delta = tr.project(center).sub(from);
|
||||
|
||||
let rho = options.curve;
|
||||
|
||||
// w₀: Initial visible span, measured in pixels at the initial scale.
|
||||
const w0 = Math.max(tr.width, tr.height);
|
||||
// w₁: Final visible span, measured in pixels with respect to the initial scale.
|
||||
const w1 = w0 / scale;
|
||||
// Length of the flight path as projected onto the ground plane, measured in pixels from
|
||||
// the world image origin at the initial scale.
|
||||
const u1 = delta.mag();
|
||||
|
||||
if ('minZoom' in options) {
|
||||
const minZoom = clamp(
|
||||
Math.min(options.minZoom, startZoom, zoom),
|
||||
tr.minZoom,
|
||||
tr.maxZoom,
|
||||
);
|
||||
// w<sub>m</sub>: Maximum visible span, measured in pixels with respect to the initial
|
||||
// scale.
|
||||
const wMax = w0 / tr.zoomScale(minZoom - startZoom);
|
||||
rho = Math.sqrt((wMax / u1) * 2);
|
||||
}
|
||||
|
||||
// ρ²
|
||||
const rho2 = rho * rho;
|
||||
|
||||
/**
|
||||
* rᵢ: Returns the zoom-out factor at one end of the animation.
|
||||
*
|
||||
* @param i 0 for the ascent or 1 for the descent.
|
||||
* @private
|
||||
*/
|
||||
function r(i: number) {
|
||||
const b =
|
||||
(w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) /
|
||||
(2 * (i ? w1 : w0) * rho2 * u1);
|
||||
return Math.log(Math.sqrt(b * b + 1) - b);
|
||||
}
|
||||
|
||||
function sinh(n: number) {
|
||||
return (Math.exp(n) - Math.exp(-n)) / 2;
|
||||
}
|
||||
function cosh(n: number) {
|
||||
return (Math.exp(n) + Math.exp(-n)) / 2;
|
||||
}
|
||||
function tanh(n: number) {
|
||||
return sinh(n) / cosh(n);
|
||||
}
|
||||
|
||||
// r₀: Zoom-out factor during ascent.
|
||||
const r0 = r(0);
|
||||
|
||||
// w(s): Returns the visible span on the ground, measured in pixels with respect to the
|
||||
// initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°.
|
||||
let w: (_: number) => number = (s) => {
|
||||
return cosh(r0) / cosh(r0 + rho * s);
|
||||
};
|
||||
|
||||
// u(s): Returns the distance along the flight path as projected onto the ground plane,
|
||||
// measured in pixels from the world image origin at the initial scale.
|
||||
let u: (_: number) => number = (s) => {
|
||||
return (w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2)) / u1;
|
||||
};
|
||||
|
||||
// S: Total length of the flight path, measured in ρ-screenfuls.
|
||||
let S = (r(1) - r0) / rho;
|
||||
|
||||
// When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
|
||||
if (Math.abs(u1) < 0.000001 || !isFinite(S)) {
|
||||
// Perform a more or less instantaneous transition if the path is too short.
|
||||
if (Math.abs(w0 - w1) < 0.000001) {
|
||||
return this.easeTo(options, eventData);
|
||||
}
|
||||
|
||||
const k = w1 < w0 ? -1 : 1;
|
||||
S = Math.abs(Math.log(w1 / w0)) / rho;
|
||||
|
||||
u = () => {
|
||||
return 0;
|
||||
};
|
||||
w = (s) => {
|
||||
return Math.exp(k * rho * s);
|
||||
};
|
||||
}
|
||||
|
||||
if ('duration' in options) {
|
||||
options.duration = +options.duration;
|
||||
} else {
|
||||
const V =
|
||||
'screenSpeed' in options ? +options.screenSpeed / rho : +options.speed;
|
||||
options.duration = (1000 * S) / V;
|
||||
}
|
||||
|
||||
if (options.maxDuration && options.duration > options.maxDuration) {
|
||||
options.duration = 0;
|
||||
}
|
||||
|
||||
this.zooming = true;
|
||||
this.rotating = startBearing !== bearing;
|
||||
this.pitching = pitch !== startPitch;
|
||||
this.padding = !tr.isPaddingEqual(padding);
|
||||
|
||||
this.prepareEase(eventData, false);
|
||||
|
||||
this.ease(
|
||||
(k) => {
|
||||
// s: The distance traveled along the flight path, measured in ρ-screenfuls.
|
||||
const s = k * S;
|
||||
// @ts-ignore
|
||||
const easeScale = 1 / w(s);
|
||||
tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(easeScale);
|
||||
|
||||
if (this.rotating) {
|
||||
tr.bearing = interpolate(startBearing, bearing, k);
|
||||
}
|
||||
if (this.pitching) {
|
||||
tr.pitch = interpolate(startPitch, pitch, k);
|
||||
}
|
||||
if (this.padding) {
|
||||
tr.interpolatePadding(startPadding, padding, k);
|
||||
// When padding is being applied, Transform#centerPoint is changing continously,
|
||||
// thus we need to recalculate offsetPoint every frame
|
||||
pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
||||
}
|
||||
|
||||
const newCenter =
|
||||
k === 1
|
||||
? center
|
||||
: tr.unproject(from.add(delta.mult(u(s))).mult(easeScale));
|
||||
tr.setLocationAtPoint(
|
||||
tr.renderWorldCopies ? newCenter.wrap() : newCenter,
|
||||
pointAtOffset,
|
||||
);
|
||||
|
||||
this.fireMoveEvents(eventData);
|
||||
},
|
||||
() => this.afterEase(eventData),
|
||||
options,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
public fitScreenCoordinates(
|
||||
p0: PointLike,
|
||||
p1: PointLike,
|
||||
bearing: number,
|
||||
options?: IAnimationOptions & ICameraOptions,
|
||||
eventData?: any,
|
||||
) {
|
||||
return this.fitInternal(
|
||||
// @ts-ignore
|
||||
this.cameraForBoxAndBearing(
|
||||
this.transform.pointLocation(Point.convert(p0)),
|
||||
this.transform.pointLocation(Point.convert(p1)),
|
||||
bearing,
|
||||
// @ts-ignore
|
||||
options,
|
||||
),
|
||||
options,
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
public stop(allowGestures?: boolean, easeId?: string) {
|
||||
if (this.easeFrameId) {
|
||||
this.cancelRenderFrame(this.easeFrameId);
|
||||
delete this.easeFrameId;
|
||||
delete this.onEaseFrame;
|
||||
}
|
||||
|
||||
if (this.onEaseEnd) {
|
||||
// The _onEaseEnd function might emit events which trigger new
|
||||
// animation, which sets a new _onEaseEnd. Ensure we don't delete
|
||||
// it unintentionally.
|
||||
const onEaseEnd = this.onEaseEnd;
|
||||
delete this.onEaseEnd;
|
||||
onEaseEnd.call(this, easeId);
|
||||
}
|
||||
// if (!allowGestures) {
|
||||
// const handlers = (this: any).handlers;
|
||||
// if (handlers) handlers.stop();
|
||||
// }
|
||||
return this;
|
||||
}
|
||||
public renderFrameCallback = () => {
|
||||
const t = Math.min((now() - this.easeStart) / this.easeOptions.duration, 1);
|
||||
this.onEaseFrame(this.easeOptions.easing(t));
|
||||
if (t < 1) {
|
||||
// this.easeFrameId = window.requestAnimationFrame(this.renderFrameCallback);
|
||||
this.easeFrameId = this.requestRenderFrame(this.renderFrameCallback);
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
private normalizeBearing(bearing: number, currentBearing: number) {
|
||||
bearing = wrap(bearing, -180, 180);
|
||||
const diff = Math.abs(bearing - currentBearing);
|
||||
if (Math.abs(bearing - 360 - currentBearing) < diff) {
|
||||
bearing -= 360;
|
||||
}
|
||||
if (Math.abs(bearing + 360 - currentBearing) < diff) {
|
||||
bearing += 360;
|
||||
}
|
||||
return bearing;
|
||||
}
|
||||
|
||||
private normalizeCenter(center: LngLat) {
|
||||
const tr = this.transform;
|
||||
if (!tr.renderWorldCopies || tr.lngRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = center.lng - tr.center.lng;
|
||||
center.lng += delta > 180 ? -360 : delta < -180 ? 360 : 0;
|
||||
}
|
||||
|
||||
private fireMoveEvents(eventData?: any) {
|
||||
this.emit('move', new Event('move', eventData));
|
||||
if (this.zooming) {
|
||||
this.emit('zoom', new Event('zoom', eventData));
|
||||
}
|
||||
if (this.rotating) {
|
||||
this.emit('rotate', new Event('rotate', eventData));
|
||||
}
|
||||
if (this.pitching) {
|
||||
this.emit('rotate', new Event('pitch', eventData));
|
||||
}
|
||||
}
|
||||
private prepareEase(
|
||||
eventData: object | undefined,
|
||||
noMoveStart: boolean = false,
|
||||
currently: { [key: string]: boolean } = {},
|
||||
) {
|
||||
this.moving = true;
|
||||
|
||||
if (!noMoveStart && !currently.moving) {
|
||||
this.emit('movestart', new Event('movestart', eventData));
|
||||
}
|
||||
if (this.zooming && !currently.zooming) {
|
||||
this.emit('zoomstart', new Event('zoomstart', eventData));
|
||||
}
|
||||
if (this.rotating && !currently.rotating) {
|
||||
this.emit('rotatestart', new Event('rotatestart', eventData));
|
||||
}
|
||||
if (this.pitching && !currently.pitching) {
|
||||
this.emit('pitchstart', new Event('pitchstart', eventData));
|
||||
}
|
||||
}
|
||||
|
||||
private afterEase(eventData: object | undefined, easeId?: string) {
|
||||
// if this easing is being stopped to start another easing with
|
||||
// the same id then don't fire any events to avoid extra start/stop events
|
||||
if (this.easeId && easeId && this.easeId === easeId) {
|
||||
return;
|
||||
}
|
||||
delete this.easeId;
|
||||
|
||||
const wasZooming = this.zooming;
|
||||
const wasRotating = this.rotating;
|
||||
const wasPitching = this.pitching;
|
||||
this.moving = false;
|
||||
this.zooming = false;
|
||||
this.rotating = false;
|
||||
this.pitching = false;
|
||||
this.padding = false;
|
||||
|
||||
if (wasZooming) {
|
||||
this.emit('zoomend', new Event('zoomend', eventData));
|
||||
}
|
||||
if (wasRotating) {
|
||||
this.emit('rotateend', new Event('rotateend', eventData));
|
||||
}
|
||||
if (wasPitching) {
|
||||
this.emit('pitchend', new Event('pitchend', eventData));
|
||||
}
|
||||
this.emit('moveend', new Event('moveend', eventData));
|
||||
}
|
||||
|
||||
private ease(
|
||||
frame: (_: number) => void,
|
||||
finish: () => void,
|
||||
options: {
|
||||
animate: boolean;
|
||||
duration: number;
|
||||
easing: (_: number) => number;
|
||||
},
|
||||
) {
|
||||
if (options.animate === false || options.duration === 0) {
|
||||
frame(1);
|
||||
finish();
|
||||
} else {
|
||||
this.easeStart = now();
|
||||
this.easeOptions = options;
|
||||
this.onEaseFrame = frame;
|
||||
this.onEaseEnd = finish;
|
||||
this.easeFrameId = this.requestRenderFrame(this.renderFrameCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private cameraForBoxAndBearing(
|
||||
p0: LngLatLike,
|
||||
p1: LngLatLike,
|
||||
bearing: number,
|
||||
options?: ICameraOptions & {
|
||||
offset: [number, number];
|
||||
maxZoom: number;
|
||||
padding: IPaddingOptions;
|
||||
},
|
||||
): void | (ICameraOptions & IAnimationOptions) {
|
||||
const defaultPadding = {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
};
|
||||
options = merge(
|
||||
{
|
||||
padding: defaultPadding,
|
||||
offset: [0, 0],
|
||||
maxZoom: this.transform.maxZoom,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (typeof options.padding === 'number') {
|
||||
const p = options.padding;
|
||||
options.padding = {
|
||||
top: p,
|
||||
bottom: p,
|
||||
right: p,
|
||||
left: p,
|
||||
};
|
||||
}
|
||||
|
||||
options.padding = merge(defaultPadding, options.padding);
|
||||
const tr = this.transform;
|
||||
const edgePadding = tr.padding as IPaddingOptions;
|
||||
|
||||
// We want to calculate the upper right and lower left of the box defined by p0 and p1
|
||||
// in a coordinate system rotate to match the destination bearing.
|
||||
const p0world = tr.project(LngLat.convert(p0));
|
||||
const p1world = tr.project(LngLat.convert(p1));
|
||||
const p0rotated = p0world.rotate((-bearing * Math.PI) / 180);
|
||||
const p1rotated = p1world.rotate((-bearing * Math.PI) / 180);
|
||||
|
||||
const upperRight = new Point(
|
||||
Math.max(p0rotated.x, p1rotated.x),
|
||||
Math.max(p0rotated.y, p1rotated.y),
|
||||
);
|
||||
const lowerLeft = new Point(
|
||||
Math.min(p0rotated.x, p1rotated.x),
|
||||
Math.min(p0rotated.y, p1rotated.y),
|
||||
);
|
||||
|
||||
// Calculate zoom: consider the original bbox and padding.
|
||||
const size = upperRight.sub(lowerLeft);
|
||||
const scaleX =
|
||||
(tr.width -
|
||||
// @ts-ignore
|
||||
(edgePadding.left +
|
||||
// @ts-ignore
|
||||
edgePadding.right +
|
||||
// @ts-ignore
|
||||
options.padding.left +
|
||||
// @ts-ignore
|
||||
options.padding.right)) /
|
||||
size.x;
|
||||
const scaleY =
|
||||
(tr.height -
|
||||
// @ts-ignore
|
||||
(edgePadding.top +
|
||||
// @ts-ignore
|
||||
edgePadding.bottom +
|
||||
// @ts-ignore
|
||||
options.padding.top +
|
||||
// @ts-ignore
|
||||
options.padding.bottom)) /
|
||||
size.y;
|
||||
|
||||
if (scaleY < 0 || scaleX < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoom = Math.min(
|
||||
tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)),
|
||||
options.maxZoom,
|
||||
);
|
||||
|
||||
// Calculate center: apply the zoom, the configured offset, as well as offset that exists as a result of padding.
|
||||
const offset = Point.convert(options.offset);
|
||||
// @ts-ignore
|
||||
const paddingOffsetX = (options.padding.left - options.padding.right) / 2;
|
||||
// @ts-ignore
|
||||
const paddingOffsetY = (options.padding.top - options.padding.bottom) / 2;
|
||||
const offsetAtInitialZoom = new Point(
|
||||
offset.x + paddingOffsetX,
|
||||
offset.y + paddingOffsetY,
|
||||
);
|
||||
const offsetAtFinalZoom = offsetAtInitialZoom.mult(
|
||||
tr.scale / tr.zoomScale(zoom),
|
||||
);
|
||||
|
||||
const center = tr.unproject(
|
||||
p0world
|
||||
.add(p1world)
|
||||
.div(2)
|
||||
.sub(offsetAtFinalZoom),
|
||||
);
|
||||
|
||||
return {
|
||||
center,
|
||||
zoom,
|
||||
bearing,
|
||||
};
|
||||
}
|
||||
|
||||
private fitInternal(
|
||||
calculatedOptions?: ICameraOptions & IAnimationOptions,
|
||||
options?: IAnimationOptions & ICameraOptions,
|
||||
eventData?: any,
|
||||
) {
|
||||
// cameraForBounds warns + returns undefined if unable to fit:
|
||||
if (!calculatedOptions) {
|
||||
return this;
|
||||
}
|
||||
|
||||
options = merge(calculatedOptions, options);
|
||||
// Explictly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly.
|
||||
delete options.padding;
|
||||
// @ts-ignore
|
||||
return options.linear
|
||||
? this.easeTo(options, eventData)
|
||||
: this.flyTo(options, eventData);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
.l7-map {
|
||||
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.l7-canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.l7-map:-webkit-full-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.l7-canary {
|
||||
background-color: salmon;
|
||||
}
|
||||
|
||||
.l7-canvas-container.l7-interactive,
|
||||
.l7-ctrl-group button.l7-ctrl-compass {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.l7-canvas-container.l7-interactive.l7-track-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.l7-canvas-container.l7-interactive:active,
|
||||
.l7-ctrl-group button.l7-ctrl-compass:active {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.l7-canvas-container.l7-touch-zoom-rotate,
|
||||
.l7-canvas-container.l7-touch-zoom-rotate .l7-canvas {
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
.l7-canvas-container.l7-touch-drag-pan,
|
||||
.l7-canvas-container.l7-touch-drag-pan .l7-canvas {
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
|
||||
.l7-canvas-container.l7-touch-zoom-rotate.l7-touch-drag-pan,
|
||||
.l7-canvas-container.l7-touch-zoom-rotate.l7-touch-drag-pan .l7-canvas {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.l7-ctrl-top-left,
|
||||
.l7-ctrl-top-right,
|
||||
.l7-ctrl-bottom-left,
|
||||
.l7-ctrl-bottom-right { position: absolute; pointer-events: none; z-index: 2; }
|
||||
.l7-ctrl-top-left { top: 0; left: 0; }
|
||||
.l7-ctrl-top-right { top: 0; right: 0; }
|
||||
.l7-ctrl-bottom-left { bottom: 0; left: 0; }
|
||||
.l7-ctrl-bottom-right { right: 0; bottom: 0; }
|
||||
|
||||
.l7-ctrl {
|
||||
clear: both;
|
||||
pointer-events: auto;
|
||||
|
||||
/* workaround for a Safari bug https://github.com/mapbox/mapbox-gl-js/issues/8185 */
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
.l7-ctrl-top-left .l7-ctrl { margin: 10px 0 0 10px; float: left; }
|
||||
.l7-ctrl-top-right .l7-ctrl { margin: 10px 10px 0 0; float: right; }
|
||||
.l7-ctrl-bottom-left .l7-ctrl { margin: 0 0 10px 10px; float: left; }
|
||||
.l7-ctrl-bottom-right .l7-ctrl { margin: 0 10px 10px 0; float: right; }
|
||||
|
||||
|
||||
.l7-crosshair,
|
||||
.l7-crosshair .l7-interactive,
|
||||
.l7-crosshair .l7-interactive:active {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.l7-boxzoom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: #fff;
|
||||
border: 2px dotted #202020;
|
||||
opacity: 0.5;
|
||||
z-index: 10;
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// @ts-ignore
|
||||
import { clamp, interpolate } from '../util';
|
||||
import Point from './point';
|
||||
|
||||
/**
|
||||
* An `EdgeInset` object represents screen space padding applied to the edges of the viewport.
|
||||
* This shifts the apprent center or the vanishing point of the map. This is useful for adding floating UI elements
|
||||
* on top of the map and having the vanishing point shift as UI elements resize.
|
||||
*
|
||||
* @param {number} [top=0]
|
||||
* @param {number} [bottom=0]
|
||||
* @param {number} [left=0]
|
||||
* @param {number} [right=0]
|
||||
*/
|
||||
export default class EdgeInsets {
|
||||
public top: number;
|
||||
public bottom: number;
|
||||
public left: number;
|
||||
public right: number;
|
||||
|
||||
constructor(
|
||||
top: number = 0,
|
||||
bottom: number = 0,
|
||||
left: number = 0,
|
||||
right: number = 0,
|
||||
) {
|
||||
if (
|
||||
isNaN(top) ||
|
||||
top < 0 ||
|
||||
isNaN(bottom) ||
|
||||
bottom < 0 ||
|
||||
isNaN(left) ||
|
||||
left < 0 ||
|
||||
isNaN(right) ||
|
||||
right < 0
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid value for edge-insets, top, bottom, left and right must all be numbers',
|
||||
);
|
||||
}
|
||||
|
||||
this.top = top;
|
||||
this.bottom = bottom;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates the inset in-place.
|
||||
* This maintains the current inset value for any inset not present in `target`.
|
||||
*
|
||||
* @param {PaddingOptions} target
|
||||
* @param {number} t
|
||||
* @returns {EdgeInsets}
|
||||
* @memberof EdgeInsets
|
||||
*/
|
||||
public interpolate(
|
||||
start: IPaddingOptions | EdgeInsets,
|
||||
target: IPaddingOptions,
|
||||
t: number,
|
||||
): EdgeInsets {
|
||||
if (target.top != null && start.top != null) {
|
||||
this.top = interpolate(start.top, target.top, t);
|
||||
}
|
||||
if (target.bottom != null && start.bottom != null) {
|
||||
this.bottom = interpolate(start.bottom, target.bottom, t);
|
||||
}
|
||||
if (target.left != null && start.left != null) {
|
||||
this.left = interpolate(start.left, target.left, t);
|
||||
}
|
||||
if (target.right != null && start.right != null) {
|
||||
this.right = interpolate(start.right, target.right, t);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that computes the new apprent center or vanishing point after applying insets.
|
||||
* This is in pixels and with the top left being (0.0) and +y being downwards.
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @returns {Point}
|
||||
* @memberof EdgeInsets
|
||||
*/
|
||||
public getCenter(width: number, height: number): Point {
|
||||
// Clamp insets so they never overflow width/height and always calculate a valid center
|
||||
const x = clamp((this.left + width - this.right) / 2, 0, width);
|
||||
const y = clamp((this.top + height - this.bottom) / 2, 0, height);
|
||||
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
public equals(other: IPaddingOptions): boolean {
|
||||
return (
|
||||
this.top === other.top &&
|
||||
this.bottom === other.bottom &&
|
||||
this.left === other.left &&
|
||||
this.right === other.right
|
||||
);
|
||||
}
|
||||
|
||||
public clone(): EdgeInsets {
|
||||
return new EdgeInsets(this.top, this.bottom, this.left, this.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current sdtate as json, useful when you want to have a
|
||||
* read-only representation of the inset.
|
||||
*
|
||||
* @returns {PaddingOptions}
|
||||
* @memberof EdgeInsets
|
||||
*/
|
||||
public toJSON(): IPaddingOptions {
|
||||
return {
|
||||
top: this.top,
|
||||
bottom: this.bottom,
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPaddingOptions {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
right?: number;
|
||||
left?: number;
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import { wrap } from '../util';
|
||||
import LngLatBounds from './lng_lat_bounds';
|
||||
export const earthRadius = 6371008.8;
|
||||
export type LngLatLike =
|
||||
| LngLat
|
||||
| { lng: number; lat: number }
|
||||
| { lon: number; lat: number }
|
||||
| [number, number];
|
||||
|
||||
export default class LngLat {
|
||||
public static convert(input: LngLatLike): LngLat {
|
||||
if (input instanceof LngLat) {
|
||||
return input;
|
||||
}
|
||||
if (Array.isArray(input) && (input.length === 2 || input.length === 3)) {
|
||||
return new LngLat(Number(input[0]), Number(input[1]));
|
||||
}
|
||||
if (!Array.isArray(input) && typeof input === 'object' && input !== null) {
|
||||
const lng = 'lng' in input ? input.lng : input.lon;
|
||||
return new LngLat(
|
||||
// flow can't refine this to have one of lng or lat, so we have to cast to any
|
||||
Number(lng),
|
||||
Number(input.lat),
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'`LngLatLike` argument must be specified as a LngLat instance, an object {lng: <lng>, lat: <lat>}, an object {lon: <lng>, lat: <lat>}, or an array of [<lng>, <lat>]',
|
||||
);
|
||||
}
|
||||
public lng: number;
|
||||
public lat: number;
|
||||
constructor(lng: number, lat: number) {
|
||||
if (isNaN(lng) || isNaN(lat)) {
|
||||
throw new Error(`Invalid LngLat object: (${lng}, ${lat})`);
|
||||
}
|
||||
this.lng = +lng;
|
||||
this.lat = +lat;
|
||||
if (this.lat > 90 || this.lat < -90) {
|
||||
throw new Error(
|
||||
'Invalid LngLat latitude value: must be between -90 and 90',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public wrap() {
|
||||
return new LngLat(wrap(this.lng, -180, 180), this.lat);
|
||||
}
|
||||
public toArray(): [number, number] {
|
||||
return [this.lng, this.lat];
|
||||
}
|
||||
public toBounds(radius: number = 0) {
|
||||
const earthCircumferenceInMetersAtEquator = 40075017;
|
||||
const latAccuracy = (360 * radius) / earthCircumferenceInMetersAtEquator;
|
||||
const lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
|
||||
|
||||
return new LngLatBounds(
|
||||
new LngLat(this.lng - lngAccuracy, this.lat - latAccuracy),
|
||||
new LngLat(this.lng + lngAccuracy, this.lat + latAccuracy),
|
||||
);
|
||||
}
|
||||
public toString() {
|
||||
return `LngLat(${this.lng}, ${this.lat})`;
|
||||
}
|
||||
public distanceTo(lngLat: LngLat) {
|
||||
const rad = Math.PI / 180;
|
||||
const lat1 = this.lat * rad;
|
||||
const lat2 = lngLat.lat * rad;
|
||||
const a =
|
||||
Math.sin(lat1) * Math.sin(lat2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.cos((lngLat.lng - this.lng) * rad);
|
||||
|
||||
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
|
||||
return maxMeters;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import LngLat, { LngLatLike } from './lng_lat';
|
||||
export type LngLatBoundsLike =
|
||||
| LngLatBounds
|
||||
| [LngLatLike, LngLatLike]
|
||||
| [number, number, number, number];
|
||||
export default class LngLatBounds {
|
||||
public static convert(input: LngLatBoundsLike): LngLatBounds {
|
||||
if (input instanceof LngLatBounds) {
|
||||
return input;
|
||||
}
|
||||
return new LngLatBounds(input);
|
||||
}
|
||||
private ne: LngLat;
|
||||
private sw: LngLat;
|
||||
constructor(sw?: any, ne?: any) {
|
||||
if (!sw) {
|
||||
// noop
|
||||
} else if (ne) {
|
||||
this.setSouthWest(sw).setNorthEast(ne);
|
||||
} else if (sw.length === 4) {
|
||||
this.setSouthWest([sw[0], sw[1]]).setNorthEast([sw[2], sw[3]]);
|
||||
} else {
|
||||
this.setSouthWest(sw[0]).setNorthEast(sw[1]);
|
||||
}
|
||||
}
|
||||
|
||||
public setNorthEast(ne: LngLatLike) {
|
||||
this.ne =
|
||||
ne instanceof LngLat ? new LngLat(ne.lng, ne.lat) : LngLat.convert(ne);
|
||||
return this;
|
||||
}
|
||||
public setSouthWest(sw: LngLatLike) {
|
||||
this.sw =
|
||||
sw instanceof LngLat ? new LngLat(sw.lng, sw.lat) : LngLat.convert(sw);
|
||||
return this;
|
||||
}
|
||||
|
||||
public extend(obj: LngLatLike | LngLatBoundsLike): this {
|
||||
const sw = this.sw;
|
||||
const ne = this.ne;
|
||||
let sw2: any;
|
||||
let ne2: any;
|
||||
|
||||
if (obj instanceof LngLat) {
|
||||
sw2 = obj;
|
||||
ne2 = obj;
|
||||
} else if (obj instanceof LngLatBounds) {
|
||||
sw2 = obj.sw;
|
||||
ne2 = obj.ne;
|
||||
|
||||
if (!sw2 || !ne2) {
|
||||
return this;
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 4 || obj.every(Array.isArray)) {
|
||||
const lngLatBoundsObj = obj as LngLatBoundsLike;
|
||||
return this.extend(LngLatBounds.convert(lngLatBoundsObj));
|
||||
} else {
|
||||
const lngLatObj = obj as LngLatLike;
|
||||
return this.extend(LngLat.convert(lngLatObj));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!sw && !ne) {
|
||||
this.sw = new LngLat(sw2.lng, sw2.lat);
|
||||
this.ne = new LngLat(ne2.lng, ne2.lat);
|
||||
} else {
|
||||
sw.lng = Math.min(sw2.lng, sw.lng);
|
||||
sw.lat = Math.min(sw2.lat, sw.lat);
|
||||
ne.lng = Math.max(ne2.lng, ne.lng);
|
||||
ne.lat = Math.max(ne2.lat, ne.lat);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
public getCenter(): LngLat {
|
||||
return new LngLat(
|
||||
(this.sw.lng + this.ne.lng) / 2,
|
||||
(this.sw.lat + this.ne.lat) / 2,
|
||||
);
|
||||
}
|
||||
|
||||
public getSouthWest(): LngLat {
|
||||
return this.sw;
|
||||
}
|
||||
|
||||
public getNorthEast(): LngLat {
|
||||
return this.ne;
|
||||
}
|
||||
|
||||
public getNorthWest(): LngLat {
|
||||
return new LngLat(this.getWest(), this.getNorth());
|
||||
}
|
||||
|
||||
public getSouthEast(): LngLat {
|
||||
return new LngLat(this.getEast(), this.getSouth());
|
||||
}
|
||||
|
||||
public getWest(): number {
|
||||
return this.sw.lng;
|
||||
}
|
||||
|
||||
public getSouth(): number {
|
||||
return this.sw.lat;
|
||||
}
|
||||
|
||||
public getEast(): number {
|
||||
return this.ne.lng;
|
||||
}
|
||||
|
||||
public getNorth(): number {
|
||||
return this.ne.lat;
|
||||
}
|
||||
|
||||
public toArray(): [[number, number], [number, number]] {
|
||||
return [this.sw.toArray(), this.ne.toArray()];
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `LngLatBounds(${this.sw.toString()}, ${this.ne.toString()})`;
|
||||
}
|
||||
|
||||
public isEmpty() {
|
||||
return !(this.sw && this.ne);
|
||||
}
|
||||
|
||||
public contains(lnglat: LngLatLike) {
|
||||
const { lng, lat } = LngLat.convert(lnglat);
|
||||
|
||||
const containsLatitude = this.sw.lat <= lat && lat <= this.ne.lat;
|
||||
let containsLongitude = this.sw.lng <= lng && lng <= this.ne.lng;
|
||||
if (this.sw.lng > this.ne.lng) {
|
||||
// wrapped coordinates
|
||||
containsLongitude = this.sw.lng >= lng && lng >= this.ne.lng;
|
||||
}
|
||||
|
||||
return containsLatitude && containsLongitude;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import LngLat, { earthRadius, LngLatLike } from '../geo/lng_lat';
|
||||
|
||||
/*
|
||||
* The average circumference of the world in meters.
|
||||
*/
|
||||
const earthCircumfrence = 2 * Math.PI * earthRadius; // meters
|
||||
|
||||
/*
|
||||
* The circumference at a line of latitude in meters.
|
||||
*/
|
||||
function circumferenceAtLatitude(latitude: number) {
|
||||
return earthCircumfrence * Math.cos((latitude * Math.PI) / 180);
|
||||
}
|
||||
|
||||
export function mercatorXfromLng(lng: number) {
|
||||
return (180 + lng) / 360;
|
||||
}
|
||||
|
||||
export function mercatorYfromLat(lat: number) {
|
||||
return (
|
||||
(180 -
|
||||
(180 / Math.PI) *
|
||||
Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360))) /
|
||||
360
|
||||
);
|
||||
}
|
||||
|
||||
export function mercatorZfromAltitude(altitude: number, lat: number) {
|
||||
return altitude / circumferenceAtLatitude(lat);
|
||||
}
|
||||
|
||||
export function lngFromMercatorX(x: number) {
|
||||
return x * 360 - 180;
|
||||
}
|
||||
|
||||
export function latFromMercatorY(y: number) {
|
||||
const y2 = 180 - y * 360;
|
||||
return (360 / Math.PI) * Math.atan(Math.exp((y2 * Math.PI) / 180)) - 90;
|
||||
}
|
||||
|
||||
export function altitudeFromMercatorZ(z: number, y: number) {
|
||||
return z * circumferenceAtLatitude(latFromMercatorY(y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the Mercator scale factor for a given latitude, see
|
||||
* https://en.wikipedia.org/wiki/Mercator_projection#Scale_factor
|
||||
*
|
||||
* At the equator the scale factor will be 1, which increases at higher latitudes.
|
||||
*
|
||||
* @param {number} lat Latitude
|
||||
* @returns {number} scale factor
|
||||
* @private
|
||||
*/
|
||||
export function mercatorScale(lat: number) {
|
||||
return 1 / Math.cos((lat * Math.PI) / 180);
|
||||
}
|
||||
|
||||
export default class MercatorCoordinate {
|
||||
public static fromLngLat(lngLatLike: LngLatLike, altitude: number = 0) {
|
||||
const lngLat = LngLat.convert(lngLatLike);
|
||||
|
||||
return new MercatorCoordinate(
|
||||
mercatorXfromLng(lngLat.lng),
|
||||
mercatorYfromLat(lngLat.lat),
|
||||
mercatorZfromAltitude(altitude, lngLat.lat),
|
||||
);
|
||||
}
|
||||
public x: number;
|
||||
public y: number;
|
||||
public z: number;
|
||||
|
||||
constructor(x: number, y: number, z: number = 0) {
|
||||
this.x = +x;
|
||||
this.y = +y;
|
||||
this.z = +z;
|
||||
}
|
||||
|
||||
public toLngLat() {
|
||||
return new LngLat(lngFromMercatorX(this.x), latFromMercatorY(this.y));
|
||||
}
|
||||
|
||||
public toAltitude() {
|
||||
return altitudeFromMercatorZ(this.z, this.y);
|
||||
}
|
||||
|
||||
public meterInMercatorCoordinateUnits() {
|
||||
// 1 meter / circumference at equator in meters * Mercator projection scale factor at this latitude
|
||||
return (1 / earthCircumfrence) * mercatorScale(latFromMercatorY(this.y));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
export type PointLike = [number, number] | Point;
|
||||
|
||||
export default class Point {
|
||||
public static convert(a: any) {
|
||||
if (a instanceof Point) {
|
||||
return a;
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
return new Point(a[0], a[1]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
public x: number;
|
||||
public y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public clone() {
|
||||
return new Point(this.x, this.y);
|
||||
}
|
||||
|
||||
public _add(p: Point) {
|
||||
this.x += p.x;
|
||||
this.y += p.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public add(p: Point) {
|
||||
return this.clone()._add(p);
|
||||
}
|
||||
|
||||
public _sub(p: Point) {
|
||||
this.x -= p.x;
|
||||
this.y -= p.y;
|
||||
return this;
|
||||
}
|
||||
public sub(p: Point) {
|
||||
return this.clone()._sub(p);
|
||||
}
|
||||
|
||||
public _multByPoint(p: Point) {
|
||||
this.x *= p.x;
|
||||
this.y *= p.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public multByPoint(p: Point) {
|
||||
return this.clone()._multByPoint(p);
|
||||
}
|
||||
|
||||
public _divByPoint(p: Point) {
|
||||
this.x /= p.x;
|
||||
this.y /= p.y;
|
||||
return this;
|
||||
}
|
||||
public divByPoint(p: Point) {
|
||||
return this.clone()._divByPoint(p);
|
||||
}
|
||||
|
||||
public _mult(k: number) {
|
||||
this.x *= k;
|
||||
this.y *= k;
|
||||
return this;
|
||||
}
|
||||
|
||||
public mult(k: number) {
|
||||
return this.clone()._mult(k);
|
||||
}
|
||||
|
||||
public _div(k: number) {
|
||||
this.x /= k;
|
||||
this.y /= k;
|
||||
return this;
|
||||
}
|
||||
|
||||
public div(k: number) {
|
||||
return this.clone()._div(k);
|
||||
}
|
||||
|
||||
public _rotate(angle: number) {
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
const x = cos * this.x - sin * this.y;
|
||||
const y = sin * this.x + cos * this.y;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public rotate(angle: number) {
|
||||
return this.clone()._rotate(angle);
|
||||
}
|
||||
|
||||
public _rotateAround(angle: number, p: Point) {
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
const x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y);
|
||||
const y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
public roateAround(angle: number, p: Point) {
|
||||
return this.clone()._rotateAround(angle, p);
|
||||
}
|
||||
|
||||
public _matMult(m: number[]) {
|
||||
const x = m[0] * this.x + m[1] * this.y;
|
||||
const y = m[2] * this.x + m[3] * this.y;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public matMult(m: number[]) {
|
||||
return this.clone()._matMult(m);
|
||||
}
|
||||
|
||||
public _unit() {
|
||||
this.div(this.mag());
|
||||
return this;
|
||||
}
|
||||
public unit() {
|
||||
return this.clone()._unit();
|
||||
}
|
||||
|
||||
public _perp() {
|
||||
const y = this.y;
|
||||
this.y = this.x;
|
||||
this.x = -y;
|
||||
return this;
|
||||
}
|
||||
public perp() {
|
||||
return this.clone()._perp();
|
||||
}
|
||||
|
||||
public _round() {
|
||||
this.x = Math.round(this.x);
|
||||
this.y = Math.round(this.y);
|
||||
return this;
|
||||
}
|
||||
|
||||
public round() {
|
||||
return this.clone()._round();
|
||||
}
|
||||
|
||||
public mag() {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}
|
||||
|
||||
public equals(other: Point) {
|
||||
return this.x === other.x && this.y === other.y;
|
||||
}
|
||||
|
||||
public dist(p: Point) {
|
||||
return Math.sqrt(this.distSqr(p));
|
||||
}
|
||||
|
||||
public distSqr(p: Point) {
|
||||
const dx = p.x - this.x;
|
||||
const dy = p.y - this.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
public angle() {
|
||||
return Math.atan2(this.y, this.x);
|
||||
}
|
||||
|
||||
public angleTo(b: Point) {
|
||||
return Math.atan2(this.y - b.y, this.x - b.x);
|
||||
}
|
||||
|
||||
public angleWith(b: Point) {
|
||||
return this.angleWithSep(b.x, b.y);
|
||||
}
|
||||
|
||||
public angleWithSep(x: number, y: number) {
|
||||
return Math.atan2(this.x * y - this.y * x, this.x * x + this.y * y);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import LngLat, { earthRadius, LngLatLike } from '../geo/lng_lat';
|
||||
|
||||
/*
|
||||
* The average circumference of the world in meters.
|
||||
*/
|
||||
const earthCircumfrence = 2 * Math.PI * earthRadius; // meters
|
||||
|
||||
/*
|
||||
* The circumference at a line of latitude in meters.
|
||||
*/
|
||||
function circumferenceAtLatitude(latitude: number) {
|
||||
return earthCircumfrence * Math.cos((latitude * Math.PI) / 180);
|
||||
}
|
||||
|
||||
export function mercatorXfromLng(lng: number) {
|
||||
return lng;
|
||||
}
|
||||
|
||||
export function mercatorYfromLat(lat: number) {
|
||||
return lat;
|
||||
}
|
||||
|
||||
export function mercatorZfromAltitude(altitude: number, lat: number) {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
export function lngFromMercatorX(x: number) {
|
||||
return x;
|
||||
}
|
||||
|
||||
export function latFromMercatorY(y: number) {
|
||||
return y;
|
||||
}
|
||||
|
||||
export function altitudeFromMercatorZ(z: number, y: number) {
|
||||
return z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the Mercator scale factor for a given latitude, see
|
||||
* https://en.wikipedia.org/wiki/Mercator_projection#Scale_factor
|
||||
*
|
||||
* At the equator the scale factor will be 1, which increases at higher latitudes.
|
||||
*
|
||||
* @param {number} lat Latitude
|
||||
* @returns {number} scale factor
|
||||
* @private
|
||||
*/
|
||||
export function mercatorScale(lat: number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
export default class SimpleCoordinate {
|
||||
public static fromLngLat(lngLatLike: LngLatLike, altitude: number = 0) {
|
||||
const lngLat = LngLat.convert(lngLatLike);
|
||||
|
||||
return new SimpleCoordinate(
|
||||
mercatorXfromLng(lngLat.lng),
|
||||
mercatorYfromLat(lngLat.lat),
|
||||
mercatorZfromAltitude(altitude, lngLat.lat),
|
||||
);
|
||||
}
|
||||
public x: number;
|
||||
public y: number;
|
||||
public z: number;
|
||||
|
||||
constructor(x: number, y: number, z: number = 0) {
|
||||
this.x = +x;
|
||||
this.y = +y;
|
||||
this.z = +z;
|
||||
}
|
||||
public toLngLat() {
|
||||
return new LngLat(this.x, this.y);
|
||||
}
|
||||
|
||||
public toAltitude() {
|
||||
return this.z;
|
||||
}
|
||||
|
||||
public meterInMercatorCoordinateUnits() {
|
||||
// 1 meter / circumference at equator in meters * Mercator projection scale factor at this latitude
|
||||
return 1;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,59 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
export interface IHandlerResult {
|
||||
panDelta?: Point;
|
||||
zoomDelta?: number;
|
||||
bearingDelta?: number;
|
||||
pitchDelta?: number;
|
||||
around?: Point | null;
|
||||
pinchAround?: Point | null;
|
||||
cameraAnimation?: (map: Map) => any;
|
||||
originalEvent?: any;
|
||||
// Makes the manager trigger a frame; allowing the handler to return multiple results over time (see scrollzoom).
|
||||
needsRenderFrame?: boolean;
|
||||
noInertia?: boolean;
|
||||
}
|
||||
|
||||
export interface IHandler {
|
||||
// Handlers can optionally implement these methods.
|
||||
// They are called with dom events whenever those dom evens are received.
|
||||
touchstart?: (
|
||||
e: TouchEvent,
|
||||
points: Point[],
|
||||
mapTouches: Touch[],
|
||||
) => IHandlerResult | void;
|
||||
touchmove?: (
|
||||
e: TouchEvent,
|
||||
points: Point[],
|
||||
mapTouches: Touch[],
|
||||
) => IHandlerResult | void;
|
||||
touchend?: (
|
||||
e: TouchEvent,
|
||||
points: Point[],
|
||||
mapTouches: Touch[],
|
||||
) => IHandlerResult | void;
|
||||
touchcancel?: (
|
||||
e: TouchEvent,
|
||||
points: Point[],
|
||||
mapTouches: Touch[],
|
||||
) => IHandlerResult | void;
|
||||
mousedown?: (e: MouseEvent, point: Point) => IHandlerResult | void;
|
||||
mousemove?: (e: MouseEvent, point: Point) => IHandlerResult | void;
|
||||
mouseup?: (e: MouseEvent, point: Point) => IHandlerResult | void;
|
||||
dblclick?: (e: MouseEvent, point: Point) => IHandlerResult | void;
|
||||
wheel?: (e: WheelEvent, point: Point) => IHandlerResult | void;
|
||||
keydown?: (e: KeyboardEvent) => IHandlerResult | void;
|
||||
keyup?: (e: KeyboardEvent) => IHandlerResult | void;
|
||||
|
||||
// `renderFrame` is the only non-dom event. It is called during render
|
||||
// frames and can be used to smooth camera changes (see scroll handler).
|
||||
renderFrame?: () => IHandlerResult | void;
|
||||
enable(options?: any): void;
|
||||
disable(): void;
|
||||
isEnabled(): boolean;
|
||||
isActive(): boolean;
|
||||
|
||||
// `reset` can be called by the manager at any time and must reset everything to it's original state
|
||||
reset(): void;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
import { MapMouseEvent, MapTouchEvent, MapWheelEvent } from './events';
|
||||
export default class BlockableMapEventHandler {
|
||||
private map: Map;
|
||||
private delayContextMenu: boolean;
|
||||
private contextMenuEvent: MouseEvent;
|
||||
|
||||
constructor(map: Map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.delayContextMenu = false;
|
||||
delete this.contextMenuEvent;
|
||||
}
|
||||
|
||||
public mousemove(e: MouseEvent) {
|
||||
// mousemove map events should not be fired when interaction handlers (pan, rotate, etc) are active
|
||||
this.map.emit(e.type, new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public mousedown() {
|
||||
this.delayContextMenu = true;
|
||||
}
|
||||
|
||||
public mouseup() {
|
||||
this.delayContextMenu = false;
|
||||
if (this.contextMenuEvent) {
|
||||
this.map.emit(
|
||||
'contextmenu',
|
||||
new MapMouseEvent('contextmenu', this.map, this.contextMenuEvent),
|
||||
);
|
||||
delete this.contextMenuEvent;
|
||||
}
|
||||
}
|
||||
public contextmenu(e: MouseEvent) {
|
||||
if (this.delayContextMenu) {
|
||||
// Mac: contextmenu fired on mousedown; we save it until mouseup for consistency's sake
|
||||
this.contextMenuEvent = e;
|
||||
} else {
|
||||
// Windows: contextmenu fired on mouseup, so fire event now
|
||||
this.map.emit(e.type, new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
// prevent browser context menu when necessary
|
||||
if (this.map.listeners('contextmenu')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return false;
|
||||
}
|
||||
public enable() {
|
||||
return true;
|
||||
}
|
||||
public disable() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
import DOM from '../utils/dom';
|
||||
import { Event } from './events/event';
|
||||
|
||||
/**
|
||||
* The `BoxZoomHandler` allows the user to zoom the map to fit within a bounding box.
|
||||
* The bounding box is defined by clicking and holding `shift` while dragging the cursor.
|
||||
*/
|
||||
class BoxZoomHandler {
|
||||
private map: Map;
|
||||
private el: HTMLElement;
|
||||
private container: HTMLElement;
|
||||
private enabled: boolean;
|
||||
private active: boolean;
|
||||
private startPos: Point;
|
||||
private lastPos: Point;
|
||||
private box: HTMLElement | null;
|
||||
private clickTolerance: number;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(
|
||||
map: Map,
|
||||
options: {
|
||||
clickTolerance: number;
|
||||
},
|
||||
) {
|
||||
this.map = map;
|
||||
this.el = map.getCanvasContainer();
|
||||
this.container = map.getContainer();
|
||||
this.clickTolerance = options.clickTolerance || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "box zoom" interaction is enabled.
|
||||
*
|
||||
* @returns {boolean} `true` if the "box zoom" interaction is enabled.
|
||||
*/
|
||||
public isEnabled() {
|
||||
return !!this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "box zoom" interaction is active, i.e. currently being used.
|
||||
*
|
||||
* @returns {boolean} `true` if the "box zoom" interaction is active.
|
||||
*/
|
||||
public isActive() {
|
||||
return !!this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "box zoom" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.boxZoom.enable();
|
||||
*/
|
||||
public enable() {
|
||||
if (this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "box zoom" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.boxZoom.disable();
|
||||
*/
|
||||
public disable() {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
public mousedown(e: MouseEvent, point: Point) {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (!(e.shiftKey && e.button === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DOM.disableDrag();
|
||||
this.startPos = this.lastPos = point;
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
public mousemoveWindow(e: MouseEvent, point: Point) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = point;
|
||||
|
||||
if (
|
||||
this.lastPos.equals(pos) ||
|
||||
(!this.box && pos.dist(this.startPos) < this.clickTolerance)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p0 = this.startPos;
|
||||
this.lastPos = pos;
|
||||
|
||||
if (!this.box) {
|
||||
this.box = DOM.create('div', 'l7-boxzoom', this.container);
|
||||
this.container.classList.add('l7-crosshair');
|
||||
this.fireEvent('boxzoomstart', e);
|
||||
}
|
||||
const minX = Math.min(p0.x, pos.x);
|
||||
const maxX = Math.max(p0.x, pos.x);
|
||||
const minY = Math.min(p0.y, pos.y);
|
||||
const maxY = Math.max(p0.y, pos.y);
|
||||
|
||||
DOM.setTransform(this.box, `translate(${minX}px,${minY}px)`);
|
||||
if (this.box) {
|
||||
this.box.style.width = `${maxX - minX}px`;
|
||||
this.box.style.height = `${maxY - minY}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public mouseupWindow(e: MouseEvent, point: Point) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p0 = this.startPos;
|
||||
const p1 = point;
|
||||
|
||||
this.reset();
|
||||
|
||||
DOM.suppressClick();
|
||||
|
||||
if (p0.x === p1.x && p0.y === p1.y) {
|
||||
this.fireEvent('boxzoomcancel', e);
|
||||
} else {
|
||||
this.map.emit(
|
||||
'boxzoomend',
|
||||
new Event('boxzoomend', { originalEvent: e }),
|
||||
);
|
||||
return {
|
||||
cameraAnimation: (map: Map) =>
|
||||
map.fitScreenCoordinates(p0, p1, this.map.getBearing(), {
|
||||
linear: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public keydown(e: KeyboardEvent) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
this.reset();
|
||||
this.fireEvent('boxzoomcancel', e);
|
||||
}
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
|
||||
this.container.classList.remove('l7-crosshair');
|
||||
|
||||
if (this.box) {
|
||||
DOM.remove(this.box);
|
||||
this.box = null;
|
||||
}
|
||||
|
||||
DOM.enableDrag();
|
||||
|
||||
delete this.startPos;
|
||||
delete this.lastPos;
|
||||
}
|
||||
|
||||
public fireEvent(type: string, e: any) {
|
||||
return this.map.emit(type, new Event(type, { originalEvent: e }));
|
||||
}
|
||||
}
|
||||
|
||||
export default BoxZoomHandler;
|
|
@ -0,0 +1,48 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
|
||||
export default class ClickZoomHandler {
|
||||
private enabled: boolean;
|
||||
private active: boolean;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
public dblclick(e: MouseEvent, point: Point) {
|
||||
e.preventDefault();
|
||||
return {
|
||||
cameraAnimation: (map: Map) => {
|
||||
map.easeTo(
|
||||
{
|
||||
duration: 300,
|
||||
zoom: map.getZoom() + (e.shiftKey ? -1 : 1),
|
||||
around: map.unproject(point),
|
||||
},
|
||||
{ originalEvent: e },
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// tslint:disable-next-line:no-submodule-imports
|
||||
import merge from 'lodash/merge';
|
||||
export class Event {
|
||||
public type: string;
|
||||
constructor(type: string, data: any = {}) {
|
||||
merge(this, data);
|
||||
this.type = type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import MapMouseEvent from './map_mouse_event';
|
||||
import MapTouchEvent from './map_touch_event';
|
||||
import MapWheelEvent, { IMapBoxZoomEvent } from './map_wheel_event';
|
||||
|
||||
export { MapMouseEvent, MapTouchEvent, MapWheelEvent };
|
|
@ -0,0 +1,67 @@
|
|||
// @ts-ignore
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
import merge from 'lodash/merge';
|
||||
import LngLat from '../../geo/lng_lat';
|
||||
import Point from '../../geo/point';
|
||||
import { Map } from '../../map';
|
||||
import DOM from '../../utils/dom';
|
||||
import { Event } from './event';
|
||||
export default class MapMouseEvent extends Event {
|
||||
/**
|
||||
* `true` if `preventDefault` has been called.
|
||||
* @private
|
||||
*/
|
||||
|
||||
public type:
|
||||
| 'mousedown'
|
||||
| 'mouseup'
|
||||
| 'click'
|
||||
| 'dblclick'
|
||||
| 'mousemove'
|
||||
| 'mouseover'
|
||||
| 'mouseenter'
|
||||
| 'mouseleave'
|
||||
| 'mouseout'
|
||||
| 'contextmenu';
|
||||
|
||||
/**
|
||||
* The `Map` object that fired the event.
|
||||
*/
|
||||
public target: Map;
|
||||
|
||||
/**
|
||||
* The DOM event which caused the map event.
|
||||
*/
|
||||
public originalEvent: MouseEvent;
|
||||
|
||||
/**
|
||||
* The pixel coordinates of the mouse cursor, relative to the map and measured from the top left corner.
|
||||
*/
|
||||
public point: Point;
|
||||
|
||||
/**
|
||||
* The geographic location on the map of the mouse cursor.
|
||||
*/
|
||||
public lngLat: LngLat;
|
||||
|
||||
public defaultPrevented: boolean;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(
|
||||
type: string,
|
||||
map: Map,
|
||||
originalEvent: MouseEvent,
|
||||
data: any = {},
|
||||
) {
|
||||
const point = DOM.mousePos(map.getCanvasContainer(), originalEvent);
|
||||
const lngLat = map.unproject(point);
|
||||
super(type, merge({ point, lngLat, originalEvent }, data));
|
||||
this.defaultPrevented = false;
|
||||
this.target = map;
|
||||
}
|
||||
public preventDefault() {
|
||||
this.defaultPrevented = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// @ts-ignore
|
||||
import LngLat from '../../geo/lng_lat';
|
||||
import Point from '../../geo/point';
|
||||
import { Map } from '../../map';
|
||||
import DOM from '../../utils/dom';
|
||||
import { Event } from './event';
|
||||
export default class MapTouchEvent extends Event {
|
||||
/**
|
||||
* The event type.
|
||||
*/
|
||||
public type: 'touchstart' | 'touchend' | 'touchcancel';
|
||||
|
||||
/**
|
||||
* The `Map` object that fired the event.
|
||||
*/
|
||||
public target: Map;
|
||||
|
||||
/**
|
||||
* The DOM event which caused the map event.
|
||||
*/
|
||||
public originalEvent: TouchEvent;
|
||||
|
||||
/**
|
||||
* The geographic location on the map of the center of the touch event points.
|
||||
*/
|
||||
public lngLat: LngLat;
|
||||
|
||||
/**
|
||||
* The pixel coordinates of the center of the touch event points, relative to the map and measured from the top left
|
||||
* corner.
|
||||
*/
|
||||
public point: Point;
|
||||
|
||||
/**
|
||||
* The array of pixel coordinates corresponding to a
|
||||
* [touch event's `touches`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/touches) property.
|
||||
*/
|
||||
public points: Point[];
|
||||
|
||||
/**
|
||||
* The geographical locations on the map corresponding to a
|
||||
* [touch event's `touches`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/touches) property.
|
||||
*/
|
||||
public lngLats: LngLat[];
|
||||
|
||||
/**
|
||||
* `true` if `preventDefault` has been called.
|
||||
* @private
|
||||
*/
|
||||
|
||||
public defaultPrevented: boolean;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(type: string, map: Map, originalEvent: TouchEvent) {
|
||||
const touches =
|
||||
type === 'touchend'
|
||||
? originalEvent.changedTouches
|
||||
: originalEvent.touches;
|
||||
const points = DOM.touchPos(map.getCanvasContainer(), touches);
|
||||
const lngLats = points.map((t: Point) => map.unproject(t));
|
||||
const point = points.reduce(
|
||||
(prev: Point, curr: Point, i: number, arr: Point[]) => {
|
||||
return prev.add(curr.div(arr.length));
|
||||
},
|
||||
new Point(0, 0),
|
||||
);
|
||||
const lngLat = map.unproject(point);
|
||||
super(type, { points, point, lngLats, lngLat, originalEvent });
|
||||
this.defaultPrevented = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents subsequent default processing of the event by the map.
|
||||
*
|
||||
* Calling this method will prevent the following default map behaviors:
|
||||
*
|
||||
* * On `touchstart` events, the behavior of {@link DragPanHandler}
|
||||
* * On `touchstart` events, the behavior of {@link TouchZoomRotateHandler}
|
||||
*
|
||||
*/
|
||||
private preventDefault() {
|
||||
this.defaultPrevented = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { Map } from '../../map';
|
||||
import { Event } from './event';
|
||||
|
||||
export interface IMapBoxZoomEvent {
|
||||
type: 'boxzoomstart' | 'boxzoomend' | 'boxzoomcancel';
|
||||
target: Map;
|
||||
originalEvent: MouseEvent;
|
||||
}
|
||||
export default class MapWheelEvent extends Event {
|
||||
/**
|
||||
* The event type.
|
||||
*/
|
||||
public type: 'wheel';
|
||||
|
||||
/**
|
||||
* The DOM event which caused the map event.
|
||||
*/
|
||||
public originalEvent: WheelEvent;
|
||||
|
||||
public defaultPrevented: boolean;
|
||||
|
||||
/**
|
||||
* The `Map` object that fired the event.
|
||||
*/
|
||||
public target: Map;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(type: string, map: Map, originalEvent: WheelEvent) {
|
||||
super(type, { originalEvent });
|
||||
this.defaultPrevented = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents subsequent default processing of the event by the map.
|
||||
*
|
||||
* Calling this method will prevent the the behavior of {@link ScrollZoomHandler}.
|
||||
*/
|
||||
private preventDefault() {
|
||||
this.defaultPrevented = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { Event } from './event';
|
||||
export default class RenderFrameEvent extends Event {
|
||||
public type: 'renderFrame' = 'renderFrame';
|
||||
public timeStamp: number;
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
import merge from 'lodash/merge';
|
||||
import { Map } from '../map';
|
||||
import { bezier, clamp, now } from '../util';
|
||||
import { IDragPanOptions } from './shim/drag_pan';
|
||||
|
||||
const defaultInertiaOptions = {
|
||||
linearity: 0.3,
|
||||
easing: bezier(0, 0, 0.3, 1),
|
||||
};
|
||||
|
||||
const defaultPanInertiaOptions = merge(
|
||||
{
|
||||
deceleration: 2500,
|
||||
maxSpeed: 1400,
|
||||
},
|
||||
defaultInertiaOptions,
|
||||
);
|
||||
|
||||
const defaultZoomInertiaOptions = merge(
|
||||
{
|
||||
deceleration: 20,
|
||||
maxSpeed: 1400,
|
||||
},
|
||||
defaultInertiaOptions,
|
||||
);
|
||||
|
||||
const defaultBearingInertiaOptions = merge(
|
||||
{
|
||||
deceleration: 1000,
|
||||
maxSpeed: 360,
|
||||
},
|
||||
defaultInertiaOptions,
|
||||
);
|
||||
|
||||
const defaultPitchInertiaOptions = merge(
|
||||
{
|
||||
deceleration: 1000,
|
||||
maxSpeed: 90,
|
||||
},
|
||||
defaultInertiaOptions,
|
||||
);
|
||||
|
||||
export interface IInertiaOptions {
|
||||
linearity: number;
|
||||
easing: (t: number) => number;
|
||||
deceleration: number;
|
||||
maxSpeed: number;
|
||||
}
|
||||
|
||||
export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent;
|
||||
|
||||
export default class HandlerInertia {
|
||||
private map: Map;
|
||||
private inertiaBuffer: Array<{
|
||||
time: number;
|
||||
settings: { [key: string]: any };
|
||||
}>;
|
||||
|
||||
constructor(map: Map) {
|
||||
this.map = map;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.inertiaBuffer = [];
|
||||
}
|
||||
|
||||
public record(settings: any) {
|
||||
this.drainInertiaBuffer();
|
||||
this.inertiaBuffer.push({ time: now(), settings });
|
||||
}
|
||||
|
||||
public drainInertiaBuffer() {
|
||||
const inertia = this.inertiaBuffer;
|
||||
const nowTime = now();
|
||||
const cutoff = 160; // msec
|
||||
|
||||
while (inertia.length > 0 && nowTime - inertia[0].time > cutoff) {
|
||||
inertia.shift();
|
||||
}
|
||||
}
|
||||
|
||||
public onMoveEnd(panInertiaOptions?: IDragPanOptions) {
|
||||
this.drainInertiaBuffer();
|
||||
if (this.inertiaBuffer.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltas = {
|
||||
zoom: 0,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
pan: new Point(0, 0),
|
||||
pinchAround: undefined,
|
||||
around: undefined,
|
||||
};
|
||||
|
||||
for (const { settings } of this.inertiaBuffer) {
|
||||
deltas.zoom += settings.zoomDelta || 0;
|
||||
deltas.bearing += settings.bearingDelta || 0;
|
||||
deltas.pitch += settings.pitchDelta || 0;
|
||||
if (settings.panDelta) {
|
||||
deltas.pan._add(settings.panDelta);
|
||||
}
|
||||
if (settings.around) {
|
||||
deltas.around = settings.around;
|
||||
}
|
||||
if (settings.pinchAround) {
|
||||
deltas.pinchAround = settings.pinchAround;
|
||||
}
|
||||
}
|
||||
|
||||
const lastEntry = this.inertiaBuffer[this.inertiaBuffer.length - 1];
|
||||
const duration = lastEntry.time - this.inertiaBuffer[0].time;
|
||||
|
||||
const easeOptions: { [key: string]: any } = {};
|
||||
|
||||
if (deltas.pan.mag()) {
|
||||
const result = calculateEasing(
|
||||
deltas.pan.mag(),
|
||||
duration,
|
||||
merge({}, defaultPanInertiaOptions, panInertiaOptions || {}),
|
||||
);
|
||||
easeOptions.offset = deltas.pan.mult(result.amount / deltas.pan.mag());
|
||||
easeOptions.center = this.map.transform.center;
|
||||
extendDuration(easeOptions, result);
|
||||
}
|
||||
|
||||
if (deltas.zoom) {
|
||||
const result = calculateEasing(
|
||||
deltas.zoom,
|
||||
duration,
|
||||
defaultZoomInertiaOptions,
|
||||
);
|
||||
easeOptions.zoom = this.map.transform.zoom + result.amount;
|
||||
extendDuration(easeOptions, result);
|
||||
}
|
||||
|
||||
if (deltas.bearing) {
|
||||
const result = calculateEasing(
|
||||
deltas.bearing,
|
||||
duration,
|
||||
defaultBearingInertiaOptions,
|
||||
);
|
||||
easeOptions.bearing =
|
||||
this.map.transform.bearing + clamp(result.amount, -179, 179);
|
||||
extendDuration(easeOptions, result);
|
||||
}
|
||||
|
||||
if (deltas.pitch) {
|
||||
const result = calculateEasing(
|
||||
deltas.pitch,
|
||||
duration,
|
||||
defaultPitchInertiaOptions,
|
||||
);
|
||||
easeOptions.pitch = this.map.transform.pitch + result.amount;
|
||||
extendDuration(easeOptions, result);
|
||||
}
|
||||
|
||||
if (easeOptions.zoom || easeOptions.bearing) {
|
||||
const last =
|
||||
deltas.pinchAround === undefined ? deltas.around : deltas.pinchAround;
|
||||
easeOptions.around = last
|
||||
? this.map.unproject(last)
|
||||
: this.map.getCenter();
|
||||
}
|
||||
|
||||
this.clear();
|
||||
return merge(easeOptions, {
|
||||
noMoveStart: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately zoom, bearing, etc can't have different durations and easings so
|
||||
// we need to choose one. We use the longest duration and it's corresponding easing.
|
||||
function extendDuration(easeOptions: any, result: any) {
|
||||
if (!easeOptions.duration || easeOptions.duration < result.duration) {
|
||||
easeOptions.duration = result.duration;
|
||||
easeOptions.easing = result.easing;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateEasing(
|
||||
amount: number,
|
||||
inertiaDuration: number,
|
||||
inertiaOptions: IInertiaOptions,
|
||||
) {
|
||||
const { maxSpeed, linearity, deceleration } = inertiaOptions;
|
||||
const speed = clamp(
|
||||
(amount * linearity) / (inertiaDuration / 1000),
|
||||
-maxSpeed,
|
||||
maxSpeed,
|
||||
);
|
||||
const duration = Math.abs(speed) / (deceleration * linearity);
|
||||
return {
|
||||
easing: inertiaOptions.easing,
|
||||
duration: duration * 1000,
|
||||
amount: speed * (duration / 2),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,634 @@
|
|||
// @ts-ignore
|
||||
// tslint:disable-next-line: no-submodule-imports
|
||||
import merge from 'lodash/merge';
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
import DOM from '../utils/dom';
|
||||
import BlockableMapEventHandler from './blockable_map_event';
|
||||
import BoxZoomHandler from './box_zoom';
|
||||
import ClickZoomHandler from './click_zoom';
|
||||
import { Event } from './events/event';
|
||||
import RenderFrameEvent from './events/render_event';
|
||||
import HandlerInertia from './handler_inertia';
|
||||
import { IHandler, IHandlerResult } from './IHandler';
|
||||
import KeyboardHandler from './keyboard';
|
||||
import MapEventHandler from './map_event';
|
||||
import {
|
||||
MousePanHandler,
|
||||
MousePitchHandler,
|
||||
MouseRotateHandler,
|
||||
} from './mouse';
|
||||
import ScrollZoomHandler from './scroll_zoom';
|
||||
import DoubleClickZoomHandler from './shim/dblclick_zoom';
|
||||
import DragPanHandler from './shim/drag_pan';
|
||||
import DragRotateHandler from './shim/drag_rotate';
|
||||
import TouchZoomRotateHandler from './shim/touch_zoom_rotate';
|
||||
import TapDragZoomHandler from './tap/tap_drag_zoom';
|
||||
import TapZoomHandler from './tap/tap_zoom';
|
||||
import {
|
||||
TouchPanHandler,
|
||||
TouchPitchHandler,
|
||||
TouchRotateHandler,
|
||||
TouchZoomHandler,
|
||||
} from './touch';
|
||||
|
||||
export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent;
|
||||
|
||||
const isMoving = (p: any) => p.zoom || p.drag || p.pitch || p.rotate;
|
||||
|
||||
function hasChange(result: IHandlerResult) {
|
||||
return (
|
||||
(result.panDelta && result.panDelta.mag()) ||
|
||||
result.zoomDelta ||
|
||||
result.bearingDelta ||
|
||||
result.pitchDelta
|
||||
);
|
||||
}
|
||||
|
||||
export interface IHandlerOptions {
|
||||
interactive: boolean;
|
||||
boxZoom: boolean;
|
||||
dragRotate: boolean;
|
||||
dragPan: boolean;
|
||||
keyboard: boolean;
|
||||
doubleClickZoom: boolean;
|
||||
touchZoomRotate: boolean;
|
||||
touchPitch: boolean;
|
||||
trackResize: boolean;
|
||||
renderWorldCopies: boolean;
|
||||
bearingSnap: number;
|
||||
clickTolerance: number;
|
||||
pitchWithRotate: boolean;
|
||||
}
|
||||
|
||||
class HandlerManager {
|
||||
private map: Map;
|
||||
private el: HTMLElement;
|
||||
private handlers: Array<{
|
||||
handlerName: string;
|
||||
handler: IHandler;
|
||||
allowed: any;
|
||||
}>;
|
||||
private eventsInProgress: any;
|
||||
private frameId: number;
|
||||
private inertia: HandlerInertia;
|
||||
private bearingSnap: number;
|
||||
private handlersById: { [key: string]: IHandler };
|
||||
private updatingCamera: boolean;
|
||||
private changes: Array<[IHandlerResult, any, any]>;
|
||||
private previousActiveHandlers: { [key: string]: IHandler };
|
||||
private bearingChanged: boolean;
|
||||
private listeners: Array<
|
||||
[HTMLElement, string, void | { passive?: boolean; capture?: boolean }]
|
||||
>;
|
||||
|
||||
constructor(map: Map, options: IHandlerOptions) {
|
||||
this.map = map;
|
||||
this.el = this.map.getCanvasContainer();
|
||||
this.handlers = [];
|
||||
this.handlersById = {};
|
||||
this.changes = [];
|
||||
|
||||
this.inertia = new HandlerInertia(map);
|
||||
this.bearingSnap = options.bearingSnap;
|
||||
this.previousActiveHandlers = {};
|
||||
|
||||
// Track whether map is currently moving, to compute start/move/end events
|
||||
this.eventsInProgress = {};
|
||||
|
||||
this.addDefaultHandlers(options);
|
||||
|
||||
const el = this.el;
|
||||
|
||||
this.listeners = [
|
||||
// Bind touchstart and touchmove with passive: false because, even though
|
||||
// they only fire a map events and therefore could theoretically be
|
||||
// passive, binding with passive: true causes iOS not to respect
|
||||
// e.preventDefault() in _other_ handlers, even if they are non-passive
|
||||
// (see https://bugs.webkit.org/show_bug.cgi?id=184251)
|
||||
[el, 'touchstart', { passive: false }],
|
||||
[el, 'touchmove', { passive: false }],
|
||||
[el, 'touchend', undefined],
|
||||
[el, 'touchcancel', undefined],
|
||||
|
||||
[el, 'mousedown', undefined],
|
||||
[el, 'mousemove', undefined],
|
||||
[el, 'mouseup', undefined],
|
||||
|
||||
// Bind window-level event listeners for move and up/end events. In the absence of
|
||||
// the pointer capture API, which is not supported by all necessary platforms,
|
||||
// window-level event listeners give us the best shot at capturing events that
|
||||
// fall outside the map canvas element. Use `{capture: true}` for the move event
|
||||
// to prevent map move events from being fired during a drag.
|
||||
// @ts-ignore
|
||||
[window.document, 'mousemove', { capture: true }],
|
||||
// @ts-ignore
|
||||
[window.document, 'mouseup', undefined],
|
||||
|
||||
[el, 'mouseover', undefined],
|
||||
[el, 'mouseout', undefined],
|
||||
[el, 'dblclick', undefined],
|
||||
[el, 'click', undefined],
|
||||
|
||||
[el, 'keydown', { capture: false }],
|
||||
[el, 'keyup', undefined],
|
||||
|
||||
[el, 'wheel', { passive: false }],
|
||||
[el, 'contextmenu', undefined],
|
||||
// @ts-ignore
|
||||
[window, 'blur', undefined],
|
||||
];
|
||||
for (const [target, type, listenerOptions] of this.listeners) {
|
||||
// @ts-ignore
|
||||
DOM.addEventListener(
|
||||
target,
|
||||
type,
|
||||
// @ts-ignore
|
||||
target === window.document ? this.handleWindowEvent : this.handleEvent,
|
||||
listenerOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
public destroy() {
|
||||
for (const [target, type, listenerOptions] of this.listeners) {
|
||||
// @ts-ignore
|
||||
DOM.removeEventListener(
|
||||
target,
|
||||
type,
|
||||
// @ts-ignore
|
||||
target === window.document ? this.handleWindowEvent : this.handleEvent,
|
||||
listenerOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
// do nothing if this method was triggered by a gesture update
|
||||
if (this.updatingCamera) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { handler } of this.handlers) {
|
||||
handler.reset();
|
||||
}
|
||||
this.inertia.clear();
|
||||
this.fireEvents({}, {});
|
||||
this.changes = [];
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
for (const { handler } of this.handlers) {
|
||||
if (handler.isActive()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public isZooming() {
|
||||
return !!this.eventsInProgress.zoom || this.map.scrollZoom.isZooming();
|
||||
}
|
||||
public isRotating() {
|
||||
return !!this.eventsInProgress.rotate;
|
||||
}
|
||||
|
||||
public isMoving() {
|
||||
return Boolean(isMoving(this.eventsInProgress)) || this.isZooming();
|
||||
}
|
||||
|
||||
public handleWindowEvent = (e: InputEvent) => {
|
||||
this.handleEvent(e, `${e.type}Window`);
|
||||
};
|
||||
|
||||
public handleEvent = (
|
||||
e: InputEvent | RenderFrameEvent,
|
||||
eventName?: string,
|
||||
) => {
|
||||
if (e.type === 'blur') {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
this.updatingCamera = true;
|
||||
const inputEvent = e.type === 'renderFrame' ? undefined : (e as InputEvent);
|
||||
|
||||
/*
|
||||
* We don't call e.preventDefault() for any events by default.
|
||||
* Handlers are responsible for calling it where necessary.
|
||||
*/
|
||||
|
||||
const mergedIHandlerResult: IHandlerResult = { needsRenderFrame: false };
|
||||
const eventsInProgress: { [key: string]: any } = {};
|
||||
const activeHandlers: { [key: string]: any } = {};
|
||||
// @ts-ignore
|
||||
const mapTouches = e.touches
|
||||
? // @ts-ignore
|
||||
this.getMapTouches(e.touches as Touch[])
|
||||
: undefined;
|
||||
const points = mapTouches
|
||||
? DOM.touchPos(this.el, mapTouches)
|
||||
: DOM.mousePos(this.el, e as MouseEvent);
|
||||
|
||||
for (const { handlerName, handler, allowed } of this.handlers) {
|
||||
if (!handler.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
let data: IHandlerResult;
|
||||
if (this.blockedByActive(activeHandlers, allowed, handlerName)) {
|
||||
handler.reset();
|
||||
} else {
|
||||
const handerName = eventName || e.type;
|
||||
// @ts-ignore
|
||||
if (handler && handler[handerName]) {
|
||||
// @ts-ignore
|
||||
data = handler[handerName](e, points, mapTouches);
|
||||
this.mergeIHandlerResult(
|
||||
mergedIHandlerResult,
|
||||
eventsInProgress,
|
||||
data,
|
||||
handlerName,
|
||||
inputEvent,
|
||||
);
|
||||
if (data && data.needsRenderFrame) {
|
||||
this.triggerRenderFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
if (data || handler.isActive()) {
|
||||
activeHandlers[handlerName] = handler;
|
||||
}
|
||||
}
|
||||
|
||||
const deactivatedHandlers: { [key: string]: any } = {};
|
||||
for (const name in this.previousActiveHandlers) {
|
||||
if (!activeHandlers[name]) {
|
||||
deactivatedHandlers[name] = inputEvent;
|
||||
}
|
||||
}
|
||||
this.previousActiveHandlers = activeHandlers;
|
||||
if (
|
||||
Object.keys(deactivatedHandlers).length ||
|
||||
hasChange(mergedIHandlerResult)
|
||||
) {
|
||||
this.changes.push([
|
||||
mergedIHandlerResult,
|
||||
eventsInProgress,
|
||||
deactivatedHandlers,
|
||||
]);
|
||||
this.triggerRenderFrame();
|
||||
}
|
||||
|
||||
if (Object.keys(activeHandlers).length || hasChange(mergedIHandlerResult)) {
|
||||
this.map.stop(true);
|
||||
}
|
||||
|
||||
this.updatingCamera = false;
|
||||
|
||||
const { cameraAnimation } = mergedIHandlerResult;
|
||||
if (cameraAnimation) {
|
||||
this.inertia.clear();
|
||||
this.fireEvents({}, {});
|
||||
this.changes = [];
|
||||
cameraAnimation(this.map);
|
||||
}
|
||||
};
|
||||
|
||||
public mergeIHandlerResult(
|
||||
mergedIHandlerResult: IHandlerResult,
|
||||
eventsInProgress: { [key: string]: any },
|
||||
HandlerResult: IHandlerResult,
|
||||
name: string,
|
||||
e?: InputEvent,
|
||||
) {
|
||||
if (!HandlerResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
merge(mergedIHandlerResult, HandlerResult);
|
||||
|
||||
const eventData = {
|
||||
handlerName: name,
|
||||
originalEvent: HandlerResult.originalEvent || e,
|
||||
};
|
||||
|
||||
// track which handler changed which camera property
|
||||
if (HandlerResult.zoomDelta !== undefined) {
|
||||
eventsInProgress.zoom = eventData;
|
||||
}
|
||||
if (HandlerResult.panDelta !== undefined) {
|
||||
eventsInProgress.drag = eventData;
|
||||
}
|
||||
if (HandlerResult.pitchDelta !== undefined) {
|
||||
eventsInProgress.pitch = eventData;
|
||||
}
|
||||
if (HandlerResult.bearingDelta !== undefined) {
|
||||
eventsInProgress.rotate = eventData;
|
||||
}
|
||||
}
|
||||
|
||||
public triggerRenderFrame() {
|
||||
if (this.frameId === undefined) {
|
||||
this.frameId = this.map.requestRenderFrame((timeStamp: number) => {
|
||||
delete this.frameId;
|
||||
this.handleEvent(new RenderFrameEvent('renderFrame', { timeStamp }));
|
||||
this.applyChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addDefaultHandlers(options: IHandlerOptions) {
|
||||
const map = this.map;
|
||||
const el = map.getCanvasContainer();
|
||||
this.add('mapEvent', new MapEventHandler(map, options));
|
||||
|
||||
const boxZoom = (map.boxZoom = new BoxZoomHandler(map, options));
|
||||
this.add('boxZoom', boxZoom);
|
||||
|
||||
const tapZoom = new TapZoomHandler();
|
||||
const clickZoom = new ClickZoomHandler();
|
||||
map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom);
|
||||
this.add('tapZoom', tapZoom);
|
||||
this.add('clickZoom', clickZoom);
|
||||
|
||||
const tapDragZoom = new TapDragZoomHandler();
|
||||
this.add('tapDragZoom', tapDragZoom);
|
||||
|
||||
const touchPitch = (map.touchPitch = new TouchPitchHandler());
|
||||
this.add('touchPitch', touchPitch);
|
||||
|
||||
const mouseRotate = new MouseRotateHandler(options);
|
||||
const mousePitch = new MousePitchHandler(options);
|
||||
map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch);
|
||||
this.add('mouseRotate', mouseRotate, ['mousePitch']);
|
||||
this.add('mousePitch', mousePitch, ['mouseRotate']);
|
||||
|
||||
const mousePan = new MousePanHandler(options);
|
||||
const touchPan = new TouchPanHandler(options);
|
||||
map.dragPan = new DragPanHandler(el, mousePan, touchPan);
|
||||
this.add('mousePan', mousePan);
|
||||
this.add('touchPan', touchPan, ['touchZoom', 'touchRotate']);
|
||||
|
||||
const touchRotate = new TouchRotateHandler();
|
||||
const touchZoom = new TouchZoomHandler();
|
||||
map.touchZoomRotate = new TouchZoomRotateHandler(
|
||||
el,
|
||||
touchZoom,
|
||||
touchRotate,
|
||||
tapDragZoom,
|
||||
);
|
||||
this.add('touchRotate', touchRotate, ['touchPan', 'touchZoom']);
|
||||
this.add('touchZoom', touchZoom, ['touchPan', 'touchRotate']);
|
||||
|
||||
const scrollZoom = (map.scrollZoom = new ScrollZoomHandler(map, this));
|
||||
this.add('scrollZoom', scrollZoom, ['mousePan']);
|
||||
|
||||
const keyboard = (map.keyboard = new KeyboardHandler());
|
||||
this.add('keyboard', keyboard);
|
||||
|
||||
this.add('blockableMapEvent', new BlockableMapEventHandler(map));
|
||||
|
||||
for (const name of [
|
||||
'boxZoom',
|
||||
'doubleClickZoom',
|
||||
'tapDragZoom',
|
||||
'touchPitch',
|
||||
'dragRotate',
|
||||
'dragPan',
|
||||
'touchZoomRotate',
|
||||
'scrollZoom',
|
||||
'keyboard',
|
||||
]) {
|
||||
// @ts-ignore
|
||||
if (options.interactive && options[name]) {
|
||||
// @ts-ignore
|
||||
map[name].enable(options[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private add(handlerName: string, handler: IHandler, allowed?: string[]) {
|
||||
this.handlers.push({ handlerName, handler, allowed });
|
||||
this.handlersById[handlerName] = handler;
|
||||
}
|
||||
|
||||
private blockedByActive(
|
||||
activeHandlers: { [key: string]: IHandler },
|
||||
allowed: string[],
|
||||
myName: string,
|
||||
) {
|
||||
for (const name in activeHandlers) {
|
||||
if (name === myName) {
|
||||
continue;
|
||||
}
|
||||
if (!allowed || allowed.indexOf(name) < 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getMapTouches(touches: Touch[]): Touch[] {
|
||||
const mapTouches = [];
|
||||
for (const t of touches) {
|
||||
const target = t.target as Node;
|
||||
if (this.el.contains(target)) {
|
||||
mapTouches.push(t);
|
||||
}
|
||||
}
|
||||
return mapTouches;
|
||||
}
|
||||
|
||||
private applyChanges() {
|
||||
const combined: { [key: string]: any } = {};
|
||||
const combinedEventsInProgress = {};
|
||||
const combinedDeactivatedHandlers = {};
|
||||
|
||||
for (const [change, eventsInProgress, deactivatedHandlers] of this
|
||||
.changes) {
|
||||
if (change.panDelta) {
|
||||
combined.panDelta = (combined.panDelta || new Point(0, 0))._add(
|
||||
change.panDelta,
|
||||
);
|
||||
}
|
||||
if (change.zoomDelta) {
|
||||
combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta;
|
||||
}
|
||||
if (change.bearingDelta) {
|
||||
combined.bearingDelta =
|
||||
(combined.bearingDelta || 0) + change.bearingDelta;
|
||||
}
|
||||
if (change.pitchDelta) {
|
||||
combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta;
|
||||
}
|
||||
if (change.around !== undefined) {
|
||||
combined.around = change.around;
|
||||
}
|
||||
if (change.pinchAround !== undefined) {
|
||||
combined.pinchAround = change.pinchAround;
|
||||
}
|
||||
if (change.noInertia) {
|
||||
combined.noInertia = change.noInertia;
|
||||
}
|
||||
|
||||
merge(combinedEventsInProgress, eventsInProgress);
|
||||
merge(combinedDeactivatedHandlers, deactivatedHandlers);
|
||||
}
|
||||
|
||||
this.updateMapTransform(
|
||||
combined,
|
||||
combinedEventsInProgress,
|
||||
combinedDeactivatedHandlers,
|
||||
);
|
||||
this.changes = [];
|
||||
}
|
||||
|
||||
private updateMapTransform(
|
||||
combinedResult: any,
|
||||
combinedEventsInProgress: any,
|
||||
deactivatedHandlers: any,
|
||||
) {
|
||||
const map = this.map;
|
||||
const tr = map.transform;
|
||||
|
||||
if (!hasChange(combinedResult)) {
|
||||
return this.fireEvents(combinedEventsInProgress, deactivatedHandlers);
|
||||
}
|
||||
const {
|
||||
panDelta,
|
||||
zoomDelta,
|
||||
bearingDelta,
|
||||
pitchDelta,
|
||||
pinchAround,
|
||||
} = combinedResult;
|
||||
let { around } = combinedResult;
|
||||
|
||||
if (pinchAround !== undefined) {
|
||||
around = pinchAround;
|
||||
}
|
||||
|
||||
// stop any ongoing camera animations (easeTo, flyTo)
|
||||
map.stop(true);
|
||||
|
||||
around = around || map.transform.centerPoint;
|
||||
const loc = tr.pointLocation(panDelta ? around.sub(panDelta) : around);
|
||||
if (bearingDelta) {
|
||||
tr.bearing += bearingDelta;
|
||||
}
|
||||
if (pitchDelta) {
|
||||
tr.pitch += pitchDelta;
|
||||
}
|
||||
if (zoomDelta) {
|
||||
tr.zoom += zoomDelta;
|
||||
}
|
||||
tr.setLocationAtPoint(loc, around);
|
||||
|
||||
this.map.update();
|
||||
if (!combinedResult.noInertia) {
|
||||
this.inertia.record(combinedResult);
|
||||
}
|
||||
this.fireEvents(combinedEventsInProgress, deactivatedHandlers);
|
||||
}
|
||||
|
||||
private fireEvents(
|
||||
newEventsInProgress: { [key: string]: any },
|
||||
deactivatedHandlers: { [key: string]: any },
|
||||
) {
|
||||
const wasMoving = isMoving(this.eventsInProgress);
|
||||
const nowMoving = isMoving(newEventsInProgress);
|
||||
|
||||
const startEvents: { [key: string]: any } = {};
|
||||
|
||||
for (const eventName in newEventsInProgress) {
|
||||
if (newEventsInProgress[eventName]) {
|
||||
const { originalEvent } = newEventsInProgress[eventName];
|
||||
if (!this.eventsInProgress[eventName]) {
|
||||
startEvents[`${eventName}start`] = originalEvent;
|
||||
}
|
||||
|
||||
this.eventsInProgress[eventName] = newEventsInProgress[eventName];
|
||||
}
|
||||
}
|
||||
|
||||
// fire start events only after this.eventsInProgress has been updated
|
||||
if (!wasMoving && nowMoving) {
|
||||
this.fireEvent('movestart', nowMoving.originalEvent);
|
||||
}
|
||||
|
||||
for (const name in startEvents) {
|
||||
if (startEvents[name]) {
|
||||
this.fireEvent(name, startEvents[name]);
|
||||
}
|
||||
}
|
||||
|
||||
if (newEventsInProgress.rotate) {
|
||||
this.bearingChanged = true;
|
||||
}
|
||||
|
||||
if (nowMoving) {
|
||||
this.fireEvent('move', nowMoving.originalEvent);
|
||||
}
|
||||
|
||||
for (const eventName in newEventsInProgress) {
|
||||
if (newEventsInProgress[eventName]) {
|
||||
const { originalEvent } = newEventsInProgress[eventName];
|
||||
this.fireEvent(eventName, originalEvent);
|
||||
}
|
||||
}
|
||||
|
||||
const endEvents: { [key: string]: any } = {};
|
||||
|
||||
let originalEndEvent;
|
||||
for (const eventName in this.eventsInProgress) {
|
||||
if (this.eventsInProgress[eventName]) {
|
||||
const { handlerName, originalEvent } = this.eventsInProgress[eventName];
|
||||
if (!this.handlersById[handlerName].isActive()) {
|
||||
delete this.eventsInProgress[eventName];
|
||||
originalEndEvent = deactivatedHandlers[handlerName] || originalEvent;
|
||||
endEvents[`${eventName}end`] = originalEndEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const name in endEvents) {
|
||||
if (endEvents[name]) {
|
||||
this.fireEvent(name, endEvents[name]);
|
||||
}
|
||||
}
|
||||
|
||||
const stillMoving = isMoving(this.eventsInProgress);
|
||||
if ((wasMoving || nowMoving) && !stillMoving) {
|
||||
this.updatingCamera = true;
|
||||
const inertialEase = this.inertia.onMoveEnd(
|
||||
this.map.dragPan.inertiaOptions,
|
||||
);
|
||||
|
||||
const shouldSnapToNorth = (bearing: number) =>
|
||||
bearing !== 0 &&
|
||||
-this.bearingSnap < bearing &&
|
||||
bearing < this.bearingSnap;
|
||||
|
||||
if (inertialEase) {
|
||||
if (shouldSnapToNorth(inertialEase.bearing || this.map.getBearing())) {
|
||||
inertialEase.bearing = 0;
|
||||
}
|
||||
this.map.easeTo(inertialEase, { originalEvent: originalEndEvent });
|
||||
} else {
|
||||
this.map.emit(
|
||||
'moveend',
|
||||
new Event('moveend', { originalEvent: originalEndEvent }),
|
||||
);
|
||||
if (shouldSnapToNorth(this.map.getBearing())) {
|
||||
this.map.resetNorth();
|
||||
}
|
||||
}
|
||||
this.bearingChanged = false;
|
||||
this.updatingCamera = false;
|
||||
}
|
||||
}
|
||||
|
||||
private fireEvent(type: string, e: any) {
|
||||
this.map.emit(type, new Event(type, e ? { originalEvent: e } : {}));
|
||||
}
|
||||
}
|
||||
|
||||
export default HandlerManager;
|
|
@ -0,0 +1,10 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
|
||||
export function indexTouches(touches: Touch[], points: Point[]) {
|
||||
const obj: { [key: string]: any } = {};
|
||||
for (let i = 0; i < touches.length; i++) {
|
||||
obj[touches[i].identifier] = points[i];
|
||||
}
|
||||
return obj;
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import { Map } from '../map';
|
||||
|
||||
const defaultOptions = {
|
||||
panStep: 100,
|
||||
bearingStep: 15,
|
||||
pitchStep: 10,
|
||||
};
|
||||
|
||||
/**
|
||||
* The `KeyboardHandler` allows the user to zoom, rotate, and pan the map using
|
||||
* the following keyboard shortcuts:
|
||||
*
|
||||
* - `=` / `+`: Increase the zoom level by 1.
|
||||
* - `Shift-=` / `Shift-+`: Increase the zoom level by 2.
|
||||
* - `-`: Decrease the zoom level by 1.
|
||||
* - `Shift--`: Decrease the zoom level by 2.
|
||||
* - Arrow keys: Pan by 100 pixels.
|
||||
* - `Shift+⇢`: Increase the rotation by 15 degrees.
|
||||
* - `Shift+⇠`: Decrease the rotation by 15 degrees.
|
||||
* - `Shift+⇡`: Increase the pitch by 10 degrees.
|
||||
* - `Shift+⇣`: Decrease the pitch by 10 degrees.
|
||||
*/
|
||||
class KeyboardHandler {
|
||||
private enabled: boolean;
|
||||
private active: boolean;
|
||||
private panStep: number;
|
||||
private bearingStep: number;
|
||||
private pitchStep: number;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor() {
|
||||
const stepOptions = defaultOptions;
|
||||
this.panStep = stepOptions.panStep;
|
||||
this.bearingStep = stepOptions.bearingStep;
|
||||
this.pitchStep = stepOptions.pitchStep;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
public keydown(e: KeyboardEvent) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
let zoomDir = 0;
|
||||
let bearingDir = 0;
|
||||
let pitchDir = 0;
|
||||
let xDir = 0;
|
||||
let yDir = 0;
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 61:
|
||||
case 107:
|
||||
case 171:
|
||||
case 187:
|
||||
zoomDir = 1;
|
||||
break;
|
||||
|
||||
case 189:
|
||||
case 109:
|
||||
case 173:
|
||||
zoomDir = -1;
|
||||
break;
|
||||
|
||||
case 37:
|
||||
if (e.shiftKey) {
|
||||
bearingDir = -1;
|
||||
} else {
|
||||
e.preventDefault();
|
||||
xDir = -1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 39:
|
||||
if (e.shiftKey) {
|
||||
bearingDir = 1;
|
||||
} else {
|
||||
e.preventDefault();
|
||||
xDir = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 38:
|
||||
if (e.shiftKey) {
|
||||
pitchDir = 1;
|
||||
} else {
|
||||
e.preventDefault();
|
||||
yDir = -1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 40:
|
||||
if (e.shiftKey) {
|
||||
pitchDir = -1;
|
||||
} else {
|
||||
e.preventDefault();
|
||||
yDir = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
cameraAnimation: (map: Map) => {
|
||||
const zoom = map.getZoom();
|
||||
map.easeTo(
|
||||
{
|
||||
duration: 300,
|
||||
easeId: 'keyboardHandler',
|
||||
easing: easeOut,
|
||||
|
||||
zoom: zoomDir
|
||||
? Math.round(zoom) + zoomDir * (e.shiftKey ? 2 : 1)
|
||||
: zoom,
|
||||
bearing: map.getBearing() + bearingDir * this.bearingStep,
|
||||
pitch: map.getPitch() + pitchDir * this.pitchStep,
|
||||
offset: [-xDir * this.panStep, -yDir * this.panStep],
|
||||
center: map.getCenter(),
|
||||
},
|
||||
{ originalEvent: e },
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
|
||||
function easeOut(t: number) {
|
||||
return t * (2 - t);
|
||||
}
|
||||
|
||||
export default KeyboardHandler;
|
|
@ -0,0 +1,110 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
import { MapMouseEvent, MapTouchEvent, MapWheelEvent } from './events';
|
||||
|
||||
export default class MapEventHandler {
|
||||
private mousedownPos: Point;
|
||||
private clickTolerance: number;
|
||||
private map: Map;
|
||||
|
||||
constructor(map: Map, options: { clickTolerance: number }) {
|
||||
this.map = map;
|
||||
this.clickTolerance = options.clickTolerance;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
delete this.mousedownPos;
|
||||
}
|
||||
|
||||
public wheel(e: WheelEvent) {
|
||||
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
|
||||
// - ScrollZoom
|
||||
return this.firePreventable(new MapWheelEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public mousedown(e: MouseEvent, point: Point) {
|
||||
this.mousedownPos = point;
|
||||
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
|
||||
// - MousePan
|
||||
// - MouseRotate
|
||||
// - MousePitch
|
||||
// - DblclickHandler
|
||||
return this.firePreventable(new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public mouseup(e: MouseEvent) {
|
||||
this.map.emit(e.type, new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public click(e: MouseEvent, point: Point) {
|
||||
if (
|
||||
this.mousedownPos &&
|
||||
this.mousedownPos.dist(point) >= this.clickTolerance
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.map.emit(e.type, new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public dblclick(e: MouseEvent) {
|
||||
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
|
||||
// - DblClickZoom
|
||||
return this.firePreventable(new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public mouseover(e: MouseEvent) {
|
||||
this.map.emit(e.type, new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public mouseout(e: MouseEvent) {
|
||||
this.map.emit(e.type, new MapMouseEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public touchstart(e: TouchEvent) {
|
||||
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
|
||||
// - TouchPan
|
||||
// - TouchZoom
|
||||
// - TouchRotate
|
||||
// - TouchPitch
|
||||
// - TapZoom
|
||||
// - SwipeZoom
|
||||
return this.firePreventable(new MapTouchEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent) {
|
||||
this.map.emit(e.type, new MapTouchEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent) {
|
||||
this.map.emit(e.type, new MapTouchEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public touchcancel(e: TouchEvent) {
|
||||
this.map.emit(e.type, new MapTouchEvent(e.type, this.map, e));
|
||||
}
|
||||
|
||||
public firePreventable(
|
||||
mapEvent: MapMouseEvent | MapTouchEvent | MapWheelEvent,
|
||||
) {
|
||||
this.map.emit(mapEvent.type, mapEvent);
|
||||
if (mapEvent.defaultPrevented) {
|
||||
// returning an object marks the handler as active and resets other handlers
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return false;
|
||||
}
|
||||
public enable() {
|
||||
return false;
|
||||
}
|
||||
public disable() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import MousePanHandler from './mousepan_handler';
|
||||
import MouseRotateHandler from './mousepitch_hander';
|
||||
import MousePitchHandler from './mouserotate_hander';
|
||||
|
||||
export { MousePanHandler, MouseRotateHandler, MousePitchHandler };
|
|
@ -0,0 +1,107 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import DOM from '../../utils/dom';
|
||||
import { buttonStillPressed } from './util';
|
||||
export default class MouseHandler {
|
||||
protected enabled: boolean;
|
||||
protected active: boolean;
|
||||
protected lastPoint: Point;
|
||||
protected eventButton: 1 | 2;
|
||||
protected moved: boolean;
|
||||
protected clickTolerance: number;
|
||||
|
||||
constructor(options: { clickTolerance: number }) {
|
||||
this.reset();
|
||||
this.clickTolerance = options.clickTolerance || 1;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
this.moved = false;
|
||||
delete this.lastPoint;
|
||||
delete this.eventButton;
|
||||
}
|
||||
|
||||
public mousedown(e: MouseEvent, point: Point) {
|
||||
if (this.lastPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventButton = DOM.mouseButton(e);
|
||||
if (!this.correctButton(e, eventButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPoint = point;
|
||||
this.eventButton = eventButton;
|
||||
}
|
||||
|
||||
public mousemoveWindow(e: MouseEvent, point: Point) {
|
||||
const lastPoint = this.lastPoint;
|
||||
if (!lastPoint) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (buttonStillPressed(e, this.eventButton)) {
|
||||
// Some browsers don't fire a `mouseup` when the mouseup occurs outside
|
||||
// the window or iframe:
|
||||
// https://github.com/mapbox/mapbox-gl-js/issues/4622
|
||||
//
|
||||
// If the button is no longer pressed during this `mousemove` it may have
|
||||
// been released outside of the window or iframe.
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.moved && point.dist(lastPoint) < this.clickTolerance) {
|
||||
return;
|
||||
}
|
||||
this.moved = true;
|
||||
this.lastPoint = point;
|
||||
|
||||
// implemented by child class
|
||||
return this.move(lastPoint, point);
|
||||
}
|
||||
|
||||
public mouseupWindow(e: MouseEvent) {
|
||||
if (!this.lastPoint) {
|
||||
return;
|
||||
}
|
||||
const eventButton = DOM.mouseButton(e);
|
||||
if (eventButton !== this.eventButton) {
|
||||
return;
|
||||
}
|
||||
if (this.moved) {
|
||||
DOM.suppressClick();
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
protected correctButton(e: MouseEvent, button: number) {
|
||||
// eslint-disable-line
|
||||
return false; // implemented by child
|
||||
}
|
||||
|
||||
protected move(lastPoint: Point, point: Point) {
|
||||
// eslint-disable-line
|
||||
return; // implemented by child
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import DOM from '../../utils/dom';
|
||||
import MouseHandler from './mouse_handler';
|
||||
import { buttonStillPressed, LEFT_BUTTON } from './util';
|
||||
export default class MousePanHandler extends MouseHandler {
|
||||
public mousedown(e: MouseEvent, point: Point) {
|
||||
super.mousedown(e, point);
|
||||
if (this.lastPoint) {
|
||||
this.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
public move(lastPoint: Point, point: Point) {
|
||||
return {
|
||||
around: point,
|
||||
panDelta: point.sub(lastPoint),
|
||||
};
|
||||
}
|
||||
protected correctButton(e: MouseEvent, button: number) {
|
||||
return button === LEFT_BUTTON && !e.ctrlKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import MouseHandler from './mouse_handler';
|
||||
import { LEFT_BUTTON, RIGHT_BUTTON } from './util';
|
||||
export default class MousePitchHandler extends MouseHandler {
|
||||
public correctButton(e: MouseEvent, button: number) {
|
||||
return (button === LEFT_BUTTON && e.ctrlKey) || button === RIGHT_BUTTON;
|
||||
}
|
||||
|
||||
public move(lastPoint: Point, point: Point) {
|
||||
const degreesPerPixelMoved = -0.5;
|
||||
const pitchDelta = (point.y - lastPoint.y) * degreesPerPixelMoved;
|
||||
if (pitchDelta) {
|
||||
this.active = true;
|
||||
return { pitchDelta };
|
||||
}
|
||||
}
|
||||
|
||||
public contextmenu(e: MouseEvent) {
|
||||
// prevent browser context menu when necessary; we don't allow it with rotation
|
||||
// because we can't discern rotation gesture start from contextmenu on Mac
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import MouseHandler from './mouse_handler';
|
||||
import { LEFT_BUTTON, RIGHT_BUTTON } from './util';
|
||||
export default class MouseRotateHandler extends MouseHandler {
|
||||
public contextmenu(e: MouseEvent) {
|
||||
// prevent browser context menu when necessary; we don't allow it with rotation
|
||||
// because we can't discern rotation gesture start from contextmenu on Mac
|
||||
e.preventDefault();
|
||||
}
|
||||
protected correctButton(e: MouseEvent, button: number) {
|
||||
return (button === LEFT_BUTTON && e.ctrlKey) || button === RIGHT_BUTTON;
|
||||
}
|
||||
|
||||
protected move(lastPoint: Point, point: Point) {
|
||||
const degreesPerPixelMoved = 0.8;
|
||||
const bearingDelta = (point.x - lastPoint.x) * degreesPerPixelMoved;
|
||||
if (bearingDelta) {
|
||||
this.active = true;
|
||||
return { bearingDelta };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export const LEFT_BUTTON = 0;
|
||||
export const RIGHT_BUTTON = 2;
|
||||
|
||||
// the values for each button in MouseEvent.buttons
|
||||
export const BUTTONS_FLAGS: { [key: number]: number } = {
|
||||
[LEFT_BUTTON]: 1,
|
||||
[RIGHT_BUTTON]: 2,
|
||||
};
|
||||
|
||||
export function buttonStillPressed(e: MouseEvent, button: 1 | 2) {
|
||||
const flag = BUTTONS_FLAGS[button];
|
||||
return e.buttons === undefined || (e.buttons & flag) !== flag;
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
// @ts-ignore
|
||||
import LngLat from '../geo/lng_lat';
|
||||
import Point from '../geo/point';
|
||||
import { Map } from '../map';
|
||||
import { bezier, ease, interpolate, now } from '../util';
|
||||
import DOM from '../utils/dom';
|
||||
import HandlerManager from './handler_manager';
|
||||
|
||||
// deltaY value for mouse scroll wheel identification
|
||||
const wheelZoomDelta = 4.000244140625;
|
||||
|
||||
// These magic numbers control the rate of zoom. Trackpad events fire at a greater
|
||||
// frequency than mouse scroll wheel, so reduce the zoom rate per wheel tick
|
||||
const defaultZoomRate = 1 / 100;
|
||||
const wheelZoomRate = 1 / 450;
|
||||
|
||||
// upper bound on how much we scale the map in any single render frame; this
|
||||
// is used to limit zoom rate in the case of very fast scrolling
|
||||
const maxScalePerFrame = 2;
|
||||
|
||||
/**
|
||||
* The `ScrollZoomHandler` allows the user to zoom the map by scrolling.
|
||||
*/
|
||||
class ScrollZoomHandler {
|
||||
private map: Map;
|
||||
private el: HTMLElement;
|
||||
private enabled: boolean;
|
||||
private active: boolean;
|
||||
private zooming: boolean;
|
||||
private aroundCenter: boolean;
|
||||
private around: LngLat;
|
||||
private aroundPoint: Point;
|
||||
private type: 'wheel' | 'trackpad' | null;
|
||||
private lastValue: number;
|
||||
private timeout: number | null; // used for delayed-handling of a single wheel movement
|
||||
private finishTimeout: number; // used to delay final '{move,zoom}end' events
|
||||
|
||||
private lastWheelEvent: any;
|
||||
private lastWheelEventTime: number;
|
||||
|
||||
private startZoom: number;
|
||||
private targetZoom: number;
|
||||
private delta: number;
|
||||
private easing: (time: number) => number;
|
||||
private prevEase: {
|
||||
start: number;
|
||||
duration: number;
|
||||
easing: (_: number) => number;
|
||||
};
|
||||
|
||||
private frameId: boolean | null;
|
||||
private handler: HandlerManager;
|
||||
|
||||
private defaultZoomRate: number;
|
||||
private wheelZoomRate: number;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(map: Map, handler: HandlerManager) {
|
||||
this.map = map;
|
||||
this.el = map.getCanvasContainer();
|
||||
this.handler = handler;
|
||||
|
||||
this.delta = 0;
|
||||
this.defaultZoomRate = defaultZoomRate;
|
||||
this.wheelZoomRate = wheelZoomRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zoom rate of a trackpad
|
||||
* @param {number} [zoomRate=1/100] The rate used to scale trackpad movement to a zoom value.
|
||||
* @example
|
||||
* // Speed up trackpad zoom
|
||||
* map.scrollZoom.setZoomRate(1/25);
|
||||
*/
|
||||
public setZoomRate(zoomRate: number) {
|
||||
this.defaultZoomRate = zoomRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zoom rate of a mouse wheel
|
||||
* @param {number} [wheelZoomRate=1/450] The rate used to scale mouse wheel movement to a zoom value.
|
||||
* @example
|
||||
* // Slow down zoom of mouse wheel
|
||||
* map.scrollZoom.setWheelZoomRate(1/600);
|
||||
*/
|
||||
public setWheelZoomRate(zoomRate: number) {
|
||||
this.wheelZoomRate = zoomRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "scroll to zoom" interaction is enabled.
|
||||
*
|
||||
* @returns {boolean} `true` if the "scroll to zoom" interaction is enabled.
|
||||
*/
|
||||
public isEnabled() {
|
||||
return !!this.enabled;
|
||||
}
|
||||
|
||||
/*
|
||||
* Active state is turned on and off with every scroll wheel event and is set back to false before the map
|
||||
* render is called, so _active is not a good candidate for determining if a scroll zoom animation is in
|
||||
* progress.
|
||||
*/
|
||||
public isActive() {
|
||||
return !!this.active || this.finishTimeout !== undefined;
|
||||
}
|
||||
|
||||
public isZooming() {
|
||||
return !!this.zooming;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "scroll to zoom" interaction.
|
||||
*
|
||||
* @param {Object} [options] Options object.
|
||||
* @param {string} [options.around] If "center" is passed, map will zoom around center of map
|
||||
*
|
||||
* @example
|
||||
* map.scrollZoom.enable();
|
||||
* @example
|
||||
* map.scrollZoom.enable({ around: 'center' })
|
||||
*/
|
||||
public enable(options?: any) {
|
||||
if (this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
this.enabled = true;
|
||||
this.aroundCenter = options && options.around === 'center';
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "scroll to zoom" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.scrollZoom.disable();
|
||||
*/
|
||||
public disable() {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
public wheel(e: WheelEvent) {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Remove `any` cast when https://github.com/facebook/flow/issues/4879 is fixed.
|
||||
let value =
|
||||
e.deltaMode === window.WheelEvent.DOM_DELTA_LINE
|
||||
? e.deltaY * 40
|
||||
: e.deltaY;
|
||||
const nowTime = now();
|
||||
const timeDelta = nowTime - (this.lastWheelEventTime || 0);
|
||||
|
||||
this.lastWheelEventTime = nowTime;
|
||||
|
||||
if (value !== 0 && value % wheelZoomDelta === 0) {
|
||||
// This one is definitely a mouse wheel event.
|
||||
this.type = 'wheel';
|
||||
} else if (value !== 0 && Math.abs(value) < 4) {
|
||||
// This one is definitely a trackpad event because it is so small.
|
||||
this.type = 'trackpad';
|
||||
} else if (timeDelta > 400) {
|
||||
// This is likely a new scroll action.
|
||||
this.type = null;
|
||||
this.lastValue = value;
|
||||
|
||||
// Start a timeout in case this was a singular event, and dely it by up to 40ms.
|
||||
// @ts-ignore
|
||||
this.timeout = setTimeout(this.onTimeout, 40, e);
|
||||
} else if (!this.type) {
|
||||
// This is a repeating event, but we don't know the type of event just yet.
|
||||
// If the delta per time is small, we assume it's a fast trackpad; otherwise we switch into wheel mode.
|
||||
this.type = Math.abs(timeDelta * value) < 200 ? 'trackpad' : 'wheel';
|
||||
|
||||
// Make sure our delayed event isn't fired again, because we accumulate
|
||||
// the previous event (which was less than 40ms ago) into this event.
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
value += this.lastValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Slow down zoom if shift key is held for more precise zooming
|
||||
if (e.shiftKey && value) {
|
||||
value = value / 4;
|
||||
}
|
||||
// Only fire the callback if we actually know what type of scrolling device the user uses.
|
||||
if (this.type) {
|
||||
this.lastWheelEvent = e;
|
||||
this.delta -= value;
|
||||
if (!this.active) {
|
||||
this.start(e);
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
public renderFrame() {
|
||||
return this.onScrollFrame();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
private onScrollFrame = () => {
|
||||
if (!this.frameId) {
|
||||
return;
|
||||
}
|
||||
this.frameId = null;
|
||||
|
||||
if (!this.isActive()) {
|
||||
return;
|
||||
}
|
||||
const tr = this.map.transform;
|
||||
|
||||
// if we've had scroll events since the last render frame, consume the
|
||||
// accumulated delta, and update the target zoom level accordingly
|
||||
if (this.delta !== 0) {
|
||||
// For trackpad events and single mouse wheel ticks, use the default zoom rate
|
||||
const zoomRate =
|
||||
this.type === 'wheel' && Math.abs(this.delta) > wheelZoomDelta
|
||||
? this.wheelZoomRate
|
||||
: this.defaultZoomRate;
|
||||
// Scale by sigmoid of scroll wheel delta.
|
||||
let scale =
|
||||
maxScalePerFrame / (1 + Math.exp(-Math.abs(this.delta * zoomRate)));
|
||||
|
||||
if (this.delta < 0 && scale !== 0) {
|
||||
scale = 1 / scale;
|
||||
}
|
||||
|
||||
const fromScale =
|
||||
typeof this.targetZoom === 'number'
|
||||
? tr.zoomScale(this.targetZoom)
|
||||
: tr.scale;
|
||||
this.targetZoom = Math.min(
|
||||
tr.maxZoom,
|
||||
Math.max(tr.minZoom, tr.scaleZoom(fromScale * scale)),
|
||||
);
|
||||
|
||||
// if this is a mouse wheel, refresh the starting zoom and easing
|
||||
// function we're using to smooth out the zooming between wheel
|
||||
// events
|
||||
if (this.type === 'wheel') {
|
||||
this.startZoom = tr.zoom;
|
||||
this.easing = this.smoothOutEasing(200);
|
||||
}
|
||||
|
||||
this.delta = 0;
|
||||
}
|
||||
|
||||
const targetZoom =
|
||||
typeof this.targetZoom === 'number' ? this.targetZoom : tr.zoom;
|
||||
const startZoom = this.startZoom;
|
||||
const easing = this.easing;
|
||||
|
||||
let finished = false;
|
||||
let zoom;
|
||||
if (this.type === 'wheel' && startZoom && easing) {
|
||||
const t = Math.min((now() - this.lastWheelEventTime) / 200, 1);
|
||||
const k = easing(t);
|
||||
zoom = interpolate(startZoom, targetZoom, k);
|
||||
if (t < 1) {
|
||||
if (!this.frameId) {
|
||||
this.frameId = true;
|
||||
}
|
||||
} else {
|
||||
finished = true;
|
||||
}
|
||||
} else {
|
||||
zoom = targetZoom;
|
||||
finished = true;
|
||||
}
|
||||
|
||||
this.active = true;
|
||||
|
||||
if (finished) {
|
||||
this.active = false;
|
||||
// @ts-ignore
|
||||
this.finishTimeout = setTimeout(() => {
|
||||
this.zooming = false;
|
||||
this.handler.triggerRenderFrame();
|
||||
delete this.targetZoom;
|
||||
delete this.finishTimeout;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
return {
|
||||
noInertia: true,
|
||||
needsRenderFrame: !finished,
|
||||
zoomDelta: zoom - tr.zoom,
|
||||
around: this.aroundPoint,
|
||||
originalEvent: this.lastWheelEvent,
|
||||
};
|
||||
};
|
||||
|
||||
private onTimeout(initialEvent: any) {
|
||||
this.type = 'wheel';
|
||||
this.delta -= this.lastValue;
|
||||
if (!this.active) {
|
||||
this.start(initialEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private start(e: any) {
|
||||
if (!this.delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.frameId) {
|
||||
this.frameId = null;
|
||||
}
|
||||
|
||||
this.active = true;
|
||||
if (!this.isZooming()) {
|
||||
this.zooming = true;
|
||||
}
|
||||
|
||||
if (this.finishTimeout) {
|
||||
clearTimeout(this.finishTimeout);
|
||||
delete this.finishTimeout;
|
||||
}
|
||||
|
||||
const pos = DOM.mousePos(this.el, e);
|
||||
|
||||
this.around = LngLat.convert(
|
||||
this.aroundCenter ? this.map.getCenter() : this.map.unproject(pos),
|
||||
);
|
||||
this.aroundPoint = this.map.transform.locationPoint(this.around);
|
||||
if (!this.frameId) {
|
||||
this.frameId = true;
|
||||
this.handler.triggerRenderFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private smoothOutEasing(duration: number) {
|
||||
let easing = ease;
|
||||
|
||||
if (this.prevEase) {
|
||||
const preEase = this.prevEase;
|
||||
const t = (now() - preEase.start) / preEase.duration;
|
||||
const speed = preEase.easing(t + 0.01) - preEase.easing(t);
|
||||
|
||||
// Quick hack to make new bezier that is continuous with last
|
||||
const x = (0.27 / Math.sqrt(speed * speed + 0.0001)) * 0.01;
|
||||
const y = Math.sqrt(0.27 * 0.27 - x * x);
|
||||
|
||||
easing = bezier(x, y, 0.25, 1);
|
||||
}
|
||||
|
||||
this.prevEase = {
|
||||
start: now(),
|
||||
duration,
|
||||
easing,
|
||||
};
|
||||
|
||||
return easing;
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollZoomHandler;
|
|
@ -0,0 +1,59 @@
|
|||
import ClickZoomHandler from '../click_zoom';
|
||||
import TapZoomHandler from '../tap/tap_zoom';
|
||||
|
||||
/**
|
||||
* The `DoubleClickZoomHandler` allows the user to zoom the map at a point by
|
||||
* double clicking or double tapping.
|
||||
*/
|
||||
export default class DoubleClickZoomHandler {
|
||||
private clickZoom: ClickZoomHandler;
|
||||
private tapZoom: TapZoomHandler;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(clickZoom: ClickZoomHandler, TapZoom: TapZoomHandler) {
|
||||
this.clickZoom = clickZoom;
|
||||
this.tapZoom = TapZoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "double click to zoom" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.doubleClickZoom.enable();
|
||||
*/
|
||||
public enable() {
|
||||
this.clickZoom.enable();
|
||||
this.tapZoom.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "double click to zoom" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.doubleClickZoom.disable();
|
||||
*/
|
||||
public disable() {
|
||||
this.clickZoom.disable();
|
||||
this.tapZoom.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "double click to zoom" interaction is enabled.
|
||||
*
|
||||
* @returns {boolean} `true` if the "double click to zoom" interaction is enabled.
|
||||
*/
|
||||
public isEnabled() {
|
||||
return this.clickZoom.isEnabled() && this.tapZoom.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "double click to zoom" interaction is active, i.e. currently being used.
|
||||
*
|
||||
* @returns {boolean} `true` if the "double click to zoom" interaction is active.
|
||||
*/
|
||||
public isActive() {
|
||||
return this.clickZoom.isActive() || this.tapZoom.isActive();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import { MousePanHandler } from '../mouse/';
|
||||
import { TouchPanHandler } from '../touch/';
|
||||
|
||||
export interface IDragPanOptions {
|
||||
linearity?: number;
|
||||
easing?: (t: number) => number;
|
||||
deceleration?: number;
|
||||
maxSpeed?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `DragPanHandler` allows the user to pan the map by clicking and dragging
|
||||
* the cursor.
|
||||
*/
|
||||
export default class DragPanHandler {
|
||||
public inertiaOptions: IDragPanOptions;
|
||||
private el: HTMLElement;
|
||||
private mousePan: MousePanHandler;
|
||||
private touchPan: TouchPanHandler;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(
|
||||
el: HTMLElement,
|
||||
mousePan: MousePanHandler,
|
||||
touchPan: TouchPanHandler,
|
||||
) {
|
||||
this.el = el;
|
||||
this.mousePan = mousePan;
|
||||
this.touchPan = touchPan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "drag to pan" interaction.
|
||||
*
|
||||
* @param {Object} [options] Options object
|
||||
* @param {number} [options.linearity=0] factor used to scale the drag velocity
|
||||
* @param {Function} [options.easing=bezier(0, 0, 0.3, 1)] easing function applled to `map.panTo` when applying the drag.
|
||||
* @param {number} [options.maxSpeed=1400] the maximum value of the drag velocity.
|
||||
* @param {number} [options.deceleration=2500] the rate at which the speed reduces after the pan ends.
|
||||
*
|
||||
* @example
|
||||
* map.dragPan.enable();
|
||||
* @example
|
||||
* map.dragPan.enable({
|
||||
* linearity: 0.3,
|
||||
* easing: bezier(0, 0, 0.3, 1),
|
||||
* maxSpeed: 1400,
|
||||
* deceleration: 2500,
|
||||
* });
|
||||
*/
|
||||
public enable(options?: IDragPanOptions) {
|
||||
this.inertiaOptions = options || {};
|
||||
this.mousePan.enable();
|
||||
this.touchPan.enable();
|
||||
this.el.classList.add('l7-touch-drag-pan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "drag to pan" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.dragPan.disable();
|
||||
*/
|
||||
public disable() {
|
||||
this.mousePan.disable();
|
||||
this.touchPan.disable();
|
||||
this.el.classList.remove('l7-touch-drag-pan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "drag to pan" interaction is enabled.
|
||||
*
|
||||
* @returns {boolean} `true` if the "drag to pan" interaction is enabled.
|
||||
*/
|
||||
public isEnabled() {
|
||||
return this.mousePan.isEnabled() && this.touchPan.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "drag to pan" interaction is active, i.e. currently being used.
|
||||
*
|
||||
* @returns {boolean} `true` if the "drag to pan" interaction is active.
|
||||
*/
|
||||
public isActive() {
|
||||
return this.mousePan.isActive() || this.touchPan.isActive();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { MousePitchHandler, MouseRotateHandler } from '../mouse';
|
||||
|
||||
/**
|
||||
* The `DragRotateHandler` allows the user to rotate the map by clicking and
|
||||
* dragging the cursor while holding the right mouse button or `ctrl` key.
|
||||
*/
|
||||
export default class DragRotateHandler {
|
||||
private mouseRotate: MouseRotateHandler;
|
||||
private mousePitch: MousePitchHandler;
|
||||
private pitchWithRotate: boolean;
|
||||
|
||||
/**
|
||||
* @param {Object} [options]
|
||||
* @param {number} [options.bearingSnap] The threshold, measured in degrees, that determines when the map's
|
||||
* bearing will snap to north.
|
||||
* @param {bool} [options.pitchWithRotate=true] Control the map pitch in addition to the bearing
|
||||
* @private
|
||||
*/
|
||||
constructor(
|
||||
options: { pitchWithRotate: boolean },
|
||||
mouseRotate: MouseRotateHandler,
|
||||
mousePitch: MousePitchHandler,
|
||||
) {
|
||||
this.pitchWithRotate = options.pitchWithRotate;
|
||||
this.mouseRotate = mouseRotate;
|
||||
this.mousePitch = mousePitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "drag to rotate" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.dragRotate.enable();
|
||||
*/
|
||||
public enable() {
|
||||
this.mouseRotate.enable();
|
||||
if (this.pitchWithRotate) {
|
||||
this.mousePitch.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "drag to rotate" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.dragRotate.disable();
|
||||
*/
|
||||
public disable() {
|
||||
this.mouseRotate.disable();
|
||||
this.mousePitch.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "drag to rotate" interaction is enabled.
|
||||
*
|
||||
* @returns {boolean} `true` if the "drag to rotate" interaction is enabled.
|
||||
*/
|
||||
public isEnabled() {
|
||||
return (
|
||||
this.mouseRotate.isEnabled() &&
|
||||
(!this.pitchWithRotate || this.mousePitch.isEnabled())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "drag to rotate" interaction is active, i.e. currently being used.
|
||||
*
|
||||
* @returns {boolean} `true` if the "drag to rotate" interaction is active.
|
||||
*/
|
||||
public isActive() {
|
||||
return this.mouseRotate.isActive() || this.mousePitch.isActive();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import TapDragZoomHandler from '../tap/tap_drag_zoom';
|
||||
import { TouchRotateHandler, TouchZoomHandler } from '../touch';
|
||||
|
||||
/**
|
||||
* The `TouchZoomRotateHandler` allows the user to zoom and rotate the map by
|
||||
* pinching on a touchscreen.
|
||||
*
|
||||
* They can zoom with one finger by double tapping and dragging. On the second tap,
|
||||
* hold the finger down and drag up or down to zoom in or out.
|
||||
*/
|
||||
export default class TouchZoomRotateHandler {
|
||||
private el: HTMLElement;
|
||||
private touchZoom: TouchZoomHandler;
|
||||
private touchRotate: TouchRotateHandler;
|
||||
private tapDragZoom: TapDragZoomHandler;
|
||||
private rotationDisabled: boolean;
|
||||
private enabled: boolean;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
constructor(
|
||||
el: HTMLElement,
|
||||
touchZoom: TouchZoomHandler,
|
||||
touchRotate: TouchRotateHandler,
|
||||
tapDragZoom: TapDragZoomHandler,
|
||||
) {
|
||||
this.el = el;
|
||||
this.touchZoom = touchZoom;
|
||||
this.touchRotate = touchRotate;
|
||||
this.tapDragZoom = tapDragZoom;
|
||||
this.rotationDisabled = false;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "pinch to rotate and zoom" interaction.
|
||||
*
|
||||
* @param {Object} [options] Options object.
|
||||
* @param {string} [options.around] If "center" is passed, map will zoom around the center
|
||||
*
|
||||
* @example
|
||||
* map.touchZoomRotate.enable();
|
||||
* @example
|
||||
* map.touchZoomRotate.enable({ around: 'center' });
|
||||
*/
|
||||
public enable(options: { around?: 'center' }) {
|
||||
this.touchZoom.enable(options);
|
||||
if (!this.rotationDisabled) {
|
||||
this.touchRotate.enable(options);
|
||||
}
|
||||
this.tapDragZoom.enable();
|
||||
this.el.classList.add('l7-touch-zoom-rotate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "pinch to rotate and zoom" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.touchZoomRotate.disable();
|
||||
*/
|
||||
public disable() {
|
||||
this.touchZoom.disable();
|
||||
this.touchRotate.disable();
|
||||
this.tapDragZoom.disable();
|
||||
this.el.classList.remove('l7-touch-zoom-rotate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Boolean indicating whether the "pinch to rotate and zoom" interaction is enabled.
|
||||
*
|
||||
* @returns {boolean} `true` if the "pinch to rotate and zoom" interaction is enabled.
|
||||
*/
|
||||
public isEnabled() {
|
||||
return (
|
||||
this.touchZoom.isEnabled() &&
|
||||
(this.rotationDisabled || this.touchRotate.isEnabled()) &&
|
||||
this.tapDragZoom.isEnabled()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the handler is enabled and has detected the start of a zoom/rotate gesture.
|
||||
*
|
||||
* @returns {boolean} //eslint-disable-line
|
||||
*/
|
||||
public isActive() {
|
||||
return (
|
||||
this.touchZoom.isActive() ||
|
||||
this.touchRotate.isActive() ||
|
||||
this.tapDragZoom.isActive()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the "pinch to rotate" interaction, leaving the "pinch to zoom"
|
||||
* interaction enabled.
|
||||
*
|
||||
* @example
|
||||
* map.touchZoomRotate.disableRotation();
|
||||
*/
|
||||
public disableRotation() {
|
||||
this.rotationDisabled = true;
|
||||
this.touchRotate.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the "pinch to rotate" interaction.
|
||||
*
|
||||
* @example
|
||||
* map.touchZoomRotate.enable();
|
||||
* map.touchZoomRotate.enableRotation();
|
||||
*/
|
||||
public enableRotation() {
|
||||
this.rotationDisabled = false;
|
||||
if (this.touchZoom.isEnabled()) {
|
||||
this.touchRotate.enable();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import { indexTouches } from '../handler_util';
|
||||
|
||||
function getCentroid(points: Point[]) {
|
||||
const sum = new Point(0, 0);
|
||||
for (const point of points) {
|
||||
sum._add(point);
|
||||
}
|
||||
// @ts-ignore
|
||||
return sum.div(points.length);
|
||||
}
|
||||
|
||||
export const MAX_TAP_INTERVAL = 500;
|
||||
export const MAX_TOUCH_TIME = 500;
|
||||
export const MAX_DIST = 30;
|
||||
|
||||
export default class SingleTapRecognizer {
|
||||
public numTouches: number;
|
||||
public centroid: Point;
|
||||
public startTime: number;
|
||||
public aborted: boolean;
|
||||
public touches: { [key: string]: Point };
|
||||
|
||||
constructor(options: { numTouches: number }) {
|
||||
this.reset();
|
||||
this.numTouches = options.numTouches;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
delete this.centroid;
|
||||
delete this.startTime;
|
||||
delete this.touches;
|
||||
this.aborted = false;
|
||||
}
|
||||
|
||||
public touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (this.centroid || mapTouches.length > this.numTouches) {
|
||||
this.aborted = true;
|
||||
}
|
||||
if (this.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.startTime === undefined) {
|
||||
this.startTime = e.timeStamp;
|
||||
}
|
||||
|
||||
if (mapTouches.length === this.numTouches) {
|
||||
this.centroid = getCentroid(points);
|
||||
this.touches = indexTouches(mapTouches, points);
|
||||
}
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (this.aborted || !this.centroid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTouches = indexTouches(mapTouches, points);
|
||||
for (const id in this.touches) {
|
||||
if (this.touches[id]) {
|
||||
const prevPos = this.touches[id];
|
||||
const pos = newTouches[id];
|
||||
if (!pos || pos.dist(prevPos) > MAX_DIST) {
|
||||
this.aborted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (!this.centroid || e.timeStamp - this.startTime > MAX_TOUCH_TIME) {
|
||||
this.aborted = true;
|
||||
}
|
||||
|
||||
if (mapTouches.length === 0) {
|
||||
const centroid = !this.aborted && this.centroid;
|
||||
this.reset();
|
||||
if (centroid) {
|
||||
return centroid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import { MAX_TAP_INTERVAL } from './single_tap_recognizer';
|
||||
import TapRecognizer from './tap_recognizer';
|
||||
|
||||
export default class TapDragZoomHandler {
|
||||
public enabled: boolean;
|
||||
public active: boolean;
|
||||
public swipePoint: Point;
|
||||
public swipeTouch: number;
|
||||
public tapTime: number;
|
||||
public tap: TapRecognizer;
|
||||
|
||||
constructor() {
|
||||
this.tap = new TapRecognizer({
|
||||
numTouches: 1,
|
||||
numTaps: 1,
|
||||
});
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
delete this.swipePoint;
|
||||
delete this.swipeTouch;
|
||||
delete this.tapTime;
|
||||
this.tap.reset();
|
||||
}
|
||||
|
||||
public touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (this.swipePoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tapTime && e.timeStamp - this.tapTime > MAX_TAP_INTERVAL) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (!this.tapTime) {
|
||||
this.tap.touchstart(e, points, mapTouches);
|
||||
} else if (mapTouches.length > 0) {
|
||||
this.swipePoint = points[0];
|
||||
this.swipeTouch = mapTouches[0].identifier;
|
||||
}
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (!this.tapTime) {
|
||||
this.tap.touchmove(e, points, mapTouches);
|
||||
} else if (this.swipePoint) {
|
||||
if (mapTouches[0].identifier !== this.swipeTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSwipePoint = points[0];
|
||||
const dist = newSwipePoint.y - this.swipePoint.y;
|
||||
this.swipePoint = newSwipePoint;
|
||||
|
||||
e.preventDefault();
|
||||
this.active = true;
|
||||
|
||||
return {
|
||||
zoomDelta: dist / 128,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (!this.tapTime) {
|
||||
const point = this.tap.touchend(e, points, mapTouches);
|
||||
if (point) {
|
||||
this.tapTime = e.timeStamp;
|
||||
}
|
||||
} else if (this.swipePoint) {
|
||||
if (mapTouches.length === 0) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public touchcancel() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import SingleTapRecognizer, {
|
||||
MAX_DIST,
|
||||
MAX_TAP_INTERVAL,
|
||||
} from './single_tap_recognizer';
|
||||
|
||||
export default class TapRecognizer {
|
||||
public singleTap: SingleTapRecognizer;
|
||||
public numTaps: number;
|
||||
public lastTime: number;
|
||||
public lastTap: Point;
|
||||
public count: number;
|
||||
|
||||
constructor(options: { numTaps: number; numTouches: number }) {
|
||||
this.singleTap = new SingleTapRecognizer(options);
|
||||
this.numTaps = options.numTaps;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.lastTime = Infinity;
|
||||
delete this.lastTap;
|
||||
this.count = 0;
|
||||
this.singleTap.reset();
|
||||
}
|
||||
|
||||
public touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
this.singleTap.touchstart(e, points, mapTouches);
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
this.singleTap.touchmove(e, points, mapTouches);
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
const tap = this.singleTap.touchend(e, points, mapTouches);
|
||||
if (tap) {
|
||||
const soonEnough = e.timeStamp - this.lastTime < MAX_TAP_INTERVAL;
|
||||
const closeEnough = !this.lastTap || this.lastTap.dist(tap) < MAX_DIST;
|
||||
|
||||
if (!soonEnough || !closeEnough) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this.count++;
|
||||
this.lastTime = e.timeStamp;
|
||||
this.lastTap = tap;
|
||||
|
||||
if (this.count === this.numTaps) {
|
||||
this.reset();
|
||||
return tap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import { Map } from '../../map';
|
||||
import TapRecognizer from './tap_recognizer';
|
||||
|
||||
export default class TapZoomHandler {
|
||||
public enabled: boolean;
|
||||
public active: boolean;
|
||||
public zoomIn: TapRecognizer;
|
||||
public zoomOut: TapRecognizer;
|
||||
|
||||
constructor() {
|
||||
this.zoomIn = new TapRecognizer({
|
||||
numTouches: 1,
|
||||
numTaps: 2,
|
||||
});
|
||||
|
||||
this.zoomOut = new TapRecognizer({
|
||||
numTouches: 2,
|
||||
numTaps: 1,
|
||||
});
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
this.zoomIn.reset();
|
||||
this.zoomOut.reset();
|
||||
}
|
||||
|
||||
public touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
this.zoomIn.touchstart(e, points, mapTouches);
|
||||
this.zoomOut.touchstart(e, points, mapTouches);
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
this.zoomIn.touchmove(e, points, mapTouches);
|
||||
this.zoomOut.touchmove(e, points, mapTouches);
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
const zoomInPoint = this.zoomIn.touchend(e, points, mapTouches);
|
||||
const zoomOutPoint = this.zoomOut.touchend(e, points, mapTouches);
|
||||
|
||||
if (zoomInPoint) {
|
||||
this.active = true;
|
||||
e.preventDefault();
|
||||
setTimeout(() => this.reset(), 0);
|
||||
return {
|
||||
cameraAnimation: (map: Map) =>
|
||||
map.easeTo(
|
||||
{
|
||||
duration: 300,
|
||||
zoom: map.getZoom() + 1,
|
||||
around: map.unproject(zoomInPoint),
|
||||
},
|
||||
{ originalEvent: e },
|
||||
),
|
||||
};
|
||||
} else if (zoomOutPoint) {
|
||||
this.active = true;
|
||||
e.preventDefault();
|
||||
setTimeout(() => this.reset(), 0);
|
||||
return {
|
||||
cameraAnimation: (map: Map) =>
|
||||
map.easeTo(
|
||||
{
|
||||
duration: 300,
|
||||
zoom: map.getZoom() - 1,
|
||||
around: map.unproject(zoomOutPoint),
|
||||
},
|
||||
{ originalEvent: e },
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public touchcancel() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import TouchPanHandler from './touch_pan';
|
||||
import TouchPitchHandler from './touch_pitch';
|
||||
import TouchRotateHandler from './touch_rotate';
|
||||
import TouchZoomHandler from './touch_zoom';
|
||||
|
||||
export {
|
||||
TouchPanHandler,
|
||||
TouchPitchHandler,
|
||||
TouchRotateHandler,
|
||||
TouchZoomHandler,
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import { indexTouches } from '../handler_util';
|
||||
|
||||
export default class TouchPanHandler {
|
||||
public enabled: boolean;
|
||||
public active: boolean;
|
||||
public touches: { [key: string]: Point };
|
||||
public minTouches: number;
|
||||
public clickTolerance: number;
|
||||
public sum: Point;
|
||||
|
||||
constructor(options: { clickTolerance: number }) {
|
||||
this.minTouches = 1;
|
||||
this.clickTolerance = options.clickTolerance || 1;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
this.touches = {};
|
||||
this.sum = new Point(0, 0);
|
||||
}
|
||||
|
||||
public touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
return this.calculateTransform(e, points, mapTouches);
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
return this.calculateTransform(e, points, mapTouches);
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
this.calculateTransform(e, points, mapTouches);
|
||||
|
||||
if (this.active && mapTouches.length < this.minTouches) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public touchcancel() {
|
||||
this.reset();
|
||||
}
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
private calculateTransform(
|
||||
e: TouchEvent,
|
||||
points: Point[],
|
||||
mapTouches: Touch[],
|
||||
) {
|
||||
if (mapTouches.length > 0) {
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
const touches = indexTouches(mapTouches, points);
|
||||
|
||||
const touchPointSum = new Point(0, 0);
|
||||
const touchDeltaSum = new Point(0, 0);
|
||||
let touchDeltaCount = 0;
|
||||
|
||||
for (const identifier in touches) {
|
||||
if (touches[identifier]) {
|
||||
const point = touches[identifier];
|
||||
const prevPoint = this.touches[identifier];
|
||||
if (prevPoint) {
|
||||
touchPointSum._add(point);
|
||||
touchDeltaSum._add(point.sub(prevPoint));
|
||||
touchDeltaCount++;
|
||||
touches[identifier] = point;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.touches = touches;
|
||||
|
||||
if (touchDeltaCount < this.minTouches || !touchDeltaSum.mag()) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const panDelta = touchDeltaSum.div(touchDeltaCount);
|
||||
this.sum._add(panDelta);
|
||||
if (this.sum.mag() < this.clickTolerance) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const around = touchPointSum.div(touchDeltaCount);
|
||||
|
||||
return {
|
||||
around,
|
||||
panDelta,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import TwoTouchHandler from './two_touch';
|
||||
|
||||
function isVertical(vector: { x: number; y: number }) {
|
||||
return Math.abs(vector.y) > Math.abs(vector.x);
|
||||
}
|
||||
|
||||
const ALLOWED_SINGLE_TOUCH_TIME = 100;
|
||||
|
||||
export default class TouchPitchHandler extends TwoTouchHandler {
|
||||
public valid: boolean | void;
|
||||
public firstMove: number;
|
||||
public lastPoints: [Point, Point];
|
||||
|
||||
public reset() {
|
||||
super.reset();
|
||||
this.valid = undefined;
|
||||
delete this.firstMove;
|
||||
delete this.lastPoints;
|
||||
}
|
||||
|
||||
public start(points: [Point, Point]) {
|
||||
this.lastPoints = points;
|
||||
if (isVertical(points[0].sub(points[1]))) {
|
||||
// fingers are more horizontal than vertical
|
||||
this.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
public move(points: [Point, Point], center: Point, e: TouchEvent) {
|
||||
const vectorA = points[0].sub(this.lastPoints[0]);
|
||||
const vectorB = points[1].sub(this.lastPoints[1]);
|
||||
|
||||
this.valid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);
|
||||
if (!this.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPoints = points;
|
||||
this.active = true;
|
||||
const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
|
||||
const degreesPerPixelMoved = -0.5;
|
||||
return {
|
||||
pitchDelta: yDeltaAverage * degreesPerPixelMoved,
|
||||
};
|
||||
}
|
||||
|
||||
public gestureBeginsVertically(
|
||||
vectorA: Point,
|
||||
vectorB: Point,
|
||||
timeStamp: number,
|
||||
) {
|
||||
if (this.valid !== undefined) {
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
const threshold = 2;
|
||||
const movedA = vectorA.mag() >= threshold;
|
||||
const movedB = vectorB.mag() >= threshold;
|
||||
|
||||
// neither finger has moved a meaningful amount, wait
|
||||
if (!movedA && !movedB) {
|
||||
return;
|
||||
}
|
||||
|
||||
// One finger has moved and the other has not.
|
||||
// If enough time has passed, decide it is not a pitch.
|
||||
if (!movedA || !movedB) {
|
||||
if (this.firstMove === undefined) {
|
||||
this.firstMove = timeStamp;
|
||||
}
|
||||
|
||||
if (timeStamp - this.firstMove < ALLOWED_SINGLE_TOUCH_TIME) {
|
||||
// still waiting for a movement from the second finger
|
||||
return undefined;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const isSameDirection = vectorA.y > 0 === vectorB.y > 0;
|
||||
return isVertical(vectorA) && isVertical(vectorB) && isSameDirection;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import DOM from '../../utils/dom';
|
||||
import TwoTouchHandler from './two_touch';
|
||||
|
||||
const ROTATION_THRESHOLD = 25; // pixels along circumference of touch circle
|
||||
|
||||
function getBearingDelta(a: Point, b: Point) {
|
||||
return (a.angleWith(b) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
export default class TouchRotateHandler extends TwoTouchHandler {
|
||||
private minDiameter: number;
|
||||
|
||||
public reset() {
|
||||
super.reset();
|
||||
delete this.minDiameter;
|
||||
delete this.startVector;
|
||||
delete this.vector;
|
||||
}
|
||||
|
||||
public start(points: [Point, Point]) {
|
||||
this.startVector = this.vector = points[0].sub(points[1]);
|
||||
this.minDiameter = points[0].dist(points[1]);
|
||||
}
|
||||
|
||||
public move(points: [Point, Point], pinchAround: Point) {
|
||||
const lastVector = this.vector;
|
||||
this.vector = points[0].sub(points[1]);
|
||||
|
||||
if (!this.active && this.isBelowThreshold(this.vector)) {
|
||||
return;
|
||||
}
|
||||
this.active = true;
|
||||
|
||||
return {
|
||||
bearingDelta: getBearingDelta(this.vector, lastVector),
|
||||
pinchAround,
|
||||
};
|
||||
}
|
||||
|
||||
private isBelowThreshold(vector: Point) {
|
||||
/*
|
||||
* The threshold before a rotation actually happens is configured in
|
||||
* pixels alongth circumference of the circle formed by the two fingers.
|
||||
* This makes the threshold in degrees larger when the fingers are close
|
||||
* together and smaller when the fingers are far apart.
|
||||
*
|
||||
* Use the smallest diameter from the whole gesture to reduce sensitivity
|
||||
* when pinching in and out.
|
||||
*/
|
||||
|
||||
this.minDiameter = Math.min(this.minDiameter, vector.mag());
|
||||
const circumference = Math.PI * this.minDiameter;
|
||||
const threshold = (ROTATION_THRESHOLD / circumference) * 360;
|
||||
|
||||
const bearingDeltaSinceStart = getBearingDelta(vector, this.startVector);
|
||||
return Math.abs(bearingDeltaSinceStart) < threshold;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import DOM from '../../utils/dom';
|
||||
import TwoTouchHandler from './two_touch';
|
||||
|
||||
const ZOOM_THRESHOLD = 0.1;
|
||||
function getZoomDelta(distance: number, lastDistance: number) {
|
||||
return Math.log(distance / lastDistance) / Math.LN2;
|
||||
}
|
||||
export default class TouchZoomHandler extends TwoTouchHandler {
|
||||
private distance: number;
|
||||
private startDistance: number;
|
||||
|
||||
public reset() {
|
||||
super.reset();
|
||||
delete this.distance;
|
||||
delete this.startDistance;
|
||||
}
|
||||
|
||||
public start(points: [Point, Point]) {
|
||||
this.startDistance = this.distance = points[0].dist(points[1]);
|
||||
}
|
||||
|
||||
public move(points: [Point, Point], pinchAround: Point) {
|
||||
const lastDistance = this.distance;
|
||||
this.distance = points[0].dist(points[1]);
|
||||
if (
|
||||
!this.active &&
|
||||
Math.abs(getZoomDelta(this.distance, this.startDistance)) < ZOOM_THRESHOLD
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.active = true;
|
||||
return {
|
||||
zoomDelta: getZoomDelta(this.distance, lastDistance),
|
||||
pinchAround,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
// @ts-ignore
|
||||
import Point from '../../geo/point';
|
||||
import DOM from '../../utils/dom';
|
||||
|
||||
export default class TwoTouchHandler {
|
||||
protected enabled: boolean;
|
||||
protected active: boolean;
|
||||
protected firstTwoTouches: [number, number];
|
||||
protected vector: Point;
|
||||
protected startVector: Point;
|
||||
protected aroundCenter: boolean;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.active = false;
|
||||
delete this.firstTwoTouches;
|
||||
}
|
||||
public start(points: [Point, Point]) {
|
||||
return;
|
||||
} // eslint-disable-line
|
||||
public move(
|
||||
points: [Point, Point],
|
||||
pinchAround: Point | null,
|
||||
e: TouchEvent,
|
||||
) {
|
||||
return;
|
||||
} // eslint-disable-line
|
||||
|
||||
public touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
// console.log(e.target, e.targetTouches.length ? e.targetTouches[0].target : null);
|
||||
// log('touchstart', points, e.target.innerHTML, e.targetTouches.length ? e.targetTouches[0].target.innerHTML: undefined);
|
||||
if (this.firstTwoTouches || mapTouches.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.firstTwoTouches = [mapTouches[0].identifier, mapTouches[1].identifier];
|
||||
|
||||
// implemented by child classes
|
||||
this.start([points[0], points[1]]);
|
||||
}
|
||||
|
||||
public touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (!this.firstTwoTouches) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const [idA, idB] = this.firstTwoTouches;
|
||||
const a = getTouchById(mapTouches, points, idA);
|
||||
const b = getTouchById(mapTouches, points, idB);
|
||||
if (!a || !b) {
|
||||
return;
|
||||
}
|
||||
const pinchAround = this.aroundCenter ? null : a.add(b).div(2);
|
||||
|
||||
// implemented by child classes
|
||||
return this.move([a, b], pinchAround, e);
|
||||
}
|
||||
|
||||
public touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]) {
|
||||
if (!this.firstTwoTouches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [idA, idB] = this.firstTwoTouches;
|
||||
const a = getTouchById(mapTouches, points, idA);
|
||||
const b = getTouchById(mapTouches, points, idB);
|
||||
if (a && b) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
DOM.suppressClick();
|
||||
}
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public touchcancel() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public enable(options?: { around?: 'center' }) {
|
||||
this.enabled = true;
|
||||
this.aroundCenter = !!options && options.around === 'center';
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public isActive() {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
|
||||
function getTouchById(
|
||||
mapTouches: Touch[],
|
||||
points: Point[],
|
||||
identifier: number,
|
||||
) {
|
||||
for (let i = 0; i < mapTouches.length; i++) {
|
||||
if (mapTouches[i].identifier === identifier) {
|
||||
return points[i];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// @ts-ignore
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
import throttle from 'lodash/throttle';
|
||||
import { Map } from './map';
|
||||
|
||||
/*
|
||||
* Adds the map's position to its page's location hash.
|
||||
* Passed as an option to the map object.
|
||||
*
|
||||
* @returns {Hash} `this`
|
||||
*/
|
||||
class Hash {
|
||||
private map: Map;
|
||||
private updateHash: () => number | void;
|
||||
private hashName?: string;
|
||||
|
||||
constructor(hashName?: string) {
|
||||
this.hashName = hashName && encodeURIComponent(hashName);
|
||||
|
||||
// Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds.
|
||||
this.updateHash = throttle(this.updateHashUnthrottled, (30 * 1000) / 100);
|
||||
}
|
||||
public addTo(map: Map) {
|
||||
this.map = map;
|
||||
window.addEventListener('hashchange', this.onHashChange, false);
|
||||
this.map.on('moveend', this.updateHash);
|
||||
return this;
|
||||
}
|
||||
public remove() {
|
||||
window.removeEventListener('hashchange', this.onHashChange, false);
|
||||
this.map.off('moveend', this.updateHash);
|
||||
// clearTimeout(this.updateHash());
|
||||
|
||||
delete this.map;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onHashChange = () => {
|
||||
const loc = this.getCurrentHash();
|
||||
if (loc.length >= 3 && !loc.some((v: string) => isNaN(+v))) {
|
||||
const bearing =
|
||||
this.map.dragRotate.isEnabled() && this.map.touchZoomRotate.isEnabled()
|
||||
? +(loc[3] || 0)
|
||||
: this.map.getBearing();
|
||||
this.map.jumpTo({
|
||||
center: [+loc[2], +loc[1]],
|
||||
zoom: +loc[0],
|
||||
bearing,
|
||||
pitch: +(loc[4] || 0),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
private getCurrentHash = () => {
|
||||
// Get the current hash from location, stripped from its number sign
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (this.hashName) {
|
||||
// Split the parameter-styled hash into parts and find the value we need
|
||||
let keyval;
|
||||
hash
|
||||
.split('&')
|
||||
.map((part) => part.split('='))
|
||||
.forEach((part) => {
|
||||
if (part[0] === this.hashName) {
|
||||
keyval = part;
|
||||
}
|
||||
});
|
||||
return (keyval ? keyval[1] || '' : '').split('/');
|
||||
}
|
||||
return hash.split('/');
|
||||
};
|
||||
|
||||
private getHashString(mapFeedback?: boolean) {
|
||||
const center = this.map.getCenter();
|
||||
const zoom = Math.round(this.map.getZoom() * 100) / 100;
|
||||
// derived from equation: 512px * 2^z / 360 / 10^d < 0.5px
|
||||
const precision = Math.ceil(
|
||||
(zoom * Math.LN2 + Math.log(512 / 360 / 0.5)) / Math.LN10,
|
||||
);
|
||||
const m = Math.pow(10, precision);
|
||||
const lng = Math.round(center.lng * m) / m;
|
||||
const lat = Math.round(center.lat * m) / m;
|
||||
const bearing = this.map.getBearing();
|
||||
const pitch = this.map.getPitch();
|
||||
let hash = '';
|
||||
if (mapFeedback) {
|
||||
// new map feedback site has some constraints that don't allow
|
||||
// us to use the same hash format as we do for the Map hash option.
|
||||
hash += `/${lng}/${lat}/${zoom}`;
|
||||
} else {
|
||||
hash += `${zoom}/${lat}/${lng}`;
|
||||
}
|
||||
|
||||
if (bearing || pitch) {
|
||||
hash += `/${Math.round(bearing * 10) / 10}`;
|
||||
}
|
||||
if (pitch) {
|
||||
hash += `/${Math.round(pitch)}`;
|
||||
}
|
||||
|
||||
if (this.hashName) {
|
||||
const hashName = this.hashName;
|
||||
let found = false;
|
||||
const parts = window.location.hash
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.map((part) => {
|
||||
const key = part.split('=')[0];
|
||||
if (key === hashName) {
|
||||
found = true;
|
||||
return `${key}=${hash}`;
|
||||
}
|
||||
return part;
|
||||
})
|
||||
.filter((a) => a);
|
||||
if (!found) {
|
||||
parts.push(`${hashName}=${hash}`);
|
||||
}
|
||||
return `#${parts.join('&')}`;
|
||||
}
|
||||
|
||||
return `#${hash}`;
|
||||
}
|
||||
|
||||
private updateHashUnthrottled = () => {
|
||||
const hash = this.getHashString();
|
||||
try {
|
||||
window.history.replaceState(window.history.state, '', hash);
|
||||
} catch (SecurityError) {
|
||||
// IE11 does not allow this if the page is within an iframe created
|
||||
// with iframe.contentWindow.document.write(...).
|
||||
// https://github.com/mapbox/mapbox-gl-js/issues/7410
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Hash;
|
|
@ -0,0 +1 @@
|
|||
export * from './map';
|
|
@ -0,0 +1,32 @@
|
|||
import { LngLatBoundsLike } from './geo/lng_lat_bounds';
|
||||
|
||||
export interface IMapOptions {
|
||||
hash: boolean;
|
||||
style?: any;
|
||||
container?: HTMLElement | string;
|
||||
center: [number, number];
|
||||
zoom: number;
|
||||
bearing: number;
|
||||
pitch: number;
|
||||
interactive: boolean;
|
||||
scrollZoom: boolean;
|
||||
bounds?: LngLatBoundsLike;
|
||||
maxBounds?: LngLatBoundsLike;
|
||||
fitBoundsOptions?: any;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
minPitch: number;
|
||||
maxPitch: number;
|
||||
boxZoom: boolean;
|
||||
dragRotate: boolean;
|
||||
dragPan: boolean;
|
||||
keyboard: boolean;
|
||||
doubleClickZoom: boolean;
|
||||
touchZoomRotate: boolean;
|
||||
touchPitch: boolean;
|
||||
trackResize: boolean;
|
||||
renderWorldCopies: boolean;
|
||||
bearingSnap: number;
|
||||
clickTolerance: number;
|
||||
pitchWithRotate: boolean;
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
import { DOM } from '@antv/l7-utils';
|
||||
import { merge } from 'lodash';
|
||||
import Camera from './camera';
|
||||
import './css/l7.css';
|
||||
import LngLat, { LngLatLike } from './geo/lng_lat';
|
||||
import LngLatBounds, { LngLatBoundsLike } from './geo/lng_lat_bounds';
|
||||
// @ts-ignore
|
||||
import Point, { PointLike } from './geo/point';
|
||||
import BoxZoomHandler from './handler/box_zoom';
|
||||
import HandlerManager from './handler/handler_manager';
|
||||
import KeyboardHandler from './handler/keyboard';
|
||||
|
||||
import ScrollZoomHandler from './handler/scroll_zoom';
|
||||
import DoubleClickZoomHandler from './handler/shim/dblclick_zoom';
|
||||
import DragPanHandler from './handler/shim/drag_pan';
|
||||
import DragRotateHandler from './handler/shim/drag_rotate';
|
||||
import TouchZoomRotateHandler from './handler/shim/touch_zoom_rotate';
|
||||
import { TouchPitchHandler } from './handler/touch';
|
||||
import Hash from './hash';
|
||||
import { IMapOptions } from './interface';
|
||||
import { renderframe } from './util';
|
||||
import { PerformanceUtils } from './utils/performance';
|
||||
import TaskQueue, { TaskID } from './utils/task_queue';
|
||||
type CallBack = (_: number) => void;
|
||||
const defaultMinZoom = -2;
|
||||
const defaultMaxZoom = 22;
|
||||
|
||||
// the default values, but also the valid range
|
||||
const defaultMinPitch = 0;
|
||||
const defaultMaxPitch = 60;
|
||||
|
||||
const DefaultOptions: IMapOptions = {
|
||||
hash: false,
|
||||
zoom: -1,
|
||||
center: [112, 32],
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
interactive: true,
|
||||
minZoom: defaultMinZoom,
|
||||
maxZoom: defaultMaxZoom,
|
||||
minPitch: defaultMinPitch,
|
||||
maxPitch: defaultMaxPitch,
|
||||
scrollZoom: true,
|
||||
boxZoom: true,
|
||||
dragRotate: true,
|
||||
dragPan: true,
|
||||
keyboard: true,
|
||||
doubleClickZoom: true,
|
||||
touchZoomRotate: true,
|
||||
touchPitch: true,
|
||||
bearingSnap: 7,
|
||||
clickTolerance: 3,
|
||||
pitchWithRotate: true,
|
||||
trackResize: true,
|
||||
renderWorldCopies: true,
|
||||
};
|
||||
export class Map extends Camera {
|
||||
public doubleClickZoom: DoubleClickZoomHandler;
|
||||
public dragRotate: DragRotateHandler;
|
||||
public dragPan: DragPanHandler;
|
||||
public touchZoomRotate: TouchZoomRotateHandler;
|
||||
public scrollZoom: ScrollZoomHandler;
|
||||
public keyboard: KeyboardHandler;
|
||||
public touchPitch: TouchPitchHandler;
|
||||
public boxZoom: BoxZoomHandler;
|
||||
public handlers: HandlerManager;
|
||||
|
||||
private container: HTMLElement;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private canvasContainer: HTMLElement;
|
||||
private renderTaskQueue: TaskQueue = new TaskQueue();
|
||||
private frame: { cancel: () => void } | null;
|
||||
private trackResize: boolean = true;
|
||||
private hash: Hash | undefined;
|
||||
constructor(options: Partial<IMapOptions>) {
|
||||
super(merge({}, DefaultOptions, options));
|
||||
this.initContainer();
|
||||
this.resize();
|
||||
this.handlers = new HandlerManager(this, this.options);
|
||||
// this.on('move', () => this.update());
|
||||
// this.on('moveend', () => this.update());
|
||||
// this.on('zoom', () => {
|
||||
// console.log('zoom');
|
||||
// });
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('online', this.onWindowOnline, false);
|
||||
window.addEventListener('resize', this.onWindowResize, false);
|
||||
window.addEventListener('orientationchange', this.onWindowResize, false);
|
||||
}
|
||||
|
||||
const hashName =
|
||||
(typeof options.hash === 'string' && options.hash) || undefined;
|
||||
if (options.hash) {
|
||||
this.hash = new Hash(hashName).addTo(this) as Hash;
|
||||
}
|
||||
|
||||
// don't set position from options if set through hash
|
||||
if (!this.hash || !this.hash.onHashChange()) {
|
||||
this.jumpTo({
|
||||
center: options.center,
|
||||
zoom: options.zoom,
|
||||
bearing: options.bearing,
|
||||
pitch: options.pitch,
|
||||
});
|
||||
|
||||
if (options.bounds) {
|
||||
this.resize();
|
||||
this.fitBounds(
|
||||
options.bounds,
|
||||
merge({}, options.fitBoundsOptions, { duration: 0 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public resize(eventData?: any) {
|
||||
const dimensions = this.containerDimensions();
|
||||
const width = dimensions[0];
|
||||
const height = dimensions[1];
|
||||
|
||||
// this.resizeCanvas(width, height);
|
||||
this.transform.resize(width, height);
|
||||
const fireMoving = !this.moving;
|
||||
if (fireMoving) {
|
||||
this.stop();
|
||||
this.emit('movestart', new Event('movestart', eventData));
|
||||
this.emit('move', new Event('move', eventData));
|
||||
}
|
||||
|
||||
this.emit('resize', new Event('resize', eventData));
|
||||
|
||||
if (fireMoving) {
|
||||
this.emit('moveend', new Event('moveend', eventData));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public getContainer() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
public getCanvas() {
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
public getCanvasContainer() {
|
||||
return this.canvasContainer;
|
||||
}
|
||||
|
||||
public project(lngLat: LngLatLike) {
|
||||
return this.transform.locationPoint(LngLat.convert(lngLat));
|
||||
}
|
||||
|
||||
public unproject(point: PointLike) {
|
||||
return this.transform.pointLocation(Point.convert(point));
|
||||
}
|
||||
|
||||
public getBounds(): LngLatBounds {
|
||||
return this.transform.getBounds();
|
||||
}
|
||||
|
||||
public getMaxBounds(): LngLatBounds | null {
|
||||
return this.transform.getMaxBounds();
|
||||
}
|
||||
|
||||
public setMaxBounds(bounds: LngLatBoundsLike) {
|
||||
this.transform.setMaxBounds(LngLatBounds.convert(bounds));
|
||||
}
|
||||
|
||||
public setStyle(style: any) {
|
||||
return;
|
||||
}
|
||||
public setMinZoom(minZoom?: number) {
|
||||
minZoom =
|
||||
minZoom === null || minZoom === undefined ? defaultMinZoom : minZoom;
|
||||
if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) {
|
||||
this.transform.minZoom = minZoom;
|
||||
if (this.getZoom() < minZoom) {
|
||||
this.setZoom(minZoom);
|
||||
}
|
||||
|
||||
return this;
|
||||
} else {
|
||||
throw new Error(
|
||||
`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getMinZoom() {
|
||||
return this.transform.minZoom;
|
||||
}
|
||||
|
||||
public setMaxZoom(maxZoom?: number) {
|
||||
maxZoom =
|
||||
maxZoom === null || maxZoom === undefined ? defaultMaxZoom : maxZoom;
|
||||
|
||||
if (maxZoom >= this.transform.minZoom) {
|
||||
this.transform.maxZoom = maxZoom;
|
||||
if (this.getZoom() > maxZoom) {
|
||||
this.setZoom(maxZoom);
|
||||
}
|
||||
|
||||
return this;
|
||||
} else {
|
||||
throw new Error('maxZoom must be greater than the current minZoom');
|
||||
}
|
||||
}
|
||||
public getMaxZoom() {
|
||||
return this.transform.maxZoom;
|
||||
}
|
||||
|
||||
public setMinPitch(minPitch?: number) {
|
||||
minPitch =
|
||||
minPitch === null || minPitch === undefined ? defaultMinPitch : minPitch;
|
||||
|
||||
if (minPitch < defaultMinPitch) {
|
||||
throw new Error(
|
||||
`minPitch must be greater than or equal to ${defaultMinPitch}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) {
|
||||
this.transform.minPitch = minPitch;
|
||||
if (this.getPitch() < minPitch) {
|
||||
this.setPitch(minPitch);
|
||||
}
|
||||
|
||||
return this;
|
||||
} else {
|
||||
throw new Error(
|
||||
`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getMinPitch() {
|
||||
return this.transform.minPitch;
|
||||
}
|
||||
|
||||
public setMaxPitch(maxPitch?: number) {
|
||||
maxPitch =
|
||||
maxPitch === null || maxPitch === undefined ? defaultMaxPitch : maxPitch;
|
||||
|
||||
if (maxPitch > defaultMaxPitch) {
|
||||
throw new Error(
|
||||
`maxPitch must be less than or equal to ${defaultMaxPitch}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (maxPitch >= this.transform.minPitch) {
|
||||
this.transform.maxPitch = maxPitch;
|
||||
if (this.getPitch() > maxPitch) {
|
||||
this.setPitch(maxPitch);
|
||||
}
|
||||
|
||||
return this;
|
||||
} else {
|
||||
throw new Error('maxPitch must be greater than the current minPitch');
|
||||
}
|
||||
}
|
||||
|
||||
public getMaxPitch() {
|
||||
return this.transform.maxPitch;
|
||||
}
|
||||
|
||||
public getRenderWorldCopies() {
|
||||
return this.transform.renderWorldCopies;
|
||||
}
|
||||
|
||||
public setRenderWorldCopies(renderWorldCopies?: boolean) {
|
||||
this.transform.renderWorldCopies = !!renderWorldCopies;
|
||||
}
|
||||
|
||||
public remove() {
|
||||
if (this.frame) {
|
||||
this.frame.cancel();
|
||||
this.frame = null;
|
||||
}
|
||||
this.renderTaskQueue.clear();
|
||||
}
|
||||
|
||||
public requestRenderFrame(cb: CallBack): TaskID {
|
||||
this.update();
|
||||
return this.renderTaskQueue.add(cb);
|
||||
}
|
||||
|
||||
public cancelRenderFrame(id: TaskID) {
|
||||
return this.renderTaskQueue.remove(id);
|
||||
}
|
||||
|
||||
public triggerRepaint() {
|
||||
if (!this.frame) {
|
||||
this.frame = renderframe((paintStartTimeStamp: number) => {
|
||||
PerformanceUtils.frame(paintStartTimeStamp);
|
||||
this.frame = null;
|
||||
this.update(paintStartTimeStamp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public update(time?: number) {
|
||||
if (!this.frame) {
|
||||
this.frame = renderframe((paintStartTimeStamp: number) => {
|
||||
PerformanceUtils.frame(paintStartTimeStamp);
|
||||
this.frame = null;
|
||||
this.renderTaskQueue.run(time);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private initContainer() {
|
||||
if (typeof this.options.container === 'string') {
|
||||
this.container = window.document.getElementById(
|
||||
this.options.container,
|
||||
) as HTMLElement;
|
||||
if (!this.container) {
|
||||
throw new Error(`Container '${this.options.container}' not found.`);
|
||||
}
|
||||
} else if (this.options.container instanceof HTMLElement) {
|
||||
this.container = this.options.container;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid type: 'container' must be a String or HTMLElement.",
|
||||
);
|
||||
}
|
||||
|
||||
const container = this.container;
|
||||
container.classList.add('l7-map');
|
||||
|
||||
const canvasContainer = (this.canvasContainer = DOM.create(
|
||||
'div',
|
||||
'l7-canvas-container',
|
||||
container,
|
||||
));
|
||||
if (this.options.interactive) {
|
||||
canvasContainer.classList.add('l7-interactive');
|
||||
}
|
||||
|
||||
// this.canvas = DOM.create(
|
||||
// 'canvas',
|
||||
// 'l7-canvas',
|
||||
// canvasContainer,
|
||||
// ) as HTMLCanvasElement;
|
||||
// this.canvas.setAttribute('tabindex', '-');
|
||||
// this.canvas.setAttribute('aria-label', 'Map');
|
||||
}
|
||||
|
||||
private containerDimensions(): [number, number] {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
if (this.container) {
|
||||
width = this.container.clientWidth || 400;
|
||||
height = this.container.clientHeight || 300;
|
||||
}
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
private resizeCanvas(width: number, height: number) {
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
this.canvas.width = pixelRatio * width;
|
||||
this.canvas.height = pixelRatio * height;
|
||||
|
||||
// Maintain the same canvas size, potentially downscaling it for HiDPI displays
|
||||
this.canvas.style.width = `${width}px`;
|
||||
this.canvas.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
private onWindowOnline = () => {
|
||||
this.update();
|
||||
};
|
||||
|
||||
private onWindowResize = (event: Event) => {
|
||||
if (this.trackResize) {
|
||||
this.resize({ originalEvent: event }).update();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// @ts-ignore
|
||||
import UnitBezier from '@mapbox/unitbezier';
|
||||
let reducedMotionQuery: MediaQueryList;
|
||||
export interface ICancelable {
|
||||
cancel: () => void;
|
||||
}
|
||||
export function wrap(n: number, min: number, max: number): number {
|
||||
const d = max - min;
|
||||
const w = ((((n - min) % d) + d) % d) + min;
|
||||
return w === min ? max : w;
|
||||
}
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, n));
|
||||
}
|
||||
|
||||
export function interpolate(a: number, b: number, t: number) {
|
||||
return a * (1 - t) + b * t;
|
||||
}
|
||||
export function bezier(
|
||||
p1x: number,
|
||||
p1y: number,
|
||||
p2x: number,
|
||||
p2y: number,
|
||||
): (t: number) => number {
|
||||
const bez = new UnitBezier(p1x, p1y, p2x, p2y);
|
||||
return (t: number) => {
|
||||
return bez.solve(t);
|
||||
};
|
||||
}
|
||||
|
||||
export const ease = bezier(0.25, 0.1, 0.25, 1);
|
||||
|
||||
export function prefersReducedMotion(): boolean {
|
||||
if (!window.matchMedia) {
|
||||
return false;
|
||||
}
|
||||
// Lazily initialize media query
|
||||
if (reducedMotionQuery == null) {
|
||||
reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
}
|
||||
return reducedMotionQuery.matches;
|
||||
}
|
||||
|
||||
export function pick(
|
||||
src: { [key: string]: any },
|
||||
properties: string[],
|
||||
): { [key: string]: any } {
|
||||
const result: { [key: string]: any } = {};
|
||||
for (const name of properties) {
|
||||
if (name in src) {
|
||||
result[name] = src[name];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const now =
|
||||
window.performance && window.performance.now
|
||||
? window.performance.now.bind(window.performance)
|
||||
: Date.now.bind(Date);
|
||||
|
||||
export const raf =
|
||||
window.requestAnimationFrame ||
|
||||
// @ts-ignore
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
// @ts-ignore
|
||||
window.msRequestAnimationFrame;
|
||||
|
||||
export const cancel =
|
||||
window.cancelAnimationFrame ||
|
||||
// @ts-ignore
|
||||
window.mozCancelAnimationFrame ||
|
||||
window.webkitCancelAnimationFrame ||
|
||||
// @ts-ignore
|
||||
window.msCancelAnimationFrame;
|
||||
|
||||
export function renderframe(
|
||||
fn: (paintStartTimestamp: number) => void,
|
||||
): ICancelable {
|
||||
const frame = raf(fn);
|
||||
return { cancel: () => cancel(frame) };
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import { vec3, vec4 } from 'gl-matrix';
|
||||
import Frustum from './primitives';
|
||||
export default class Aabb {
|
||||
public min: vec3;
|
||||
public max: vec3;
|
||||
public center: vec3;
|
||||
|
||||
constructor(min: vec3, max: vec3) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.center = vec3.scale(
|
||||
new Float32Array(3),
|
||||
vec3.add(new Float32Array(3), this.min, this.max),
|
||||
0.5,
|
||||
);
|
||||
}
|
||||
|
||||
public quadrant(index: number): Aabb {
|
||||
const split = [index % 2 === 0, index < 2];
|
||||
const qMin = vec3.clone(this.min);
|
||||
const qMax = vec3.clone(this.max);
|
||||
for (let axis = 0; axis < split.length; axis++) {
|
||||
qMin[axis] = split[axis] ? this.min[axis] : this.center[axis];
|
||||
qMax[axis] = split[axis] ? this.center[axis] : this.max[axis];
|
||||
}
|
||||
// Elevation is always constant, hence quadrant.max.z = this.max.z
|
||||
qMax[2] = this.max[2];
|
||||
return new Aabb(qMin, qMax);
|
||||
}
|
||||
|
||||
public distanceX(point: number[]): number {
|
||||
const pointOnAabb = Math.max(Math.min(this.max[0], point[0]), this.min[0]);
|
||||
return pointOnAabb - point[0];
|
||||
}
|
||||
|
||||
public distanceY(point: number[]): number {
|
||||
const pointOnAabb = Math.max(Math.min(this.max[1], point[1]), this.min[1]);
|
||||
return pointOnAabb - point[1];
|
||||
}
|
||||
|
||||
// Performs a frustum-aabb intersection test. Returns 0 if there's no intersection,
|
||||
// 1 if shapes are intersecting and 2 if the aabb if fully inside the frustum.
|
||||
public intersects(frustum: Frustum): number {
|
||||
// Execute separating axis test between two convex objects to find intersections
|
||||
// Each frustum plane together with 3 major axes define the separating axes
|
||||
// Note: test only 4 points as both min and max points have equal elevation
|
||||
|
||||
const aabbPoints = [
|
||||
[this.min[0], this.min[1], 0.0, 1],
|
||||
[this.max[0], this.min[1], 0.0, 1],
|
||||
[this.max[0], this.max[1], 0.0, 1],
|
||||
[this.min[0], this.max[1], 0.0, 1],
|
||||
];
|
||||
|
||||
let fullyInside = true;
|
||||
|
||||
for (const plane of frustum.planes) {
|
||||
let pointsInside = 0;
|
||||
|
||||
for (const i of aabbPoints) {
|
||||
// @ts-ignore
|
||||
pointsInside += vec4.dot(plane, i) >= 0;
|
||||
}
|
||||
|
||||
if (pointsInside === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (pointsInside !== aabbPoints.length) {
|
||||
fullyInside = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fullyInside) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
for (let axis = 0; axis < 3; axis++) {
|
||||
let projMin = Number.MAX_VALUE;
|
||||
let projMax = -Number.MAX_VALUE;
|
||||
|
||||
for (const p of frustum.points) {
|
||||
const projectedPoint = p[axis] - this.min[axis];
|
||||
|
||||
projMin = Math.min(projMin, projectedPoint);
|
||||
projMax = Math.max(projMax, projectedPoint);
|
||||
}
|
||||
|
||||
if (projMax < 0 || projMin > this.max[axis] - this.min[axis]) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
// @ts-ignore
|
||||
import Point from '../geo/point';
|
||||
|
||||
const DOM: {
|
||||
[key: string]: (...arg: any[]) => any;
|
||||
} = {};
|
||||
export default DOM;
|
||||
|
||||
DOM.create = (tagName: string, className?: string, container?: HTMLElement) => {
|
||||
const el = window.document.createElement(tagName);
|
||||
if (className !== undefined) {
|
||||
el.className = className;
|
||||
}
|
||||
if (container) {
|
||||
container.appendChild(el);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
DOM.createNS = (namespaceURI: string, tagName: string) => {
|
||||
const el = window.document.createElementNS(namespaceURI, tagName);
|
||||
return el;
|
||||
};
|
||||
|
||||
const docStyle = window.document && window.document.documentElement.style;
|
||||
|
||||
function testProp(props: any) {
|
||||
if (!docStyle) {
|
||||
return props[0];
|
||||
}
|
||||
for (const i of props) {
|
||||
if (i in docStyle) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return props[0];
|
||||
}
|
||||
|
||||
const selectProp = testProp([
|
||||
'userSelect',
|
||||
'MozUserSelect',
|
||||
'WebkitUserSelect',
|
||||
'msUserSelect',
|
||||
]);
|
||||
let userSelect: any;
|
||||
|
||||
DOM.disableDrag = () => {
|
||||
if (docStyle && selectProp) {
|
||||
userSelect = docStyle[selectProp];
|
||||
docStyle[selectProp] = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
DOM.enableDrag = () => {
|
||||
if (docStyle && selectProp) {
|
||||
docStyle[selectProp] = userSelect;
|
||||
}
|
||||
};
|
||||
|
||||
const transformProp = testProp(['transform', 'WebkitTransform']);
|
||||
|
||||
DOM.setTransform = (el: HTMLElement, value: string) => {
|
||||
// https://github.com/facebook/flow/issues/7754
|
||||
// $FlowFixMe
|
||||
el.style[transformProp] = value;
|
||||
};
|
||||
|
||||
// Feature detection for {passive: false} support in add/removeEventListener.
|
||||
let passiveSupported = false;
|
||||
|
||||
try {
|
||||
// https://github.com/facebook/flow/issues/285
|
||||
// $FlowFixMe
|
||||
const options = Object.defineProperty({}, 'passive', {
|
||||
get() {
|
||||
// eslint-disable-line
|
||||
passiveSupported = true;
|
||||
},
|
||||
});
|
||||
window.addEventListener('test', options, options);
|
||||
window.removeEventListener('test', options, options);
|
||||
} catch (err) {
|
||||
passiveSupported = false;
|
||||
}
|
||||
|
||||
DOM.addEventListener = (
|
||||
target: any,
|
||||
type: any,
|
||||
callback: any,
|
||||
options: { passive?: boolean; capture?: boolean } = {},
|
||||
) => {
|
||||
if ('passive' in options && passiveSupported) {
|
||||
target.addEventListener(type, callback, options);
|
||||
} else {
|
||||
target.addEventListener(type, callback, options.capture);
|
||||
}
|
||||
};
|
||||
|
||||
DOM.removeEventListener = (
|
||||
target: any,
|
||||
type: any,
|
||||
callback: any,
|
||||
options: { passive?: boolean; capture?: boolean } = {},
|
||||
) => {
|
||||
if ('passive' in options && passiveSupported) {
|
||||
target.removeEventListener(type, callback, options);
|
||||
} else {
|
||||
target.removeEventListener(type, callback, options.capture);
|
||||
}
|
||||
};
|
||||
|
||||
// Suppress the next click, but only if it's immediate.
|
||||
const suppressClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.removeEventListener('click', suppressClick, true);
|
||||
};
|
||||
|
||||
DOM.suppressClick = () => {
|
||||
window.addEventListener('click', suppressClick, true);
|
||||
window.setTimeout(() => {
|
||||
window.removeEventListener('click', suppressClick, true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
DOM.mousePos = (el: HTMLElement, e: MouseEvent | Touch) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return new Point(
|
||||
e.clientX - rect.left - el.clientLeft,
|
||||
e.clientY - rect.top - el.clientTop,
|
||||
);
|
||||
};
|
||||
|
||||
DOM.touchPos = (el: HTMLElement, touches: Touch[]) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const points = [];
|
||||
for (const touche of touches) {
|
||||
points.push(
|
||||
new Point(
|
||||
touche.clientX - rect.left - el.clientLeft,
|
||||
touche.clientY - rect.top - el.clientTop,
|
||||
),
|
||||
);
|
||||
}
|
||||
return points;
|
||||
};
|
||||
|
||||
DOM.mouseButton = (e: MouseEvent) => {
|
||||
if (
|
||||
// @ts-ignore
|
||||
typeof window.InstallTrigger !== 'undefined' &&
|
||||
e.button === 2 &&
|
||||
e.ctrlKey &&
|
||||
window.navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||
) {
|
||||
// Fix for https://github.com/mapbox/mapbox-gl-js/issues/3131:
|
||||
// Firefox (detected by InstallTrigger) on Mac determines e.button = 2 when
|
||||
// using Control + left click
|
||||
return 0;
|
||||
}
|
||||
return e.button;
|
||||
};
|
||||
|
||||
DOM.remove = (node: HTMLElement) => {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
let lastFrameTime: number | null = null;
|
||||
let frameTimes: number[] = [];
|
||||
|
||||
const minFramerateTarget = 30;
|
||||
const frameTimeTarget = 1000 / minFramerateTarget;
|
||||
const performance = window.performance;
|
||||
|
||||
export interface IPerformanceMetrics {
|
||||
loadTime: number;
|
||||
fullLoadTime: number;
|
||||
fps: number;
|
||||
percentDroppedFrames: number;
|
||||
}
|
||||
|
||||
export const PerformanceMarkers = {
|
||||
create: 'create',
|
||||
load: 'load',
|
||||
fullLoad: 'fullLoad',
|
||||
};
|
||||
|
||||
export const PerformanceUtils = {
|
||||
mark(marker: string) {
|
||||
performance.mark(marker);
|
||||
},
|
||||
frame(timestamp: number) {
|
||||
const currTimestamp = timestamp;
|
||||
if (lastFrameTime != null) {
|
||||
const frameTime = currTimestamp - lastFrameTime;
|
||||
frameTimes.push(frameTime);
|
||||
}
|
||||
lastFrameTime = currTimestamp;
|
||||
},
|
||||
clearMetrics() {
|
||||
lastFrameTime = null;
|
||||
frameTimes = [];
|
||||
performance.clearMeasures('loadTime');
|
||||
performance.clearMeasures('fullLoadTime');
|
||||
// @ts-ignore
|
||||
// tslint:disable-next-line:forin
|
||||
for (const marker in PerformanceMarkers) {
|
||||
// @ts-ignore
|
||||
performance.clearMarks(PerformanceMarkers[marker]);
|
||||
}
|
||||
},
|
||||
getPerformanceMetrics(): IPerformanceMetrics {
|
||||
const loadTime = performance.measure(
|
||||
'loadTime',
|
||||
PerformanceMarkers.create,
|
||||
PerformanceMarkers.load,
|
||||
// @ts-ignore
|
||||
).duration;
|
||||
|
||||
const fullLoadTime = performance.measure(
|
||||
'fullLoadTime',
|
||||
PerformanceMarkers.create,
|
||||
PerformanceMarkers.fullLoad,
|
||||
// @ts-ignore
|
||||
).duration;
|
||||
const totalFrames = frameTimes.length;
|
||||
|
||||
const avgFrameTime =
|
||||
frameTimes.reduce((prev, curr) => prev + curr, 0) / totalFrames / 1000;
|
||||
const fps = 1 / avgFrameTime;
|
||||
|
||||
// count frames that missed our framerate target
|
||||
const droppedFrames = frameTimes
|
||||
.filter((frameTime) => frameTime > frameTimeTarget)
|
||||
.reduce((acc, curr) => {
|
||||
return acc + (curr - frameTimeTarget) / frameTimeTarget;
|
||||
}, 0);
|
||||
const percentDroppedFrames =
|
||||
(droppedFrames / (totalFrames + droppedFrames)) * 100;
|
||||
|
||||
return {
|
||||
loadTime,
|
||||
fullLoadTime,
|
||||
fps,
|
||||
percentDroppedFrames,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
import { vec3, vec4 } from 'gl-matrix';
|
||||
export default class Frustum {
|
||||
public static fromInvProjectionMatrix(
|
||||
invProj: Float32Array,
|
||||
worldSize: number,
|
||||
zoom: number,
|
||||
): Frustum {
|
||||
const clipSpaceCorners: Array<[number, number, number, number]> = [
|
||||
[-1, 1, -1, 1],
|
||||
[1, 1, -1, 1],
|
||||
[1, -1, -1, 1],
|
||||
[-1, -1, -1, 1],
|
||||
[-1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, -1, 1, 1],
|
||||
[-1, -1, 1, 1],
|
||||
];
|
||||
|
||||
const scale = Math.pow(2, zoom);
|
||||
|
||||
// Transform frustum corner points from clip space to tile space
|
||||
const frustumCoords = clipSpaceCorners
|
||||
.map((v) => vec4.transformMat4(new Float32Array([]), v, invProj))
|
||||
.map((v) =>
|
||||
vec4.scale(new Float32Array([]), v, (1.0 / v[3] / worldSize) * scale),
|
||||
);
|
||||
|
||||
const frustumPlanePointIndices: Array<[number, number, number]> = [
|
||||
[0, 1, 2], // near
|
||||
[6, 5, 4], // far
|
||||
[0, 3, 7], // left
|
||||
[2, 1, 5], // right
|
||||
[3, 2, 6], // bottom
|
||||
[0, 4, 5], // top
|
||||
];
|
||||
|
||||
const frustumPlanes = frustumPlanePointIndices.map(
|
||||
(p: [number, number, number]) => {
|
||||
const a = vec3.sub(
|
||||
new Float32Array(3),
|
||||
new Float32Array(frustumCoords[p[0]]),
|
||||
new Float32Array(frustumCoords[p[1]]),
|
||||
);
|
||||
const b = vec3.sub(
|
||||
new Float32Array(3),
|
||||
new Float32Array(frustumCoords[p[2]]),
|
||||
new Float32Array(frustumCoords[p[1]]),
|
||||
);
|
||||
const n = vec3.normalize(
|
||||
new Float32Array(3),
|
||||
vec3.cross(new Float32Array(3), a, b),
|
||||
);
|
||||
const d = -vec3.dot(n, new Float32Array(frustumCoords[p[1]]));
|
||||
return (n as number[]).concat(d);
|
||||
},
|
||||
);
|
||||
|
||||
return new Frustum(frustumCoords as number[][], frustumPlanes);
|
||||
}
|
||||
public points: number[][];
|
||||
public planes: number[][];
|
||||
|
||||
constructor(points: number[][], planes: number[][]) {
|
||||
this.points = points;
|
||||
this.planes = planes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
export type TaskID = number; // can't mark opaque due to https://github.com/flowtype/flow-remove-types/pull/61
|
||||
interface ITask {
|
||||
callback: (timeStamp: number) => void;
|
||||
id: TaskID;
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
class TaskQueue {
|
||||
private queue: ITask[];
|
||||
private id: TaskID;
|
||||
private cleared: boolean;
|
||||
private currentlyRunning: ITask[] | false;
|
||||
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.id = 0;
|
||||
this.cleared = false;
|
||||
this.currentlyRunning = false;
|
||||
}
|
||||
|
||||
public add(callback: (timeStamp: number) => void): TaskID {
|
||||
const id = ++this.id;
|
||||
const queue = this.queue;
|
||||
queue.push({ callback, id, cancelled: false });
|
||||
return id;
|
||||
}
|
||||
|
||||
public remove(id: TaskID) {
|
||||
const running = this.currentlyRunning;
|
||||
const queue = running ? this.queue.concat(running) : this.queue;
|
||||
for (const task of queue) {
|
||||
if (task.id === id) {
|
||||
task.cancelled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public run(timeStamp: number = 0) {
|
||||
const queue = (this.currentlyRunning = this.queue);
|
||||
|
||||
// Tasks queued by callbacks in the current queue should be executed
|
||||
// on the next run, not the current run.
|
||||
this.queue = [];
|
||||
|
||||
for (const task of queue) {
|
||||
if (task.cancelled) {
|
||||
continue;
|
||||
}
|
||||
task.callback(timeStamp);
|
||||
if (this.cleared) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.cleared = false;
|
||||
this.currentlyRunning = false;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
if (this.currentlyRunning) {
|
||||
this.cleared = true;
|
||||
}
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
export default TaskQueue;
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"declarationDir": "./es",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"*": ["node_modules", "typings/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import GaodeMap from './amap/';
|
||||
import Map from './map/';
|
||||
import Mapbox from './mapbox/';
|
||||
|
||||
export { GaodeMap, Mapbox };
|
||||
export { GaodeMap, Mapbox, Map };
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { IMapCamera, IViewport } from '@antv/l7-core';
|
||||
import WebMercatorViewport from 'viewport-mercator-project';
|
||||
|
||||
export default class Viewport implements IViewport {
|
||||
private viewport: WebMercatorViewport;
|
||||
|
||||
public syncWithMapCamera(mapCamera: Partial<IMapCamera>) {
|
||||
const {
|
||||
center,
|
||||
zoom,
|
||||
pitch,
|
||||
bearing,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
} = mapCamera;
|
||||
|
||||
/**
|
||||
* Deck.gl 使用的也是 Mapbox 同步相机,相机参数保持一致
|
||||
* 例如相机高度固定为 height * 1.5,因此不需要传
|
||||
*/
|
||||
this.viewport = new WebMercatorViewport({
|
||||
width: viewportWidth,
|
||||
height: viewportHeight,
|
||||
longitude: center && center[0],
|
||||
latitude: center && center[1],
|
||||
zoom,
|
||||
pitch,
|
||||
bearing,
|
||||
});
|
||||
}
|
||||
|
||||
public getZoom(): number {
|
||||
return this.viewport.zoom;
|
||||
}
|
||||
|
||||
public getZoomScale(): number {
|
||||
return Math.pow(2, this.getZoom());
|
||||
}
|
||||
|
||||
public getCenter(): [number, number] {
|
||||
return [this.viewport.longitude, this.viewport.latitude];
|
||||
}
|
||||
|
||||
public getProjectionMatrix(): number[] {
|
||||
return this.viewport.projectionMatrix;
|
||||
}
|
||||
|
||||
public getViewMatrix(): number[] {
|
||||
return this.viewport.viewMatrix;
|
||||
}
|
||||
|
||||
public getViewMatrixUncentered(): number[] {
|
||||
// @ts-ignore
|
||||
return this.viewport.viewMatrixUncentered;
|
||||
}
|
||||
public getViewProjectionMatrix(): number[] {
|
||||
// @ts-ignore
|
||||
return this.viewport.viewProjectionMatrix;
|
||||
}
|
||||
|
||||
public getViewProjectionMatrixUncentered(): number[] {
|
||||
// @ts-ignore
|
||||
return this.viewport.viewProjectionMatrix;
|
||||
}
|
||||
public getFocalDistance() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public projectFlat(
|
||||
lngLat: [number, number],
|
||||
scale?: number | undefined,
|
||||
): [number, number] {
|
||||
return this.viewport.projectFlat(lngLat, scale);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Map } from '@antv/l7-map';
|
||||
import BaseMapWrapper from '../BaseMapWrapper';
|
||||
import MapboxService from './map';
|
||||
export default class MapboxWrapper extends BaseMapWrapper<Map> {
|
||||
protected getServiceConstructor() {
|
||||
return MapboxService;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
/**
|
||||
* MapboxService
|
||||
*/
|
||||
import {
|
||||
Bounds,
|
||||
CoordinateSystem,
|
||||
ICoordinateSystemService,
|
||||
IGlobalConfigService,
|
||||
ILngLat,
|
||||
ILogService,
|
||||
IMapConfig,
|
||||
IMapService,
|
||||
IMercator,
|
||||
IPoint,
|
||||
IStatusOptions,
|
||||
IViewport,
|
||||
MapServiceEvent,
|
||||
MapStyle,
|
||||
TYPES,
|
||||
} from '@antv/l7-core';
|
||||
import { Map } from '@antv/l7-map';
|
||||
import { DOM } from '@antv/l7-utils';
|
||||
import { mat4, vec2, vec3 } from 'gl-matrix';
|
||||
import { inject, injectable } from 'inversify';
|
||||
|
||||
import Viewport from './Viewport';
|
||||
const EventMap: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
mapmove: 'move',
|
||||
camerachange: 'move',
|
||||
zoomchange: 'zoom',
|
||||
dragging: 'drag',
|
||||
};
|
||||
import { MapTheme } from './theme';
|
||||
|
||||
const LNGLAT_OFFSET_ZOOM_THRESHOLD = 12;
|
||||
/**
|
||||
* AMapService
|
||||
*/
|
||||
@injectable()
|
||||
export default class L7MapService implements IMapService<Map> {
|
||||
public map: Map;
|
||||
|
||||
@inject(TYPES.MapConfig)
|
||||
private readonly config: Partial<IMapConfig>;
|
||||
|
||||
@inject(TYPES.IGlobalConfigService)
|
||||
private readonly configService: IGlobalConfigService;
|
||||
|
||||
@inject(TYPES.ILogService)
|
||||
private readonly logger: ILogService;
|
||||
@inject(TYPES.ICoordinateSystemService)
|
||||
private readonly coordinateSystemService: ICoordinateSystemService;
|
||||
|
||||
@inject(TYPES.IEventEmitter)
|
||||
private eventEmitter: any;
|
||||
private viewport: Viewport;
|
||||
private markerContainer: HTMLElement;
|
||||
private cameraChangedCallback: (viewport: IViewport) => void;
|
||||
private $mapContainer: HTMLElement | null;
|
||||
|
||||
// init
|
||||
public addMarkerContainer(): void {
|
||||
const container = this.map.getCanvasContainer();
|
||||
this.markerContainer = DOM.create('div', 'l7-marker-container', container);
|
||||
this.markerContainer.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
public getMarkerContainer(): HTMLElement {
|
||||
return this.markerContainer;
|
||||
}
|
||||
|
||||
// map event
|
||||
public on(type: string, handle: (...args: any[]) => void): void {
|
||||
if (MapServiceEvent.indexOf(type) !== -1) {
|
||||
this.eventEmitter.on(type, handle);
|
||||
} else {
|
||||
// 统一事件名称
|
||||
this.map.on(EventMap[type] || type, handle);
|
||||
}
|
||||
}
|
||||
public off(type: string, handle: (...args: any[]) => void): void {
|
||||
this.map.off(EventMap[type] || type, handle);
|
||||
}
|
||||
|
||||
public getContainer(): HTMLElement | null {
|
||||
return this.map.getContainer();
|
||||
}
|
||||
|
||||
public getMapCanvasContainer(): HTMLElement {
|
||||
return this.map.getCanvasContainer() as HTMLElement;
|
||||
}
|
||||
|
||||
public getSize(): [number, number] {
|
||||
const size = this.map.transform;
|
||||
return [size.width, size.height];
|
||||
}
|
||||
// get mapStatus method
|
||||
|
||||
public getType() {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
public getZoom(): number {
|
||||
return this.map.getZoom();
|
||||
}
|
||||
|
||||
public setZoom(zoom: number) {
|
||||
return this.map.setZoom(zoom);
|
||||
}
|
||||
|
||||
public getCenter(): ILngLat {
|
||||
return this.map.getCenter();
|
||||
}
|
||||
|
||||
public setCenter(lnglat: [number, number]): void {
|
||||
this.map.setCenter(lnglat);
|
||||
}
|
||||
|
||||
public getPitch(): number {
|
||||
return this.map.getPitch();
|
||||
}
|
||||
|
||||
public getRotation(): number {
|
||||
return this.map.getBearing();
|
||||
}
|
||||
|
||||
public getBounds(): Bounds {
|
||||
return this.map.getBounds().toArray() as Bounds;
|
||||
}
|
||||
|
||||
public getMinZoom(): number {
|
||||
return this.map.getMinZoom();
|
||||
}
|
||||
|
||||
public getMaxZoom(): number {
|
||||
return this.map.getMaxZoom();
|
||||
}
|
||||
|
||||
public setRotation(rotation: number): void {
|
||||
this.map.setBearing(rotation);
|
||||
}
|
||||
|
||||
public zoomIn(option?: any, eventData?: any): void {
|
||||
this.map.zoomIn(option, eventData);
|
||||
}
|
||||
public zoomOut(option?: any, eventData?: any): void {
|
||||
this.map.zoomOut(option, eventData);
|
||||
}
|
||||
public setPitch(pitch: number) {
|
||||
return this.map.setPitch(pitch);
|
||||
}
|
||||
|
||||
public panTo(p: [number, number]): void {
|
||||
this.map.panTo(p);
|
||||
}
|
||||
|
||||
public panBy(pixel: [number, number]): void {
|
||||
this.panTo(pixel);
|
||||
}
|
||||
|
||||
public fitBounds(bound: Bounds, fitBoundsOptions?: any): void {
|
||||
this.map.fitBounds(bound, fitBoundsOptions);
|
||||
}
|
||||
|
||||
public setMaxZoom(max: number): void {
|
||||
this.map.setMaxZoom(max);
|
||||
}
|
||||
|
||||
public setMinZoom(min: number): void {
|
||||
this.map.setMinZoom(min);
|
||||
}
|
||||
public setMapStatus(option: Partial<IStatusOptions>): void {
|
||||
if (option.doubleClickZoom === true) {
|
||||
this.map.doubleClickZoom.enable();
|
||||
}
|
||||
if (option.doubleClickZoom === false) {
|
||||
this.map.doubleClickZoom.disable();
|
||||
}
|
||||
if (option.dragEnable === false) {
|
||||
this.map.dragPan.disable();
|
||||
}
|
||||
if (option.dragEnable === true) {
|
||||
this.map.dragPan.enable();
|
||||
}
|
||||
if (option.rotateEnable === false) {
|
||||
this.map.dragRotate.disable();
|
||||
}
|
||||
if (option.dragEnable === true) {
|
||||
this.map.dragRotate.enable();
|
||||
}
|
||||
if (option.keyboardEnable === false) {
|
||||
this.map.keyboard.disable();
|
||||
}
|
||||
if (option.keyboardEnable === true) {
|
||||
this.map.keyboard.enable();
|
||||
}
|
||||
if (option.zoomEnable === false) {
|
||||
this.map.scrollZoom.disable();
|
||||
}
|
||||
if (option.zoomEnable === true) {
|
||||
this.map.scrollZoom.enable();
|
||||
}
|
||||
}
|
||||
|
||||
public setZoomAndCenter(zoom: number, center: [number, number]): void {
|
||||
this.map.flyTo({
|
||||
zoom,
|
||||
center,
|
||||
});
|
||||
}
|
||||
|
||||
public setMapStyle(style: any): void {
|
||||
this.map.setStyle(this.getMapStyle(style));
|
||||
}
|
||||
// TODO: 计算像素坐标
|
||||
public pixelToLngLat(pixel: [number, number]): ILngLat {
|
||||
return this.map.unproject(pixel);
|
||||
}
|
||||
|
||||
public lngLatToPixel(lnglat: [number, number]): IPoint {
|
||||
return this.map.project(lnglat);
|
||||
}
|
||||
|
||||
public containerToLngLat(pixel: [number, number]): ILngLat {
|
||||
return this.map.unproject(pixel);
|
||||
}
|
||||
|
||||
public lngLatToContainer(lnglat: [number, number]): IPoint {
|
||||
return this.map.project(lnglat);
|
||||
}
|
||||
public lngLatToMercator(
|
||||
lnglat: [number, number],
|
||||
altitude: number,
|
||||
): IMercator {
|
||||
throw new Error('not implement');
|
||||
}
|
||||
public getModelMatrix(
|
||||
lnglat: [number, number],
|
||||
altitude: number,
|
||||
rotate: [number, number, number],
|
||||
scale: [number, number, number] = [1, 1, 1],
|
||||
origin: IMercator = { x: 0, y: 0, z: 0 },
|
||||
): number[] {
|
||||
throw new Error('not implement');
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
const {
|
||||
id = 'map',
|
||||
attributionControl = false,
|
||||
style = 'light',
|
||||
rotation = 0,
|
||||
mapInstance,
|
||||
...rest
|
||||
} = this.config;
|
||||
|
||||
this.viewport = new Viewport();
|
||||
|
||||
if (mapInstance) {
|
||||
// @ts-ignore
|
||||
this.map = mapInstance;
|
||||
this.$mapContainer = this.map.getContainer();
|
||||
} else {
|
||||
this.$mapContainer = this.creatAmapContainer(id);
|
||||
// @ts-ignore
|
||||
this.map = new Map({
|
||||
container: this.$mapContainer,
|
||||
style: this.getMapStyle(style),
|
||||
bearing: rotation,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
this.map.on('load', this.handleCameraChanged);
|
||||
this.map.on('move', this.handleCameraChanged);
|
||||
|
||||
// 不同于高德地图,需要手动触发首次渲染
|
||||
this.handleCameraChanged();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.eventEmitter.removeAllListeners();
|
||||
if (this.map) {
|
||||
this.map.remove();
|
||||
this.$mapContainer = null;
|
||||
}
|
||||
}
|
||||
public emit(name: string, ...args: any[]) {
|
||||
this.eventEmitter.emit(name, ...args);
|
||||
}
|
||||
public once(name: string, ...args: any[]) {
|
||||
this.eventEmitter.once(name, ...args);
|
||||
}
|
||||
|
||||
public getMapContainer() {
|
||||
return this.$mapContainer;
|
||||
}
|
||||
|
||||
public exportMap(type: 'jpg' | 'png'): string {
|
||||
const renderCanvas = this.map.getCanvas();
|
||||
const layersPng =
|
||||
type === 'jpg'
|
||||
? (renderCanvas?.toDataURL('image/jpeg') as string)
|
||||
: (renderCanvas?.toDataURL('image/png') as string);
|
||||
return layersPng;
|
||||
}
|
||||
public onCameraChanged(callback: (viewport: IViewport) => void): void {
|
||||
this.cameraChangedCallback = callback;
|
||||
}
|
||||
|
||||
private handleCameraChanged = () => {
|
||||
const { lat, lng } = this.map.getCenter();
|
||||
|
||||
// resync
|
||||
this.viewport.syncWithMapCamera({
|
||||
bearing: this.map.getBearing(),
|
||||
center: [lng, lat],
|
||||
viewportHeight: this.map.transform.height,
|
||||
pitch: this.map.getPitch(),
|
||||
viewportWidth: this.map.transform.width,
|
||||
zoom: this.map.getZoom(),
|
||||
// mapbox 中固定相机高度为 viewport 高度的 1.5 倍
|
||||
cameraHeight: 0,
|
||||
});
|
||||
// set coordinate system
|
||||
if (this.viewport.getZoom() > LNGLAT_OFFSET_ZOOM_THRESHOLD) {
|
||||
this.coordinateSystemService.setCoordinateSystem(
|
||||
CoordinateSystem.LNGLAT_OFFSET,
|
||||
);
|
||||
} else {
|
||||
this.coordinateSystemService.setCoordinateSystem(CoordinateSystem.LNGLAT);
|
||||
}
|
||||
|
||||
this.cameraChangedCallback(this.viewport);
|
||||
};
|
||||
|
||||
private creatAmapContainer(id: string | HTMLDivElement) {
|
||||
let $wrapper = id as HTMLDivElement;
|
||||
if (typeof id === 'string') {
|
||||
$wrapper = document.getElementById(id) as HTMLDivElement;
|
||||
}
|
||||
return $wrapper;
|
||||
}
|
||||
private getMapStyle(name: MapStyle) {
|
||||
if (typeof name !== 'string') {
|
||||
return name;
|
||||
}
|
||||
return MapTheme[name] ? MapTheme[name] : name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
export const MapTheme: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
light: 'mapbox://styles/zcxduo/ck2ypyb1r3q9o1co1766dex29',
|
||||
dark: 'mapbox://styles/zcxduo/ck241p6413s0b1cpayzldv7x7',
|
||||
normal: 'mapbox://styles/mapbox/streets-v11',
|
||||
blank: {
|
||||
version: 8,
|
||||
// sprite: 'https://lzxue.github.io/font-glyphs/sprite/sprite',
|
||||
// glyphs:
|
||||
// 'https://gw.alipayobjects.com/os/antvdemo/assets/mapbox/glyphs/{fontstack}/{range}.pbf',
|
||||
sources: {},
|
||||
layers: [
|
||||
{
|
||||
id: 'background',
|
||||
type: 'background',
|
||||
layout: {
|
||||
visibility: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -37,7 +37,7 @@ const EventMap: {
|
|||
dragging: 'drag',
|
||||
};
|
||||
import { MapTheme } from './theme';
|
||||
|
||||
let mapdivCount = 0;
|
||||
const LNGLAT_OFFSET_ZOOM_THRESHOLD = 12;
|
||||
const MAPBOX_API_KEY =
|
||||
'pk.eyJ1IjoibHp4dWUiLCJhIjoiYnhfTURyRSJ9.Ugm314vAKPHBzcPmY1p4KQ';
|
||||
|
@ -71,6 +71,7 @@ export default class MapboxService
|
|||
public addMarkerContainer(): void {
|
||||
const container = this.map.getCanvasContainer();
|
||||
this.markerContainer = DOM.create('div', 'l7-marker-container', container);
|
||||
this.markerContainer.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
public getMarkerContainer(): HTMLElement {
|
||||
|
@ -404,12 +405,28 @@ export default class MapboxService
|
|||
this.cameraChangedCallback(this.viewport);
|
||||
};
|
||||
|
||||
// private creatAmapContainer(id: string | HTMLDivElement) {
|
||||
// let $wrapper = id as HTMLDivElement;
|
||||
// if (typeof id === 'string') {
|
||||
// $wrapper = document.getElementById(id) as HTMLDivElement;
|
||||
// }
|
||||
// return $wrapper;
|
||||
// }
|
||||
private creatAmapContainer(id: string | HTMLDivElement) {
|
||||
let $wrapper = id as HTMLDivElement;
|
||||
if (typeof id === 'string') {
|
||||
$wrapper = document.getElementById(id) as HTMLDivElement;
|
||||
}
|
||||
return $wrapper;
|
||||
const $amapdiv = document.createElement('div');
|
||||
$amapdiv.style.cssText += `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
$amapdiv.id = 'l7_mapbox_div' + mapdivCount++;
|
||||
$wrapper.appendChild($amapdiv);
|
||||
return $amapdiv;
|
||||
}
|
||||
private getMapStyle(name: MapStyle) {
|
||||
if (typeof name !== 'string') {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { createPortal } from 'react-dom';
|
|||
import { SceneContext } from './SceneContext';
|
||||
interface IPopupProps {
|
||||
option?: Partial<IPopupOption>;
|
||||
lnglat: number[];
|
||||
lnglat: number[] | { lng: number; lat: number };
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export default class PopupComponet extends React.PureComponent<IPopupProps> {
|
||||
|
@ -25,7 +25,10 @@ export default class PopupComponet extends React.PureComponent<IPopupProps> {
|
|||
}
|
||||
public componentDidMount() {
|
||||
const { lnglat, children, option } = this.props;
|
||||
const p = new Popup(option);
|
||||
const p = new Popup({
|
||||
...option,
|
||||
stopPropagation: false,
|
||||
});
|
||||
|
||||
if (lnglat) {
|
||||
p.setLnglat(lnglat);
|
||||
|
@ -38,12 +41,21 @@ export default class PopupComponet extends React.PureComponent<IPopupProps> {
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IPopupProps) {
|
||||
const positionChanged =
|
||||
prevProps?.lnglat?.toString() !== this.props?.lnglat?.toString();
|
||||
// @ts-ignore
|
||||
const preLnglat = Array.isArray(prevProps.lnglat)
|
||||
? prevProps.lnglat
|
||||
: [prevProps?.lnglat?.lng, prevProps?.lnglat?.lat];
|
||||
const nowLnglat = Array.isArray(this.props.lnglat)
|
||||
? this.props.lnglat
|
||||
: [this.props?.lnglat?.lng, this.props?.lnglat?.lat];
|
||||
const positionChanged = preLnglat.toString() !== nowLnglat.toString();
|
||||
|
||||
if (positionChanged) {
|
||||
this.popup.remove();
|
||||
this.popup = new Popup(this.props.option);
|
||||
this.popup = new Popup({
|
||||
...this.props.option,
|
||||
stopPropagation: false,
|
||||
});
|
||||
this.popup.setLnglat(this.props.lnglat);
|
||||
this.popup.setDOMContent(this.el);
|
||||
this.scene.addPopup(this.popup);
|
||||
|
|
|
@ -73,7 +73,11 @@ export default class ReglTexture2D implements ITexture2D {
|
|||
public get() {
|
||||
return this.texture;
|
||||
}
|
||||
public update() {
|
||||
public update(props: regl.Texture2DOptions = {}) {
|
||||
this.texture(props);
|
||||
}
|
||||
|
||||
public bind() {
|
||||
// @ts-ignore
|
||||
this.texture._texture.bind();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// @ts-ignore
|
||||
import { PolygonLayer } from '@antv/l7-layers';
|
||||
import { Map } from '@antv/l7-maps';
|
||||
import { Scene } from '../src/';
|
||||
describe('template', () => {
|
||||
const el = document.createElement('div');
|
||||
el.id = 'test-div-id';
|
||||
el.style.width = '500px';
|
||||
el.style.height = '500px';
|
||||
document.querySelector('body')?.appendChild(el);
|
||||
const scene = new Scene({
|
||||
id: 'test-div-id',
|
||||
map: new Map({
|
||||
center: [110.19382669582967, 30.258134],
|
||||
pitch: 0,
|
||||
zoom: 2,
|
||||
}),
|
||||
});
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json',
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const layer = new PolygonLayer({
|
||||
name: '01',
|
||||
});
|
||||
|
||||
layer
|
||||
.source(data)
|
||||
.size('name', [0, 10000, 50000, 30000, 100000])
|
||||
.color('name', [
|
||||
'#2E8AE6',
|
||||
'#69D1AB',
|
||||
'#DAF291',
|
||||
'#FFD591',
|
||||
'#FF7A45',
|
||||
'#CF1D49',
|
||||
])
|
||||
.shape('fill')
|
||||
.select(true)
|
||||
.style({
|
||||
opacity: 1.0,
|
||||
});
|
||||
scene.addLayer(layer);
|
||||
});
|
||||
it('scene l7 map method', () => {
|
||||
// console.log(scene.getZoom());
|
||||
});
|
||||
});
|
|
@ -58,9 +58,9 @@ export default class GlTFThreeJSDemo extends React.Component {
|
|||
y: 'latitude',
|
||||
},
|
||||
})
|
||||
.shape('name', ['00', '01', '02'])
|
||||
// .shape('triangle')
|
||||
// .color('red')
|
||||
// .shape('name', ['00', '01', '02'])
|
||||
.shape('triangle')
|
||||
.color('red')
|
||||
.active(true)
|
||||
.size(20);
|
||||
scene.addLayer(imageLayer);
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class MarkerComponent extends React.Component {
|
|||
|
||||
const popup = new Popup({
|
||||
offsets: [0, 20],
|
||||
}).setText('hello');
|
||||
}).setHTML('<h1 onclick= alert("12223")>11111</h1>');
|
||||
|
||||
const marker = new Marker({
|
||||
offsets: [0, -20],
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class Country extends React.Component {
|
|||
public async componentDidMount() {
|
||||
const scene = new Scene({
|
||||
id: 'map',
|
||||
map: new GaodeMap({
|
||||
map: new Mapbox({
|
||||
center: [116.2825, 39.9],
|
||||
pitch: 0,
|
||||
style: 'blank',
|
||||
|
@ -50,8 +50,11 @@ export default class Country extends React.Component {
|
|||
},
|
||||
popup: {
|
||||
enable: true,
|
||||
openTriggerEvent: 'click',
|
||||
Html: (props: any) => {
|
||||
return `<span>${props.NAME_CHN + ':' + props.value}</span>`;
|
||||
return `<span><button onclick='alert(11111)'>点击</button>${props.NAME_CHN +
|
||||
':' +
|
||||
props.value}</span>`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,6 +22,9 @@ export default class Point3D extends React.Component {
|
|||
}),
|
||||
});
|
||||
const pointLayer = new PointLayer();
|
||||
scene.on('resize', () => {
|
||||
console.log('resize');
|
||||
});
|
||||
pointLayer
|
||||
.source(data, {
|
||||
cluster: true,
|
||||
|
|
|
@ -70,9 +70,9 @@ export default React.memo(function Map() {
|
|||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/e62a2f3b-ea99-4c98-9314-01d7c886263d.json',
|
||||
).then((d) => d.json()),
|
||||
fetch('https://lab.isaaclin.cn/nCoV/api/area?latest=1').then((d) =>
|
||||
d.json(),
|
||||
),
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/55a7dd2e-3fb4-4442-8899-900bb03ee67a.json',
|
||||
).then((d) => d.json()),
|
||||
]);
|
||||
setData(joinData(geoData, ncovData.results));
|
||||
};
|
||||
|
|
|
@ -85,9 +85,9 @@ export default React.memo(function Map() {
|
|||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/e62a2f3b-ea99-4c98-9314-01d7c886263d.json',
|
||||
).then((d) => d.json()),
|
||||
fetch('https://lab.isaaclin.cn/nCoV/api/area?latest=1').then((d) =>
|
||||
d.json(),
|
||||
),
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/55a7dd2e-3fb4-4442-8899-900bb03ee67a.json',
|
||||
).then((d) => d.json()),
|
||||
]);
|
||||
const worldData = joinData(geoData, ncovData.results);
|
||||
const pointdata = worldData.features.map((feature: any) => {
|
||||
|
@ -133,7 +133,17 @@ export default React.memo(function Map() {
|
|||
margin: 0,
|
||||
}}
|
||||
>
|
||||
<li>现有确诊:{popupInfo.feature.currentConfirmedCount}</li>
|
||||
<li>
|
||||
<button
|
||||
onMouseDown={() => {
|
||||
alert('test');
|
||||
}}
|
||||
value="点击"
|
||||
>
|
||||
点击
|
||||
</button>
|
||||
现有确诊:{popupInfo.feature.currentConfirmedCount}
|
||||
</li>
|
||||
<li>累计确诊:{popupInfo.feature.confirmedCount}</li>
|
||||
<li>治愈:{popupInfo.feature.curedCount}</li>
|
||||
<li>死亡:{popupInfo.feature.deadCount}</li>
|
||||
|
@ -204,22 +214,7 @@ export default React.memo(function Map() {
|
|||
},
|
||||
}}
|
||||
color={{
|
||||
field: 'confirmedCount',
|
||||
values: (count) => {
|
||||
return count > 10000
|
||||
? colors[6]
|
||||
: count > 1000
|
||||
? colors[5]
|
||||
: count > 500
|
||||
? colors[4]
|
||||
: count > 100
|
||||
? colors[3]
|
||||
: count > 10
|
||||
? colors[2]
|
||||
: count > 1
|
||||
? colors[1]
|
||||
: colors[0];
|
||||
},
|
||||
values: '#b10026',
|
||||
}}
|
||||
shape={{
|
||||
values: 'circle',
|
||||
|
@ -237,8 +232,8 @@ export default React.memo(function Map() {
|
|||
opacity: 0.6,
|
||||
}}
|
||||
>
|
||||
<LayerEvent type="mousemove" handler={showPopup} />
|
||||
<LayerEvent type="mouseout" handler={hidePopup} />
|
||||
<LayerEvent type="click" handler={showPopup} />
|
||||
{/* <LayerEvent type="mouseout" handler={hidePopup} /> */}
|
||||
</PointLayer>,
|
||||
<PointLayer
|
||||
key={'5'}
|
||||
|
@ -269,7 +264,7 @@ export default React.memo(function Map() {
|
|||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<LayerEvent type="mousemove" handler={showPopup} />
|
||||
<LayerEvent type="click" handler={showPopup} />
|
||||
</PointLayer>,
|
||||
]}
|
||||
</MapboxScene>
|
||||
|
|
|
@ -77,9 +77,9 @@ export default React.memo(function Map() {
|
|||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/e62a2f3b-ea99-4c98-9314-01d7c886263d.json',
|
||||
).then((d) => d.json()),
|
||||
fetch('https://lab.isaaclin.cn/nCoV/api/area?latest=1').then((d) =>
|
||||
d.json(),
|
||||
),
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/55a7dd2e-3fb4-4442-8899-900bb03ee67a.json',
|
||||
).then((d) => d.json()),
|
||||
]);
|
||||
const worldData = joinData(geoData, ncovData.results);
|
||||
const pointdata = worldData.features.map((feature: any) => {
|
||||
|
|
|
@ -72,9 +72,9 @@ export default React.memo(function Map() {
|
|||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/e62a2f3b-ea99-4c98-9314-01d7c886263d.json',
|
||||
).then((d) => d.json()),
|
||||
fetch('https://lab.isaaclin.cn/nCoV/api/area?latest=1').then((d) =>
|
||||
d.json(),
|
||||
),
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/55a7dd2e-3fb4-4442-8899-900bb03ee67a.json',
|
||||
).then((d) => d.json()),
|
||||
]);
|
||||
const worldData = joinData(geoData, ncovData.results);
|
||||
const pointdata = worldData.features.map((feature: any) => {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
LayerEvent,
|
||||
LineLayer,
|
||||
MapboxScene,
|
||||
AMapScene,
|
||||
Marker,
|
||||
PolygonLayer,
|
||||
Popup,
|
||||
|
@ -64,15 +66,27 @@ function joinData(geodata: any, ncovData: any) {
|
|||
|
||||
export default React.memo(function Map() {
|
||||
const [data, setData] = React.useState();
|
||||
const [popupInfo, setPopupInfo] = React.useState<{
|
||||
lnglat: number[];
|
||||
feature: any;
|
||||
}>();
|
||||
|
||||
function showPopup(args: any): void {
|
||||
console.log(args.lngLat);
|
||||
setPopupInfo({
|
||||
lnglat: args.lngLat,
|
||||
feature: args.feature,
|
||||
});
|
||||
}
|
||||
React.useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const [geoData, ncovData] = await Promise.all([
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/e62a2f3b-ea99-4c98-9314-01d7c886263d.json',
|
||||
).then((d) => d.json()),
|
||||
fetch('https://lab.isaaclin.cn/nCoV/api/area?latest=1').then((d) =>
|
||||
d.json(),
|
||||
),
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/bmw-prod/55a7dd2e-3fb4-4442-8899-900bb03ee67a.json',
|
||||
).then((d) => d.json()),
|
||||
]);
|
||||
setData(joinData(geoData, ncovData.results));
|
||||
};
|
||||
|
@ -80,7 +94,7 @@ export default React.memo(function Map() {
|
|||
}, []);
|
||||
return (
|
||||
<>
|
||||
<MapboxScene
|
||||
<AMapScene
|
||||
map={{
|
||||
center: [110.19382669582967, 50.258134],
|
||||
pitch: 0,
|
||||
|
@ -95,6 +109,31 @@ export default React.memo(function Map() {
|
|||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
{popupInfo && (
|
||||
<Popup lnglat={popupInfo.lnglat}>
|
||||
{popupInfo.feature.name}
|
||||
<ul
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
onClick={() => {
|
||||
alert('test');
|
||||
}}
|
||||
value="点击"
|
||||
>
|
||||
点击
|
||||
</button>
|
||||
现有确诊:{popupInfo.feature.currentConfirmedCount}
|
||||
</li>
|
||||
<li>累计确诊:{popupInfo.feature.confirmedCount}</li>
|
||||
<li>治愈:{popupInfo.feature.curedCount}</li>
|
||||
<li>死亡:{popupInfo.feature.deadCount}</li>
|
||||
</ul>
|
||||
</Popup>
|
||||
)}
|
||||
{data && [
|
||||
<PolygonLayer
|
||||
key={'1'}
|
||||
|
@ -137,7 +176,11 @@ export default React.memo(function Map() {
|
|||
style={{
|
||||
opacity: 1,
|
||||
}}
|
||||
/>,
|
||||
>
|
||||
<LayerEvent type="click" handler={showPopup} />
|
||||
{/* <LayerEvent type="mouseout" handler={hidePopup} /> */}
|
||||
</PolygonLayer>,
|
||||
,
|
||||
<LineLayer
|
||||
key={'2'}
|
||||
source={{
|
||||
|
@ -157,7 +200,7 @@ export default React.memo(function Map() {
|
|||
}}
|
||||
/>,
|
||||
]}
|
||||
</MapboxScene>
|
||||
</AMapScene>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
// @ts-ignore
|
||||
import { Scene } from '@antv/l7';
|
||||
import { PolygonLayer } from '@antv/l7-layers';
|
||||
import { Map } from '@antv/l7-maps';
|
||||
import * as React from 'react';
|
||||
|
||||
export default class ScaleComponent extends React.Component {
|
||||
private scene: Scene;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.scene.destroy();
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const scene = new Scene({
|
||||
id: 'map',
|
||||
map: new Map({
|
||||
hash: true,
|
||||
center: [110.19382669582967, 30.258134],
|
||||
pitch: 0,
|
||||
zoom: 2,
|
||||
}),
|
||||
});
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json',
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const layer = new PolygonLayer({
|
||||
name: '01',
|
||||
});
|
||||
|
||||
layer
|
||||
.source(data)
|
||||
.size('name', [0, 10000, 50000, 30000, 100000])
|
||||
.color('name', [
|
||||
'#2E8AE6',
|
||||
'#69D1AB',
|
||||
'#DAF291',
|
||||
'#FFD591',
|
||||
'#FF7A45',
|
||||
'#CF1D49',
|
||||
])
|
||||
.shape('fill')
|
||||
.select(true)
|
||||
.style({
|
||||
opacity: 1.0,
|
||||
});
|
||||
scene.addLayer(layer);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
id="map"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// @ts-ignore
|
||||
import { Scene } from '@antv/l7';
|
||||
import { PolygonLayer } from '@antv/l7-layers';
|
||||
import { Map } from '@antv/l7-maps';
|
||||
import * as React from 'react';
|
||||
|
||||
export default class ScaleComponent extends React.Component {
|
||||
private scene: Scene;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.scene.destroy();
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const scene = new Scene({
|
||||
id: 'map',
|
||||
map: new Map({
|
||||
coord: 'simple',
|
||||
hash: true,
|
||||
center: [200, 200],
|
||||
pitch: 0,
|
||||
zoom: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
const layer = new PolygonLayer({
|
||||
name: '01',
|
||||
});
|
||||
|
||||
layer
|
||||
.source({
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[20, 20],
|
||||
[400, 20],
|
||||
[400, 400],
|
||||
[20, 400],
|
||||
[20, 20],
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.color('#2E8AE6')
|
||||
.shape('fill')
|
||||
.style({
|
||||
opacity: 1.0,
|
||||
});
|
||||
scene.addLayer(layer);
|
||||
scene.on('loaded', () => {
|
||||
console.log(scene.getCenter());
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
id="map"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import Map from './components/Map';
|
||||
// @ts-ignore
|
||||
storiesOf('自定义地图', module)
|
||||
.add('地图', () => <Map />)
|
|
@ -11290,7 +11290,7 @@ eventemitter3@^3.1.0:
|
|||
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
|
||||
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
|
||||
|
||||
eventemitter3@^4.0.0:
|
||||
eventemitter3@^4.0.0, eventemitter3@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384"
|
||||
integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==
|
||||
|
|
Loading…
Reference in New Issue