7 Commits

Author SHA1 Message Date
0f3359f725 1.8.0 2026-01-26 13:17:02 +07:00
25081f9cfc Add app icon, web app manifest 2026-01-26 13:16:24 +07:00
2efb15041e Update status doc 2026-01-24 21:15:58 +07:00
da64ae1572 1.7.0 2026-01-24 21:07:40 +07:00
1a96f95c82 Hide settings pane on outside click, render above places pane 2026-01-24 21:06:50 +07:00
911e6ddf38 Add setting for Overpass API provider 2026-01-24 20:47:55 +07:00
e61dc00725 Restore some lost styles 2026-01-24 20:47:42 +07:00
39 changed files with 385 additions and 30 deletions

View File

@@ -16,6 +16,7 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
- Detects clicks on visual vector tiles.
- Falls back to fetching authoritative data from an **Overpass API** service.
- Uses a **heuristic** (distance + type matching) to link visual clicks to API results (handling data desynchronization).
- **Logic Upgrade:** Map intelligently detects if _any_ sidebar/pane is open and handles outside clicks to close them instead of initiating new searches.
- **Optimization:** Added **10px hit tolerance** for easier tapping on mobile devices.
- **Visuals:** Increased bookmark marker size (Radius 9px) and added a subtle drop shadow.
- **Feedback:** Implemented a "pulse" animation (via OpenLayers Overlay) at the click location to visualize the search radius (30m/50m).
@@ -50,7 +51,9 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
- **Optimization:** Implemented **Debounced Reload** (200ms) for bookmark updates to handle rapid change events efficiently.
- **Optimization:** Correctly handles deletion/updates by clearing stale data for reloaded geohash sectors.
- `osm.js`: Fetches nearby POIs from Overpass API.
- **Configurable:** Now supports dynamic API endpoints via `SettingsService`.
- **Reliability:** Implemented `fetchWithRetry` to handle HTTP 504/502/503 timeouts and 429 rate limits, in addition to network errors.
- `settings.js`: Manages user preferences (currently Overpass API provider) persisted to `localStorage`.
- **UI Components:**
- `places-sidebar.gjs`: Displays a list of nearby POIs.
- **Layout:** Responsive design that transforms into a **Bottom Sheet** (50% height) on mobile screens (`<=768px`) with rounded corners and upward shadow.
@@ -59,7 +62,9 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
- **Layout:** Polished UI with distinct sections for Actions and Meta info.
- `app-header.gjs`: Transparent header with "Menu" button (Settings) and User Avatar (Login).
- `settings-pane.gjs`: Sidebar component for app info ("About" section) and settings.
- **Features:** Dropdown to select Overpass API provider (bke.ro, overpass-api.de, private.coffee).
- **Mobile:** Renders as a 2/3 height bottom sheet on mobile.
- **Z-Index:** Configured to overlay the Places sidebar correctly (`z-index: 3200`).
- **Geo Utils:**
- `app/utils/geo.js`: Haversine distance calculations.
- `app/utils/geohash-coverage.js`: Logic to calculate required 4-char geohash prefixes for a given bounding box.
@@ -82,12 +87,13 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
5. User clicks "Save Bookmark" -> Stores JSON in RemoteStorage.
6. 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.
8. **Settings:** User can change the Overpass API provider via the new Settings menu.
## Files Currently in Focus
- `app/styles/app.css`: Responsive sidebar styles and mobile optimizations.
- `app/components/place-details.gjs`: Place display and editing logic.
- `app/services/storage.js`: Data sync and update logic.
- `app/templates/application.gjs`: Core layout and "Outside Click" logic.
- `app/components/settings-pane.gjs`: Settings UI.
- `app/services/settings.js`: Settings persistence.
## Next Steps & Pending Tasks

View File

