diff --git a/app/components/place-details.gjs b/app/components/place-details.gjs
index 0c6bb2f..a118445 100644
--- a/app/components/place-details.gjs
+++ b/app/components/place-details.gjs
@@ -23,6 +23,7 @@ export default class PlaceDetails extends Component {
@service storage;
@service nostrAuth;
@service nostrData;
+ @service mapUi;
@tracked isEditing = false;
@tracked showLists = false;
@tracked isPhotoUploadActive = false;
@@ -345,9 +346,21 @@ export default class PlaceDetails extends Component {
get osmUrl() {
const id = this.place.osmId;
- if (!id) return null;
- const type = this.place.osmType || 'node';
- return `https://www.openstreetmap.org/${type}/${id}`;
+ if (id) {
+ const type = this.place.osmType || 'node';
+ return `https://www.openstreetmap.org/${type}/${id}`;
+ }
+
+ const lat = this.place.lat;
+ const lon = this.place.lon;
+ if (!lat || !lon) return null;
+
+ const viewLat = this.mapUi.currentCenter?.lat ?? lat;
+ const viewLon = this.mapUi.currentCenter?.lon ?? lon;
+ const zoom = this.mapUi.currentZoom ?? 17;
+ const roundedZoom = Math.round(zoom);
+
+ return `https://www.openstreetmap.org/search?lat=${lat}&lon=${lon}&zoom=${roundedZoom}#map=${roundedZoom}/${Number(viewLat).toFixed(5)}/${Number(viewLon).toFixed(5)}`;
}
get gmapsUrl() {
@@ -591,7 +604,7 @@ export default class PlaceDetails extends Component {
- {{#if this.osmUrl}}
+ {{#if this.place.osmId}}
diff --git a/app/styles/app.css b/app/styles/app.css
index 1aafd6b..14a1b97 100644
--- a/app/styles/app.css
+++ b/app/styles/app.css
@@ -1266,6 +1266,20 @@ span.icon {
gap: 0.5rem;
}
+.content-with-icon > span:not(.icon) {
+ min-width: 0;
+ flex: 1;
+}
+
+.content-with-icon > span:not(.icon) a {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: bottom;
+}
+
.content-with-icon .icon {
margin-top: 0.15rem;
}
diff --git a/app/utils/icons.js b/app/utils/icons.js
index f92ecf3..5744072 100644
--- a/app/utils/icons.js
+++ b/app/utils/icons.js
@@ -59,8 +59,12 @@ import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
+import carAndWrench from '@waysidemapping/pinhead/dist/icons/car_and_wrench.svg?raw';
+import castleKeep from '@waysidemapping/pinhead/dist/icons/castle_keep.svg?raw';
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
+import cityGate from '@waysidemapping/pinhead/dist/icons/city_gate.svg?raw';
import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
+import classicalBuildingWithClock from '@waysidemapping/pinhead/dist/icons/classical_building_with_clock.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 commercialBuilding from '@waysidemapping/pinhead/dist/icons/commercial_building.svg?raw';
@@ -88,6 +92,7 @@ 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 industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw';
+import infoI from '@waysidemapping/pinhead/dist/icons/info_i.svg?raw';
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
@@ -157,11 +162,13 @@ const ICONS = {
'chevron-left': chevronLeft,
'chevron-right': chevronRight,
'cigarette-with-smoke-curl': cigaretteWithSmokeCurl,
+ 'city-gate': cityGate,
climbing_wall: climbingWall,
check,
'alert-circle': alertCircle,
'alert-triangle': alertTriangle,
'classical-building': classicalBuilding,
+ 'classical-building-with-clock': classicalBuildingWithClock,
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
'classical-building-with-flag': classicalBuildingWithFlag,
'commercial-building': commercialBuilding,
@@ -198,6 +205,7 @@ const ICONS = {
'ice-cream-on-cone': iceCreamOnCone,
'industrial-building': industrialBuilding,
info,
+ 'info-i': infoI,
instagram,
jewel,
'log-in': logIn,
@@ -262,6 +270,8 @@ const ICONS = {
winding_way_wide: windingWayWide,
parking_p: parkingP,
car,
+ 'car-and-wrench': carAndWrench,
+ 'castle-keep': castleKeep,
x,
zap,
'loading-ring': loadingRing,
diff --git a/app/utils/osm-icons.js b/app/utils/osm-icons.js
index f4be54e..4b4741e 100644
--- a/app/utils/osm-icons.js
+++ b/app/utils/osm-icons.js
@@ -27,6 +27,8 @@ export const POI_ICON_RULES = [
{ tags: { amenity: 'bank' }, icon: 'banknote' },
{ tags: { amenity: 'place_of_worship' }, icon: 'place-of-worship-building' },
+ { tags: { amenity: 'townhall' }, icon: 'classical-building-with-clock' },
+ { tags: { building: 'townhall' }, icon: 'classical-building-with-clock' },
{ 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' },
@@ -72,6 +74,7 @@ export const POI_ICON_RULES = [
tags: { shop: 'beauty' },
icon: 'fancy-mirror-with-reflection-and-stars',
},
+ { tags: { shop: 'car_repair' }, icon: 'car-and-wrench' },
{ tags: { craft: 'tailor' }, icon: 'needle-and-spool-of-thread' },
{ tags: { office: 'estate_agent' }, icon: 'village-buildings' },
{ tags: { office: true }, icon: 'commercial-building' },
@@ -103,6 +106,7 @@ export const POI_ICON_RULES = [
{ tags: { tourism: 'viewpoint' }, icon: 'camera' },
{ tags: { tourism: 'zoo' }, icon: 'camera' },
{ tags: { tourism: 'artwork' }, icon: 'camera' },
+ { tags: { tourism: 'information' }, icon: 'info-i' },
{ tags: { amenity: 'cinema' }, icon: 'film' },
{ tags: { amenity: 'theatre' }, icon: 'camera' },
{ tags: { amenity: 'arts_centre' }, icon: 'comedy-mask-and-tragedy-mask' },
@@ -113,7 +117,9 @@ export const POI_ICON_RULES = [
{ tags: { historic: 'bridge' }, icon: 'bridge' },
{ tags: { historic: 'bridge_site' }, icon: 'bridge' },
{ tags: { historic: 'fort' }, icon: 'fort' },
+ { tags: { historic: 'city_gate' }, icon: 'city-gate' },
{ tags: { historic: 'castle' }, icon: 'palace' },
+ { tags: { building: 'tower', historic: 'yes' }, icon: 'castle-keep' },
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
{ tags: { historic: 'archaeological_site' }, icon: 'grecian-vase' },
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
diff --git a/app/utils/osm.js b/app/utils/osm.js
index b16295c..e0a16b4 100644
--- a/app/utils/osm.js
+++ b/app/utils/osm.js
@@ -56,15 +56,24 @@ const PLACE_TYPE_KEYS = [
export function getPlaceType(tags) {
if (!tags) return null;
+ let fallbackKey = null;
+
for (const key of PLACE_TYPE_KEYS) {
const value = tags[key];
if (value) {
if (value === 'yes') {
- return humanizeOsmTag(key);
+ if (!fallbackKey) {
+ fallbackKey = key;
+ }
+ continue;
}
return humanizeOsmTag(value);
}
}
+ if (fallbackKey) {
+ return humanizeOsmTag(fallbackKey);
+ }
+
return null;
}
diff --git a/app/utils/poi-categories.js b/app/utils/poi-categories.js
index 2eeb91c..fe1cf92 100644
--- a/app/utils/poi-categories.js
+++ b/app/utils/poi-categories.js
@@ -43,7 +43,7 @@ export const POI_CATEGORIES = [
label: 'Things to do',
icon: 'feather-camera',
filter: [
- '["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork)$"]',
+ '["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork|information)$"]',
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
'["leisure"~"^(sports_centre|stadium|water_park)$"]',
'["historic"]',
@@ -55,7 +55,7 @@ export const POI_CATEGORIES = [
id: 'accommodation',
label: 'Hotels',
icon: 'person-sleeping-in-bed',
- filter: ['["tourism"~"^(hotel|hostel|motel|chalet)$"]'],
+ filter: ['["tourism"~"^(hotel|hostel|motel|chalet|guest_house)$"]'],
types: ['node', 'way', 'relation'],
},
];
diff --git a/tests/integration/components/place-details-test.gjs b/tests/integration/components/place-details-test.gjs
index 6969046..6a70d2b 100644
--- a/tests/integration/components/place-details-test.gjs
+++ b/tests/integration/components/place-details-test.gjs
@@ -334,4 +334,54 @@ module('Integration | Component | place-details', function (hooks) {
assert.dom(links[0]).hasText('+44 987 654 321');
assert.dom(links[1]).hasText('+1 234-567 8900');
});
+
+ test('it renders correct OpenStreetMap link for an OSM place', async function (assert) {
+ const place = {
+ title: 'OSM Place',
+ osmId: '12345',
+ osmType: 'node',
+ lat: 52.520008,
+ lon: 13.404954,
+ };
+
+ await render();
+
+ const osmLink = this.element.querySelector(
+ '.meta-info a[href^="https://www.openstreetmap.org/node/12345"]'
+ );
+ assert.ok(osmLink, 'OpenStreetMap link is rendered');
+ assert.strictEqual(
+ osmLink.getAttribute('href'),
+ 'https://www.openstreetmap.org/node/12345'
+ );
+
+ assert.dom('button.btn-link').hasText('Add a photo');
+ });
+
+ test('it renders correct search-based OpenStreetMap link for a custom saved place', async function (assert) {
+ class MockMapUi extends Service {
+ currentCenter = { lat: 52.5, lon: 13.4 };
+ currentZoom = 15.6;
+ }
+ this.owner.register('service:map-ui', MockMapUi);
+
+ const place = {
+ title: 'Custom Place',
+ lat: 52.520008,
+ lon: 13.404954,
+ };
+
+ await render();
+
+ const osmLink = this.element.querySelector(
+ '.meta-info a[href^="https://www.openstreetmap.org/search"]'
+ );
+ assert.ok(osmLink, 'OpenStreetMap search link is rendered');
+ assert.strictEqual(
+ osmLink.getAttribute('href'),
+ 'https://www.openstreetmap.org/search?lat=52.520008&lon=13.404954&zoom=16#map=16/52.50000/13.40000'
+ );
+
+ assert.dom('button.btn-link').doesNotExist();
+ });
});
diff --git a/tests/unit/utils/osm-test.js b/tests/unit/utils/osm-test.js
index 939937e..97acf83 100644
--- a/tests/unit/utils/osm-test.js
+++ b/tests/unit/utils/osm-test.js
@@ -89,6 +89,12 @@ module('Unit | Utility | osm', function (hooks) {
assert.strictEqual(result, 'Building');
});
+ test('getPlaceType ignores generic "yes" values if a more specific tag is present', function (assert) {
+ const tags = { historic: 'yes', building: 'tower' };
+ const result = getPlaceType(tags);
+ assert.strictEqual(result, 'Tower');
+ });
+
test('getPlaceType prioritizes order (amenity > shop > building)', function (assert) {
// If something is both a shop and a building, it should be a shop
const tags = { building: 'yes', shop: 'supermarket' };