mirror of https://gitee.com/antv-l7/antv-l7
feat(map): 新增 L7 独立坐标系
This commit is contained in:
parent
7e93570b07
commit
e5fa732002
|
@ -10,12 +10,7 @@ L7 Layer 接口设计遵循图形语法,所有图层都继承于该基类。
|
|||
语法示例
|
||||
|
||||
```javascript
|
||||
const layer = new Layer(option)
|
||||
.source()
|
||||
.color()
|
||||
.size()
|
||||
.shape()
|
||||
.style();
|
||||
const layer = new Layer(option).source().color().size().shape().style();
|
||||
|
||||
scene.addLayer(layer);
|
||||
```
|
||||
|
@ -83,7 +78,7 @@ layer.source(data, {
|
|||
transforms: [
|
||||
{
|
||||
type: 'map',
|
||||
callback: function(item) {
|
||||
callback: function (item) {
|
||||
const [x, y] = item.coordinates;
|
||||
item.lat = item.lat * 1;
|
||||
item.lng = item.lng * 1;
|
||||
|
|
|
@ -10,12 +10,7 @@ L7 Layer 接口设计遵循图形语法,所有图层都继承于该基类。
|
|||
语法示例
|
||||
|
||||
```javascript
|
||||
const layer = new Layer(option)
|
||||
.source()
|
||||
.color()
|
||||
.size()
|
||||
.shape()
|
||||
.style();
|
||||
const layer = new Layer(option).source().color().size().shape().style();
|
||||
|
||||
scene.addLayer(layer);
|
||||
```
|
||||
|
@ -87,7 +82,7 @@ layer.source(data, {
|
|||
transforms: [
|
||||
{
|
||||
type: 'map',
|
||||
callback: function(item) {
|
||||
callback: function (item) {
|
||||
const [x, y] = item.coordinates;
|
||||
item.lat = item.lat * 1;
|
||||
item.lng = item.lng * 1;
|
||||
|
|
|
@ -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.11",
|
||||
"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.11",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
"@mapbox/unitbezier": "^0.0.0",
|
||||
"eventemitter3": "^4.0.4",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,875 @@
|
|||
import Point, { PointLike } from '@mapbox/point-geometry';
|
||||
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 Transform from './geo/transform';
|
||||
import { IMapOptions } from './interface';
|
||||
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;
|
||||
}
|
||||
|
||||
export default class Camera extends EventEmitter {
|
||||
protected transform: Transform;
|
||||
protected options: IMapOptions;
|
||||
private moving: boolean;
|
||||
private zooming: boolean;
|
||||
private rotating: boolean;
|
||||
private pitching: boolean;
|
||||
private 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;
|
||||
private requestRenderFrame: (_: any) => number = raf;
|
||||
private cancelRenderFrame: (_: number) => void = cancel;
|
||||
|
||||
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 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 setPitch(pitch: number, eventData?: object) {
|
||||
this.jumpTo({ pitch }, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public getBearing(): number {
|
||||
return this.transform.bearing;
|
||||
}
|
||||
|
||||
public panTo(
|
||||
lnglat: LngLatLike,
|
||||
options?: IAnimationOptions,
|
||||
eventData?: object,
|
||||
) {
|
||||
return this.easeTo(
|
||||
merge(
|
||||
{
|
||||
center: lnglat,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
|
||||
public zoomOut(options?: IAnimationOptions, eventData?: object) {
|
||||
this.zoomTo(this.getZoom() - 1, options, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setBearing(bearing: number, eventData?: object) {
|
||||
this.jumpTo({ bearing }, eventData);
|
||||
return this;
|
||||
}
|
||||
public setZoom(zoom: number, eventData?: object) {
|
||||
this.jumpTo({ zoom }, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public zoomIn(options?: IAnimationOptions, eventData?: object) {
|
||||
this.zoomTo(this.getZoom() + 1, options, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public zoomTo(zoom: number, options?: IAnimationOptions, eventData?: object) {
|
||||
return this.easeTo(
|
||||
merge(
|
||||
{
|
||||
zoom,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
|
||||
public getPadding(): IPaddingOptions {
|
||||
return this.transform.padding;
|
||||
}
|
||||
|
||||
public setPadding(padding: IPaddingOptions, eventData?: object) {
|
||||
this.jumpTo({ padding }, eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public rotateTo(
|
||||
bearing: number,
|
||||
options?: IAnimationOptions,
|
||||
eventData?: object,
|
||||
) {
|
||||
return this.easeTo(
|
||||
merge(
|
||||
{
|
||||
bearing,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
}
|
||||
|
||||
public resetNorth(options?: IAnimationOptions, eventData?: object) {
|
||||
this.rotateTo(0, merge({ duration: 1000 }, options), eventData);
|
||||
return this;
|
||||
}
|
||||
|
||||
public resetNorthPitch(options?: IAnimationOptions, eventData?: object) {
|
||||
this.easeTo(
|
||||
merge(
|
||||
{
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
duration: 1000,
|
||||
},
|
||||
options,
|
||||
),
|
||||
eventData,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
public fitBounds(
|
||||
bounds: LngLatBoundsLike,
|
||||
options?: IAnimationOptions & ICameraOptions,
|
||||
eventData?: object,
|
||||
) {
|
||||
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?: object) {
|
||||
if (Math.abs(this.getBearing()) < this.bearingSnap) {
|
||||
return this.resetNorth(options, eventData);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public jumpTo(options: ICameraOptions = {}, eventData?: object) {
|
||||
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?: object,
|
||||
) {
|
||||
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) {
|
||||
window.cancelAnimationFrame(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);
|
||||
} 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?: object) {
|
||||
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 = window.requestAnimationFrame(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?: object,
|
||||
) {
|
||||
// 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,79 @@
|
|||
.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; }
|
|
@ -0,0 +1,129 @@
|
|||
import Point from '@mapbox/point-geometry';
|
||||
import { clamp, interpolate } from '../util';
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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,93 @@
|
|||
// @flow
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
export * from './map';
|
|
@ -0,0 +1,25 @@
|
|||
export interface IMapOptions {
|
||||
container?: HTMLElement | string;
|
||||
center: [number, number];
|
||||
zoom: number;
|
||||
bearing: number;
|
||||
pitch: number;
|
||||
interactive: boolean;
|
||||
scrollZoom: boolean;
|
||||
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,258 @@
|
|||
import { DOM } from '@antv/l7-utils';
|
||||
import Point, { PointLike } from '@mapbox/point-geometry';
|
||||
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';
|
||||
import { IMapOptions } from './interface';
|
||||
const defaultMinZoom = -2;
|
||||
const defaultMaxZoom = 22;
|
||||
|
||||
// the default values, but also the valid range
|
||||
const defaultMinPitch = 0;
|
||||
const defaultMaxPitch = 60;
|
||||
|
||||
const DefaultOptions: IMapOptions = {
|
||||
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 {
|
||||
private container: HTMLElement;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private canvasContainer: HTMLElement;
|
||||
constructor(options: Partial<IMapOptions>) {
|
||||
super(merge({}, DefaultOptions, options));
|
||||
this.initContainer();
|
||||
this.resize();
|
||||
this.flyTo({
|
||||
center: options.center,
|
||||
zoom: options.zoom,
|
||||
bearing: options.bearing,
|
||||
pitch: options.pitch,
|
||||
});
|
||||
}
|
||||
|
||||
public resize() {
|
||||
const dimensions = this.containerDimensions();
|
||||
const width = dimensions[0];
|
||||
const height = dimensions[1];
|
||||
this.resizeCanvas(width, height);
|
||||
this.transform.resize(width, height);
|
||||
}
|
||||
|
||||
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 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() {
|
||||
throw new Error('空');
|
||||
}
|
||||
|
||||
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', '0');
|
||||
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`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import UnitBezier from '@mapbox/unitbezier';
|
||||
let reducedMotionQuery: MediaQueryList;
|
||||
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;
|
|
@ -0,0 +1,92 @@
|
|||
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([], vec3.add([], 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,52 @@
|
|||
import { vec3, vec4 } from 'gl-matrix';
|
||||
export default class Frustum {
|
||||
public static fromInvProjectionMatrix(
|
||||
invProj: Float64Array,
|
||||
worldSize: number,
|
||||
zoom: number,
|
||||
): Frustum {
|
||||
const clipSpaceCorners = [
|
||||
[-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([], v, invProj))
|
||||
.map((v) => vec4.scale([], v, (1.0 / v[3] / worldSize) * scale));
|
||||
|
||||
const frustumPlanePointIndices = [
|
||||
[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[]) => {
|
||||
const a = vec3.sub([], frustumCoords[p[0]], frustumCoords[p[1]]);
|
||||
const b = vec3.sub([], frustumCoords[p[2]], frustumCoords[p[1]]);
|
||||
const n = vec3.normalize([], vec3.cross([], a, b));
|
||||
const d = -vec3.dot(n, frustumCoords[p[1]]);
|
||||
return n.concat(d);
|
||||
});
|
||||
|
||||
return new Frustum(frustumCoords, frustumPlanes);
|
||||
}
|
||||
public points: number[][];
|
||||
public planes: number[][];
|
||||
|
||||
constructor(points: number[][], planes: number[][]) {
|
||||
this.points = points;
|
||||
this.planes = planes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"declarationDir": "./es",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"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,352 @@
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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?: unknown): 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),
|
||||
attributionControl,
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -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: 9,
|
||||
}),
|
||||
});
|
||||
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());
|
||||
});
|
||||
});
|
|
@ -25,6 +25,7 @@
|
|||
"@antv/l7-component": "2.2.11",
|
||||
"@antv/l7-core": "2.2.11",
|
||||
"@antv/l7-maps": "2.2.11",
|
||||
"@antv/l7-layers": "2.2.11",
|
||||
"@antv/l7-renderer": "2.2.11",
|
||||
"@antv/l7-utils": "2.2.11",
|
||||
"@babel/runtime": "^7.7.7",
|
||||
|
|
|
@ -30,14 +30,9 @@ export default class ZoomComponent extends React.Component {
|
|||
this.scene = scene;
|
||||
const layer = new PolygonLayer({});
|
||||
|
||||
layer
|
||||
.source(data)
|
||||
.color('#fff')
|
||||
.shape('name', 'text')
|
||||
.size(16)
|
||||
.style({
|
||||
opacity: 1.0,
|
||||
});
|
||||
layer.source(data).color('#fff').shape('name', 'text').size(16).style({
|
||||
opacity: 1.0,
|
||||
});
|
||||
scene.addLayer(layer);
|
||||
const zoomControl = new Zoom({
|
||||
position: 'bottomright',
|
||||
|
|
|
@ -11281,7 +11281,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==
|
||||
|
@ -13187,7 +13187,7 @@ github-slugger@^1.2.1, github-slugger@^1.3.0:
|
|||
dependencies:
|
||||
emoji-regex ">=6.0.0 <=6.1.1"
|
||||
|
||||
gl-matrix@^3.0.0, gl-matrix@^3.1.0, gl-matrix@^3.2.1:
|
||||
gl-matrix@^3.0.0, gl-matrix@^3.1.0, gl-matrix@^3.2.1, gl-matrix@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b"
|
||||
integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA==
|
||||
|
|
Loading…
Reference in New Issue