Compare commits

..

6 Commits

Author SHA1 Message Date
2f440d4971 1.16.0
All checks were successful
CI / Lint (push) Successful in 48s
CI / Test (push) Successful in 57s
2026-03-18 14:48:49 +04:00
1c6cbe6b0f Merge pull request 'Update OSM data when opening saved places' (#32) from feature/update_place_data into master
All checks were successful
CI / Lint (push) Successful in 48s
CI / Test (push) Successful in 57s
Reviewed-on: #32
2026-03-18 10:46:33 +00:00
bdd5db157c Update OSM data when opening saved places
All checks were successful
CI / Lint (pull_request) Successful in 49s
CI / Test (pull_request) Successful in 57s
Release Drafter / Update release notes draft (pull_request) Successful in 19s
2026-03-18 14:42:15 +04:00
f7c40095d5 1.15.4
All checks were successful
CI / Lint (push) Successful in 50s
CI / Test (push) Successful in 58s
2026-03-17 20:08:37 +04:00
579892067e Tweak sidebar width 2026-03-17 20:07:49 +04:00
48f87f98d6 Remove obsolete styles
All checks were successful
CI / Lint (push) Successful in 51s
CI / Test (push) Successful in 57s
2026-03-17 16:59:54 +04:00
11 changed files with 361 additions and 41 deletions

View File

@@ -101,6 +101,23 @@ export default class PlaceRoute extends Route {
return null; return null;
} }
setupController(controller, model) {
super.setupController(controller, model);
this.checkUpdates(model);
}
async checkUpdates(place) {
// Only check for updates if it's a saved place (has ID) and is an OSM object
if (place && place.id && place.osmId && place.osmType) {
const updatedPlace = await this.storage.refreshPlace(place);
if (updatedPlace) {
// If an update occurred, refresh the map UI selection without moving the camera
// This ensures the sidebar shows the new data
this.mapUi.selectPlace(updatedPlace, { preventZoom: true });
}
}
}
serialize(model) { serialize(model) {
// If the model is a saved bookmark, use its ID // If the model is a saved bookmark, use its ID
if (model.id) { if (model.id) {

View File

@@ -1,4 +1,4 @@
import Service from '@ember/service'; import Service, { service } from '@ember/service';
import RemoteStorage from 'remotestoragejs'; import RemoteStorage from 'remotestoragejs';
import Places from '@remotestorage/module-places'; import Places from '@remotestorage/module-places';
import Widget from 'remotestorage-widget'; import Widget from 'remotestorage-widget';
@@ -7,8 +7,10 @@ import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { debounceTask } from 'ember-lifeline'; import { debounceTask } from 'ember-lifeline';
import Geohash from 'latlon-geohash'; import Geohash from 'latlon-geohash';
import { getLocalizedName } from '../utils/osm';
export default class StorageService extends Service { export default class StorageService extends Service {
@service osm;
rs; rs;
widget; widget;
@tracked placesInView = []; @tracked placesInView = [];
@@ -366,6 +368,82 @@ export default class StorageService extends Service {
} }
} }
async refreshPlace(place) {
if (!place || !place.id || !place.osmId || !place.osmType) {
return null;
}
try {
console.debug(`Checking for updates for ${place.title} (${place.osmId})`);
const freshData = await this.osm.fetchOsmObject(
place.osmId,
place.osmType
);
if (!freshData) {
console.warn('Could not fetch fresh data for', place.osmId);
return null;
}
// Check for changes
let hasChanges = false;
const changes = {};
// 1. Check Coordinates (allow tiny drift < ~1m)
const latDiff = Math.abs(place.lat - freshData.lat);
const lonDiff = Math.abs(place.lon - freshData.lon);
if (latDiff > 0.00001 || lonDiff > 0.00001) {
hasChanges = true;
changes.lat = freshData.lat;
changes.lon = freshData.lon;
}
// 2. Check Tags
const oldTags = place.osmTags || {};
const newTags = freshData.osmTags || {};
const allKeys = new Set([
...Object.keys(oldTags),
...Object.keys(newTags),
]);
for (const key of allKeys) {
if (oldTags[key] !== newTags[key]) {
hasChanges = true;
changes.osmTags = newTags;
break;
}
}
if (!hasChanges) {
console.debug('No changes detected for', place.title);
return null;
}
console.debug('Changes detected:', changes);
// 3. Prepare Update
const updatedPlace = {
...place,
...changes,
};
// If the current title matches the old localized name, update it to the
// new localized name. If the user renamed it (custom title), keep it.
const oldDefaultName = getLocalizedName(oldTags);
const newDefaultName = getLocalizedName(newTags);
if (place.title === oldDefaultName && oldDefaultName !== newDefaultName) {
updatedPlace.title = newDefaultName;
}
// 4. Save
return await this.updatePlace(updatedPlace);
} catch (e) {
console.error('Failed to refresh place:', e);
return null;
}
}
@action @action
connect() { connect() {
this.isWidgetOpen = true; this.isWidgetOpen = true;

View File

@@ -3,7 +3,7 @@
:root { :root {
--default-list-color: #fc3; --default-list-color: #fc3;
--hover-bg: #f8f9fa; --hover-bg: #f8f9fa;
--sidebar-width: 360px; --sidebar-width: 350px;
--link-color: #2a7fff; --link-color: #2a7fff;
--link-color-visited: #6a4fbf; --link-color-visited: #6a4fbf;
} }
@@ -344,11 +344,6 @@ body {
font-size: 0.9rem; font-size: 0.9rem;
} }
.sidebar-content details .link-list {
padding: 0;
margin: 0;
}
@keyframes details-slide-down { @keyframes details-slide-down {
from { from {
opacity: 0; opacity: 0;
@@ -361,14 +356,6 @@ body {
} }
} }
.sidebar-content details .link-list li {
margin-bottom: 0.5rem;
}
.sidebar-content details .link-list li:last-child {
margin-bottom: 0;
}
.edit-form { .edit-form {
margin: -1rem; margin: -1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -521,26 +508,6 @@ abbr[title] {
text-decoration: underline dotted; text-decoration: underline dotted;
} }
.link-list {
list-style: none;
padding: 0;
margin: 0;
}
.link-list li {
margin-bottom: 0.5rem;
}
.link-list a {
color: var(--link-color);
text-decoration: none;
font-size: 0.95rem;
}
.link-list a:hover {
text-decoration: underline;
}
.places-list { .places-list {
list-style: none; list-style: none;
padding: 0; padding: 0;

View File

@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.15.3", "version": "1.16.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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-DMekdiSt.js"></script> <script type="module" crossorigin src="/assets/main-gEUnNw-L.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CZskVGin.css"> <link rel="stylesheet" crossorigin href="/assets/main-BOfcjRke.css">
</head> </head>
<body> <body>
</body> </body>

View File

@@ -125,4 +125,60 @@ module('Unit | Route | place', function (hooks) {
assert.notOk(fetchCalled, 'fetchOsmObject should NOT be called for nodes'); assert.notOk(fetchCalled, 'fetchOsmObject should NOT be called for nodes');
}); });
test('setupController triggers checkUpdates', async function (assert) {
let route = this.owner.lookup('route:place');
// Stub Storage Service
let refreshPlaceCalled = false;
class StorageStub extends Service {
async refreshPlace(place) {
refreshPlaceCalled = true;
assert.strictEqual(place.id, '123', 'Passed correct place to storage');
return {
...place,
title: 'Updated Title',
};
}
}
// Stub MapUi Service
let selectPlaceCalled = false;
class MapUiStub extends Service {
selectPlace(place, options) {
selectPlaceCalled = true;
assert.strictEqual(
place.title,
'Updated Title',
'Selected updated place'
);
assert.ok(options.preventZoom, 'Prevented zoom on update');
}
stopSearch() {}
}
this.owner.register('service:storage', StorageStub);
this.owner.register('service:map-ui', MapUiStub);
let model = {
id: '123',
osmId: '456',
osmType: 'node',
title: 'Original Title',
};
let controller = {};
// Trigger setupController
route.setupController(controller, model);
// checkUpdates is async and not awaited in setupController, so we need to wait a tick
await new Promise((resolve) => setTimeout(resolve, 10));
assert.ok(refreshPlaceCalled, 'refreshPlace should be called');
assert.ok(
selectPlaceCalled,
'mapUi.selectPlace should be called with update'
);
});
}); });

View File

@@ -0,0 +1,202 @@
import { module, test } from 'qunit';
import { setupTest } from 'marco/tests/helpers';
import Service from '@ember/service';
module('Unit | Service | storage', function (hooks) {
setupTest(hooks);
test('refreshPlace skips invalid places', async function (assert) {
let service = this.owner.lookup('service:storage');
let result = await service.refreshPlace({});
assert.strictEqual(result, null);
});
test('refreshPlace detects coordinate drift', async function (assert) {
let service = this.owner.lookup('service:storage');
// Stub OSM Service
class OsmStub extends Service {
async fetchOsmObject(id, type) {
return {
osmId: id,
osmType: type,
lat: 52.5201, // Changed significantly from 52.5200
lon: 13.405,
osmTags: { name: 'Foo' },
};
}
}
this.owner.register('service:osm', OsmStub);
// Mock storage update
let updatePlaceCalled = false;
service.updatePlace = async (place) => {
updatePlaceCalled = true;
return place;
};
let place = {
id: '123',
osmId: '456',
osmType: 'node',
lat: 52.52,
lon: 13.405,
osmTags: { name: 'Foo' },
title: 'Foo',
};
let result = await service.refreshPlace(place);
assert.ok(updatePlaceCalled, 'updatePlace should be called');
assert.strictEqual(result.lat, 52.5201, 'Latitude updated');
});
test('refreshPlace ignores tiny coordinate drift', async function (assert) {
let service = this.owner.lookup('service:storage');
class OsmStub extends Service {
async fetchOsmObject(id, type) {
return {
osmId: id,
osmType: type,
lat: 52.5200005, // Tiny change (< 0.00001)
lon: 13.405,
osmTags: { name: 'Foo' },
};
}
}
this.owner.register('service:osm', OsmStub);
let updatePlaceCalled = false;
service.updatePlace = async () => {
updatePlaceCalled = true;
};
let place = {
id: '123',
osmId: '456',
osmType: 'node',
lat: 52.52,
lon: 13.405,
osmTags: { name: 'Foo' },
title: 'Foo',
};
await service.refreshPlace(place);
assert.notOk(updatePlaceCalled, 'updatePlace should NOT be called');
});
test('refreshPlace detects tag changes', async function (assert) {
let service = this.owner.lookup('service:storage');
class OsmStub extends Service {
async fetchOsmObject(id, type) {
return {
osmId: id,
osmType: type,
lat: 52.52,
lon: 13.405,
osmTags: { name: 'Bar' }, // Changed name
};
}
}
this.owner.register('service:osm', OsmStub);
let updatePlaceCalled = false;
service.updatePlace = async (place) => {
updatePlaceCalled = true;
return place;
};
let place = {
id: '123',
osmId: '456',
osmType: 'node',
lat: 52.52,
lon: 13.405,
osmTags: { name: 'Foo' },
title: 'Foo',
};
let result = await service.refreshPlace(place);
assert.ok(updatePlaceCalled, 'updatePlace should be called');
assert.strictEqual(result.osmTags.name, 'Bar', 'Tags updated');
});
test('refreshPlace updates title if it was default', async function (assert) {
let service = this.owner.lookup('service:storage');
class OsmStub extends Service {
async fetchOsmObject(id, type) {
return {
osmId: id,
osmType: type,
lat: 52.52,
lon: 13.405,
osmTags: { name: 'New Name' },
};
}
}
this.owner.register('service:osm', OsmStub);
service.updatePlace = async (place) => place;
let place = {
id: '123',
osmId: '456',
osmType: 'node',
lat: 52.52,
lon: 13.405,
osmTags: { name: 'Old Name' },
title: 'Old Name', // Matches default
};
let result = await service.refreshPlace(place);
assert.strictEqual(result.title, 'New Name', 'Title should update');
});
test('refreshPlace preserves custom title', async function (assert) {
let service = this.owner.lookup('service:storage');
class OsmStub extends Service {
async fetchOsmObject(id, type) {
return {
osmId: id,
osmType: type,
lat: 52.52,
lon: 13.405,
osmTags: { name: 'New Name' },
};
}
}
this.owner.register('service:osm', OsmStub);
service.updatePlace = async (place) => place;
let place = {
id: '123',
osmId: '456',
osmType: 'node',
lat: 52.52,
lon: 13.405,
osmTags: { name: 'Old Name' },
title: 'My Custom Place', // User renamed it
};
let result = await service.refreshPlace(place);
assert.strictEqual(
result.title,
'My Custom Place',
'Title should NOT update'
);
assert.strictEqual(
result.osmTags.name,
'New Name',
'Tags should still update'
);
});
});