Use local PMTiles basemap
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
export const featureTypes = {
|
||||
lotBoundary: {
|
||||
label: "Lot Boundary",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#7b684d",
|
||||
weight: 2,
|
||||
dashArray: "8 5",
|
||||
fillColor: "#dccaab",
|
||||
fillOpacity: 0.08,
|
||||
},
|
||||
},
|
||||
fence: {
|
||||
label: "Fence",
|
||||
geometryType: "LineString",
|
||||
detailFields: {
|
||||
fenceType: "string",
|
||||
},
|
||||
style: {
|
||||
color: "#705033",
|
||||
weight: 4,
|
||||
opacity: 0.95,
|
||||
},
|
||||
},
|
||||
yardGrid: {
|
||||
label: "Yard Grid",
|
||||
geometryType: "LineString",
|
||||
detailFields: {
|
||||
axis: "string",
|
||||
offsetFeet: "number",
|
||||
spacingFeet: "number",
|
||||
},
|
||||
style: (details) => ({
|
||||
color: details.offsetFeet === 0 ? "#1f4f63" : "#2f7f98",
|
||||
weight: details.offsetFeet === 0 ? 3 : 2,
|
||||
opacity: details.offsetFeet === 0 ? 0.75 : 0.55,
|
||||
dashArray: details.offsetFeet === 0 ? undefined : "6 5",
|
||||
}),
|
||||
},
|
||||
house: {
|
||||
label: "House",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#8d5a35",
|
||||
weight: 2,
|
||||
fillColor: "#c98d58",
|
||||
fillOpacity: 0.35,
|
||||
},
|
||||
},
|
||||
garage: {
|
||||
label: "Garage",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#60636b",
|
||||
weight: 2,
|
||||
fillColor: "#a4acb9",
|
||||
fillOpacity: 0.45,
|
||||
},
|
||||
},
|
||||
porch: {
|
||||
label: "Porch",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#aa7148",
|
||||
weight: 2,
|
||||
fillColor: "#deb588",
|
||||
fillOpacity: 0.38,
|
||||
},
|
||||
},
|
||||
deck: {
|
||||
label: "Deck",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#7f5a3c",
|
||||
weight: 2,
|
||||
fillColor: "#b98c66",
|
||||
fillOpacity: 0.32,
|
||||
},
|
||||
},
|
||||
walkway: {
|
||||
label: "Walkway",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#b8b3a6",
|
||||
weight: 2,
|
||||
fillColor: "#d6d1c3",
|
||||
fillOpacity: 0.75,
|
||||
},
|
||||
},
|
||||
driveway: {
|
||||
label: "Driveway",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
color: "#9c9890",
|
||||
weight: 2,
|
||||
fillColor: "#bbb7af",
|
||||
fillOpacity: 0.8,
|
||||
},
|
||||
},
|
||||
flowerBed: {
|
||||
label: "Flower Bed",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {
|
||||
soil: "string",
|
||||
mulch: "string",
|
||||
sunExposure: "string",
|
||||
},
|
||||
style: {
|
||||
color: "#7f7f31",
|
||||
weight: 2,
|
||||
fillColor: "#bfc97b",
|
||||
fillOpacity: 0.38,
|
||||
},
|
||||
},
|
||||
plant: {
|
||||
label: "Plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
commonName: "string",
|
||||
genus: "string",
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
gardenOrgId: "string",
|
||||
note: "string",
|
||||
},
|
||||
externalLinks: [
|
||||
{
|
||||
label: "Garden.org",
|
||||
url: (details) =>
|
||||
details.gardenOrgId
|
||||
? `https://garden.org/plants/view/${encodeURIComponent(details.gardenOrgId)}/`
|
||||
: null,
|
||||
},
|
||||
],
|
||||
style: {
|
||||
color: "#3d7140",
|
||||
weight: 2,
|
||||
radius: 5,
|
||||
fillColor: "#78a658",
|
||||
fillOpacity: 0.8,
|
||||
},
|
||||
},
|
||||
flower: {
|
||||
label: "Flower",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
commonName: "string",
|
||||
genus: "string",
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
bloomColor: "string",
|
||||
bloomSeason: "string",
|
||||
note: "string",
|
||||
},
|
||||
style: {
|
||||
color: "#9c3f74",
|
||||
weight: 2,
|
||||
fillColor: "#d979aa",
|
||||
fillOpacity: 0.85,
|
||||
iconUrl: "./src/assets/icons/flower.svg",
|
||||
iconDiameterMeters: 0.4,
|
||||
},
|
||||
},
|
||||
daylily: {
|
||||
label: "Daylily",
|
||||
parentKind: "flower",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
commonName: "string",
|
||||
genus: "string",
|
||||
cultivar: "string",
|
||||
bloomColor: "string",
|
||||
bloomSeason: "string",
|
||||
note: "string",
|
||||
},
|
||||
style: {
|
||||
color: "#9f5e1d",
|
||||
weight: 2,
|
||||
radius: 5,
|
||||
fillColor: "#f0b44c",
|
||||
fillOpacity: 0.88,
|
||||
iconUrl: "./src/assets/icons/daylily.svg",
|
||||
iconDiameterMeters: 2.5 / 3.280839895,
|
||||
},
|
||||
},
|
||||
shrub: {
|
||||
label: "Shrub",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
commonName: "string",
|
||||
genus: "string",
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
canopyDiameterMeters: "number",
|
||||
heightMeters: "number",
|
||||
note: "string",
|
||||
},
|
||||
style: (details) => ({
|
||||
color: "#426a3d",
|
||||
weight: 1,
|
||||
radiusMeters: (details.canopyDiameterMeters ?? 1) / 2,
|
||||
fillColor: "#7e9d58",
|
||||
fillOpacity: 0.62,
|
||||
}),
|
||||
},
|
||||
tree: {
|
||||
label: "Tree",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
commonName: "string",
|
||||
genus: "string",
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
canopyDiameterMeters: "number",
|
||||
heightMeters: "number",
|
||||
note: "string",
|
||||
},
|
||||
style: (details) => ({
|
||||
color: "#2f6b3d",
|
||||
weight: 1,
|
||||
radiusMeters: (details.canopyDiameterMeters ?? 2) / 2,
|
||||
fillColor: "#5c9e64",
|
||||
fillOpacity: 0.55,
|
||||
}),
|
||||
},
|
||||
sprinkler: {
|
||||
label: "Sprinkler",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
zone: "string",
|
||||
headType: "string",
|
||||
flowRateGallonsPerMinute: "number",
|
||||
},
|
||||
style: {
|
||||
color: "#2d7dd2",
|
||||
radius: 5,
|
||||
fillColor: "#88bdf2",
|
||||
fillOpacity: 0.95,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getFeatureType(kind) {
|
||||
const definition = featureTypes[kind];
|
||||
if (!definition) {
|
||||
throw new Error(`Unknown feature type: ${kind}`);
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
export function listFeatureTypeSchemas() {
|
||||
return Object.entries(featureTypes).map(([kind, definition]) => ({
|
||||
kind,
|
||||
label: definition.label,
|
||||
parentKind: definition.parentKind ?? null,
|
||||
geometryType: definition.geometryType,
|
||||
detailFields: getInheritedDetailFields(kind),
|
||||
externalLinks: getInheritedExternalLinks(kind).map((link) => ({
|
||||
label: link.label,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
export function resolveFeatureStyle(kind, details = {}) {
|
||||
const { style } = getFeatureType(kind);
|
||||
return typeof style === "function" ? style(details) : { ...style };
|
||||
}
|
||||
|
||||
export function resolveFeatureLinks(kind, details = {}) {
|
||||
return getInheritedExternalLinks(kind)
|
||||
.map((link) => ({
|
||||
label: link.label,
|
||||
url: link.url(details),
|
||||
}))
|
||||
.filter((link) => link.url);
|
||||
}
|
||||
|
||||
function getFeatureTypeLineage(kind) {
|
||||
const lineage = [];
|
||||
let currentKind = kind;
|
||||
|
||||
while (currentKind) {
|
||||
const definition = getFeatureType(currentKind);
|
||||
lineage.unshift(definition);
|
||||
currentKind = definition.parentKind;
|
||||
}
|
||||
|
||||
return lineage;
|
||||
}
|
||||
|
||||
function getInheritedDetailFields(kind) {
|
||||
return Object.assign(
|
||||
{},
|
||||
...getFeatureTypeLineage(kind).map((definition) => definition.detailFields ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
function getInheritedExternalLinks(kind) {
|
||||
return getFeatureTypeLineage(kind).flatMap((definition) => definition.externalLinks ?? []);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
const FEET_PER_METER = 3.280839895;
|
||||
const METERS_PER_FOOT = 1 / FEET_PER_METER;
|
||||
const EARTH_METERS_PER_DEGREE_LAT = 111320;
|
||||
|
||||
function toRadians(value) {
|
||||
return (value * Math.PI) / 180;
|
||||
}
|
||||
|
||||
function rotateLocalToTrueNorthEast(northMeters, eastMeters, assumedNorthClockwiseDegrees = 0) {
|
||||
const clockwiseRadians = toRadians(assumedNorthClockwiseDegrees);
|
||||
const trueNorthMeters =
|
||||
northMeters * Math.cos(clockwiseRadians) - eastMeters * Math.sin(clockwiseRadians);
|
||||
const trueEastMeters =
|
||||
northMeters * Math.sin(clockwiseRadians) + eastMeters * Math.cos(clockwiseRadians);
|
||||
|
||||
return {
|
||||
northMeters: trueNorthMeters,
|
||||
eastMeters: trueEastMeters,
|
||||
};
|
||||
}
|
||||
|
||||
export function point(lat, lon) {
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
export function lngLat(lon, lat) {
|
||||
return point(lat, lon);
|
||||
}
|
||||
|
||||
export function anchor(lon, lat, options = {}) {
|
||||
return {
|
||||
...lngLat(lon, lat),
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function pointsFromLngLat(coordinates) {
|
||||
return coordinates.map(([lon, lat]) => lngLat(lon, lat));
|
||||
}
|
||||
|
||||
export function ft(value) {
|
||||
return value * METERS_PER_FOOT;
|
||||
}
|
||||
|
||||
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 translate(origin, northMeters, eastMeters) {
|
||||
const latDelta = northMeters / EARTH_METERS_PER_DEGREE_LAT;
|
||||
const lonDelta = eastMeters / (EARTH_METERS_PER_DEGREE_LAT * Math.cos(toRadians(origin.lat)));
|
||||
|
||||
return point(origin.lat + latDelta, origin.lon + lonDelta);
|
||||
}
|
||||
|
||||
export function offset(origin, ...moves) {
|
||||
if (moves.length === 0) {
|
||||
return { kind: "offset-anchor", origin };
|
||||
}
|
||||
|
||||
const net = moves.reduce(
|
||||
(accumulator, move) => ({
|
||||
northMeters: accumulator.northMeters + move.northMeters,
|
||||
eastMeters: accumulator.eastMeters + move.eastMeters,
|
||||
}),
|
||||
{ northMeters: 0, eastMeters: 0 },
|
||||
);
|
||||
|
||||
const adjusted = rotateLocalToTrueNorthEast(
|
||||
net.northMeters,
|
||||
net.eastMeters,
|
||||
origin.assumedNorthClockwiseDegrees ?? 0,
|
||||
);
|
||||
|
||||
return translate(origin, adjusted.northMeters, adjusted.eastMeters);
|
||||
}
|
||||
|
||||
export function north(distanceMeters) {
|
||||
return { northMeters: distanceMeters, eastMeters: 0 };
|
||||
}
|
||||
|
||||
export function south(distanceMeters) {
|
||||
return { northMeters: -distanceMeters, eastMeters: 0 };
|
||||
}
|
||||
|
||||
export function east(distanceMeters) {
|
||||
return { northMeters: 0, eastMeters: distanceMeters };
|
||||
}
|
||||
|
||||
export function west(distanceMeters) {
|
||||
return { northMeters: 0, eastMeters: -distanceMeters };
|
||||
}
|
||||
|
||||
function buildFeature(type, geometryType, style = {}) {
|
||||
const coordinates = [];
|
||||
|
||||
return {
|
||||
add(...values) {
|
||||
coordinates.push(resolveCoordinate(values));
|
||||
return this;
|
||||
},
|
||||
toGeoJSON() {
|
||||
if (geometryType === "Point") {
|
||||
const [first] = coordinates;
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: { type, style },
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [first.lon, first.lat],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ring = coordinates.map((coordinate) => [coordinate.lon, coordinate.lat]);
|
||||
const geometry =
|
||||
geometryType === "LineString"
|
||||
? { type: "LineString", coordinates: ring }
|
||||
: { type: "Polygon", coordinates: [closeRing(ring)] };
|
||||
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: { type, style },
|
||||
geometry,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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]];
|
||||
}
|
||||
|
||||
export function polygon(type, style) {
|
||||
return buildFeature(type, "Polygon", style);
|
||||
}
|
||||
|
||||
export function path(type, style) {
|
||||
return buildFeature(type, "LineString", style);
|
||||
}
|
||||
|
||||
export function marker(type, style) {
|
||||
return buildFeature(type, "Point", style);
|
||||
}
|
||||
|
||||
export function sprinkler(name) {
|
||||
return marker(name, {
|
||||
color: "#2d7dd2",
|
||||
radius: 5,
|
||||
fillColor: "#88bdf2",
|
||||
fillOpacity: 0.95,
|
||||
});
|
||||
}
|
||||
|
||||
export function tree(name) {
|
||||
return marker(name, {
|
||||
color: "#2f6b3d",
|
||||
radius: 9,
|
||||
fillColor: "#5c9e64",
|
||||
fillOpacity: 0.85,
|
||||
});
|
||||
}
|
||||
|
||||
export function collectGeoJSON(...collections) {
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: collections.flat().map((entry) => entry.toGeoJSON()),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
|
||||
import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js";
|
||||
|
||||
export const YARD_SCHEMA_VERSION = 2;
|
||||
|
||||
class YardFeature {
|
||||
constructor(kind, geometryType, name, details = {}) {
|
||||
const featureType = getFeatureType(kind);
|
||||
if (featureType.geometryType !== geometryType) {
|
||||
throw new Error(
|
||||
`${kind} features must use ${featureType.geometryType} geometry, got ${geometryType}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.kind = kind;
|
||||
this.geometryType = geometryType;
|
||||
this.name = name ?? featureType.label;
|
||||
this.id = buildFeatureId(kind, this.name);
|
||||
const { plantedDate = null, removedDate = null, ...restDetails } = details;
|
||||
this.plantedDate = plantedDate;
|
||||
this.removedDate = removedDate;
|
||||
this.details = { ...restDetails };
|
||||
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;
|
||||
}
|
||||
|
||||
plantedOn(plantedDate) {
|
||||
this.plantedDate = plantedDate;
|
||||
return this;
|
||||
}
|
||||
|
||||
removedOn(removedDate) {
|
||||
this.removedDate = removedDate;
|
||||
return this;
|
||||
}
|
||||
|
||||
withLifecycleDates(plantedDate, removedDate = null) {
|
||||
this.plantedDate = plantedDate;
|
||||
this.removedDate = removedDate;
|
||||
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 properties = {
|
||||
id: this.id,
|
||||
kind: this.kind,
|
||||
label,
|
||||
name: this.name,
|
||||
plantedDate: this.plantedDate,
|
||||
removedDate: this.removedDate,
|
||||
parentId: this.parentId,
|
||||
groupIds: this.groupIds,
|
||||
details: this.details,
|
||||
};
|
||||
|
||||
if (this.geometryType === "Point") {
|
||||
const [first] = this.coordinates;
|
||||
return {
|
||||
type: "Feature",
|
||||
properties,
|
||||
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,
|
||||
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)) {
|
||||
if (rest.length === 0) {
|
||||
return first.origin;
|
||||
}
|
||||
|
||||
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 YardGridLine extends LineFeature {
|
||||
constructor(name = "Yard Grid", details = {}) {
|
||||
super("yardGrid", 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 Plant extends PointFeature {
|
||||
constructor(name = "Plant", details = {}, kind = "plant") {
|
||||
super(kind, name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Flower extends Plant {
|
||||
constructor(name = "Flower", details = {}, kind = "flower") {
|
||||
super(name, details, kind);
|
||||
}
|
||||
}
|
||||
|
||||
export class Daylily extends Flower {
|
||||
constructor(name = "Daylily", details = {}) {
|
||||
super(name, {
|
||||
commonName: "Daylily",
|
||||
genus: "Hemerocallis",
|
||||
...details,
|
||||
}, "daylily");
|
||||
}
|
||||
}
|
||||
|
||||
export class Shrub extends Plant {
|
||||
constructor(name = "Shrub", details = {}) {
|
||||
super(name, details, "shrub");
|
||||
}
|
||||
}
|
||||
|
||||
export class Bush extends Shrub {
|
||||
constructor(name = "Bush", details = {}) {
|
||||
super(name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class Tree extends Plant {
|
||||
constructor(name = "Tree", details = {}) {
|
||||
super(name, details, "tree");
|
||||
}
|
||||
}
|
||||
|
||||
export class Sprinkler extends PointFeature {
|
||||
constructor(name = "Sprinkler", details = {}) {
|
||||
super("sprinkler", name, details);
|
||||
}
|
||||
}
|
||||
|
||||
export function collectYardData(features, groups = []) {
|
||||
const collection = collectGeoJSON(features);
|
||||
collection.schemaVersion = YARD_SCHEMA_VERSION;
|
||||
collection.lifecycleFields = {
|
||||
plantedDate: "date",
|
||||
removedDate: "date",
|
||||
};
|
||||
collection.featureTypes = listFeatureTypeSchemas();
|
||||
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