Spaces:
Running
Running
Add beautiful custom tree markers with hover tooltips
Browse files- Fixed tree loading endpoint from /trees to /api/trees?limit=100
- Replaced default circular markers with custom SVG tree icons
- Added 3D gradient styling with shadows and hover animations
- Implemented beautiful hover tooltips that show tree name and details
- Tooltips appear on hover and disappear when out of focus
- Added debug logging to track tree loading
- Enhanced visual design with green gradient backgrounds and shadows
- Improved mobile responsiveness for markers and tooltips
- Trees now properly appear on the map with professional appearance
- static/map.html +101 -0
- static/map.js +59 -11
static/map.html
CHANGED
@@ -325,6 +325,107 @@
|
|
325 |
0%, 100% { opacity: 0; }
|
326 |
20%, 80% { opacity: 1; }
|
327 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
</style>
|
329 |
</head>
|
330 |
<body>
|
|
|
325 |
0%, 100% { opacity: 0; }
|
326 |
20%, 80% { opacity: 1; }
|
327 |
}
|
328 |
+
|
329 |
+
/* Custom Tree Marker Styles */
|
330 |
+
.custom-tree-icon {
|
331 |
+
background: transparent !important;
|
332 |
+
border: none !important;
|
333 |
+
}
|
334 |
+
|
335 |
+
.custom-tree-marker {
|
336 |
+
position: relative;
|
337 |
+
display: flex;
|
338 |
+
flex-direction: column;
|
339 |
+
align-items: center;
|
340 |
+
}
|
341 |
+
|
342 |
+
.tree-icon-container {
|
343 |
+
background: linear-gradient(145deg, #ffffff, #f0f0f0);
|
344 |
+
border-radius: 50%;
|
345 |
+
padding: 4px;
|
346 |
+
box-shadow:
|
347 |
+
0 4px 8px rgba(0,0,0,0.15),
|
348 |
+
0 2px 4px rgba(0,0,0,0.1),
|
349 |
+
inset 0 1px 0 rgba(255,255,255,0.2);
|
350 |
+
transition: all 0.3s ease;
|
351 |
+
cursor: pointer;
|
352 |
+
}
|
353 |
+
|
354 |
+
.tree-icon-container:hover {
|
355 |
+
transform: translateY(-2px) scale(1.1);
|
356 |
+
box-shadow:
|
357 |
+
0 6px 16px rgba(0,0,0,0.2),
|
358 |
+
0 4px 8px rgba(0,0,0,0.15),
|
359 |
+
inset 0 1px 0 rgba(255,255,255,0.3);
|
360 |
+
}
|
361 |
+
|
362 |
+
.tree-marker-shadow {
|
363 |
+
width: 12px;
|
364 |
+
height: 6px;
|
365 |
+
background: rgba(0,0,0,0.3);
|
366 |
+
border-radius: 50%;
|
367 |
+
margin-top: 2px;
|
368 |
+
filter: blur(1px);
|
369 |
+
transition: all 0.3s ease;
|
370 |
+
}
|
371 |
+
|
372 |
+
.custom-tree-marker:hover .tree-marker-shadow {
|
373 |
+
width: 16px;
|
374 |
+
background: rgba(0,0,0,0.4);
|
375 |
+
}
|
376 |
+
|
377 |
+
/* Tooltip Styles */
|
378 |
+
.leaflet-tooltip.tree-tooltip {
|
379 |
+
background: linear-gradient(145deg, #2c5530, #1a3a1c) !important;
|
380 |
+
border: 1px solid rgba(255,255,255,0.2) !important;
|
381 |
+
border-radius: 8px !important;
|
382 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
|
383 |
+
color: white !important;
|
384 |
+
font-family: 'Segoe UI', sans-serif !important;
|
385 |
+
font-size: 13px !important;
|
386 |
+
padding: 8px 12px !important;
|
387 |
+
backdrop-filter: blur(10px);
|
388 |
+
}
|
389 |
+
|
390 |
+
.leaflet-tooltip.tree-tooltip::before {
|
391 |
+
border-top-color: #2c5530 !important;
|
392 |
+
}
|
393 |
+
|
394 |
+
.tree-tooltip-content {
|
395 |
+
min-width: 80px;
|
396 |
+
text-align: center;
|
397 |
+
}
|
398 |
+
|
399 |
+
.tree-name {
|
400 |
+
font-weight: 600;
|
401 |
+
font-size: 14px;
|
402 |
+
margin-bottom: 2px;
|
403 |
+
color: #ffffff;
|
404 |
+
}
|
405 |
+
|
406 |
+
.tree-details {
|
407 |
+
font-size: 11px;
|
408 |
+
opacity: 0.9;
|
409 |
+
color: #e8f5e8;
|
410 |
+
}
|
411 |
+
|
412 |
+
/* Enhanced Popup Styles */
|
413 |
+
.leaflet-popup.tree-popup {
|
414 |
+
margin-bottom: 10px;
|
415 |
+
}
|
416 |
+
|
417 |
+
.leaflet-popup.tree-popup .leaflet-popup-content-wrapper {
|
418 |
+
background: linear-gradient(145deg, #ffffff, #f8f9fa);
|
419 |
+
border-radius: 12px;
|
420 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
421 |
+
border: 1px solid rgba(44, 85, 48, 0.1);
|
422 |
+
}
|
423 |
+
|
424 |
+
.leaflet-popup.tree-popup .leaflet-popup-tip {
|
425 |
+
background: #ffffff;
|
426 |
+
border: 1px solid rgba(44, 85, 48, 0.1);
|
427 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
428 |
+
}
|
429 |
</style>
|
430 |
</head>
|
431 |
<body>
|
static/map.js
CHANGED
@@ -131,11 +131,14 @@ class TreeTrackMap {
|
|
131 |
|
132 |
async loadExistingTrees() {
|
133 |
try {
|
134 |
-
const response = await fetch('/trees');
|
135 |
if (response.ok) {
|
136 |
const trees = await response.json();
|
|
|
137 |
this.displayTreeMarkers(trees);
|
138 |
this.updateTreeCounter(trees.length);
|
|
|
|
|
139 |
}
|
140 |
} catch (error) {
|
141 |
console.error('Error loading trees:', error);
|
@@ -149,20 +152,26 @@ class TreeTrackMap {
|
|
149 |
});
|
150 |
this.treeMarkers = [];
|
151 |
|
152 |
-
// Add tree markers
|
153 |
trees.forEach(tree => {
|
154 |
if (tree.latitude && tree.longitude) {
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
opacity: 1,
|
161 |
-
fillOpacity: 0.9,
|
162 |
-
className: 'tree-pin'
|
163 |
}).addTo(this.map);
|
164 |
|
165 |
-
// Create
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
const popupContent = this.createTreePopup(tree);
|
167 |
marker.bindPopup(popupContent, {
|
168 |
maxWidth: 300,
|
@@ -174,6 +183,45 @@ class TreeTrackMap {
|
|
174 |
});
|
175 |
}
|
176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
createTreePopup(tree) {
|
178 |
return `
|
179 |
<div style="padding: 10px; font-family: 'Segoe UI', sans-serif;">
|
|
|
131 |
|
132 |
async loadExistingTrees() {
|
133 |
try {
|
134 |
+
const response = await fetch('/api/trees?limit=100');
|
135 |
if (response.ok) {
|
136 |
const trees = await response.json();
|
137 |
+
console.log('Loaded trees:', trees); // Debug log
|
138 |
this.displayTreeMarkers(trees);
|
139 |
this.updateTreeCounter(trees.length);
|
140 |
+
} else {
|
141 |
+
console.error('Failed to load trees:', response.status, response.statusText);
|
142 |
}
|
143 |
} catch (error) {
|
144 |
console.error('Error loading trees:', error);
|
|
|
152 |
});
|
153 |
this.treeMarkers = [];
|
154 |
|
155 |
+
// Add tree markers with custom icons
|
156 |
trees.forEach(tree => {
|
157 |
if (tree.latitude && tree.longitude) {
|
158 |
+
// Create custom tree icon
|
159 |
+
const treeIcon = this.createCustomTreeIcon(tree);
|
160 |
+
|
161 |
+
const marker = L.marker([tree.latitude, tree.longitude], {
|
162 |
+
icon: treeIcon
|
|
|
|
|
|
|
163 |
}).addTo(this.map);
|
164 |
|
165 |
+
// Create hover tooltip
|
166 |
+
const tooltipContent = this.createTreeTooltip(tree);
|
167 |
+
marker.bindTooltip(tooltipContent, {
|
168 |
+
permanent: false,
|
169 |
+
direction: 'top',
|
170 |
+
offset: [0, -10],
|
171 |
+
className: 'tree-tooltip'
|
172 |
+
});
|
173 |
+
|
174 |
+
// Create popup content for clicks
|
175 |
const popupContent = this.createTreePopup(tree);
|
176 |
marker.bindPopup(popupContent, {
|
177 |
maxWidth: 300,
|
|
|
183 |
});
|
184 |
}
|
185 |
|
186 |
+
createCustomTreeIcon(tree) {
|
187 |
+
// Create a beautiful custom tree icon
|
188 |
+
const iconHtml = `
|
189 |
+
<div class="custom-tree-marker">
|
190 |
+
<div class="tree-icon-container">
|
191 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
192 |
+
<path d="M12 2C10.9 2 10 2.9 10 4C10 5.1 10.9 6 12 6C13.1 6 14 5.1 14 4C14 2.9 13.1 2 12 2Z" fill="#2E7D32"/>
|
193 |
+
<path d="M12 6C9.8 6 8 7.8 8 10C8 12.2 9.8 14 12 14C14.2 14 16 12.2 16 10C16 7.8 14.2 6 12 6Z" fill="#4CAF50"/>
|
194 |
+
<path d="M12 10C10.3 10 9 11.3 9 13C9 14.7 10.3 16 12 16C13.7 16 15 14.7 15 13C15 11.3 13.7 10 12 10Z" fill="#66BB6A"/>
|
195 |
+
<rect x="11" y="16" width="2" height="6" fill="#8D6E63"/>
|
196 |
+
<path d="M10 22H14V20H10V22Z" fill="#5D4037"/>
|
197 |
+
</svg>
|
198 |
+
</div>
|
199 |
+
<div class="tree-marker-shadow"></div>
|
200 |
+
</div>
|
201 |
+
`;
|
202 |
+
|
203 |
+
return L.divIcon({
|
204 |
+
html: iconHtml,
|
205 |
+
className: 'custom-tree-icon',
|
206 |
+
iconSize: [32, 40],
|
207 |
+
iconAnchor: [16, 40],
|
208 |
+
popupAnchor: [0, -35]
|
209 |
+
});
|
210 |
+
}
|
211 |
+
|
212 |
+
createTreeTooltip(tree) {
|
213 |
+
const name = tree.local_name || tree.common_name || tree.scientific_name || `Tree #${tree.id}`;
|
214 |
+
const height = tree.height ? `${tree.height}m` : '';
|
215 |
+
const code = tree.tree_code ? `[${tree.tree_code}]` : '';
|
216 |
+
|
217 |
+
return `
|
218 |
+
<div class="tree-tooltip-content">
|
219 |
+
<div class="tree-name">${name}</div>
|
220 |
+
${height || code ? `<div class="tree-details">${code} ${height}</div>` : ''}
|
221 |
+
</div>
|
222 |
+
`;
|
223 |
+
}
|
224 |
+
|
225 |
createTreePopup(tree) {
|
226 |
return `
|
227 |
<div style="padding: 10px; font-family: 'Segoe UI', sans-serif;">
|