Compare commits
2 Commits
5b37894821
...
438bf0c31c
| Author | SHA1 | Date | |
|---|---|---|---|
|
438bf0c31c
|
|||
|
af57e7fe57
|
@@ -2,7 +2,7 @@ import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { modifier } from 'ember-modifier';
|
||||
import 'ol/ol.css';
|
||||
import Map from 'ol/Map.js';
|
||||
import OlMap from 'ol/Map.js';
|
||||
import { defaults as defaultControls, Control } from 'ol/control.js';
|
||||
import { defaults as defaultInteractions, DragPan } from 'ol/interaction.js';
|
||||
import Kinetic from 'ol/Kinetic.js';
|
||||
@@ -16,8 +16,10 @@ import Feature from 'ol/Feature.js';
|
||||
import GeoJSON from 'ol/format/GeoJSON.js';
|
||||
import Point from 'ol/geom/Point.js';
|
||||
import Geolocation from 'ol/Geolocation.js';
|
||||
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
|
||||
import { Style, Circle, Fill, Stroke, Icon } from 'ol/style.js';
|
||||
import { apply } from 'ol-mapbox-style';
|
||||
import { getIcon } from '../utils/icons';
|
||||
import { getIconNameForTags } from '../utils/osm-icons';
|
||||
|
||||
export default class MapComponent extends Component {
|
||||
@service osm;
|
||||
@@ -28,6 +30,7 @@ export default class MapComponent extends Component {
|
||||
|
||||
mapInstance;
|
||||
bookmarkSource;
|
||||
searchResultsSource;
|
||||
selectedShapeSource;
|
||||
searchOverlay;
|
||||
searchOverlayElement;
|
||||
@@ -110,6 +113,101 @@ export default class MapComponent extends Component {
|
||||
zIndex: 10, // Ensure it sits above the map tiles
|
||||
});
|
||||
|
||||
// Create a vector source and layer for search results
|
||||
this.searchResultsSource = new VectorSource();
|
||||
const cachedIconUrls = new Map();
|
||||
|
||||
const searchResultStyle = (feature) => {
|
||||
const originalPlace = feature.get('originalPlace');
|
||||
// Some search results might be just the place object without separate tags
|
||||
// If it's a raw place object, it might have osmTags property.
|
||||
// Or it might be the tags object itself.
|
||||
const tags = originalPlace.osmTags || originalPlace;
|
||||
const iconName = getIconNameForTags(tags);
|
||||
|
||||
// Use 'default' key for the standard red dot marker. Use iconName as key if present.
|
||||
const cacheKey = iconName || 'default';
|
||||
|
||||
if (!cachedIconUrls.has(cacheKey)) {
|
||||
const markerColor =
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--marker-color-primary')
|
||||
.trim() || '#ea4335';
|
||||
|
||||
// Default content: Red circle
|
||||
let innerContent = `<circle cx="12" cy="12" r="8" fill="${markerColor}"/>`;
|
||||
|
||||
if (iconName) {
|
||||
const rawSvg = getIcon(iconName);
|
||||
if (rawSvg) {
|
||||
// Pinhead icons are usually 15x15 viewBox="0 0 15 15".
|
||||
// We want to center it on 12,12.
|
||||
// A 12x12 icon centered at 12,12 means top-left at 6,6.
|
||||
// However, since we are embedding a new SVG, we can just use x/y/width/height.
|
||||
// But we need to strip the outer <svg> tag to embed the paths cleanly if we want full control,
|
||||
// or we can nest the SVG. Nesting is safer.
|
||||
|
||||
// The rawSvg string contains <svg ...>...</svg>.
|
||||
// We want to make it white. We can add a group with fill="white".
|
||||
// But if the SVG has fill attributes, they override. Pinhead icons usually don't have fills.
|
||||
|
||||
// Let's strip the outer SVG tag to get the path content.
|
||||
let content = rawSvg.trim();
|
||||
const svgStart = content.indexOf('<svg');
|
||||
const svgEnd = content.indexOf('>', svgStart);
|
||||
const contentStart = svgEnd + 1;
|
||||
const contentEnd = content.lastIndexOf('</svg>');
|
||||
|
||||
if (svgStart !== -1 && contentEnd !== -1) {
|
||||
content = content.substring(contentStart, contentEnd);
|
||||
}
|
||||
|
||||
// We render the red circle background, then the icon on top.
|
||||
// Icon is scaled down slightly to fit nicely inside the circle.
|
||||
// 15x15 scaled by 0.8 is 12x12.
|
||||
// Translate to 6,6 to center.
|
||||
innerContent = `
|
||||
<circle cx="12" cy="12" r="8" fill="${markerColor}"/>
|
||||
<g transform="translate(6, 6) scale(0.8)" fill="white">
|
||||
${content}
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 40" width="40" height="50">
|
||||
<defs>
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="1.5" flood-color="black" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12C2 17.5 12 24 12 24C12 24 22 17.5 22 12C22 6.5 17.5 2 12 2Z" fill="white" filter="url(#shadow)"/>
|
||||
${innerContent}
|
||||
</svg>
|
||||
`;
|
||||
|
||||
cachedIconUrls.set(
|
||||
cacheKey,
|
||||
'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg.trim())
|
||||
);
|
||||
}
|
||||
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: cachedIconUrls.get(cacheKey),
|
||||
anchor: [0.5, 0.65],
|
||||
scale: 1,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const searchResultLayer = new VectorLayer({
|
||||
source: this.searchResultsSource,
|
||||
style: searchResultStyle,
|
||||
zIndex: 11, // Above bookmarks (10)
|
||||
});
|
||||
|
||||
// Default view settings
|
||||
let center = [14.21683569, 27.060114248];
|
||||
let zoom = 2.661;
|
||||
@@ -141,9 +239,14 @@ export default class MapComponent extends Component {
|
||||
projection: 'EPSG:3857',
|
||||
});
|
||||
|
||||
this.mapInstance = new Map({
|
||||
this.mapInstance = new OlMap({
|
||||
target: element,
|
||||
layers: [openfreemap, selectedShapeLayer, bookmarkLayer],
|
||||
layers: [
|
||||
openfreemap,
|
||||
selectedShapeLayer,
|
||||
searchResultLayer,
|
||||
bookmarkLayer,
|
||||
],
|
||||
view: view,
|
||||
controls: defaultControls({
|
||||
zoom: true,
|
||||
@@ -178,7 +281,7 @@ export default class MapComponent extends Component {
|
||||
const pinIcon = document.createElement('div');
|
||||
pinIcon.className = 'selected-pin';
|
||||
// Simple SVG for Map Pin
|
||||
pinIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3" style="fill: #b31412; stroke: none;"></circle></svg>`;
|
||||
pinIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3" style="fill: var(--marker-color-dark); stroke: none;"></circle></svg>`;
|
||||
|
||||
const pinShadow = document.createElement('div');
|
||||
pinShadow.className = 'selected-pin-shadow';
|
||||
@@ -464,6 +567,33 @@ export default class MapComponent extends Component {
|
||||
);
|
||||
});
|
||||
|
||||
updateSearchResults = modifier(() => {
|
||||
if (!this.searchResultsSource) return;
|
||||
|
||||
this.searchResultsSource.clear();
|
||||
const results = this.mapUi.searchResults;
|
||||
|
||||
if (!results || results.length === 0) return;
|
||||
|
||||
const features = [];
|
||||
results.forEach((place) => {
|
||||
if (place.lat && place.lon) {
|
||||
const feature = new Feature({
|
||||
geometry: new Point(fromLonLat([place.lon, place.lat])),
|
||||
name: place.title,
|
||||
id: place.id,
|
||||
isSearchResult: true,
|
||||
originalPlace: place,
|
||||
});
|
||||
features.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
this.searchResultsSource.addFeatures(features);
|
||||
}
|
||||
});
|
||||
|
||||
// Track the selected place from the UI Service (Router -> Map)
|
||||
updateSelectedPin = modifier(() => {
|
||||
const selected = this.mapUi.selectedPlace;
|
||||
@@ -946,6 +1076,7 @@ export default class MapComponent extends Component {
|
||||
hitTolerance: 10,
|
||||
});
|
||||
let clickedBookmark = null;
|
||||
let clickedSearchResult = null;
|
||||
let selectedFeatureName = null;
|
||||
|
||||
if (features && features.length > 0) {
|
||||
@@ -954,8 +1085,12 @@ export default class MapComponent extends Component {
|
||||
console.debug(f);
|
||||
}
|
||||
const bookmarkFeature = features.find((f) => f.get('isBookmark'));
|
||||
const searchResultFeature = features.find((f) => f.get('isSearchResult'));
|
||||
|
||||
if (bookmarkFeature) {
|
||||
clickedBookmark = bookmarkFeature.get('originalPlace');
|
||||
} else if (searchResultFeature) {
|
||||
clickedSearchResult = searchResultFeature.get('originalPlace');
|
||||
}
|
||||
// Also get visual props for standard map click logic later
|
||||
const props = features[0].getProperties();
|
||||
@@ -966,14 +1101,15 @@ export default class MapComponent extends Component {
|
||||
|
||||
// Special handling when sidebar is OPEN
|
||||
if (this.args.isSidebarOpen) {
|
||||
// If it's a bookmark, we allow "switching" to it even if sidebar is open
|
||||
if (clickedBookmark) {
|
||||
// If it's a bookmark or search result, we allow "switching" to it even if sidebar is open
|
||||
const targetPlace = clickedBookmark || clickedSearchResult;
|
||||
if (targetPlace) {
|
||||
console.debug(
|
||||
'Clicked bookmark while sidebar open (switching):',
|
||||
clickedBookmark
|
||||
'Clicked feature while sidebar open (switching):',
|
||||
targetPlace
|
||||
);
|
||||
this.mapUi.preventNextZoom = true;
|
||||
this.router.transitionTo('place', clickedBookmark);
|
||||
this.router.transitionTo('place', targetPlace);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -992,6 +1128,13 @@ export default class MapComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickedSearchResult) {
|
||||
console.debug('Clicked search result:', clickedSearchResult);
|
||||
this.mapUi.preventNextZoom = true;
|
||||
this.router.transitionTo('place', clickedSearchResult);
|
||||
return;
|
||||
}
|
||||
|
||||
// Require Zoom >= 17 for generic map searches
|
||||
// This prevents accidental searches when interacting with the map at a high level
|
||||
const currentZoom = this.mapInstance.getView().getZoom();
|
||||
@@ -1041,6 +1184,7 @@ export default class MapComponent extends Component {
|
||||
{{this.setupMap}}
|
||||
{{this.updateInteractions}}
|
||||
{{this.updateBookmarks}}
|
||||
{{this.updateSearchResults}}
|
||||
{{this.updateSelectedPin}}
|
||||
{{this.syncPulse}}
|
||||
{{this.syncCreationMode}}
|
||||
|
||||
10
app/routes/index.js
Normal file
10
app/routes/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class IndexRoute extends Route {
|
||||
@service mapUi;
|
||||
|
||||
activate() {
|
||||
this.mapUi.clearSearchResults();
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ export default class SearchRoute extends Route {
|
||||
super.setupController(controller, model);
|
||||
// Ensure pulse is stopped if we reach here
|
||||
this.mapUi.stopSearch();
|
||||
this.mapUi.setSearchResults(model);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -12,6 +12,7 @@ export default class MapUiService extends Service {
|
||||
@tracked searchBoxHasFocus = false;
|
||||
@tracked selectionOptions = {};
|
||||
@tracked preventNextZoom = false;
|
||||
@tracked searchResults = [];
|
||||
|
||||
selectPlace(place, options = {}) {
|
||||
this.selectedPlace = place;
|
||||
@@ -24,6 +25,14 @@ export default class MapUiService extends Service {
|
||||
this.preventNextZoom = false;
|
||||
}
|
||||
|
||||
setSearchResults(results) {
|
||||
this.searchResults = results || [];
|
||||
}
|
||||
|
||||
clearSearchResults() {
|
||||
this.searchResults = [];
|
||||
}
|
||||
|
||||
startSearch() {
|
||||
this.isSearching = true;
|
||||
this.isCreating = false;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
--sidebar-width: 350px;
|
||||
--link-color: #2a7fff;
|
||||
--link-color-visited: #6a4fbf;
|
||||
--marker-color-primary: #ea4335;
|
||||
--marker-color-dark: #b31412;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -872,15 +874,15 @@ span.icon {
|
||||
.selected-pin {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #ea4335; /* Google Red */
|
||||
color: var(--marker-color-primary);
|
||||
filter: drop-shadow(0 4px 6px rgb(0 0 0 / 30%));
|
||||
}
|
||||
|
||||
.selected-pin svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: #ea4335;
|
||||
stroke: #b31412; /* Darker red stroke */
|
||||
fill: var(--marker-color-primary);
|
||||
stroke: var(--marker-color-dark);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||
@@ -20,53 +20,109 @@ import menu from 'feather-icons/dist/icons/menu.svg?raw';
|
||||
import navigation from 'feather-icons/dist/icons/navigation.svg?raw';
|
||||
import phone from 'feather-icons/dist/icons/phone.svg?raw';
|
||||
import plus from 'feather-icons/dist/icons/plus.svg?raw';
|
||||
import server from 'feather-icons/dist/icons/server.svg?raw';
|
||||
import search from 'feather-icons/dist/icons/search.svg?raw';
|
||||
import server from 'feather-icons/dist/icons/server.svg?raw';
|
||||
import settings from 'feather-icons/dist/icons/settings.svg?raw';
|
||||
import target from 'feather-icons/dist/icons/target.svg?raw';
|
||||
import user from 'feather-icons/dist/icons/user.svg?raw';
|
||||
import x from 'feather-icons/dist/icons/x.svg?raw';
|
||||
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
||||
|
||||
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.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 burgerAndDrinkCupWithStraw from '@waysidemapping/pinhead/dist/icons/burger_and_drink_cup_with_straw.svg?raw';
|
||||
import camera from '@waysidemapping/pinhead/dist/icons/camera.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 classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw';
|
||||
import cleaver from '@waysidemapping/pinhead/dist/icons/cleaver.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 croissant from '@waysidemapping/pinhead/dist/icons/croissant.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 film from '@waysidemapping/pinhead/dist/icons/film.svg?raw';
|
||||
import flagCheckered from '@waysidemapping/pinhead/dist/icons/flag_checkered.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 iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
||||
import memorialStoneWithInscription from '@waysidemapping/pinhead/dist/icons/memorial_stone_with_inscription.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 personJockeyingRacehorse from '@waysidemapping/pinhead/dist/icons/person_jockeying_racehorse.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 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 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 shoppingBasket from '@waysidemapping/pinhead/dist/icons/shopping_basket.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 wikipedia from '../icons/wikipedia.svg?raw';
|
||||
|
||||
const ICONS = {
|
||||
'arrow-left': arrowLeft,
|
||||
activity,
|
||||
angelfish,
|
||||
'arrow-left': arrowLeft,
|
||||
'beach-umbrella-in-ground': beachUmbrellaInGround,
|
||||
'beer-mug-with-foam': beerMugWithFoam,
|
||||
bookmark,
|
||||
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
|
||||
camera,
|
||||
'check-square': checkSquare,
|
||||
'classical-building': classicalBuilding,
|
||||
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
|
||||
'classical-building-with-flag': classicalBuildingWithFlag,
|
||||
cleaver,
|
||||
clock,
|
||||
'coffee-bean': coffeeBean,
|
||||
'comedy-mask-and-tragedy-mask': comedyMaskAndTragedyMask,
|
||||
croissant,
|
||||
'cup-and-saucer': cupAndSaucer,
|
||||
donut,
|
||||
edit,
|
||||
facebook,
|
||||
film,
|
||||
'flag-checkered': flagCheckered,
|
||||
'fork-and-knife': forkAndKnife,
|
||||
fort,
|
||||
gift,
|
||||
globe,
|
||||
heart,
|
||||
home,
|
||||
'ice-cream-on-cone': iceCreamOnCone,
|
||||
info,
|
||||
instagram,
|
||||
'fork-and-knife': forkAndKnife,
|
||||
'log-in': logIn,
|
||||
'log-out': logOut,
|
||||
mail,
|
||||
map,
|
||||
'map-pin': mapPin,
|
||||
'memorial-stone-with-inscription': memorialStoneWithInscription,
|
||||
menu,
|
||||
navigation,
|
||||
palace,
|
||||
'person-cricket-batting-at-cricket-ball': personCricketBattingAtCricketBall,
|
||||
'person-jockeying-racehorse': personJockeyingRacehorse,
|
||||
'person-running': personRunning,
|
||||
'person-sleeping-in-bed': personSleepingInBed,
|
||||
'person-swimming-in-water': personSwimmingInWater,
|
||||
'person-swinging-golf-club': personSwingingGolfClub,
|
||||
phone,
|
||||
plus,
|
||||
server,
|
||||
'round-structure-with-flag': roundStructureWithFlag,
|
||||
'sailing-ship-in-water': sailingShipInWater,
|
||||
search,
|
||||
server,
|
||||
settings,
|
||||
'shopping-basket': shoppingBasket,
|
||||
'shopping-cart': shoppingCart,
|
||||
target,
|
||||
user,
|
||||
'wall-hanging-with-mountains-and-sun': wallHangingWithMountainsAndSun,
|
||||
wikipedia,
|
||||
x,
|
||||
zap,
|
||||
@@ -76,6 +132,7 @@ const FILLED_ICONS = [
|
||||
'fork-and-knife',
|
||||
'wikipedia',
|
||||
'cup-and-saucer',
|
||||
'coffee-bean',
|
||||
'shopping-basket',
|
||||
'camera',
|
||||
'person-sleeping-in-bed',
|
||||
|
||||
131
app/utils/osm-icons.js
Normal file
131
app/utils/osm-icons.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { getIcon } from './icons';
|
||||
|
||||
// Rules for mapping OSM tags to icons.
|
||||
// Rules are evaluated in order. The first rule where all specified tags match is used.
|
||||
export const POI_ICON_RULES = [
|
||||
// Specific Cuisine
|
||||
{ tags: { cuisine: 'donut' }, icon: 'donut' },
|
||||
{ tags: { cuisine: 'doughnut' }, icon: 'donut' },
|
||||
{ tags: { cuisine: 'coffee_shop' }, icon: 'coffee-bean' },
|
||||
{ tags: { cuisine: 'coffee' }, icon: 'coffee-bean' },
|
||||
|
||||
// General Amenity/Shop Types
|
||||
{ tags: { amenity: 'ice_cream' }, icon: 'ice-cream-on-cone' },
|
||||
{ tags: { cuisine: 'ice_cream' }, icon: 'ice-cream-on-cone' },
|
||||
{ tags: { shop: 'ice_cream' }, icon: 'ice-cream-on-cone' },
|
||||
|
||||
{ tags: { amenity: 'cafe' }, icon: 'cup-and-saucer' },
|
||||
{ tags: { amenity: 'restaurant' }, icon: 'fork-and-knife' },
|
||||
{ tags: { amenity: 'fast_food' }, icon: 'burger-and-drink-cup-with-straw' },
|
||||
{ tags: { amenity: 'pub' }, icon: 'beer-mug-with-foam' },
|
||||
{ tags: { amenity: 'food_court' }, icon: 'fork-and-knife' },
|
||||
|
||||
{ tags: { shop: 'coffee' }, icon: 'coffee-bean' },
|
||||
{ tags: { shop: 'tea' }, icon: 'coffee-bean' },
|
||||
{ tags: { shop: 'pastry' }, icon: 'donut' }, // Pastry shops often have donuts
|
||||
|
||||
// Groceries
|
||||
{ tags: { shop: 'supermarket' }, icon: 'shopping-cart' },
|
||||
{ tags: { shop: 'convenience' }, icon: 'shopping-basket' },
|
||||
{ tags: { shop: 'grocery' }, icon: 'shopping-basket' },
|
||||
{ tags: { shop: 'greengrocer' }, icon: 'shopping-basket' },
|
||||
{ tags: { shop: 'bakery' }, icon: 'croissant' },
|
||||
{ tags: { shop: 'butcher' }, icon: 'cleaver' },
|
||||
{ tags: { shop: 'deli' }, icon: 'shopping-basket' },
|
||||
|
||||
// Natural
|
||||
{ tags: { natural: 'beach' }, icon: 'beach-umbrella-in-ground' },
|
||||
|
||||
// Tourism
|
||||
{ tags: { tourism: 'museum' }, icon: 'classical-building' },
|
||||
{ tags: { tourism: 'gallery' }, icon: 'wall-hanging-with-mountains-and-sun' },
|
||||
{ tags: { tourism: 'aquarium' }, icon: 'angelfish' },
|
||||
{ tags: { tourism: 'theme_park' }, icon: 'camera' },
|
||||
{ tags: { tourism: 'attraction' }, icon: 'camera' },
|
||||
{ tags: { tourism: 'viewpoint' }, icon: 'camera' },
|
||||
{ tags: { tourism: 'zoo' }, icon: 'camera' },
|
||||
{ tags: { tourism: 'artwork' }, icon: 'camera' },
|
||||
{ tags: { amenity: 'cinema' }, icon: 'film' },
|
||||
{ tags: { amenity: 'theatre' }, icon: 'camera' },
|
||||
{ tags: { amenity: 'arts_centre' }, icon: 'comedy-mask-and-tragedy-mask' },
|
||||
{ tags: { amenity: 'arts_center' }, icon: 'comedy-mask-and-tragedy-mask' },
|
||||
|
||||
// Historic
|
||||
{ tags: { historic: 'fort' }, icon: 'fort' },
|
||||
{ tags: { historic: 'castle' }, icon: 'palace' },
|
||||
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
||||
{ tags: { historic: 'archaeological_site' }, icon: 'camera' },
|
||||
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
|
||||
{
|
||||
tags: { historic: 'monument' },
|
||||
icon: 'classical-building-with-dome-and-flag',
|
||||
},
|
||||
{ tags: { historic: 'ship' }, icon: 'sailing-ship-in-water' },
|
||||
|
||||
// Accommodation
|
||||
{ tags: { tourism: 'hotel' }, icon: 'person-sleeping-in-bed' },
|
||||
{ tags: { tourism: 'hostel' }, icon: 'person-sleeping-in-bed' },
|
||||
{ tags: { tourism: 'motel' }, icon: 'person-sleeping-in-bed' },
|
||||
{ tags: { tourism: 'guest_house' }, icon: 'person-sleeping-in-bed' },
|
||||
|
||||
// Sports / Motorsports
|
||||
{ tags: { sport: 'motor' }, icon: 'flag-checkered' },
|
||||
{ tags: { sport: 'karting' }, icon: 'flag-checkered' },
|
||||
{ tags: { sport: 'motocross' }, icon: 'flag-checkered' },
|
||||
{
|
||||
tags: { sport: 'cricket' },
|
||||
icon: 'person-cricket-batting-at-cricket-ball',
|
||||
},
|
||||
{ tags: { leisure: 'water_park' }, icon: 'person-swimming-in-water' },
|
||||
{ tags: { sport: 'golf' }, icon: 'person-swinging-golf-club' },
|
||||
{ tags: { leisure: 'golf_course' }, icon: 'person-swinging-golf-club' },
|
||||
{ tags: { sport: 'horse_racing' }, icon: 'person-jockeying-racehorse' },
|
||||
{ tags: { leisure: 'stadium' }, icon: 'round-structure-with-flag' },
|
||||
{ tags: { sport: 'stadium' }, icon: 'round-structure-with-flag' },
|
||||
|
||||
{ tags: { leisure: 'sports_centre' }, icon: 'person-running' },
|
||||
{ tags: { sport: 'fitness_centre' }, icon: 'person-running' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Finds the appropriate icon name based on the place's OSM tags.
|
||||
* @param {Object} tags - The OSM tags of the place.
|
||||
* @returns {string|null} - The name of the icon or null if no match found.
|
||||
*/
|
||||
export function getIconNameForTags(tags) {
|
||||
if (!tags) return null;
|
||||
|
||||
for (const rule of POI_ICON_RULES) {
|
||||
let match = true;
|
||||
for (const [key, expectedValue] of Object.entries(rule.tags)) {
|
||||
const tagValue = tags[key];
|
||||
if (!tagValue) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for exact match or if value is in a semicolon-separated list
|
||||
// e.g. "donut;coffee_shop"
|
||||
const values = tagValue.split(';').map((v) => v.trim());
|
||||
if (!values.includes(expectedValue)) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return rule.icon;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw SVG string for the icon corresponding to the given tags.
|
||||
* @param {Object} tags - The OSM tags.
|
||||
* @returns {string|null} - The raw SVG string or null.
|
||||
*/
|
||||
export function getIconSvgForTags(tags) {
|
||||
const iconName = getIconNameForTags(tags);
|
||||
if (!iconName) return null;
|
||||
return getIcon(iconName);
|
||||
}
|
||||
Reference in New Issue
Block a user