feat: 提供线图层偏移点位计算的通用方法 (#1293)

* feat: 增加线偏移点的计算

* style: lint style

* feat: 支持对 greatcircle 的偏移点位的计算

* feat: 完善弧线偏移点的 featureId 过滤

* style: lint style

* style: lint style

* feat: 支持对普通线偏移点的计算支持

* feat: 增加线图层偏移点位计算的测试用例

* style: lint style

Co-authored-by: shihui <yiqianyao.yqy@alibaba-inc.com>
This commit is contained in:
YiQianYao 2022-08-18 16:04:21 +08:00 committed by GitHub
parent 02ac2ab4a1
commit 4de2369a1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 945 additions and 6 deletions

View File

@ -0,0 +1,2 @@
### Arc Line
<code src="./arcline.tsx"></code>

View File

@ -0,0 +1,139 @@
// @ts-ignore
import {
LineLayer,
Scene,
Source,
lineAtOffset,
lineAtOffsetAsyc,
PointLayer,
} from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [105, 32],
zoom: 4,
// pitch: 60
}),
});
const geoData = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
offset: 0.3,
},
geometry: {
type: 'MultiLineString',
coordinates: [
[
[99.228515625, 37.43997405227057],
[100.72265625, 27.994401411046148],
],
[
[108.544921875, 37.71859032558816],
[112.412109375, 32.84267363195431],
],
],
},
},
{
type: 'Feature',
properties: {
offset: 0.8,
},
geometry: {
type: 'MultiLineString',
coordinates: [
[
[120, 30],
[120, 40],
],
],
},
},
],
};
const source = new Source(geoData);
// scene.on('zoom', e => console.log(e))
const layer = new LineLayer({ blend: 'normal' })
.source(source)
.size(1)
.shape('arc')
.color('#f00')
.style({
// thetaOffset: 'offset'
// segmentNumber: 10,
thetaOffset: 0.5,
});
source.on('sourceUpdate', () => {
const midPoints = lineAtOffset(source, {
offset: 0.1,
shape: 'arc',
thetaOffset: 0.5,
mapVersion: scene.getMapService().version,
});
const point = new PointLayer({ blend: 'normal', zIndex: 1 })
.source(midPoints, {
parser: {
type: 'json',
x: '_lng',
y: '_lat',
},
})
.shape('circle')
.size(10)
.color('#ff0');
scene.addLayer(point);
});
(async () => {
// const midPoints = await lineAtOffsetAsyc(source, 0.1, 'arc', 'offset');
const midPoints = await lineAtOffsetAsyc(source, {
offset: 0.3,
shape: 'arc',
thetaOffset: 0.5,
mapVersion: scene.getMapService().version,
featureId: 1,
});
const point = new PointLayer({ blend: 'normal', zIndex: 1 })
.source(midPoints, {
parser: {
type: 'json',
x: '_lng',
y: '_lat',
},
})
.shape('circle')
.size(5)
.color('#0f0')
.style({
opacity: 0.8,
});
scene.addLayer(point);
})();
scene.on('loaded', () => {
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,2 @@
### GreatCircle Line
<code src="./greatcircleline.tsx"></code>

View File

@ -0,0 +1,143 @@
// @ts-ignore
import {
LineLayer,
Scene,
Source,
lineAtOffset,
lineAtOffsetAsyc,
PointLayer,
} from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [105, 32],
zoom: 4,
// pitch: 60
}),
});
const geoData = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
offset: 0.3,
},
geometry: {
type: 'MultiLineString',
coordinates: [
[
[99.228515625, 37.43997405227057],
[100.72265625, 27.994401411046148],
],
[
[108.544921875, 37.71859032558816],
[112.412109375, 32.84267363195431],
],
],
},
},
{
type: 'Feature',
properties: {
offset: 0.8,
},
geometry: {
type: 'MultiLineString',
coordinates: [
[
[120, 30],
[120, 40],
],
],
},
},
],
};
const source = new Source(geoData);
// scene.on('zoom', e => console.log(e))
const layer = new LineLayer({ blend: 'normal' })
.source(source)
.size(1)
.shape('greatcircle')
.color('#f00')
.style({
// thetaOffset: 'offset'
// segmentNumber: 10,
// thetaOffset: 0.5,
});
source.on('sourceUpdate', () => {
// console.log(scene.getMapService().version)
// const midPoints = lineAtOffset(source, 0.3, 'arc', 'offset');
const midPoints = lineAtOffset(source, {
offset: 2 / 30,
shape: 'greatcircle',
// thetaOffset: 0.5,
mapVersion: scene.getMapService().version,
});
const point = new PointLayer({ blend: 'normal', zIndex: 1 })
.source(midPoints, {
parser: {
type: 'json',
x: '_lng',
y: '_lat',
},
})
.shape('circle')
.size(10)
.color('#ff0');
scene.addLayer(point);
});
(async () => {
// const midPoints = await lineAtOffsetAsyc(source, 0.1, 'arc', 'offset');
const midPoints = await lineAtOffsetAsyc(source, {
// offset: 12.681/30,
// offset: 12/31,
// offset: 48/186,
offset: 0.3,
shape: 'greatcircle',
// thetaOffset: 0.5,
mapVersion: scene.getMapService().version,
});
const point = new PointLayer({ blend: 'normal', zIndex: 1 })
.source(midPoints, {
parser: {
type: 'json',
x: '_lng',
y: '_lat',
},
})
.shape('circle')
.size(5)
.color('#0f0')
.style({
opacity: 0.8,
});
scene.addLayer(point);
})();
scene.on('loaded', () => {
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -0,0 +1,2 @@
### Line
<code src="./line.tsx"></code>

View File

@ -0,0 +1,138 @@
// @ts-ignore
import {
LineLayer,
Scene,
Source,
lineAtOffset,
lineAtOffsetAsyc,
PointLayer,
} from '@antv/l7';
// @ts-ignore
import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react';
export default () => {
useEffect(() => {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [105, 32],
zoom: 4,
// pitch: 60
}),
});
const geoData = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
offset: 0.3,
},
geometry: {
type: 'MultiLineString',
coordinates: [
[
[99.228515625, 37.43997405227057],
[100.72265625, 27.994401411046148],
[110, 27.994401411046148],
[110, 25],
[100, 25],
],
[
[108.544921875, 37.71859032558816],
[112.412109375, 32.84267363195431],
[115, 32.84267363195431],
[115, 35],
],
],
},
},
{
type: 'Feature',
properties: {
offset: 0.8,
},
geometry: {
type: 'MultiLineString',
coordinates: [
[
[110, 30],
[120, 30],
[120, 40],
],
],
},
},
],
};
const source = new Source(geoData);
// scene.on('zoom', e => console.log(e))
const layer = new LineLayer({ blend: 'normal' })
.source(source)
.size(1)
.shape('line')
.color('#f00')
.style({});
source.on('sourceUpdate', () => {
console.log(source);
const midPoints = lineAtOffset(source, {
offset: 0.1,
shape: 'line',
});
const point = new PointLayer({ blend: 'normal', zIndex: 1 })
.source(midPoints, {
parser: {
type: 'json',
x: '_lng',
y: '_lat',
},
})
.shape('circle')
.size(10)
.color('#ff0');
scene.addLayer(point);
});
(async () => {
// const midPoints = await lineAtOffsetAsyc(source, 0.1, 'arc', 'offset');
const midPoints = await lineAtOffsetAsyc(source, {
offset: 0.5,
shape: 'line',
featureId: 1,
});
const point = new PointLayer({ blend: 'normal', zIndex: 1 })
.source(midPoints, {
parser: {
type: 'json',
x: '_lng',
y: '_lat',
},
})
.shape('circle')
.size(5)
.color('#0f0')
.style({
opacity: 0.8,
});
scene.addLayer(point);
})();
scene.on('loaded', () => {
scene.addLayer(layer);
});
}, []);
return (
<div
id="map"
style={{
height: '500px',
position: 'relative',
}}
/>
);
};

