diff --git a/AGENTS.md b/AGENTS.md index 047b4ed..d358fb5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,14 +16,17 @@ Static client-side yard mapping app. Core files: - `src/lib/geometry.js`: absolute points, local offsets, feet conversion, per-anchor rotation support. -- `src/lib/feature-types.js`: feature-kind style defaults. -- `src/lib/yard-features.js`: typed feature classes, stable ids, groups, parent-child relationships, GeoJSON export. +- `src/lib/feature-types.js`: feature-kind labels, geometry constraints, detail field schemas, and style defaults. +- `src/lib/yard-features.js`: typed feature classes, stable ids, groups, parent-child relationships, schema metadata, GeoJSON export. - `src/data/*.js`: yard content split by domain. - `src/main.js`: Leaflet map, legend, popup rendering. Top-level exported data shape from `src/data/yard.js`: - `type: "FeatureCollection"` +- `schemaVersion: 1` +- `lifecycleFields: { startDate: "date", endDate: "date" }` +- `featureTypes: [...]` - `features: [...]` - `groups: [...]` @@ -33,10 +36,11 @@ Each feature currently exports: - `properties.kind` - `properties.label` - `properties.name` +- `properties.startDate` +- `properties.endDate` - `properties.parentId` - `properties.groupIds` - `properties.details` -- `properties.style` ## Important decisions already made diff --git a/README.md b/README.md index 5a78759..9c093b0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The project currently has: - A static Leaflet map with OSM raster tiles. - Client-side JavaScript-authored yard geometry converted to GeoJSON at load time. - Typed feature classes with centralized styling. +- Schema-versioned yard data with a top-level feature type catalog. - Per-anchor local coordinate rotation for offset-based authoring. - Feature relationship metadata (`id`, `parentId`, `groupIds`) and a top-level group catalog for future visibility controls. - Example yard data mostly copied from `buildings.gpkg` and then preserved as JS modules. @@ -33,13 +34,13 @@ The example yard is now split by concern: - `src/data/plants.js` - `src/data/irrigation.js` -The typed feature registry lives in `src/lib/feature-types.js`, feature classes live in `src/lib/yard-features.js`, and shared coordinate helpers live in `src/lib/geometry.js`. +The typed feature registry lives in `src/lib/feature-types.js`, feature classes live in `src/lib/yard-features.js`, and shared coordinate helpers live in `src/lib/geometry.js`. Feature type definitions include the label, required geometry type, known detail fields, and default Leaflet style. Local `north`, `south`, `east`, and `west` offsets are rotated per anchor. In this example, `topLeftLotCorner` in `src/data/anchors.js` carries `assumedNorthClockwiseDegrees` from `src/data/settings.js`. A negative value means that anchor's local north is counterclockwise from true north, so local east drifts a bit toward true north. Most example geometry was copied from `buildings.gpkg` and preserved as JS-authored coordinates. Tree records also carry custom attributes like canopy diameter, height, and planted date for popup rendering. -Feature relationships are exported with each GeoJSON feature as stable `id`, `parentId`, and `groupIds` properties, and the collection also exposes a top-level `groups` array. The current sample marks the porch and deck as part of the house and groups the Medora junipers under `Juniper Trees`. +Feature relationships are exported with each GeoJSON feature as stable `id`, `parentId`, and `groupIds` properties. Each feature also exports universal `startDate` and `endDate` lifecycle fields for future time-slider filtering. The collection exposes `schemaVersion`, `lifecycleFields`, `featureTypes`, and `groups` at the top level. Style is resolved from the feature type registry at render time rather than stored on each exported feature. The current sample marks the porch and deck as part of the house and groups the Medora junipers under `Juniper Trees`. ## Project structure diff --git a/index.html b/index.html index cd2cb15..9d61ed1 100644 --- a/index.html +++ b/index.html @@ -4,12 +4,7 @@ Yard Map - + @@ -40,11 +35,8 @@ new Sprinkler("Front bed sprinkler") - + + diff --git a/src/assets/icons/daylily.svg b/src/assets/icons/daylily.svg new file mode 100644 index 0000000..6384ac2 --- /dev/null +++ b/src/assets/icons/daylily.svg @@ -0,0 +1,45 @@ + + Daylily + Abstract daylily clump with spindly fronds and flower stalks. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/data/anchors.js b/src/data/anchors.js index f7a197b..75f7735 100644 --- a/src/data/anchors.js +++ b/src/data/anchors.js @@ -1,7 +1,6 @@ import { anchor } from "../lib/geometry.js"; -import { lotAssumedNorthClockwiseDegrees } from "./settings.js"; -export const topLeftLotCorner = anchor(-100.89895832874616, 46.82679973173392, { - assumedNorthClockwiseDegrees: lotAssumedNorthClockwiseDegrees, +export const topLeftLotCorner = anchor(-100.898958, 46.826799, { + assumedNorthClockwiseDegrees: -11.25, }); export const lotCorner = topLeftLotCorner; \ No newline at end of file diff --git a/src/data/boundaries.js b/src/data/boundaries.js index ae61595..6208fdc 100644 --- a/src/data/boundaries.js +++ b/src/data/boundaries.js @@ -1,11 +1,12 @@ +import * as geo from "../lib/geometry.js"; import { Fence, LotBoundary } from "../lib/yard-features.js"; +import { lotCorner } from "./anchors.js"; -const lotBoundary = new LotBoundary().trace([ - [-100.89895832874616, 46.82679973173392], - [-100.89839320971627, 46.8268766687031], - [-100.89835104203355, 46.82673956300183], - [-100.89891868391628, 46.82666533836659], -]); +const lotBoundary = new LotBoundary() + .add(geo.offset(lotCorner)) + .add(geo.offset(lotCorner), geo.east(geo.ft(144))) + .add(geo.offset(lotCorner), geo.east(geo.ft(144)), geo.south(geo.ft(51))) + .add(geo.offset(lotCorner), geo.south(geo.ft(51))); const picketFence = new Fence("Short picket fence", { fenceType: "Short picket", diff --git a/src/data/grid.js b/src/data/grid.js new file mode 100644 index 0000000..3e199dd --- /dev/null +++ b/src/data/grid.js @@ -0,0 +1,57 @@ +import * as geo from "../lib/geometry.js"; +import { defineGroup, YardGridLine } from "../lib/yard-features.js"; +import { lotCorner } from "./anchors.js"; + +const GRID_SPACING_FEET = 10; +const MIN_EAST_FEET = -20; +const MAX_EAST_FEET = 180; +const MIN_NORTH_FEET = -140; +const MAX_NORTH_FEET = 20; + +export const yardGridGroup = defineGroup("group:yard-grid", "Yard Grid"); + +function localPoint(eastFeet, northFeet) { + return geo.offset( + lotCorner, + geo.east(geo.ft(eastFeet)), + northFeet >= 0 ? geo.north(geo.ft(northFeet)) : geo.south(geo.ft(Math.abs(northFeet))), + ); +} + +function makeNorthLine(eastFeet) { + return new YardGridLine(`Grid east ${eastFeet} ft`, { + axis: "north", + offsetFeet: eastFeet, + spacingFeet: GRID_SPACING_FEET, + }) + .inGroup(yardGridGroup) + .add(localPoint(eastFeet, MIN_NORTH_FEET)) + .add(localPoint(eastFeet, MAX_NORTH_FEET)); +} + +function makeEastLine(northFeet) { + return new YardGridLine(`Grid north ${northFeet} ft`, { + axis: "east", + offsetFeet: northFeet, + spacingFeet: GRID_SPACING_FEET, + }) + .inGroup(yardGridGroup) + .add(localPoint(MIN_EAST_FEET, northFeet)) + .add(localPoint(MAX_EAST_FEET, northFeet)); +} + +function rangeByFeet(minFeet, maxFeet, spacingFeet) { + const values = []; + + for (let value = minFeet; value <= maxFeet; value += spacingFeet) { + values.push(value); + } + + return values; +} + +const northLines = rangeByFeet(MIN_EAST_FEET, MAX_EAST_FEET, GRID_SPACING_FEET).map(makeNorthLine); +const eastLines = rangeByFeet(MIN_NORTH_FEET, MAX_NORTH_FEET, GRID_SPACING_FEET).map(makeEastLine); + +export const gridFeatures = [...northLines, ...eastLines]; +export const gridGroups = [yardGridGroup]; diff --git a/src/data/plants.js b/src/data/plants.js index d7fa3df..c70a6c9 100644 --- a/src/data/plants.js +++ b/src/data/plants.js @@ -1,6 +1,6 @@ -import { defineGroup, FlowerBed, Tree } from "../lib/yard-features.js"; -import { east, ft, lngLat, offset } from "../lib/geometry.js"; -import { topLeftLotCorner } from "./anchors.js"; +import { Daylily, defineGroup, FlowerBed, Tree } from "../lib/yard-features.js"; +import * as geo from "../lib/geometry.js"; +import { lotCorner } from "./anchors.js"; export const juniperTreesGroup = defineGroup("group:juniper-trees", "Juniper Trees"); @@ -47,60 +47,75 @@ const flowerBeds = [ ]), ]; +const daylilies = [ + new Daylily("You Had Me At Merlot", { + daylilyDatabaseId: "194128", + daylilyDatabaseUrl: "https://daylilydatabase.org/detail.php?id=194128", + }).add(geo.offset(lotCorner), geo.east(geo.ft(28)), geo.south(geo.ft(45))), + new Daylily("Mapping North Dakota", { + daylilyDatabaseId: "158640", + daylilyDatabaseUrl: "https://daylilydatabase.org/detail.php?id=158640", + }).add(geo.offset(lotCorner), geo.east(geo.ft(28)), geo.south(geo.ft(47))), + new Daylily("Daylily", { + daylilyDatabaseId: "", + daylilyDatabaseUrl: "", + }).add(geo.offset(lotCorner), geo.east(geo.ft(31)), geo.south(geo.ft(47))), +]; + const trees = [ new Tree("Japanese Empress Elm", { species: "Japanese Empress Elm", canopyDiameterMeters: 6, heightMeters: 7, - }).add(lngLat(-100.89889018162828, 46.82675624026659)), + }).add(geo.offset(lotCorner), geo.east(geo.ft(9)), geo.south(geo.ft(16))), new Tree("Boulevard Linden", { species: "Boulevard Linden", canopyDiameterMeters: 3, heightMeters: 6, plantedDate: "2021-06-08", - }).add(lngLat(-100.8990003556445, 46.826776648309774)), + }).add(geo.lngLat(-100.8990003556445, 46.826776648309774)), new Tree("Boulevard Linden 2", { species: "Boulevard Linden", canopyDiameterMeters: 3, heightMeters: 6, - }).add(lngLat(-100.89898269567485, 46.826709328342595)), + }).add(geo.lngLat(-100.89898269567485, 46.826709328342595)), new Tree("Evergreen", { species: "Evergreen", canopyDiameterMeters: 8, heightMeters: 15, - }).add(lngLat(-100.89888268258135, 46.826680107011924)), + }).add(geo.lngLat(-100.89888268258135, 46.826680107011924)), new Tree("Medora Juniper 1", { species: "Medora Juniper", canopyDiameterMeters: 1.5, heightMeters: 2.5, - }).inGroup(juniperTreesGroup).add(lngLat(-100.89880933964611, 46.826705259550664)), + }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89880933964611, 46.826705259550664)), new Tree("Medora Juniper 2", { species: "Medora Juniper", canopyDiameterMeters: 1.5, heightMeters: 2.5, - }).inGroup(juniperTreesGroup).add(lngLat(-100.89877004677517, 46.82670396593881)), + }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89877004677517, 46.82670396593881)), new Tree("Medora Juniper 3", { species: "Medora Juniper", canopyDiameterMeters: 1.5, heightMeters: 2.5, - }).inGroup(juniperTreesGroup).add(lngLat(-100.898799106227, 46.82669830398086)), + }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.898799106227, 46.82669830398086)), new Tree("Medora Juniper 4", { species: "Medora Juniper", canopyDiameterMeters: 1.5, heightMeters: 2.5, - }).inGroup(juniperTreesGroup).add(lngLat(-100.89878871410515, 46.82669251034885)), + }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89878871410515, 46.82669251034885)), new Tree("Medora Juniper 5", { species: "Medora Juniper", canopyDiameterMeters: 1.5, heightMeters: 2.5, - }).inGroup(juniperTreesGroup).add(lngLat(-100.8987812086838, 46.82671120797719)), + }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.8987812086838, 46.82671120797719)), new Tree("Offset Test Tree", { species: "Test tree", canopyDiameterMeters: 4, heightMeters: 5, note: "70 feet east of top-left lot corner using assumedNorth", - }).add(offset(topLeftLotCorner), east(ft(70))), + }).add(geo.offset(lotCorner), geo.east(geo.ft(70))), ]; -export const plantFeatures = [...flowerBeds, ...trees]; -export const plantGroups = [juniperTreesGroup]; \ No newline at end of file +export const plantFeatures = [...flowerBeds, ...daylilies, ...trees]; +export const plantGroups = [juniperTreesGroup]; diff --git a/src/data/settings.js b/src/data/settings.js deleted file mode 100644 index 0700634..0000000 --- a/src/data/settings.js +++ /dev/null @@ -1 +0,0 @@ -export const lotAssumedNorthClockwiseDegrees = -11.25369719051117; \ No newline at end of file diff --git a/src/data/yard.js b/src/data/yard.js index 88f411b..9f4dda3 100644 --- a/src/data/yard.js +++ b/src/data/yard.js @@ -1,6 +1,7 @@ import { collectYardData } from "../lib/yard-features.js"; import { boundaryFeatures } from "./boundaries.js"; import { buildingFeatures, buildingGroups } from "./buildings.js"; +import { gridFeatures, gridGroups } from "./grid.js"; import { irrigationFeatures } from "./irrigation.js"; import { pavementFeatures } from "./pavement.js"; import { plantFeatures, plantGroups } from "./plants.js"; @@ -8,6 +9,7 @@ import { plantFeatures, plantGroups } from "./plants.js"; export const yardGeoJSON = collectYardData( [ ...boundaryFeatures, + ...gridFeatures, ...buildingFeatures, ...pavementFeatures, ...plantFeatures, @@ -15,6 +17,7 @@ export const yardGeoJSON = collectYardData( ], [ ...buildingGroups, + ...gridGroups, ...plantGroups, ], -); \ No newline at end of file +); diff --git a/src/error-overlay.js b/src/error-overlay.js new file mode 100644 index 0000000..cb73f16 --- /dev/null +++ b/src/error-overlay.js @@ -0,0 +1,82 @@ +const overlay = document.createElement("section"); +overlay.className = "error-overlay"; +overlay.hidden = true; +overlay.innerHTML = ` +
+ JavaScript error +
+ + +
+
+

