Files
shed-hunting-map/bear-river-shed-map.html
eric 47fd00d79f Initial commit: interactive shed hunting map for Bear River Range
Single-file HTML app (Leaflet.js) with USGS topo/satellite base layers,
Cache NF boundary, 12 south-facing slope zones, 7 fence crossings,
9 travel corridors, 10 curated hotspot markers, custom waypoint system
with localStorage persistence, GPX/GeoJSON export, GPS tracking,
distance measurement, and species filtering. Mobile-first design
for field use at shed.jfamily.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:40:19 -06:00

1116 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Shed Hunting Map — Bear River Range</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
#map { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1; }
/* Sidebar */
#sidebar {
position: absolute; top: 0; left: 0; bottom: 0; width: 300px;
background: #1a1a2e; color: #e0e0e0; z-index: 1000;
overflow-y: auto; transform: translateX(-100%); transition: transform 0.3s ease;
box-shadow: 4px 0 20px rgba(0,0,0,0.4);
-webkit-overflow-scrolling: touch;
}
#sidebar.open { transform: translateX(0); }
#sidebar-toggle {
position: absolute; top: 10px; left: 10px; z-index: 1001;
width: 44px; height: 44px; border: none; border-radius: 8px;
background: #1a1a2e; color: #fff; font-size: 22px; cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center;
}
#sidebar-toggle:hover { background: #16213e; }
.sidebar-backdrop {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 999;
}
.sidebar-backdrop.active { display: block; }
.sidebar-header {
padding: 16px; background: #16213e; border-bottom: 1px solid #0f3460;
display: flex; justify-content: space-between; align-items: center;
}
.sidebar-header h1 { font-size: 14px; font-weight: 700; color: #e94560; line-height: 1.3; }
.sidebar-close { background: none; border: none; color: #888; font-size: 24px; cursor: pointer; width: 44px; height: 44px; }
.sidebar-section { padding: 12px 16px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.sidebar-section h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #888; margin-bottom: 8px; }
.radio-group label, .check-group label {
display: flex; align-items: center; gap: 8px; padding: 6px 0; cursor: pointer;
font-size: 14px; min-height: 36px;
}
.radio-group input, .check-group input { width: 18px; height: 18px; accent-color: #e94560; flex-shrink: 0; }
.layer-dot {
display: inline-block; width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
}
/* Tools */
.tool-btn {
display: block; width: 100%; padding: 10px 12px; margin: 4px 0;
background: #16213e; border: 1px solid #0f3460; border-radius: 6px;
color: #e0e0e0; font-size: 14px; cursor: pointer; text-align: left;
}
.tool-btn:hover { background: #0f3460; }
.tool-btn.active { background: #e94560; border-color: #e94560; color: #fff; }
/* Legend */
.legend-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 12px; }
.legend-line { width: 24px; height: 3px; flex-shrink: 0; border-radius: 2px; }
.legend-dash { width: 24px; height: 0; border-top: 3px dashed; flex-shrink: 0; }
/* Marker icons */
.hotspot-icon {
width: 28px; height: 28px; border-radius: 50%; border: 2.5px solid #fff;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; color: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
}
.waypoint-icon {
width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
font-size: 22px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
}
/* Popups */
.leaflet-popup-content-wrapper { border-radius: 10px; max-width: 300px; }
.leaflet-popup-content { margin: 12px; font-size: 13px; line-height: 1.5; max-height: 55vh; overflow-y: auto; }
.popup-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.popup-subtitle { font-size: 12px; color: #666; margin-bottom: 8px; }
.species-badge {
display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px;
font-weight: 600; margin-right: 4px; color: #fff;
}
.badge-elk { background: #2d6a4f; }
.badge-deer { background: #9b5de5; }
.priority-high { color: #e94560; font-weight: 600; }
.priority-medium { color: #f4a261; font-weight: 600; }
.popup-section { margin-top: 8px; }
.popup-section summary { font-weight: 600; cursor: pointer; padding: 4px 0; }
.popup-section p { margin-top: 4px; color: #444; font-size: 12px; }
.popup-btn {
display: inline-block; margin-top: 8px; padding: 6px 14px;
background: #e94560; color: #fff; border: none; border-radius: 6px;
font-size: 12px; cursor: pointer;
}
.popup-btn:hover { background: #c73652; }
/* Waypoint form */
.wp-form label { display: block; margin-top: 6px; font-weight: 600; font-size: 12px; }
.wp-form input, .wp-form textarea, .wp-form select {
width: 100%; padding: 6px 8px; margin-top: 2px; border: 1px solid #ccc;
border-radius: 4px; font-size: 14px; font-family: inherit;
}
.wp-form textarea { height: 60px; resize: vertical; }
.wp-form .btn-row { display: flex; gap: 6px; margin-top: 8px; }
/* Coord display */
#coord-display {
position: absolute; bottom: 6px; right: 10px; z-index: 800;
background: rgba(26,26,46,0.85); color: #e0e0e0; padding: 4px 10px;
border-radius: 6px; font-size: 12px; font-family: monospace; pointer-events: none;
}
/* GPS button */
#gps-btn {
position: absolute; bottom: 30px; right: 10px; z-index: 800;
width: 44px; height: 44px; border-radius: 50%; border: none;
background: #1a1a2e; color: #4cc9f0; font-size: 20px; cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center;
}
#gps-btn:hover { background: #16213e; }
#gps-btn.tracking { background: #4cc9f0; color: #1a1a2e; }
/* Welcome modal */
#welcome-modal {
display: none; position: fixed; inset: 0; z-index: 2000;
background: rgba(0,0,0,0.6); align-items: center; justify-content: center;
}
#welcome-modal.show { display: flex; }
.modal-content {
background: #fff; border-radius: 12px; padding: 24px; max-width: 420px;
margin: 16px; max-height: 85vh; overflow-y: auto; color: #333;
}
.modal-content h2 { font-size: 20px; margin-bottom: 12px; color: #1a1a2e; }
.modal-content p, .modal-content li { font-size: 14px; line-height: 1.6; margin-bottom: 8px; }
.modal-content ul { padding-left: 20px; }
.modal-content .warning { background: #fff3cd; padding: 10px; border-radius: 6px; border-left: 4px solid #ffc107; margin: 12px 0; }
.modal-close { display: block; width: 100%; padding: 12px; background: #e94560; color: #fff; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; margin-top: 16px; }
/* Measure control styling */
.leaflet-control-polylinemeasure { background: #1a1a2e !important; border-radius: 8px !important; }
@media (min-width: 768px) {
#sidebar { transform: translateX(0); }
#sidebar-toggle { display: none; }
#map { left: 300px; }
.sidebar-backdrop { display: none !important; }
.sidebar-close { display: none; }
}
</style>
</head>
<body>
<div id="map"></div>
<button id="sidebar-toggle" aria-label="Open menu">&#9776;</button>
<div class="sidebar-backdrop" id="backdrop"></div>
<div id="sidebar">
<div class="sidebar-header">
<h1>SHED HUNTING MAP<br>Bear River Range</h1>
<button class="sidebar-close" id="sidebar-close">&times;</button>
</div>
<div class="sidebar-section">
<h3>Base Map</h3>
<div class="radio-group">
<label><input type="radio" name="base" value="topo" checked> USGS Topo</label>
<label><input type="radio" name="base" value="satellite"> Satellite</label>
<label><input type="radio" name="base" value="osm"> OpenStreetMap</label>
</div>
</div>
<div class="sidebar-section">
<h3>Overlays</h3>
<div class="check-group">
<label><input type="checkbox" id="lyr-forest" checked> <span class="layer-dot" style="background:rgba(34,139,34,0.5)"></span> Cache Nat'l Forest</label>
<label><input type="checkbox" id="lyr-private" checked> <span class="layer-dot" style="background:rgba(220,50,50,0.3)"></span> Private Land</label>
<label><input type="checkbox" id="lyr-south" checked> <span class="layer-dot" style="background:rgba(255,165,0,0.5)"></span> South-Facing Slopes</label>
<label><input type="checkbox" id="lyr-fence"> <span class="layer-dot" style="background:#8B008B"></span> Fence Crossings</label>
<label><input type="checkbox" id="lyr-corridors"> <span class="legend-dash" style="border-color:#4682B4"></span> Travel Corridors</label>
<label><input type="checkbox" id="lyr-hotspots" checked> Hotspot Markers</label>
<label><input type="checkbox" id="lyr-waypoints" checked> My Waypoints</label>
</div>
</div>
<div class="sidebar-section">
<h3>Filter by Species</h3>
<div class="check-group">
<label><input type="checkbox" id="filter-elk" checked> <span class="species-badge badge-elk">ELK</span></label>
<label><input type="checkbox" id="filter-deer" checked> <span class="species-badge badge-deer">DEER</span></label>
</div>
</div>
<div class="sidebar-section">
<h3>Elevation Bands</h3>
<div class="check-group">
<label><input type="checkbox" id="lyr-elk-elev"> <span class="layer-dot" style="background:rgba(0,100,0,0.4)"></span> Elk Range (6,000-7,200 ft)</label>
<label><input type="checkbox" id="lyr-deer-elev"> <span class="layer-dot" style="background:rgba(139,69,19,0.3)"></span> Deer Range (5,000-6,500 ft)</label>
</div>
</div>
<div class="sidebar-section">
<h3>Tools</h3>
<button class="tool-btn" id="btn-waypoint">+ Add Waypoint</button>
<button class="tool-btn" id="btn-measure">Measure Distance</button>
<button class="tool-btn" id="btn-export-gpx">Export Waypoints (GPX)</button>
<button class="tool-btn" id="btn-export-json">Export Waypoints (GeoJSON)</button>
<button class="tool-btn" id="btn-clear-wp" style="color:#e94560">Clear All Waypoints</button>
</div>
<div class="sidebar-section">
<h3>Legend</h3>
<div class="legend-item"><span class="layer-dot" style="background:#DC143C"></span> Canyon Mouth</div>
<div class="legend-item"><span class="layer-dot" style="background:#FF8C00"></span> Bedding Area</div>
<div class="legend-item"><span class="layer-dot" style="background:#228B22"></span> Feeding Area</div>
<div class="legend-item"><span class="layer-dot" style="background:#4682B4"></span> Travel Corridor</div>
<div class="legend-item"><span class="layer-dot" style="background:#8B008B"></span> Fence Crossing</div>
<div class="legend-item"><span class="layer-dot" style="background:#FFD700;border:1px solid #888"></span> My Waypoint</div>
<div class="legend-item"><span class="legend-dash" style="border-color:#FF8C00"></span> South-Facing Slope</div>
<div class="legend-item"><span class="legend-line" style="background:#228B22"></span> National Forest Bdy</div>
</div>
<div class="sidebar-section" style="font-size:11px;color:#666;padding-bottom:20px;">
Bear River Range, Cache County UT<br>
Data derived from USGS 7.5-min quads<br>
Boundaries are approximate
</div>
</div>
<div id="coord-display">--</div>
<button id="gps-btn" title="Center on my location">&#9737;</button>
<div id="welcome-modal">
<div class="modal-content">
<h2>Bear River Range Shed Hunting Map</h2>
<p>Interactive tool for finding shed antlers in the Bear River Range, Cache County, Utah.</p>
<div class="warning">
<strong>Utah Ethics Course Required (Jan 1 - May 31)</strong><br>
Complete the free course at wildlife.utah.gov and carry your certificate. After May 31, no course is needed.
</div>
<ul>
<li>Use the sidebar to toggle map layers and filter by species</li>
<li>Tap hotspot markers for terrain details and tips</li>
<li>Add your own waypoints and export them to GPX for your GPS</li>
<li>Green shading = Cache National Forest (public). Pink = private land.</li>
<li>Orange dashed areas = south-facing slopes (prime shed terrain)</li>
</ul>
<p style="font-size:12px;color:#888;">Tip: View this map on WiFi and pan around to cache tiles for offline use in the field.</p>
<button class="modal-close" id="modal-close">Got It &mdash; Let's Hunt</button>
</div>
</div>
<script>
// ============================================================
// SECTION 1: DATA
// ============================================================
// Cache National Forest boundary (approximate, traced from USGS quads)
// Covers the Bear River Range within the study area
const cacheNFBoundary = {
type: "Feature",
properties: { name: "Cache National Forest (Uinta-Wasatch-Cache)" },
geometry: {
type: "Polygon",
coordinates: [[
// East boundary (foothills, south to north)
[-111.395, 41.755],
[-111.400, 41.770],
[-111.390, 41.775], // Red Pine Canyon mouth indent
[-111.400, 41.780],
[-111.395, 41.790], // Round Valley edge
[-111.385, 41.800], // Dry Canyon / Eagle Canyon mouth
[-111.395, 41.810],
[-111.400, 41.820],
[-111.395, 41.830], // Meadowville foothills
[-111.400, 41.840],
[-111.395, 41.850],
[-111.390, 41.860], // Burnt Fork area
[-111.395, 41.870],
[-111.390, 41.880], // Chubby Hollow area
[-111.385, 41.890],
[-111.380, 41.895], // Garden City Canyon mouth
[-111.390, 41.900],
[-111.385, 41.910],
[-111.380, 41.920],
[-111.385, 41.930], // Swan Creek area
[-111.380, 41.940],
[-111.385, 41.950],
[-111.390, 41.960],
[-111.395, 41.970],
[-111.400, 41.980],
[-111.405, 41.990],
[-111.410, 41.998],
// North boundary (west along ridge)
[-111.430, 42.000],
[-111.460, 41.998],
[-111.490, 41.995],
[-111.510, 41.990],
[-111.530, 41.985],
[-111.550, 41.980],
[-111.570, 41.975],
[-111.590, 41.970],
[-111.610, 41.965],
[-111.625, 41.960],
// West boundary (range crest, north to south)
[-111.620, 41.940],
[-111.615, 41.920],
[-111.620, 41.900],
[-111.625, 41.880],
[-111.620, 41.860],
[-111.615, 41.840],
[-111.610, 41.820],
[-111.605, 41.800],
[-111.600, 41.780],
[-111.595, 41.760],
[-111.590, 41.750],
// South boundary (east along base)
[-111.560, 41.748],
[-111.530, 41.750],
[-111.500, 41.752],
[-111.470, 41.753],
[-111.440, 41.754],
[-111.420, 41.755],
[-111.395, 41.755]
]]
}
};
// Private land (inverted polygon — everything OUTSIDE the NF boundary)
const privateLand = {
type: "Feature",
properties: { name: "Private Land" },
geometry: {
type: "Polygon",
coordinates: [
// Outer ring (big bounding box)
[[-111.65, 41.70], [-111.30, 41.70], [-111.30, 42.05], [-111.65, 42.05], [-111.65, 41.70]],
// Inner ring (NF boundary, reversed winding)
cacheNFBoundary.geometry.coordinates[0].slice().reverse()
]
}
};
// South-facing slope zones (identified from topo quads)
const southFacingSlopes = [
{
name: "Garden City Canyon — North Wall",
why: "The north wall of Garden City Canyon faces directly south. Winter sun exposure melts snow early, creating prime bedding. Elk and deer travel the canyon floor and bed on these warm slopes.",
elevation: "5,800 - 6,800 ft",
coords: [[-111.440, 41.900], [-111.430, 41.902], [-111.420, 41.900], [-111.410, 41.898], [-111.400, 41.896], [-111.400, 41.892], [-111.410, 41.893], [-111.420, 41.895], [-111.430, 41.897], [-111.440, 41.900]]
},
{
name: "Chubby Hollow — South-Facing Benches",
why: "Series of south-facing benches above Chubby Hollow. Protected from north wind by the ridge behind. Sagebrush browse and scattered timber provide food and cover. Classic elk wintering habitat.",
elevation: "6,200 - 7,000 ft",
coords: [[-111.460, 41.885], [-111.450, 41.888], [-111.440, 41.886], [-111.430, 41.883], [-111.430, 41.878], [-111.440, 41.880], [-111.450, 41.882], [-111.460, 41.885]]
},
{
name: "Swan Creek — South Aspect",
why: "South-facing fingers along the north side of Swan Creek drainage. Lower elevation makes these some of the earliest spots to shed. Agricultural fields at the canyon mouth draw deer down from the forest.",
elevation: "5,600 - 6,400 ft",
coords: [[-111.440, 41.945], [-111.430, 41.948], [-111.420, 41.946], [-111.410, 41.943], [-111.410, 41.938], [-111.420, 41.940], [-111.430, 41.942], [-111.440, 41.945]]
},
{
name: "Dry Canyon — North Wall",
why: "Steep south-facing wall on the north side of Dry Canyon. Contour lines are tight here — deer use the benches and ledges. The junction with Eagle Canyon concentrates movement.",
elevation: "5,800 - 6,400 ft",
coords: [[-111.445, 41.810], [-111.435, 41.812], [-111.425, 41.810], [-111.415, 41.808], [-111.415, 41.803], [-111.425, 41.805], [-111.435, 41.807], [-111.445, 41.810]]
},
{
name: "Eagle Canyon — South Exposure",
why: "Eagle Canyon runs roughly east-west, with excellent south-facing exposure on its north wall. Less accessible than Dry Canyon, meaning less hunting pressure and more sheds left behind.",
elevation: "6,000 - 6,600 ft",
coords: [[-111.450, 41.795], [-111.440, 41.797], [-111.430, 41.795], [-111.425, 41.792], [-111.425, 41.788], [-111.430, 41.789], [-111.440, 41.791], [-111.450, 41.795]]
},
{
name: "Temple Peak — South Slopes",
why: "Large south-facing bowl below Temple Peak. Higher elevation elk habitat with scattered aspen and conifer. Bulls push up here in late winter as snow recedes. Look for sheds March-April.",
elevation: "6,800 - 7,400 ft",
coords: [[-111.535, 41.825], [-111.525, 41.830], [-111.515, 41.828], [-111.505, 41.823], [-111.505, 41.815], [-111.515, 41.818], [-111.525, 41.822], [-111.535, 41.825]]
},
{
name: "Red Pine Canyon — South Face",
why: "South-facing slopes above Red Pine Canyon. Mixed conifer and aspen forest with open meadow pockets. Travel corridor between Cache Valley and the range interior.",
elevation: "6,000 - 6,800 ft",
coords: [[-111.470, 41.775], [-111.460, 41.778], [-111.450, 41.776], [-111.440, 41.773], [-111.440, 41.768], [-111.450, 41.770], [-111.460, 41.772], [-111.470, 41.775]]
},
{
name: "Bear Hollow — Timber Pockets",
why: "South-facing timber pockets in Bear Hollow. Thick cover provides bedding, and the south aspect keeps conditions milder. Elk bed in the timber and feed on open south-facing sagebrush above.",
elevation: "6,500 - 7,200 ft",
coords: [[-111.500, 41.865], [-111.490, 41.868], [-111.480, 41.866], [-111.470, 41.862], [-111.470, 41.857], [-111.480, 41.859], [-111.490, 41.862], [-111.500, 41.865]]
},
{
name: "Burnt Fork / South Sink",
why: "South-facing slopes between Burnt Fork and South Sink. Mix of open sage and scattered timber. The sinks (limestone depressions) create sheltered pockets where animals bed. Both elk and deer use this area.",
elevation: "6,200 - 6,800 ft",
coords: [[-111.460, 41.875], [-111.450, 41.877], [-111.440, 41.875], [-111.435, 41.872], [-111.435, 41.868], [-111.440, 41.869], [-111.450, 41.871], [-111.460, 41.875]]
},
{
name: "Left Hand Fork — South Aspect",
why: "South-facing wall of Left Hand Fork / Pine Canyon area. Good vehicle access from the forest road below. South-facing sagebrush hillsides transition to timber higher up.",
elevation: "6,000 - 6,600 ft",
coords: [[-111.460, 41.850], [-111.450, 41.853], [-111.440, 41.851], [-111.430, 41.848], [-111.430, 41.843], [-111.440, 41.845], [-111.450, 41.847], [-111.460, 41.850]]
},
{
name: "North Sink — South-Facing Bowl",
why: "South-facing bowl at North Sink. Limestone terrain creates natural depressions where animals shelter from wind. South-facing exposure keeps snow shallow. Good elk sign on the topo.",
elevation: "6,400 - 7,000 ft",
coords: [[-111.465, 41.895], [-111.455, 41.898], [-111.445, 41.896], [-111.440, 41.892], [-111.440, 41.888], [-111.445, 41.889], [-111.455, 41.892], [-111.465, 41.895]]
},
{
name: "Meadowville Western Foothills",
why: "Lower elevation south-facing foothills above Meadowville. Mule deer concentrate here in late winter at the ag-forest interface. Earliest shed drops in the study area — check late February.",
elevation: "5,200 - 5,800 ft",
coords: [[-111.410, 41.840], [-111.405, 41.845], [-111.395, 41.843], [-111.390, 41.838], [-111.390, 41.833], [-111.395, 41.835], [-111.405, 41.837], [-111.410, 41.840]]
}
];
// Fence crossing points (where NF boundary crosses drainages)
const fenceCrossings = [
{
name: "Garden City Canyon Fence",
why: "NF boundary crosses the canyon bottom. Barbed wire fence forces animals to jump — the jolt knocks antlers loose. Search both sides of the fence line, 50 yards in each direction.",
coords: [[-111.385, 41.893], [-111.378, 41.897]]
},
{
name: "Swan Creek Fence Line",
why: "Forest boundary crosses Swan Creek drainage. Deer and elk cross here moving between private ag land and NF winter range. Check for antlers along the fence in both directions.",
coords: [[-111.388, 41.938], [-111.382, 41.942]]
},
{
name: "Dry Canyon Mouth Fence",
why: "Fence line at the mouth of Dry Canyon where NF boundary crosses. Animals funnel through the narrow canyon mouth. High-probability shed location.",
coords: [[-111.390, 41.802], [-111.383, 41.806]]
},
{
name: "Eagle Canyon Fence",
why: "NF boundary fence crossing at Eagle Canyon mouth. Less foot traffic than Dry Canyon means sheds linger longer. Check thoroughly.",
coords: [[-111.392, 41.790], [-111.385, 41.793]]
},
{
name: "Red Pine Canyon Fence",
why: "Forest boundary fence at the Red Pine Canyon drainage crossing. Animals move between private ranch land in the valley and NF slopes.",
coords: [[-111.395, 41.772], [-111.388, 41.776]]
},
{
name: "Meadowville Foothills Fence",
why: "NF boundary fence along the Meadowville foothills. Mule deer cross between hay fields and forest cover. Best checked late February through March for fresh drops.",
coords: [[-111.398, 41.835], [-111.392, 41.838]]
},
{
name: "Burnt Fork Fence Crossing",
why: "Fence line where NF boundary crosses near Burnt Fork drainage. Both elk and deer use this crossing. South-facing slopes on both sides.",
coords: [[-111.393, 41.862], [-111.387, 41.866]]
}
];
// Canyon / drainage travel corridors
const corridors = [
{ name: "Garden City Canyon", coords: [[-111.460, 41.895], [-111.440, 41.896], [-111.420, 41.895], [-111.400, 41.893], [-111.385, 41.895], [-111.375, 41.895]] },
{ name: "Swan Creek", coords: [[-111.450, 41.940], [-111.430, 41.942], [-111.410, 41.940], [-111.390, 41.938], [-111.375, 41.942]] },
{ name: "Chubby Hollow", coords: [[-111.455, 41.880], [-111.440, 41.878], [-111.425, 41.876], [-111.410, 41.878]] },
{ name: "Left Hand Fork / Pine Canyon", coords: [[-111.460, 41.845], [-111.445, 41.847], [-111.430, 41.845], [-111.415, 41.846]] },
{ name: "Dry Canyon", coords: [[-111.455, 41.803], [-111.440, 41.805], [-111.425, 41.803], [-111.410, 41.800], [-111.390, 41.800]] },
{ name: "Eagle Canyon", coords: [[-111.460, 41.788], [-111.445, 41.790], [-111.430, 41.788], [-111.415, 41.786]] },
{ name: "Red Pine Canyon", coords: [[-111.470, 41.770], [-111.455, 41.772], [-111.440, 41.770], [-111.420, 41.768], [-111.400, 41.770]] },
{ name: "Beaver Creek", coords: [[-111.540, 41.850], [-111.525, 41.855], [-111.510, 41.860], [-111.495, 41.858]] },
{ name: "Bear Hollow", coords: [[-111.510, 41.860], [-111.495, 41.862], [-111.480, 41.860], [-111.465, 41.858]] }
];
// Hotspot markers
const hotspots = [
{
name: "Garden City Canyon Mouth",
lat: 41.895, lng: -111.393,
type: "canyon_mouth", species: ["elk", "deer"],
elevation: "5,600 ft", priority: "high",
description: "Major drainage funnel at the mouth of Garden City Canyon. Animals travel the canyon floor between Cache NF winter range and lower-elevation browse. The NF boundary fence here forces animals to jump, knocking antlers loose.",
terrain: "Canyon narrows with sagebrush and scattered aspen. South-facing wall above provides bedding. Road access from Garden City makes this an easy starting point.",
access: "Drive Garden City Canyon road from town. Park at the NF boundary gate. Walk the canyon bottom and both hillsides.",
tips: "Check both sides of the fence line for 100 yards. Walk into the sun in the morning to catch tine shadows. This spot gets moderate pressure — go early in the season."
},
{
name: "Chubby Hollow South Slopes",
lat: 41.882, lng: -111.445,
type: "bedding", species: ["elk"],
elevation: "6,200 - 7,000 ft", priority: "high",
description: "Series of south-facing benches and timber pockets above Chubby Hollow. Protected from north wind by the ridge. Classic elk wintering habitat with browse and cover.",
terrain: "Sagebrush-covered south-facing benches transitioning to conifer at higher elevation. Look for oval bed depressions in the timber edges and matted grass patches.",
access: "Access via forest road up from Garden City Canyon area. Moderate hike to reach the upper benches. 4WD recommended for the approach road.",
tips: "Grid walk the benches 20-30 yards apart. Elk bed in timber but shed on the open sage slopes when they stand. Focus where timber meets sage."
},
{
name: "Swan Creek Drainage",
lat: 41.940, lng: -111.418,
type: "travel_corridor", species: ["elk", "deer"],
elevation: "5,800 - 6,500 ft", priority: "medium",
description: "Major drainage on the north end of the study area. Both elk and deer use Swan Creek as a travel corridor. Agricultural fields at the mouth draw animals down from the forest.",
terrain: "Wide drainage with mixed terrain — open meadows, aspen groves, and sagebrush hillsides. The south-facing slopes above the north side of the creek are the main target.",
access: "Approach from the north end of Bear Lake or from Garden City area. Check NF boundary access points.",
tips: "Less pressure than Garden City Canyon. The ag-forest interface at the mouth is excellent for deer sheds in late February."
},
{
name: "Dry Canyon / Eagle Canyon Junction",
lat: 41.800, lng: -111.420,
type: "canyon_mouth", species: ["deer"],
elevation: "5,800 - 6,400 ft", priority: "high",
description: "Two major drainages converge here, concentrating mule deer movement. South-facing canyon walls provide prime winter bedding. Less accessible than northern areas — fewer hunters, more sheds.",
terrain: "Steep canyon walls with sagebrush and scattered aspen. The junction creates a natural funnel. Contour lines are tight — use caution on steeper slopes.",
access: "Approach from Meadowville area. Forest roads may be gated in winter/early spring. Be prepared for a longer hike.",
tips: "Walk the ridgeline between the two canyons for a panoramic view, then glass the south-facing slopes with binoculars before committing to a canyon."
},
{
name: "Temple Peak South Slopes",
lat: 41.822, lng: -111.520,
type: "bedding", species: ["elk"],
elevation: "6,800 - 7,400 ft", priority: "medium",
description: "Large south-facing bowl below Temple Peak. Higher elevation elk habitat. Bulls push up here as spring progresses and snow recedes on south aspects. Later-season spot (March-April).",
terrain: "Open south-facing bowl with scattered aspen and conifer. Alpine meadow pockets. Snow lingers longer at this elevation — time your visits to catch snowmelt.",
access: "Deep backcountry access. Long approach hike from forest roads to the east. Recommend horseback or ATV where allowed. Full-day commitment.",
tips: "This is trophy elk country. The higher elevation means later shed drops. Glass the open bowls from ridgelines. Mark any finds on GPS to build a pattern over years."
},
{
name: "Red Pine Canyon",
lat: 41.770, lng: -111.450,
type: "travel_corridor", species: ["elk", "deer"],
elevation: "6,000 - 6,800 ft", priority: "medium",
description: "Major travel corridor between Cache Valley and the Bear River Range interior. Mixed conifer and aspen forest with open meadow pockets. Both elk and deer use this drainage.",
terrain: "Moderate canyon with good trail access. South-facing slopes above the north side of the canyon. Meadow openings along the creek bottom are good feeding areas.",
access: "Approach from the south end of the study area. Check forest road conditions — may require early-season 4WD.",
tips: "Walk the trail along the creek bottom, then make side excursions up onto south-facing benches. Fence crossing at the NF boundary is a must-check spot."
},
{
name: "Bear Hollow Timber Pockets",
lat: 41.862, lng: -111.485,
type: "bedding", species: ["elk"],
elevation: "6,500 - 7,000 ft", priority: "high",
description: "South-facing timber pockets in Bear Hollow. Thick conifer cover provides excellent bedding habitat. Elk bed in the timber edges and feed on open sagebrush slopes above.",
terrain: "Transition zone between timber and open sage. Look for concentrated droppings, matted grass, and oval bed depressions in the timber margins.",
access: "Mid-range backcountry. Forest road approach, then moderate hike. Best accessed from the central part of the range.",
tips: "Antlers often pop off when elk stand from their beds. Search the timber edges systematically — grid walk 15-20 yards apart in thick cover. After finding one side, circle tight 100-200 yards for the match."
},
{
name: "Burnt Fork / South Sink",
lat: 41.870, lng: -111.448,
type: "feeding", species: ["elk", "deer"],
elevation: "6,200 - 6,800 ft", priority: "medium",
description: "Feeding area between Burnt Fork and South Sink. Mix of open sage and scattered timber. The limestone sinks create sheltered pockets where animals concentrate.",
terrain: "Rolling sagebrush hills with limestone depressions (sinks). The sinks provide wind protection and warmer micro-climates. Both elk and deer use the same terrain here.",
access: "Accessible from forest roads in the central study area. Moderate terrain — walkable without technical difficulty.",
tips: "Walk the rims of the sinks and check inside them. Animals drop antlers in sheltered depressions. A mix of brown and white antlers here = reliable year-after-year drop zone."
},
{
name: "Round Valley Ag-Forest Edge",
lat: 41.790, lng: -111.392,
type: "feeding", species: ["deer"],
elevation: "5,200 - 5,800 ft", priority: "high",
description: "Agricultural fields along the western edge of Round Valley meet the Cache NF foothills. Mule deer concentrate at this interface in winter, feeding on crop stubble and south-slope browse.",
terrain: "Gentle transition from flat ag land to rolling sagebrush foothills. Hay meadows and crop stubble draw deer. The first sagebrush-covered ridges above the fields are prime bedding.",
access: "Easy access from roads near Meadowville and Round Valley. Some areas may be private — verify boundaries on onX Maps. Get written permission for any private land.",
tips: "Lowest elevation spot = earliest shed drops. Start checking late February. Walk fence lines between private fields and NF land. Deer jump these fences daily."
},
{
name: "Meadowville Foothills",
lat: 41.833, lng: -111.398,
type: "fence_crossing", species: ["deer"],
elevation: "5,000 - 5,500 ft", priority: "medium",
description: "NF boundary fence along the Meadowville foothills. Mule deer cross between private hay fields and forest cover. The fence line itself is a linear shed magnet.",
terrain: "Gentle sage-covered foothills. Easy walking. The NF boundary is well-defined with fence posts. Private land below, public forest above.",
access: "Very easy access from Meadowville. Park near town and walk up into the foothills. Stay on public NF land above the fence or get landowner permission below.",
tips: "Walk the fence line in both directions for as far as you can. Check 50 yards on each side. Morning sun angle helps — walk west to east so the sun is at your back, casting tine shadows ahead of you."
}
];
// Marker type colors
const typeColors = {
canyon_mouth: '#DC143C',
bedding: '#FF8C00',
feeding: '#228B22',
travel_corridor: '#4682B4',
fence_crossing: '#8B008B'
};
const typeSymbols = {
canyon_mouth: '\u25BC',
bedding: '\u25CE',
feeding: '\u25C6',
travel_corridor: '\u2794',
fence_crossing: '\u2016'
};
// ============================================================
// SECTION 2: MAP INIT + TILE LAYERS
// ============================================================
const map = L.map('map', {
center: [41.875, -111.47],
zoom: 12,
minZoom: 10,
maxZoom: 18,
maxBounds: [[41.70, -111.65], [42.05, -111.30]],
zoomControl: true
});
const baseLayers = {
topo: L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', {
attribution: 'USGS',
maxZoom: 16
}),
satellite: L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}', {
attribution: 'USGS',
maxZoom: 16
}),
osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
maxZoom: 19
})
};
baseLayers.topo.addTo(map);
// Base layer switching
document.querySelectorAll('input[name="base"]').forEach(radio => {
radio.addEventListener('change', e => {
Object.values(baseLayers).forEach(l => map.removeLayer(l));
baseLayers[e.target.value].addTo(map);
});
});
L.control.scale({ imperial: true, metric: true, position: 'bottomleft' }).addTo(map);
// ============================================================
// SECTION 3: OVERLAY LAYERS
// ============================================================
// Cache NF boundary
const forestLayer = L.geoJSON(cacheNFBoundary, {
style: { color: '#228B22', weight: 2.5, fillColor: 'rgba(34,139,34,0.12)', fillOpacity: 1, dashArray: null }
}).addTo(map);
// Private land overlay
const privateLayer = L.geoJSON(privateLand, {
style: { color: 'transparent', weight: 0, fillColor: '#DC3232', fillOpacity: 0.06 }
}).addTo(map);
// South-facing slopes
const slopeFeatures = southFacingSlopes.map(s => ({
type: "Feature",
properties: { name: s.name, why: s.why, elevation: s.elevation },
geometry: { type: "Polygon", coordinates: [s.coords] }
}));
const slopeLayer = L.geoJSON({ type: "FeatureCollection", features: slopeFeatures }, {
style: { color: '#FF8C00', weight: 1.5, dashArray: '6,4', fillColor: '#FFA500', fillOpacity: 0.18 },
onEachFeature: (feature, layer) => {
layer.on('mouseover', () => layer.setStyle({ fillOpacity: 0.35 }));
layer.on('mouseout', () => layer.setStyle({ fillOpacity: 0.18 }));
layer.bindPopup(`
<div class="popup-title">${feature.properties.name}</div>
<div class="popup-subtitle">South-Facing Slope &bull; ${feature.properties.elevation}</div>
<p style="font-size:12px;color:#444;margin-top:6px;">${feature.properties.why}</p>
`);
}
}).addTo(map);
// Fence crossings
const fenceFeatures = fenceCrossings.map(f => ({
type: "Feature",
properties: { name: f.name, why: f.why },
geometry: { type: "LineString", coordinates: f.coords }
}));
const fenceLayer = L.geoJSON({ type: "FeatureCollection", features: fenceFeatures }, {
style: { color: '#8B008B', weight: 4, dashArray: '8,6', opacity: 0.9 },
onEachFeature: (feature, layer) => {
layer.bindPopup(`
<div class="popup-title">${feature.properties.name}</div>
<div class="popup-subtitle">Fence Crossing — Prime Shed Location</div>
<p style="font-size:12px;color:#444;margin-top:6px;">${feature.properties.why}</p>
`);
}
});
// Travel corridors
const corridorFeatures = corridors.map(c => ({
type: "Feature",
properties: { name: c.name },
geometry: { type: "LineString", coordinates: c.coords }
}));
const corridorLayer = L.geoJSON({ type: "FeatureCollection", features: corridorFeatures }, {
style: { color: '#4682B4', weight: 2, dashArray: '8,6', opacity: 0.7 },
onEachFeature: (feature, layer) => {
layer.bindPopup(`<div class="popup-title">${feature.properties.name}</div><div class="popup-subtitle">Travel Corridor / Drainage</div>`);
}
});
// Hotspot markers
const hotspotMarkers = L.layerGroup();
hotspots.forEach(h => {
const icon = L.divIcon({
className: '',
html: `<div class="hotspot-icon" style="background:${typeColors[h.type]}">${typeSymbols[h.type]}</div>`,
iconSize: [28, 28],
iconAnchor: [14, 14]
});
const speciesBadges = h.species.map(s =>
`<span class="species-badge badge-${s}">${s.toUpperCase()}</span>`
).join('');
const priorityClass = h.priority === 'high' ? 'priority-high' : 'priority-medium';
const popup = `
<div class="popup-title">${h.name}</div>
<div class="popup-subtitle">${speciesBadges} &bull; ${h.elevation} &bull; <span class="${priorityClass}">${h.priority.toUpperCase()} PRIORITY</span></div>
<p style="margin-top:6px;">${h.description}</p>
<details class="popup-section"><summary>Terrain Details</summary><p>${h.terrain}</p></details>
<details class="popup-section"><summary>Access & Parking</summary><p>${h.access}</p></details>
<details class="popup-section"><summary>Tips</summary><p>${h.tips}</p></details>
<button class="popup-btn" onclick="saveHotspotAsWaypoint(${h.lat}, ${h.lng}, '${h.name.replace(/'/g, "\\'")}')">Save as Waypoint</button>
`;
const marker = L.marker([h.lat, h.lng], { icon }).bindPopup(popup, { maxWidth: 300 });
marker._shedData = h;
hotspotMarkers.addLayer(marker);
});
hotspotMarkers.addTo(map);
// Elevation bands (off by default)
const elkElevBand = L.polygon([
[41.998, -111.625], [41.998, -111.410], [41.750, -111.410], [41.750, -111.590]
], { color: 'transparent', fillColor: '#006400', fillOpacity: 0.08, interactive: false });
const deerElevBand = L.polygon([
[41.998, -111.410], [41.998, -111.375], [41.750, -111.375], [41.750, -111.410]
], { color: 'transparent', fillColor: '#8B4513', fillOpacity: 0.08, interactive: false });
// ============================================================
// SECTION 4: LAYER CONTROL + SIDEBAR
// ============================================================
const layerMap = {
'lyr-forest': forestLayer,
'lyr-private': privateLayer,
'lyr-south': slopeLayer,
'lyr-fence': fenceLayer,
'lyr-corridors': corridorLayer,
'lyr-hotspots': hotspotMarkers,
'lyr-elk-elev': elkElevBand,
'lyr-deer-elev': deerElevBand
};
Object.keys(layerMap).forEach(id => {
const cb = document.getElementById(id);
if (!cb) return;
cb.addEventListener('change', () => {
if (cb.checked) map.addLayer(layerMap[id]);
else map.removeLayer(layerMap[id]);
});
// Sync initial state
if (!cb.checked && map.hasLayer(layerMap[id])) map.removeLayer(layerMap[id]);
if (cb.checked && !map.hasLayer(layerMap[id])) map.addLayer(layerMap[id]);
});
// Species filter
function applySpeciesFilter() {
const showElk = document.getElementById('filter-elk').checked;
const showDeer = document.getElementById('filter-deer').checked;
hotspotMarkers.eachLayer(marker => {
const sp = marker._shedData.species;
const visible = (!showElk && !showDeer) ||
(showElk && sp.includes('elk')) ||
(showDeer && sp.includes('deer'));
marker.setOpacity(visible ? 1 : 0.15);
if (!visible) marker.closePopup();
});
}
document.getElementById('filter-elk').addEventListener('change', applySpeciesFilter);
document.getElementById('filter-deer').addEventListener('change', applySpeciesFilter);
// Sidebar toggle
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('backdrop');
document.getElementById('sidebar-toggle').addEventListener('click', () => {
sidebar.classList.add('open');
backdrop.classList.add('active');
});
function closeSidebar() {
sidebar.classList.remove('open');
backdrop.classList.remove('active');
}
document.getElementById('sidebar-close').addEventListener('click', closeSidebar);
backdrop.addEventListener('click', closeSidebar);
// ============================================================
// SECTION 5: WAYPOINT SYSTEM
// ============================================================
const WP_KEY = 'shedHuntingWaypoints';
const waypointLayer = L.layerGroup().addTo(map);
let addingWaypoint = false;
function loadWaypoints() {
try { return JSON.parse(localStorage.getItem(WP_KEY)) || []; }
catch { return []; }
}
function saveWaypoints(wps) {
localStorage.setItem(WP_KEY, JSON.stringify(wps));
}
function renderWaypoints() {
waypointLayer.clearLayers();
const wps = loadWaypoints();
wps.forEach(wp => {
const icon = L.divIcon({
className: '',
html: '<div class="waypoint-icon">\u2B50</div>',
iconSize: [30, 30],
iconAnchor: [15, 15]
});
const marker = L.marker([wp.lat, wp.lng], { icon, draggable: true })
.bindPopup(() => createWaypointPopup(wp));
marker.on('dragend', e => {
const pos = e.target.getLatLng();
const wps = loadWaypoints();
const w = wps.find(w => w.id === wp.id);
if (w) { w.lat = pos.lat; w.lng = pos.lng; saveWaypoints(wps); }
});
waypointLayer.addLayer(marker);
});
}
function createWaypointPopup(wp) {
const div = document.createElement('div');
div.className = 'wp-form';
div.innerHTML = `
<label>Name<input type="text" id="wp-name-${wp.id}" value="${wp.name || ''}"></label>
<label>Notes<textarea id="wp-notes-${wp.id}">${wp.notes || ''}</textarea></label>
<label>Species
<select id="wp-species-${wp.id}">
<option value="" ${!wp.species ? 'selected' : ''}>--</option>
<option value="elk" ${wp.species === 'elk' ? 'selected' : ''}>Elk</option>
<option value="deer" ${wp.species === 'deer' ? 'selected' : ''}>Deer</option>
<option value="both" ${wp.species === 'both' ? 'selected' : ''}>Both</option>
</select>
</label>
<label>Date<input type="date" id="wp-date-${wp.id}" value="${wp.date || ''}"></label>
<div class="btn-row">
<button class="popup-btn" onclick="updateWaypoint('${wp.id}')">Save</button>
<button class="popup-btn" style="background:#666" onclick="deleteWaypoint('${wp.id}')">Delete</button>
</div>
<div style="font-size:11px;color:#888;margin-top:6px;">${wp.lat.toFixed(5)}N, ${Math.abs(wp.lng).toFixed(5)}W</div>
`;
return div;
}
window.updateWaypoint = function(id) {
const wps = loadWaypoints();
const wp = wps.find(w => w.id === id);
if (!wp) return;
wp.name = document.getElementById(`wp-name-${id}`).value;
wp.notes = document.getElementById(`wp-notes-${id}`).value;
wp.species = document.getElementById(`wp-species-${id}`).value;
wp.date = document.getElementById(`wp-date-${id}`).value;
saveWaypoints(wps);
renderWaypoints();
};
window.deleteWaypoint = function(id) {
if (!confirm('Delete this waypoint?')) return;
const wps = loadWaypoints().filter(w => w.id !== id);
saveWaypoints(wps);
renderWaypoints();
};
window.saveHotspotAsWaypoint = function(lat, lng, name) {
const wps = loadWaypoints();
wps.push({
id: 'wp_' + Date.now(),
lat, lng, name,
notes: 'Saved from hotspot marker',
species: '', date: new Date().toISOString().slice(0, 10)
});
saveWaypoints(wps);
renderWaypoints();
map.closePopup();
};
// Click to add waypoint
const wpBtn = document.getElementById('btn-waypoint');
wpBtn.addEventListener('click', () => {
addingWaypoint = !addingWaypoint;
wpBtn.classList.toggle('active', addingWaypoint);
map.getContainer().style.cursor = addingWaypoint ? 'crosshair' : '';
});
map.on('click', e => {
if (!addingWaypoint) return;
addingWaypoint = false;
wpBtn.classList.remove('active');
map.getContainer().style.cursor = '';
const wps = loadWaypoints();
const newWp = {
id: 'wp_' + Date.now(),
lat: e.latlng.lat, lng: e.latlng.lng,
name: '', notes: '', species: '',
date: new Date().toISOString().slice(0, 10)
};
wps.push(newWp);
saveWaypoints(wps);
renderWaypoints();
// Open the popup for the new waypoint
waypointLayer.eachLayer(m => {
const pos = m.getLatLng();
if (Math.abs(pos.lat - newWp.lat) < 0.0001 && Math.abs(pos.lng - newWp.lng) < 0.0001) {
m.openPopup();
}
});
});
// Waypoint layer toggle
document.getElementById('lyr-waypoints').addEventListener('change', function() {
if (this.checked) map.addLayer(waypointLayer);
else map.removeLayer(waypointLayer);
});
// Clear all waypoints
document.getElementById('btn-clear-wp').addEventListener('click', () => {
if (!confirm('Clear ALL your waypoints? This cannot be undone.')) return;
saveWaypoints([]);
renderWaypoints();
});
renderWaypoints();
// ============================================================
// SECTION 6: EXPORT
// ============================================================
function downloadFile(content, filename, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
document.getElementById('btn-export-gpx').addEventListener('click', () => {
const wps = loadWaypoints();
if (!wps.length) return alert('No waypoints to export.');
const wpts = wps.map(w => ` <wpt lat="${w.lat}" lon="${w.lng}">
<name>${w.name || 'Waypoint'}</name>
<desc>${(w.notes || '').replace(/&/g,'&amp;').replace(/</g,'&lt;')}</desc>
<time>${w.date || ''}</time>
</wpt>`).join('\n');
const gpx = `<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="shed.jfamily.io">
${wpts}
</gpx>`;
downloadFile(gpx, 'shed-waypoints.gpx', 'application/gpx+xml');
});
document.getElementById('btn-export-json').addEventListener('click', () => {
const wps = loadWaypoints();
if (!wps.length) return alert('No waypoints to export.');
const fc = {
type: "FeatureCollection",
features: wps.map(w => ({
type: "Feature",
properties: { name: w.name, notes: w.notes, species: w.species, date: w.date },
geometry: { type: "Point", coordinates: [w.lng, w.lat] }
}))
};
downloadFile(JSON.stringify(fc, null, 2), 'shed-waypoints.geojson', 'application/geo+json');
});
// ============================================================
// SECTION 7: MEASUREMENT, GPS, COORD DISPLAY, WELCOME
// ============================================================
// Simple measurement tool (no external plugin — built-in)
let measuring = false;
let measurePoints = [];
let measureLine = null;
let measureMarkers = [];
const measureBtn = document.getElementById('btn-measure');
measureBtn.addEventListener('click', () => {
measuring = !measuring;
measureBtn.classList.toggle('active', measuring);
if (measuring) {
map.getContainer().style.cursor = 'crosshair';
measurePoints = [];
if (measureLine) { map.removeLayer(measureLine); measureLine = null; }
measureMarkers.forEach(m => map.removeLayer(m));
measureMarkers = [];
} else {
map.getContainer().style.cursor = '';
}
});
map.on('click', e => {
if (!measuring) return;
measurePoints.push(e.latlng);
const dot = L.circleMarker(e.latlng, { radius: 5, color: '#e94560', fillColor: '#e94560', fillOpacity: 1 }).addTo(map);
measureMarkers.push(dot);
if (measurePoints.length > 1) {
if (measureLine) map.removeLayer(measureLine);
measureLine = L.polyline(measurePoints, { color: '#e94560', weight: 2, dashArray: '6,4' }).addTo(map);
let totalMeters = 0;
for (let i = 1; i < measurePoints.length; i++) {
totalMeters += measurePoints[i - 1].distanceTo(measurePoints[i]);
}
const miles = (totalMeters * 0.000621371).toFixed(2);
const feet = Math.round(totalMeters * 3.28084);
const label = L.tooltip({ permanent: true, direction: 'top', className: '' })
.setLatLng(e.latlng)
.setContent(`<b>${miles} mi</b> (${feet.toLocaleString()} ft)`)
.addTo(map);
measureMarkers.push(label);
}
});
// Double-click to finish measuring
map.on('dblclick', e => {
if (measuring) {
measuring = false;
measureBtn.classList.remove('active');
map.getContainer().style.cursor = '';
e.originalEvent.preventDefault();
e.originalEvent.stopPropagation();
}
});
// Coordinate display
const coordDisplay = document.getElementById('coord-display');
map.on('mousemove', e => {
coordDisplay.textContent = `${e.latlng.lat.toFixed(5)}N, ${Math.abs(e.latlng.lng).toFixed(5)}W`;
});
map.on('touchmove', e => {
if (e.latlng) coordDisplay.textContent = `${e.latlng.lat.toFixed(5)}N, ${Math.abs(e.latlng.lng).toFixed(5)}W`;
});
// GPS location
let gpsMarker = null;
let gpsCircle = null;
let gpsWatchId = null;
const gpsBtn = document.getElementById('gps-btn');
gpsBtn.addEventListener('click', () => {
if (gpsWatchId !== null) {
navigator.geolocation.clearWatch(gpsWatchId);
gpsWatchId = null;
if (gpsMarker) { map.removeLayer(gpsMarker); gpsMarker = null; }
if (gpsCircle) { map.removeLayer(gpsCircle); gpsCircle = null; }
gpsBtn.classList.remove('tracking');
return;
}
if (!navigator.geolocation) return alert('Geolocation not supported.');
gpsBtn.classList.add('tracking');
gpsWatchId = navigator.geolocation.watchPosition(pos => {
const latlng = [pos.coords.latitude, pos.coords.longitude];
const acc = pos.coords.accuracy;
if (!gpsMarker) {
gpsMarker = L.circleMarker(latlng, {
radius: 8, color: '#4cc9f0', fillColor: '#4cc9f0', fillOpacity: 1, weight: 3
}).addTo(map);
gpsCircle = L.circle(latlng, { radius: acc, color: '#4cc9f0', fillOpacity: 0.1, weight: 1 }).addTo(map);
map.setView(latlng, 14);
} else {
gpsMarker.setLatLng(latlng);
gpsCircle.setLatLng(latlng).setRadius(acc);
}
}, err => {
alert('GPS error: ' + err.message);
gpsBtn.classList.remove('tracking');
gpsWatchId = null;
}, { enableHighAccuracy: true, maximumAge: 10000 });
});
// Welcome modal
const modal = document.getElementById('welcome-modal');
if (!localStorage.getItem('shedMapWelcomeSeen')) {
modal.classList.add('show');
}
document.getElementById('modal-close').addEventListener('click', () => {
modal.classList.remove('show');
localStorage.setItem('shedMapWelcomeSeen', 'true');
});
</script>
</body>
</html>