terra-route
Version:
A library for routing along GeoJSON LineString networks
298 lines (260 loc) • 9.15 kB
text/typescript
import { FeatureCollection, LineString, Feature, Position } from "geojson";
/**
* Generates a grid of LineStrings with n x n nodes, spaced by the given spacing.
* Each node is connected to its right and upward neighbors, and diagonals are included.
*
* @param n - Number of nodes in each dimension (n x n grid)
* @param spacing - Distance between consecutive nodes
* @returns FeatureCollection of LineStrings representing the grid
*/
export function generateGridWithDiagonals(n: number, spacing: number): FeatureCollection<LineString> {
const features: Feature<LineString>[] = [];
const coord = (x: number, y: number): Position => [x * spacing, y * spacing];
for (let y = 0; y < n; y++) {
for (let x = 0; x < n; x++) {
// Horizontal edge (to the right)
if (x < n - 1) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
coord(x, y),
coord(x + 1, y)
]
},
properties: {}
});
}
// Vertical edge (upward)
if (y < n - 1) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
coord(x, y),
coord(x, y + 1)
]
},
properties: {}
});
}
// Diagonal bottom-left to top-right
if (x < n - 1 && y < n - 1) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
coord(x, y),
coord(x + 1, y + 1)
]
},
properties: {}
});
}
// Diagonal bottom-right to top-left
if (x > 0 && y < n - 1) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
coord(x, y),
coord(x - 1, y + 1)
]
},
properties: {}
});
}
}
}
return {
type: "FeatureCollection",
features
};
}
/**
* Generate a star-like polygon with n vertices.
* If connectAll is true, connects every vertex to every other (complete graph).
* If false, connects only the outer ring to form a polygon perimeter.
*
* @param n - Number of vertices (>= 3)
* @param radius - Radius in degrees for placing vertices in a circle
* @param center - Center of the polygon [lng, lat]
* @param connectAll - If true, connects every pair of vertices. If false, only connects the outer ring.
* @returns FeatureCollection of LineStrings
*/
export function generateStarPolygon(
n: number,
radius = 0.01,
center: Position = [0, 0],
connectAll = true
): FeatureCollection<LineString> {
if (n < 3) {
throw new Error("Star polygon requires at least 3 vertices.");
}
const angleStep = (2 * Math.PI) / n;
const vertices: Position[] = [];
// Generate points in a circle
for (let i = 0; i < n; i++) {
const angle = i * angleStep;
const x = center[0] + radius * Math.cos(angle);
const y = center[1] + radius * Math.sin(angle);
vertices.push([x, y]);
}
const features: Feature<LineString>[] = [];
if (connectAll) {
// Connect every vertex to every other vertex
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [vertices[i], vertices[j]],
},
properties: {},
});
}
}
} else {
// Connect outer ring only
for (let i = 0; i < n; i++) {
const next = (i + 1) % n;
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [vertices[i], vertices[next]],
},
properties: {},
});
}
}
return {
type: "FeatureCollection",
features,
};
}
/**
* Generate a spatial n-depth tree as a FeatureCollection<LineString>.
*
* @param depth - Number of depth levels (>= 1)
* @param branchingFactor - Number of children per node
* @param root - Root position [lng, lat]
* @param length - Distance between each parent and child
* @returns FeatureCollection of LineStrings representing the tree
*/
export function generateTreeFeatureCollection(
depth: number,
branchingFactor: number,
root: Position = [0, 0],
length = 0.01
): FeatureCollection<LineString> {
if (depth < 1) {
throw new Error("Tree must have at least depth 1.");
}
const features: Feature<LineString>[] = [];
interface TreeNode {
position: Position;
level: number;
}
const nodes: TreeNode[] = [{ position: root, level: 0 }];
const RAD = Math.PI / 180;
for (let level = 0; level < depth; level++) {
const newNodes: TreeNode[] = [];
for (const node of nodes.filter(n => n.level === level)) {
const angleStart = -90 - ((branchingFactor - 1) * 20) / 2;
for (let i = 0; i < branchingFactor; i++) {
const angle = angleStart + i * 20; // spread branches 20 degrees apart
const radians = angle * RAD;
const dx = length * Math.cos(radians);
const dy = length * Math.sin(radians);
const child: Position = [node.position[0] + dx, node.position[1] + dy];
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [node.position, child],
},
properties: {},
});
newNodes.push({ position: child, level: level + 1 });
}
}
nodes.push(...newNodes);
}
return {
type: "FeatureCollection",
features,
};
}
/**
* Generates a connected graph of concentric rings, each ring fully connected
* around itself and connected radially to the next ring.
*
* @param numRings - Number of concentric rings
* @param pointsPerRing - How many points (nodes) on each ring
* @param spacing - Distance between consecutive rings
* @param center - [lng, lat] center of the rings
* @returns A FeatureCollection of LineStrings for the rings + radial connections
*/
export function generateConcentricRings(
numRings: number,
pointsPerRing: number,
spacing: number,
center: Position = [0, 0]
): FeatureCollection<LineString> {
// Holds all the ring coordinates: ringPoints[i][j] => coordinate
const ringPoints: Position[][] = [];
// Create ring points
for (let i = 0; i < numRings; i++) {
const ringRadius = (i + 1) * spacing;
const ring: Position[] = [];
for (let j = 0; j < pointsPerRing; j++) {
const angle = (2 * Math.PI * j) / pointsPerRing;
const x = center[0] + ringRadius * Math.cos(angle);
const y = center[1] + ringRadius * Math.sin(angle);
ring.push([x, y]);
}
ringPoints.push(ring);
}
// Build the graph as a collection of LineStrings
const features: Feature<LineString>[] = [];
// 1. Add each ring as a closed loop
for (let i = 0; i < numRings; i++) {
const coords = ringPoints[i];
// Close the ring by appending the first point again
const ringWithClosure = [...coords, coords[0]];
features.push({
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: ringWithClosure,
},
});
}
// 2. Connect rings radially
// (i.e., ring i node j to ring i+1 node j)
for (let i = 0; i < numRings - 1; i++) {
for (let j = 0; j < pointsPerRing; j++) {
const start = ringPoints[i][j];
const end = ringPoints[i + 1][j];
features.push({
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: [start, end],
},
});
}
}
return {
type: "FeatureCollection",
features,
};
}