@@ -1,10 +1,16 @@
import Component from '@glimmer/component';
import { on } from '@ember/modifier';
import { service } from '@ember/service';
import { action } from '@ember/object';
import Icon from '#components/icon';
import eq from 'ember-truth-helpers/helpers/eq';
export default class SettingsPane extends Component {
constructor() {
super(...arguments);
@service settings;
@action
updateApi(event) {
this.settings.updateOverpassApi(event.target.value);
}
<template>
@@ -19,9 +25,23 @@ export default class SettingsPane extends Component {
<div class="sidebar-content">
<section class="settings-section">
<h3>Settings</h3>
<p>
<em>App settings/preferences go here.</em>
</p>
<div class="form-group">
<label for="overpass-api">Overpass API Provider</label>
<select
id="overpass-api"
class="form-control"
{{on "change" this.updateApi}}
>
{{#each this.settings.overpassApis as |api|}}
<option
value={{api.url}}
selected={{if (eq api.url this.settings.overpassApi) "selected"}}
>
{{api.name}}
</option>
{{/each}}
</select>
</div>
</section>
<section class="settings-section">
<h3>About</h3>

View File

@@ -1,6 +1,8 @@
import Service from '@ember/service';
import Service, { service } from '@ember/service';
export default class OsmService extends Service {
@service settings;
controller = null;
async getNearbyPois(lat, lon, radius = 50) {
@@ -23,10 +25,7 @@ export default class OsmService extends Service {
out center;
`.trim();
const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent(
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
query
)}`;
const url = `${this.settings.overpassApi}?data=${encodeURIComponent(query)}`;
try {
const res = await this.fetchWithRetry(url, { signal });
@@ -101,10 +100,7 @@ out center;
`.trim();
}
const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent(
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
query
)}`;
const url = `${this.settings.overpassApi}?data=${encodeURIComponent(query)}`;
const res = await this.fetchWithRetry(url);
if (!res.ok) throw new Error('Overpass request failed');
const data = await res.json();

32
app/services/settings.js Normal file
View File

@@ -0,0 +1,32 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class SettingsService extends Service {
@tracked overpassApi = 'https://overpass.bke.ro/api/interpreter';
overpassApis = [
{ name: 'bke.ro', url: 'https://overpass.bke.ro/api/interpreter' },
{ name: 'overpass-api.de', url: 'https://overpass-api.de/api/interpreter' },
{
name: 'private.coffee',
url: 'https://overpass.private.coffee/api/interpreter',
},
];
constructor() {
super(...arguments);
this.loadSettings();
}
loadSettings() {
const savedApi = localStorage.getItem('marco-overpass-api');
if (savedApi) {
this.overpassApi = savedApi;
}
}
updateOverpassApi(url) {
this.overpassApi = url;
localStorage.setItem('marco-overpass-api', url);
}
}

View File

@@ -197,6 +197,10 @@ body {
flex-direction: column;
}
.settings-pane.sidebar {
z-index: 3200; /* Higher than Places Sidebar (3100) */
}
/* Settings Pane Mobile Overrides */
@media (width <= 768px) {
.settings-pane.sidebar {
@@ -280,6 +284,10 @@ body {
letter-spacing: 0.5px;
}
.settings-section .form-group {
margin-top: 1rem;
}
.btn-full {
width: 100%;
}
@@ -298,6 +306,16 @@ body {
background: #0069d9;
}
.meta-info {
font-size: 0.9rem;
}
.meta-info p:first-child {
margin-top: 1.2rem;
padding-top: 1.2rem;
border-top: 1px solid #eee;
}
.meta-info a {
color: #007bff;
text-decoration: none;

View File

@@ -8,7 +8,7 @@ import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { eq } from 'ember-truth-helpers';
import { and } from 'ember-truth-helpers';
import { and, or } from 'ember-truth-helpers';
import { on } from '@ember/modifier';
export default class ApplicationComponent extends Component {
@@ -74,6 +74,15 @@ export default class ApplicationComponent extends Component {
}
}
@action
handleOutsideClick() {
if (this.isSettingsOpen) {
this.closeSettings();
} else {
this.closeSidebar();
}
}
@action
closeSidebar() {
this.nearbyPlaces = null;
@@ -86,7 +95,7 @@ export default class ApplicationComponent extends Component {
}
<template>
{{pageTitle "M/\RCO"}}
{{pageTitle "Marco"}}
<AppHeader @onToggleMenu={{this.toggleSettings}} />
@@ -105,8 +114,8 @@ export default class ApplicationComponent extends Component {
<Map
@onPlacesFound={{this.showPlaces}}
@isSidebarOpen={{this.isSidebarOpen}}
@onOutsideClick={{this.closeSidebar}}
@isSidebarOpen={{or this.isSidebarOpen this.isSettingsOpen}}
@onOutsideClick={{this.handleOutsideClick}}
/>
{{#if (and (eq this.router.currentRouteName "index") this.nearbyPlaces)}}

View File

@@ -6,7 +6,27 @@
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/app/styles/app.css">
<!-- App identity -->
<meta name="application-name" content="Marco">
<meta name="apple-mobile-web-app-title" content="Marco">
<meta name="theme-color" content="#F6E9A6">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.webmanifest">
<!-- Icons -->
<link rel="icon" href="/icons/icon.svg" type="image/svg+xml">
<!-- iOS -->
<link rel="apple-touch-icon" href="/icons/icon-180.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<!-- Windows -->
<meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
<link rel="stylesheet" href="/app/styles/app.css">
</head>
<body>
<script type="module">

View File

@@ -1,6 +1,6 @@
{
"name": "marco",
"version": "1.6.1",
"version": "1.8.0",
"private": true,
"description": "Small description for marco goes here",
"repository": "",

BIN
public/icons/icon-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/icons/icon-144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
public/icons/icon-152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/icons/icon-167.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/icons/icon-180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
public/icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/icons/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

BIN
public/icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,45 @@
<svg
width="1024"
height="1024"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background -->
<rect
x="0"
y="0"
width="1024"
height="1024"
rx="220"
fill="#F6E9A6"
/>
<!-- Subtle map grid (kept well outside safe zone) -->
<g stroke="#E6D88A" stroke-width="10" opacity="0.6">
<line x1="256" y1="0" x2="256" y2="1024" />
<line x1="512" y1="0" x2="512" y2="1024" />
<line x1="768" y1="0" x2="768" y2="1024" />
<line x1="0" y1="256" x2="1024" y2="256" />
<line x1="0" y1="512" x2="1024" y2="512" />
<line x1="0" y1="768" x2="1024" y2="768" />
</g>
<!-- Location pin (exact app shape, larger, centered, safe-zone compliant) -->
<!-- Safe zone target: ~680px diameter -->
<g
transform="
translate(512 512)
scale(22)
translate(-12 -12)
"
fill="#ea4335"
stroke="#b31412"
stroke-width="1"
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" />
<circle cx="12" cy="10" r="3" fill="#b31412" stroke="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

44
public/icons/icon.svg Normal file
View File

@@ -0,0 +1,44 @@
<svg
width="1024"
height="1024"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background -->
<rect
x="0"
y="0"
width="1024"
height="1024"
fill="#F6E9A6"
/>
<!-- Subtle map grid (kept well outside safe zone) -->
<g stroke="#E6D88A" stroke-width="10" opacity="0.6">
<line x1="256" y1="0" x2="256" y2="1024" />
<line x1="512" y1="0" x2="512" y2="1024" />
<line x1="768" y1="0" x2="768" y2="1024" />
<line x1="0" y1="256" x2="1024" y2="256" />
<line x1="0" y1="512" x2="1024" y2="512" />
<line x1="0" y1="768" x2="1024" y2="768" />
</g>
<!-- Location pin (exact app shape, larger, centered, safe-zone compliant) -->
<!-- Safe zone target: ~680px diameter -->
<g
transform="
translate(512 512)
scale(22)
translate(-12 -12)
"
fill="#ea4335"
stroke="#b31412"
stroke-width="1"
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" />
<circle cx="12" cy="10" r="3" fill="#b31412" stroke="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,28 @@
{
"name": "Marco",
"short_name": "Marco",
"description": "Marco (as in Marco Polo) is an unhosted maps application that respects your privacy and choices",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#F6E9A6",
"theme_color": "#F6E9A6",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

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

BIN
release/icons/icon-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
release/icons/icon-144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
release/icons/icon-152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
release/icons/icon-167.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
release/icons/icon-180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
release/icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
release/icons/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

BIN
release/icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
release/icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,45 @@
<svg
width="1024"
height="1024"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background -->
<rect
x="0"
y="0"
width="1024"
height="1024"
rx="220"
fill="#F6E9A6"
/>
<!-- Subtle map grid (kept well outside safe zone) -->
<g stroke="#E6D88A" stroke-width="10" opacity="0.6">
<line x1="256" y1="0" x2="256" y2="1024" />
<line x1="512" y1="0" x2="512" y2="1024" />
<line x1="768" y1="0" x2="768" y2="1024" />
<line x1="0" y1="256" x2="1024" y2="256" />
<line x1="0" y1="512" x2="1024" y2="512" />
<line x1="0" y1="768" x2="1024" y2="768" />
</g>
<!-- Location pin (exact app shape, larger, centered, safe-zone compliant) -->
<!-- Safe zone target: ~680px diameter -->
<g
transform="
translate(512 512)
scale(22)
translate(-12 -12)
"
fill="#ea4335"
stroke="#b31412"
stroke-width="1"
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" />
<circle cx="12" cy="10" r="3" fill="#b31412" stroke="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

44
release/icons/icon.svg Normal file
View File

@@ -0,0 +1,44 @@
<svg
width="1024"
height="1024"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background -->
<rect
x="0"
y="0"
width="1024"
height="1024"
fill="#F6E9A6"
/>
<!-- Subtle map grid (kept well outside safe zone) -->
<g stroke="#E6D88A" stroke-width="10" opacity="0.6">
<line x1="256" y1="0" x2="256" y2="1024" />
<line x1="512" y1="0" x2="512" y2="1024" />
<line x1="768" y1="0" x2="768" y2="1024" />
<line x1="0" y1="256" x2="1024" y2="256" />
<line x1="0" y1="512" x2="1024" y2="512" />
<line x1="0" y1="768" x2="1024" y2="768" />
</g>
<!-- Location pin (exact app shape, larger, centered, safe-zone compliant) -->
<!-- Safe zone target: ~680px diameter -->
<g
transform="
translate(512 512)
scale(22)
translate(-12 -12)
"
fill="#ea4335"
stroke="#b31412"
stroke-width="1"
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" />
<circle cx="12" cy="10" r="3" fill="#b31412" stroke="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,9 +5,29 @@
<title>Marco</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="module" crossorigin src="/assets/main-CSdnGzLO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CrGC4Dlj.css">
<!-- App identity -->
<meta name="application-name" content="Marco">
<meta name="apple-mobile-web-app-title" content="Marco">
<meta name="theme-color" content="#F6E9A6">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.webmanifest">
<!-- Icons -->
<link rel="icon" href="/icons/icon.svg" type="image/svg+xml">
<!-- iOS -->
<link rel="apple-touch-icon" href="/icons/icon-180.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<!-- Windows -->
<meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-CYQJLBu1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-B9HZHSjP.css">
</head>
<body>
</body>

View File

@@ -0,0 +1,28 @@
{
"name": "Marco",
"short_name": "Marco",
"description": "Marco (as in Marco Polo) is an unhosted maps application that respects your privacy and choices",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#F6E9A6",
"theme_color": "#F6E9A6",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}