import SkeletonBuilder from "https://cdn.skypack.dev/straight-skeleton@1.0.1";
import {
Point,
Vector,
Line,
Polygon,
Multiline
} from "https://cdn.skypack.dev/@flatten-js/core@1.3.5";
import earcut from "https://cdn.skypack.dev/earcut@2.2.4";
mapboxgl.accessToken =
"pk.eyJ1IjoiYnJhbnpoYW5nIiwiYSI6ImNqM3FycmVldjAxZTUzM2xqMmllNnBjMHkifQ.Wv3ekbtia0BuUHGWVUGoFg";
const beforeMap = new mapboxgl.Map({
container: "before",
style: "mapbox://styles/mapbox/streets-v12",
projection: "mercator",
center: [114.31548, 30.565478],
zoom: 15.3
});
const afterMap = new mapboxgl.Map({
container: "after",
style: "mapbox://styles/mapbox/streets-v12",
projection: "mercator",
center: [114.31548, 30.565478],
zoom: 15.3
});
const container = "#comparison-container";
const map = new mapboxgl.Compare(beforeMap, afterMap, container, {});
afterMap.on("load", () => {
afterMap.setLayoutProperty("water-shadow", "visibility", "none");
const layer = new shadowLayer(
{
id: "new-water-shadow"
},
"water-shadow"
);
afterMap.addLayer(layer);
});
const lightGreen = [0, 0, 0, 0];
const darkGreen = [49 / 255, 165 / 255, 0, 0];
const middleGreen = [20 / 255, 20 / 255, 20 / 255, 0.15];
let bufferArray = new BufferArray();
class shadowLayer {
constructor(options) {
this.id = options.layerId || "shadow-layer";
this.type = "custom";
this.renderingMode = "3d";
}
onAdd(map, gl) {
this.map = map;
map.on("moveend", this.updateData.bind(this));
this.updateData();
const vert = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(
vert,
`
uniform mat4 u_matrix;
attribute vec4 a_pos;
varying float v_distance;
void main(void) {
gl_Position = u_matrix * vec4(a_pos.xy, 0.0, 1.0);
v_distance = a_pos[2];
}
`
);
gl.compileShader(vert);
const frag = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(
frag,
`
varying highp float v_distance;
uniform highp float u_stroke_width;
uniform highp vec4 u_stroke_colour;
uniform highp vec4 u_fill_colour;
uniform highp float u_stroke_offset;
uniform highp vec4 u_inset_colour;
uniform highp float u_inset_width;
void main(void) {
highp float inset_dist = (u_inset_width - v_distance) / u_inset_width;
highp float inset_alpha = clamp(min(1.0, smoothstep(0.0, 1.5, inset_dist)), 0.0, 1.0);
highp float fill_alpha = clamp(sign(v_distance + u_stroke_offset) - inset_alpha, 0.0, 1.0);
gl_FragColor = u_inset_colour * inset_alpha + u_fill_colour * fill_alpha;
}
`
);
gl.compileShader(frag);
this.program = gl.createProgram();
gl.attachShader(this.program, vert);
gl.attachShader(this.program, frag);
gl.linkProgram(this.program);
this.u_stroke_width = gl.getUniformLocation(this.program, "u_stroke_width");
this.u_stroke_colour = gl.getUniformLocation(
this.program,
"u_stroke_colour"
);
this.u_fill_colour = gl.getUniformLocation(this.program, "u_fill_colour");
this.u_stroke_offset = gl.getUniformLocation(
this.program,
"u_stroke_offset"
);
this.u_inset_width = gl.getUniformLocation(this.program, "u_inset_width");
this.u_inset_colour = gl.getUniformLocation(this.program, "u_inset_colour");
this.u_inset_blur = gl.getUniformLocation(this.program, "u_inset_blur");
this.a_pos = gl.getAttribLocation(this.program, "a_pos");
}
render(gl, matrix) {
gl.useProgram(this.program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, bufferArray.arrayBuffer, gl.STATIC_DRAW);
gl.enableVertexAttribArray(this.a_pos);
gl.vertexAttribPointer(
this.a_pos,
4,
gl.FLOAT,
false,
bufferArray.byteSize,
0
);
gl.uniformMatrix4fv(
gl.getUniformLocation(this.program, "u_matrix"),
false,
matrix
);
gl.uniform1f(this.u_stroke_width, 0);
gl.uniform4fv(this.u_stroke_colour, darkGreen);
gl.uniform4fv(this.u_fill_colour, lightGreen);
gl.uniform1f(this.u_stroke_offset, 0);
gl.uniform4fv(this.u_inset_colour, middleGreen);
gl.uniform1f(this.u_inset_width, 0.000001);
// gl.enable(gl.BLEND);
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.TRIANGLES, 0, bufferArray.pos / bufferArray.byteSize);
}
updateData() {
this.center = mapboxgl.MercatorCoordinate.fromLngLat(this.map.getCenter());
try {
const features = this.map.queryRenderedFeatures({ layers: ["water"] });
if (features.length === 0) {
return;
}
const data = turf.dissolve(
turf.flatten({
type: "FeatureCollection",
features: features
})
);
bufferArray = new BufferArray();
data.features.forEach((feature) => {
const coordinates = this.lngLatToMercator(feature.geometry.coordinates);
coordinates.forEach((cc) => {
cc.pop();
});
const skeleton = SkeletonBuilder.BuildFromGeoJSON([coordinates]);
for (const poly of calculateRoundCorner(skeleton)) {
for (const i of poly.indices) {
const x = poly.flat[i * 3] / 1000000 + this.center.x;
const y = poly.flat[i * 3 + 1] / 1000000 + this.center.y;
const d = poly.flat[i * 3 + 2] / 1000000;
bufferArray.add(x, y, d || 0);
}
}
});
} catch (e) {
console.log(e);
}
}
lngLatToMercator(coordinates) {
if (typeof coordinates[0] === "number") {
const coordinate = mapboxgl.MercatorCoordinate.fromLngLat({
lng: coordinates[0],
lat: coordinates[1]
});
return [
(coordinate.x - this.center.x) * 1000000,
(coordinate.y - this.center.y) * 1000000
];
} else {
return coordinates.map((c) => this.lngLatToMercator(c));
}
}
}
function BufferArray() {
this.arrayBuffer = new ArrayBuffer(1024000);
this.int16 = new Int16Array(this.arrayBuffer);
this.float32 = new Float32Array(this.arrayBuffer);
this.pos = 0;
this.byteSize = 32;
}
BufferArray.prototype.add = function (x, y, dx, dy) {
this.float32[this.pos / 4 + 0] = x;
this.float32[this.pos / 4 + 1] = y;
this.float32[this.pos / 4 + 2] = dx;
this.pos += this.byteSize;
};
function calculateRoundCorner(skeleton) {
const originalVertices = new Map();
for (const [key, value] of skeleton.Distances) {
originalVertices.set(`${key.X},${key.Y}`, value);
}
const output = [];
// traverse every edge of original Polygon.
for (const { Edge: e, Polygon: p } of skeleton.Edges) {
const poly = new Polygon(p.map((po) => [po.X, po.Y]));
let polygonCut = false;
for (let i = 0; i < p.length - 1; i++) {
if (
skeleton.Distances.get(p[i]) === 0 &&
skeleton.Distances.get(p[i + 1]) === 0
) {
// this edge is coming from original Polygon.
const cutLines = [];
const previous = i > 0 ? p[i - 1] : p[p.length - 1];
const next = i + 1 < p.length - 1 ? p[i + 2] : p[0];
if (p[i + 1].Sub(p[i]).Dot(previous.Sub(p[i])) < 0) {
// create a new line perpendicular to this edge
// !! the definition of Norm in 'straight-skeleton' is different from 'flatten-js'.
cutLines.push(
new Line(new Point(p[i].X, p[i].Y), new Vector(e.Norm.X, e.Norm.Y))
);
}
if (p[i].Sub(p[i + 1]).Dot(next.Sub(p[i + 1])) < 0) {
cutLines.push(
new Line(
new Point(p[i + 1].X, p[i + 1].Y),
new Vector(e.Norm.X, e.Norm.Y)
)
);
}
if (cutLines.length > 0) {
polygonCut = true;
const multiline = new Multiline(cutLines);
for (let line of cutLines) {
const ip = line.intersect(poly);
const ipSorted = line.sortPoints(ip);
multiline.split(ipSorted);
}
const splitPolygons = poly.cut(multiline);
for (const splitPolygon of splitPolygons) {
const sharePoints = sharePointsWithOriginalPolygon(
splitPolygon,
originalVertices
);
if (sharePoints.length === 2) {
const copy = new Map();
for (const [key, value] of originalVertices) {
copy.set(key, value);
}
splitPolygon.vertices.forEach((vertex) => {
if (!copy.has(`${vertex.x},${vertex.y}`)) {
copy.set(
`${vertex.x},${vertex.y}`,
new Point(vertex.x, vertex.y).distanceTo(
new Line(
new Point(sharePoints[0].x, sharePoints[0].y),
new Point(sharePoints[1].x, sharePoints[1].y)
)
)[0]
);
}
});
output.push(triangulate(splitPolygon, copy));
} else if (sharePoints.length === 1) {
// recalculate distance
const copy = new Map();
for (const [key, value] of originalVertices) {
copy.set(key, value);
}
splitPolygon.vertices.forEach((vertex) => {
copy.set(
`${vertex.x},${vertex.y}`,
Math.sqrt(
(vertex.x - sharePoints[0].x) *
(vertex.x - sharePoints[0].x) +
(vertex.y - sharePoints[0].y) *
(vertex.y - sharePoints[0].y)
)
);
});
output.push(triangulate(splitPolygon, copy));
} else {
console.log("maybe error");
output.push(triangulate(splitPolygon, originalVertices));
}
}
}
}
}
if (!polygonCut) {
output.push(triangulate(poly, originalVertices));
}
}
return output;
}
function sharePointsWithOriginalPolygon(polygon, originalVertices) {
return polygon.vertices.filter(
(vertex) => originalVertices.get(`${vertex.x},${vertex.y}`) === 0
);
}
function triangulate(polygon, originalVertices) {
const flat = [];
polygon.vertices.forEach((vertex) => {
flat.push(
vertex.x,
vertex.y,
originalVertices.get(`${vertex.x},${vertex.y}`)
);
});
return {
flat: flat,
indices: earcut(flat, undefined, 3)
};
}