Published
Edited
Feb 7, 2022
Insert cell
Insert cell
Insert cell
Insert cell
currentSegmentJoiner = new SegmentJoiner(selectedLineCoords)
Insert cell
firstSegment = currentSegmentJoiner.startSegments()
Insert cell
// joinSegments(currentWeld, currentSegmentIndex) {
// let nextSegment = this.findNextSegment(currentWeld, currentSegmentIndex);

// if (nextSegment === false)
// return `${currentSegmentIndex} is is the final segmentIndex of this LineString`;

// this.orderedSegmentIndices.push(nextSegment);
// let nextWeld = this.findNextWeld(nextSegment);

// // yield*
// this.joinSegments(nextWeld, nextSegment);
// }

// joinAllSegments() {
// this.orderedSegmentIndices = [];
// const firstSegment = this.startSegment();
// const firstWeld = this.findNextWeld(firstSegment);

// this.orderedSegmentIndices.push(firstSegment);

// this.joinSegments(firstWeld, firstSegment);

// return this.mergeMultiLineString();
// }
Insert cell
// firstWeld = currentSegmentJoiner.findNextWeld(firstSegment)
Insert cell
// joinedLineString = ({
// type: "LineString",
// properties: selectedLineProperties,
// coordinates: currentSegmentJoiner.joinAllSegments()
// })

