Compare commits
20 Commits
438bf0c31c
...
v1.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
2b219fe0cf
|
|||
|
9fd6c4d64d
|
|||
|
8e5b2c7439
|
|||
|
0f29430e1a
|
|||
|
0059d89cc3
|
|||
|
54e2766dc4
|
|||
|
5978f67d48
|
|||
|
d72e5f3de2
|
|||
|
582ab4f8b3
|
|||
|
0ac6db65cb
|
|||
|
86b20fd474
|
|||
|
8478e00253
|
|||
|
818ec35071
|
|||
|
46605dbd32
|
|||
|
bcc51efecc
|
|||
|
8bec4b978e
|
|||
|
cd9676047d
|
|||
|
a92b44ec13
|
|||
|
0c2d1f8419
|
|||
|
bb77ed8337
|
@@ -10,6 +10,7 @@ import CategoryChips from '#components/category-chips';
|
|||||||
|
|
||||||
export default class AppHeaderComponent extends Component {
|
export default class AppHeaderComponent extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
|
@service settings;
|
||||||
@tracked isUserMenuOpen = false;
|
@tracked isUserMenuOpen = false;
|
||||||
@tracked searchQuery = '';
|
@tracked searchQuery = '';
|
||||||
|
|
||||||
@@ -49,9 +50,11 @@ export default class AppHeaderComponent extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if this.settings.showQuickSearchButtons}}
|
||||||
<div class="header-center {{if this.hasQuery 'searching'}}">
|
<div class="header-center {{if this.hasQuery 'searching'}}">
|
||||||
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="user-menu-container">
|
<div class="user-menu-container">
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export default class AppMenuSettings extends Component {
|
|||||||
this.settings.updateMapKinetic(event.target.value === 'true');
|
this.settings.updateMapKinetic(event.target.value === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleQuickSearchButtons(event) {
|
||||||
|
this.settings.updateShowQuickSearchButtons(event.target.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updatePhotonApi(event) {
|
updatePhotonApi(event) {
|
||||||
this.settings.updatePhotonApi(event.target.value);
|
this.settings.updatePhotonApi(event.target.value);
|
||||||
@@ -36,6 +41,30 @@ export default class AppMenuSettings extends Component {
|
|||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="show-quick-search">Quick search buttons visible</label>
|
||||||
|
<select
|
||||||
|
id="show-quick-search"
|
||||||
|
class="form-control"
|
||||||
|
{{on "change" this.toggleQuickSearchButtons}}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="true"
|
||||||
|
selected={{if this.settings.showQuickSearchButtons "selected"}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="false"
|
||||||
|
selected={{unless
|
||||||
|
this.settings.showQuickSearchButtons
|
||||||
|
"selected"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
|
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { on } from '@ember/modifier';
|
|||||||
import { fn } from '@ember/helper';
|
import { fn } from '@ember/helper';
|
||||||
import Icon from '#components/icon';
|
import Icon from '#components/icon';
|
||||||
import { POI_CATEGORIES } from '../utils/poi-categories';
|
import { POI_CATEGORIES } from '../utils/poi-categories';
|
||||||
|
import { eq, and } from 'ember-truth-helpers';
|
||||||
|
|
||||||
export default class CategoryChipsComponent extends Component {
|
export default class CategoryChipsComponent extends Component {
|
||||||
@service router;
|
@service router;
|
||||||
@@ -41,6 +42,10 @@ export default class CategoryChipsComponent extends Component {
|
|||||||
class="category-chip"
|
class="category-chip"
|
||||||
{{on "click" (fn this.searchCategory category)}}
|
{{on "click" (fn this.searchCategory category)}}
|
||||||
aria-label={{category.label}}
|
aria-label={{category.label}}
|
||||||
|
disabled={{and
|
||||||
|
(eq this.mapUi.loadingState.type "category")
|
||||||
|
(eq this.mapUi.loadingState.value category.id)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon @name={{category.icon}} @size={{16}} />
|
<Icon @name={{category.icon}} @size={{16}} />
|
||||||
<span>{{category.label}}</span>
|
<span>{{category.label}}</span>
|
||||||
|
|||||||
@@ -1099,6 +1099,20 @@ export default class MapComponent extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to transition with proper state
|
||||||
|
const transitionToPlace = (place) => {
|
||||||
|
// If we are currently in search mode OR have active search results,
|
||||||
|
// we want the "Back" button on the place details to return to the search results.
|
||||||
|
if (
|
||||||
|
this.router.currentRouteName === 'search' ||
|
||||||
|
(this.mapUi.currentSearch && this.mapUi.searchResults.length > 0)
|
||||||
|
) {
|
||||||
|
this.mapUi.returnToSearch = true;
|
||||||
|
}
|
||||||
|
this.mapUi.preventNextZoom = true;
|
||||||
|
this.router.transitionTo('place', place);
|
||||||
|
};
|
||||||
|
|
||||||
// Special handling when sidebar is OPEN
|
// Special handling when sidebar is OPEN
|
||||||
if (this.args.isSidebarOpen) {
|
if (this.args.isSidebarOpen) {
|
||||||
// If it's a bookmark or search result, we allow "switching" to it even if sidebar is open
|
// If it's a bookmark or search result, we allow "switching" to it even if sidebar is open
|
||||||
@@ -1108,8 +1122,7 @@ export default class MapComponent extends Component {
|
|||||||
'Clicked feature while sidebar open (switching):',
|
'Clicked feature while sidebar open (switching):',
|
||||||
targetPlace
|
targetPlace
|
||||||
);
|
);
|
||||||
this.mapUi.preventNextZoom = true;
|
transitionToPlace(targetPlace);
|
||||||
this.router.transitionTo('place', targetPlace);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,15 +1136,13 @@ export default class MapComponent extends Component {
|
|||||||
// Normal behavior (sidebar is closed)
|
// Normal behavior (sidebar is closed)
|
||||||
if (clickedBookmark) {
|
if (clickedBookmark) {
|
||||||
console.debug('Clicked bookmark:', clickedBookmark);
|
console.debug('Clicked bookmark:', clickedBookmark);
|
||||||
this.mapUi.preventNextZoom = true;
|
transitionToPlace(clickedBookmark);
|
||||||
this.router.transitionTo('place', clickedBookmark);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clickedSearchResult) {
|
if (clickedSearchResult) {
|
||||||
console.debug('Clicked search result:', clickedSearchResult);
|
console.debug('Clicked search result:', clickedSearchResult);
|
||||||
this.mapUi.preventNextZoom = true;
|
transitionToPlace(clickedSearchResult);
|
||||||
this.router.transitionTo('place', clickedSearchResult);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,6 +1183,7 @@ export default class MapComponent extends Component {
|
|||||||
lat: lat.toFixed(6),
|
lat: lat.toFixed(6),
|
||||||
lon: lon.toFixed(6),
|
lon: lon.toFixed(6),
|
||||||
q: null, // Clear q to force spatial search
|
q: null, // Clear q to force spatial search
|
||||||
|
category: null, // Clear category to force spatial search
|
||||||
selected: selectedFeatureName || null,
|
selected: selectedFeatureName || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { task, timeout } from 'ember-concurrency';
|
|||||||
import Icon from '#components/icon';
|
import Icon from '#components/icon';
|
||||||
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
||||||
import { POI_CATEGORIES } from '../utils/poi-categories';
|
import { POI_CATEGORIES } from '../utils/poi-categories';
|
||||||
import eq from 'ember-truth-helpers/helpers/eq';
|
import { eq, or } from 'ember-truth-helpers';
|
||||||
|
|
||||||
export default class SearchBoxComponent extends Component {
|
export default class SearchBoxComponent extends Component {
|
||||||
@service photon;
|
@service photon;
|
||||||
|
@service osm;
|
||||||
@service router;
|
@service router;
|
||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service map; // Assuming we might need map context, but mostly we use router
|
@service map; // Assuming we might need map context, but mostly we use router
|
||||||
@@ -178,6 +179,11 @@ export default class SearchBoxComponent extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
clear() {
|
clear() {
|
||||||
|
this.searchTask.cancelAll();
|
||||||
|
this.mapUi.stopLoading();
|
||||||
|
this.osm.cancelAll();
|
||||||
|
this.photon.cancelAll();
|
||||||
|
|
||||||
this.query = '';
|
this.query = '';
|
||||||
this.results = [];
|
this.results = [];
|
||||||
if (this.args.onQueryChange) {
|
if (this.args.onQueryChange) {
|
||||||
@@ -211,7 +217,16 @@ export default class SearchBoxComponent extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button type="submit" class="search-submit-btn" aria-label="Search">
|
<button type="submit" class="search-submit-btn" aria-label="Search">
|
||||||
|
{{#if
|
||||||
|
(or
|
||||||
|
(eq this.mapUi.loadingState.type "text")
|
||||||
|
(eq this.mapUi.loadingState.type "category")
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
<Icon @name="loading-ring" @size={{20}} />
|
||||||
|
{{else}}
|
||||||
<Icon @name="search" @size={{20}} @color="#5f6368" />
|
<Icon @name="search" @size={{20}} @color="#5f6368" />
|
||||||
|
{{/if}}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{{#if this.query}}
|
{{#if this.query}}
|
||||||
|
|||||||
14
app/components/toast.gjs
Normal file
14
app/components/toast.gjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class ToastComponent extends Component {
|
||||||
|
@service toast;
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.toast.isVisible}}
|
||||||
|
<div class="toast-notification">
|
||||||
|
{{this.toast.message}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
||||||
1
app/icons/270-ring.svg
Normal file
1
app/icons/270-ring.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-ember-extension="1"><path d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"><animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12" repeatCount="indefinite"/></path></svg>
|
||||||
|
After Width: | Height: | Size: 464 B |
@@ -9,6 +9,7 @@ export default class SearchRoute extends Route {
|
|||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service storage;
|
@service storage;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service toast;
|
||||||
|
|
||||||
queryParams = {
|
queryParams = {
|
||||||
lat: { refreshModel: true },
|
lat: { refreshModel: true },
|
||||||
@@ -22,9 +23,16 @@ export default class SearchRoute extends Route {
|
|||||||
const lat = params.lat ? parseFloat(params.lat) : null;
|
const lat = params.lat ? parseFloat(params.lat) : null;
|
||||||
const lon = params.lon ? parseFloat(params.lon) : null;
|
const lon = params.lon ? parseFloat(params.lon) : null;
|
||||||
let pois = [];
|
let pois = [];
|
||||||
|
let loadingType = null;
|
||||||
|
let loadingValue = null;
|
||||||
|
|
||||||
|
try {
|
||||||
// Case 0: Category Search (category parameter present)
|
// Case 0: Category Search (category parameter present)
|
||||||
if (params.category && lat && lon) {
|
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.
|
// We need bounds. If we have active map state, use it.
|
||||||
let bounds = this.mapUi.currentBounds;
|
let bounds = this.mapUi.currentBounds;
|
||||||
|
|
||||||
@@ -42,7 +50,12 @@ export default class SearchRoute extends Route {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pois = await this.osm.getCategoryPois(bounds, params.category, lat, lon);
|
pois = await this.osm.getCategoryPois(
|
||||||
|
bounds,
|
||||||
|
params.category,
|
||||||
|
lat,
|
||||||
|
lon
|
||||||
|
);
|
||||||
|
|
||||||
// Sort by distance from center
|
// Sort by distance from center
|
||||||
pois = pois
|
pois = pois
|
||||||
@@ -54,6 +67,10 @@ export default class SearchRoute extends Route {
|
|||||||
}
|
}
|
||||||
// Case 1: Text Search (q parameter present)
|
// Case 1: Text Search (q parameter present)
|
||||||
else if (params.q) {
|
else if (params.q) {
|
||||||
|
loadingType = 'text';
|
||||||
|
loadingValue = params.q;
|
||||||
|
this.mapUi.startLoading(loadingType, loadingValue);
|
||||||
|
|
||||||
// Search with Photon (using lat/lon for bias if available)
|
// Search with Photon (using lat/lon for bias if available)
|
||||||
pois = await this.photon.search(params.q, lat, lon);
|
pois = await this.photon.search(params.q, lat, lon);
|
||||||
|
|
||||||
@@ -80,6 +97,7 @@ export default class SearchRoute extends Route {
|
|||||||
}
|
}
|
||||||
// Case 2: Nearby Search (lat/lon present, no q)
|
// Case 2: Nearby Search (lat/lon present, no q)
|
||||||
else if (lat && lon) {
|
else if (lat && lon) {
|
||||||
|
// Nearby search does NOT trigger loading state (pulse is used instead)
|
||||||
const searchRadius = 50; // Default radius
|
const searchRadius = 50; // Default radius
|
||||||
|
|
||||||
// Fetch POIs from Overpass
|
// Fetch POIs from Overpass
|
||||||
@@ -114,6 +132,11 @@ export default class SearchRoute extends Route {
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a._distance - b._distance);
|
.sort((a, b) => a._distance - b._distance);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (loadingType && loadingValue) {
|
||||||
|
this.mapUi.stopLoading(loadingType, loadingValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of these are already bookmarked
|
// Check if any of these are already bookmarked
|
||||||
// We resolve them to the bookmark version if they exist
|
// We resolve them to the bookmark version if they exist
|
||||||
@@ -170,11 +193,19 @@ export default class SearchRoute extends Route {
|
|||||||
// Ensure pulse is stopped if we reach here
|
// Ensure pulse is stopped if we reach here
|
||||||
this.mapUi.stopSearch();
|
this.mapUi.stopSearch();
|
||||||
this.mapUi.setSearchResults(model);
|
this.mapUi.setSearchResults(model);
|
||||||
|
|
||||||
|
// Store current search params to allow "Up" navigation from place details
|
||||||
|
const { q, category, lat, lon } = this.paramsFor('search');
|
||||||
|
this.mapUi.currentSearch = { q, category, lat, lon };
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
error() {
|
error(error, transition) {
|
||||||
this.mapUi.stopSearch();
|
this.mapUi.stopSearch();
|
||||||
return true; // Bubble error
|
this.toast.show('Search request failed. Please try again.');
|
||||||
|
if (transition) {
|
||||||
|
transition.abort();
|
||||||
|
}
|
||||||
|
return false; // Prevent bubble and stop transition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export default class MapUiService extends Service {
|
|||||||
@tracked selectionOptions = {};
|
@tracked selectionOptions = {};
|
||||||
@tracked preventNextZoom = false;
|
@tracked preventNextZoom = false;
|
||||||
@tracked searchResults = [];
|
@tracked searchResults = [];
|
||||||
|
@tracked currentSearch = null;
|
||||||
|
@tracked loadingState = null;
|
||||||
|
|
||||||
selectPlace(place, options = {}) {
|
selectPlace(place, options = {}) {
|
||||||
this.selectedPlace = place;
|
this.selectedPlace = place;
|
||||||
@@ -31,6 +33,7 @@ export default class MapUiService extends Service {
|
|||||||
|
|
||||||
clearSearchResults() {
|
clearSearchResults() {
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
|
this.currentSearch = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
startSearch() {
|
startSearch() {
|
||||||
@@ -68,4 +71,26 @@ export default class MapUiService extends Service {
|
|||||||
updateBounds(bounds) {
|
updateBounds(bounds) {
|
||||||
this.currentBounds = bounds;
|
this.currentBounds = bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startLoading(type, value) {
|
||||||
|
this.loadingState = { type, value };
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading(type = null, value = null) {
|
||||||
|
// If no arguments provided, force stop (legacy/cleanup)
|
||||||
|
if (!type && !value) {
|
||||||
|
this.loadingState = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only clear if the current state matches the request
|
||||||
|
// This prevents a previous search from clearing the state of a new search
|
||||||
|
if (
|
||||||
|
this.loadingState &&
|
||||||
|
this.loadingState.type === type &&
|
||||||
|
this.loadingState.value === value
|
||||||
|
) {
|
||||||
|
this.loadingState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export default class OsmService extends Service {
|
|||||||
cachedResults = null;
|
cachedResults = null;
|
||||||
lastQueryKey = null;
|
lastQueryKey = null;
|
||||||
|
|
||||||
|
cancelAll() {
|
||||||
|
if (this.controller) {
|
||||||
|
this.controller.abort();
|
||||||
|
this.controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getNearbyPois(lat, lon, radius = 50) {
|
async getNearbyPois(lat, lon, radius = 50) {
|
||||||
const queryKey = `${lat},${lon},${radius}`;
|
const queryKey = `${lat},${lon},${radius}`;
|
||||||
|
|
||||||
@@ -40,15 +47,26 @@ export default class OsmService extends Service {
|
|||||||
];
|
];
|
||||||
const typeKeysQuery = [`~"^(${typeKeys.join('|')})$"~".*"`];
|
const typeKeysQuery = [`~"^(${typeKeys.join('|')})$"~".*"`];
|
||||||
|
|
||||||
|
const negativeFilters = {
|
||||||
|
public_transport: ['stop_area', 'platform'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const negativeFiltersQuery = Object.entries(negativeFilters)
|
||||||
|
.map(([key, values]) => {
|
||||||
|
const valueRegex = `^(${values.join('|')})$`;
|
||||||
|
return `["${key}"!~"${valueRegex}"]`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
[out:json][timeout:25];
|
[out:json][timeout:25];
|
||||||
(
|
(
|
||||||
node(around:${radius},${lat},${lon})
|
node(around:${radius},${lat},${lon})
|
||||||
[${typeKeysQuery}][~"^name"~"."];
|
[${typeKeysQuery}]${negativeFiltersQuery}[~"^name"~"."];
|
||||||
way(around:${radius},${lat},${lon})
|
way(around:${radius},${lat},${lon})
|
||||||
[${typeKeysQuery}][~"^name"~"."];
|
[${typeKeysQuery}]${negativeFiltersQuery}[~"^name"~"."];
|
||||||
relation(around:${radius},${lat},${lon})
|
relation(around:${radius},${lat},${lon})
|
||||||
[${typeKeysQuery}][~"^name"~"."];
|
[${typeKeysQuery}]${negativeFiltersQuery}[~"^name"~"."];
|
||||||
);
|
);
|
||||||
out center;
|
out center;
|
||||||
`.trim();
|
`.trim();
|
||||||
@@ -137,7 +155,7 @@ out center;
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
console.error('Category search failed', e);
|
console.error('Category search failed', e);
|
||||||
return [];
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import { humanizeOsmTag } from '../utils/format-text';
|
|||||||
export default class PhotonService extends Service {
|
export default class PhotonService extends Service {
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
|
controller = null;
|
||||||
|
|
||||||
|
cancelAll() {
|
||||||
|
if (this.controller) {
|
||||||
|
this.controller.abort();
|
||||||
|
this.controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
return this.settings.photonApi;
|
return this.settings.photonApi;
|
||||||
}
|
}
|
||||||
@@ -12,6 +21,12 @@ export default class PhotonService extends Service {
|
|||||||
async search(query, lat, lon, limit = 10) {
|
async search(query, lat, lon, limit = 10) {
|
||||||
if (!query || query.length < 2) return [];
|
if (!query || query.length < 2) return [];
|
||||||
|
|
||||||
|
if (this.controller) {
|
||||||
|
this.controller.abort();
|
||||||
|
}
|
||||||
|
this.controller = new AbortController();
|
||||||
|
const signal = this.controller.signal;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
q: query,
|
q: query,
|
||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
@@ -25,7 +40,7 @@ export default class PhotonService extends Service {
|
|||||||
const url = `${this.baseUrl}?${params.toString()}`;
|
const url = `${this.baseUrl}?${params.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.fetchWithRetry(url);
|
const res = await this.fetchWithRetry(url, { signal });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Photon request failed with status ${res.status}`);
|
throw new Error(`Photon request failed with status ${res.status}`);
|
||||||
}
|
}
|
||||||
@@ -35,6 +50,9 @@ export default class PhotonService extends Service {
|
|||||||
|
|
||||||
return data.features.map((f) => this.normalizeFeature(f));
|
return data.features.map((f) => this.normalizeFeature(f));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e.name === 'AbortError') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
console.error('Photon search error:', e);
|
console.error('Photon search error:', e);
|
||||||
// Return empty array on error so UI doesn't break
|
// Return empty array on error so UI doesn't break
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default class SettingsService extends Service {
|
|||||||
@tracked overpassApi = 'https://overpass-api.de/api/interpreter';
|
@tracked overpassApi = 'https://overpass-api.de/api/interpreter';
|
||||||
@tracked mapKinetic = true;
|
@tracked mapKinetic = true;
|
||||||
@tracked photonApi = 'https://photon.komoot.io/api/';
|
@tracked photonApi = 'https://photon.komoot.io/api/';
|
||||||
|
@tracked showQuickSearchButtons = true;
|
||||||
|
|
||||||
overpassApis = [
|
overpassApis = [
|
||||||
{
|
{
|
||||||
@@ -56,6 +57,13 @@ export default class SettingsService extends Service {
|
|||||||
this.mapKinetic = savedKinetic === 'true';
|
this.mapKinetic = savedKinetic === 'true';
|
||||||
}
|
}
|
||||||
// Default is true (initialized in class field)
|
// Default is true (initialized in class field)
|
||||||
|
|
||||||
|
const savedShowQuickSearch = localStorage.getItem(
|
||||||
|
'marco:show-quick-search'
|
||||||
|
);
|
||||||
|
if (savedShowQuickSearch !== null) {
|
||||||
|
this.showQuickSearchButtons = savedShowQuickSearch === 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverpassApi(url) {
|
updateOverpassApi(url) {
|
||||||
@@ -68,6 +76,11 @@ export default class SettingsService extends Service {
|
|||||||
localStorage.setItem('marco:map-kinetic', String(enabled));
|
localStorage.setItem('marco:map-kinetic', String(enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateShowQuickSearchButtons(enabled) {
|
||||||
|
this.showQuickSearchButtons = enabled;
|
||||||
|
localStorage.setItem('marco:show-quick-search', String(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
updatePhotonApi(url) {
|
updatePhotonApi(url) {
|
||||||
this.photonApi = url;
|
this.photonApi = url;
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/services/toast.js
Normal file
21
app/services/toast.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class ToastService extends Service {
|
||||||
|
@tracked message = null;
|
||||||
|
@tracked isVisible = false;
|
||||||
|
timeoutId = null;
|
||||||
|
|
||||||
|
show(message, duration = 3000) {
|
||||||
|
this.message = message;
|
||||||
|
this.isVisible = true;
|
||||||
|
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.isVisible = false;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1315,3 +1315,42 @@ button.create-place {
|
|||||||
.category-chip:active {
|
.category-chip:active {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category-chip:disabled {
|
||||||
|
opacity: 0.75;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Notification */
|
||||||
|
.toast-notification {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: rgb(51 51 51 / 85%);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||||
|
animation: fade-in-up 0.3s ease-out forwards;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 90%;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { pageTitle } from 'ember-page-title';
|
|||||||
import Map from '#components/map';
|
import Map from '#components/map';
|
||||||
import AppHeader from '#components/app-header';
|
import AppHeader from '#components/app-header';
|
||||||
import AppMenu from '#components/app-menu/index';
|
import AppMenu from '#components/app-menu/index';
|
||||||
|
import Toast from '#components/toast';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
@@ -89,6 +90,8 @@ export default class ApplicationComponent extends Component {
|
|||||||
<AppMenu @onClose={{this.closeAppMenu}} />
|
<AppMenu @onClose={{this.closeAppMenu}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ export default class PlaceTemplate extends Component {
|
|||||||
navigateBack(place) {
|
navigateBack(place) {
|
||||||
// The sidebar calls this with null when "Back" is clicked.
|
// The sidebar calls this with null when "Back" is clicked.
|
||||||
if (place === null) {
|
if (place === null) {
|
||||||
// If we came from search results, go back in history
|
// If we have an active search context, return to it (UP navigation)
|
||||||
if (this.mapUi.returnToSearch) {
|
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
||||||
window.history.back();
|
this.router.transitionTo('search', {
|
||||||
|
queryParams: this.mapUi.currentSearch,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Otherwise just close the sidebar (return to map index)
|
// Otherwise just close the sidebar (return to map index)
|
||||||
this.router.transitionTo('index');
|
this.router.transitionTo('index');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// We don't need to manually set currentSearch here because
|
||||||
|
// it was already set in the route's setupController
|
||||||
this.router.transitionTo('place', place);
|
this.router.transitionTo('place', place);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
|||||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||||
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
||||||
|
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
|
||||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||||
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
||||||
|
import eyeglasses from '@waysidemapping/pinhead/dist/icons/eyeglasses.svg?raw';
|
||||||
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
||||||
|
import fancyMirrorWithReflectionAndStars from '@waysidemapping/pinhead/dist/icons/fancy_mirror_with_reflection_and_stars.svg?raw';
|
||||||
|
import familyRestroomSymbol from '@waysidemapping/pinhead/dist/icons/family_restroom_symbol.svg?raw';
|
||||||
import gift from 'feather-icons/dist/icons/gift.svg?raw';
|
import gift from 'feather-icons/dist/icons/gift.svg?raw';
|
||||||
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
||||||
import heart from 'feather-icons/dist/icons/heart.svg?raw';
|
import heart from 'feather-icons/dist/icons/heart.svg?raw';
|
||||||
@@ -13,11 +17,13 @@ import info from 'feather-icons/dist/icons/info.svg?raw';
|
|||||||
import instagram from 'feather-icons/dist/icons/instagram.svg?raw';
|
import instagram from 'feather-icons/dist/icons/instagram.svg?raw';
|
||||||
import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
||||||
import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
|
import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
|
||||||
|
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
|
||||||
import mail from 'feather-icons/dist/icons/mail.svg?raw';
|
import mail from 'feather-icons/dist/icons/mail.svg?raw';
|
||||||
import map from 'feather-icons/dist/icons/map.svg?raw';
|
import map from 'feather-icons/dist/icons/map.svg?raw';
|
||||||
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
|
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
|
||||||
import menu from 'feather-icons/dist/icons/menu.svg?raw';
|
import menu from 'feather-icons/dist/icons/menu.svg?raw';
|
||||||
import navigation from 'feather-icons/dist/icons/navigation.svg?raw';
|
import navigation from 'feather-icons/dist/icons/navigation.svg?raw';
|
||||||
|
import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw';
|
||||||
import phone from 'feather-icons/dist/icons/phone.svg?raw';
|
import phone from 'feather-icons/dist/icons/phone.svg?raw';
|
||||||
import plus from 'feather-icons/dist/icons/plus.svg?raw';
|
import plus from 'feather-icons/dist/icons/plus.svg?raw';
|
||||||
import search from 'feather-icons/dist/icons/search.svg?raw';
|
import search from 'feather-icons/dist/icons/search.svg?raw';
|
||||||
@@ -25,58 +31,106 @@ import server from 'feather-icons/dist/icons/server.svg?raw';
|
|||||||
import settings from 'feather-icons/dist/icons/settings.svg?raw';
|
import settings from 'feather-icons/dist/icons/settings.svg?raw';
|
||||||
import target from 'feather-icons/dist/icons/target.svg?raw';
|
import target from 'feather-icons/dist/icons/target.svg?raw';
|
||||||
import user from 'feather-icons/dist/icons/user.svg?raw';
|
import user from 'feather-icons/dist/icons/user.svg?raw';
|
||||||
|
import villageBuildings from '@waysidemapping/pinhead/dist/icons/village_buildings.svg?raw';
|
||||||
import x from 'feather-icons/dist/icons/x.svg?raw';
|
import x from 'feather-icons/dist/icons/x.svg?raw';
|
||||||
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
||||||
|
import loadingRing from '../icons/270-ring.svg?raw';
|
||||||
|
|
||||||
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw';
|
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw';
|
||||||
|
import barbell from '@waysidemapping/pinhead/dist/icons/barbell.svg?raw';
|
||||||
|
import banknote from '@waysidemapping/pinhead/dist/icons/banknote.svg?raw';
|
||||||
|
import badgeShieldWithFire from '@waysidemapping/pinhead/dist/icons/badge_shield_with_fire.svg?raw';
|
||||||
import beachUmbrellaInGround from '@waysidemapping/pinhead/dist/icons/beach_umbrella_in_ground.svg?raw';
|
import beachUmbrellaInGround from '@waysidemapping/pinhead/dist/icons/beach_umbrella_in_ground.svg?raw';
|
||||||
import beerMugWithFoam from '@waysidemapping/pinhead/dist/icons/beer_mug_with_foam.svg?raw';
|
import beerMugWithFoam from '@waysidemapping/pinhead/dist/icons/beer_mug_with_foam.svg?raw';
|
||||||
import burgerAndDrinkCupWithStraw from '@waysidemapping/pinhead/dist/icons/burger_and_drink_cup_with_straw.svg?raw';
|
import burgerAndDrinkCupWithStraw from '@waysidemapping/pinhead/dist/icons/burger_and_drink_cup_with_straw.svg?raw';
|
||||||
|
import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
|
||||||
|
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
|
||||||
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
|
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
|
||||||
import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
|
import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
|
||||||
import classicalBuildingWithDomeAndFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_dome_and_flag.svg?raw';
|
import classicalBuildingWithDomeAndFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_dome_and_flag.svg?raw';
|
||||||
import classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw';
|
import classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw';
|
||||||
|
import commercialBuilding from '@waysidemapping/pinhead/dist/icons/commercial_building.svg?raw';
|
||||||
|
import clothesHanger from '@waysidemapping/pinhead/dist/icons/clothes_hanger.svg?raw';
|
||||||
import cleaver from '@waysidemapping/pinhead/dist/icons/cleaver.svg?raw';
|
import cleaver from '@waysidemapping/pinhead/dist/icons/cleaver.svg?raw';
|
||||||
|
import cloth from '@waysidemapping/pinhead/dist/icons/cloth.svg?raw';
|
||||||
|
import cocktail from '@waysidemapping/pinhead/dist/icons/cocktail.svg?raw';
|
||||||
import coffeeBean from '@waysidemapping/pinhead/dist/icons/coffee_bean.svg?raw';
|
import coffeeBean from '@waysidemapping/pinhead/dist/icons/coffee_bean.svg?raw';
|
||||||
import comedyMaskAndTragedyMask from '@waysidemapping/pinhead/dist/icons/comedy_mask_and_tragedy_mask.svg?raw';
|
import comedyMaskAndTragedyMask from '@waysidemapping/pinhead/dist/icons/comedy_mask_and_tragedy_mask.svg?raw';
|
||||||
import croissant from '@waysidemapping/pinhead/dist/icons/croissant.svg?raw';
|
import croissant from '@waysidemapping/pinhead/dist/icons/croissant.svg?raw';
|
||||||
import cupAndSaucer from '@waysidemapping/pinhead/dist/icons/cup_and_saucer.svg?raw';
|
import cupAndSaucer from '@waysidemapping/pinhead/dist/icons/cup_and_saucer.svg?raw';
|
||||||
import donut from '@waysidemapping/pinhead/dist/icons/donut.svg?raw';
|
import donut from '@waysidemapping/pinhead/dist/icons/donut.svg?raw';
|
||||||
import film from '@waysidemapping/pinhead/dist/icons/film.svg?raw';
|
import film from '@waysidemapping/pinhead/dist/icons/film.svg?raw';
|
||||||
|
import fingernailPolished from '@waysidemapping/pinhead/dist/icons/fingernail_polished.svg?raw';
|
||||||
|
import fish from '@waysidemapping/pinhead/dist/icons/fish.svg?raw';
|
||||||
import flagCheckered from '@waysidemapping/pinhead/dist/icons/flag_checkered.svg?raw';
|
import flagCheckered from '@waysidemapping/pinhead/dist/icons/flag_checkered.svg?raw';
|
||||||
|
import flowerBouquet from '@waysidemapping/pinhead/dist/icons/flower_bouquet.svg?raw';
|
||||||
import fort from '@waysidemapping/pinhead/dist/icons/fort.svg?raw';
|
import fort from '@waysidemapping/pinhead/dist/icons/fort.svg?raw';
|
||||||
import forkAndKnife from '@waysidemapping/pinhead/dist/icons/fork_and_knife.svg?raw';
|
import forkAndKnife from '@waysidemapping/pinhead/dist/icons/fork_and_knife.svg?raw';
|
||||||
|
import gravestone from '@waysidemapping/pinhead/dist/icons/gravestone.svg?raw';
|
||||||
|
import grecianVase from '@waysidemapping/pinhead/dist/icons/grecian_vase.svg?raw';
|
||||||
|
import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw';
|
||||||
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
||||||
|
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
|
||||||
|
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
|
||||||
import memorialStoneWithInscription from '@waysidemapping/pinhead/dist/icons/memorial_stone_with_inscription.svg?raw';
|
import memorialStoneWithInscription from '@waysidemapping/pinhead/dist/icons/memorial_stone_with_inscription.svg?raw';
|
||||||
|
import mobilePhoneWithKeypadAndAntenna from '@waysidemapping/pinhead/dist/icons/mobile_phone_with_keypad_and_antenna.svg?raw';
|
||||||
|
import molarTooth from '@waysidemapping/pinhead/dist/icons/molar_tooth.svg?raw';
|
||||||
|
import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
|
||||||
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
|
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
|
||||||
import personCricketBattingAtCricketBall from '@waysidemapping/pinhead/dist/icons/person_cricket_batting_at_cricket_ball.svg?raw';
|
import personCricketBattingAtCricketBall from '@waysidemapping/pinhead/dist/icons/person_cricket_batting_at_cricket_ball.svg?raw';
|
||||||
|
import personBoardingTramWithDestinationDisplayAndPantographOnTramTrack from '@waysidemapping/pinhead/dist/icons/person_boarding_tram_with_destination_display_and_pantograph_on_tram_track.svg?raw';
|
||||||
import personJockeyingRacehorse from '@waysidemapping/pinhead/dist/icons/person_jockeying_racehorse.svg?raw';
|
import personJockeyingRacehorse from '@waysidemapping/pinhead/dist/icons/person_jockeying_racehorse.svg?raw';
|
||||||
|
import personPlayingTennis from '@waysidemapping/pinhead/dist/icons/person_playing_tennis.svg?raw';
|
||||||
import personRunning from '@waysidemapping/pinhead/dist/icons/person_running.svg?raw';
|
import personRunning from '@waysidemapping/pinhead/dist/icons/person_running.svg?raw';
|
||||||
import personSleepingInBed from '@waysidemapping/pinhead/dist/icons/person_sleeping_in_bed.svg?raw';
|
import personSleepingInBed from '@waysidemapping/pinhead/dist/icons/person_sleeping_in_bed.svg?raw';
|
||||||
import personSwimmingInWater from '@waysidemapping/pinhead/dist/icons/person_swimming_in_water.svg?raw';
|
import personSwimmingInWater from '@waysidemapping/pinhead/dist/icons/person_swimming_in_water.svg?raw';
|
||||||
import personSwingingGolfClub from '@waysidemapping/pinhead/dist/icons/person_swinging_golf_club.svg?raw';
|
import personSwingingGolfClub from '@waysidemapping/pinhead/dist/icons/person_swinging_golf_club.svg?raw';
|
||||||
|
import plantInRaisedPlanter from '@waysidemapping/pinhead/dist/icons/plant_in_raised_planter.svg?raw';
|
||||||
|
import placeOfWorshipBuilding from '@waysidemapping/pinhead/dist/icons/place_of_worship_building.svg?raw';
|
||||||
|
import playStructureWithSlide from '@waysidemapping/pinhead/dist/icons/play_structure_with_slide.svg?raw';
|
||||||
|
import policeOfficerWithStopArm from '@waysidemapping/pinhead/dist/icons/police_officer_with_stop_arm.svg?raw';
|
||||||
|
import planeTopRight from '@waysidemapping/pinhead/dist/icons/plane_top_right.svg?raw';
|
||||||
import roundStructureWithFlag from '@waysidemapping/pinhead/dist/icons/round_structure_with_flag.svg?raw';
|
import roundStructureWithFlag from '@waysidemapping/pinhead/dist/icons/round_structure_with_flag.svg?raw';
|
||||||
import sailingShipInWater from '@waysidemapping/pinhead/dist/icons/sailing_ship_in_water.svg?raw';
|
import sailingShipInWater from '@waysidemapping/pinhead/dist/icons/sailing_ship_in_water.svg?raw';
|
||||||
|
import scissorsOpen from '@waysidemapping/pinhead/dist/icons/scissors_open.svg?raw';
|
||||||
|
import shipwreckInWater from '@waysidemapping/pinhead/dist/icons/shipwreck_in_water.svg?raw';
|
||||||
|
import shoppingBag from '@waysidemapping/pinhead/dist/icons/shopping_bag.svg?raw';
|
||||||
import shoppingBasket from '@waysidemapping/pinhead/dist/icons/shopping_basket.svg?raw';
|
import shoppingBasket from '@waysidemapping/pinhead/dist/icons/shopping_basket.svg?raw';
|
||||||
|
import tableTennisPaddle from '@waysidemapping/pinhead/dist/icons/table_tennis_paddle.svg?raw';
|
||||||
|
import tattooMachine from '@waysidemapping/pinhead/dist/icons/tattoo_machine.svg?raw';
|
||||||
|
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 shoppingCart from '@waysidemapping/pinhead/dist/icons/shopping_cart.svg?raw';
|
import shoppingCart from '@waysidemapping/pinhead/dist/icons/shopping_cart.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 womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw';
|
||||||
|
|
||||||
import wikipedia from '../icons/wikipedia.svg?raw';
|
import wikipedia from '../icons/wikipedia.svg?raw';
|
||||||
|
import parkingP from '@waysidemapping/pinhead/dist/icons/parking_p.svg?raw';
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
activity,
|
activity,
|
||||||
angelfish,
|
angelfish,
|
||||||
'arrow-left': arrowLeft,
|
'arrow-left': arrowLeft,
|
||||||
|
barbell,
|
||||||
|
banknote,
|
||||||
|
'badge-shield-with-fire': badgeShieldWithFire,
|
||||||
'beach-umbrella-in-ground': beachUmbrellaInGround,
|
'beach-umbrella-in-ground': beachUmbrellaInGround,
|
||||||
'beer-mug-with-foam': beerMugWithFoam,
|
'beer-mug-with-foam': beerMugWithFoam,
|
||||||
bookmark,
|
bookmark,
|
||||||
|
'boxing-glove-up': boxingGloveUp,
|
||||||
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
|
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
|
||||||
|
bus,
|
||||||
camera,
|
camera,
|
||||||
'check-square': checkSquare,
|
'check-square': checkSquare,
|
||||||
|
'cigarette-with-smoke-curl': cigaretteWithSmokeCurl,
|
||||||
'classical-building': classicalBuilding,
|
'classical-building': classicalBuilding,
|
||||||
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
|
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
|
||||||
'classical-building-with-flag': classicalBuildingWithFlag,
|
'classical-building-with-flag': classicalBuildingWithFlag,
|
||||||
|
'commercial-building': commercialBuilding,
|
||||||
|
'clothes-hanger': clothesHanger,
|
||||||
cleaver,
|
cleaver,
|
||||||
|
cloth,
|
||||||
|
cocktail,
|
||||||
clock,
|
clock,
|
||||||
'coffee-bean': coffeeBean,
|
'coffee-bean': coffeeBean,
|
||||||
'comedy-mask-and-tragedy-mask': comedyMaskAndTragedyMask,
|
'comedy-mask-and-tragedy-mask': comedyMaskAndTragedyMask,
|
||||||
@@ -84,48 +138,83 @@ const ICONS = {
|
|||||||
'cup-and-saucer': cupAndSaucer,
|
'cup-and-saucer': cupAndSaucer,
|
||||||
donut,
|
donut,
|
||||||
edit,
|
edit,
|
||||||
|
eyeglasses,
|
||||||
facebook,
|
facebook,
|
||||||
|
'fancy-mirror-with-reflection-and-stars': fancyMirrorWithReflectionAndStars,
|
||||||
|
'family-restroom-symbol': familyRestroomSymbol,
|
||||||
film,
|
film,
|
||||||
|
'fingernail-polished': fingernailPolished,
|
||||||
|
fish,
|
||||||
'flag-checkered': flagCheckered,
|
'flag-checkered': flagCheckered,
|
||||||
|
'flower-bouquet': flowerBouquet,
|
||||||
'fork-and-knife': forkAndKnife,
|
'fork-and-knife': forkAndKnife,
|
||||||
fort,
|
fort,
|
||||||
gift,
|
gift,
|
||||||
globe,
|
globe,
|
||||||
|
gravestone,
|
||||||
|
'grecian-vase': grecianVase,
|
||||||
|
'greek-cross': greekCross,
|
||||||
heart,
|
heart,
|
||||||
home,
|
home,
|
||||||
'ice-cream-on-cone': iceCreamOnCone,
|
'ice-cream-on-cone': iceCreamOnCone,
|
||||||
info,
|
info,
|
||||||
instagram,
|
instagram,
|
||||||
|
jewel,
|
||||||
'log-in': logIn,
|
'log-in': logIn,
|
||||||
'log-out': logOut,
|
'log-out': logOut,
|
||||||
|
'lowrise-building': lowriseBuilding,
|
||||||
mail,
|
mail,
|
||||||
map,
|
map,
|
||||||
'map-pin': mapPin,
|
'map-pin': mapPin,
|
||||||
|
'market-stall': marketStall,
|
||||||
'memorial-stone-with-inscription': memorialStoneWithInscription,
|
'memorial-stone-with-inscription': memorialStoneWithInscription,
|
||||||
menu,
|
menu,
|
||||||
|
'mobile-phone-with-keypad-and-antenna': mobilePhoneWithKeypadAndAntenna,
|
||||||
|
'molar-tooth': molarTooth,
|
||||||
navigation,
|
navigation,
|
||||||
|
'needle-and-spool-of-thread': needleAndSpoolOfThread,
|
||||||
|
'open-book': openBook,
|
||||||
palace,
|
palace,
|
||||||
'person-cricket-batting-at-cricket-ball': personCricketBattingAtCricketBall,
|
'person-cricket-batting-at-cricket-ball': personCricketBattingAtCricketBall,
|
||||||
|
'person-boarding-tram-with-destination-display-and-pantograph-on-tram-track':
|
||||||
|
personBoardingTramWithDestinationDisplayAndPantographOnTramTrack,
|
||||||
'person-jockeying-racehorse': personJockeyingRacehorse,
|
'person-jockeying-racehorse': personJockeyingRacehorse,
|
||||||
|
'person-playing-tennis': personPlayingTennis,
|
||||||
'person-running': personRunning,
|
'person-running': personRunning,
|
||||||
'person-sleeping-in-bed': personSleepingInBed,
|
'person-sleeping-in-bed': personSleepingInBed,
|
||||||
'person-swimming-in-water': personSwimmingInWater,
|
'person-swimming-in-water': personSwimmingInWater,
|
||||||
'person-swinging-golf-club': personSwingingGolfClub,
|
'person-swinging-golf-club': personSwingingGolfClub,
|
||||||
phone,
|
phone,
|
||||||
|
'plane-top-right': planeTopRight,
|
||||||
|
'plant-in-raised-planter': plantInRaisedPlanter,
|
||||||
|
'place-of-worship-building': placeOfWorshipBuilding,
|
||||||
|
'play-structure-with-slide': playStructureWithSlide,
|
||||||
|
'police-officer-with-stop-arm': policeOfficerWithStopArm,
|
||||||
plus,
|
plus,
|
||||||
'round-structure-with-flag': roundStructureWithFlag,
|
'round-structure-with-flag': roundStructureWithFlag,
|
||||||
'sailing-ship-in-water': sailingShipInWater,
|
'sailing-ship-in-water': sailingShipInWater,
|
||||||
|
'scissors-open': scissorsOpen,
|
||||||
|
'shipwreck-in-water': shipwreckInWater,
|
||||||
|
'shopping-bag': shoppingBag,
|
||||||
search,
|
search,
|
||||||
server,
|
server,
|
||||||
settings,
|
settings,
|
||||||
'shopping-basket': shoppingBasket,
|
'shopping-basket': shoppingBasket,
|
||||||
'shopping-cart': shoppingCart,
|
'shopping-cart': shoppingCart,
|
||||||
|
'table-tennis-paddle': tableTennisPaddle,
|
||||||
|
'tattoo-machine': tattooMachine,
|
||||||
|
toolbox,
|
||||||
target,
|
target,
|
||||||
|
'tree-and-bench-with-backrest': treeAndBenchWithBackrest,
|
||||||
user,
|
user,
|
||||||
|
'village-buildings': villageBuildings,
|
||||||
'wall-hanging-with-mountains-and-sun': wallHangingWithMountainsAndSun,
|
'wall-hanging-with-mountains-and-sun': wallHangingWithMountainsAndSun,
|
||||||
|
'womens-and-mens-restroom-symbol': womensAndMensRestroomSymbol,
|
||||||
wikipedia,
|
wikipedia,
|
||||||
|
parking_p: parkingP,
|
||||||
x,
|
x,
|
||||||
zap,
|
zap,
|
||||||
|
'loading-ring': loadingRing,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILLED_ICONS = [
|
const FILLED_ICONS = [
|
||||||
@@ -136,6 +225,7 @@ const FILLED_ICONS = [
|
|||||||
'shopping-basket',
|
'shopping-basket',
|
||||||
'camera',
|
'camera',
|
||||||
'person-sleeping-in-bed',
|
'person-sleeping-in-bed',
|
||||||
|
'loading-ring',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getIcon(name) {
|
export function getIcon(name) {
|
||||||
|
|||||||
@@ -18,23 +18,80 @@ export const POI_ICON_RULES = [
|
|||||||
{ tags: { amenity: 'restaurant' }, icon: 'fork-and-knife' },
|
{ tags: { amenity: 'restaurant' }, icon: 'fork-and-knife' },
|
||||||
{ tags: { amenity: 'fast_food' }, icon: 'burger-and-drink-cup-with-straw' },
|
{ tags: { amenity: 'fast_food' }, icon: 'burger-and-drink-cup-with-straw' },
|
||||||
{ tags: { amenity: 'pub' }, icon: 'beer-mug-with-foam' },
|
{ tags: { amenity: 'pub' }, icon: 'beer-mug-with-foam' },
|
||||||
|
{ tags: { amenity: 'bar' }, icon: 'cocktail' },
|
||||||
{ tags: { amenity: 'food_court' }, icon: 'fork-and-knife' },
|
{ tags: { amenity: 'food_court' }, icon: 'fork-and-knife' },
|
||||||
|
{ tags: { amenity: 'childcare' }, icon: 'family-restroom-symbol' },
|
||||||
|
{ tags: { amenity: 'community_centre' }, icon: 'family-restroom-symbol' },
|
||||||
|
{ tags: { amenity: 'social_centre' }, icon: 'family-restroom-symbol' },
|
||||||
|
{ tags: { amenity: 'social_facility' }, icon: 'family-restroom-symbol' },
|
||||||
|
|
||||||
|
{ tags: { amenity: 'bank' }, icon: 'banknote' },
|
||||||
|
{ tags: { amenity: 'place_of_worship' }, icon: 'place-of-worship-building' },
|
||||||
|
{ tags: { amenity: 'fire_station' }, icon: 'badge-shield-with-fire' },
|
||||||
|
{ tags: { amenity: 'police' }, icon: 'police-officer-with-stop-arm' },
|
||||||
|
{ tags: { amenity: 'toilets' }, icon: 'womens-and-mens-restroom-symbol' },
|
||||||
|
{ tags: { amenity: 'school' }, icon: 'open-book' },
|
||||||
|
|
||||||
{ tags: { shop: 'coffee' }, icon: 'coffee-bean' },
|
{ tags: { shop: 'coffee' }, icon: 'coffee-bean' },
|
||||||
{ tags: { shop: 'tea' }, icon: 'coffee-bean' },
|
{ tags: { shop: 'tea' }, icon: 'coffee-bean' },
|
||||||
{ tags: { shop: 'pastry' }, icon: 'donut' }, // Pastry shops often have donuts
|
{ tags: { shop: 'pastry' }, icon: 'donut' },
|
||||||
|
|
||||||
// Groceries
|
// Shopping
|
||||||
{ tags: { shop: 'supermarket' }, icon: 'shopping-cart' },
|
{ tags: { shop: 'supermarket' }, icon: 'shopping-cart' },
|
||||||
{ tags: { shop: 'convenience' }, icon: 'shopping-basket' },
|
{ tags: { shop: 'convenience' }, icon: 'shopping-basket' },
|
||||||
{ tags: { shop: 'grocery' }, icon: 'shopping-basket' },
|
{ tags: { shop: 'grocery' }, icon: 'shopping-basket' },
|
||||||
{ tags: { shop: 'greengrocer' }, icon: 'shopping-basket' },
|
{ tags: { shop: 'greengrocer' }, icon: 'shopping-basket' },
|
||||||
{ tags: { shop: 'bakery' }, icon: 'croissant' },
|
{ tags: { shop: 'bakery' }, icon: 'croissant' },
|
||||||
{ tags: { shop: 'butcher' }, icon: 'cleaver' },
|
{ tags: { shop: 'butcher' }, icon: 'cleaver' },
|
||||||
|
{ tags: { shop: 'seafood' }, icon: 'fish' },
|
||||||
{ tags: { shop: 'deli' }, icon: 'shopping-basket' },
|
{ tags: { shop: 'deli' }, icon: 'shopping-basket' },
|
||||||
|
{ tags: { shop: 'clothes' }, icon: 'clothes-hanger' },
|
||||||
|
{ tags: { shop: 'clothing' }, icon: 'clothes-hanger' },
|
||||||
|
{ tags: { shop: 'hairdresser' }, icon: 'scissors-open' },
|
||||||
|
{ tags: { shop: 'optician' }, icon: 'eyeglasses' },
|
||||||
|
{ tags: { shop: 'fabric' }, icon: 'cloth' },
|
||||||
|
{ tags: { shop: 'flea_market' }, icon: 'market-stall' },
|
||||||
|
{ tags: { shop: 'kiosk' }, icon: 'shopping-basket' },
|
||||||
|
{ tags: { shop: 'leather' }, icon: 'shopping-bag' },
|
||||||
|
{ tags: { shop: 'tailor' }, icon: 'needle-and-spool-of-thread' },
|
||||||
|
{ tags: { shop: 'jewelry' }, icon: 'jewel' },
|
||||||
|
{ tags: { shop: 'jewellery' }, icon: 'jewel' },
|
||||||
|
{ tags: { shop: 'tobacco' }, icon: 'cigarette-with-smoke-curl' },
|
||||||
|
{ tags: { shop: 'cannabis' }, icon: 'cigarette-with-smoke-curl' },
|
||||||
|
{ tags: { shop: 'florist' }, icon: 'flower-bouquet' },
|
||||||
|
{ tags: { shop: 'garden_centre' }, icon: 'plant-in-raised-planter' },
|
||||||
|
{ tags: { shop: 'estate_agent' }, icon: 'village-buildings' },
|
||||||
|
{
|
||||||
|
tags: { shop: 'mobile_phone' },
|
||||||
|
icon: 'mobile-phone-with-keypad-and-antenna',
|
||||||
|
},
|
||||||
|
{ tags: { beauty: 'nails' }, icon: 'fingernail-polished' },
|
||||||
|
{ tags: { shop: 'tattoo' }, icon: 'tattoo-machine' },
|
||||||
|
{
|
||||||
|
tags: { shop: 'beauty' },
|
||||||
|
icon: 'fancy-mirror-with-reflection-and-stars',
|
||||||
|
},
|
||||||
|
{ tags: { craft: 'tailor' }, icon: 'needle-and-spool-of-thread' },
|
||||||
|
{ tags: { office: 'estate_agent' }, icon: 'village-buildings' },
|
||||||
|
{ tags: { office: true }, icon: 'commercial-building' },
|
||||||
|
{ tags: { craft: true }, icon: 'toolbox' },
|
||||||
|
{ tags: { shop: true }, icon: 'shopping-bag' },
|
||||||
|
|
||||||
// Natural
|
// Natural
|
||||||
{ tags: { natural: 'beach' }, icon: 'beach-umbrella-in-ground' },
|
{ tags: { natural: 'beach' }, icon: 'beach-umbrella-in-ground' },
|
||||||
|
{ tags: { leisure: 'park' }, icon: 'tree-and-bench-with-backrest' },
|
||||||
|
{ tags: { leisure: 'playground' }, icon: 'play-structure-with-slide' },
|
||||||
|
|
||||||
|
// Transport
|
||||||
|
{ tags: { aeroway: 'aerodrome' }, icon: 'plane-top-right' },
|
||||||
|
{ tags: { aeroway: 'heliport' }, icon: 'plane-top-right' },
|
||||||
|
{ tags: { aeroway: 'helipad' }, icon: 'plane-top-right' },
|
||||||
|
{ tags: { highway: 'bus_stop' }, icon: 'bus' },
|
||||||
|
{ tags: { bus: true }, icon: 'bus' },
|
||||||
|
{
|
||||||
|
tags: { railway: 'tram_stop' },
|
||||||
|
icon: 'person-boarding-tram-with-destination-display-and-pantograph-on-tram-track',
|
||||||
|
},
|
||||||
|
|
||||||
// Tourism
|
// Tourism
|
||||||
{ tags: { tourism: 'museum' }, icon: 'classical-building' },
|
{ tags: { tourism: 'museum' }, icon: 'classical-building' },
|
||||||
@@ -54,13 +111,18 @@ export const POI_ICON_RULES = [
|
|||||||
{ 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' },
|
||||||
{ tags: { historic: 'archaeological_site' }, icon: 'camera' },
|
{ tags: { historic: 'archaeological_site' }, icon: 'grecian-vase' },
|
||||||
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
|
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
|
||||||
|
{ tags: { historic: 'tomb' }, icon: 'gravestone' },
|
||||||
{
|
{
|
||||||
tags: { historic: 'monument' },
|
tags: { historic: 'monument' },
|
||||||
icon: 'classical-building-with-dome-and-flag',
|
icon: 'classical-building-with-dome-and-flag',
|
||||||
},
|
},
|
||||||
{ tags: { historic: 'ship' }, icon: 'sailing-ship-in-water' },
|
{ tags: { historic: 'ship' }, icon: 'sailing-ship-in-water' },
|
||||||
|
{ tags: { historic: 'wreck' }, icon: 'shipwreck-in-water' },
|
||||||
|
{ tags: { historic: 'ruins' }, icon: 'camera' },
|
||||||
|
{ tags: { historic: 'ruin' }, icon: 'camera' },
|
||||||
|
{ tags: { historic: 'yes' }, icon: 'camera' },
|
||||||
|
|
||||||
// Accommodation
|
// Accommodation
|
||||||
{ tags: { tourism: 'hotel' }, icon: 'person-sleeping-in-bed' },
|
{ tags: { tourism: 'hotel' }, icon: 'person-sleeping-in-bed' },
|
||||||
@@ -76,15 +138,38 @@ export const POI_ICON_RULES = [
|
|||||||
tags: { sport: 'cricket' },
|
tags: { sport: 'cricket' },
|
||||||
icon: 'person-cricket-batting-at-cricket-ball',
|
icon: 'person-cricket-batting-at-cricket-ball',
|
||||||
},
|
},
|
||||||
|
{ tags: { sport: 'boxing' }, icon: 'boxing-glove-up' },
|
||||||
|
{ tags: { sport: 'martial_arts' }, icon: 'boxing-glove-up' },
|
||||||
|
{ tags: { sport: 'tennis' }, icon: 'person-playing-tennis' },
|
||||||
|
{ tags: { sport: 'squash' }, icon: 'person-playing-tennis' },
|
||||||
|
{ tags: { sport: 'padel' }, icon: 'person-playing-tennis' },
|
||||||
|
{ tags: { sport: 'table_tennis' }, icon: 'table-tennis-paddle' },
|
||||||
{ tags: { leisure: 'water_park' }, icon: 'person-swimming-in-water' },
|
{ tags: { leisure: 'water_park' }, icon: 'person-swimming-in-water' },
|
||||||
|
{ tags: { sport: 'swimming' }, icon: 'person-swimming-in-water' },
|
||||||
{ tags: { sport: 'golf' }, icon: 'person-swinging-golf-club' },
|
{ tags: { sport: 'golf' }, icon: 'person-swinging-golf-club' },
|
||||||
{ tags: { leisure: 'golf_course' }, icon: 'person-swinging-golf-club' },
|
{ tags: { leisure: 'golf_course' }, icon: 'person-swinging-golf-club' },
|
||||||
{ tags: { sport: 'horse_racing' }, icon: 'person-jockeying-racehorse' },
|
{ tags: { sport: 'horse_racing' }, icon: 'person-jockeying-racehorse' },
|
||||||
{ tags: { leisure: 'stadium' }, icon: 'round-structure-with-flag' },
|
{ tags: { sport: 'fitness' }, icon: 'barbell' },
|
||||||
{ tags: { sport: 'stadium' }, icon: 'round-structure-with-flag' },
|
{ tags: { sport: 'fitness_centre' }, icon: 'barbell' },
|
||||||
|
{ tags: { leisure: 'fitness_centre' }, icon: 'barbell' },
|
||||||
|
|
||||||
|
{ tags: { sport: 'stadium' }, icon: 'round-structure-with-flag' },
|
||||||
|
{ tags: { leisure: 'stadium' }, icon: 'round-structure-with-flag' },
|
||||||
{ tags: { leisure: 'sports_centre' }, icon: 'person-running' },
|
{ tags: { leisure: 'sports_centre' }, icon: 'person-running' },
|
||||||
{ tags: { sport: 'fitness_centre' }, icon: 'person-running' },
|
{ tags: { leisure: 'pitch' }, icon: 'person-running' },
|
||||||
|
{ tags: { sport: true }, icon: 'person-running' },
|
||||||
|
|
||||||
|
// Healthcare
|
||||||
|
{ tags: { amenity: 'dentist' }, icon: 'molar-tooth' },
|
||||||
|
{ tags: { healthcare: 'dentist' }, icon: 'molar-tooth' },
|
||||||
|
{ tags: { healthcare: true }, icon: 'greek-cross' },
|
||||||
|
|
||||||
|
// Parking
|
||||||
|
{ tags: { amenity: 'parking' }, icon: 'parking_p' },
|
||||||
|
|
||||||
|
// Buildings
|
||||||
|
{ tags: { building: 'commercial' }, icon: 'commercial-building' },
|
||||||
|
{ tags: { building: 'apartments' }, icon: 'lowrise-building' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +192,12 @@ export function getIconNameForTags(tags) {
|
|||||||
// Check for exact match or if value is in a semicolon-separated list
|
// Check for exact match or if value is in a semicolon-separated list
|
||||||
// e.g. "donut;coffee_shop"
|
// e.g. "donut;coffee_shop"
|
||||||
const values = tagValue.split(';').map((v) => v.trim());
|
const values = tagValue.split(';').map((v) => v.trim());
|
||||||
|
|
||||||
|
// If expectedValue is boolean true, any value is a match
|
||||||
|
if (expectedValue === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!values.includes(expectedValue)) {
|
if (!values.includes(expectedValue)) {
|
||||||
match = false;
|
match = false;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export const POI_CATEGORIES = [
|
|||||||
id: 'restaurants',
|
id: 'restaurants',
|
||||||
label: 'Restaurants',
|
label: 'Restaurants',
|
||||||
icon: 'fork-and-knife',
|
icon: 'fork-and-knife',
|
||||||
filter: ['["amenity"~"^(restaurant|fast_food|food_court|pub|cafe)$"]'],
|
filter: [
|
||||||
|
'["amenity"~"^(restaurant|fast_food|food_court|pub|cafe)$"]["cuisine"!~"coffee"]',
|
||||||
|
],
|
||||||
types: ['node', 'way'],
|
types: ['node', 'way'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,6 +47,7 @@ export const POI_CATEGORIES = [
|
|||||||
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
|
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
|
||||||
'["leisure"~"^(sports_centre|stadium|water_park)$"]',
|
'["leisure"~"^(sports_centre|stadium|water_park)$"]',
|
||||||
'["historic"]',
|
'["historic"]',
|
||||||
|
'["shop"="flea_market"]',
|
||||||
],
|
],
|
||||||
types: ['node', 'way', 'relation'],
|
types: ['node', 'way', 'relation'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.16.0",
|
"version": "1.17.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
release/assets/main-DM7YMuyX.js
Normal file
2
release/assets/main-DM7YMuyX.js
Normal file
File diff suppressed because one or more lines are too long
1
release/assets/main-OLSOzTKA.css
Normal file
1
release/assets/main-OLSOzTKA.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -39,8 +39,8 @@
|
|||||||
<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-C4F17h3W.js"></script>
|
<script type="module" crossorigin src="/assets/main-DM7YMuyX.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-CKp1bFPU.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-OLSOzTKA.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
182
tests/acceptance/map-search-reset-test.js
Normal file
182
tests/acceptance/map-search-reset-test.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { visit, currentURL, waitFor, triggerEvent } from '@ember/test-helpers';
|
||||||
|
import { setupApplicationTest } from 'marco/tests/helpers';
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
module('Acceptance | map search reset', function (hooks) {
|
||||||
|
setupApplicationTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
// Seed localStorage with a high zoom level to ensure map is interactive
|
||||||
|
const highZoomState = {
|
||||||
|
center: [13.4, 52.5],
|
||||||
|
zoom: 18,
|
||||||
|
};
|
||||||
|
window.localStorage.setItem(
|
||||||
|
'marco:map-view',
|
||||||
|
JSON.stringify(highZoomState)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stub window.fetch using Sinon
|
||||||
|
// We want to intercept map style requests and let everything else through
|
||||||
|
this.fetchStub = sinon.stub(window, 'fetch');
|
||||||
|
|
||||||
|
this.fetchStub.callsFake(async (input, init) => {
|
||||||
|
let url = input;
|
||||||
|
if (typeof input === 'object' && input !== null && 'url' in input) {
|
||||||
|
url = input.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof url === 'string' &&
|
||||||
|
url.includes('tiles.openfreemap.org/styles/liberty')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
version: 8,
|
||||||
|
name: 'Liberty',
|
||||||
|
sources: {
|
||||||
|
openmaptiles: {
|
||||||
|
type: 'vector',
|
||||||
|
url: 'https://tiles.openfreemap.org/planet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'background',
|
||||||
|
type: 'background',
|
||||||
|
paint: {
|
||||||
|
'background-color': '#123456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
glyphs:
|
||||||
|
'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
|
||||||
|
sprite: 'https://tiles.openfreemap.org/sprites/liberty',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through to the original implementation
|
||||||
|
return this.fetchStub.wrappedMethod(input, init);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(function () {
|
||||||
|
window.localStorage.removeItem('marco:map-view');
|
||||||
|
// Restore the original fetch
|
||||||
|
this.fetchStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking the map clears the category search parameter', async function (assert) {
|
||||||
|
// Mock OSM Service
|
||||||
|
class MockOsmService extends Service {
|
||||||
|
async getCategoryPois() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Cafe Test',
|
||||||
|
lat: 52.52,
|
||||||
|
lon: 13.405,
|
||||||
|
osmId: '123',
|
||||||
|
osmType: 'N',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
async getNearbyPois() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:osm', MockOsmService);
|
||||||
|
|
||||||
|
// Mock Storage
|
||||||
|
this.owner.register(
|
||||||
|
'service:storage',
|
||||||
|
class extends Service {
|
||||||
|
rs = { on: () => {} };
|
||||||
|
placesInView = [];
|
||||||
|
savedPlaces = [];
|
||||||
|
loadPlacesInBounds() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
findPlaceById() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Visit a category search URL
|
||||||
|
await visit('/search?category=coffee&lat=52.52&lon=13.405');
|
||||||
|
|
||||||
|
assert.dom('.sidebar-header').includesText('Results');
|
||||||
|
assert.ok(
|
||||||
|
currentURL().includes('category=coffee'),
|
||||||
|
'URL should have category param'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Click the map (First click closes sidebar)
|
||||||
|
await waitFor('canvas', { timeout: 2000 });
|
||||||
|
|
||||||
|
const canvas = document.querySelector('canvas');
|
||||||
|
if (canvas) {
|
||||||
|
// First Click (Close Sidebar)
|
||||||
|
await triggerEvent(canvas, 'pointerdown', {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200,
|
||||||
|
button: 0,
|
||||||
|
isPrimary: true,
|
||||||
|
});
|
||||||
|
await triggerEvent(canvas, 'pointerup', {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200,
|
||||||
|
button: 0,
|
||||||
|
isPrimary: true,
|
||||||
|
});
|
||||||
|
await triggerEvent(canvas, 'click', {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for transition to index
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
assert.strictEqual(
|
||||||
|
currentURL(),
|
||||||
|
'/',
|
||||||
|
'Should have transitioned to index (closed sidebar)'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Second Click (Start new search)
|
||||||
|
// Click slightly differently to ensure fresh event
|
||||||
|
await triggerEvent(canvas, 'pointerdown', {
|
||||||
|
clientX: 250,
|
||||||
|
clientY: 250,
|
||||||
|
button: 0,
|
||||||
|
isPrimary: true,
|
||||||
|
});
|
||||||
|
await triggerEvent(canvas, 'pointerup', {
|
||||||
|
clientX: 250,
|
||||||
|
clientY: 250,
|
||||||
|
button: 0,
|
||||||
|
isPrimary: true,
|
||||||
|
});
|
||||||
|
await triggerEvent(canvas, 'click', {
|
||||||
|
clientX: 250,
|
||||||
|
clientY: 250,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Wait for transition
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
const newUrl = currentURL();
|
||||||
|
assert.notOk(
|
||||||
|
newUrl.includes('category=coffee'),
|
||||||
|
`New URL ${newUrl} should not contain category param`
|
||||||
|
);
|
||||||
|
assert.ok(newUrl.includes('/search'), 'Should be on search route');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -64,11 +64,9 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
this.owner.register('service:storage', MockStorageService);
|
this.owner.register('service:storage', MockStorageService);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('navigating from search results to place and back uses history', async function (assert) {
|
test('navigating from search results to place and back returns to search', async function (assert) {
|
||||||
const mapUi = this.owner.lookup('service:map-ui');
|
const mapUi = this.owner.lookup('service:map-ui');
|
||||||
const backStub = sinon.stub(window.history, 'back');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await visit('/search?lat=1&lon=1');
|
await visit('/search?lat=1&lon=1');
|
||||||
assert.strictEqual(currentURL(), '/search?lat=1&lon=1');
|
assert.strictEqual(currentURL(), '/search?lat=1&lon=1');
|
||||||
|
|
||||||
@@ -79,10 +77,11 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
// Click the back button in the sidebar
|
// Click the back button in the sidebar
|
||||||
await click('.back-btn');
|
await click('.back-btn');
|
||||||
|
|
||||||
assert.true(backStub.calledOnce, 'window.history.back() was called');
|
assert.strictEqual(
|
||||||
} finally {
|
currentURL(),
|
||||||
backStub.restore();
|
'/search?lat=1&lon=1',
|
||||||
}
|
'Returned to search results'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('closing the sidebar resets the returnToSearch flag', async function (assert) {
|
test('closing the sidebar resets the returnToSearch flag', async function (assert) {
|
||||||
|
|||||||
136
tests/acceptance/search-loading-test.js
Normal file
136
tests/acceptance/search-loading-test.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { visit, click, fillIn, currentURL } from '@ember/test-helpers';
|
||||||
|
import { setupApplicationTest } from 'marco/tests/helpers';
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import { Promise } from 'rsvp';
|
||||||
|
|
||||||
|
class MockPhotonService extends Service {
|
||||||
|
cancelAll() {}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
if (query === 'slow') {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Test Place',
|
||||||
|
lat: 1,
|
||||||
|
lon: 1,
|
||||||
|
osmId: '123',
|
||||||
|
osmType: 'node',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockOsmService extends Service {
|
||||||
|
cancelAll() {}
|
||||||
|
|
||||||
|
async getCategoryPois(bounds, category) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
if (category === 'slow_category') {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
async getNearbyPois() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module('Acceptance | search loading', function (hooks) {
|
||||||
|
setupApplicationTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.owner.register('service:photon', MockPhotonService);
|
||||||
|
this.owner.register('service:osm', MockOsmService);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search shows loading indicator but nearby search does not', async function (assert) {
|
||||||
|
const mapUi = this.owner.lookup('service:map-ui');
|
||||||
|
|
||||||
|
// 1. Text Search
|
||||||
|
// Start a search and check for loading state immediately
|
||||||
|
const searchPromise = visit('/search?q=slow');
|
||||||
|
|
||||||
|
// We can't easily check the DOM mid-transition in acceptance tests without complicated helpers,
|
||||||
|
// so we check the service state which drives the UI.
|
||||||
|
// Wait a tiny bit for the route to start processing
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
{ type: 'text', value: 'slow' },
|
||||||
|
'Loading state is set for text search'
|
||||||
|
);
|
||||||
|
|
||||||
|
await searchPromise;
|
||||||
|
assert.strictEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
null,
|
||||||
|
'Loading state is cleared after text search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Category Search
|
||||||
|
const catPromise = visit('/search?category=slow_category&lat=1&lon=1');
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
{ type: 'category', value: 'slow_category' },
|
||||||
|
'Loading state is set for category search'
|
||||||
|
);
|
||||||
|
|
||||||
|
await catPromise;
|
||||||
|
assert.strictEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
null,
|
||||||
|
'Loading state is cleared after category search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Nearby Search
|
||||||
|
await visit('/search?lat=1&lon=1');
|
||||||
|
assert.strictEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
null,
|
||||||
|
'Loading state is NOT set for nearby search'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearing search stops loading indicator', async function (assert) {
|
||||||
|
const mapUi = this.owner.lookup('service:map-ui');
|
||||||
|
|
||||||
|
// 1. Start from index
|
||||||
|
await visit('/');
|
||||||
|
|
||||||
|
// 2. Type "slow" to trigger autocomplete (which is async)
|
||||||
|
await fillIn('.search-input', 'slow');
|
||||||
|
|
||||||
|
// 3. Submit search to trigger route loading
|
||||||
|
click('.search-submit-btn'); // Intentionally no await to not block on transition
|
||||||
|
|
||||||
|
// Wait for loading state to activate
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
{ type: 'text', value: 'slow' },
|
||||||
|
'Loading state is set'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Click the clear button (should be visible since input has value)
|
||||||
|
await click('.search-clear-btn');
|
||||||
|
|
||||||
|
// Verify loading state is cleared immediately
|
||||||
|
assert.strictEqual(
|
||||||
|
mapUi.loadingState,
|
||||||
|
null,
|
||||||
|
'Loading state is cleared immediately after clicking clear'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify we are back on index (or at least query is gone)
|
||||||
|
assert.strictEqual(currentURL(), '/', 'Navigated to index');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -218,4 +218,56 @@ module('Acceptance | search', function (hooks) {
|
|||||||
// Ensure it shows "Results" not "Nearby"
|
// Ensure it shows "Results" not "Nearby"
|
||||||
assert.dom('.sidebar-header h2').includesText('Results');
|
assert.dom('.sidebar-header h2').includesText('Results');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('search error handling prevents opening empty panel and shows toast', async function (assert) {
|
||||||
|
// Mock Osm Service to throw an error
|
||||||
|
class MockOsmService extends Service {
|
||||||
|
async getCategoryPois() {
|
||||||
|
throw new Error('Overpass request failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:osm', MockOsmService);
|
||||||
|
|
||||||
|
class MockStorageService extends Service {
|
||||||
|
savedPlaces = [];
|
||||||
|
findPlaceById() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
isPlaceSaved() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rs = { on: () => {} };
|
||||||
|
placesInView = [];
|
||||||
|
loadPlacesInBounds() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:storage', MockStorageService);
|
||||||
|
|
||||||
|
class MockMapService extends Service {
|
||||||
|
getBounds() {
|
||||||
|
return {
|
||||||
|
minLat: 52.5,
|
||||||
|
minLon: 13.4,
|
||||||
|
maxLat: 52.6,
|
||||||
|
maxLon: 13.5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:map', MockMapService);
|
||||||
|
|
||||||
|
await visit('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await visit('/search?category=coffee&lat=52.52&lon=13.405');
|
||||||
|
} catch {
|
||||||
|
// Aborted transition throws, which is expected
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.dom('.toast-notification').exists('Toast should be visible');
|
||||||
|
assert
|
||||||
|
.dom('.toast-notification')
|
||||||
|
.hasText('Search request failed. Please try again.');
|
||||||
|
assert.dom('.places-sidebar').doesNotExist('Results panel should not open');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ module('Integration | Component | app-header', function (hooks) {
|
|||||||
class MockRouterService extends Service {}
|
class MockRouterService extends Service {}
|
||||||
class MockMapUiService extends Service {}
|
class MockMapUiService extends Service {}
|
||||||
class MockMapService extends Service {}
|
class MockMapService extends Service {}
|
||||||
|
class MockSettingsService extends Service {
|
||||||
|
showQuickSearchButtons = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.owner.register('service:photon', MockPhotonService);
|
this.owner.register('service:photon', MockPhotonService);
|
||||||
this.owner.register('service:router', MockRouterService);
|
this.owner.register('service:router', MockRouterService);
|
||||||
this.owner.register('service:map-ui', MockMapUiService);
|
this.owner.register('service:map-ui', MockMapUiService);
|
||||||
this.owner.register('service:map', MockMapService);
|
this.owner.register('service:map', MockMapService);
|
||||||
|
this.owner.register('service:settings', MockSettingsService);
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
||||||
@@ -43,11 +47,15 @@ module('Integration | Component | app-header', function (hooks) {
|
|||||||
currentCenter = null;
|
currentCenter = null;
|
||||||
}
|
}
|
||||||
class MockMapService extends Service {}
|
class MockMapService extends Service {}
|
||||||
|
class MockSettingsService extends Service {
|
||||||
|
showQuickSearchButtons = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.owner.register('service:photon', MockPhotonService);
|
this.owner.register('service:photon', MockPhotonService);
|
||||||
this.owner.register('service:router', MockRouterService);
|
this.owner.register('service:router', MockRouterService);
|
||||||
this.owner.register('service:map-ui', MockMapUiService);
|
this.owner.register('service:map-ui', MockMapUiService);
|
||||||
this.owner.register('service:map', MockMapService);
|
this.owner.register('service:map', MockMapService);
|
||||||
|
this.owner.register('service:settings', MockSettingsService);
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
||||||
|
|||||||
68
tests/unit/services/map-ui-test.js
Normal file
68
tests/unit/services/map-ui-test.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'marco/tests/helpers';
|
||||||
|
|
||||||
|
module('Unit | Service | map-ui', function (hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
test('it handles loading state correctly', function (assert) {
|
||||||
|
let service = this.owner.lookup('service:map-ui');
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
assert.strictEqual(
|
||||||
|
service.loadingState,
|
||||||
|
null,
|
||||||
|
'loadingState starts as null'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start loading search A
|
||||||
|
service.startLoading('search', 'A');
|
||||||
|
assert.deepEqual(
|
||||||
|
service.loadingState,
|
||||||
|
{ type: 'search', value: 'A' },
|
||||||
|
'loadingState is set to search A'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stop loading search A (successful case)
|
||||||
|
service.stopLoading('search', 'A');
|
||||||
|
assert.strictEqual(
|
||||||
|
service.loadingState,
|
||||||
|
null,
|
||||||
|
'loadingState is cleared when stopped with matching parameters'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it handles race condition: stopLoading only clears if parameters match', function (assert) {
|
||||||
|
let service = this.owner.lookup('service:map-ui');
|
||||||
|
|
||||||
|
// 1. Start loading search A
|
||||||
|
service.startLoading('search', 'A');
|
||||||
|
assert.deepEqual(service.loadingState, { type: 'search', value: 'A' });
|
||||||
|
|
||||||
|
// 2. Start loading search B (interruption)
|
||||||
|
// In a real app, search B would start before search A finishes.
|
||||||
|
service.startLoading('search', 'B');
|
||||||
|
assert.deepEqual(
|
||||||
|
service.loadingState,
|
||||||
|
{ type: 'search', value: 'B' },
|
||||||
|
'loadingState updates to search B'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Search A finishes and tries to stop loading
|
||||||
|
// The service should ignore this because current loading state is for B
|
||||||
|
service.stopLoading('search', 'A');
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
service.loadingState,
|
||||||
|
{ type: 'search', value: 'B' },
|
||||||
|
'loadingState remains search B even after stopping search A'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Search B finishes
|
||||||
|
service.stopLoading('search', 'B');
|
||||||
|
assert.strictEqual(
|
||||||
|
service.loadingState,
|
||||||
|
null,
|
||||||
|
'loadingState is cleared when search B stops'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
tests/unit/utils/osm-icons-test.js
Normal file
39
tests/unit/utils/osm-icons-test.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { getIconNameForTags } from 'marco/utils/osm-icons';
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
|
module('Unit | Utility | osm-icons', function () {
|
||||||
|
test('it returns molar-tooth for amenity=dentist', function (assert) {
|
||||||
|
let result = getIconNameForTags({ amenity: 'dentist' });
|
||||||
|
assert.strictEqual(result, 'molar-tooth');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns molar-tooth for healthcare=dentist', function (assert) {
|
||||||
|
let result = getIconNameForTags({ healthcare: 'dentist' });
|
||||||
|
assert.strictEqual(result, 'molar-tooth');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns greek-cross for healthcare=hospital (catch-all)', function (assert) {
|
||||||
|
let result = getIconNameForTags({ healthcare: 'hospital' });
|
||||||
|
assert.strictEqual(result, 'greek-cross');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns greek-cross for healthcare=yes (catch-all)', function (assert) {
|
||||||
|
let result = getIconNameForTags({ healthcare: 'yes' });
|
||||||
|
assert.strictEqual(result, 'greek-cross');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns shopping-basket for known shop types like convenience', function (assert) {
|
||||||
|
let result = getIconNameForTags({ shop: 'convenience' });
|
||||||
|
assert.strictEqual(result, 'shopping-basket');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns shopping-bag for unknown shop types (catch-all)', function (assert) {
|
||||||
|
let result = getIconNameForTags({ shop: 'unknown_shop_type' });
|
||||||
|
assert.strictEqual(result, 'shopping-bag');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns null for unknown tags', function (assert) {
|
||||||
|
let result = getIconNameForTags({ foo: 'bar' });
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user