Many improvements
This commit is contained in:
@@ -16,14 +16,17 @@ Static client-side yard mapping app.
|
|||||||
Core files:
|
Core files:
|
||||||
|
|
||||||
- `src/lib/geometry.js`: absolute points, local offsets, feet conversion, per-anchor rotation support.
|
- `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/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, GeoJSON export.
|
- `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/data/*.js`: yard content split by domain.
|
||||||
- `src/main.js`: Leaflet map, legend, popup rendering.
|
- `src/main.js`: Leaflet map, legend, popup rendering.
|
||||||
|
|
||||||
Top-level exported data shape from `src/data/yard.js`:
|
Top-level exported data shape from `src/data/yard.js`:
|
||||||
|
|
||||||
- `type: "FeatureCollection"`
|
- `type: "FeatureCollection"`
|
||||||
|
- `schemaVersion: 1`
|
||||||
|
- `lifecycleFields: { startDate: "date", endDate: "date" }`
|
||||||
|
- `featureTypes: [...]`
|
||||||
- `features: [...]`
|
- `features: [...]`
|
||||||
- `groups: [...]`
|
- `groups: [...]`
|
||||||
|
|
||||||
@@ -33,10 +36,11 @@ Each feature currently exports:
|
|||||||
- `properties.kind`
|
- `properties.kind`
|
||||||
- `properties.label`
|
- `properties.label`
|
||||||
- `properties.name`
|
- `properties.name`
|
||||||
|
- `properties.startDate`
|
||||||
|
- `properties.endDate`
|
||||||
- `properties.parentId`
|
- `properties.parentId`
|
||||||
- `properties.groupIds`
|
- `properties.groupIds`
|
||||||
- `properties.details`
|
- `properties.details`
|
||||||
- `properties.style`
|
|
||||||
|
|
||||||
## Important decisions already made
|
## Important decisions already made
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ The project currently has:
|
|||||||
- A static Leaflet map with OSM raster tiles.
|
- A static Leaflet map with OSM raster tiles.
|
||||||
- Client-side JavaScript-authored yard geometry converted to GeoJSON at load time.
|
- Client-side JavaScript-authored yard geometry converted to GeoJSON at load time.
|
||||||
- Typed feature classes with centralized styling.
|
- 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.
|
- 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.
|
- 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.
|
- 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/plants.js`
|
||||||
- `src/data/irrigation.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.
|
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.
|
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
|
## Project structure
|
||||||
|
|
||||||
|
|||||||
+3
-11
@@ -4,12 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Yard Map</title>
|
<title>Yard Map</title>
|
||||||
<link
|
<link rel="stylesheet" href="./vendor/leaflet/leaflet.css" />
|
||||||
rel="stylesheet"
|
|
||||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
||||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
||||||
crossorigin=""
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -40,11 +35,8 @@ new Sprinkler("Front bed sprinkler")
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script
|
<script src="./src/error-overlay.js"></script>
|
||||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
<script src="./vendor/leaflet/leaflet.js"></script>
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
||||||
crossorigin=""
|
|
||||||
></script>
|
|
||||||
<script type="module" src="./src/main.js"></script>
|
<script type="module" src="./src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Daylily</title>
|
||||||
|
<desc id="desc">Abstract daylily clump with spindly fronds and flower stalks.</desc>
|
||||||
|
<defs>
|
||||||
|
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="0" dy="1.5" stdDeviation="1.2" flood-color="#1d2d16" flood-opacity="0.28" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g fill="none" stroke-linecap="round" stroke-linejoin="round" filter="url(#shadow)">
|
||||||
|
<g stroke="#315f2b">
|
||||||
|
<path d="M32 58 C24 47 16 40 8 34" stroke-width="3.1" />
|
||||||
|
<path d="M32 58 C25 45 22 35 20 22" stroke-width="2.7" />
|
||||||
|
<path d="M32 58 C30 43 31 29 35 14" stroke-width="2.7" />
|
||||||
|
<path d="M32 58 C40 45 47 37 56 30" stroke-width="3.1" />
|
||||||
|
<path d="M32 58 C38 48 42 40 44 27" stroke-width="2.5" />
|
||||||
|
</g>
|
||||||
|
<g stroke="#5f8d39">
|
||||||
|
<path d="M32 58 C21 51 13 48 5 48" stroke-width="2.4" />
|
||||||
|
<path d="M32 58 C24 49 19 44 13 39" stroke-width="2.2" />
|
||||||
|
<path d="M32 58 C29 47 28 38 29 27" stroke-width="2.2" />
|
||||||
|
<path d="M32 58 C35 47 37 37 40 26" stroke-width="2.2" />
|
||||||
|
<path d="M32 58 C42 50 50 46 60 44" stroke-width="2.4" />
|
||||||
|
<path d="M32 58 C43 53 51 52 58 55" stroke-width="2.1" />
|
||||||
|
</g>
|
||||||
|
<g stroke="#6f7f32">
|
||||||
|
<path d="M32 57 C33 43 34 30 35 16" stroke-width="1.7" />
|
||||||
|
<path d="M31 57 C28 43 27 31 25 18" stroke-width="1.5" />
|
||||||
|
<path d="M33 57 C42 45 47 34 50 22" stroke-width="1.5" />
|
||||||
|
</g>
|
||||||
|
<g fill="#f0b44c" stroke="#9f5e1d" stroke-width="1.4">
|
||||||
|
<path d="M35 15 C30 13 29 8 32 4 C36 7 38 11 35 15" />
|
||||||
|
<path d="M35 15 C39 11 44 12 47 16 C42 18 38 18 35 15" />
|
||||||
|
<path d="M35 15 C39 18 39 23 36 27 C33 23 32 19 35 15" />
|
||||||
|
<circle cx="35" cy="15" r="1.7" fill="#743719" stroke="none" />
|
||||||
|
<path d="M25 18 C21 15 21 11 24 8 C27 11 28 15 25 18" />
|
||||||
|
<path d="M25 18 C29 16 32 18 33 22 C29 23 26 21 25 18" />
|
||||||
|
<path d="M25 18 C23 22 19 23 16 21 C18 17 21 16 25 18" />
|
||||||
|
<circle cx="25" cy="18" r="1.5" fill="#743719" stroke="none" />
|
||||||
|
<path d="M50 22 C46 19 47 15 50 12 C53 15 54 19 50 22" />
|
||||||
|
<path d="M50 22 C54 20 58 22 59 26 C55 27 52 25 50 22" />
|
||||||
|
<path d="M50 22 C49 26 45 28 42 26 C44 23 47 21 50 22" />
|
||||||
|
<circle cx="50" cy="22" r="1.4" fill="#743719" stroke="none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
+2
-3
@@ -1,7 +1,6 @@
|
|||||||
import { anchor } from "../lib/geometry.js";
|
import { anchor } from "../lib/geometry.js";
|
||||||
import { lotAssumedNorthClockwiseDegrees } from "./settings.js";
|
|
||||||
|
|
||||||
export const topLeftLotCorner = anchor(-100.89895832874616, 46.82679973173392, {
|
export const topLeftLotCorner = anchor(-100.898958, 46.826799, {
|
||||||
assumedNorthClockwiseDegrees: lotAssumedNorthClockwiseDegrees,
|
assumedNorthClockwiseDegrees: -11.25,
|
||||||
});
|
});
|
||||||
export const lotCorner = topLeftLotCorner;
|
export const lotCorner = topLeftLotCorner;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import * as geo from "../lib/geometry.js";
|
||||||
import { Fence, LotBoundary } from "../lib/yard-features.js";
|
import { Fence, LotBoundary } from "../lib/yard-features.js";
|
||||||
|
import { lotCorner } from "./anchors.js";
|
||||||
|
|
||||||
const lotBoundary = new LotBoundary().trace([
|
const lotBoundary = new LotBoundary()
|
||||||
[-100.89895832874616, 46.82679973173392],
|
.add(geo.offset(lotCorner))
|
||||||
[-100.89839320971627, 46.8268766687031],
|
.add(geo.offset(lotCorner), geo.east(geo.ft(144)))
|
||||||
[-100.89835104203355, 46.82673956300183],
|
.add(geo.offset(lotCorner), geo.east(geo.ft(144)), geo.south(geo.ft(51)))
|
||||||
[-100.89891868391628, 46.82666533836659],
|
.add(geo.offset(lotCorner), geo.south(geo.ft(51)));
|
||||||
]);
|
|
||||||
|
|
||||||
const picketFence = new Fence("Short picket fence", {
|
const picketFence = new Fence("Short picket fence", {
|
||||||
fenceType: "Short picket",
|
fenceType: "Short picket",
|
||||||
|
|||||||
@@ -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];
|
||||||
+29
-14
@@ -1,6 +1,6 @@
|
|||||||
import { defineGroup, FlowerBed, Tree } from "../lib/yard-features.js";
|
import { Daylily, defineGroup, FlowerBed, Tree } from "../lib/yard-features.js";
|
||||||
import { east, ft, lngLat, offset } from "../lib/geometry.js";
|
import * as geo from "../lib/geometry.js";
|
||||||
import { topLeftLotCorner } from "./anchors.js";
|
import { lotCorner } from "./anchors.js";
|
||||||
|
|
||||||
export const juniperTreesGroup = defineGroup("group:juniper-trees", "Juniper Trees");
|
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 = [
|
const trees = [
|
||||||
new Tree("Japanese Empress Elm", {
|
new Tree("Japanese Empress Elm", {
|
||||||
species: "Japanese Empress Elm",
|
species: "Japanese Empress Elm",
|
||||||
canopyDiameterMeters: 6,
|
canopyDiameterMeters: 6,
|
||||||
heightMeters: 7,
|
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", {
|
new Tree("Boulevard Linden", {
|
||||||
species: "Boulevard Linden",
|
species: "Boulevard Linden",
|
||||||
canopyDiameterMeters: 3,
|
canopyDiameterMeters: 3,
|
||||||
heightMeters: 6,
|
heightMeters: 6,
|
||||||
plantedDate: "2021-06-08",
|
plantedDate: "2021-06-08",
|
||||||
}).add(lngLat(-100.8990003556445, 46.826776648309774)),
|
}).add(geo.lngLat(-100.8990003556445, 46.826776648309774)),
|
||||||
new Tree("Boulevard Linden 2", {
|
new Tree("Boulevard Linden 2", {
|
||||||
species: "Boulevard Linden",
|
species: "Boulevard Linden",
|
||||||
canopyDiameterMeters: 3,
|
canopyDiameterMeters: 3,
|
||||||
heightMeters: 6,
|
heightMeters: 6,
|
||||||
}).add(lngLat(-100.89898269567485, 46.826709328342595)),
|
}).add(geo.lngLat(-100.89898269567485, 46.826709328342595)),
|
||||||
new Tree("Evergreen", {
|
new Tree("Evergreen", {
|
||||||
species: "Evergreen",
|
species: "Evergreen",
|
||||||
canopyDiameterMeters: 8,
|
canopyDiameterMeters: 8,
|
||||||
heightMeters: 15,
|
heightMeters: 15,
|
||||||
}).add(lngLat(-100.89888268258135, 46.826680107011924)),
|
}).add(geo.lngLat(-100.89888268258135, 46.826680107011924)),
|
||||||
new Tree("Medora Juniper 1", {
|
new Tree("Medora Juniper 1", {
|
||||||
species: "Medora Juniper",
|
species: "Medora Juniper",
|
||||||
canopyDiameterMeters: 1.5,
|
canopyDiameterMeters: 1.5,
|
||||||
heightMeters: 2.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", {
|
new Tree("Medora Juniper 2", {
|
||||||
species: "Medora Juniper",
|
species: "Medora Juniper",
|
||||||
canopyDiameterMeters: 1.5,
|
canopyDiameterMeters: 1.5,
|
||||||
heightMeters: 2.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", {
|
new Tree("Medora Juniper 3", {
|
||||||
species: "Medora Juniper",
|
species: "Medora Juniper",
|
||||||
canopyDiameterMeters: 1.5,
|
canopyDiameterMeters: 1.5,
|
||||||
heightMeters: 2.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", {
|
new Tree("Medora Juniper 4", {
|
||||||
species: "Medora Juniper",
|
species: "Medora Juniper",
|
||||||
canopyDiameterMeters: 1.5,
|
canopyDiameterMeters: 1.5,
|
||||||
heightMeters: 2.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", {
|
new Tree("Medora Juniper 5", {
|
||||||
species: "Medora Juniper",
|
species: "Medora Juniper",
|
||||||
canopyDiameterMeters: 1.5,
|
canopyDiameterMeters: 1.5,
|
||||||
heightMeters: 2.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", {
|
new Tree("Offset Test Tree", {
|
||||||
species: "Test tree",
|
species: "Test tree",
|
||||||
canopyDiameterMeters: 4,
|
canopyDiameterMeters: 4,
|
||||||
heightMeters: 5,
|
heightMeters: 5,
|
||||||
note: "70 feet east of top-left lot corner using assumedNorth",
|
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 plantFeatures = [...flowerBeds, ...daylilies, ...trees];
|
||||||
export const plantGroups = [juniperTreesGroup];
|
export const plantGroups = [juniperTreesGroup];
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const lotAssumedNorthClockwiseDegrees = -11.25369719051117;
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { collectYardData } from "../lib/yard-features.js";
|
import { collectYardData } from "../lib/yard-features.js";
|
||||||
import { boundaryFeatures } from "./boundaries.js";
|
import { boundaryFeatures } from "./boundaries.js";
|
||||||
import { buildingFeatures, buildingGroups } from "./buildings.js";
|
import { buildingFeatures, buildingGroups } from "./buildings.js";
|
||||||
|
import { gridFeatures, gridGroups } from "./grid.js";
|
||||||
import { irrigationFeatures } from "./irrigation.js";
|
import { irrigationFeatures } from "./irrigation.js";
|
||||||
import { pavementFeatures } from "./pavement.js";
|
import { pavementFeatures } from "./pavement.js";
|
||||||
import { plantFeatures, plantGroups } from "./plants.js";
|
import { plantFeatures, plantGroups } from "./plants.js";
|
||||||
@@ -8,6 +9,7 @@ import { plantFeatures, plantGroups } from "./plants.js";
|
|||||||
export const yardGeoJSON = collectYardData(
|
export const yardGeoJSON = collectYardData(
|
||||||
[
|
[
|
||||||
...boundaryFeatures,
|
...boundaryFeatures,
|
||||||
|
...gridFeatures,
|
||||||
...buildingFeatures,
|
...buildingFeatures,
|
||||||
...pavementFeatures,
|
...pavementFeatures,
|
||||||
...plantFeatures,
|
...plantFeatures,
|
||||||
@@ -15,6 +17,7 @@ export const yardGeoJSON = collectYardData(
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
...buildingGroups,
|
...buildingGroups,
|
||||||
|
...gridGroups,
|
||||||
...plantGroups,
|
...plantGroups,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const overlay = document.createElement("section");
|
||||||
|
overlay.className = "error-overlay";
|
||||||
|
overlay.hidden = true;
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="error-overlay-header">
|
||||||
|
<strong>JavaScript error</strong>
|
||||||
|
<div class="error-overlay-actions">
|
||||||
|
<button type="button" data-action="copy">Copy</button>
|
||||||
|
<button type="button" data-action="dismiss" aria-label="Dismiss error">Dismiss</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre></pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
+179
-13
@@ -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 = {
|
export const featureTypes = {
|
||||||
lotBoundary: {
|
lotBoundary: {
|
||||||
label: "Lot Boundary",
|
label: "Lot Boundary",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#7b684d",
|
color: "#7b684d",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -20,14 +13,35 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
fence: {
|
fence: {
|
||||||
label: "Fence",
|
label: "Fence",
|
||||||
|
geometryType: "LineString",
|
||||||
|
detailFields: {
|
||||||
|
fenceType: "string",
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
color: "#705033",
|
color: "#705033",
|
||||||
weight: 4,
|
weight: 4,
|
||||||
opacity: 0.95,
|
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: {
|
house: {
|
||||||
label: "House",
|
label: "House",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#8d5a35",
|
color: "#8d5a35",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -37,6 +51,8 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
garage: {
|
garage: {
|
||||||
label: "Garage",
|
label: "Garage",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#60636b",
|
color: "#60636b",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -46,6 +62,8 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
porch: {
|
porch: {
|
||||||
label: "Porch",
|
label: "Porch",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#aa7148",
|
color: "#aa7148",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -55,6 +73,8 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
deck: {
|
deck: {
|
||||||
label: "Deck",
|
label: "Deck",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#7f5a3c",
|
color: "#7f5a3c",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -64,6 +84,8 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
walkway: {
|
walkway: {
|
||||||
label: "Walkway",
|
label: "Walkway",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#b8b3a6",
|
color: "#b8b3a6",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -73,6 +95,8 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
driveway: {
|
driveway: {
|
||||||
label: "Driveway",
|
label: "Driveway",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
style: {
|
style: {
|
||||||
color: "#9c9890",
|
color: "#9c9890",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -82,6 +106,12 @@ export const featureTypes = {
|
|||||||
},
|
},
|
||||||
flowerBed: {
|
flowerBed: {
|
||||||
label: "Flower Bed",
|
label: "Flower Bed",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {
|
||||||
|
soil: "string",
|
||||||
|
mulch: "string",
|
||||||
|
sunExposure: "string",
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
color: "#7f7f31",
|
color: "#7f7f31",
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@@ -89,18 +119,130 @@ export const featureTypes = {
|
|||||||
fillOpacity: 0.38,
|
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: {
|
tree: {
|
||||||
label: "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) => ({
|
style: (details) => ({
|
||||||
color: "#2f6b3d",
|
color: "#2f6b3d",
|
||||||
weight: 2,
|
weight: 1,
|
||||||
radius: clamp(4 + (details.canopyDiameterMeters ?? 2), treeRadiusRange.min, treeRadiusRange.max),
|
radiusMeters: (details.canopyDiameterMeters ?? 2) / 2,
|
||||||
fillColor: "#5c9e64",
|
fillColor: "#5c9e64",
|
||||||
fillOpacity: 0.85,
|
fillOpacity: 0.55,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
sprinkler: {
|
sprinkler: {
|
||||||
label: "Sprinkler",
|
label: "Sprinkler",
|
||||||
|
geometryType: "Point",
|
||||||
|
detailFields: {
|
||||||
|
zone: "string",
|
||||||
|
headType: "string",
|
||||||
|
flowRateGallonsPerMinute: "number",
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
color: "#2d7dd2",
|
color: "#2d7dd2",
|
||||||
radius: 5,
|
radius: 5,
|
||||||
@@ -119,7 +261,31 @@ export function getFeatureType(kind) {
|
|||||||
return definition;
|
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 = {}) {
|
export function resolveFeatureStyle(kind, details = {}) {
|
||||||
const { style } = getFeatureType(kind);
|
const { style } = getFeatureType(kind);
|
||||||
return typeof style === "function" ? style(details) : style;
|
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);
|
||||||
}
|
}
|
||||||
+96
-26
@@ -1,13 +1,25 @@
|
|||||||
import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
|
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 {
|
class YardFeature {
|
||||||
constructor(kind, geometryType, name, details = {}) {
|
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.kind = kind;
|
||||||
this.geometryType = geometryType;
|
this.geometryType = geometryType;
|
||||||
this.name = name ?? getFeatureType(kind).label;
|
this.name = name ?? featureType.label;
|
||||||
this.id = buildFeatureId(kind, this.name);
|
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.coordinates = [];
|
||||||
this.parentId = null;
|
this.parentId = null;
|
||||||
this.groupIds = [];
|
this.groupIds = [];
|
||||||
@@ -36,6 +48,22 @@ class YardFeature {
|
|||||||
return this;
|
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) {
|
childOf(parent) {
|
||||||
this.parentId = resolveFeatureReference(parent);
|
this.parentId = resolveFeatureReference(parent);
|
||||||
return this;
|
return this;
|
||||||
@@ -49,22 +77,23 @@ class YardFeature {
|
|||||||
|
|
||||||
toGeoJSON() {
|
toGeoJSON() {
|
||||||
const label = getFeatureType(this.kind).label;
|
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") {
|
if (this.geometryType === "Point") {
|
||||||
const [first] = this.coordinates;
|
const [first] = this.coordinates;
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties,
|
||||||
id: this.id,
|
|
||||||
kind: this.kind,
|
|
||||||
label,
|
|
||||||
name: this.name,
|
|
||||||
parentId: this.parentId,
|
|
||||||
groupIds: this.groupIds,
|
|
||||||
details: this.details,
|
|
||||||
style,
|
|
||||||
},
|
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: [first.lon, first.lat],
|
coordinates: [first.lon, first.lat],
|
||||||
@@ -80,16 +109,7 @@ class YardFeature {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties,
|
||||||
id: this.id,
|
|
||||||
kind: this.kind,
|
|
||||||
label,
|
|
||||||
name: this.name,
|
|
||||||
parentId: this.parentId,
|
|
||||||
groupIds: this.groupIds,
|
|
||||||
details: this.details,
|
|
||||||
style,
|
|
||||||
},
|
|
||||||
geometry,
|
geometry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -194,6 +214,10 @@ function resolveCoordinate(values) {
|
|||||||
|
|
||||||
const [first, ...rest] = values;
|
const [first, ...rest] = values;
|
||||||
if (isOffsetAnchor(first) && rest.every(isMove)) {
|
if (isOffsetAnchor(first) && rest.every(isMove)) {
|
||||||
|
if (rest.length === 0) {
|
||||||
|
return first.origin;
|
||||||
|
}
|
||||||
|
|
||||||
return offset(first.origin, ...rest);
|
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 {
|
export class House extends PolygonFeature {
|
||||||
constructor(name = "House", details = {}) {
|
constructor(name = "House", details = {}) {
|
||||||
super("house", name, 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 = {}) {
|
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 = []) {
|
export function collectYardData(features, groups = []) {
|
||||||
const collection = collectGeoJSON(features);
|
const collection = collectGeoJSON(features);
|
||||||
|
collection.schemaVersion = YARD_SCHEMA_VERSION;
|
||||||
|
collection.lifecycleFields = {
|
||||||
|
startDate: "date",
|
||||||
|
endDate: "date",
|
||||||
|
};
|
||||||
|
collection.featureTypes = listFeatureTypeSchemas();
|
||||||
collection.groups = groups.map((group) => ({
|
collection.groups = groups.map((group) => ({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
label: group.label,
|
label: group.label,
|
||||||
|
|||||||
+149
-12
@@ -1,4 +1,5 @@
|
|||||||
import { yardGeoJSON } from "./data/yard.js";
|
import { yardGeoJSON } from "./data/yard.js";
|
||||||
|
import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js";
|
||||||
|
|
||||||
const TILE_MAX_NATIVE_ZOOM = 19;
|
const TILE_MAX_NATIVE_ZOOM = 19;
|
||||||
const MAP_MAX_ZOOM = 24;
|
const MAP_MAX_ZOOM = 24;
|
||||||
@@ -7,6 +8,7 @@ const map = L.map("map", {
|
|||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
maxZoom: MAP_MAX_ZOOM,
|
maxZoom: MAP_MAX_ZOOM,
|
||||||
});
|
});
|
||||||
|
const meterScaledIconMarkers = [];
|
||||||
|
|
||||||
L.control
|
L.control
|
||||||
.zoom({
|
.zoom({
|
||||||
@@ -20,26 +22,55 @@ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
const geoJsonLayer = L.geoJSON(yardGeoJSON, {
|
L.geoJSON(yardGeoJSON, {
|
||||||
pointToLayer(feature, latlng) {
|
pointToLayer(feature, latlng) {
|
||||||
const style = feature.properties.style;
|
const style = getStyle(feature);
|
||||||
return L.circleMarker(latlng, {
|
const options = {
|
||||||
radius: style.radius ?? 6,
|
|
||||||
color: style.color ?? "#333333",
|
color: style.color ?? "#333333",
|
||||||
fillColor: style.fillColor ?? style.color ?? "#333333",
|
fillColor: style.fillColor ?? style.color ?? "#333333",
|
||||||
fillOpacity: style.fillOpacity ?? 0.8,
|
fillOpacity: style.fillOpacity ?? 0.8,
|
||||||
weight: style.weight ?? 2,
|
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) {
|
style(feature) {
|
||||||
return feature.properties.style;
|
if (feature.geometry.type === "Point") {
|
||||||
|
return getPathStyle(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getStyle(feature);
|
||||||
},
|
},
|
||||||
onEachFeature(feature, layer) {
|
onEachFeature(feature, layer) {
|
||||||
layer.bindPopup(buildPopupContent(feature.properties));
|
layer.bindPopup(buildPopupContent(feature.properties));
|
||||||
},
|
},
|
||||||
}).addTo(map);
|
}).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 legend = document.getElementById("legend");
|
||||||
const legendEntries = summarizeTypes(yardGeoJSON.features);
|
const legendEntries = summarizeTypes(yardGeoJSON.features);
|
||||||
@@ -66,10 +97,11 @@ function summarizeTypes(features) {
|
|||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const key = feature.properties.kind;
|
const key = feature.properties.kind;
|
||||||
|
const style = getStyle(feature);
|
||||||
const existing = summary.get(key) ?? {
|
const existing = summary.get(key) ?? {
|
||||||
label: feature.properties.label,
|
label: feature.properties.label,
|
||||||
count: 0,
|
count: 0,
|
||||||
color: feature.properties.style.color ?? "#333333",
|
color: style.color ?? "#333333",
|
||||||
};
|
};
|
||||||
|
|
||||||
existing.count += 1;
|
existing.count += 1;
|
||||||
@@ -79,10 +111,96 @@ function summarizeTypes(features) {
|
|||||||
return [...summary.values()];
|
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) {
|
function buildPopupContent(properties) {
|
||||||
const { label, name, details, parentId, groupIds } = properties;
|
const { kind, label, name, startDate, endDate, details, parentId, groupIds } = properties;
|
||||||
const entries = Object.entries(details ?? {}).filter(([, value]) => value !== null && value !== undefined);
|
const entries = Object.entries(details ?? {}).filter(
|
||||||
|
([key, value]) => value !== null && value !== undefined && !key.endsWith("Url"),
|
||||||
|
);
|
||||||
|
const lifecycleEntries = [];
|
||||||
const relationshipEntries = [];
|
const relationshipEntries = [];
|
||||||
|
const linkEntries = resolveFeatureLinks(kind, details);
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
lifecycleEntries.push(["startDate", startDate]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
lifecycleEntries.push(["endDate", endDate]);
|
||||||
|
}
|
||||||
|
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
relationshipEntries.push(["parentFeature", parentId]);
|
relationshipEntries.push(["parentFeature", parentId]);
|
||||||
@@ -94,16 +212,19 @@ function buildPopupContent(properties) {
|
|||||||
|
|
||||||
const title = `<strong>${escapeHtml(name)}</strong>`;
|
const title = `<strong>${escapeHtml(name)}</strong>`;
|
||||||
const subtitle = name === label ? "" : `<div>${escapeHtml(label)}</div>`;
|
const subtitle = name === label ? "" : `<div>${escapeHtml(label)}</div>`;
|
||||||
const allEntries = [...entries, ...relationshipEntries];
|
const allEntries = [...lifecycleEntries, ...entries, ...relationshipEntries];
|
||||||
if (allEntries.length === 0) {
|
if (allEntries.length === 0 && linkEntries.length === 0) {
|
||||||
return `${title}${subtitle}`;
|
return `${title}${subtitle}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailRows = allEntries
|
const detailRows = allEntries
|
||||||
.map(([key, value]) => `<div><span>${escapeHtml(formatDetailLabel(key))}:</span> ${escapeHtml(formatDetailValue(key, value))}</div>`)
|
.map(([key, value]) => `<div><span>${escapeHtml(formatDetailLabel(key))}:</span> ${escapeHtml(formatDetailValue(key, value))}</div>`)
|
||||||
.join("");
|
.join("");
|
||||||
|
const linkRows = linkEntries
|
||||||
|
.map((link) => `<div><span>${escapeHtml(link.label)}:</span> <a href="${escapeHtml(link.url)}" target="_blank" rel="noreferrer">${escapeHtml(link.url)}</a></div>`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
return `${title}${subtitle}<div>${detailRows}</div>`;
|
return `${title}${subtitle}<div>${detailRows}${linkRows}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDetailLabel(key) {
|
function formatDetailLabel(key) {
|
||||||
@@ -111,8 +232,20 @@ function formatDetailLabel(key) {
|
|||||||
canopyDiameterMeters: "Canopy diameter",
|
canopyDiameterMeters: "Canopy diameter",
|
||||||
heightMeters: "Height",
|
heightMeters: "Height",
|
||||||
plantedDate: "Planted",
|
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",
|
species: "Species",
|
||||||
fenceType: "Fence type",
|
fenceType: "Fence type",
|
||||||
|
axis: "Axis",
|
||||||
|
offsetFeet: "Offset",
|
||||||
|
spacingFeet: "Spacing",
|
||||||
note: "Note",
|
note: "Note",
|
||||||
parentFeature: "Parent",
|
parentFeature: "Parent",
|
||||||
groups: "Groups",
|
groups: "Groups",
|
||||||
@@ -126,6 +259,10 @@ function formatDetailValue(key, value) {
|
|||||||
return `${value} m`;
|
return `${value} m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "offsetFeet" || key === "spacingFeet") {
|
||||||
|
return `${value} ft`;
|
||||||
|
}
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
@@ -125,6 +125,63 @@ main {
|
|||||||
font-family: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", serif;
|
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) {
|
@media (max-width: 900px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
Vendored
+661
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+6
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user