View File

@ -19,13 +19,14 @@ export default () => {
zIndex: 1,
});
layerTile.source(
'//t3.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=f1f2021a42d110057042177cd22d856f',
// '//t3.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=f1f2021a42d110057042177cd22d856f',
'http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
{
parser: {
type: 'rasterTile',
tileSize: 256,
// zoomOffset: 0
zoomOffset: 1,
// zoomOffset: 1,
},
},
);

View File

@ -177,7 +177,7 @@
"release": "lerna publish from-package --force-publish && yarn sync",
"release-cdn": "antv-bin upload -n @antv/l7",
"test": "umi-test",
"test-cover": "umi-test --coverage",
"test-cover": "yarn run worker && umi-test --coverage",
"test-cover-viewer": "umi-test --coverage && cd coverage && http-server -p 6001 -o",
"test-live": "umi-test --watch",
"tsc": "tsc",

View File

@ -52,7 +52,8 @@ vec2 midPoint(vec2 source, vec2 target, float arcThetaOffset) {
// return mid;
}
float getSegmentRatio(float index) {
return smoothstep(0.0, 1.0, index / (segmentNumber - 1.));
// return smoothstep(0.0, 1.0, index / (segmentNumber - 1.));
return index / (segmentNumber - 1.);
}
vec2 interpolate (vec2 source, vec2 target, float t, float arcThetaOffset) {
// if the angularDist is PI, linear interpolation is applied. otherwise, use spherical interpolation
@ -130,8 +131,9 @@ void main() {
styleMappingMat[3].b = d_distance_ratio;
// styleMappingMat[0][1] - arcThetaOffset
vec4 curr = project_position(vec4(interpolate(source, target, segmentRatio, styleMappingMat[0][1]), 0.0, 1.0));
vec4 next = project_position(vec4(interpolate(source, target, nextSegmentRatio, styleMappingMat[0][1]), 0.0, 1.0));
float arcThetaOffset = styleMappingMat[0][1];
vec4 curr = project_position(vec4(interpolate(source, target, segmentRatio, arcThetaOffset), 0.0, 1.0));
vec4 next = project_position(vec4(interpolate(source, target, nextSegmentRatio, arcThetaOffset), 0.0, 1.0));
// v_normal = getNormal((next.xy - curr.xy) * indexDir, a_Position.y);
//unProjCustomCoord

View File

@ -0,0 +1,177 @@
import { Version } from '../src/interface/map';
import { lineAtOffset, lineAtOffsetAsyc } from '../src/lineAtOffset/index';
const arcSource = {
inited: true,
data: {
dataArray: [
{
coordinates: [
[99.228515625, 37.43997405227057],
[100.72265625, 27.994401411046148],
],
offset: 0.3,
_id: 0
}
]
}
};
describe('lineAtOffset', () => {
(async () => {
const asyncOffsetPoint = await lineAtOffsetAsyc(arcSource, {
shape: 'arc',
offset: 0.1,
mapVersion: Version['GAODE1.x']
})
it('asyncOffsetPoint', () => {
expect(asyncOffsetPoint).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 99.66757224332352,
_lat: 36.507847734146466,
_height: 0
}
]);
});
})()
const arcOffsetPoint = lineAtOffset(arcSource, {
shape: 'arc',
offset: 0.1,
mapVersion: Version['GAODE1.x']
})
const arcOffsetPoint2 = lineAtOffset(arcSource, {
shape: 'arc',
offset: 0.1,
mapVersion: Version['GAODE2.x'],
thetaOffset: 0.314
})
const arcOffsetPoint3 = lineAtOffset(arcSource, {
shape: 'arc',
offset: 0.1,
mapVersion: Version['GAODE2.x'],
thetaOffset: 0.314,
featureId: 0
})
const greatCircleOffsetPoint = lineAtOffset(arcSource, {
shape: 'greatcircle',
offset: 0.1,
mapVersion: Version['GAODE1.x']
})
const greatCircleOffsetPoint2 = lineAtOffset(arcSource, {
shape: 'greatcircle',
offset: 0.1,
mapVersion: Version['GAODE2.x']
})
const lineOffsetPoint = lineAtOffset(arcSource, {
shape: 'line',
offset: 0.1,
mapVersion: Version['GAODE1.x']
})
it('arcOffsetPoint', () => {
expect(arcOffsetPoint).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 99.66757224332352,
_lat: 36.507847734146466,
_height: 0
}
]);
});
it('arcOffsetPoint2', () => {
expect(arcOffsetPoint2).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 99.72191962749187,
_lat: 36.54640697914567,
_height: 0
}
]);
});
it('arcOffsetPoint3', () => {
expect(arcOffsetPoint3).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 99.72191962749187,
_lat: 36.54640697914567,
_height: 0
}
]);
});
it('greatCircleOffsetPoint', () => {
expect(greatCircleOffsetPoint).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 99.39897617955312,
_lat: 36.46373143064039,
_height: undefined
}
]);
});
it('greatCircleOffsetPoint2', () => {
expect(greatCircleOffsetPoint2).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 1.7395272931153063,
_lat: 0.6371821457776072,
_height: 0
}
]);
});
it('lineOffsetPoint', () => {
expect(lineOffsetPoint).toEqual( [
{
coordinates: [
[ 99.228515625, 37.43997405227057 ],
[ 100.72265625, 27.994401411046148 ]
],
offset: 0.3,
_id: 0,
_lng: 99.3779296875,
_lat: 36.495416788148134,
_height: 0
}
]);
});
});

