Refactor to use routes, make POIs linkable
This commit is contained in:
@@ -77,14 +77,17 @@ export default class MapComponent extends Component {
|
||||
// Listen to changes in the /places/ scope
|
||||
// keeping this as a backup or for future real-time sync support
|
||||
this.storage.rs.scope('/places/').on('change', (event) => {
|
||||
console.log('RemoteStorage change detected:', event);
|
||||
// this.loadBookmarks(); // Disabling auto-update for now per instructions, using explicit version action instead
|
||||
console.log('RemoteStorage change detected:', event);
|
||||
// this.loadBookmarks(); // Disabling auto-update for now per instructions, using explicit version action instead
|
||||
});
|
||||
});
|
||||
|
||||
// Re-fetch bookmarks when the version changes (triggered by parent action)
|
||||
updateBookmarks = modifier((element, [version]) => {
|
||||
this.loadBookmarks();
|
||||
// Re-fetch bookmarks when the version changes (triggered by parent action or service)
|
||||
updateBookmarks = modifier(() => {
|
||||
// Depend on the tracked storage.version
|
||||
if (this.storage.version >= 0) {
|
||||
this.loadBookmarks();
|
||||
}
|
||||
});
|
||||
|
||||
async loadBookmarks() {
|
||||
@@ -94,21 +97,21 @@ export default class MapComponent extends Component {
|
||||
this.bookmarkSource.clear();
|
||||
|
||||
if (places && Array.isArray(places)) {
|
||||
places.forEach(place => {
|
||||
places.forEach((place) => {
|
||||
if (place.lat && place.lon) {
|
||||
const feature = new Feature({
|
||||
geometry: new Point(fromLonLat([place.lon, place.lat])),
|
||||
name: place.title,
|
||||
id: place.id,
|
||||
isBookmark: true, // Marker property to distinguish
|
||||
originalPlace: place
|
||||
originalPlace: place,
|
||||
});
|
||||
this.bookmarkSource.addFeature(feature);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load bookmarks:", e);
|
||||
console.error('Failed to load bookmarks:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,43 +123,46 @@ export default class MapComponent extends Component {
|
||||
let selectedFeatureType = null;
|
||||
|
||||
if (features && features.length > 0) {
|
||||
const bookmarkFeature = features.find(f => f.get('isBookmark'));
|
||||
if (bookmarkFeature) {
|
||||
clickedBookmark = bookmarkFeature.get('originalPlace');
|
||||
}
|
||||
// Also get visual props for standard map click logic later
|
||||
const props = features[0].getProperties();
|
||||
if (props.name) {
|
||||
selectedFeatureName = props.name;
|
||||
selectedFeatureType = props.class || props.subclass;
|
||||
}
|
||||
const bookmarkFeature = features.find((f) => f.get('isBookmark'));
|
||||
if (bookmarkFeature) {
|
||||
clickedBookmark = bookmarkFeature.get('originalPlace');
|
||||
}
|
||||
// Also get visual props for standard map click logic later
|
||||
const props = features[0].getProperties();
|
||||
if (props.name) {
|
||||
selectedFeatureName = props.name;
|
||||
selectedFeatureType = props.class || props.subclass;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling when sidebar is OPEN
|
||||
if (this.args.isSidebarOpen) {
|
||||
// If it's a bookmark, we allow "switching" to it even if sidebar is open
|
||||
if (clickedBookmark) {
|
||||
console.log("Clicked bookmark while sidebar open (switching):", clickedBookmark);
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound([], clickedBookmark);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If it's a bookmark, we allow "switching" to it even if sidebar is open
|
||||
if (clickedBookmark) {
|
||||
console.log(
|
||||
'Clicked bookmark while sidebar open (switching):',
|
||||
clickedBookmark
|
||||
);
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound([], clickedBookmark);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise (empty map or non-bookmark feature), close the sidebar
|
||||
if (this.args.onOutsideClick) {
|
||||
this.args.onOutsideClick();
|
||||
}
|
||||
return;
|
||||
// Otherwise (empty map or non-bookmark feature), close the sidebar
|
||||
if (this.args.onOutsideClick) {
|
||||
this.args.onOutsideClick();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal behavior (sidebar is closed)
|
||||
if (clickedBookmark) {
|
||||
console.log("Clicked bookmark:", clickedBookmark);
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound([], clickedBookmark);
|
||||
}
|
||||
return;
|
||||
console.log('Clicked bookmark:', clickedBookmark);
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound([], clickedBookmark);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = toLonLat(event.coordinate);
|
||||
@@ -170,46 +176,63 @@ export default class MapComponent extends Component {
|
||||
let pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
|
||||
|
||||
// Sort by distance from click
|
||||
pois = pois.map(p => {
|
||||
// Use center lat/lon for ways/relations if available, else lat/lon
|
||||
const pLat = p.lat || p.center?.lat;
|
||||
const pLon = p.lon || p.center?.lon;
|
||||
return {
|
||||
...p,
|
||||
_distance: (pLat && pLon) ? getDistance(lat, lon, pLat, pLon) : 9999
|
||||
};
|
||||
}).sort((a, b) => a._distance - b._distance);
|
||||
pois = pois
|
||||
.map((p) => {
|
||||
// Use center lat/lon for ways/relations if available, else lat/lon
|
||||
const pLat = p.lat || p.center?.lat;
|
||||
const pLon = p.lon || p.center?.lon;
|
||||
return {
|
||||
...p,
|
||||
_distance: pLat && pLon ? getDistance(lat, lon, pLat, pLon) : 9999,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a._distance - b._distance);
|
||||
|
||||
let matchedPlace = null;
|
||||
|
||||
if (selectedFeatureName && pois.length > 0) {
|
||||
// Heuristic:
|
||||
// 1. Exact Name Match
|
||||
matchedPlace = pois.find(p => p.tags && (p.tags.name === selectedFeatureName || p.tags['name:en'] === selectedFeatureName));
|
||||
matchedPlace = pois.find(
|
||||
(p) =>
|
||||
p.tags &&
|
||||
(p.tags.name === selectedFeatureName ||
|
||||
p.tags['name:en'] === selectedFeatureName)
|
||||
);
|
||||
|
||||
// 2. If no exact match, look for VERY close (<=20m) and matching type
|
||||
if (!matchedPlace) {
|
||||
const topCandidate = pois[0];
|
||||
if (topCandidate._distance <= 20) {
|
||||
// Check type compatibility if available
|
||||
// (visual tile 'class' is often 'cafe', osm tag is 'amenity'='cafe')
|
||||
const pType = topCandidate.tags.amenity || topCandidate.tags.shop || topCandidate.tags.tourism;
|
||||
if (selectedFeatureType && pType && (selectedFeatureType === pType || pType.includes(selectedFeatureType))) {
|
||||
console.log("Heuristic match found (distance + type):", topCandidate);
|
||||
matchedPlace = topCandidate;
|
||||
} else if (topCandidate._distance <= 10) {
|
||||
// Even without type match, if it's super close (<=10m), it's likely the one.
|
||||
console.log("Heuristic match found (proximity):", topCandidate);
|
||||
matchedPlace = topCandidate;
|
||||
}
|
||||
}
|
||||
const topCandidate = pois[0];
|
||||
if (topCandidate._distance <= 20) {
|
||||
// Check type compatibility if available
|
||||
// (visual tile 'class' is often 'cafe', osm tag is 'amenity'='cafe')
|
||||
const pType =
|
||||
topCandidate.tags.amenity ||
|
||||
topCandidate.tags.shop ||
|
||||
topCandidate.tags.tourism;
|
||||
if (
|
||||
selectedFeatureType &&
|
||||
pType &&
|
||||
(selectedFeatureType === pType ||
|
||||
pType.includes(selectedFeatureType))
|
||||
) {
|
||||
console.log(
|
||||
'Heuristic match found (distance + type):',
|
||||
topCandidate
|
||||
);
|
||||
matchedPlace = topCandidate;
|
||||
} else if (topCandidate._distance <= 10) {
|
||||
// Even without type match, if it's super close (<=10m), it's likely the one.
|
||||
console.log('Heuristic match found (proximity):', topCandidate);
|
||||
matchedPlace = topCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound(pois, matchedPlace);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch POIs:', error);
|
||||
}
|
||||
@@ -218,7 +241,7 @@ export default class MapComponent extends Component {
|
||||
<template>
|
||||
<div
|
||||
{{this.setupMap}}
|
||||
{{this.updateBookmarks @bookmarksVersion}}
|
||||
{{this.updateBookmarks}}
|
||||
style="position: absolute; inset: 0;"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { on } from '@ember/modifier';
|
||||
import { fn } from '@ember/helper';
|
||||
import or from 'ember-truth-helpers/helpers/or';
|
||||
@@ -22,7 +21,7 @@ export default class PlacesSidebar extends Component {
|
||||
if (this.args.onSelect) {
|
||||
this.args.onSelect(null);
|
||||
}
|
||||
|
||||
|
||||
// Fallback logic: if no list available, close sidebar
|
||||
if (!this.args.places || this.args.places.length === 0) {
|
||||
if (this.args.onClose) {
|
||||
@@ -47,13 +46,35 @@ export default class PlacesSidebar extends Component {
|
||||
if (this.args.onBookmarkChange) {
|
||||
this.args.onBookmarkChange();
|
||||
}
|
||||
|
||||
|
||||
// Update selection to the new saved place object
|
||||
// This updates the local UI state immediately without a route refresh
|
||||
if (this.args.onUpdate) {
|
||||
// When deleting, we revert to a "fresh" object or just close.
|
||||
// Since we close the sidebar below, we might not strictly need to update local state,
|
||||
// but it's good practice.
|
||||
// Reconstruct the "original" place without ID/Geohash/CreatedAt
|
||||
const freshPlace = {
|
||||
...place,
|
||||
id: undefined,
|
||||
geohash: undefined,
|
||||
createdAt: undefined
|
||||
};
|
||||
this.args.onUpdate(freshPlace);
|
||||
}
|
||||
|
||||
// Also fire onSelect if it exists (for list view)
|
||||
if (this.args.onSelect) {
|
||||
// Similar logic for select if needed, but we usually close.
|
||||
this.args.onSelect(null);
|
||||
}
|
||||
|
||||
// Close sidebar after delete
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose();
|
||||
}
|
||||
} else {
|
||||
alert('Cannot delete: Missing ID or Geohash');
|
||||
alert('Cannot delete: Missing ID or Geohash');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to delete:', e);
|
||||
@@ -66,18 +87,23 @@ export default class PlacesSidebar extends Component {
|
||||
title: place.tags.name || place.tags['name:en'] || 'Untitled Place',
|
||||
lat: place.lat,
|
||||
lon: place.lon,
|
||||
tags: [],
|
||||
tags: [],
|
||||
url: place.tags.website,
|
||||
osmId: String(place.id),
|
||||
osmId: String(place.osmId || place.id), // Ensure we grab osmId if available, or fallback to id
|
||||
};
|
||||
|
||||
try {
|
||||
const savedPlace = await this.storage.places.store(placeData);
|
||||
console.log('Place saved:', placeData.title);
|
||||
|
||||
|
||||
// Notify parent to refresh map bookmarks
|
||||
if (this.args.onBookmarkChange) {
|
||||
this.args.onBookmarkChange();
|
||||
this.args.onBookmarkChange();
|
||||
}
|
||||
|
||||
// Update selection to the new saved place object
|
||||
if (this.args.onUpdate) {
|
||||
this.args.onUpdate(savedPlace);
|
||||
}
|
||||
|
||||
// Update selection to the new saved place object
|
||||
@@ -95,44 +121,86 @@ export default class PlacesSidebar extends Component {
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
{{#if @selectedPlace}}
|
||||
<button type="button" class="back-btn" {{on "click" this.clearSelection}}>←</button>
|
||||
<h2>Details</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="back-btn"
|
||||
{{on "click" this.clearSelection}}
|
||||
>←</button>
|
||||
<h2>Details</h2>
|
||||
{{else}}
|
||||
<h2>Nearby Places</h2>
|
||||
<h2>Nearby Places</h2>
|
||||
{{/if}}
|
||||
<button type="button" class="close-btn" {{on "click" @onClose}}>×</button>
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn"
|
||||
{{on "click" @onClose}}
|
||||
>×</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sidebar-content">
|
||||
{{#if @selectedPlace}}
|
||||
<div class="place-details">
|
||||
<h3>{{or @selectedPlace.title @selectedPlace.tags.name @selectedPlace.tags.name:en "Unnamed Place"}}</h3>
|
||||
<h3>{{or
|
||||
@selectedPlace.title
|
||||
@selectedPlace.tags.name
|
||||
@selectedPlace.tags.name:en
|
||||
"Unnamed Place"
|
||||
}}</h3>
|
||||
<p class="place-meta">
|
||||
{{#if @selectedPlace.tags.amenity}}
|
||||
{{or @selectedPlace.tags.amenity @selectedPlace.tags.shop @selectedPlace.tags.tourism}}
|
||||
{{or
|
||||
@selectedPlace.tags.amenity
|
||||
@selectedPlace.tags.shop
|
||||
@selectedPlace.tags.tourism
|
||||
}}
|
||||
{{else}}
|
||||
{{@selectedPlace.description}}
|
||||
{{@selectedPlace.description}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
|
||||
{{#if (or @selectedPlace.url @selectedPlace.tags.website)}}
|
||||
<p><a href={{or @selectedPlace.url @selectedPlace.tags.website}} target="_blank" rel="noopener noreferrer">Website</a></p>
|
||||
<p><a
|
||||
href={{or @selectedPlace.url @selectedPlace.tags.website}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Website</a></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if @selectedPlace.tags.opening_hours}}
|
||||
<p><strong>Open:</strong> {{@selectedPlace.tags.opening_hours}}</p>
|
||||
<p><strong>Open:</strong>
|
||||
{{@selectedPlace.tags.opening_hours}}</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class={{if @selectedPlace.createdAt "btn-secondary" "btn-primary"}} {{on "click" (fn this.toggleSave @selectedPlace)}}>
|
||||
{{if @selectedPlace.createdAt "Saved ✓" "Save"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={{if
|
||||
@selectedPlace.createdAt
|
||||
"btn-secondary"
|
||||
"btn-primary"
|
||||
}}
|
||||
{{on "click" (fn this.toggleSave @selectedPlace)}}
|
||||
>
|
||||
{{if @selectedPlace.createdAt "Saved ✓" "Save"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="meta-info">
|
||||
{{#if (or @selectedPlace.osmId @selectedPlace.id)}}
|
||||
<p><small>OSM ID: <a href="https://www.openstreetmap.org/{{if @selectedPlace.type @selectedPlace.type 'node'}}/{{or @selectedPlace.osmId @selectedPlace.id}}" target="_blank" rel="noopener noreferrer">{{or @selectedPlace.osmId @selectedPlace.id}}</a></small></p>
|
||||
{{/if}}
|
||||
{{#if (or @selectedPlace.osmId @selectedPlace.id)}}
|
||||
<p><small>OSM ID:
|
||||
<a
|
||||
href="https://www.openstreetmap.org/{{if
|
||||
@selectedPlace.type
|
||||
@selectedPlace.type
|
||||
'node'
|
||||
}}/{{or @selectedPlace.osmId @selectedPlace.id}}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{or
|
||||
@selectedPlace.osmId
|
||||
@selectedPlace.id
|
||||
}}</a></small></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
@@ -140,9 +208,22 @@ export default class PlacesSidebar extends Component {
|
||||
<ul class="places-list">
|
||||
{{#each @places as |place|}}
|
||||
<li>
|
||||
<button type="button" class="place-item" {{on "click" (fn this.selectPlace place)}}>
|
||||
<div class="place-name">{{or place.tags.name place.tags.name:en "Unnamed Place"}}</div>
|
||||
<div class="place-type">{{or place.tags.amenity place.tags.shop place.tags.tourism "Point of Interest"}}</div>
|
||||
<button
|
||||
type="button"
|
||||
class="place-item"
|
||||
{{on "click" (fn this.selectPlace place)}}
|
||||
>
|
||||
<div class="place-name">{{or
|
||||
place.tags.name
|
||||
place.tags.name:en
|
||||
"Unnamed Place"
|
||||
}}</div>
|
||||
<div class="place-type">{{or
|
||||
place.tags.amenity
|
||||
place.tags.shop
|
||||
place.tags.tourism
|
||||
"Point of Interest"
|
||||
}}</div>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
||||
Reference in New Issue
Block a user