Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2087cfc4f7
|
|||
|
8572032481
|
|||
|
b4c3f5c88d
|
|||
|
cff19980d5
|
|||
|
cf251f702b
|
|||
|
d2eb888dcf
|
|||
|
a0b4a4b3f3
|
|||
|
cb3ee48909
|
|||
|
1d022b21bd
|
|||
|
3e831a7686
|
@@ -782,14 +782,19 @@ export default class MapComponent extends Component {
|
|||||||
// Check if mobile (width <= 768px matches CSS)
|
// Check if mobile (width <= 768px matches CSS)
|
||||||
if (size[0] <= 768) {
|
if (size[0] <= 768) {
|
||||||
// On mobile, the bottom 50% is covered by the sheet.
|
// On mobile, the bottom 50% is covered by the sheet.
|
||||||
// We want the pin to be in the center of the TOP 50% (visible area).
|
// We want the pin to be in the center of the TOP 50% (visible area), minus the header.
|
||||||
// That means the pin should be at y = height * 0.25 (25% down from top).
|
|
||||||
// The map center is at y = height * 0.50.
|
|
||||||
// So the pin is "above" the center by 25% of the height in pixels.
|
|
||||||
// To put the pin there, the map center needs to be "below" the pin by that amount.
|
|
||||||
|
|
||||||
const height = size[1];
|
const height = size[1];
|
||||||
const offsetPixels = height * 0.25; // Distance from desired pin pos to map center
|
const headerEl = document.querySelector('.app-header');
|
||||||
|
const headerHeight = headerEl ? headerEl.offsetHeight : 60;
|
||||||
|
|
||||||
|
// Visible area is from headerHeight to height / 2 (bottom sheet covers bottom 50%)
|
||||||
|
const visibleCenterY = headerHeight + (height / 2 - headerHeight) / 2;
|
||||||
|
|
||||||
|
// The map center is at y = height * 0.50.
|
||||||
|
// So the pin is "above" the center by (height/2 - visibleCenterY) pixels.
|
||||||
|
// To put the pin there, the map center needs to be "below" the pin by that amount.
|
||||||
|
const offsetPixels = height / 2 - visibleCenterY; // Distance from desired pin pos to map center
|
||||||
const offsetMapUnits = offsetPixels * resolution;
|
const offsetMapUnits = offsetPixels * resolution;
|
||||||
|
|
||||||
// Shift center SOUTH (decrease Y).
|
// Shift center SOUTH (decrease Y).
|
||||||
@@ -849,6 +854,9 @@ export default class MapComponent extends Component {
|
|||||||
let targetPixelY = pixel[1];
|
let targetPixelY = pixel[1];
|
||||||
let needsPan = false;
|
let needsPan = false;
|
||||||
|
|
||||||
|
const headerEl = document.querySelector('.app-header');
|
||||||
|
const headerHeight = headerEl ? headerEl.offsetHeight : 60;
|
||||||
|
|
||||||
// 1. Mobile Bottom Sheet Logic (Screen <= 768px)
|
// 1. Mobile Bottom Sheet Logic (Screen <= 768px)
|
||||||
if (size[0] <= 768) {
|
if (size[0] <= 768) {
|
||||||
const height = size[1];
|
const height = size[1];
|
||||||
@@ -856,7 +864,7 @@ export default class MapComponent extends Component {
|
|||||||
|
|
||||||
// If in bottom half
|
// If in bottom half
|
||||||
if (pixel[1] > splitPoint) {
|
if (pixel[1] > splitPoint) {
|
||||||
targetPixelY = height * 0.25; // Target: Center of top half
|
targetPixelY = headerHeight + (height / 2 - headerHeight) / 2; // Target: Center of visible area above bottom sheet
|
||||||
needsPan = true;
|
needsPan = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -877,11 +885,10 @@ export default class MapComponent extends Component {
|
|||||||
|
|
||||||
// 3. Header Logic (Any screen size)
|
// 3. Header Logic (Any screen size)
|
||||||
// Check if the (potentially new) target Y is under the header
|
// Check if the (potentially new) target Y is under the header
|
||||||
const headerHeight = 60;
|
const minTopDistance = headerHeight + 20; // Provide some padding
|
||||||
const minTopDistance = headerHeight + 20; // 80px
|
|
||||||
|
|
||||||
if (targetPixelY < minTopDistance) {
|
if (targetPixelY < minTopDistance) {
|
||||||
targetPixelY = minTopDistance + 30; // Move it to ~110px, clear of header
|
targetPixelY = minTopDistance + 30; // Move it clear of header
|
||||||
needsPan = true;
|
needsPan = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,6 +1151,8 @@ export default class MapComponent extends Component {
|
|||||||
this.mapUi.returnToSearch = true;
|
this.mapUi.returnToSearch = true;
|
||||||
}
|
}
|
||||||
this.mapUi.preventNextZoom = true;
|
this.mapUi.preventNextZoom = true;
|
||||||
|
this.mapUi.selectPlace(place, { preventZoom: true });
|
||||||
|
this.mapUi.showSidebar();
|
||||||
this.router.transitionTo('place', place);
|
this.router.transitionTo('place', place);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
import { getDistance } from '../utils/geo';
|
||||||
|
|
||||||
export default class SearchController extends Controller {
|
export default class SearchController extends Controller {
|
||||||
|
@service osm;
|
||||||
|
@service photon;
|
||||||
|
@service mapUi;
|
||||||
|
@service storage;
|
||||||
|
@service router;
|
||||||
|
@service toast;
|
||||||
|
|
||||||
queryParams = ['lat', 'lon', 'q', 'selected', 'category'];
|
queryParams = ['lat', 'lon', 'q', 'selected', 'category'];
|
||||||
|
|
||||||
lat = null;
|
lat = null;
|
||||||
@@ -8,4 +18,175 @@ export default class SearchController extends Controller {
|
|||||||
q = null;
|
q = null;
|
||||||
selected = null;
|
selected = null;
|
||||||
category = null;
|
category = null;
|
||||||
|
|
||||||
|
fetchResultsTask = task({ restartable: true }, async (params) => {
|
||||||
|
// Hide sidebar and clear previous results immediately to signal a new search
|
||||||
|
this.mapUi.hideSidebar();
|
||||||
|
this.mapUi.clearSearchResults();
|
||||||
|
|
||||||
|
const lat = params.lat ? parseFloat(params.lat) : null;
|
||||||
|
const lon = params.lon ? parseFloat(params.lon) : null;
|
||||||
|
let pois = [];
|
||||||
|
let loadingType = null;
|
||||||
|
let loadingValue = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Case 0: Category Search (category parameter present)
|
||||||
|
if (params.category && lat && lon) {
|
||||||
|
loadingType = 'category';
|
||||||
|
loadingValue = params.category;
|
||||||
|
this.mapUi.startLoading(loadingType, loadingValue);
|
||||||
|
|
||||||
|
// We need bounds. If we have active map state, use it.
|
||||||
|
let bounds = this.mapUi.currentBounds;
|
||||||
|
|
||||||
|
// If we don't have bounds (direct URL visit), estimate them from lat/lon/zoom(16)
|
||||||
|
// or just use a fixed box around the center.
|
||||||
|
if (!bounds) {
|
||||||
|
// Approximate 0.01 degrees ~ 1km at equator. A viewport is roughly 0.02x0.01 at zoom 16?
|
||||||
|
// Let's take a safe box of ~1km radius.
|
||||||
|
const delta = 0.01;
|
||||||
|
bounds = {
|
||||||
|
minLat: lat - delta,
|
||||||
|
maxLat: lat + delta,
|
||||||
|
minLon: lon - delta,
|
||||||
|
maxLon: lon + delta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pois = await this.osm.getCategoryPois(
|
||||||
|
bounds,
|
||||||
|
params.category,
|
||||||
|
lat,
|
||||||
|
lon
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort by distance from center
|
||||||
|
pois = pois
|
||||||
|
.map((p) => ({
|
||||||
|
...p,
|
||||||
|
_distance: getDistance(lat, lon, p.lat, p.lon),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a._distance - b._distance);
|
||||||
|
}
|
||||||
|
// Case 1: Text Search (q parameter present)
|
||||||
|
else if (params.q) {
|
||||||
|
loadingType = 'text';
|
||||||
|
loadingValue = params.q;
|
||||||
|
this.mapUi.startLoading(loadingType, loadingValue);
|
||||||
|
|
||||||
|
// Search with Photon (using lat/lon for bias if available)
|
||||||
|
pois = await this.photon.search(params.q, lat, lon);
|
||||||
|
|
||||||
|
// Search local bookmarks by name
|
||||||
|
const queryLower = params.q.toLowerCase();
|
||||||
|
const localMatches = this.storage.savedPlaces.filter((p) => {
|
||||||
|
return (
|
||||||
|
p.title?.toLowerCase().includes(queryLower) ||
|
||||||
|
p.description?.toLowerCase().includes(queryLower)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge local matches
|
||||||
|
localMatches.forEach((local) => {
|
||||||
|
const exists = pois.find(
|
||||||
|
(poi) =>
|
||||||
|
(local.osmId && poi.osmId === local.osmId) ||
|
||||||
|
(poi.id && poi.id === local.id)
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
pois.push(local);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Case 2: Nearby Search (lat/lon present, no q)
|
||||||
|
else if (lat && lon) {
|
||||||
|
// Nearby search does NOT trigger loading state (pulse is used instead)
|
||||||
|
const searchRadius = 50; // Default radius
|
||||||
|
|
||||||
|
// Fetch POIs from Overpass
|
||||||
|
pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
|
||||||
|
|
||||||
|
// Get cached/saved places in search radius
|
||||||
|
const localMatches = this.storage.savedPlaces.filter((p) => {
|
||||||
|
const dist = getDistance(lat, lon, p.lat, p.lon);
|
||||||
|
return dist <= searchRadius;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge local matches
|
||||||
|
localMatches.forEach((local) => {
|
||||||
|
const exists = pois.find(
|
||||||
|
(poi) =>
|
||||||
|
(local.osmId && poi.osmId === local.osmId) ||
|
||||||
|
(poi.id && poi.id === local.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
pois.push(local);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by distance from click
|
||||||
|
pois = pois
|
||||||
|
.map((p) => {
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
_distance: getDistance(lat, lon, p.lat, p.lon),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a._distance - b._distance);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search request failed.', error);
|
||||||
|
this.toast.show('Search request failed. Please try again.');
|
||||||
|
this.mapUi.stopSearch();
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
if (loadingType && loadingValue) {
|
||||||
|
this.mapUi.stopLoading(loadingType, loadingValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any of these are already bookmarked
|
||||||
|
// We resolve them to the bookmark version if they exist
|
||||||
|
pois = pois.map((p) => {
|
||||||
|
const saved = this.storage.findPlaceById(p.osmId);
|
||||||
|
return saved || p;
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetName = params.selected || params.q;
|
||||||
|
|
||||||
|
if (targetName && pois.length > 0) {
|
||||||
|
let matchedPlace = null;
|
||||||
|
|
||||||
|
// 1. Exact Name Match
|
||||||
|
matchedPlace = pois.find(
|
||||||
|
(p) =>
|
||||||
|
p.osmTags &&
|
||||||
|
(p.osmTags.name === targetName || p.osmTags['name:en'] === targetName)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. High Proximity Match (<= 10m) - Only if we don't have a name match
|
||||||
|
// Note: MapComponent had logic for <=20m + type match.
|
||||||
|
// We might want to pass the 'type' in queryParams if we want to be that precise.
|
||||||
|
// For now, let's stick to name or very close proximity.
|
||||||
|
if (!matchedPlace) {
|
||||||
|
const topCandidate = pois[0];
|
||||||
|
if (topCandidate._distance <= 10) {
|
||||||
|
matchedPlace = topCandidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedPlace) {
|
||||||
|
// Direct transition!
|
||||||
|
this.router.replaceWith('place', matchedPlace);
|
||||||
|
this.mapUi.stopSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mapUi.setSearchResults(pois);
|
||||||
|
this.mapUi.showSidebar();
|
||||||
|
this.mapUi.stopSearch();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export default class PlaceRoute extends Route {
|
|||||||
if (model) {
|
if (model) {
|
||||||
const options = { preventZoom: this.mapUi.preventNextZoom };
|
const options = { preventZoom: this.mapUi.preventNextZoom };
|
||||||
this.mapUi.selectPlace(model, options);
|
this.mapUi.selectPlace(model, options);
|
||||||
|
this.mapUi.showSidebar();
|
||||||
this.mapUi.preventNextZoom = false;
|
this.mapUi.preventNextZoom = false;
|
||||||
}
|
}
|
||||||
// Stop the pulse animation if it was running (e.g. redirected from search)
|
// Stop the pulse animation if it was running (e.g. redirected from search)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default class PlaceNewRoute extends Route {
|
|||||||
this.mapUi.updateCreationCoordinates(model.lat, model.lon);
|
this.mapUi.updateCreationCoordinates(model.lat, model.lon);
|
||||||
}
|
}
|
||||||
this.mapUi.startCreating();
|
this.mapUi.startCreating();
|
||||||
|
this.mapUi.showSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { getDistance } from '../utils/geo';
|
|
||||||
|
|
||||||
export default class SearchRoute extends Route {
|
export default class SearchRoute extends Route {
|
||||||
@service osm;
|
|
||||||
@service photon;
|
|
||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service storage;
|
|
||||||
@service router;
|
|
||||||
@service toast;
|
@service toast;
|
||||||
|
|
||||||
queryParams = {
|
queryParams = {
|
||||||
@@ -19,186 +14,29 @@ export default class SearchRoute extends Route {
|
|||||||
category: { refreshModel: true },
|
category: { refreshModel: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
async model(params) {
|
model(params) {
|
||||||
const lat = params.lat ? parseFloat(params.lat) : null;
|
// Just return params, doing the async fetch in the controller
|
||||||
const lon = params.lon ? parseFloat(params.lon) : null;
|
return params;
|
||||||
let pois = [];
|
|
||||||
let loadingType = null;
|
|
||||||
let loadingValue = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Case 0: Category Search (category parameter present)
|
|
||||||
if (params.category && lat && lon) {
|
|
||||||
loadingType = 'category';
|
|
||||||
loadingValue = params.category;
|
|
||||||
this.mapUi.startLoading(loadingType, loadingValue);
|
|
||||||
|
|
||||||
// We need bounds. If we have active map state, use it.
|
|
||||||
let bounds = this.mapUi.currentBounds;
|
|
||||||
|
|
||||||
// If we don't have bounds (direct URL visit), estimate them from lat/lon/zoom(16)
|
|
||||||
// or just use a fixed box around the center.
|
|
||||||
if (!bounds) {
|
|
||||||
// Approximate 0.01 degrees ~ 1km at equator. A viewport is roughly 0.02x0.01 at zoom 16?
|
|
||||||
// Let's take a safe box of ~1km radius.
|
|
||||||
const delta = 0.01;
|
|
||||||
bounds = {
|
|
||||||
minLat: lat - delta,
|
|
||||||
maxLat: lat + delta,
|
|
||||||
minLon: lon - delta,
|
|
||||||
maxLon: lon + delta,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pois = await this.osm.getCategoryPois(
|
|
||||||
bounds,
|
|
||||||
params.category,
|
|
||||||
lat,
|
|
||||||
lon
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort by distance from center
|
|
||||||
pois = pois
|
|
||||||
.map((p) => ({
|
|
||||||
...p,
|
|
||||||
_distance: getDistance(lat, lon, p.lat, p.lon),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a._distance - b._distance);
|
|
||||||
}
|
|
||||||
// Case 1: Text Search (q parameter present)
|
|
||||||
else if (params.q) {
|
|
||||||
loadingType = 'text';
|
|
||||||
loadingValue = params.q;
|
|
||||||
this.mapUi.startLoading(loadingType, loadingValue);
|
|
||||||
|
|
||||||
// Search with Photon (using lat/lon for bias if available)
|
|
||||||
pois = await this.photon.search(params.q, lat, lon);
|
|
||||||
|
|
||||||
// Search local bookmarks by name
|
|
||||||
const queryLower = params.q.toLowerCase();
|
|
||||||
const localMatches = this.storage.savedPlaces.filter((p) => {
|
|
||||||
return (
|
|
||||||
p.title?.toLowerCase().includes(queryLower) ||
|
|
||||||
p.description?.toLowerCase().includes(queryLower)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Merge local matches
|
|
||||||
localMatches.forEach((local) => {
|
|
||||||
const exists = pois.find(
|
|
||||||
(poi) =>
|
|
||||||
(local.osmId && poi.osmId === local.osmId) ||
|
|
||||||
(poi.id && poi.id === local.id)
|
|
||||||
);
|
|
||||||
if (!exists) {
|
|
||||||
pois.push(local);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Case 2: Nearby Search (lat/lon present, no q)
|
|
||||||
else if (lat && lon) {
|
|
||||||
// Nearby search does NOT trigger loading state (pulse is used instead)
|
|
||||||
const searchRadius = 50; // Default radius
|
|
||||||
|
|
||||||
// Fetch POIs from Overpass
|
|
||||||
pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
|
|
||||||
|
|
||||||
// Get cached/saved places in search radius
|
|
||||||
const localMatches = this.storage.savedPlaces.filter((p) => {
|
|
||||||
const dist = getDistance(lat, lon, p.lat, p.lon);
|
|
||||||
return dist <= searchRadius;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Merge local matches
|
|
||||||
localMatches.forEach((local) => {
|
|
||||||
const exists = pois.find(
|
|
||||||
(poi) =>
|
|
||||||
(local.osmId && poi.osmId === local.osmId) ||
|
|
||||||
(poi.id && poi.id === local.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
pois.push(local);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort by distance from click
|
|
||||||
pois = pois
|
|
||||||
.map((p) => {
|
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
_distance: getDistance(lat, lon, p.lat, p.lon),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a._distance - b._distance);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (loadingType && loadingValue) {
|
|
||||||
this.mapUi.stopLoading(loadingType, loadingValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any of these are already bookmarked
|
|
||||||
// We resolve them to the bookmark version if they exist
|
|
||||||
pois = pois.map((p) => {
|
|
||||||
const saved = this.storage.findPlaceById(p.osmId);
|
|
||||||
return saved || p;
|
|
||||||
});
|
|
||||||
|
|
||||||
return pois;
|
|
||||||
}
|
|
||||||
|
|
||||||
afterModel(model, transition) {
|
|
||||||
const { q, selected } = transition.to.queryParams;
|
|
||||||
|
|
||||||
// Heuristic Match Logic (ported from MapComponent)
|
|
||||||
// If 'selected' is provided (from map click), try to find that specific feature.
|
|
||||||
// If 'q' is provided (from text search), try to find an exact match to auto-select.
|
|
||||||
const targetName = selected || q;
|
|
||||||
|
|
||||||
if (targetName && model.length > 0) {
|
|
||||||
let matchedPlace = null;
|
|
||||||
|
|
||||||
// 1. Exact Name Match
|
|
||||||
matchedPlace = model.find(
|
|
||||||
(p) =>
|
|
||||||
p.osmTags &&
|
|
||||||
(p.osmTags.name === targetName || p.osmTags['name:en'] === targetName)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. High Proximity Match (<= 10m) - Only if we don't have a name match
|
|
||||||
// Note: MapComponent had logic for <=20m + type match.
|
|
||||||
// We might want to pass the 'type' in queryParams if we want to be that precise.
|
|
||||||
// For now, let's stick to name or very close proximity.
|
|
||||||
if (!matchedPlace) {
|
|
||||||
const topCandidate = model[0];
|
|
||||||
if (topCandidate._distance <= 10) {
|
|
||||||
matchedPlace = topCandidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedPlace) {
|
|
||||||
// Direct transition!
|
|
||||||
this.router.replaceWith('place', matchedPlace);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the pulse animation since search is done (and we are staying here)
|
|
||||||
this.mapUi.stopSearch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
super.setupController(controller, model);
|
super.setupController(controller, model);
|
||||||
// Ensure pulse is stopped if we reach here
|
|
||||||
this.mapUi.stopSearch();
|
// Trigger the background task to fetch results
|
||||||
this.mapUi.setSearchResults(model);
|
controller.fetchResultsTask.perform(model);
|
||||||
|
|
||||||
// Store current search params to allow "Up" navigation from place details
|
// Store current search params to allow "Up" navigation from place details
|
||||||
const { q, category, lat, lon } = this.paramsFor('search');
|
const { q, category, lat, lon } = this.paramsFor('search');
|
||||||
this.mapUi.currentSearch = { q, category, lat, lon };
|
this.mapUi.currentSearch = { q, category, lat, lon };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
controller.fetchResultsTask.cancelAll();
|
||||||
|
this.mapUi.stopSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
error(error, transition) {
|
error(error, transition) {
|
||||||
this.mapUi.stopSearch();
|
this.mapUi.stopSearch();
|
||||||
@@ -206,6 +44,6 @@ export default class SearchRoute extends Route {
|
|||||||
if (transition) {
|
if (transition) {
|
||||||
transition.abort();
|
transition.abort();
|
||||||
}
|
}
|
||||||
return false; // Prevent bubble and stop transition
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ export default class MapUiService extends Service {
|
|||||||
@tracked searchResults = [];
|
@tracked searchResults = [];
|
||||||
@tracked currentSearch = null;
|
@tracked currentSearch = null;
|
||||||
@tracked loadingState = null;
|
@tracked loadingState = null;
|
||||||
|
@tracked isSidebarVisible = false;
|
||||||
|
|
||||||
|
showSidebar() {
|
||||||
|
this.isSidebarVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSidebar() {
|
||||||
|
this.isSidebarVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
selectPlace(place, options = {}) {
|
selectPlace(place, options = {}) {
|
||||||
this.selectedPlace = place;
|
this.selectedPlace = place;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Service from '@ember/service';
|
|||||||
import { RelayPool } from 'applesauce-relay';
|
import { RelayPool } from 'applesauce-relay';
|
||||||
|
|
||||||
export default class NostrRelayService extends Service {
|
export default class NostrRelayService extends Service {
|
||||||
pool = new RelayPool();
|
pool = new RelayPool({ enablePing: true });
|
||||||
|
|
||||||
async publish(relays, event) {
|
async publish(relays, event) {
|
||||||
if (!relays || relays.length === 0) {
|
if (!relays || relays.length === 0) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getLocalizedName } from '../utils/osm';
|
|||||||
|
|
||||||
export default class StorageService extends Service {
|
export default class StorageService extends Service {
|
||||||
@service osm;
|
@service osm;
|
||||||
|
@service toast;
|
||||||
rs;
|
rs;
|
||||||
widget;
|
widget;
|
||||||
@tracked placesInView = [];
|
@tracked placesInView = [];
|
||||||
@@ -23,10 +24,13 @@ export default class StorageService extends Service {
|
|||||||
@tracked connected = false;
|
@tracked connected = false;
|
||||||
@tracked userAddress = null;
|
@tracked userAddress = null;
|
||||||
@tracked isWidgetOpen = false;
|
@tracked isWidgetOpen = false;
|
||||||
|
isNewConnection = true;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
|
this.checkInitialConnectionState();
|
||||||
|
|
||||||
this.rs = new RemoteStorage({
|
this.rs = new RemoteStorage({
|
||||||
modules: [Places],
|
modules: [Places],
|
||||||
});
|
});
|
||||||
@@ -57,6 +61,12 @@ export default class StorageService extends Service {
|
|||||||
this.rs.on('connected', () => {
|
this.rs.on('connected', () => {
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
this.userAddress = this.rs.remote.userAddress;
|
this.userAddress = this.rs.remote.userAddress;
|
||||||
|
|
||||||
|
if (this.isNewConnection) {
|
||||||
|
this.toast.show('Remote storage connected', 3000);
|
||||||
|
this.isNewConnection = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.loadLists();
|
this.loadLists();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,6 +82,7 @@ export default class StorageService extends Service {
|
|||||||
this.loadedPrefixes = [];
|
this.loadedPrefixes = [];
|
||||||
this.lists = [];
|
this.lists = [];
|
||||||
this.initialSyncDone = false;
|
this.initialSyncDone = false;
|
||||||
|
this.isNewConnection = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rs.on('sync-done', () => {
|
this.rs.on('sync-done', () => {
|
||||||
@@ -93,6 +104,31 @@ export default class StorageService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkInitialConnectionState() {
|
||||||
|
this.isNewConnection = true;
|
||||||
|
try {
|
||||||
|
if (window.localStorage) {
|
||||||
|
const keys = [
|
||||||
|
'remotestorage:wireclient',
|
||||||
|
'remotestorage:dropbox',
|
||||||
|
'remotestorage:googledrive',
|
||||||
|
];
|
||||||
|
for (const key of keys) {
|
||||||
|
const data = window.localStorage.getItem(key);
|
||||||
|
if (data) {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
if (parsed && parsed.token) {
|
||||||
|
this.isNewConnection = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to check localStorage for existing connection:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handlePlaceChange(event) {
|
handlePlaceChange(event) {
|
||||||
const { newValue, relativePath } = event;
|
const { newValue, relativePath } = event;
|
||||||
|
|
||||||
|
|||||||
@@ -1365,10 +1365,10 @@ span.icon {
|
|||||||
@media (width <= 768px) {
|
@media (width <= 768px) {
|
||||||
/* Sidebar/Bottom Sheet is open (Mobile: Bottom 50%) */
|
/* Sidebar/Bottom Sheet is open (Mobile: Bottom 50%) */
|
||||||
|
|
||||||
/* Center Y = (height/2) / 2 = height/4 = 25% */
|
/* Center Y = (height/2) / 2 = height/4 = 25% + half header height */
|
||||||
.map-container.sidebar-open .map-crosshair {
|
.map-container.sidebar-open .map-crosshair {
|
||||||
left: 50%; /* Reset desktop shift */
|
left: 50%; /* Reset desktop shift */
|
||||||
top: 25%;
|
top: calc(25% + 30px); /* 30px approx half header height */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ export default class ApplicationComponent extends Component {
|
|||||||
@tracked isAppMenuOpen = false;
|
@tracked isAppMenuOpen = false;
|
||||||
|
|
||||||
get isSidebarOpen() {
|
get isSidebarOpen() {
|
||||||
// We consider the sidebar "open" if we are in search or place routes.
|
// We consider the sidebar "open" if we are in search or place routes AND it's visible.
|
||||||
// This helps the map know if it should shift the center or adjust view.
|
// This helps the map know if it should shift the center or adjust view.
|
||||||
return (
|
return (
|
||||||
this.router.currentRouteName === 'place' ||
|
this.mapUi.isSidebarVisible &&
|
||||||
|
(this.router.currentRouteName === 'place' ||
|
||||||
this.router.currentRouteName === 'place.new' ||
|
this.router.currentRouteName === 'place.new' ||
|
||||||
this.router.currentRouteName === 'search'
|
this.router.currentRouteName === 'search')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +49,12 @@ export default class ApplicationComponent extends Component {
|
|||||||
handleOutsideClick() {
|
handleOutsideClick() {
|
||||||
if (this.isAppMenuOpen) {
|
if (this.isAppMenuOpen) {
|
||||||
this.closeAppMenu();
|
this.closeAppMenu();
|
||||||
} else if (this.router.currentRouteName === 'search') {
|
} else if (
|
||||||
this.router.transitionTo('index');
|
this.router.currentRouteName === 'search' ||
|
||||||
} else if (this.router.currentRouteName === 'place') {
|
this.router.currentRouteName === 'place'
|
||||||
// If in place route, decide if we want to go back to search or index
|
) {
|
||||||
// For now, let's go to index or maybe back to search if search params exist?
|
this.mapUi.clearSelection();
|
||||||
// Simplest behavior: clear selection
|
this.mapUi.hideSidebar();
|
||||||
this.router.transitionTo('index');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default class PlaceTemplate extends Component {
|
|||||||
if (place === null) {
|
if (place === null) {
|
||||||
// If we have an active search context, return to it (UP navigation)
|
// If we have an active search context, return to it (UP navigation)
|
||||||
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
||||||
|
this.mapUi.showSidebar();
|
||||||
this.router.transitionTo('search', {
|
this.router.transitionTo('search', {
|
||||||
queryParams: this.mapUi.currentSearch,
|
queryParams: this.mapUi.currentSearch,
|
||||||
});
|
});
|
||||||
@@ -88,17 +89,19 @@ export default class PlaceTemplate extends Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If a place is selected (unlikely in this view, but possible if we add related links)
|
// If a place is selected (unlikely in this view, but possible if we add related links)
|
||||||
|
this.mapUi.showSidebar();
|
||||||
this.router.transitionTo('place', place);
|
this.router.transitionTo('place', place);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
close() {
|
close() {
|
||||||
// Clear search results so we don't fall back to the list
|
this.mapUi.clearSelection();
|
||||||
this.router.transitionTo('index');
|
this.mapUi.hideSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
{{#if this.mapUi.isSidebarVisible}}
|
||||||
<PlacesSidebar
|
<PlacesSidebar
|
||||||
@selectedPlace={{this.place}}
|
@selectedPlace={{this.place}}
|
||||||
@onClose={{this.close}}
|
@onClose={{this.close}}
|
||||||
@@ -106,5 +109,6 @@ export default class PlaceTemplate extends Component {
|
|||||||
@onBookmarkChange={{this.refreshMap}}
|
@onBookmarkChange={{this.refreshMap}}
|
||||||
@onUpdate={{this.handleUpdate}}
|
@onUpdate={{this.handleUpdate}}
|
||||||
/>
|
/>
|
||||||
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,15 +56,16 @@ export default class PlaceNewTemplate extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
{{#if this.mapUi.isSidebarVisible}}
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h2><Icon @name="plus-circle" @size={{20}} @color="#ea4335" />
|
<h2><Icon @name="plus-circle" @size={{20}} @color="#ea4335" />
|
||||||
New Place</h2>
|
New Place</h2>
|
||||||
<button type="button" class="close-btn" {{on "click" this.close}}><Icon
|
<button
|
||||||
@name="x"
|
type="button"
|
||||||
@size={{20}}
|
class="close-btn"
|
||||||
@color="#333"
|
{{on "click" this.close}}
|
||||||
/></button>
|
><Icon @name="x" @size={{20}} @color="#333" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
@@ -79,5 +80,6 @@ export default class PlaceNewTemplate extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default class SearchTemplate extends Component {
|
|||||||
selectPlace(place) {
|
selectPlace(place) {
|
||||||
if (place) {
|
if (place) {
|
||||||
this.mapUi.returnToSearch = true;
|
this.mapUi.returnToSearch = true;
|
||||||
|
this.mapUi.showSidebar();
|
||||||
|
this.mapUi.preventNextZoom = true;
|
||||||
// We don't need to manually set currentSearch here because
|
// We don't need to manually set currentSearch here because
|
||||||
// it was already set in the route's setupController
|
// it was already set in the route's setupController
|
||||||
this.router.transitionTo('place', place);
|
this.router.transitionTo('place', place);
|
||||||
@@ -19,14 +21,16 @@ export default class SearchTemplate extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
close() {
|
close() {
|
||||||
this.router.transitionTo('index');
|
this.mapUi.hideSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
{{#if this.mapUi.isSidebarVisible}}
|
||||||
<PlacesSidebar
|
<PlacesSidebar
|
||||||
@places={{@model}}
|
@places={{this.mapUi.searchResults}}
|
||||||
@onSelect={{this.selectPlace}}
|
@onSelect={{this.selectPlace}}
|
||||||
@onClose={{this.close}}
|
@onClose={{this.close}}
|
||||||
/>
|
/>
|
||||||
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ import toolbox from '@waysidemapping/pinhead/dist/icons/toolbox.svg?raw';
|
|||||||
import treeAndBenchWithBackrest from '@waysidemapping/pinhead/dist/icons/tree_and_bench_with_backrest.svg?raw';
|
import treeAndBenchWithBackrest from '@waysidemapping/pinhead/dist/icons/tree_and_bench_with_backrest.svg?raw';
|
||||||
import villageBuildings from '@waysidemapping/pinhead/dist/icons/village_buildings.svg?raw';
|
import villageBuildings from '@waysidemapping/pinhead/dist/icons/village_buildings.svg?raw';
|
||||||
import wallHangingWithMountainsAndSun from '@waysidemapping/pinhead/dist/icons/wall_hanging_with_mountains_and_sun.svg?raw';
|
import wallHangingWithMountainsAndSun from '@waysidemapping/pinhead/dist/icons/wall_hanging_with_mountains_and_sun.svg?raw';
|
||||||
|
import windingWayWide from '@waysidemapping/pinhead/dist/icons/winding_way_wide.svg?raw';
|
||||||
import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw';
|
import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw';
|
||||||
|
|
||||||
import loadingRing from '../icons/270-ring.svg?raw';
|
import loadingRing from '../icons/270-ring.svg?raw';
|
||||||
@@ -243,6 +244,7 @@ const ICONS = {
|
|||||||
'womens-and-mens-restroom-symbol': womensAndMensRestroomSymbol,
|
'womens-and-mens-restroom-symbol': womensAndMensRestroomSymbol,
|
||||||
whatsapp,
|
whatsapp,
|
||||||
wikipedia,
|
wikipedia,
|
||||||
|
winding_way_wide: windingWayWide,
|
||||||
parking_p: parkingP,
|
parking_p: parkingP,
|
||||||
car,
|
car,
|
||||||
x,
|
x,
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ export const POI_ICON_RULES = [
|
|||||||
{ tags: { amenity: 'arts_center' }, icon: 'comedy-mask-and-tragedy-mask' },
|
{ tags: { amenity: 'arts_center' }, icon: 'comedy-mask-and-tragedy-mask' },
|
||||||
|
|
||||||
// Historic
|
// Historic
|
||||||
|
{ tags: { historic: 'canal' }, icon: 'winding_way_wide' },
|
||||||
{ tags: { historic: 'bridge' }, icon: 'bridge' },
|
{ tags: { historic: 'bridge' }, icon: 'bridge' },
|
||||||
|
{ tags: { historic: 'bridge_site' }, icon: 'bridge' },
|
||||||
{ tags: { historic: 'fort' }, icon: 'fort' },
|
{ tags: { historic: 'fort' }, icon: 'fort' },
|
||||||
{ tags: { historic: 'castle' }, icon: 'palace' },
|
{ tags: { historic: 'castle' }, icon: 'palace' },
|
||||||
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.20.4",
|
"version": "1.20.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"qunit": "^2.25.0",
|
"qunit": "^2.25.0",
|
||||||
"qunit-dom": "^3.5.0",
|
"qunit-dom": "^3.5.0",
|
||||||
"remotestorage-widget": "^1.8.1",
|
"remotestorage-widget": "^1.8.1",
|
||||||
"remotestoragejs": "2.0.0-beta.8",
|
"remotestoragejs": "2.0.0-beta.9",
|
||||||
"sinon": "^21.0.1",
|
"sinon": "^21.0.1",
|
||||||
"stylelint": "^16.26.1",
|
"stylelint": "^16.26.1",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
|
|||||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -196,8 +196,8 @@ importers:
|
|||||||
specifier: ^1.8.1
|
specifier: ^1.8.1
|
||||||
version: 1.8.1
|
version: 1.8.1
|
||||||
remotestoragejs:
|
remotestoragejs:
|
||||||
specifier: 2.0.0-beta.8
|
specifier: 2.0.0-beta.9
|
||||||
version: 2.0.0-beta.8
|
version: 2.0.0-beta.9
|
||||||
sinon:
|
sinon:
|
||||||
specifier: ^21.0.1
|
specifier: ^21.0.1
|
||||||
version: 21.0.1
|
version: 21.0.1
|
||||||
@@ -1720,9 +1720,6 @@ packages:
|
|||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@20.14.0':
|
|
||||||
resolution: {integrity: sha512-5cHBxFGJx6L4s56Bubp4fglrEpmyJypsqI6RgzMfBHWUJQGWAAi8cWcgetEbZXHYXo9C2Fa4EEds/uSyS4cxmA==}
|
|
||||||
|
|
||||||
'@types/node@25.0.7':
|
'@types/node@25.0.7':
|
||||||
resolution: {integrity: sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==}
|
resolution: {integrity: sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==}
|
||||||
|
|
||||||
@@ -1735,9 +1732,6 @@ packages:
|
|||||||
'@types/symlink-or-copy@1.2.2':
|
'@types/symlink-or-copy@1.2.2':
|
||||||
resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==}
|
resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==}
|
||||||
|
|
||||||
'@types/tv4@1.2.33':
|
|
||||||
resolution: {integrity: sha512-7phCVTXC6Bj50IV1iKOwqGkR4JONJyMbRZnKTSuujv1S/tO9rG5OdCt7BMSjytO+zJmYdn1/I4fd3SH0gtO99g==}
|
|
||||||
|
|
||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
@@ -4906,8 +4900,8 @@ packages:
|
|||||||
remotestorage-widget@1.8.1:
|
remotestorage-widget@1.8.1:
|
||||||
resolution: {integrity: sha512-HxNu2VvIRW3wzkf5fLEzs56ySQ7+YQbRqyp3CKvmw/G+zKhRsmj06HtFoAcm3B14/nJh2SOAv3LyfKuXfUsKPw==}
|
resolution: {integrity: sha512-HxNu2VvIRW3wzkf5fLEzs56ySQ7+YQbRqyp3CKvmw/G+zKhRsmj06HtFoAcm3B14/nJh2SOAv3LyfKuXfUsKPw==}
|
||||||
|
|
||||||
remotestoragejs@2.0.0-beta.8:
|
remotestoragejs@2.0.0-beta.9:
|
||||||
resolution: {integrity: sha512-rtyHTG2VbtiKTRmbwjponRf5VTPJMcHv/ijNid1zX48C0Z0F8ZCBBfkKD2QCxTQyQvCupkWNy3wuIu4HE+AEng==}
|
resolution: {integrity: sha512-d09ByL7ecbZLMuzl4mQ3SXMFlsCwvvINm6l1CfdR8ylvX9E1nsq44t8gmRxzW6GUS5cwonyYA4FRXYKEhARjTA==}
|
||||||
|
|
||||||
remove-types@1.0.0:
|
remove-types@1.0.0:
|
||||||
resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==}
|
resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==}
|
||||||
@@ -5578,9 +5572,6 @@ packages:
|
|||||||
underscore@1.13.7:
|
underscore@1.13.7:
|
||||||
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||||
|
|
||||||
undici-types@5.26.5:
|
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
|
||||||
|
|
||||||
undici-types@7.16.0:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
@@ -5747,8 +5738,8 @@ packages:
|
|||||||
web-worker@1.5.0:
|
web-worker@1.5.0:
|
||||||
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
|
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
|
||||||
|
|
||||||
webfinger.js@2.8.2:
|
webfinger.js@3.0.4:
|
||||||
resolution: {integrity: sha512-Zqn9KXkGrD1tVEm029bVUIfmzef2KXs3G7OZrdqehDHtgv9YSxX1oy4RoPoMk2PHWIifwWCA0xwKZOAZqXMpfg==}
|
resolution: {integrity: sha512-5c15N1n4qCm/jGJjUt32mBdPVlSugLbAztIDNBpuDfukGz2E9NhmXPfLikayn2p3kcgEZsI/UOdOwVpxOr8qJA==}
|
||||||
|
|
||||||
webidl-conversions@7.0.0:
|
webidl-conversions@7.0.0:
|
||||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
@@ -7648,10 +7639,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@20.14.0':
|
|
||||||
dependencies:
|
|
||||||
undici-types: 5.26.5
|
|
||||||
|
|
||||||
'@types/node@25.0.7':
|
'@types/node@25.0.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
@@ -7665,8 +7652,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/symlink-or-copy@1.2.2': {}
|
'@types/symlink-or-copy@1.2.2': {}
|
||||||
|
|
||||||
'@types/tv4@1.2.33': {}
|
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)':
|
'@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)':
|
||||||
@@ -11530,13 +11515,11 @@ snapshots:
|
|||||||
|
|
||||||
remotestorage-widget@1.8.1: {}
|
remotestorage-widget@1.8.1: {}
|
||||||
|
|
||||||
remotestoragejs@2.0.0-beta.8:
|
remotestoragejs@2.0.0-beta.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.14.0
|
|
||||||
'@types/tv4': 1.2.33
|
|
||||||
esm: 3.2.25
|
esm: 3.2.25
|
||||||
tv4: 1.3.0
|
tv4: 1.3.0
|
||||||
webfinger.js: 2.8.2
|
webfinger.js: 3.0.4
|
||||||
xhr2: 0.2.1
|
xhr2: 0.2.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
@@ -12416,8 +12399,6 @@ snapshots:
|
|||||||
|
|
||||||
underscore@1.13.7: {}
|
underscore@1.13.7: {}
|
||||||
|
|
||||||
undici-types@5.26.5: {}
|
|
||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||||
@@ -12569,7 +12550,7 @@ snapshots:
|
|||||||
|
|
||||||
web-worker@1.5.0: {}
|
web-worker@1.5.0: {}
|
||||||
|
|
||||||
webfinger.js@2.8.2: {}
|
webfinger.js@3.0.4: {}
|
||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
webidl-conversions@7.0.0: {}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -39,7 +39,7 @@
|
|||||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/main-CIpd5fcK.js"></script>
|
<script type="module" crossorigin src="/assets/main-Dhq0XoTm.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-CHuW_yI-.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-CHuW_yI-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -140,12 +140,14 @@ module('Acceptance | map search reset', function (hooks) {
|
|||||||
bubbles: true,
|
bubbles: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for transition to index
|
// Wait for transition or UI update
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
assert.strictEqual(
|
|
||||||
currentURL(),
|
// Sidebar should be hidden, but we should stay on the search route
|
||||||
'/',
|
assert.dom('.sidebar').doesNotExist('Sidebar should be closed');
|
||||||
'Should have transitioned to index (closed sidebar)'
|
assert.ok(
|
||||||
|
currentURL().includes('category=coffee'),
|
||||||
|
'Should have stayed on the search route with markers intact'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second Click (Start new search)
|
// Second Click (Start new search)
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
// Click the Close (X) button
|
// Click the Close (X) button
|
||||||
await click('.close-btn');
|
await click('.close-btn');
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), '/', 'Returned to index');
|
assert.dom('.sidebar').doesNotExist('Sidebar should be closed');
|
||||||
assert.false(mapUi.returnToSearch, 'Flag is reset after closing sidebar');
|
assert.ok(currentURL().includes('/place/'), 'Remains on place route');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('navigating directly to place and back closes sidebar', async function (assert) {
|
test('navigating directly to place and back closes sidebar', async function (assert) {
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { visit, click, fillIn, currentURL } from '@ember/test-helpers';
|
import { visit, click, fillIn, currentURL, settled } from '@ember/test-helpers';
|
||||||
import { setupApplicationTest } from 'marco/tests/helpers';
|
import { setupApplicationTest } from 'marco/tests/helpers';
|
||||||
import Service from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import { Promise } from 'rsvp';
|
import { Promise } from 'rsvp';
|
||||||
|
|
||||||
|
let photonResolve;
|
||||||
|
let osmResolve;
|
||||||
|
|
||||||
class MockPhotonService extends Service {
|
class MockPhotonService extends Service {
|
||||||
cancelAll() {}
|
cancelAll() {}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
// Simulate network delay
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
if (query === 'slow') {
|
if (query === 'slow') {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
// Return a promise that we can manually resolve in the test
|
||||||
|
// to avoid race conditions with native setTimeout
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
photonResolve = () => {
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
title: 'Test Place',
|
||||||
|
lat: 1,
|
||||||
|
lon: 1,
|
||||||
|
osmId: '123',
|
||||||
|
osmType: 'node',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -29,9 +44,12 @@ class MockOsmService extends Service {
|
|||||||
cancelAll() {}
|
cancelAll() {}
|
||||||
|
|
||||||
async getCategoryPois(bounds, category) {
|
async getCategoryPois(bounds, category) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
if (category === 'slow_category') {
|
if (category === 'slow_category') {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
return new Promise((resolve) => {
|
||||||
|
osmResolve = () => {
|
||||||
|
resolve([]);
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -44,6 +62,8 @@ module('Acceptance | search loading', function (hooks) {
|
|||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
|
photonResolve = null;
|
||||||
|
osmResolve = null;
|
||||||
this.owner.register('service:photon', MockPhotonService);
|
this.owner.register('service:photon', MockPhotonService);
|
||||||
this.owner.register('service:osm', MockOsmService);
|
this.owner.register('service:osm', MockOsmService);
|
||||||
});
|
});
|
||||||
@@ -66,7 +86,12 @@ module('Acceptance | search loading', function (hooks) {
|
|||||||
'Loading state is set for text search'
|
'Loading state is set for text search'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve the manual promise so the task can finish deterministically
|
||||||
|
photonResolve();
|
||||||
|
|
||||||
await searchPromise;
|
await searchPromise;
|
||||||
|
await settled(); // Wait for ember-concurrency tasks to fully settle
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
mapUi.loadingState,
|
mapUi.loadingState,
|
||||||
null,
|
null,
|
||||||
@@ -83,7 +108,12 @@ module('Acceptance | search loading', function (hooks) {
|
|||||||
'Loading state is set for category search'
|
'Loading state is set for category search'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve the manual promise
|
||||||
|
osmResolve();
|
||||||
|
|
||||||
await catPromise;
|
await catPromise;
|
||||||
|
await settled();
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
mapUi.loadingState,
|
mapUi.loadingState,
|
||||||
null,
|
null,
|
||||||
@@ -122,6 +152,7 @@ module('Acceptance | search loading', function (hooks) {
|
|||||||
|
|
||||||
// 4. Click the clear button (should be visible since input has value)
|
// 4. Click the clear button (should be visible since input has value)
|
||||||
await click('.search-clear-btn');
|
await click('.search-clear-btn');
|
||||||
|
// Wait for the click and transition to settle
|
||||||
|
|
||||||
// Verify loading state is cleared immediately
|
// Verify loading state is cleared immediately
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
@@ -130,6 +161,11 @@ module('Acceptance | search loading', function (hooks) {
|
|||||||
'Loading state is cleared immediately after clicking clear'
|
'Loading state is cleared immediately after clicking clear'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Clean up the dangling promise
|
||||||
|
if (photonResolve) {
|
||||||
|
photonResolve();
|
||||||
|
}
|
||||||
|
|
||||||
// Verify we are back on index (or at least query is gone)
|
// Verify we are back on index (or at least query is gone)
|
||||||
assert.strictEqual(currentURL(), '/', 'Navigated to index');
|
assert.strictEqual(currentURL(), '/', 'Navigated to index');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ module('Unit | Route | place', function (hooks) {
|
|||||||
selectPlaceCalled = true;
|
selectPlaceCalled = true;
|
||||||
}
|
}
|
||||||
stopSearch() {}
|
stopSearch() {}
|
||||||
|
showSidebar() {}
|
||||||
|
hideSidebar() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owner.register('service:osm', OsmStub);
|
this.owner.register('service:osm', OsmStub);
|
||||||
@@ -76,6 +78,8 @@ module('Unit | Route | place', function (hooks) {
|
|||||||
class MapUiStub extends Service {
|
class MapUiStub extends Service {
|
||||||
selectPlace() {}
|
selectPlace() {}
|
||||||
stopSearch() {}
|
stopSearch() {}
|
||||||
|
showSidebar() {}
|
||||||
|
hideSidebar() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owner.register('service:osm', OsmStub);
|
this.owner.register('service:osm', OsmStub);
|
||||||
@@ -110,6 +114,8 @@ module('Unit | Route | place', function (hooks) {
|
|||||||
class MapUiStub extends Service {
|
class MapUiStub extends Service {
|
||||||
selectPlace() {}
|
selectPlace() {}
|
||||||
stopSearch() {}
|
stopSearch() {}
|
||||||
|
showSidebar() {}
|
||||||
|
hideSidebar() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owner.register('service:osm', OsmStub);
|
this.owner.register('service:osm', OsmStub);
|
||||||
@@ -155,6 +161,8 @@ module('Unit | Route | place', function (hooks) {
|
|||||||
assert.ok(options.preventZoom, 'Prevented zoom on update');
|
assert.ok(options.preventZoom, 'Prevented zoom on update');
|
||||||
}
|
}
|
||||||
stopSearch() {}
|
stopSearch() {}
|
||||||
|
showSidebar() {}
|
||||||
|
hideSidebar() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owner.register('service:storage', StorageStub);
|
this.owner.register('service:storage', StorageStub);
|
||||||
|
|||||||
Reference in New Issue
Block a user