feat(map): 新增 L7 独立坐标系

This commit is contained in:
thinkinggis 2020-06-09 21:39:32 +08:00
parent 7e93570b07
commit e5fa732002
28 changed files with 3511 additions and 25 deletions

View File

@ -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;

View File

@ -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;

11
packages/map/README.md Normal file
View File

@ -0,0 +1,11 @@
# `map`
> TODO: description
## Usage
```
const map = require('map');
// TODO: DEMONSTRATE API
```

View File

@ -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,
});
});
});

46
packages/map/package.json Normal file
View File

@ -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"
}
}

875
packages/map/src/camera.ts Normal file
View File

@ -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 doesnt 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);
}
}

View File

@ -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; }

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -0,0 +1 @@
export * from './map';

View File

@ -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;
}

258
packages/map/src/map.ts Normal file
View File

@ -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`;
}
}

73
packages/map/src/util.ts Normal file
View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"declarationDir": "./es",
"rootDir": "./src",
"baseUrl": "./"
},
"include": ["./src"]
}

View File

@ -1,4 +1,5 @@
import GaodeMap from './amap/';
import Map from './map/';
import Mapbox from './mapbox/';
export { GaodeMap, Mapbox };
export { GaodeMap, Mapbox, Map };

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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',
},
},
],
},
};

View File

@ -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());
});
});

View File

@ -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",

View File

@ -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',

View File

@ -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==