Prototype
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
|
||||
import { getFeatureType, resolveFeatureStyle } from "./feature-types.js";
|
||||
|
||||
class YardFeature {
|
||||
constructor(kind, geometryType, name, details = {}) {
|
||||
this.kind = kind;
|
||||
this.geometryType = geometryType;
|
||||
this.name = name ?? getFeatureType(kind).label;
|
||||
this.id = buildFeatureId(kind, this.name);
|
||||
this.details = { ...details };
|
||||
this.coordinates = [];
|
||||
this.parentId = null;
|
||||
this.groupIds = [];
|
||||
}
|
||||
|
||||
add(...values) {
|
||||
this.coordinates.push(resolveCoordinate(values));
|
||||
return this;
|
||||
}
|
||||
|
||||
trace(coordinates) {
|
||||
for (const coordinate of pointsFromLngLat(coordinates)) {
|
||||
this.add(coordinate);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
withDetails(details) {
|
||||
Object.assign(this.details, details);
|
||||
return this;
|
||||
}
|
||||
|
||||
withId(id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
childOf(parent) {
|
||||
this.parentId = resolveFeatureReference(parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
inGroup(...groups) {
|
||||
this.groupIds.push(...groups.map(resolveGroupReference));
|
||||
this.groupIds = [...new Set(this.groupIds)];
|
||||
return this;
|
||||
}
|
||||
|
||||
toGeoJSON() {
|
||||
const label = getFeatureType(this.kind).label;
|
||||
const style = resolveFeatureStyle(this.kind, this.details);
|
||||
|
||||
if (this.geometryType === "Point") {
|
||||
const [first] = this.coordinates;
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: this.id,
|
||||
kind: this.kind,
|
||||
label,
|
||||
name: this.name,
|
||||
parentId: this.parentId,
|
||||
groupIds: this.groupIds,
|
||||
details: this.details,
|
||||
style,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [first.lon, first.lat],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const coordinates = this.coordinates.map((coordinate) => [coordinate.lon, coordinate.lat]);
|
||||
const geometry =
|
||||
this.geometryType === "LineString"
|
||||
? { type: "LineString", coordinates }
|
||||
: { type: "Polygon", coordinates: [closeRing(coordinates)] };
|
||||
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: this.id,
|
||||
kind: this.kind,
|
||||
label,
|
||||
name: this.name,
|
||||
parentId: this.parentId,
|
||||
groupIds: this.groupIds,
|
||||
details: this.details,
|
||||
style,
|
||||
},
|
||||
geometry,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildFeatureId(kind, name) {
|
||||
return `${kind}:${slugify(name)}`;
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return String(value)
|
||||
.toLowerCase()
|
||||
.replaceAll(/[^a-z0-9]+/g, "-")
|
||||
.replaceAll(/^-+|-+$/g, "")
|
||||
.replaceAll(/-{2,}/g, "-");
|
||||
}
|
||||
|
||||
function resolveFeatureReference(featureOrId) {
|
||||
if (typeof featureOrId === "string") {
|
||||
return featureOrId;
|
||||
}
|
||||
|
||||
if (featureOrId && typeof featureOrId.id === "string") {
|
||||
return featureOrId.id;
|
||||
}
|
||||
|
||||
throw new Error("Expected a feature or feature id");
|
||||
}
|
||||
|
||||
function resolveGroupReference(groupOrId) {
|
||||
if (typeof groupOrId === "string") {
|
||||
return groupOrId;
|
||||
}
|
||||
|
||||
if (groupOrId && typeof groupOrId.id === "string") {
|
||||
return groupOrId.id;
|
||||
}
|
||||
|
||||
throw new Error("Expected a group or group id");
|
||||
}
|
||||
|
||||
export function defineGroup(id, label, options = {}) {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
parentGroupId: options.parentGroupId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
class PolygonFeature extends YardFeature {
|
||||
constructor(kind, name, details) {
|
||||
super(kind, "Polygon", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
class LineFeature extends YardFeature {
|
||||
constructor(kind, name, details) {
|
||||
super(kind, "LineString", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
class PointFeature extends YardFeature {
|
||||
constructor(kind, name, details) {
|
||||
super(kind, "Point", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
function closeRing(coordinates) {
|
||||
if (coordinates.length === 0) {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
const [firstLon, firstLat] = coordinates[0];
|
||||
const [lastLon, lastLat] = coordinates[coordinates.length - 1];
|
||||
if (firstLon === lastLon && firstLat === lastLat) {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
return [...coordinates, coordinates[0]];
|
||||
}
|
||||
|
||||
function isPoint(value) {
|
||||
return Boolean(value) && typeof value.lat === "number" && typeof value.lon === "number";
|
||||
}
|
||||
|
||||
function isMove(value) {
|
||||
return (
|
||||
Boolean(value) &&
|
||||
typeof value.northMeters === "number" &&
|
||||
typeof value.eastMeters === "number"
|
||||
);
|
||||
}
|
||||
|
||||
function isOffsetAnchor(value) {
|
||||
return Boolean(value) && value.kind === "offset-anchor" && isPoint(value.origin);
|
||||
}
|
||||
|
||||
function resolveCoordinate(values) {
|
||||
if (values.length === 1 && isPoint(values[0])) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const [first, ...rest] = values;
|
||||
if (isOffsetAnchor(first) && rest.every(isMove)) {
|
||||
return offset(first.origin, ...rest);
|
||||
}
|
||||
|
||||
throw new Error("add(...) expects a point, or offset(origin) followed by directional moves");
|
||||
}
|
||||
|
||||
export class LotBoundary extends PolygonFeature {
|
||||
constructor(name = "Lot Boundary", details = {}) {
|
||||
super("lotBoundary", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Fence extends LineFeature {
|
||||
constructor(name = "Fence", details = {}) {
|
||||
super("fence", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class House extends PolygonFeature {
|
||||
constructor(name = "House", details = {}) {
|
||||
super("house", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Garage extends PolygonFeature {
|
||||
constructor(name = "Garage", details = {}) {
|
||||
super("garage", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Porch extends PolygonFeature {
|
||||
constructor(name = "Porch", details = {}) {
|
||||
super("porch", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Deck extends PolygonFeature {
|
||||
constructor(name = "Deck", details = {}) {
|
||||
super("deck", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Walkway extends PolygonFeature {
|
||||
constructor(name = "Walkway", details = {}) {
|
||||
super("walkway", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Driveway extends PolygonFeature {
|
||||
constructor(name = "Driveway", details = {}) {
|
||||
super("driveway", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class FlowerBed extends PolygonFeature {
|
||||
constructor(name = "Flower Bed", details = {}) {
|
||||
super("flowerBed", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Tree extends PointFeature {
|
||||
constructor(name = "Tree", details = {}) {
|
||||
super("tree", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Sprinkler extends PointFeature {
|
||||
constructor(name = "Sprinkler", details = {}) {
|
||||
super("sprinkler", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export function collectYardData(features, groups = []) {
|
||||
const collection = collectGeoJSON(features);
|
||||
collection.groups = groups.map((group) => ({
|
||||
id: group.id,
|
||||
label: group.label,
|
||||
parentGroupId: group.parentGroupId ?? null,
|
||||
}));
|
||||
return collection;
|
||||
}
|
||||
|
||||
export { collectGeoJSON };
|
||||
Reference in New Issue
Block a user