diff --git a/src/layers/renderers/drawCoastline.ts b/src/layers/renderers/drawCoastline.ts new file mode 100644 index 00000000..92dffb0e --- /dev/null +++ b/src/layers/renderers/drawCoastline.ts @@ -0,0 +1,50 @@ +import * as d3 from "d3"; + +import {simplify} from "scripts/simplify"; +import {clipPoly} from "utils/lineUtils"; +import {round} from "utils/stringUtils"; + +export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) { + const landMask = defs.select("#land"); + const waterMask = defs.select("#water"); + + const lineGen = d3.line().curve(d3.curveBasisClosed); + const SIMPLIFICATION_TOLERANCE = 0.5; // px + + for (const feature of features) { + if (!feature) continue; + + const points = clipPoly(feature.vertices.map(vertex => vertices.p[vertex])); + const simplifiedPoints = simplify(points, SIMPLIFICATION_TOLERANCE); + const path = round(lineGen(simplifiedPoints)!); + + landMask + .append("path") + .attr("d", path) + .attr("fill", "black") + .attr("id", "land_" + feature.i); + + if (feature.type === "lake") { + lakes + .select("#freshwater") + .append("path") + .attr("d", path) + .attr("id", "lake_" + feature.i) + .attr("data-f", feature.i); + } else { + waterMask + .append("path") + .attr("d", path) + .attr("fill", "black") + .attr("id", "water_" + feature.i); + + const group = feature.group === "lake_island" ? "lake_island" : "sea_island"; + coastline + .select("#" + group) + .append("path") + .attr("d", path) + .attr("id", "island_" + feature.i) + .attr("data-f", feature.i); + } + } +} diff --git a/src/layers/renderers/index.ts b/src/layers/renderers/index.ts index a1cb8913..88dc56b8 100644 --- a/src/layers/renderers/index.ts +++ b/src/layers/renderers/index.ts @@ -3,6 +3,7 @@ import {TIME} from "config/logging"; import {drawBiomes} from "./drawBiomes"; import {drawBorders} from "./drawBorders"; import {drawCells} from "./drawCells"; +import {drawCoastline} from "./drawCoastline"; import {drawCoordinates} from "./drawCoordinates"; import {drawCultures} from "./drawCultures"; import {drawEmblems} from "./drawEmblems"; @@ -23,6 +24,7 @@ const layerRenderersMap = { biomes: drawBiomes, borders: drawBorders, cells: drawCells, + coastline: drawCoastline, coordinates: drawCoordinates, cultures: drawCultures, emblems: drawEmblems, diff --git a/src/modules/coastline.js b/src/modules/coastline.js deleted file mode 100644 index 343ed1e7..00000000 --- a/src/modules/coastline.js +++ /dev/null @@ -1,141 +0,0 @@ -import * as d3 from "d3"; - -import {ERROR, TIME} from "config/logging"; -import {clipPoly} from "utils/lineUtils"; -import {round} from "utils/stringUtils"; -import {Ruler} from "modules/measurers"; - -// Detect and draw the coastline -export function drawCoastline(pack) { - TIME && console.time("drawCoastline"); - - const {cells, vertices, features} = pack; - const n = cells.i.length; - - const used = new Uint8Array(features.length); // store connected features - const largestLand = d3.scan( - features.map(f => (f.land ? f.cells : 0)), - (a, b) => b - a - ); - - const landMask = defs.select("#land"); - const waterMask = defs.select("#water"); - const lineGen = d3.line().curve(d3.curveBasisClosed); - - for (const i of cells.i) { - const startFromEdge = !i && cells.h[i] >= 20; - if (!startFromEdge && cells.t[i] !== -1 && cells.t[i] !== 1) continue; // non-edge cell - const f = cells.f[i]; - if (used[f]) continue; // already connected - if (features[f].type === "ocean") continue; // ocean cell - - const type = features[f].type === "lake" ? 1 : -1; // type value to search for - const start = findStart(i, type); - if (start === -1) continue; // cannot start here - let vchain = connectVertices(start, type); - if (features[f].type === "lake") relax(vchain, 1.2); - used[f] = 1; - let points = clipPoly(vchain.map(v => vertices.p[v])); - const area = d3.polygonArea(points); // area with lakes/islands - if (area > 0 && features[f].type === "lake") { - points = points.reverse(); - vchain = vchain.reverse(); - } - - features[f].area = Math.abs(area); - features[f].vertices = vchain; - - const path = round(lineGen(points)); - if (features[f].type === "lake") { - landMask - .append("path") - .attr("d", path) - .attr("fill", "black") - .attr("id", "land_" + f); - // waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes - lakes - .select("#freshwater") - .append("path") - .attr("d", path) - .attr("id", "lake_" + f) - .attr("data-f", f); // draw the lake - } else { - landMask - .append("path") - .attr("d", path) - .attr("fill", "white") - .attr("id", "land_" + f); - waterMask - .append("path") - .attr("d", path) - .attr("fill", "black") - .attr("id", "water_" + f); - const g = features[f].group === "lake_island" ? "lake_island" : "sea_island"; - coastline - .select("#" + g) - .append("path") - .attr("d", path) - .attr("id", "island_" + f) - .attr("data-f", f); // draw the coastline - } - - // draw ruler to cover the biggest land piece - if (f === largestLand) { - const from = points[d3.scan(points, (a, b) => a[0] - b[0])]; - const to = points[d3.scan(points, (a, b) => b[0] - a[0])]; - rulers.create(Ruler, [from, to]); - } - } - - // find cell vertex to start path detection - function findStart(i, t) { - if (t === -1 && cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell - const filtered = cells.c[i].filter(c => cells.t[c] === t); - const index = cells.c[i].indexOf(d3.min(filtered)); - return index === -1 ? index : cells.v[i][index]; - } - - // connect vertices to chain - function connectVertices(start, t) { - const chain = []; // vertices chain to form a path - for (let i = 0, current = start; i === 0 || (current !== start && i < 50000); i++) { - const prev = chain[chain.length - 1]; // previous vertex in chain - chain.push(current); // add current vertex to sequence - const c = vertices.c[current]; // cells adjacent to vertex - const v = vertices.v[current]; // neighboring vertices - const c0 = c[0] >= n || cells.t[c[0]] === t; - const c1 = c[1] >= n || cells.t[c[1]] === t; - const c2 = c[2] >= n || cells.t[c[2]] === t; - if (v[0] !== prev && c0 !== c1) current = v[0]; - else if (v[1] !== prev && c1 !== c2) current = v[1]; - else if (v[2] !== prev && c0 !== c2) current = v[2]; - if (current === chain[chain.length - 1]) { - ERROR && console.error("Next vertex is not found"); - break; - } - } - return chain; - } - - // move vertices that are too close to already added ones - function relax(vchain, r) { - const p = vertices.p, - tree = d3.quadtree(); - - for (let i = 0; i < vchain.length; i++) { - const v = vchain[i]; - let [x, y] = [p[v][0], p[v][1]]; - if (i && vchain[i + 1] && tree.find(x, y, r) !== undefined) { - const v1 = vchain[i - 1]; - const v2 = vchain[i + 1]; - const [x1, y1] = [p[v1][0], p[v1][1]]; - const [x2, y2] = [p[v2][0], p[v2][1]]; - [x, y] = [(x1 + x2) / 2, (y1 + y2) / 2]; - p[v] = [x, y]; - } - tree.add([x, y]); - } - } - - TIME && console.timeEnd("drawCoastline"); -} diff --git a/src/modules/markup.ts b/src/modules/markup.ts index a6b5dabd..95428f12 100644 --- a/src/modules/markup.ts +++ b/src/modules/markup.ts @@ -1,14 +1,11 @@ -import * as d3 from "d3"; - import {MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation"; -import {ERROR, TIME} from "config/logging"; +import {TIME} from "config/logging"; import {INT8_MAX} from "constants"; // @ts-expect-error js module import {aleaPRNG} from "scripts/aleaPRNG"; import {createTypedArray} from "utils/arrayUtils"; -import {dist2, pick} from "utils/functionUtils"; -import {getColors} from "utils/colorUtils"; -import {clipPoly} from "utils/lineUtils"; +import {dist2} from "utils/functionUtils"; +import {getFeatureVertices} from "scripts/connectVertices"; const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD; @@ -81,8 +78,8 @@ export function markupPackFeatures( ) { TIME && console.time("markupPackFeatures"); - const packCellsNumber = cells.h.length; const gridCellsNumber = grid.cells.h.length; + const packCellsNumber = cells.c.length; const features: TPackFeatures = [0]; const featureIds = new Uint16Array(packCellsNumber); // ids of features, starts from 1 @@ -99,73 +96,6 @@ export function markupPackFeatures( harbor[cellId] = waterCells.length; }; - const OCEAN_MIN_SIZE = gridCellsNumber / 25; - const SEA_MIN_SIZE = gridCellsNumber / 1000; - const CONTINENT_MIN_SIZE = gridCellsNumber / 10; - const ISLAND_MIN_SIZE = gridCellsNumber / 1000; - - function defineOceanGroup(cellsNumber: number) { - if (cellsNumber > OCEAN_MIN_SIZE) return "ocean"; - if (cellsNumber > SEA_MIN_SIZE) return "sea"; - return "gulf"; - } - - function defineIslandGroup(firstCell: number, cellsNumber: number) { - const prevCellFeature = features[featureIds[firstCell - 1]]; - - if (prevCellFeature && prevCellFeature.type === "lake") return "lake_island"; - if (cellsNumber > CONTINENT_MIN_SIZE) return "continent"; - if (cellsNumber > ISLAND_MIN_SIZE) return "island"; - return "isle"; - } - - function addIsland(featureId: number, border: boolean, firstCell: number, cells: number, vertices: number[]) { - const group = defineIslandGroup(firstCell, cells); - const feature: IPackFeatureIsland = { - i: featureId, - type: "island", - group, - land: true, - border, - cells, - firstCell, - vertices - }; - features.push(feature); - } - - function addOcean(featureId: number, firstCell: number, cells: number, vertices: number[]) { - const group = defineOceanGroup(cells); - const feature: IPackFeatureOcean = { - i: featureId, - type: "ocean", - group, - land: false, - border: false, - cells, - firstCell, - vertices - }; - features.push(feature); - } - - function addLake(featureId: number, firstCell: number, cells: number, vertices: number[]) { - const group = "freshwater"; // temp, to be defined later - const name = ""; // temp, to be defined later - const feature: IPackFeatureLake = { - i: featureId, - type: "lake", - group, - name, - land: false, - border: false, - cells, - firstCell, - vertices - }; - features.push(feature); - } - const queue = [0]; for (let featureId = 1; queue[0] !== -1; featureId++) { const firstCell = queue[0]; @@ -175,8 +105,6 @@ export function markupPackFeatures( let border = false; // true if feature touches map border let cellNumber = 1; // count cells in a feature - const featureCells = [firstCell]; - while (queue.length) { const cellId = queue.pop()!; if (cells.b[cellId]) border = true; @@ -199,59 +127,30 @@ export function markupPackFeatures( queue.push(neighborId); featureIds[neighborId] = featureId; cellNumber++; - featureCells.push(neighborId); } } } - cells.v[firstCell] - .map(v => vertices.p[v]) - .forEach(([x, y]) => { - d3.select("#debug").append("circle").attr("cx", x).attr("cy", y).attr("r", 0.2).attr("fill", "yellow"); - }); + const featureVertices = getFeatureVertices({firstCell, vertices, cells, featureIds, featureId}); - const startingCell = findStartingCell({firstCell, featureIds, featureId, vertices, cells, packCellsNumber}); + // let points = clipPoly(vchain.map(v => vertices.p[v])); + // const area = d3.polygonArea(points); // area with lakes/islands + // if (area > 0 && features[f].type === "lake") { + // points = points.reverse(); + // vchain = vchain.reverse(); + // } - const isOuterCell = (cellId: number) => cellId >= packCellsNumber; - const startingVertex = findStartingVertex({ - startingCell, + const feature = addFeature({ + features, + firstCell, + land, border, - featureIds, + featureVertices, featureId, - vertices, - cells, - isOuterCell + cellNumber, + gridCellsNumber }); - - if (startingVertex === undefined || startingVertex > vertices.p.length) { - throw new Error("Starting vertex not found"); - } - - const color = featureId === 1 ? "#2274cc" : getColors(12)[featureId % 12]; - const paths: TPoint[][] = featureCells.map(i => cells.v[i].map(v => vertices.p[v])); - d3.select("#cells") - .append("path") - .attr("d", "M" + paths.join("M")) - .attr("fill", color) - .attr("fill-opacity", 0.5) - .attr("stroke", "#333") - .attr("stroke-width", "0.1"); - - const [x, y] = cells.p[firstCell]; - d3.select("#debug").append("circle").attr("cx", x).attr("cy", y).attr("r", 1).attr("fill", "blue"); - const [cx, cy] = vertices.p[startingVertex]; - d3.select("#debug").append("circle").attr("cx", cx).attr("cy", cy).attr("r", 1.5).attr("fill", "red"); - - const featureVertices = connectVertices({vertices, startingVertex, featureIds, featureId}); - - const lineGen = d3.line(); - const points = clipPoly(featureVertices.map(v => vertices.p[v])); - const path = lineGen(points)!; - d3.select("#sea_island").attr("stroke", "black").append("path").attr("d", path); - - if (land) addIsland(featureId, border, firstCell, cellNumber, []); - else if (border) addOcean(featureId, firstCell, cellNumber, []); - else addLake(featureId, firstCell, cellNumber, []); + features.push(feature); queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell } @@ -264,6 +163,97 @@ export function markupPackFeatures( return {features, featureIds, distanceField: dfLandMarked, haven, harbor}; } +function addFeature({ + features, + firstCell, + land, + border, + featureVertices, + featureId, + cellNumber, + gridCellsNumber +}: { + features: TPackFeatures; + firstCell: number; + land: boolean; + border: boolean; + featureVertices: number[]; + featureId: number; + cellNumber: number; + gridCellsNumber: number; +}) { + const OCEAN_MIN_SIZE = gridCellsNumber / 25; + const SEA_MIN_SIZE = gridCellsNumber / 1000; + const CONTINENT_MIN_SIZE = gridCellsNumber / 10; + const ISLAND_MIN_SIZE = gridCellsNumber / 1000; + + if (land) return addIsland(); + if (border) return addOcean(); + return addLake(); + + function addIsland() { + const group = defineIslandGroup(); + const feature: IPackFeatureIsland = { + i: featureId, + type: "island", + group, + land: true, + border, + cells: cellNumber, + firstCell, + vertices: featureVertices + }; + return feature; + } + + function addOcean() { + const group = defineOceanGroup(); + const feature: IPackFeatureOcean = { + i: featureId, + type: "ocean", + group, + land: false, + border: false, + cells: cellNumber, + firstCell, + vertices: featureVertices + }; + return feature; + } + + function addLake() { + const group = "freshwater"; // temp, to be defined later + const name = ""; // temp, to be defined later + const feature: IPackFeatureLake = { + i: featureId, + type: "lake", + group, + name, + land: false, + border: false, + cells: cellNumber, + firstCell, + vertices: featureVertices + }; + return feature; + } + + function defineOceanGroup() { + if (cellNumber > OCEAN_MIN_SIZE) return "ocean"; + if (cellNumber > SEA_MIN_SIZE) return "sea"; + return "gulf"; + } + + function defineIslandGroup() { + const prevFeature = features.at(-1); + + if (prevFeature && prevFeature.type === "lake") return "lake_island"; + if (cellNumber > CONTINENT_MIN_SIZE) return "continent"; + if (cellNumber > ISLAND_MIN_SIZE) return "island"; + return "isle"; + } +} + // calculate distance to coast for every cell function markup({ distanceField, @@ -294,111 +284,3 @@ function markup({ return distanceField; } - -function findStartingCell({ - firstCell, - featureIds, - featureId, - vertices, - cells, - packCellsNumber -}: { - firstCell: number; - featureIds: Uint16Array; - featureId: number; - vertices: IGraphVertices; - cells: Pick; - packCellsNumber: number; -}) { - const bordersOtherFeature = cells.c[firstCell].some(neighbor => featureIds[neighbor] !== featureId); - if (bordersOtherFeature) return firstCell; - - const neibCells = cells.c[firstCell].sort((a, b) => a - b); - for (const neibCell of neibCells) { - const cellVertices = cells.v[neibCell]; - const edgingVertex = cellVertices.findIndex(vertex => vertices.c[vertex].some(cellId => cellId >= packCellsNumber)); - if (edgingVertex !== -1) { - const engingCell = cells.c[neibCell]; - return engingCell[edgingVertex]; - } - } - - throw new Error(`Markup: firstCell ${firstCell} of feature ${featureId} has no neighbors of other features`); -} - -function findStartingVertex({ - startingCell, - border, - featureIds, - featureId, - vertices, - cells, - isOuterCell -}: { - startingCell: number; - border: boolean; - featureIds: Uint16Array; - featureId: number; - vertices: IGraphVertices; - cells: Pick; - isOuterCell: (cellId: number) => boolean; -}) { - const neibCells = cells.c[startingCell]; - const cellVertices = cells.v[startingCell]; - - if (border) { - const externalVertex = cellVertices.find(vertex => { - const [x, y] = vertices.p[vertex]; - if (x < 0 || y < 0) return true; - return vertices.c[vertex].some(isOuterCell); - }); - if (externalVertex !== undefined) return externalVertex; - } - - const otherFeatureNeibs = neibCells.filter(neibCell => featureIds[neibCell] !== featureId); - if (!otherFeatureNeibs.length) { - throw new Error(`Markup: firstCell ${startingCell} of feature ${featureId} has no neighbors of other features`); - } - - const index = neibCells.indexOf(d3.min(otherFeatureNeibs)!); - return cellVertices[index]; -} - -const CONNECT_VERTICES_MAX_ITERATIONS = 50000; - -// connect vertices around feature -function connectVertices({ - vertices, - startingVertex, - featureIds, - featureId -}: { - vertices: IGraphVertices; - startingVertex: number; - featureIds: Uint16Array; - featureId: number; -}) { - const ofSameType = (cellId: number) => featureIds[cellId] === featureId; - const chain: number[] = []; // vertices chain to form a path - - let next = startingVertex; - for (let i = 0; i === 0 || (next !== startingVertex && i < CONNECT_VERTICES_MAX_ITERATIONS); i++) { - const previous = chain.at(-1); - const current = next; - chain.push(current); - - const [c1, c2, c3] = vertices.c[current].map(ofSameType); - const [v1, v2, v3] = vertices.v[current]; - - if (v1 !== previous && c1 !== c2) next = v1; - else if (v2 !== previous && c2 !== c3) next = v2; - else if (v3 !== previous && c1 !== c3) next = v3; - - if (next === current) { - ERROR && console.error("Next vertex is not found"); - break; - } - } - - return chain; -} diff --git a/src/scripts/connectVertices.ts b/src/scripts/connectVertices.ts new file mode 100644 index 00000000..f9d65be1 --- /dev/null +++ b/src/scripts/connectVertices.ts @@ -0,0 +1,150 @@ +import * as d3 from "d3"; + +import {ERROR} from "config/logging"; +import {clipPoly} from "utils/lineUtils"; + +export function getFeatureVertices({ + firstCell, + vertices, + cells, + featureIds, + featureId +}: { + firstCell: number; + vertices: IGraphVertices; + cells: Pick; + featureIds: Uint16Array; + featureId: number; +}) { + const packCellsNumber = cells.c.length; + + const startingCell = findStartingCell({firstCell, featureIds, featureId, vertices, cells, packCellsNumber}); + const startingVertex = findStartingVertex({startingCell, featureIds, featureId, vertices, cells, packCellsNumber}); + const featureVertices = connectVertices({vertices, startingVertex, featureIds, featureId}); + + // temp: draw feature vertices + cells.v[firstCell] + .map(v => vertices.p[v]) + .forEach(([x, y]) => { + d3.select("#debug").append("circle").attr("cx", x).attr("cy", y).attr("r", 0.2).attr("fill", "yellow"); + }); + + const [cx, cy] = vertices.p[startingVertex]; + d3.select("#debug").append("circle").attr("cx", cx).attr("cy", cy).attr("r", 1.5).attr("fill", "red"); + + const lineGen = d3.line(); + const points = clipPoly(featureVertices.map(v => vertices.p[v])); + const path = lineGen(points)!; + d3.select("#debug") + .attr("fill", "none") + .attr("stroke", "black") + .attr("stroke-width", 0.1) + .append("path") + .attr("d", path); + + return featureVertices; +} + +function findStartingCell({ + firstCell, + featureIds, + featureId, + vertices, + cells, + packCellsNumber +}: { + firstCell: number; + featureIds: Uint16Array; + featureId: number; + vertices: IGraphVertices; + cells: Pick; + packCellsNumber: number; +}) { + const bordersOtherFeature = cells.c[firstCell].some(neighbor => featureIds[neighbor] !== featureId); + if (bordersOtherFeature) return firstCell; + + const neibCells = cells.c[firstCell].sort((a, b) => a - b); + for (const neibCell of neibCells) { + const cellVertices = cells.v[neibCell]; + const edgingVertex = cellVertices.findIndex(vertex => vertices.c[vertex].some(cellId => cellId >= packCellsNumber)); + if (edgingVertex !== -1) { + const engingCell = cells.c[neibCell]; + return engingCell[edgingVertex]; + } + } + + throw new Error(`Markup: firstCell ${firstCell} of feature ${featureId} has no neighbors of other features`); +} + +function findStartingVertex({ + startingCell, + featureIds, + featureId, + vertices, + cells, + packCellsNumber +}: { + startingCell: number; + featureIds: Uint16Array; + featureId: number; + vertices: IGraphVertices; + cells: Pick; + packCellsNumber: number; +}) { + const neibCells = cells.c[startingCell]; + const cellVertices = cells.v[startingCell]; + + const externalVertex = cellVertices.find(vertex => { + const [x, y] = vertices.p[vertex]; + if (x < 0 || y < 0) return true; + return vertices.c[vertex].some((cellId: number) => cellId >= packCellsNumber); + }); + if (externalVertex !== undefined) return externalVertex; + + const otherFeatureNeibs = neibCells.filter(neibCell => featureIds[neibCell] !== featureId); + if (!otherFeatureNeibs.length) { + throw new Error(`Markup: firstCell ${startingCell} of feature ${featureId} has no neighbors of other features`); + } + + const index = neibCells.indexOf(d3.min(otherFeatureNeibs)!); + return cellVertices[index]; +} + +const CONNECT_VERTICES_MAX_ITERATIONS = 50000; + +// connect vertices around feature +function connectVertices({ + vertices, + startingVertex, + featureIds, + featureId +}: { + vertices: IGraphVertices; + startingVertex: number; + featureIds: Uint16Array; + featureId: number; +}) { + const ofSameType = (cellId: number) => featureIds[cellId] === featureId; + const chain: number[] = []; // vertices chain to form a path + + let next = startingVertex; + for (let i = 0; i === 0 || (next !== startingVertex && i < CONNECT_VERTICES_MAX_ITERATIONS); i++) { + const previous = chain.at(-1); + const current = next; + chain.push(current); + + const [c1, c2, c3] = vertices.c[current].map(ofSameType); + const [v1, v2, v3] = vertices.v[current]; + + if (v1 !== previous && c1 !== c2) next = v1; + else if (v2 !== previous && c2 !== c3) next = v2; + else if (v3 !== previous && c1 !== c3) next = v3; + + if (next === current) { + ERROR && console.error("Next vertex is not found"); + break; + } + } + + return chain; +} diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 21bcd3d1..fbfa33fc 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -5,7 +5,7 @@ import {closeDialogs} from "dialogs/utils"; import {openDialog} from "dialogs"; import {initLayers, restoreLayers} from "layers"; // @ts-expect-error js module -import {drawCoastline} from "modules/coastline"; +import {drawCoastline} from "layers/renderers/drawCoastline"; // @ts-expect-error js module import {drawScaleBar, Rulers} from "modules/measurers"; // @ts-expect-error js module @@ -29,6 +29,7 @@ import {showStatistics} from "../statistics"; import {createGrid} from "./grid"; import {createPack} from "./pack"; import {getInputValue, setInputValue} from "utils/nodeUtils"; +// import {Ruler} from "modules/measurers"; const {Zoom, ThreeD} = window; @@ -56,6 +57,8 @@ async function generate(options?: IGenerationOptions) { const newGrid = await createGrid(grid, precreatedGraph); const newPack = createPack(newGrid); + // TODO: draw default ruler + // redefine global grid and pack grid = newGrid; pack = newPack; diff --git a/src/scripts/generation/pack.ts b/src/scripts/generation/pack.ts index 8cb5c987..f47f4414 100644 --- a/src/scripts/generation/pack.ts +++ b/src/scripts/generation/pack.ts @@ -2,7 +2,7 @@ import * as d3 from "d3"; import {renderLayer} from "layers"; // @ts-expect-error js module -import {drawCoastline} from "modules/coastline"; +import {drawCoastline} from "layers/renderers/drawCoastline"; import {markupPackFeatures} from "modules/markup"; // @ts-expect-error js module import {drawScaleBar} from "modules/measurers"; @@ -24,6 +24,8 @@ export function createPack(grid: IGrid): IPack { const markup = markupPackFeatures(grid, vertices, pick(cells, "v", "c", "b", "p", "h")); + renderLayer("coastline", vertices, markup.features); + // drawCoastline({vertices, cells}); // split into vertices definition and rendering // Rivers.generate(newPack, grid); @@ -131,3 +133,6 @@ function repackGrid(grid: IGrid) { TIME && console.timeEnd("repackGrid"); return pack; } +function drawLayer(arg0: string, vertices: IGraphVertices, features: TPackFeatures) { + throw new Error("Function not implemented."); +} diff --git a/src/scripts/simplify.ts b/src/scripts/simplify.ts new file mode 100644 index 00000000..1a813aa8 --- /dev/null +++ b/src/scripts/simplify.ts @@ -0,0 +1,100 @@ +/* + (c) 2017, Vladimir Agafonkin + Simplify.js, a high-performance JS polyline simplification library + mourner.github.io/simplify-js +*/ + +// square distance between 2 points +function getSqDist([x1, y1]: TPoint, [x2, y2]: TPoint) { + const dx = x1 - x2; + const dy = y1 - y2; + + return dx * dx + dy * dy; +} + +// square distance from a point to a segment +function getSqSegDist([x1, y1]: TPoint, [x, y]: TPoint, [x2, y2]: TPoint) { + let dx = x2 - x; + let dy = y2 - y; + + if (dx !== 0 || dy !== 0) { + const t = ((x1 - x) * dx + (y1 - y) * dy) / (dx * dx + dy * dy); + + if (t > 1) { + x = x2; + y = y2; + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = x1 - x; + dy = y1 - y; + + return dx * dx + dy * dy; +} +// rest of the code doesn't care about point format + +// basic distance-based simplification +function simplifyRadialDist(points: TPoints, sqTolerance: number) { + let prevPoint = points[0]; + const newPoints = [prevPoint]; + let point; + + for (let i = 1, len = points.length; i < len; i++) { + point = points[i]; + + if (getSqDist(point, prevPoint) > sqTolerance) { + newPoints.push(point); + prevPoint = point; + } + } + + if (point && prevPoint !== point) newPoints.push(point); + + return newPoints; +} + +function simplifyDPStep(points: TPoints, first: number, last: number, sqTolerance: number, simplified: TPoints) { + let maxSqDist = sqTolerance; + let index = first; + + for (let i = first + 1; i < last; i++) { + const sqDist = getSqSegDist(points[i], points[first], points[last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified); + simplified.push(points[index]); + if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified); + } +} + +// simplification using Ramer-Douglas-Peucker algorithm +function simplifyDouglasPeucker(points: TPoints, sqTolerance: number) { + const last = points.length - 1; + + const simplified = [points[0]]; + simplifyDPStep(points, 0, last, sqTolerance, simplified); + simplified.push(points[last]); + + return simplified; +} + +// both algorithms combined for awesome performance +export function simplify(points: TPoints, tolerance: number, highestQuality = false) { + if (points.length <= 2) return points; + + const sqTolerance = tolerance * tolerance; + + points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); + points = simplifyDouglasPeucker(points, sqTolerance); + + return points; +}