View File

@ -191,6 +191,15 @@ export function amap2Project(lng: number, lat: number): [number, number] {
return [lng * Tg, lat * Tg];
}
export function amap2UnProject(x: number, y: number): [number, number] {
const Rg = Math.PI / 180;
const Tg = 6378137;
const lng = (x/Tg) / Rg;
const lat = (2 * (Math.atan(Math.exp((y / Tg))) - Math.PI/4))/Rg;
return [lng, lat];
}
export function lnglatDistance(
coordinates1: [number, number],
coordinates2: [number, number],

View File

@ -19,3 +19,4 @@ export * from './cull';
export * from './env';
export * from './tileset-manager';
export * from './workers/triangulation';
export * from './lineAtOffset'

View File

@ -0,0 +1,8 @@
export enum Version {
'GAODE1.x' = 'GAODE1.x',
'GAODE2.x' = 'GAODE2.x',
'MAPBOX' = 'MAPBOX',
'L7MAP' = 'L7MAP',
'SIMPLE' = 'SIMPLE',
'GLOBEL' = 'GLOBEL',
}

View File

@ -0,0 +1,76 @@
import { Point } from './interface';
import { amap2Project, amap2UnProject } from '../geo';
import { Version } from '../interface/map';
// arc
export function arcLineAtOffset(
source: Point,
target: Point,
offset: number,
thetaOffset: number | undefined,
mapVersion: Version | undefined,
segmentNumber: number = 30,
autoFit: boolean,
) {
let pointOffset = offset;
if (autoFit) {
// Tip: 自动偏移到线的节点位置
pointOffset =
Math.round(offset * (segmentNumber - 1)) / (segmentNumber - 1);
}
if (!thetaOffset) {
return interpolate(source, target, pointOffset, 0.314, mapVersion);
} else {
return interpolate(source, target, pointOffset, thetaOffset, mapVersion);
}
}
function bezier3(arr: Point, t: number) {
const ut = 1 - t;
return (arr[0] * ut + arr[1] * t) * ut + (arr[1] * ut + arr[2] * t) * t;
}
function calDistance(p1: Point, p2: Point) {
return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2));
}
function midPoint(source: Point, target: Point, thetaOffset: number) {
const center = [target[0] - source[0], target[1] - source[1]]; //target - source;
const r = calDistance(center, [0, 0]);
const theta = Math.atan2(center[1], center[0]);
const r2 = r / 2.0 / Math.cos(thetaOffset);
const theta2 = theta + thetaOffset;
const mid = [
r2 * Math.cos(theta2) + source[0],
r2 * Math.sin(theta2) + source[1],
];
return mid;
}
function interpolate(
source: Point,
target: Point,
offset: number,
thetaOffset: number,
mapVersion?: Version,
) {
if (mapVersion === Version['GAODE2.x']) {
// amap2
const sourceFlat = amap2Project(source[0], source[1]);
const targetFlat = amap2Project(target[0], target[1]);
const mid = midPoint(sourceFlat, targetFlat, thetaOffset);
const x = [sourceFlat[0], mid[0], targetFlat[0]];
const y = [sourceFlat[1], mid[1], targetFlat[1]];
return [...amap2UnProject(bezier3(x, offset), bezier3(y, offset)), 0];
} else {
// amap
const mid = midPoint(source, target, thetaOffset);
const x = [source[0], mid[0], target[0]];
const y = [source[1], mid[1], target[1]];
return [bezier3(x, offset), bezier3(y, offset), 0];
}
}

