Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
fast_graph_path = {
const allcoords = _Imports.Geospatial.turf.coordAll(densified_lines);
const mappings = new Map();

const coords = new Map();

function serialise(a, b) {
return `1:${a},2:${b}`;
}

for (const coord of allcoords) {
const cs = serialise(...coord);
if (coords.has(cs)) continue;
coords.set(cs, coord);
}

const idx = new _Imports.Geospatial.flatbush.default(coords.size);

for (const coord of [...coords.values()]) {
mappings.set(idx.add(coord[0], coord[1]), coord);
}

idx.finish();

const graph = _Imports.Graph.ngraph.graph();

const seen_links = new Set();

function serialise_link(a, b) {
return [`1:${a},2:${b}`, `2:${b},1:${a}`];
}

_Imports.Geospatial.turf.segmentEach(
densified_lines,
function (currentSegment, featureIndex) {
const left = idx.neighbors(
...currentSegment.geometry.coordinates[0],
1
)[0];
const right = idx.neighbors(
...currentSegment.geometry.coordinates[1],
1
)[0];

const ls = serialise_link(left, right);

if (seen_links.has(ls[0]) || seen_links.has(ls[1])) {
console.log("seen link", left, right);
return;
} else {
graph.addLink(left, right);

seen_links.add(ls[0]);
seen_links.add(ls[1]);
}
}
);

const pathfinder = _Imports.Graph.ngraph.path.aStar(graph);

function findPath(c1, c2) {
const left = idx.neighbors(...c1, 1)[0];
const right = idx.neighbors(...c2, 1)[0];
let maybeSkip = false;

const path = pathfinder.find(left, right).map((n) => {
if (n.links.size > 2) {
maybeSkip = true;
}
return mappings.get(n.id);
});
if (path.length == 0) {
console.log(left, right, path);
}
return {path, maybeSkip};
}

function findPoint(coord) {
return idx.neighbors(...coord, 1).map((n) => mappings.get(n))[0];
}

function findPointIndex(coord) {
return idx.neighbors(...coord, 1)[0];
}
function findCoordFromIndex(index) {
return mappings.get(index);
}

return {
findPath,
findPoint,
findPointIndex,
findCoordFromIndex,
graph: JSON.parse(_Imports.Graph.ngraph.tojson.default(graph))
};
}
Insert cell
Step4_02_LinesWithLinksAttributes = {
const all =
process_all ==
`Process all lines (${InputFiles.input_routes.features.length})`;

const mp_skeleton = _Imports.Geospatial.turf.combine(
InputFiles.input_skeleton
);

const normalised_lines = [];

let idx = 1;

for (const route of InputFiles.input_routes.features) {
if (route.properties.id !== select_single_line && !all) continue;
console.log(
"Processing route:",
route.properties.id,
`(${idx++} of ${InputFiles.input_routes.features.length})`
);

const route_coords = [];
const AlongCoords = [route.geometry.coordinates[0]];
let distance = move_distance,
reachedEnd = false;
let shouldSkipRound = false,
canSkipRound = 0;
while (reachedEnd == false) {
const prevPoint = AlongCoords[AlongCoords.length - 1];

let thisDistance = distance;
if (shouldSkipRound) {
distance = distance + 0.025;
}

const nextPoint = _Imports.Geospatial.turf.along(route, distance, {
units: "kilometers"
}).geometry.coordinates;

const sk_points = {
first: _Imports.Geospatial.turf.point(
fast_graph_path.findPoint(prevPoint)
),
last: _Imports.Geospatial.turf.point(
fast_graph_path.findPoint(nextPoint)
)
};

const { path, maybeSkip } = fast_graph_path.findPath(
prevPoint,
nextPoint
);

if (maybeSkip && canSkipRound < 5) {
canSkipRound++;
shouldSkipRound = true;
continue;
} else {
canSkipRound = 0;
}

if (prevPoint[0] == nextPoint[0] && prevPoint[1] == nextPoint[1]) {
reachedEnd = true;
} else {
AlongCoords.push(nextPoint);
distance += move_distance;
}

if (AlongCoords.length < max_processing && !all) {
reachedEnd = true;
}

let pathNormalised = [];
if (
path[0][0] == sk_points.first[0] &&
path[0][1] == sk_points.first[1]
) {
pathNormalised.push(...path);
} else {
pathNormalised.push(...path.toReversed());
}
route_coords.push(...pathNormalised);
}

normalised_lines.push(
_Imports.Geospatial.turf.cleanCoords(
_Imports.Geospatial.turf.truncate(
_Imports.Geospatial.turf.lineString(route_coords, route.properties),
{ precision: 5, coordinates: 2 }
)
)
);
}

return _Imports.Geospatial.turf.featureCollection(normalised_lines);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function CompareCoords(c1, c2) {
return c1[0] == c2[0] && c1[1] == c2[1];
}
Insert cell
LineSegments = {
function GetLineSegments(TopoJsonLines) {
console.time();
const Segments = new Map();

// add arcs as nodes
for (let i = 0; i < TopoJsonLines.arcs.length; i++) {
Segments.set(`Segment:${i}`, {
geometry: TopoJsonLines.arcs[i],
top: TopoJsonLines.arcs[i][0],
bottom: TopoJsonLines.arcs[i][TopoJsonLines.arcs[i].length - 1],
colors: [
...new Set(
TopoJsonLines.objects.lines.geometries.map((line) => {
if (
i < 0 ? line.arcs.includes(i * -1 - 1) : line.arcs.includes(i)
)
return line.properties.stroke || "#000000";
})
)
].filter((e) => e != null)
});
}
console.timeEnd();
return Segments;
}

return GetLineSegments(ProcessedTJLines);
}
Insert cell
LineConnections = {
function GetLineConnections(TopoJsonLines) {
const Connections = [];
const ConnectionsDedupe = [];

for (const line of TopoJsonLines.objects.lines.geometries) {
line.arcs.reduce((prev, cur) => {
const fromId = prev < 0 ? prev * -1 - 1 : prev;
const fromPart = prev < 0 ? "top" : "bottom";
const toId = cur < 0 ? cur * -1 - 1 : cur;
const toPart = cur < 0 ? "bottom" : "top";
const DedupeString = [
line.properties.stroke,
fromId,
fromPart,
toId,
toPart
].join(":");
if (!ConnectionsDedupe.includes(DedupeString)) {
Connections.push({
from: [fromId, fromPart],
to: [toId, toPart],
color: line.properties.stroke
});
ConnectionsDedupe.push(DedupeString);
}
return cur;
});
}

return Connections;
}

return GetLineConnections(ProcessedTJLines);
}
Insert cell
ClipLine = {
const LineSliceAlong = _Imports.Geospatial.turf.lineSliceAlong;
const Length = _Imports.Geospatial.turf.length;

return function ClipLine(line, clip_dist_in) {
const clipdist = parseFloat(clip_dist_in.toFixed(4));
const LineLength = Length(line);
const MinLineLength = LineLength / 3;

// Throw line when we try clip a line that's smaller than a third of the distance asked OR has fewer than 4 coordinates.
if (MinLineLength <= clipdist || line.geometry.coordinates.length < 4) {
return line;
}

const StartDistance = parseFloat(
Math.min(MinLineLength, clipdist).toFixed(4)
);
const EndDistance = parseFloat(
Math.max(MinLineLength * 2, LineLength - clipdist).toFixed(4)
);

const SlicedLine = LineSliceAlong(line, StartDistance, EndDistance);
return _Imports.Geospatial.turf.cleanCoords(SlicedLine);
};
}
Insert cell
RenderLines = {
const LineOffset = _Imports.Geospatial.turf.lineOffset;
const LineString = _Imports.Geospatial.turf.lineString;
const MultiLineString = _Imports.Geospatial.turf.multiLineString;
const _ClipLine = ClipLine;

const zero = _Imports.Geospatial.turf.point([0, 0]);

return function RenderLines(RequestedSpacing) {
const spacing = Math.max(0.00005, Math.min(0.3, RequestedSpacing));

const RenderedColourLine = new Map();
const ColourSegmentsEndpoints = new Map();

for (const [segment_id, segment] of LineSegments.entries()) {
const shouldReverseColor =
_Imports.Geospatial.turf.distance(zero, segment.geometry[0]) >
_Imports.Geospatial.turf.distance(
zero,
segment.geometry[segment.geometry.length - 1]
);

const sortedcolors = segment.colors.sort();

const colors = shouldReverseColor
? sortedcolors.toReversed()
: sortedcolors;

const linespace = spacing * 2;
const totallines = segment.colors.length;

const line = LineString(segment.geometry);
const line_clipped = _ClipLine(line, Math.max(0.005, spacing * 5));

for (let i = 0; i < totallines; i++) {
const color = colors[i];
const lineoffset = i * linespace - ((totallines - 1) * linespace) / 2;

const line_clipped_offset = LineOffset(
line_clipped,
parseFloat(lineoffset.toFixed(5))
);

const line_processed = line_clipped_offset.geometry.coordinates;

ColourSegmentsEndpoints.set(
`${segment_id}:${color}:top`,
line_processed[0]
);
ColourSegmentsEndpoints.set(
`${segment_id}:${color}:bottom`,
line_processed[line_processed.length - 1]
);

if (RenderedColourLine.has(color)) {
RenderedColourLine.get(color).push(line_processed);
} else {
RenderedColourLine.set(color, [line_processed]);
}
}
}

for (const connection of LineConnections) {
const coords = [
ColourSegmentsEndpoints.get(
`Segment:${connection.from[0]}:${connection.color}:${connection.from[1]}`
),
ColourSegmentsEndpoints.get(
`Segment:${connection.to[0]}:${connection.color}:${connection.to[1]}`
)
];

const finalCoords = coords.filter((c) => {
return c !== undefined && c.length > 1;
});

RenderedColourLine.get(connection.color).push(finalCoords);
}

const rendered = [];

for (const [color, coords] of RenderedColourLine.entries()) {
rendered.push(
MultiLineString(coords, {
id: color,
stroke: color
})
);
}

return _Imports.Geospatial.turf.featureCollection(rendered);
};
}
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