Skip to content

Commit

Permalink
refactor: 升级新版地图交互事件机制
Browse files Browse the repository at this point in the history
  • Loading branch information
lvisei committed Jun 4, 2024
1 parent 8939e9b commit 285c045
Show file tree
Hide file tree
Showing 78 changed files with 17,337 additions and 2 deletions.
5 changes: 4 additions & 1 deletion packages/map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
"@antv/l7-utils": "workspace:*",
"@babel/runtime": "^7.7.7",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/unitbezier": "^0.0.0",
"@mapbox/unitbezier": "^0.0.1",
"eventemitter3": "^4.0.4",
"gl-matrix": "^3.1.0"
},
"devDependencies": {
"@types/mapbox__point-geometry": "^0.1.4"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
Expand Down
32 changes: 32 additions & 0 deletions packages/map/src/map-next/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Map

Map fork from [maplibre-gl-js](maplibre-gl-js), keep event, responds user interaction and updates the internal state of the map (current viewport, camera angle, etc.)

```mermaid
sequenceDiagram
actor user
participant DOM
participant handler_manager
participant handler
participant camera
participant transform
participant map
user->>camera: map#setCenter, map#panTo
camera->>transform: update
camera->>map: fire move event
map->>map: _render()
user->>DOM: resize, pan,<br>click, scroll,<br>...
DOM->>handler_manager: DOM events
handler_manager->>handler: forward event
handler-->>handler_manager: HandlerResult
handler_manager->>transform: update
handler_manager->>map: fire move event
map->>map: _render()
```

- [Transform](../src/geo/transform.ts) holds the current viewport details (pitch, zoom, bearing, bounds, etc.). Two places in the code update transform directly:
- [Camera](../src/ui/camera.ts) (parent class of [Map](../src/ui/map)) in response to explicit calls to [Camera#panTo](../src/ui/camera.ts#L207), [Camera#setCenter](../src/ui/camera.ts#L169)
- [HandlerManager](../src/ui/handler_manager.ts) in response to DOM events. It forwards those events to interaction processors that live in [src/ui/handler](../src/ui/handler), which accumulate a merged [HandlerResult](../src/ui/handler_manager.ts#L64) that kick off a render frame loop, decreasing the inertia and nudging map.transform by that amount on each frame from [HandlerManager#\_updateMapTransform()](../src/ui/handler_manager.ts#L413). That loop continues in the inertia decreases to 0.
- Both camera and handler_manager are responsible for firing `move`, `zoom`, `movestart`, `moveend`, ... events on the map after they update transform. Each of these events (along with style changes and data load events) triggers a call to [Map#\_render()](../src/ui/map.ts#L2480) which renders a single frame of the map.
81 changes: 81 additions & 0 deletions packages/map/src/map-next/geo/edge_insets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { EdgeInsets } from '../geo/edge_insets';

describe('EdgeInsets', () => {
describe('#constructor', () => {
test('creates an object with default values', () => {
expect(new EdgeInsets() instanceof EdgeInsets).toBeTruthy();
});

test('invalid initialization', () => {
expect(() => {
new EdgeInsets(NaN, 10);
}).toThrow('Invalid value for edge-insets, top, bottom, left and right must all be numbers');

expect(() => {
new EdgeInsets(-10, 10, 20, 10);
}).toThrow('Invalid value for edge-insets, top, bottom, left and right must all be numbers');
});

test('valid initialization', () => {
const top = 10;
const bottom = 15;
const left = 26;
const right = 19;

const inset = new EdgeInsets(top, bottom, left, right);
expect(inset.top).toBe(top);
expect(inset.bottom).toBe(bottom);
expect(inset.left).toBe(left);
expect(inset.right).toBe(right);
});
});

describe('#getCenter', () => {
test('valid input', () => {
const inset = new EdgeInsets(10, 15, 50, 10);
const center = inset.getCenter(600, 400);
expect(center.x).toBe(320);
expect(center.y).toBe(197.5);
});

test('center clamping', () => {
const inset = new EdgeInsets(300, 200, 500, 200);
const center = inset.getCenter(600, 400);

// Midpoint of the overlap when padding overlaps
expect(center.x).toBe(450);
expect(center.y).toBe(250);
});
});

describe('#interpolate', () => {
test('it works', () => {
const inset1 = new EdgeInsets(10, 15, 50, 10);
const inset2 = new EdgeInsets(20, 30, 100, 10);
const inset3 = inset1.interpolate(inset1, inset2, 0.5);

// inset1 is mutated in-place
expect(inset3).toBe(inset1);

expect(inset3.top).toBe(15);
expect(inset3.bottom).toBe(22.5);
expect(inset3.left).toBe(75);
expect(inset3.right).toBe(10);
});
});

test('#equals', () => {
const inset1 = new EdgeInsets(10, 15, 50, 10);
const inset2 = new EdgeInsets(10, 15, 50, 10);
const inset3 = new EdgeInsets(10, 15, 50, 11);
expect(inset1.equals(inset2)).toBeTruthy();
expect(inset2.equals(inset3)).toBeFalsy();
});

test('#clone', () => {
const inset1 = new EdgeInsets(10, 15, 50, 10);
const inset2 = inset1.clone();
expect(inset2 === inset1).toBeFalsy();
expect(inset1.equals(inset2)).toBeTruthy();
});
});
158 changes: 158 additions & 0 deletions packages/map/src/map-next/geo/edge_insets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import Point from '@mapbox/point-geometry';
import { clamp, interpolates } from '../util/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.
*
* @group Geography and Geometry
*/
export class EdgeInsets {
/**
* @defaultValue 0
*/
top: number;
/**
* @defaultValue 0
*/
bottom: number;
/**
* @defaultValue 0
*/
left: number;
/**
* @defaultValue 0
*/
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 start - interpolation start
* @param target - interpolation target
* @param t - interpolation step/weight
* @returns the insets
*/
interpolate(start: PaddingOptions | EdgeInsets, target: PaddingOptions, t: number): EdgeInsets {
if (target.top != null && start.top != null)
this.top = interpolates.number(start.top, target.top, t);
if (target.bottom != null && start.bottom != null)
this.bottom = interpolates.number(start.bottom, target.bottom, t);
if (target.left != null && start.left != null)
this.left = interpolates.number(start.left, target.left, t);
if (target.right != null && start.right != null)
this.right = interpolates.number(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 width - the width
* @param height - the height
* @returns the point
*/
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);
}

equals(other: PaddingOptions): boolean {
return (
this.top === other.top &&
this.bottom === other.bottom &&
this.left === other.left &&
this.right === other.right
);
}

clone(): EdgeInsets {
return new EdgeInsets(this.top, this.bottom, this.left, this.right);
}

/**
* Returns the current state as json, useful when you want to have a
* read-only representation of the inset.
*
* @returns state as json
*/
toJSON(): PaddingOptions {
return {
top: this.top,
bottom: this.bottom,
left: this.left,
right: this.right,
};
}
}

/**
* Options for setting padding on calls to methods such as {@link Map#fitBounds}, {@link Map#fitScreenCoordinates}, and {@link Map#setPadding}. Adjust these options to set the amount of padding in pixels added to the edges of the canvas. Set a uniform padding on all edges or individual values for each edge. All properties of this object must be
* non-negative integers.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let bbox = [[-79, 43], [-73, 45]];
* map.fitBounds(bbox, {
* padding: {top: 10, bottom:25, left: 15, right: 5}
* });
* ```
*
* @example
* ```ts
* let bbox = [[-79, 43], [-73, 45]];
* map.fitBounds(bbox, {
* padding: 20
* });
* ```
* @see [Fit to the bounds of a LineString](https://maplibre.org/maplibre-gl-js/docs/examples/zoomto-linestring/)
* @see [Fit a map to a bounding box](https://maplibre.org/maplibre-gl-js/docs/examples/fitbounds/)
*/
export type PaddingOptions = {
/**
* Padding in pixels from the top of the map canvas.
*/
top: number;
/**
* Padding in pixels from the bottom of the map canvas.
*/
bottom: number;
/**
* Padding in pixels from the left of the map canvas.
*/
right: number;
/**
* Padding in pixels from the right of the map canvas.
*/
left: number;
};
65 changes: 65 additions & 0 deletions packages/map/src/map-next/geo/lng_lat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { LngLat } from '../geo/lng_lat';

describe('LngLat', () => {
test('#constructor', () => {
expect(new LngLat(0, 0) instanceof LngLat).toBeTruthy();

expect(() => {
/*eslint no-new: 0*/
new LngLat(0, -91);
}).toThrow('Invalid LngLat latitude value: must be between -90 and 90');

expect(() => {
/*eslint no-new: 0*/
new LngLat(0, 91);
}).toThrow('Invalid LngLat latitude value: must be between -90 and 90');
});

test('#convert', () => {
expect(LngLat.convert([0, 10]) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({ lng: 0, lat: 10 }) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({ lng: 0, lat: 0 }) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({ lon: 0, lat: 10 }) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({ lon: 0, lat: 0 }) instanceof LngLat).toBeTruthy();
expect(LngLat.convert(new LngLat(0, 0)) instanceof LngLat).toBeTruthy();
});

test('#wrap', () => {
expect(new LngLat(0, 0).wrap()).toEqual({ lng: 0, lat: 0 });
expect(new LngLat(10, 20).wrap()).toEqual({ lng: 10, lat: 20 });
expect(new LngLat(360, 0).wrap()).toEqual({ lng: 0, lat: 0 });
expect(new LngLat(190, 0).wrap()).toEqual({ lng: -170, lat: 0 });
});

test('#toArray', () => {
expect(new LngLat(10, 20).toArray()).toEqual([10, 20]);
});

test('#toString', () => {
expect(new LngLat(10, 20).toString()).toBe('LngLat(10, 20)');
});

test('#distanceTo', () => {
const newYork = new LngLat(-74.006, 40.7128);
const losAngeles = new LngLat(-118.2437, 34.0522);
const d = newYork.distanceTo(losAngeles); // 3935751.690893987, "true distance" is 3966km
expect(d > 3935750).toBeTruthy();
expect(d < 3935752).toBeTruthy();
});

test('#distanceTo to pole', () => {
const newYork = new LngLat(-74.006, 40.7128);
const northPole = new LngLat(-135, 90);
const d = newYork.distanceTo(northPole); // 5480494.158486183 , "true distance" is 5499km
expect(d > 5480493).toBeTruthy();
expect(d < 5480495).toBeTruthy();
});

test('#distanceTo to Null Island', () => {
const newYork = new LngLat(-74.006, 40.7128);
const nullIsland = new LngLat(0, 0);
const d = newYork.distanceTo(nullIsland); // 8667080.125666846 , "true distance" is 8661km
expect(d > 8667079).toBeTruthy();
expect(d < 8667081).toBeTruthy();
});
});
Loading

0 comments on commit 285c045

Please sign in to comment.