Insert cell
currentSegmentJoiner.getSegmentMap
Insert cell
currentSegmentJoiner.getWeldMap
Insert cell
currentSegmentJoiner
Insert cell
// can't really handle split paths and quadruple welds
// TODO: static function to get array of segments from any geoJSON object?
class SegmentJoiner {
constructor(segments) {
// todo allow passing of multilinestring geoJSON object and read segmenst ie linestring coords from that
if (!segments)
throw Error("Missing required argument to parameter 'segments'");
if (!(segments instanceof Array))
throw "Argument to parameter 'segments' must be an array";
this.segments = segments; // mutated by joinAllSegments()
this.segmentsCopy = [...segments];
this.orderedSegmentIndices = [];
this.segmentMap = this.groupBySegment(segments); // mutated by joinAllSegments()
this.weldMap = this.groupByWelds(segments); // mutated by joinAllSegments()
// this.copyWeldMap = new Map(this.weldMap);
}

get getWeldMap() {
return this.weldMap;
}

get getSegmentMap() {
return this.segmentMap;
}

// to find start/end coords of a segment
groupBySegment(segments) {
const startEndPerSegment = new d3.InternMap([], JSON.stringify);

// save start and end coord of each segments to InternMap
segments.map((coords, i) => {
const start = coords[0];

const { length } = coords;
const end = coords[length - 1];

startEndPerSegment.set(i, { start, end });
});

return startEndPerSegment;
}

groupByWelds(segments) {
const coordsMap = new d3.InternMap([], JSON.stringify);

// Direction of segments is not always the same
segments.map((coords, i) => {
const { length } = coords;
const startNode = coords[0];
const endNode = coords[length - 1];

if (coordsMap.has(startNode)) {
if (coordsMap.get(startNode).has("startOf")) {
coordsMap.get(startNode).set("alsoStartOf", i);
} else {
coordsMap.get(startNode).set("startOf", i);
}
} else {
coordsMap.set(startNode, new Map([["startOf", i]]));
}

if (coordsMap.has(endNode)) {
if (coordsMap.get(endNode).has("endOf")) {
coordsMap.get(endNode).set("alsoEndOf", i);
} else {
coordsMap.get(endNode).set("endOf", i);
}
} else {
coordsMap.set(endNode, new Map([["endOf", i]]));
}
});

return coordsMap;
}

// 1. find welds with only start (or only end)
startSegments() {
const starts = [];

for (const [coordString, startEndOfSegmentMap] of this.weldMap) {
if (startEndOfSegmentMap.size === 1) {
if (startEndOfSegmentMap.has("startOf")) {
starts.push({
index: startEndOfSegmentMap.get("startOf"),
reverse: false
});
console.log("start segment:", startEndOfSegmentMap.get("startOf"));
// return { index: startEndOfSegmentMap.get("startOf"), reverse: false };
}
}
}

// only if there's none with only startOf in the weldMap
for (const [coordString, startEndOfSegmentMap] of this.weldMap) {
if (startEndOfSegmentMap.size === 1) {
if (startEndOfSegmentMap.has("endOf")) {
console.log(
"Reversed, MultiLine has no startOf at ends",
startEndOfSegmentMap.get("endOf")
);
starts.push({
index: startEndOfSegmentMap.get("endOf"),
reverse: true
});
// return { index: startEndOfSegmentMap.get("endOf"), reverse: true };
}
}
}

return starts;
}

findNextSegment(weld, prevSegment) {
if (!weld || !prevSegment) {
return "Not enough args to findNextSegment :(";
}

console.log(
"finding next segment of weld",
{ weld },
"with prev segment",
prevSegment
);
const segmentsOfWeld = this.weldMap.get(weld);

// TODO: could pass in the previous end type and delete that from the map with needing to iterate over it
for (const [termination, segmentIndex] of segmentsOfWeld) {
console.log(termination, prevSegment.index);
if (segmentIndex === prevSegment.index) {
console.log("Remove previous segment index", segmentIndex);
segmentsOfWeld.delete(termination);
}
}

if (segmentsOfWeld.has("startOf")) {
console.log("Normal direction");
const index = segmentsOfWeld.get("startOf");
segmentsOfWeld.delete("startOf");
return { index, reverse: false };
}

if (segmentsOfWeld.has("endOf")) {
console.log("Reverse direction");
const index = segmentsOfWeld.get("endOf");
segmentsOfWeld.delete("endOf");
return { index, reverse: true };
}

if (segmentsOfWeld.has("alsoStartOf")) {
console.log("Again normal direction");
const index = segmentsOfWeld.get("alsoStartOf");
segmentsOfWeld.delete("alsoStartOf");
return { index, reverse: false };
}

if (segmentsOfWeld.has("alsoEndOf")) {
console.log("Again reverse direction");
const index = segmentsOfWeld.get("alsoEndOf");
segmentsOfWeld.delete("alsoEndOf");
return { index, reverse: true };
}

// this must be the end (or some condition I did not think of ¯\_(ツ)_/¯)
return false;
}

findNextWeld(segment) {
console.log("finding next weld of segment", segment);
const nextWeld = this.segmentMap.get(segment.index);

// special case for the first node?

if (!segment.reverse) return nextWeld.end;

return nextWeld.start; // TODO: or alsoStartOf
}

// TODO: turn this into a recursive generator
// recursive function
joinSegments(currentWeld, currentSegmentIndex) {
let nextSegment = this.findNextSegment(currentWeld, currentSegmentIndex);

if (nextSegment === false)
return `${currentSegmentIndex} is is the final segmentIndex of this LineString`;

this.orderedSegmentIndices.push(nextSegment);
let nextWeld = this.findNextWeld(nextSegment);

// yield*
this.joinSegments(nextWeld, nextSegment);
}

joinAllSegments() {
this.orderedSegmentIndices = [];
const firstSegment = this.startSegment();
const firstWeld = this.findNextWeld(firstSegment);

this.orderedSegmentIndices.push(firstSegment);

this.joinSegments(firstWeld, firstSegment);

return this.mergeMultiLineString();
}

mergeMultiLineString() {
if (!this.orderedSegmentIndices.length) return "No segment indices found";

const orderedLineString = this.orderedSegmentIndices.map((seg) => {
if (seg.reverse) return this.segments[seg.index].reverse(); // mutates this.segments[seg]
return this.segments[seg.index];
});

return orderedLineString.flat(); // remove double nodes too?
}
}
Insert cell
Insert cell
projection = d3
.geoIdentity(true)
.reflectY(true)
.fitExtent(
[
[5, 5],
[width - 5, height - 5]
],
rotterdam
)
Insert cell
Insert cell
Insert cell
selectedLine = groupedByLine.get(selected)
Insert cell
Insert cell
selectedLineCoords = selectedLine.map((d) => d.geometry.coordinates)
Insert cell
selectedLineProperties = selectedLine[0].properties
Insert cell
Insert cell
groupedByLine = d3.group(rotterdam.features, (d) => d.properties.ref)
Insert cell
rotterdam = FileAttachment("rotterdam.geojson").json()
Insert cell
// https://www.npmjs.com/package/@turf/circle
Insert cell
circle = require("@turf/circle")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more