Notebooks 2.0 is here.

Published
Edited
1 star
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("font", "10px sans-serif");

const g = svg.append("g").attr("cursor", "grab");

const nrOfSegments = selectedMultiLineString.geometry.coordinates.length;

const color = d3
.scaleSequential(d3.interpolateReds)
.domain([0, nrOfSegments]);

const segments = g
.append("g")
.attr("class", "segments")
.selectAll("path")
.data(selectedMultiLineString.geometry.coordinates)
.join("path")
.attr("d", (d) => {
// d3.geoPath() only takes geoJSON objects
const lineString = {
type: "LineString",
coordinates: d
};

return path(lineString);
})
.attr("fill", "none")
.attr("stroke-linejoin", "round")
.attr("stroke", (d, i) => color(i))
.attr("stroke-width", (d, i) => 2.5 + i * 0.75)
.append("title")
.text((d, i) => i);

// g.append("path")
// .datum(joinedLineString)
// .attr("d", path)
// .attr("stroke", "gold")
// .attr("stroke-width", 2)
// .attr("stroke-linejoin", "round")
// .attr("fill", "none");

const weld = g
.append("g")
.attr("class", "welds")
.selectAll("g")
.data(plotWeldMap)
.join("g")
.attr("transform", (d) => `translate(${d[0]},${d[1]})`);

weld
.append("circle")
.attr("r", 0.5)
.attr("stroke", "white")
.attr("stroke-width", 0.5);

weld
.append("text")
.text((d) => d.ends)
.attr("dy", -4)
.attr("dx", 4)
.attr("opacity", 0.8)
.attr("transform", "rotate(45)")
.attr("mix-blend-mode", "exclude")
.attr("paint-order", "stroke")
.attr("stroke", "white")
.attr("stroke-width", "1.5px");

svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.scaleExtent([1, 8])
.on("zoom", zoomed)
);

function zoomed(event) {
g.attr("transform", event.transform);
}

return svg.node();
}
Insert cell
currentSegmentJoiner = new SegmentJoiner(selectedMultiLineString.geometry.coordinates);
Insert cell
// joinedLineString = ({ type: "LineString", "coordinates": currentSegmentJoiner.joinAllSegments() })
Insert cell
// joinedLineString = ({ type: "LineString", "coordinates": currentSegmentJoiner.joinAllSegments() })
Insert cell
plotWeldMap = {
return [...currentSegmentJoiner.weldMap].map(d => {
const coord = d[0].split(",")
const projectedCoord = projection(coord);
const endsText = Array.from(d[1])
.map(d => d.join(": "))
.join(", ")
projectedCoord.ends = endsText;
return projectedCoord
})
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
firstSegment = currentSegmentJoiner.startSegment()
Insert cell
firstWeld = currentSegmentJoiner.findNextWeld(firstSegment);
Insert cell
currentSegmentJoiner.joinSegments(firstWeld, firstSegment.index);
Insert cell
// if (typeof segments === "object" && !(segments instanceof Array) ) {
// console.log("segments is an object, not an array")
// if (segments.type === "FeatureCollection") {
// console.log("geoJSON FeatureCollection obj");
// } else if (segments.type === "Feature") {
// if (segments.geometry.type === "MultiLineString") {
// this.segments = segments.geometry.coordinates;
// }
// }
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.copyWeldMap
}
get getSegmentMap() {
return this.segmentMap
}
// to find start/end coords of a segment
groupBySegment(segments) {
const startEndPerSegment = new Map();
// save start and end coord of each segments to Map
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 Map();

// Direction of segments is not always the same

segments.map((coords, i) => {
const { length } = coords;
const startNode = coords[0];
const endNode = coords[length - 1];

// turn coords into a string so they're unique in the Map
const startNodeString = startNode.toString();
const endNodeString = endNode.toString();

if (coordsMap.has(startNodeString)) {

if (coordsMap.get(startNodeString).has("startOf")) {
coordsMap.get(startNodeString).set("alsoStartOf", i)
} else {
coordsMap.get(startNodeString).set("startOf", i)
}

} else {
coordsMap.set(startNodeString, new Map([["startOf", i]]))
}
if (coordsMap.has(endNodeString)) {
if (coordsMap.get(endNodeString).has("endOf")) {
coordsMap.get(endNodeString).set("alsoEndOf", i)
} else {
coordsMap.get(endNodeString).set("endOf", i)
}
} else {
coordsMap.set(endNodeString, new Map([["endOf", i]]))
}
})
return coordsMap
}
// 1. find weld with only start. Read start segment (or only end)
startSegment() {

for (const [coordString, startEndOfSegmentMap] of this.weldMap) {
if (startEndOfSegmentMap.size === 1) {

if (startEndOfSegmentMap.has("startOf")) {
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"))
return { index: startEndOfSegmentMap.get("endOf"), reverse: true }
}
}
}

}
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.toString()
return nextWeld.start.toString() // 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
selectedMultiLineString = multiLineStringsData.features[num];
Insert cell
multiLineStringsData = FileAttachment("amsterdam-transit-geo-multilinestrings.json").json()
Insert cell
Insert cell
// center = require("@turf/center")
Insert cell
import {slider, radio} from "@jashkenas/inputs"
Insert cell
d3 = import("d3@v6")
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