chore(layer): 线图层增加拐角效果

This commit is contained in:
thinkinggis 2020-07-14 19:00:17 +08:00
parent 39dd92c3c2
commit 12b1b9d225
13 changed files with 415 additions and 94 deletions

View File

@ -220,18 +220,6 @@ export default class StyleAttributeService implements IStyleAttributeService {
indicesForCurrentFeature.forEach((i) => { indicesForCurrentFeature.forEach((i) => {
indices.push(i + verticesNum); indices.push(i + verticesNum);
}); });
verticesForCurrentFeature.forEach((index) => {
vertices.push(index);
});
// fix Maximum call stack size exceeded https://stackoverflow.com/questions/22123769/rangeerror-maximum-call-stack-size-exceeded-why
if (normalsForCurrentFeature) {
normalsForCurrentFeature.forEach((normal) => {
normals.push(normal);
});
}
// if (featureIdx % 500 === 0) {
// await sleep(100);
// }
size = vertexSize; size = vertexSize;
const verticesNumForCurrentFeature = const verticesNumForCurrentFeature =
verticesForCurrentFeature.length / vertexSize; verticesForCurrentFeature.length / vertexSize;
@ -252,21 +240,20 @@ export default class StyleAttributeService implements IStyleAttributeService {
vertexIdx < verticesNumForCurrentFeature; vertexIdx < verticesNumForCurrentFeature;
vertexIdx++ vertexIdx++
) { ) {
const normal =
normalsForCurrentFeature?.slice(vertexIdx * 3, vertexIdx * 3 + 3) ||
[];
const vertice = verticesForCurrentFeature.slice(
vertexIdx * vertexSize,
vertexIdx * vertexSize + vertexSize,
);
descriptors.forEach((descriptor, attributeIdx) => { descriptors.forEach((descriptor, attributeIdx) => {
if (descriptor && descriptor.update) { if (descriptor && descriptor.update) {
const normal =
normalsForCurrentFeature?.slice(
vertexIdx * 3,
vertexIdx * 3 + 3,
) || [];
(descriptor.buffer.data as number[]).push( (descriptor.buffer.data as number[]).push(
...descriptor.update( ...descriptor.update(
feature, feature,
featureIdx, featureIdx,
verticesForCurrentFeature.slice( vertice,
vertexIdx * vertexSize,
vertexIdx * vertexSize + vertexSize,
),
vertexIdx, // 当前顶点所在feature索引 vertexIdx, // 当前顶点所在feature索引
normal, normal,
// TODO: 传入顶点索引 vertexIdx // TODO: 传入顶点索引 vertexIdx

View File

@ -28,7 +28,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.8.3/polyfill.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.8.3/polyfill.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script> <script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script>
<script src="../dist/l7.js"></script> <script src="../dist/l7-dev.js"></script>
<script> <script>
console.log(L7); console.log(L7);
const scene = new L7.Scene({ const scene = new L7.Scene({

View File

@ -27,7 +27,7 @@
<!-- <script src="https://cdn.jsdelivr.net/npm/symbol-es6@0.1.2/symbol-es6.min.js"></script> --> <!-- <script src="https://cdn.jsdelivr.net/npm/symbol-es6@0.1.2/symbol-es6.min.js"></script> -->
<script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script> <script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script>
<script src="../dist/l7.js"></script> <script src="../dist/l7-dev.js"></script>
<script> <script>
console.log(L7); console.log(L7);
const scene = new L7.Scene({ const scene = new L7.Scene({

View File

@ -26,7 +26,7 @@
<div id="map"></div> <div id="map"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.8.3/polyfill.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.8.3/polyfill.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script> <script src="https://api.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js"></script>
<script src="../dist/l7.js"></script> <script src="../dist/l7-dev.js"></script>
<script> <script>
const data = const data =
{"type":"FeatureCollection","features":[ {"type":"FeatureCollection","features":[

View File

@ -34,6 +34,7 @@
"d3-scale": "2", "d3-scale": "2",
"earcut": "^2.2.1", "earcut": "^2.2.1",
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.0",
"extrude-polyline": "^1.0.6",
"gl-matrix": "^3.1.0", "gl-matrix": "^3.1.0",
"gl-vec2": "^1.3.0", "gl-vec2": "^1.3.0",
"inversify": "^5.0.1", "inversify": "^5.0.1",

View File

@ -2,8 +2,8 @@ import { IEncodeFeature } from '@antv/l7-core';
import { aProjectFlat, lngLatToMeters } from '@antv/l7-utils'; import { aProjectFlat, lngLatToMeters } from '@antv/l7-utils';
import earcut from 'earcut'; import earcut from 'earcut';
import { vec3 } from 'gl-matrix'; import { vec3 } from 'gl-matrix';
import ExtrudePolyline from '../utils/extrude_polyline';
import { calculteCentroid } from '../utils/geo'; import { calculteCentroid } from '../utils/geo';
import getNormals from '../utils/polylineNormal';
import extrudePolygon, { import extrudePolygon, {
extrude_PolygonNormal, extrude_PolygonNormal,
fillPolygon, fillPolygon,
@ -74,11 +74,16 @@ export function LineTriangulation(feature: IEncodeFeature) {
if (Array.isArray(path[0][0])) { if (Array.isArray(path[0][0])) {
path = coordinates[0] as number[][]; path = coordinates[0] as number[][];
} }
const line = getNormals(path as number[][], false, 0); const line = new ExtrudePolyline({
dash: true,
join: 'bevel', //
});
const linebuffer = line.extrude(path as number[][]);
return { return {
vertices: line.attrPos, // [ x,y,z, distance, miter,total ] vertices: linebuffer.positions, // [ x,y,z, distance, miter,total ]
indices: line.attrIndex, indices: linebuffer.indices,
normals: line.normals, normals: linebuffer.normals,
size: 6, size: 6,
}; };
} }

View File

@ -52,6 +52,7 @@ export default class LineModel extends BaseModel {
vertexShader: line_vert, vertexShader: line_vert,
fragmentShader: line_frag, fragmentShader: line_frag,
triangulation: LineTriangulation, triangulation: LineTriangulation,
primitive: gl.TRIANGLES,
blend: this.getBlend(), blend: this.getBlend(),
depth: { enable: false }, depth: { enable: false },
}), }),
@ -146,6 +147,7 @@ export default class LineModel extends BaseModel {
type: gl.FLOAT, type: gl.FLOAT,
}, },
size: 3, size: 3,
// @ts-ignore
update: ( update: (
feature: IEncodeFeature, feature: IEncodeFeature,
featureIdx: number, featureIdx: number,

View File

@ -0,0 +1,23 @@
import { aProjectFlat } from '@antv/l7-utils';
import ExtrudePolyLine from '../extrude_polyline';
describe('extrude polyline', () => {
const coords = [
[57.65624999999999, 55.178867663281984],
[74.8828125, 54.77534585936447],
[74.8828125, 49.83798245308484],
];
const extrude = new ExtrudePolyLine({
thickness: 1,
});
it('extrude line', () => {
coords.forEach((coord) => {
const [lng, lat] = aProjectFlat(coord);
coord[0] = lng;
coord[1] = lat;
});
const mesh = extrude.extrude(coords);
expect(mesh.indices.length).toBe(12);
});
});

View File

@ -0,0 +1,332 @@
import { aProjectFlat } from '@antv/l7-utils';
import { vec2 } from 'gl-matrix';
const tmp = vec2.create();
const capEnd = vec2.create();
const lineA = vec2.create();
const lineB = vec2.create();
const tangent = vec2.create();
export function computeMiter(
lineTangent: vec2,
miter: vec2,
start: vec2,
end: vec2,
halfThick: number,
): [number, vec2] {
vec2.add(lineTangent, start, end);
vec2.normalize(lineTangent, lineTangent);
miter = vec2.fromValues(-lineTangent[1], lineTangent[0]);
const tmpvec = vec2.fromValues(-start[1], start[0]);
return [halfThick / vec2.dot(miter, tmpvec), miter];
}
export function computeNormal(out: vec2, dir: vec2) {
return vec2.set(out, -dir[1], dir[0]);
}
export function direction(out: vec2, a: vec2, b: vec2) {
vec2.sub(out, a, b);
vec2.normalize(out, out);
return out;
}
function isPointEqual(a: vec2, b: vec2) {
return a[0] === b[0] && a[1] === b[1];
}
export interface IExtrudeLineOption {
join: string;
cap: string;
dash: boolean;
closed: boolean;
indexOffset: number;
miterLimit: number;
thickness: number;
}
export default class ExtrudePolyline {
private join: string;
private cap: string;
private miterLimit: number;
private thickness: number;
private normal: vec2 | null;
private lastFlip: number = -1;
private miter: vec2 = vec2.fromValues(0, 0);
private started: boolean = false;
private dash: boolean = false;
private totalDistance: number = 0;
constructor(opts: Partial<IExtrudeLineOption> = {}) {
this.join = opts.join || 'miter';
this.cap = opts.cap || 'butt';
this.miterLimit = opts.miterLimit || 10;
this.thickness = opts.thickness || 1;
this.dash = opts.dash || false;
}
public extrude(points: number[][]) {
const complex: {
positions: number[];
indices: number[];
normals: number[];
} = {
positions: [],
indices: [],
normals: [],
};
if (points.length <= 1) {
return complex;
}
this.lastFlip = -1;
this.started = false;
this.normal = null;
this.totalDistance = 0;
const total = points.length;
for (let i = 1, count = 0; i < total; i++) {
const last = points[i - 1] as vec2;
const cur = points[i] as vec2;
const next = i < points.length - 1 ? points[i + 1] : null;
// 如果当前点和前一点相同,跳过
if (isPointEqual(last, cur)) {
continue;
}
const amt = this.segment(complex, count, last, cur, next as vec2);
count += amt;
}
if (this.dash) {
for (let i = 0; i < complex.positions.length / 6; i++) {
complex.positions[i * 6 + 5] = this.totalDistance;
}
}
return complex;
}
private segment(
complex: any,
index: number,
last: vec2,
cur: vec2,
next: vec2,
) {
let count = 0;
const indices = complex.indices;
const positions = complex.positions;
const normals = complex.normals;
const capSquare = this.cap === 'square';
const joinBevel = this.join === 'bevel';
const flatCur = aProjectFlat([cur[0], cur[1]]) as [number, number];
const flatLast = aProjectFlat([last[0], last[1]]) as [number, number];
direction(lineA, flatCur, flatLast);
let segmentDistance = 0;
if (this.dash) {
segmentDistance = this.lineSegmentDistance(flatCur, flatLast);
this.totalDistance += segmentDistance;
}
if (!this.normal) {
this.normal = vec2.create();
computeNormal(this.normal, lineA);
}
if (!this.started) {
this.started = true;
// if the end cap is type square, we can just push the verts out a bit
if (capSquare) {
// vec2.scaleAndAdd(capEnd, last, lineA, -this.thickness);
const out1 = vec2.create();
const out2 = vec2.create();
vec2.add(out1, this.normal, lineA);
vec2.add(out2, this.normal, lineA);
normals.push(out2[0], out2[1], 0);
normals.push(out1[0], out1[1], 0);
positions.push(
last[0],
last[1],
0,
this.totalDistance - segmentDistance,
-this.thickness,
0,
);
positions.push(
last[0],
last[1],
0,
this.totalDistance - segmentDistance,
this.thickness,
0,
);
// this.extrusions(positions, normals, last, out, this.thickness);
// last = capEnd;
} else {
this.extrusions(
positions,
normals,
last,
this.normal,
this.thickness,
this.totalDistance - segmentDistance,
);
}
}
indices.push(index + 0, index + 1, index + 2);
if (!next) {
computeNormal(this.normal, lineA);
if (capSquare) {
// vec2.scaleAndAdd(capEnd, cur, lineA, this.thickness);
// cur = capEnd;
const out1 = vec2.create();
const out2 = vec2.create();
vec2.sub(out2, lineA, this.normal);
vec2.add(out1, lineA, this.normal);
// this.extrusions(positions, normals, cur, out, this.thickness);
normals.push(out2[0], out2[1], 0);
normals.push(out1[0], out1[1], 0);
positions.push(
cur[0],
cur[1],
0,
this.totalDistance,
this.thickness,
0,
);
positions.push(
cur[0],
cur[1],
0,
this.totalDistance,
this.thickness,
0,
);
} else {
this.extrusions(
positions,
normals,
cur,
this.normal,
this.thickness,
this.totalDistance,
);
}
// this.extrusions(positions, normals, cur, this.normal, this.thickness);
indices.push(
...(this.lastFlip === 1
? [index, index + 2, index + 3]
: [index + 2, index + 1, index + 3]),
);
count += 2;
} else {
const flatNext = aProjectFlat([next[0], next[1]]) as [number, number];
if (isPointEqual(flatCur, flatNext)) {
vec2.add(
flatNext,
flatCur,
vec2.normalize(flatNext, vec2.subtract(flatNext, flatCur, flatLast)),
);
}
direction(lineB, flatNext, flatCur);
// stores tangent & miter
const [miterLen, miter] = computeMiter(
tangent,
vec2.create(),
lineA,
lineB,
this.thickness,
);
// normal(tmp, lineA)
// get orientation
let flip = vec2.dot(tangent, this.normal) < 0 ? -1 : 1;
let bevel = joinBevel;
if (!bevel && this.join === 'miter') {
const limit = miterLen;
if (limit > this.miterLimit) {
bevel = true;
}
}
if (bevel) {
normals.push(this.normal[0], this.normal[1], 0);
normals.push(miter[0], miter[1], 0);
positions.push(
cur[0],
cur[1],
0,
this.totalDistance,
-this.thickness * flip,
0,
);
positions.push(
cur[0],
cur[1],
0,
this.totalDistance,
this.thickness * flip,
0,
);
indices.push(
...(this.lastFlip !== -flip
? [index, index + 2, index + 3]
: [index + 2, index + 1, index + 3]),
);
// now add the bevel triangle
indices.push(index + 2, index + 3, index + 4);
computeNormal(tmp, lineB);
vec2.copy(this.normal, tmp); // store normal for next round
normals.push(this.normal[0], this.normal[1], 0);
positions.push(
cur[0],
cur[1],
0,
this.totalDistance,
-this.thickness * flip,
0,
);
count += 3;
} else {
this.extrusions(
positions,
normals,
cur,
miter,
miterLen,
this.totalDistance,
);
indices.push(
...(this.lastFlip === 1
? [index, index + 2, index + 3]
: [index + 2, index + 1, index + 3]),
);
flip = -1;
// the miter is now the normal for our next join
vec2.copy(this.normal, miter);
count += 2;
}
this.lastFlip = flip;
}
return count;
}
private extrusions(
positions: number[],
normals: number[],
point: vec2, // 顶点
normal: vec2, // 法向量
thickness: number, // 高度
distanceRadio: number,
) {
normals.push(normal[0], normal[1], 0);
normals.push(normal[0], normal[1], 0);
positions.push(point[0], point[1], 0, distanceRadio, -thickness, 0);
positions.push(point[0], point[1], 0, distanceRadio, thickness, 0);
}
private lineSegmentDistance(b1: vec2, a1: vec2) {
const dx = a1[0] - b1[0];
const dy = a1[1] - b1[1];
return Math.sqrt(dx * dx + dy * dy);
}
}

View File

@ -71,7 +71,7 @@ export default function(
let lineNormal = null; let lineNormal = null;
const tmp = vec2.create(); const tmp = vec2.create();
let count = indexOffset || 0; let count = indexOffset || 0;
const miterLimit = 3; const miterLimit = 4;
const out: number[] = []; const out: number[] = [];
const attrPos: number[] = []; const attrPos: number[] = [];
@ -172,7 +172,6 @@ export default function(
count += 4; count += 4;
continue; continue;
} }
if (bevel) { if (bevel) {
miterLen = miterLimit; miterLen = miterLimit;
@ -232,7 +231,7 @@ export default function(
return { return {
normals: out, normals: out,
attrIndex, attrIndex,
attrPos: pickData, // [x,y,z, distance, miter ,tatal ] attrPos: pickData, // [x,y,z, distance, miter ,t0tal ]
}; };
} }
// [x,y,z, distance, miter ] // [x,y,z, distance, miter ]

View File

@ -199,16 +199,17 @@ export default class Country extends React.Component {
const Layer = new CountryLayer(scene, { const Layer = new CountryLayer(scene, {
visible: true, visible: true,
data: ProvinceData, data: ProvinceData,
geoDataLevel: 2, geoDataLevel: 1,
joinBy: ['NAME_CHN', 'name'], joinBy: ['NAME_CHN', 'name'],
showBorder: false, showBorder: true,
provinceStroke: 'red',
label: { label: {
field: 'name', field: 'name',
size: 10, size: 10,
padding: [5, 5], padding: [5, 5],
textAllowOverlap: true, textAllowOverlap: true,
}, },
depth: 2, depth: 1,
fill: { fill: {
color: { color: {
field: 'NAME_CHN', field: 'NAME_CHN',
@ -230,52 +231,18 @@ export default class Country extends React.Component {
}, },
}); });
Layer.on('loaded', () => { Layer.on('loaded', () => {
const filldata = Layer.getFillData(); // const filldata = Layer.getFillData();
const border = new LineLayer({ // const border = new LineLayer({
zIndex: 5, // 设置显示层级 // zIndex: 5, // 设置显示层级
}) // })
.source(filldata) // .source(filldata)
.shape('line') // .shape('line')
.size(0.6) // .size(4)
.color('#a00') // .color('#a00')
.style({ // .style({
opacity: 1, // opacity: 1,
}); // });
const hightLayer = new LineLayer({ // scene.addLayer(border);
zIndex: 4, // 设置显示层级
name: 'line3',
})
.source(filldata)
.shape('line')
.size(1.2)
.color('#000')
.style({
opacity: 1,
});
const hightLayer2 = new LineLayer({
zIndex: 3, // 设置显示层级
name: 'line3',
})
.source(filldata)
.shape('line')
.size(2.4)
.color('#fff')
.style({
opacity: 1,
});
scene.addLayer(border);
scene.addLayer(hightLayer);
scene.addLayer(hightLayer2);
Layer.fillLayer.on('click', (feature) => {
hightLayer.setData({
type: 'FeatureCollection',
features: [feature.feature],
});
hightLayer2.setData({
type: 'FeatureCollection',
features: [feature.feature],
});
});
}); });
}); });
this.scene = scene; this.scene = scene;

View File

@ -14,7 +14,7 @@ export default class Country extends React.Component {
public async componentDidMount() { public async componentDidMount() {
const scene = new Scene({ const scene = new Scene({
id: 'map', id: 'map',
map: new Mapbox({ map: new GaodeMap({
center: [116.2825, 39.9], center: [116.2825, 39.9],
pitch: 0, pitch: 0,
style: 'blank', style: 'blank',

View File

@ -21,8 +21,8 @@ export default class MultiLine extends React.Component {
map: new GaodeMap({ map: new GaodeMap({
pitch: 0, pitch: 0,
style: 'dark', style: 'dark',
center: [101.775374, 3], center: [101.775374, 20],
zoom: 14.1, zoom: 3,
}), }),
}); });
@ -34,16 +34,18 @@ export default class MultiLine extends React.Component {
type: 'Feature', type: 'Feature',
properties: {}, properties: {},
geometry: { geometry: {
type: 'MultiLineString', type: 'LineString',
coordinates: [ coordinates: [
[ [90.703125, 34.59704151614417],
[100, 0], [112.8515625, 39.095962936305476],
[101, 1], [117.42187500000001, 29.53522956294847],
], [127.61718749999999, 34.016241889667015],
[ [129.0234375, 40.713955826286046],
[102, 2], [136.40625, 36.87962060502676],
[103, 3], [136.40625, 28.304380682962783],
], [130.078125, 25.16517336866393],
[125.5078125, 20.96143961409684],
[130.078125, 17.644022027872726],
], ],
}, },
}, },
@ -51,8 +53,11 @@ export default class MultiLine extends React.Component {
}) })
.shape('line') .shape('line')
.color('red') .color('red')
.size(2)
.style({ .style({
opacity: 1.0, opacity: 0.5,
lineType: 'dash',
dashArray: [2, 2, 4, 2],
}); });
scene.addLayer(layer); scene.addLayer(layer);
} }