View File

@ -0,0 +1,93 @@
import { Point } from './interface';
import { calDistance } from '../geo';
import { Version } from '../interface/map';
import { degreesToRadians, radiansToDegrees } from '@turf/helpers';
// arc
export function greatCircleLineAtOffset(
source: Point,
target: Point,
offset: number,
thetaOffset: number | undefined,
mapVersion: Version | undefined,
segmentNumber: number = 30,
autoFit: boolean,
) {
let pointOffset = offset;
if (autoFit) {
// Tip: 自动偏移到线的节点位置
// greatcircle 暂时不支持配置 segmentNumber 和 thetaOffset
pointOffset = Math.round(offset * 29) / 29;
}
return interpolate(source, target, pointOffset, mapVersion);
}
function midPoint(source: Point, target: Point) {
const center = [target[0] - source[0], target[1] - source[1]]; //target - source;
const r = calDistance(center, [0, 0]);
const theta = Math.atan2(center[1], center[0]);
const thetaOffset = 0.314;
const r2 = r / 2.0 / Math.cos(thetaOffset);
const theta2 = theta + thetaOffset;
const mid = [
r2 * Math.cos(theta2) + source[0],
r2 * Math.sin(theta2) + source[1],
];
return mid;
}
function bezier3(arr: Point, t: number) {
const ut = 1 - t;
return (arr[0] * ut + arr[1] * t) * ut + (arr[1] * ut + arr[2] * t) * t;
}
function getAngularDist(source: Point, target: Point) {
const delta = [source[0] - target[0], source[1] - target[1]];
const sin_half_delta = [Math.sin(delta[0] / 2.0), Math.sin(delta[1] / 2.0)]; //Math.sin(delta / 2.0);
const a =
sin_half_delta[1] * sin_half_delta[1] +
Math.cos(source[1]) *
Math.cos(target[1]) *
sin_half_delta[0] *
sin_half_delta[0];
return 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1.0 - a));
}
export function interpolate(
s: Point,
t: Point,
offset: number,
mapVersion: string | undefined,
) {
const source = [degreesToRadians(s[0]), degreesToRadians(s[1])];
const target = [degreesToRadians(t[0]), degreesToRadians(t[1])];
if (mapVersion === 'GAODE2.x') {
// gaode2.x
const mid = midPoint(source, target);
const x = [source[0], mid[0], target[0]];
const y = [source[1], mid[1], target[1]];
return [bezier3(x, offset), bezier3(y, offset), 0];
} else {
const angularDist = getAngularDist(source, target);
if (Math.abs(angularDist - Math.PI) < 0.001) {
return [
(1.0 - offset) * source[0] + offset * target[0],
(1.0 - offset) * source[1] + offset * target[1],
];
}
const a = Math.sin((1.0 - offset) * angularDist) / Math.sin(angularDist);
const b = Math.sin(offset * angularDist) / Math.sin(angularDist);
const sin_source = [Math.sin(source[0]), Math.sin(source[1])];
const cos_source = [Math.cos(source[0]), Math.cos(source[1])];
const sin_target = [Math.sin(target[0]), Math.sin(target[1])];
const cos_target = [Math.cos(target[0]), Math.cos(target[1])];
const x =
a * cos_source[1] * cos_source[0] + b * cos_target[1] * cos_target[0];
const y =
a * cos_source[1] * sin_source[0] + b * cos_target[1] * sin_target[0];
const z = a * sin_source[1] + b * sin_target[1];
return [
radiansToDegrees(Math.atan2(y, x)),
radiansToDegrees(Math.atan2(z, Math.sqrt(x * x + y * y))),
];
}
}