+`;
+
+const messageNode = overlay.querySelector("pre");
+const copyButton = overlay.querySelector("[data-action='copy']");
+const dismissButton = overlay.querySelector("[data-action='dismiss']");
+
+copyButton.addEventListener("click", async () => {
+  await copyText(messageNode.textContent);
+  showCopyStatus();
+});
+
+dismissButton.addEventListener("click", () => {
+  overlay.hidden = true;
+});
+
+document.addEventListener("DOMContentLoaded", () => {
+  document.body.append(overlay);
+});
+
+window.addEventListener("error", (event) => {
+  showError(formatErrorEvent(event));
+});
+
+window.addEventListener("unhandledrejection", (event) => {
+  showError(formatReason(event.reason));
+});
+
+function showError(message) {
+  messageNode.textContent = message;
+  copyButton.textContent = "Copy";
+  overlay.hidden = false;
+}
+
+async function copyText(value) {
+  if (navigator.clipboard?.writeText) {
+    await navigator.clipboard.writeText(value);
+    return;
+  }
+
+  const textArea = document.createElement("textarea");
+  textArea.value = value;
+  textArea.setAttribute("readonly", "");
+  textArea.className = "error-overlay-copy-buffer";
+  document.body.append(textArea);
+  textArea.select();
+  document.execCommand("copy");
+  textArea.remove();
+}
+
+function showCopyStatus() {
+  copyButton.textContent = "Copied";
+
+  window.setTimeout(() => {
+    copyButton.textContent = "Copy";
+  }, 1400);
+}
+
+function formatErrorEvent(event) {
+  const location = [event.filename, event.lineno, event.colno].filter(Boolean).join(":");
+  const detail = formatReason(event.error ?? event.message);
+  return location ? `${detail}\n\n${location}` : detail;
+}
+
+function formatReason(reason) {
+  if (reason instanceof Error) {
+    return reason.stack ?? `${reason.name}: ${reason.message}`;
+  }
+
+  return String(reason);
+}
diff --git a/src/lib/feature-types.js b/src/lib/feature-types.js
index 1bbb191..6a1b4f9 100644
--- a/src/lib/feature-types.js
+++ b/src/lib/feature-types.js
@@ -1,15 +1,8 @@
-const treeRadiusRange = {
-  min: 5,
-  max: 14,
-};
-
-function clamp(value, min, max) {
-  return Math.min(max, Math.max(min, value));
-}
-
 export const featureTypes = {
   lotBoundary: {
     label: "Lot Boundary",
+    geometryType: "Polygon",
+    detailFields: {},
     style: {
       color: "#7b684d",
       weight: 2,
@@ -20,14 +13,35 @@ export const featureTypes = {
   },
   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,
@@ -37,6 +51,8 @@ export const featureTypes = {
   },
   garage: {
     label: "Garage",
+    geometryType: "Polygon",
+    detailFields: {},
     style: {
       color: "#60636b",
       weight: 2,
@@ -46,6 +62,8 @@ export const featureTypes = {
   },
   porch: {
     label: "Porch",
+    geometryType: "Polygon",
+    detailFields: {},
     style: {
       color: "#aa7148",
       weight: 2,
@@ -55,6 +73,8 @@ export const featureTypes = {
   },
   deck: {
     label: "Deck",
+    geometryType: "Polygon",
+    detailFields: {},
     style: {
       color: "#7f5a3c",
       weight: 2,
@@ -64,6 +84,8 @@ export const featureTypes = {
   },
   walkway: {
     label: "Walkway",
+    geometryType: "Polygon",
+    detailFields: {},
     style: {
       color: "#b8b3a6",
       weight: 2,
@@ -73,6 +95,8 @@ export const featureTypes = {
   },
   driveway: {
     label: "Driveway",
+    geometryType: "Polygon",
+    detailFields: {},
     style: {
       color: "#9c9890",
       weight: 2,
@@ -82,6 +106,12 @@ export const featureTypes = {
   },
   flowerBed: {
     label: "Flower Bed",
+    geometryType: "Polygon",
+    detailFields: {
+      soil: "string",
+      mulch: "string",
+      sunExposure: "string",
+    },
     style: {
       color: "#7f7f31",
       weight: 2,
@@ -89,18 +119,130 @@ export const featureTypes = {
       fillOpacity: 0.38,
     },
   },
+  plant: {
+    label: "Plant",
+    geometryType: "Point",
+    detailFields: {
+      commonName: "string",
+      genus: "string",
+      species: "string",
+      cultivar: "string",
+      plantedDate: "date",
+      note: "string",
+    },
+    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",
+      plantedDate: "date",
+      note: "string",
+    },
+    style: {
+      color: "#9c3f74",
+      weight: 2,
+      radius: 5,
+      fillColor: "#d979aa",
+      fillOpacity: 0.85,
+    },
+  },
+  daylily: {
+    label: "Daylily",
+    parentKind: "flower",
+    geometryType: "Point",
+    detailFields: {
+      commonName: "string",
+      genus: "string",
+      cultivar: "string",
+      bloomColor: "string",
+      bloomSeason: "string",
+      daylilyDatabaseId: "string",
+      daylilyDatabaseUrl: "url",
+      plantedDate: "date",
+      note: "string",
+    },
+    externalLinks: [
+      {
+        label: "Daylily database",
+        url: (details) => details.daylilyDatabaseUrl ?? null,
+      },
+    ],
+    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",
+      plantedDate: "date",
+      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",
+      plantedDate: "date",
+      note: "string",
+    },
     style: (details) => ({
       color: "#2f6b3d",
-      weight: 2,
-      radius: clamp(4 + (details.canopyDiameterMeters ?? 2), treeRadiusRange.min, treeRadiusRange.max),
+      weight: 1,
+      radiusMeters: (details.canopyDiameterMeters ?? 2) / 2,
       fillColor: "#5c9e64",
-      fillOpacity: 0.85,
+      fillOpacity: 0.55,
     }),
   },
   sprinkler: {
     label: "Sprinkler",
+    geometryType: "Point",
+    detailFields: {
+      zone: "string",
+      headType: "string",
+      flowRateGallonsPerMinute: "number",
+    },
     style: {
       color: "#2d7dd2",
       radius: 5,
@@ -119,7 +261,31 @@ export function getFeatureType(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: { ...definition.detailFields },
+    externalLinks: (definition.externalLinks ?? []).map((link) => ({
+      label: link.label,
+    })),
+  }));
+}
+
 export function resolveFeatureStyle(kind, details = {}) {
   const { style } = getFeatureType(kind);
-  return typeof style === "function" ? style(details) : style;
-}
\ No newline at end of file
+  return typeof style === "function" ? style(details) : { ...style };
+}
+
+export function resolveFeatureLinks(kind, details = {}) {
+  const { externalLinks = [] } = getFeatureType(kind);
+
+  return externalLinks
+    .map((link) => ({
+      label: link.label,
+      url: link.url(details),
+    }))
+    .filter((link) => link.url);
+}
diff --git a/src/lib/yard-features.js b/src/lib/yard-features.js
index 85ec4a7..b6d2858 100644
--- a/src/lib/yard-features.js
+++ b/src/lib/yard-features.js
@@ -1,13 +1,25 @@
 import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
-import { getFeatureType, resolveFeatureStyle } from "./feature-types.js";
+import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js";
+
+export const YARD_SCHEMA_VERSION = 1;
 
 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 ?? getFeatureType(kind).label;
+    this.name = name ?? featureType.label;
     this.id = buildFeatureId(kind, this.name);
-    this.details = { ...details };
+    const { startDate = null, endDate = null, ...restDetails } = details;
+    this.startDate = startDate;
+    this.endDate = endDate;
+    this.details = { ...restDetails };
     this.coordinates = [];
     this.parentId = null;
     this.groupIds = [];
@@ -36,6 +48,22 @@ class YardFeature {
     return this;
   }
 
+  activeFrom(startDate) {
+    this.startDate = startDate;
+    return this;
+  }
+
+  activeUntil(endDate) {
+    this.endDate = endDate;
+    return this;
+  }
+
+  withDateRange(startDate, endDate = null) {
+    this.startDate = startDate;
+    this.endDate = endDate;
+    return this;
+  }
+
   childOf(parent) {
     this.parentId = resolveFeatureReference(parent);
     return this;
@@ -49,22 +77,23 @@ class YardFeature {
 
   toGeoJSON() {
     const label = getFeatureType(this.kind).label;
-    const style = resolveFeatureStyle(this.kind, this.details);
+    const properties = {
+      id: this.id,
+      kind: this.kind,
+      label,
+      name: this.name,
+      startDate: this.startDate,
+      endDate: this.endDate,
+      parentId: this.parentId,
+      groupIds: this.groupIds,
+      details: 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,
-        },
+        properties,
         geometry: {
           type: "Point",
           coordinates: [first.lon, first.lat],
@@ -80,16 +109,7 @@ class YardFeature {
 
     return {
       type: "Feature",
-      properties: {
-        id: this.id,
-        kind: this.kind,
-        label,
-        name: this.name,
-        parentId: this.parentId,
-        groupIds: this.groupIds,
-        details: this.details,
-        style,
-      },
+      properties,
       geometry,
     };
   }
@@ -194,6 +214,10 @@ function resolveCoordinate(values) {
 
   const [first, ...rest] = values;
   if (isOffsetAnchor(first) && rest.every(isMove)) {
+    if (rest.length === 0) {
+      return first.origin;
+    }
+
     return offset(first.origin, ...rest);
   }
 
@@ -212,6 +236,12 @@ export class Fence extends LineFeature {
   }
 }
 
+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);
@@ -254,9 +284,43 @@ export class FlowerBed extends PolygonFeature {
   }
 }
 
-export class Tree extends PointFeature {
+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("tree", name, details);
+    super(name, details, "tree");
   }
 }
 
@@ -268,6 +332,12 @@ export class Sprinkler extends PointFeature {
 
 export function collectYardData(features, groups = []) {
   const collection = collectGeoJSON(features);
+  collection.schemaVersion = YARD_SCHEMA_VERSION;
+  collection.lifecycleFields = {
+    startDate: "date",
+    endDate: "date",
+  };
+  collection.featureTypes = listFeatureTypeSchemas();
   collection.groups = groups.map((group) => ({
     id: group.id,
     label: group.label,
@@ -276,4 +346,4 @@ export function collectYardData(features, groups = []) {
   return collection;
 }
 
-export { collectGeoJSON };
\ No newline at end of file
+export { collectGeoJSON };
diff --git a/src/main.js b/src/main.js
index d558fe7..c9b06ab 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,4 +1,5 @@
 import { yardGeoJSON } from "./data/yard.js";
+import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js";
 
 const TILE_MAX_NATIVE_ZOOM = 19;
 const MAP_MAX_ZOOM = 24;
@@ -7,6 +8,7 @@ const map = L.map("map", {
   zoomControl: false,
   maxZoom: MAP_MAX_ZOOM,
 });
+const meterScaledIconMarkers = [];
 
 L.control
   .zoom({
@@ -20,26 +22,55 @@ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
   attribution: '© OpenStreetMap',
 }).addTo(map);
 
-const geoJsonLayer = L.geoJSON(yardGeoJSON, {
+L.geoJSON(yardGeoJSON, {
   pointToLayer(feature, latlng) {
-    const style = feature.properties.style;
-    return L.circleMarker(latlng, {
-      radius: style.radius ?? 6,
+    const style = getStyle(feature);
+    const options = {
       color: style.color ?? "#333333",
       fillColor: style.fillColor ?? style.color ?? "#333333",
       fillOpacity: style.fillOpacity ?? 0.8,
       weight: style.weight ?? 2,
+    };
+
+    if (style.iconUrl) {
+      const marker = L.marker(latlng, {
+        icon: buildIcon(style, latlng),
+      });
+
+      if (typeof style.iconDiameterMeters === "number") {
+        meterScaledIconMarkers.push({ marker, style, latlng });
+      }
+
+      return marker;
+    }
+
+    if (typeof style.radiusMeters === "number") {
+      return L.circle(latlng, {
+        ...options,
+        radius: style.radiusMeters,
+      });
+    }
+
+    return L.circleMarker(latlng, {
+      ...options,
+      radius: style.radius ?? 6,
     });
   },
   style(feature) {
-    return feature.properties.style;
+    if (feature.geometry.type === "Point") {
+      return getPathStyle(feature);
+    }
+
+    return getStyle(feature);
   },
   onEachFeature(feature, layer) {
     layer.bindPopup(buildPopupContent(feature.properties));
   },
 }).addTo(map);
 
-map.fitBounds(geoJsonLayer.getBounds().pad(0.2), { maxZoom: MAP_MAX_ZOOM });
+map.fitBounds(getGeoJSONBounds(yardGeoJSON).pad(0.2), { maxZoom: MAP_MAX_ZOOM });
+updateMeterScaledIcons();
+map.on("zoomend", updateMeterScaledIcons);
 
 const legend = document.getElementById("legend");
 const legendEntries = summarizeTypes(yardGeoJSON.features);
@@ -66,10 +97,11 @@ function summarizeTypes(features) {
 
   for (const feature of features) {
     const key = feature.properties.kind;
+    const style = getStyle(feature);
     const existing = summary.get(key) ?? {
       label: feature.properties.label,
       count: 0,
-      color: feature.properties.style.color ?? "#333333",
+      color: style.color ?? "#333333",
     };
 
     existing.count += 1;
@@ -79,10 +111,96 @@ function summarizeTypes(features) {
   return [...summary.values()];
 }
 
+function getStyle(feature) {
+  return resolveFeatureStyle(feature.properties.kind, feature.properties.details);
+}
+
+function getPathStyle(feature) {
+  const {
+    iconAnchor,
+    iconDiameterMeters,
+    iconSize,
+    iconUrl,
+    popupAnchor,
+    radius,
+    radiusMeters,
+    ...style
+  } = getStyle(feature);
+  return style;
+}
+
+function buildIcon(style, latlng) {
+  const iconSize = getIconSize(style, latlng);
+
+  return L.icon({
+    iconUrl: style.iconUrl,
+    iconSize,
+    iconAnchor: [iconSize[0] / 2, iconSize[1] / 2],
+    popupAnchor: [0, -iconSize[1] / 2],
+  });
+}
+
+function updateMeterScaledIcons() {
+  for (const { marker, style, latlng } of meterScaledIconMarkers) {
+    marker.setIcon(buildIcon(style, latlng));
+  }
+}
+
+function getIconSize(style, latlng) {
+  if (typeof style.iconDiameterMeters !== "number") {
+    return style.iconSize ?? [24, 24];
+  }
+
+  if (typeof map.getZoom() !== "number") {
+    return style.iconSize ?? [24, 24];
+  }
+
+  const diameterPixels = style.iconDiameterMeters / getMetersPerPixel(latlng.lat);
+  return [diameterPixels, diameterPixels];
+}
+
+function getMetersPerPixel(lat) {
+  return (156543.03392 * Math.cos((lat * Math.PI) / 180)) / 2 ** map.getZoom();
+}
+
+function getGeoJSONBounds(collection) {
+  const bounds = L.latLngBounds();
+
+  for (const feature of collection.features) {
+    extendBounds(bounds, feature.geometry.coordinates);
+  }
+
+  return bounds;
+}
+
+function extendBounds(bounds, coordinates) {
+  if (typeof coordinates[0] === "number") {
+    const [lon, lat] = coordinates;
+    bounds.extend([lat, lon]);
+    return;
+  }
+
+  for (const coordinate of coordinates) {
+    extendBounds(bounds, coordinate);
+  }
+}
+
 function buildPopupContent(properties) {
-  const { label, name, details, parentId, groupIds } = properties;
-  const entries = Object.entries(details ?? {}).filter(([, value]) => value !== null && value !== undefined);
+  const { kind, label, name, startDate, endDate, details, parentId, groupIds } = properties;
+  const entries = Object.entries(details ?? {}).filter(
+    ([key, value]) => value !== null && value !== undefined && !key.endsWith("Url"),
+  );
+  const lifecycleEntries = [];
   const relationshipEntries = [];
+  const linkEntries = resolveFeatureLinks(kind, details);
+
+  if (startDate) {
+    lifecycleEntries.push(["startDate", startDate]);
+  }
+
+  if (endDate) {
+    lifecycleEntries.push(["endDate", endDate]);
+  }
 
   if (parentId) {
     relationshipEntries.push(["parentFeature", parentId]);
@@ -94,16 +212,19 @@ function buildPopupContent(properties) {
 
   const title = `${escapeHtml(name)}`;
   const subtitle = name === label ? "" : `
