Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
20f63065ad
|
|||
|
39a7ec3595
|
|||
| 32dfa3a30f | |||
|
64ccc694d3
|
|||
|
87e2380ef6
|
|||
| 66c31b19f1 | |||
|
55aecbd699
|
|||
|
ccaa56b78f
|
|||
|
d30375707a
|
|||
|
53300b92f5
|
|||
|
c37f794eea
|
|||
|
4bc92bb7cc
|
@@ -24,7 +24,7 @@ export default class AppHeaderComponent extends Component {
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<button
|
<button
|
||||||
class="icon-btn"
|
class="menu-btn btn-press"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Menu"
|
aria-label="Menu"
|
||||||
{{on "click" @onToggleMenu}}
|
{{on "click" @onToggleMenu}}
|
||||||
@@ -36,7 +36,7 @@ export default class AppHeaderComponent extends Component {
|
|||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="user-menu-container">
|
<div class="user-menu-container">
|
||||||
<button
|
<button
|
||||||
class="user-btn"
|
class="user-btn btn-press"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="User Menu"
|
aria-label="User Menu"
|
||||||
{{on "click" this.toggleUserMenu}}
|
{{on "click" this.toggleUserMenu}}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default class MapComponent extends Component {
|
|||||||
// Default view settings
|
// Default view settings
|
||||||
let center = [14.21683569, 27.060114248];
|
let center = [14.21683569, 27.060114248];
|
||||||
let zoom = 2.661;
|
let zoom = 2.661;
|
||||||
|
let restoredFromStorage = false;
|
||||||
|
|
||||||
// Try to restore from localStorage
|
// Try to restore from localStorage
|
||||||
try {
|
try {
|
||||||
@@ -82,6 +83,7 @@ export default class MapComponent extends Component {
|
|||||||
) {
|
) {
|
||||||
center = parsed.center;
|
center = parsed.center;
|
||||||
zoom = parsed.zoom;
|
zoom = parsed.zoom;
|
||||||
|
restoredFromStorage = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -99,7 +101,7 @@ export default class MapComponent extends Component {
|
|||||||
layers: [openfreemap, bookmarkLayer],
|
layers: [openfreemap, bookmarkLayer],
|
||||||
view: view,
|
view: view,
|
||||||
controls: defaultControls({
|
controls: defaultControls({
|
||||||
zoom: false,
|
zoom: true,
|
||||||
rotate: true,
|
rotate: true,
|
||||||
attribution: true,
|
attribution: true,
|
||||||
}),
|
}),
|
||||||
@@ -243,6 +245,7 @@ export default class MapComponent extends Component {
|
|||||||
const coordinates = geolocation.getPosition();
|
const coordinates = geolocation.getPosition();
|
||||||
const accuracyGeometry = geolocation.getAccuracyGeometry();
|
const accuracyGeometry = geolocation.getAccuracyGeometry();
|
||||||
const accuracy = geolocation.getAccuracy();
|
const accuracy = geolocation.getAccuracy();
|
||||||
|
console.debug('Geolocation change:', { coordinates, accuracy });
|
||||||
|
|
||||||
if (!coordinates) return;
|
if (!coordinates) return;
|
||||||
|
|
||||||
@@ -307,7 +310,8 @@ export default class MapComponent extends Component {
|
|||||||
this.mapInstance.getView().animate(viewOptions);
|
this.mapInstance.getView().animate(viewOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
locateBtn.addEventListener('click', () => {
|
const startLocating = () => {
|
||||||
|
console.debug('Getting current geolocation...')
|
||||||
// 1. Clear any previous session
|
// 1. Clear any previous session
|
||||||
stopLocating();
|
stopLocating();
|
||||||
|
|
||||||
@@ -331,7 +335,9 @@ export default class MapComponent extends Component {
|
|||||||
locateTimeout = setTimeout(() => {
|
locateTimeout = setTimeout(() => {
|
||||||
stopLocating();
|
stopLocating();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
locateBtn.addEventListener('click', startLocating);
|
||||||
|
|
||||||
const locateControl = new Control({
|
const locateControl = new Control({
|
||||||
element: locateElement,
|
element: locateElement,
|
||||||
@@ -340,6 +346,11 @@ export default class MapComponent extends Component {
|
|||||||
this.mapInstance.addLayer(geolocationLayer);
|
this.mapInstance.addLayer(geolocationLayer);
|
||||||
this.mapInstance.addControl(locateControl);
|
this.mapInstance.addControl(locateControl);
|
||||||
|
|
||||||
|
// Auto-locate on first visit (if not restored from storage and on home page)
|
||||||
|
if (!restoredFromStorage && this.router.currentRouteName === 'index') {
|
||||||
|
startLocating();
|
||||||
|
}
|
||||||
|
|
||||||
this.mapInstance.on('singleclick', this.handleMapClick);
|
this.mapInstance.on('singleclick', this.handleMapClick);
|
||||||
|
|
||||||
// Load places when map moves
|
// Load places when map moves
|
||||||
@@ -733,6 +744,13 @@ export default class MapComponent extends Component {
|
|||||||
return;
|
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();
|
||||||
|
if (currentZoom < 16) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const coords = toLonLat(event.coordinate);
|
const coords = toLonLat(event.coordinate);
|
||||||
const [lon, lat] = coords;
|
const [lon, lat] = coords;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
|
|||||||
import { fn } from '@ember/helper';
|
import { fn } from '@ember/helper';
|
||||||
import { on } from '@ember/modifier';
|
import { on } from '@ember/modifier';
|
||||||
import { humanizeOsmTag } from '../utils/format-text';
|
import { humanizeOsmTag } from '../utils/format-text';
|
||||||
|
import { getLocalizedName } from '../utils/osm';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import PlaceEditForm from './place-edit-form';
|
import PlaceEditForm from './place-edit-form';
|
||||||
|
|
||||||
@@ -22,8 +23,7 @@ export default class PlaceDetails extends Component {
|
|||||||
get name() {
|
get name() {
|
||||||
return (
|
return (
|
||||||
this.place.title ||
|
this.place.title ||
|
||||||
this.tags.name ||
|
getLocalizedName(this.tags) ||
|
||||||
this.tags['name:en'] ||
|
|
||||||
'Unnamed Place'
|
'Unnamed Place'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import or from 'ember-truth-helpers/helpers/or';
|
|||||||
import PlaceDetails from './place-details';
|
import PlaceDetails from './place-details';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
||||||
|
import { getLocalizedName } from '../utils/osm';
|
||||||
|
|
||||||
export default class PlacesSidebar extends Component {
|
export default class PlacesSidebar extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
@@ -85,8 +86,7 @@ export default class PlacesSidebar extends Component {
|
|||||||
} else {
|
} else {
|
||||||
// It's a fresh POI -> Save it
|
// It's a fresh POI -> Save it
|
||||||
const placeData = {
|
const placeData = {
|
||||||
title:
|
title: getLocalizedName(place.osmTags, 'Untitled Place'),
|
||||||
place.osmTags.name || place.osmTags['name:en'] || 'Untitled Place',
|
|
||||||
lat: place.lat,
|
lat: place.lat,
|
||||||
lon: place.lon,
|
lon: place.lon,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default class SettingsPane extends Component {
|
|||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
value="false"
|
value="false"
|
||||||
selected={{if (not this.settings.mapKinetic) "selected"}}
|
selected={{unless this.settings.mapKinetic "selected"}}
|
||||||
>
|
>
|
||||||
Off
|
Off
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Service, { service } from '@ember/service';
|
import Service, { service } from '@ember/service';
|
||||||
|
import { getLocalizedName } from '../utils/osm';
|
||||||
|
|
||||||
export default class OsmService extends Service {
|
export default class OsmService extends Service {
|
||||||
@service settings;
|
@service settings;
|
||||||
@@ -61,7 +62,7 @@ out center;
|
|||||||
|
|
||||||
normalizePoi(poi) {
|
normalizePoi(poi) {
|
||||||
return {
|
return {
|
||||||
title: poi.tags?.name || poi.tags?.['name:en'] || 'Untitled Place',
|
title: getLocalizedName(poi.tags),
|
||||||
lat: poi.lat || poi.center?.lat,
|
lat: poi.lat || poi.center?.lat,
|
||||||
lon: poi.lon || poi.center?.lon,
|
lon: poi.lon || poi.center?.lon,
|
||||||
url: poi.tags?.website,
|
url: poi.tags?.website,
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */
|
overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -69,7 +74,15 @@ body {
|
|||||||
pointer-events: auto; /* Re-enable clicks for buttons */
|
pointer-events: auto; /* Re-enable clicks for buttons */
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn {
|
.btn-press {
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-press:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn {
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -80,11 +93,6 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-btn {
|
.user-btn {
|
||||||
@@ -539,22 +547,40 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Locate Control */
|
/* Zoom Control - Moved to bottom right above attribution */
|
||||||
|
.ol-zoom {
|
||||||
|
top: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
bottom: 2.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-touch .ol-zoom {
|
||||||
|
bottom: 3.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Locate Control - Above Zoom */
|
||||||
.ol-control.ol-locate {
|
.ol-control.ol-locate {
|
||||||
inset: auto 0.5em 2.5em auto;
|
top: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
bottom: 6.5em;
|
||||||
|
right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-touch .ol-control.ol-locate {
|
.ol-touch .ol-control.ol-locate {
|
||||||
inset: auto 0.5em 3.5em auto;
|
bottom: 8.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rotate Control */
|
/* Rotate Control - Above Locate */
|
||||||
.ol-rotate {
|
.ol-rotate {
|
||||||
inset: auto 0.5em 5em auto;
|
top: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
bottom: 9em;
|
||||||
|
right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-touch .ol-rotate {
|
.ol-touch .ol-rotate {
|
||||||
inset: auto 0.5em 6em auto;
|
bottom: 11.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.icon {
|
span.icon {
|
||||||
|
|||||||
32
app/utils/osm.js
Normal file
32
app/utils/osm.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export function getLocalizedName(tags, defaultName = 'Untitled Place') {
|
||||||
|
if (!tags) return defaultName;
|
||||||
|
|
||||||
|
// 1. Get user's preferred languages
|
||||||
|
const languages = navigator.languages || [navigator.language || 'en'];
|
||||||
|
|
||||||
|
// 2. Try to find a match for each preferred language
|
||||||
|
for (const lang of languages) {
|
||||||
|
if (!lang) continue;
|
||||||
|
|
||||||
|
// Handle "en-US", "de-DE", etc. -> look for "name:en", "name:de"
|
||||||
|
const shortLang = lang.split('-')[0];
|
||||||
|
const tagKey = `name:${shortLang}`;
|
||||||
|
|
||||||
|
if (tags[tagKey]) {
|
||||||
|
return tags[tagKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to standard "name"
|
||||||
|
if (tags.name) {
|
||||||
|
return tags.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback to "name:en" (common in international places without local name)
|
||||||
|
if (tags['name:en']) {
|
||||||
|
return tags['name:en'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Final fallback
|
||||||
|
return defaultName;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.11.2",
|
"version": "1.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"lint:js:fix": "eslint . --fix",
|
"lint:js:fix": "eslint . --fix",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"test": "vite build --mode development && testem ci --port 0",
|
"test": "vite build --mode development && testem ci --port 0",
|
||||||
|
"preversion": "pnpm test",
|
||||||
"version": "pnpm build && git add release/"
|
"version": "pnpm build && git add release/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
1
release/assets/main-G8wPYi_P.css
Normal file
1
release/assets/main-G8wPYi_P.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
File diff suppressed because one or more lines are too long
@@ -26,8 +26,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-CYFdUlXN.js"></script>
|
<script type="module" crossorigin src="/assets/main-ji2SNMnp.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-D53xPL_H.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-G8wPYi_P.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
|
|
||||||
// Click the Close (X) button
|
// Click the Close (X) button
|
||||||
await click('.close-btn');
|
await click('.close-btn');
|
||||||
await settled();
|
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), '/', 'Returned to index');
|
assert.strictEqual(currentURL(), '/', 'Returned to index');
|
||||||
assert.false(mapUi.returnToSearch, 'Flag is reset after closing sidebar');
|
assert.false(mapUi.returnToSearch, 'Flag is reset after closing sidebar');
|
||||||
@@ -95,7 +95,7 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
assert.ok(currentURL().includes('/place/'), 'Visited place directly');
|
assert.ok(currentURL().includes('/place/'), 'Visited place directly');
|
||||||
|
|
||||||
await click('.back-btn');
|
await click('.back-btn');
|
||||||
await settled();
|
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), '/', 'Returned to index/map');
|
assert.strictEqual(currentURL(), '/', 'Returned to index/map');
|
||||||
assert.true(backStub.notCalled, 'window.history.back() was NOT called');
|
assert.true(backStub.notCalled, 'window.history.back() was NOT called');
|
||||||
|
|||||||
Reference in New Issue
Block a user