View File

@ -0,0 +1,82 @@
import { Source, ILineAtOffset } from './interface';
import { arcLineAtOffset } from './arc';
import { greatCircleLineAtOffset } from './greatCircle';
import { pathLineAtOffset } from './line';
export function lineAtOffset(source: Source, option: ILineAtOffset) {
const { featureId } = option;
let features = source.data.dataArray;
if (typeof featureId === 'number') {
features = features.filter(({ _id }: { _id: number }) => _id === featureId);
}
return features.map((feature: any) => {
const position = getLineOffsetPosition(feature, option);
return {
...feature,
...position,
};
});
}
export function lineAtOffsetAsyc(source: Source, option: ILineAtOffset) {
return new Promise((resolve, reject) => {
if (source.inited) {
resolve(lineAtOffset(source, option));
} else {
source.once('sourceUpdate', () => {
resolve(lineAtOffset(source, option));
});
}
});
}
function getLineOffsetPosition(feature: any, option: ILineAtOffset) {
const {
offset,
shape,
thetaOffset,
mapVersion,
segmentNumber = 30,
autoFit = true,
} = option;
const { coordinates } = feature;
if (shape === 'line') {
return pathLineAtOffset(coordinates, offset);
}
const source = coordinates[0];
const target = coordinates[1];
let linetheatOffset;
if (typeof thetaOffset === 'string') {
linetheatOffset = feature[thetaOffset] || 0;
} else {
linetheatOffset = thetaOffset;
}
let calFunc;
switch (shape) {
case 'arc':
calFunc = arcLineAtOffset;
break;
case 'greatcircle':
calFunc = greatCircleLineAtOffset;
break;
default:
calFunc = arcLineAtOffset;
}
const [lng, lat, height] = calFunc(
source,
target,
offset,
linetheatOffset,
mapVersion,
segmentNumber,
autoFit,
);
return {
_lng: lng,
_lat: lat,
_height: height,
};
}

