From e07bf91cd7a51979cff27176133912ca36a2f4ac Mon Sep 17 00:00:00 2001 From: Azgaar Date: Tue, 13 Sep 2022 00:13:13 +0300 Subject: [PATCH] refactor: draw state labels start --- src/layers/renderers/drawLabels.ts | 128 ++++++++++++++++++++--- src/scripts/generation/generation.ts | 4 +- src/scripts/getPolesOfInaccessibility.ts | 4 +- src/utils/debugUtils.ts | 13 +++ 4 files changed, 131 insertions(+), 18 deletions(-) diff --git a/src/layers/renderers/drawLabels.ts b/src/layers/renderers/drawLabels.ts index a515f0cc..bcaecec2 100644 --- a/src/layers/renderers/drawLabels.ts +++ b/src/layers/renderers/drawLabels.ts @@ -1,13 +1,17 @@ -import {MIN_LAND_HEIGHT} from "config/generation"; import * as d3 from "d3"; import Delaunator from "delaunator"; -import {Voronoi} from "modules/voronoi"; +import FlatQueue from "flatqueue"; +import {simplify} from "scripts/simplify"; +import {Voronoi} from "modules/voronoi"; +import {MIN_LAND_HEIGHT} from "config/generation"; import {findCell} from "utils/graphUtils"; import {isState} from "utils/typeUtils"; +import {drawPath, drawPoint, drawPolyline} from "utils/debugUtils"; export function drawLabels() { /* global */ const {cells, vertices, features, states, burgs} = pack; + /* global: findCell, graphWidth, graphHeight */ drawStateLabels(cells, features, states, vertices); drawBurgLabels(burgs); @@ -62,42 +66,64 @@ function drawStateLabels(cells: IPack["cells"], features: TPackFeatures, states: const mode = options.stateLabelsMode || "auto"; const labelPaths = getLabelPaths(); + console.log(labelPaths); function getLabelPaths() { - const labelPaths: number[][] = []; + const labelPaths: [number, TPoints][] = []; const MIN_HULL_SIZE = 20; + const lineGen = d3.line().curve(d3.curveBundle.beta(1)); for (const state of states) { if (!isState(state)) continue; - const used: Dict = {}; + const used: Dict = {}; // mutable const visualCenter = findCell(...state.pole); - const start = cells.state[visualCenter] === state.i ? visualCenter : state.center; - const hull = getHull(start, state.i, state.cells, used); + const startingCell = cells.state[visualCenter] === state.i ? visualCenter : state.center; + const hull = getHull(startingCell, state.i, state.cells, used); const points = [...hull].map(vertex => vertices.p[vertex]); + const delaunay = Delaunator.from(points); const voronoi = new Voronoi(delaunay, points, points.length); - const chain = connectCenters(voronoi.vertices, state.pole[1]); - const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length); - labelPaths.push([state.i, relaxed]); + const chain = connectVertices(voronoi.vertices, state.pole, used); + + drawPoint(state.pole, {color: "blue", radius: 1}); + + if (state.i === 1) { + points.forEach(point => { + drawPoint(point, {color: "red", radius: 0.5}); + }); + } + + const pathPoints = simplify( + chain.map(i => voronoi.vertices.p[i]), + 30 + ); + + drawPath(lineGen(pathPoints)!, {stroke: "red", strokeWidth: 0.5}); + + labelPaths.push([state.i, pathPoints]); } return labelPaths; function getHull(start: number, stateId: number, stateCells: number, used: Dict) { - const queue = [start]; - const hull = new Set(); - const addHull = (cellId: number, neibCellIndex: number) => hull.add(cells.v[cellId][neibCellIndex]); const maxPassableLakeSize = stateCells / 10; + const queue = [start]; + + const hull = new Set(); + const addToHull = (cellId: number, index: number) => { + const vertex = cells.v[cellId][index]; + if (vertex) hull.add(vertex); + }; while (queue.length) { const cellId = queue.pop()!; - cells.c[cellId].forEach((neibCellId, neibCellIndex) => { + cells.c[cellId].forEach((neibCellId, index) => { if (used[neibCellId]) return; - if (isHullEdge(neibCellId)) return addHull(neibCellId, neibCellIndex); - used[neibCellId] = true; + + if (isHullEdge(neibCellId)) return addToHull(cellId, index); return queue.push(neibCellId); }); } @@ -125,5 +151,77 @@ function drawStateLabels(cells: IPack["cells"], features: TPackFeatures, states: return false; } } + + function connectVertices(vertices: Voronoi["vertices"], pole: TPoint, used: Dict) { + // check if vertex is inside the area + const inside = vertices.p.map(([x, y]) => { + if (x <= 0 || y <= 0 || x >= graphWidth || y >= graphHeight) return false; // out of the screen + return used[findCell(x, y)]; + }); + + const innerVertices = d3.range(vertices.p.length).filter(i => inside[i]); + if (innerVertices.length < 2) return [0]; + + const horyzontalShift = getHoryzontalShift(vertices.p.length); + const {right: start, left: end} = getEdgeVertices(innerVertices, vertices.p, pole, horyzontalShift); + + // connect leftmost and rightmost vertices with shortest path + const cost: number[] = []; + const from: number[] = []; + const queue = new FlatQueue(); + queue.push(start, 0); + + while (queue.length) { + const priority = queue.peekValue()!; + const next = queue.pop()!; + + if (next === end) break; + + for (const neibVertex of vertices.v[next]) { + if (neibVertex === -1) continue; + + const totalCost = priority + (inside[neibVertex] ? 1 : 100); + if (from[neibVertex] || totalCost >= cost[neibVertex]) continue; + + cost[neibVertex] = totalCost; + from[neibVertex] = next; + queue.push(neibVertex, totalCost); + } + } + + // restore path + const chain = [end]; + let cur = end; + while (cur !== start) { + cur = from[cur]; + if (inside[cur]) chain.push(cur); + } + return chain; + } + + function getHoryzontalShift(verticesNumber: number) { + console.log({verticesNumber}); + return 0; + if (verticesNumber < 100) return 1; + if (verticesNumber < 200) return 0.3; + if (verticesNumber < 300) return 0.1; + return 0; + } + + function getEdgeVertices(innerVertices: number[], points: TPoints, pole: TPoint, horyzontalShift: number) { + let leftmost = {value: Infinity, vertex: innerVertices.at(0)!}; + let rightmost = {value: -Infinity, vertex: innerVertices.at(-1)!}; + + for (const vertex of innerVertices) { + const [x, y] = points[vertex]; + const valueX = x - pole[0]; + const valueY = Math.abs(y - pole[1]) * horyzontalShift; + + if (valueX + valueY < leftmost.value) leftmost = {value: valueX + valueY, vertex}; + if (valueX - valueY > rightmost.value) rightmost = {value: valueX - valueY, vertex}; + } + + return {left: leftmost.vertex, right: rightmost.vertex}; + } } } diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index afbe73db..5338eb5e 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -69,8 +69,8 @@ async function generate(options?: IGenerationOptions) { // renderLayer("biomes"); renderLayer("burgs"); renderLayer("routes"); - // renderLayer("states"); - renderLayer("provinces"); + renderLayer("states"); + renderLayer("labels"); // pack.cells.route.forEach((route, index) => { // if (route === 2) drawPoint(pack.cells.p[index], {color: "black"}); diff --git a/src/scripts/getPolesOfInaccessibility.ts b/src/scripts/getPolesOfInaccessibility.ts index 384cea99..e7e7a414 100644 --- a/src/scripts/getPolesOfInaccessibility.ts +++ b/src/scripts/getPolesOfInaccessibility.ts @@ -14,10 +14,12 @@ interface IGetPolesProps { export function getPolesOfInaccessibility(props: IGetPolesProps) { TIME && console.time("getPolesOfInaccessibility"); const multiPolygons = getMultiPolygons(props); + const sortByLength = (a: unknown[], b: unknown[]) => b.length - a.length; + console.log(multiPolygons); const poles: Dict = Object.fromEntries( Object.entries(multiPolygons).map(([id, multiPolygon]) => { - const [x, y] = polylabel(multiPolygon, 20); + const [x, y] = polylabel(multiPolygon.sort(sortByLength), 20); return [id, [rn(x), rn(y)]]; }) ); diff --git a/src/utils/debugUtils.ts b/src/utils/debugUtils.ts index f72c614f..c973cedf 100644 --- a/src/utils/debugUtils.ts +++ b/src/utils/debugUtils.ts @@ -31,6 +31,19 @@ export function drawLine([x1, y1]: TPoint, [x2, y2]: TPoint, {stroke = "#444", s .attr("stroke-width", strokeWidth); } +export function drawPolyline(points: TPoints, {fill = "none", stroke = "#444", strokeWidth = 0.2} = {}) { + debug + .append("polyline") + .attr("points", points.join(" ")) + .attr("fill", fill) + .attr("stroke", stroke) + .attr("stroke-width", strokeWidth); +} + +export function drawPath(d: string, {fill = "none", stroke = "#444", strokeWidth = 0.2} = {}) { + debug.append("path").attr("d", d).attr("fill", fill).attr("stroke", stroke).attr("stroke-width", strokeWidth); +} + export function drawArrow([x1, y1]: TPoint, [x2, y2]: TPoint, {width = 1, color = "#444"} = {}) { const normal = getNormal([x1, y1], [x2, y2]); const [xMid, yMid] = [(x1 + x2) / 2, (y1 + y2) / 2];