${escapeHtml(label)}
`; - const allEntries = [...entries, ...relationshipEntries]; - if (allEntries.length === 0) { + const allEntries = [...lifecycleEntries, ...entries, ...relationshipEntries]; + if (allEntries.length === 0 && linkEntries.length === 0) { return `${title}${subtitle}`; } const detailRows = allEntries .map(([key, value]) => `
${escapeHtml(formatDetailLabel(key))}: ${escapeHtml(formatDetailValue(key, value))}
`) .join(""); + const linkRows = linkEntries + .map((link) => `
${escapeHtml(link.label)}: ${escapeHtml(link.url)}
`) + .join(""); - return `${title}${subtitle}
${detailRows}
`; + return `${title}${subtitle}
${detailRows}${linkRows}
`; } function formatDetailLabel(key) { @@ -111,8 +232,20 @@ function formatDetailLabel(key) { canopyDiameterMeters: "Canopy diameter", heightMeters: "Height", plantedDate: "Planted", + startDate: "Start", + endDate: "End", + commonName: "Common name", + genus: "Genus", + cultivar: "Cultivar", + bloomColor: "Bloom color", + bloomSeason: "Bloom season", + daylilyDatabaseId: "Daylily database ID", + daylilyDatabaseUrl: "Daylily database URL", species: "Species", fenceType: "Fence type", + axis: "Axis", + offsetFeet: "Offset", + spacingFeet: "Spacing", note: "Note", parentFeature: "Parent", groups: "Groups", @@ -126,6 +259,10 @@ function formatDetailValue(key, value) { return `${value} m`; } + if (key === "offsetFeet" || key === "spacingFeet") { + return `${value} ft`; + } + return String(value); } @@ -136,4 +273,4 @@ function escapeHtml(value) { .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); -} \ No newline at end of file +} diff --git a/styles.css b/styles.css index f55f3ac..89152a8 100644 --- a/styles.css +++ b/styles.css @@ -125,6 +125,63 @@ main { font-family: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", serif; } +.error-overlay { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 10000; + width: min(42rem, calc(100vw - 2rem)); + max-height: min(28rem, calc(100vh - 2rem)); + overflow: auto; + border: 1px solid rgba(111, 23, 23, 0.32); + border-radius: 8px; + background: #fff7f4; + box-shadow: 0 18px 60px rgba(59, 20, 20, 0.22); + color: #331514; +} + +.error-overlay[hidden] { + display: none; +} + +.error-overlay-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.8rem 1rem; + border-bottom: 1px solid rgba(111, 23, 23, 0.18); + background: #f6d5ce; +} + +.error-overlay-actions { + display: flex; + gap: 0.45rem; +} + +.error-overlay button { + border: 1px solid rgba(111, 23, 23, 0.28); + border-radius: 6px; + padding: 0.35rem 0.55rem; + background: #fffaf8; + color: inherit; + font: inherit; + cursor: pointer; +} + +.error-overlay pre { + margin: 0; + border-radius: 0; + background: transparent; + white-space: pre-wrap; +} + +.error-overlay-copy-buffer { + position: fixed; + top: -100vh; + left: -100vw; +} + @media (max-width: 900px) { .app-shell { grid-template-columns: 1fr; @@ -133,4 +190,4 @@ main { #map { min-height: 65vh; } -} \ No newline at end of file +} diff --git a/vendor/leaflet/images/layers-2x.png b/vendor/leaflet/images/layers-2x.png new file mode 100644 index 0000000..200c333 Binary files /dev/null and b/vendor/leaflet/images/layers-2x.png differ diff --git a/vendor/leaflet/images/layers.png b/vendor/leaflet/images/layers.png new file mode 100644 index 0000000..1a72e57 Binary files /dev/null and b/vendor/leaflet/images/layers.png differ diff --git a/vendor/leaflet/images/marker-icon-2x.png b/vendor/leaflet/images/marker-icon-2x.png new file mode 100644 index 0000000..88f9e50 Binary files /dev/null and b/vendor/leaflet/images/marker-icon-2x.png differ diff --git a/vendor/leaflet/images/marker-icon.png b/vendor/leaflet/images/marker-icon.png new file mode 100644 index 0000000..950edf2 Binary files /dev/null and b/vendor/leaflet/images/marker-icon.png differ diff --git a/vendor/leaflet/images/marker-shadow.png b/vendor/leaflet/images/marker-shadow.png new file mode 100644 index 0000000..9fd2979 Binary files /dev/null and b/vendor/leaflet/images/marker-shadow.png differ diff --git a/vendor/leaflet/leaflet.css b/vendor/leaflet/leaflet.css new file mode 100644 index 0000000..2961b76 --- /dev/null +++ b/vendor/leaflet/leaflet.css @@ -0,0 +1,661 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + } diff --git a/vendor/leaflet/leaflet.js b/vendor/leaflet/leaflet.js new file mode 100644 index 0000000..a3bf693 --- /dev/null +++ b/vendor/leaflet/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1