View File

@ -0,0 +1,15 @@
import { Version } from '../interface/map';
export type Source = any;
export type ILineShape = 'line' | 'arc' | 'arc3d' | 'greatcircle';
export type IThetaOffset = string | number | undefined;
export type Point = number[];
export interface ILineAtOffset {
offset: number;
shape: ILineShape;
mapVersion: Version;
thetaOffset?: IThetaOffset;
featureId?: number | undefined;
segmentNumber?: number;
autoFit?: boolean;
}

View File

@ -0,0 +1,49 @@
import { Point } from './interface';
export function pathLineAtOffset(coords: Point[], offset: number) {
let totalDistance = 0;
const cachePoints = [];
for (let i = 0; i < coords.length - 1; i++) {
const p1 = coords[i] as [number, number];
const p2 = coords[i + 1] as [number, number];
const distance = calDistance(p1, p2);
const lastTotalDistance = totalDistance;
totalDistance += distance;
cachePoints.push({
p1,
p2,
totalDistance,
distance,
lastTotalDistance,
});
}
const offsetDistance = totalDistance * offset;
let _lng, _lat;
for (let i = 0; i < cachePoints.length; i++) {
const currentDistance = cachePoints[i].totalDistance;
if (currentDistance > offsetDistance) {
const p1 = cachePoints[i].p1;
const p2 = cachePoints[i].p2;
const radius =
(offsetDistance - cachePoints[i].lastTotalDistance) /
cachePoints[i].distance;
const offsetPoint = mixPoint(p2, p1, radius);
_lng = offsetPoint[0];
_lat = offsetPoint[1];
break;
}
}
return {
_lng,
_lat,
_height: 0,
};
}
function mixPoint(p1: Point, p2: Point, r: number) {
return [p1[0] * r + p2[0] * (1 - r), p1[1] * r + p2[1] * (1 - r)];
}
function calDistance(p1: Point, p2: Point) {
return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2));
}