Compare commits
4 Commits
v1.10.1
...
0af9d9f16d
| Author | SHA1 | Date | |
|---|---|---|---|
|
0af9d9f16d
|
|||
|
a0f132ec64
|
|||
|
925f26ae5d
|
|||
|
58bb8831f3
|
@@ -92,6 +92,17 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
|
|||||||
- **Smart Linking:** The `showPlaces` action intercepts search results and automatically resolves them to existing **Bookmarks** if a match is found (via `storage.findPlaceById`). This ensures the app navigates to the persistent Bookmark URL (ULID) and correctly reflects the "Saved" status in the UI instead of treating it as a new generic OSM place.
|
- **Smart Linking:** The `showPlaces` action intercepts search results and automatically resolves them to existing **Bookmarks** if a match is found (via `storage.findPlaceById`). This ensures the app navigates to the persistent Bookmark URL (ULID) and correctly reflects the "Saved" status in the UI instead of treating it as a new generic OSM place.
|
||||||
- **Data Normalization:** Refactored `OsmService` to return normalized objects (`osmTags`, `osmType`) for all queries. This ensures consistent data structures between fresh Overpass results and saved bookmarks throughout the app.
|
- **Data Normalization:** Refactored `OsmService` to return normalized objects (`osmTags`, `osmType`) for all queries. This ensures consistent data structures between fresh Overpass results and saved bookmarks throughout the app.
|
||||||
|
|
||||||
|
### 5. Creation & Editing Workflow
|
||||||
|
|
||||||
|
- **Create Place:**
|
||||||
|
- Implemented `/place/new` route for creating new private places.
|
||||||
|
- **UX:** Map displays a central crosshair for precise location selection.
|
||||||
|
- **Mobile Optimization:**
|
||||||
|
- Disabled map inertia (`kinetic: false`) to ensure the map stops exactly where the finger releases.
|
||||||
|
- `PlaceEditForm` conditionally disables autofocus on mobile screens (`<= 768px`) to prevent the onscreen keyboard from obscuring the map view immediately.
|
||||||
|
- Responsive crosshair sizing (48px desktop / 24px mobile).
|
||||||
|
- **Persistence:** Form data (Title, Description) and Map coordinates are securely saved to RemoteStorage via `storage.storePlace`.
|
||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
- **Repo:** The app runs via `pnpm start`.
|
- **Repo:** The app runs via `pnpm start`.
|
||||||
@@ -102,20 +113,20 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
|
|||||||
- If direct match: Redirect to `/place/:id`.
|
- If direct match: Redirect to `/place/:id`.
|
||||||
- If multiple results: Show `/search` list view.
|
- If multiple results: Show `/search` list view.
|
||||||
4. Sidebar displays details via `<PlaceDetails>` component (Bottom sheet on mobile).
|
4. Sidebar displays details via `<PlaceDetails>` component (Bottom sheet on mobile).
|
||||||
5. User clicks "Save Bookmark" -> Stores JSON in RemoteStorage.
|
5. **Creation:** User clicks "Create Place" -> Enters creation mode (crosshair) -> Positions map -> Enters details -> Save.
|
||||||
6. RemoteStorage change event -> Debounced reload updates the map reactive-ly.
|
6. **Persistence:** RemoteStorage change event -> Debounced reload updates the map reactive-ly.
|
||||||
7. **Editing:** User can edit the Title and Description of saved bookmarks via an "Edit" button in the details view.
|
7. **Editing:** User can edit the Title and Description of saved bookmarks via an "Edit" button in the details view.
|
||||||
8. **Settings:** User can change the Overpass API provider via the new Settings menu.
|
8. **Settings:** User can change the Overpass API provider via the new Settings menu.
|
||||||
|
|
||||||
## Files Currently in Focus
|
## Files Currently in Focus
|
||||||
|
|
||||||
- `app/services/osm.js`: Caching logic.
|
- `app/components/map.gjs`
|
||||||
- `app/routes/search.js`: Search heuristics.
|
- `app/components/place-edit-form.gjs`
|
||||||
- `app/components/place-details.gjs`: Formatting logic.
|
- `app/templates/place/new.gjs`
|
||||||
|
|
||||||
## Next Steps & Pending Tasks
|
## Next Steps & Pending Tasks
|
||||||
|
|
||||||
1. **Linting & Code Quality:** Fix remaining CSS errors, remove inline styles in `map.gjs`, and address unused variables/runloop usage.
|
1. **Linting & Code Quality:** Fix remaining CSS errors and address unused variables/runloop usage.
|
||||||
2. **Testing:** Add automated tests for the geohash coverage, retry logic, and new editing features.
|
2. **Testing:** Add automated tests for the geohash coverage, retry logic, and new editing features.
|
||||||
3. **Performance:** Monitor performance with large datasets (thousands of bookmarks).
|
3. **Performance:** Monitor performance with large datasets (thousands of bookmarks).
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { modifier } from 'ember-modifier';
|
|||||||
import 'ol/ol.css';
|
import 'ol/ol.css';
|
||||||
import Map from 'ol/Map.js';
|
import Map from 'ol/Map.js';
|
||||||
import { defaults as defaultControls, Control } from 'ol/control.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';
|
||||||
import View from 'ol/View.js';
|
import View from 'ol/View.js';
|
||||||
import { fromLonLat, toLonLat, getPointResolution } from 'ol/proj.js';
|
import { fromLonLat, toLonLat, getPointResolution } from 'ol/proj.js';
|
||||||
import Overlay from 'ol/Overlay.js';
|
import Overlay from 'ol/Overlay.js';
|
||||||
@@ -21,6 +23,7 @@ export default class MapComponent extends Component {
|
|||||||
@service storage;
|
@service storage;
|
||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service settings;
|
||||||
|
|
||||||
mapInstance;
|
mapInstance;
|
||||||
bookmarkSource;
|
bookmarkSource;
|
||||||
@@ -100,6 +103,9 @@ export default class MapComponent extends Component {
|
|||||||
rotate: true,
|
rotate: true,
|
||||||
attribution: true,
|
attribution: true,
|
||||||
}),
|
}),
|
||||||
|
interactions: defaultInteractions({
|
||||||
|
dragPan: false, // Disable default DragPan to add a custom one
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty');
|
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty');
|
||||||
@@ -353,6 +359,28 @@ export default class MapComponent extends Component {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateInteractions = modifier(() => {
|
||||||
|
if (!this.mapInstance) return;
|
||||||
|
|
||||||
|
// Remove existing DragPan interactions
|
||||||
|
this.mapInstance.getInteractions().getArray().slice().forEach((interaction) => {
|
||||||
|
if (interaction instanceof DragPan) {
|
||||||
|
this.mapInstance.removeInteraction(interaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new DragPan with current setting
|
||||||
|
const kinetic = this.settings.mapKinetic
|
||||||
|
? new Kinetic(-0.005, 0.05, 100)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
this.mapInstance.addInteraction(
|
||||||
|
new DragPan({
|
||||||
|
kinetic: kinetic,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Track the selected place from the UI Service (Router -> Map)
|
// Track the selected place from the UI Service (Router -> Map)
|
||||||
updateSelectedPin = modifier(() => {
|
updateSelectedPin = modifier(() => {
|
||||||
const selected = this.mapUi.selectedPlace;
|
const selected = this.mapUi.selectedPlace;
|
||||||
@@ -736,6 +764,7 @@ export default class MapComponent extends Component {
|
|||||||
<div
|
<div
|
||||||
class="map-container {{if @isSidebarOpen 'sidebar-open'}}"
|
class="map-container {{if @isSidebarOpen 'sidebar-open'}}"
|
||||||
{{this.setupMap}}
|
{{this.setupMap}}
|
||||||
|
{{this.updateInteractions}}
|
||||||
{{this.updateBookmarks}}
|
{{this.updateBookmarks}}
|
||||||
{{this.updateSelectedPin}}
|
{{this.updateSelectedPin}}
|
||||||
{{this.syncPulse}}
|
{{this.syncPulse}}
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ export default class PlaceEditForm extends Component {
|
|||||||
this.description = this.args.place?.description || '';
|
this.description = this.args.place?.description || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get shouldAutofocus() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return window.innerWidth > 768;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleSubmit(event) {
|
handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -45,7 +52,7 @@ export default class PlaceEditForm extends Component {
|
|||||||
{{on "input" this.updateTitle}}
|
{{on "input" this.updateTitle}}
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Name of the place"
|
placeholder="Name of the place"
|
||||||
autofocus
|
autofocus={{this.shouldAutofocus}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { service } from '@ember/service';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import Icon from '#components/icon';
|
import Icon from '#components/icon';
|
||||||
import eq from 'ember-truth-helpers/helpers/eq';
|
import eq from 'ember-truth-helpers/helpers/eq';
|
||||||
|
import not from 'ember-truth-helpers/helpers/not';
|
||||||
|
|
||||||
export default class SettingsPane extends Component {
|
export default class SettingsPane extends Component {
|
||||||
@service settings;
|
@service settings;
|
||||||
@@ -13,6 +14,11 @@ export default class SettingsPane extends Component {
|
|||||||
this.settings.updateOverpassApi(event.target.value);
|
this.settings.updateOverpassApi(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleKinetic(event) {
|
||||||
|
this.settings.updateMapKinetic(event.target.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar settings-pane">
|
<div class="sidebar settings-pane">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -25,6 +31,27 @@ export default class SettingsPane extends Component {
|
|||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
|
||||||
|
<select
|
||||||
|
id="map-kinetic"
|
||||||
|
class="form-control"
|
||||||
|
{{on "change" this.toggleKinetic}}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="true"
|
||||||
|
selected={{if this.settings.mapKinetic "selected"}}
|
||||||
|
>
|
||||||
|
On
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="false"
|
||||||
|
selected={{if (not this.settings.mapKinetic) "selected"}}
|
||||||
|
>
|
||||||
|
Off
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="overpass-api">Overpass API Provider</label>
|
<label for="overpass-api">Overpass API Provider</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking';
|
|||||||
|
|
||||||
export default class SettingsService extends Service {
|
export default class SettingsService extends Service {
|
||||||
@tracked overpassApi = 'https://overpass.bke.ro/api/interpreter';
|
@tracked overpassApi = 'https://overpass.bke.ro/api/interpreter';
|
||||||
|
@tracked mapKinetic = true;
|
||||||
|
|
||||||
overpassApis = [
|
overpassApis = [
|
||||||
{ name: 'bke.ro', url: 'https://overpass.bke.ro/api/interpreter' },
|
{ name: 'bke.ro', url: 'https://overpass.bke.ro/api/interpreter' },
|
||||||
@@ -19,14 +20,30 @@ export default class SettingsService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
const savedApi = localStorage.getItem('marco-overpass-api');
|
const savedApi = localStorage.getItem('marco:overpass-api');
|
||||||
if (savedApi) {
|
if (savedApi) {
|
||||||
this.overpassApi = savedApi;
|
this.overpassApi = savedApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedKinetic = localStorage.getItem('marco:map-kinetic');
|
||||||
|
if (savedKinetic !== null) {
|
||||||
|
this.mapKinetic = savedKinetic === 'true';
|
||||||
|
} else {
|
||||||
|
// Default: disabled on small screens (mobile), enabled on desktop
|
||||||
|
// We check for typical mobile width (<= 768px)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
this.mapKinetic = window.innerWidth > 768;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverpassApi(url) {
|
updateOverpassApi(url) {
|
||||||
this.overpassApi = url;
|
this.overpassApi = url;
|
||||||
localStorage.setItem('marco-overpass-api', url);
|
localStorage.setItem('marco:overpass-api', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMapKinetic(enabled) {
|
||||||
|
this.mapKinetic = enabled;
|
||||||
|
localStorage.setItem('marco:map-kinetic', String(enabled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.10.1",
|
"version": "1.11.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
@@ -26,7 +26,7 @@
|
|||||||
<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-Dep3TjPE.js"></script>
|
<script type="module" crossorigin src="/assets/main-y8e9Z0x2.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-D53xPL_H.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-D53xPL_H.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user