Compare commits
1 Commits
v1.16.0
...
c362268567
| Author | SHA1 | Date | |
|---|---|---|---|
|
c362268567
|
@@ -1,14 +0,0 @@
|
|||||||
name-template: 'v$RESOLVED_VERSION'
|
|
||||||
tag-template: 'v$RESOLVED_VERSION'
|
|
||||||
version-resolver:
|
|
||||||
major:
|
|
||||||
labels:
|
|
||||||
- 'release/major'
|
|
||||||
minor:
|
|
||||||
labels:
|
|
||||||
- 'release/minor'
|
|
||||||
- 'feature'
|
|
||||||
patch:
|
|
||||||
labels:
|
|
||||||
- 'release/patch'
|
|
||||||
default: patch
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
name: Release Drafter
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
jobs:
|
|
||||||
release_drafter_job:
|
|
||||||
name: Update release notes draft
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Release Drafter
|
|
||||||
uses: https://github.com/raucao/gitea-release-drafter@dev
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { on } from '@ember/modifier';
|
|
||||||
import Icon from '#components/icon';
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{! template-lint-disable no-nested-interactive }}
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<button type="button" class="back-btn" {{on "click" @onBack}}>
|
|
||||||
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
<h2>About</h2>
|
|
||||||
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
|
||||||
<Icon @name="x" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-content">
|
|
||||||
<section class="about-section">
|
|
||||||
<p>
|
|
||||||
<strong>Marco</strong>
|
|
||||||
(as in
|
|
||||||
<a
|
|
||||||
href="https://en.wikipedia.org/wiki/Marco_Polo"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>Marco Polo</a>) is an unhosted maps application that respects your
|
|
||||||
privacy and choices.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Connect your own
|
|
||||||
<a
|
|
||||||
href="https://remotestorage.io/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>remote storage</a>
|
|
||||||
to sync place bookmarks across apps and devices.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<Icon @name="gift" @size={{20}} />
|
|
||||||
<span>Open Source</span>
|
|
||||||
</summary>
|
|
||||||
<div class="details-content">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Source</th>
|
|
||||||
<th>License</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="https://gitea.kosmos.org/raucao/marco"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
Marco App
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<abbr title="GNU Affero General Public License">AGPL</abbr>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="https://openstreetmap.org/copyright"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
Map Data
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="https://opendatacommons.org/licenses/odbl/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<abbr
|
|
||||||
title="Open Data Commons Open Database License"
|
|
||||||
>ODbL</abbr>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="https://github.com/feathericons/feather"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
Feather Icons
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="https://en.wikipedia.org/wiki/MIT_License"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<abbr title="MIT License">MIT</abbr>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<Icon @name="heart" @size={{20}} @color="#e5533d" />
|
|
||||||
<span>Contribute</span>
|
|
||||||
</summary>
|
|
||||||
<div class="details-content">
|
|
||||||
<p>
|
|
||||||
<strong>Most impactful:</strong>
|
|
||||||
Add and improve data for points of interest in
|
|
||||||
<a
|
|
||||||
href="https://www.openstreetmap.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>OpenStreetMap</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Most appreciated:</strong>
|
|
||||||
Use this app as much as you can and
|
|
||||||
<a
|
|
||||||
href="https://community.remotestorage.io/t/marco-an-unhosted-maps-app/941"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>submit feedback</a>
|
|
||||||
about your experience, problems, feature wishes, etc.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Most supportive:</strong>
|
|
||||||
Tell others about this app, on social media, in blog posts,
|
|
||||||
educational videos, etc.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { on } from '@ember/modifier';
|
|
||||||
import { fn } from '@ember/helper';
|
|
||||||
import { htmlSafe } from '@ember/template';
|
|
||||||
import Icon from '#components/icon';
|
|
||||||
import iconRounded from '../../icons/icon-rounded.svg?raw';
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h2>
|
|
||||||
<span class="app-logo-icon">
|
|
||||||
{{htmlSafe iconRounded}}
|
|
||||||
</span>
|
|
||||||
Marco
|
|
||||||
</h2>
|
|
||||||
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
|
||||||
<Icon @name="x" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-content">
|
|
||||||
<ul class="app-menu">
|
|
||||||
<li>
|
|
||||||
<button type="button" {{on "click" (fn @onNavigate "settings")}}>
|
|
||||||
<Icon @name="settings" @size={{20}} />
|
|
||||||
<span>Settings</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button type="button" {{on "click" (fn @onNavigate "about")}}>
|
|
||||||
<Icon @name="info" @size={{20}} />
|
|
||||||
<span>About</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Component from '@glimmer/component';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
import { fn } from '@ember/helper';
|
|
||||||
import eq from 'ember-truth-helpers/helpers/eq';
|
|
||||||
|
|
||||||
import AppMenuHome from './home';
|
|
||||||
import AppMenuSettings from './settings';
|
|
||||||
import AppMenuAbout from './about';
|
|
||||||
|
|
||||||
export default class AppMenu extends Component {
|
|
||||||
@tracked currentView = 'menu'; // 'menu', 'settings', 'about'
|
|
||||||
|
|
||||||
@action
|
|
||||||
setView(view) {
|
|
||||||
this.currentView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="sidebar app-menu-pane">
|
|
||||||
{{#if (eq this.currentView "menu")}}
|
|
||||||
<AppMenuHome @onNavigate={{this.setView}} @onClose={{@onClose}} />
|
|
||||||
|
|
||||||
{{else if (eq this.currentView "settings")}}
|
|
||||||
<AppMenuSettings
|
|
||||||
@onBack={{fn this.setView "menu"}}
|
|
||||||
@onClose={{@onClose}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{else if (eq this.currentView "about")}}
|
|
||||||
<AppMenuAbout
|
|
||||||
@onBack={{fn this.setView "menu"}}
|
|
||||||
@onClose={{@onClose}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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 AppMenuSettings extends Component {
|
|
||||||
@service settings;
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateApi(event) {
|
|
||||||
this.settings.updateOverpassApi(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
toggleKinetic(event) {
|
|
||||||
this.settings.updateMapKinetic(event.target.value === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updatePhotonApi(event) {
|
|
||||||
this.settings.updatePhotonApi(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<button type="button" class="back-btn" {{on "click" @onBack}}>
|
|
||||||
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
|
||||||
<Icon @name="x" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-content">
|
|
||||||
<section class="settings-section">
|
|
||||||
<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={{unless this.settings.mapKinetic "selected"}}
|
|
||||||
>
|
|
||||||
Off
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="photon-api">Photon API Provider</label>
|
|
||||||
<select
|
|
||||||
id="photon-api"
|
|
||||||
class="form-control"
|
|
||||||
{{on "change" this.updatePhotonApi}}
|
|
||||||
>
|
|
||||||
{{#each this.settings.photonApis as |api|}}
|
|
||||||
<option
|
|
||||||
value={{api.url}}
|
|
||||||
selected={{if (eq api.url this.settings.photonApi) "selected"}}
|
|
||||||
>
|
|
||||||
{{api.name}}
|
|
||||||
</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -499,9 +499,10 @@ export default class MapComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.preventZoom) {
|
if (options.preventZoom) {
|
||||||
// If we are preventing zoom (e.g. user clicked a bookmark), we rely on visibility check.
|
// If we are preventing zoom (e.g. user clicked a bookmark), we still need to center
|
||||||
// This avoids unnecessary panning if the place is already visible.
|
// but without changing the zoom level.
|
||||||
this.handlePinVisibility(coords, { maintainZoom: true });
|
// We use animateToSmartCenter without a second argument (zoom=null).
|
||||||
|
this.animateToSmartCenter(coords);
|
||||||
} else if (selected.bbox) {
|
} else if (selected.bbox) {
|
||||||
this.zoomToBbox(selected.bbox);
|
this.zoomToBbox(selected.bbox);
|
||||||
} else {
|
} else {
|
||||||
@@ -546,10 +547,7 @@ export default class MapComponent extends Component {
|
|||||||
}
|
}
|
||||||
// Desktop: Sidebar covers left side (approx 400px)
|
// Desktop: Sidebar covers left side (approx 400px)
|
||||||
else if (this.args.isSidebarOpen) {
|
else if (this.args.isSidebarOpen) {
|
||||||
const sidebarWidthVar = getComputedStyle(document.documentElement)
|
const sidebarWidth = 400;
|
||||||
.getPropertyValue('--sidebar-width')
|
|
||||||
.trim();
|
|
||||||
const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360;
|
|
||||||
const visibleWidth = size[0] - sidebarWidth;
|
const visibleWidth = size[0] - sidebarWidth;
|
||||||
|
|
||||||
// Left padding: Sidebar + 15% of visible width
|
// Left padding: Sidebar + 15% of visible width
|
||||||
@@ -568,15 +566,14 @@ export default class MapComponent extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePinVisibility(coords, options = {}) {
|
handlePinVisibility(coords) {
|
||||||
if (!this.mapInstance) return;
|
if (!this.mapInstance) return;
|
||||||
|
|
||||||
const view = this.mapInstance.getView();
|
const view = this.mapInstance.getView();
|
||||||
const currentZoom = view.getZoom();
|
const currentZoom = view.getZoom();
|
||||||
|
|
||||||
// If too far out (e.g. world view), zoom in to neighborhood level (16)
|
// If too far out (e.g. world view), zoom in to neighborhood level (16)
|
||||||
// UNLESS we want to maintain the current zoom
|
if (currentZoom < 16) {
|
||||||
if (!options.maintainZoom && currentZoom < 16) {
|
|
||||||
this.animateToSmartCenter(coords, 16);
|
this.animateToSmartCenter(coords, 16);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -593,12 +590,8 @@ export default class MapComponent extends Component {
|
|||||||
pixel[1] > size[1];
|
pixel[1] > size[1];
|
||||||
|
|
||||||
if (isOffScreen) {
|
if (isOffScreen) {
|
||||||
// If off-screen, center it smartly (considering sidebar/bottom sheet)
|
this.animateToSmartCenter(coords);
|
||||||
// Pass maintainZoom to prevent zoom reset if desired
|
|
||||||
const zoom = options.maintainZoom ? null : 16;
|
|
||||||
this.animateToSmartCenter(coords, zoom);
|
|
||||||
} else {
|
} else {
|
||||||
// If on-screen, only pan if obscured by UI
|
|
||||||
this.panIfObscured(coords);
|
this.panIfObscured(coords);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,28 +627,6 @@ export default class MapComponent extends Component {
|
|||||||
// To move the camera South (Lower Y), we subtract.
|
// To move the camera South (Lower Y), we subtract.
|
||||||
targetCenter = [coords[0], coords[1] - offsetMapUnits];
|
targetCenter = [coords[0], coords[1] - offsetMapUnits];
|
||||||
}
|
}
|
||||||
// Desktop: Check if sidebar is open
|
|
||||||
else if (this.args.isSidebarOpen) {
|
|
||||||
const sidebarWidthVar = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue('--sidebar-width')
|
|
||||||
.trim();
|
|
||||||
const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360;
|
|
||||||
|
|
||||||
// We want the pin to be in the center of the remaining space.
|
|
||||||
// Remaining space starts at x = sidebarWidth.
|
|
||||||
// Center of remaining space = sidebarWidth + (totalWidth - sidebarWidth) / 2
|
|
||||||
// = sidebarWidth/2 + totalWidth/2
|
|
||||||
// Map Center is totalWidth/2
|
|
||||||
// Offset = sidebarWidth/2 (to the right)
|
|
||||||
|
|
||||||
const offsetPixels = sidebarWidth / 2;
|
|
||||||
const offsetMapUnits = offsetPixels * resolution;
|
|
||||||
|
|
||||||
// We want pin at center + offset.
|
|
||||||
// So map center must be pin - offset.
|
|
||||||
// X increases to the right.
|
|
||||||
targetCenter = [coords[0] - offsetMapUnits, coords[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
const animationOptions = {
|
const animationOptions = {
|
||||||
center: targetCenter,
|
center: targetCenter,
|
||||||
@@ -674,73 +645,33 @@ export default class MapComponent extends Component {
|
|||||||
if (!this.mapInstance) return;
|
if (!this.mapInstance) return;
|
||||||
|
|
||||||
const size = this.mapInstance.getSize();
|
const size = this.mapInstance.getSize();
|
||||||
|
// Check if mobile (width <= 768px matches CSS)
|
||||||
|
if (size[0] > 768) return;
|
||||||
|
|
||||||
const pixel = this.mapInstance.getPixelFromCoordinate(coords);
|
const pixel = this.mapInstance.getPixelFromCoordinate(coords);
|
||||||
if (!pixel) return;
|
if (!pixel) return;
|
||||||
|
|
||||||
const view = this.mapInstance.getView();
|
const height = size[1];
|
||||||
const center = view.getCenter();
|
|
||||||
const resolution = view.getResolution();
|
|
||||||
|
|
||||||
// Default targets (current position)
|
// Sidebar covers the bottom 50%
|
||||||
let targetPixelX = pixel[0];
|
const splitPoint = height / 2;
|
||||||
let targetPixelY = pixel[1];
|
|
||||||
let needsPan = false;
|
|
||||||
|
|
||||||
// 1. Mobile Bottom Sheet Logic (Screen <= 768px)
|
// If the pin is in the bottom half (y > splitPoint), it is obscured
|
||||||
if (size[0] <= 768) {
|
if (pixel[1] > splitPoint) {
|
||||||
const height = size[1];
|
// Target position: Center of top half = height * 0.25
|
||||||
const splitPoint = height / 2;
|
const targetY = height * 0.25;
|
||||||
|
const deltaY = pixel[1] - targetY;
|
||||||
|
|
||||||
// If in bottom half
|
const view = this.mapInstance.getView();
|
||||||
if (pixel[1] > splitPoint) {
|
const center = view.getCenter();
|
||||||
targetPixelY = height * 0.25; // Target: Center of top half
|
const resolution = view.getResolution();
|
||||||
needsPan = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 2. Desktop Sidebar Logic (Screen > 768px + Sidebar Open)
|
|
||||||
else if (this.args.isSidebarOpen) {
|
|
||||||
const sidebarWidthVar = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue('--sidebar-width')
|
|
||||||
.trim();
|
|
||||||
const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360;
|
|
||||||
|
|
||||||
// If under sidebar
|
// Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y)
|
||||||
if (pixel[0] < sidebarWidth) {
|
const deltaMapUnits = deltaY * resolution;
|
||||||
const visibleWidth = size[0] - sidebarWidth;
|
const newCenter = [center[0], center[1] - deltaMapUnits];
|
||||||
targetPixelX = sidebarWidth + visibleWidth / 2; // Target: Center of visible area
|
|
||||||
needsPan = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Header Logic (Any screen size)
|
|
||||||
// Check if the (potentially new) target Y is under the header
|
|
||||||
const headerHeight = 60;
|
|
||||||
const minTopDistance = headerHeight + 20; // 80px
|
|
||||||
|
|
||||||
if (targetPixelY < minTopDistance) {
|
|
||||||
targetPixelY = minTopDistance + 30; // Move it to ~110px, clear of header
|
|
||||||
needsPan = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsPan) {
|
|
||||||
const deltaPixelX = pixel[0] - targetPixelX;
|
|
||||||
const deltaPixelY = pixel[1] - targetPixelY;
|
|
||||||
|
|
||||||
// X: Camera moves same direction as we want the world to move? No.
|
|
||||||
// If we want pin to move RIGHT (pixel increases), Camera must move LEFT (X decreases).
|
|
||||||
// deltaPixelX = current - target. If current < target (want move right), delta is negative.
|
|
||||||
// center + negative = decrease. Correct.
|
|
||||||
const newCenterX = center[0] + deltaPixelX * resolution;
|
|
||||||
|
|
||||||
// Y: Camera moves opposite direction to world relative to pixel coords.
|
|
||||||
// Pixel Y increases DOWN. Map Y increases UP.
|
|
||||||
// If we want pin to move DOWN (pixel increases), Camera must move UP (Y increases).
|
|
||||||
// deltaPixelY = current - target. If current < target (want move down), delta is negative.
|
|
||||||
// center - negative = increase. Correct.
|
|
||||||
const newCenterY = center[1] - deltaPixelY * resolution;
|
|
||||||
|
|
||||||
view.animate({
|
view.animate({
|
||||||
center: [newCenterX, newCenterY],
|
center: newCenter,
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: (t) => t * (2 - t), // Ease-out
|
easing: (t) => t * (2 - t), // Ease-out
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export default class PlaceDetails extends Component {
|
|||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@name="bookmark"
|
@name="bookmark"
|
||||||
@color={{if this.isSaved "currentColor" "var(--link-color)"}}
|
@color={{if this.isSaved "currentColor" "#007bff"}}
|
||||||
/>
|
/>
|
||||||
{{if this.isSaved "Saved" "Save"}}
|
{{if this.isSaved "Saved" "Save"}}
|
||||||
</button>
|
</button>
|
||||||
@@ -307,7 +307,7 @@ export default class PlaceDetails extends Component {
|
|||||||
title="Edit"
|
title="Edit"
|
||||||
{{on "click" this.startEditing}}
|
{{on "click" this.startEditing}}
|
||||||
>
|
>
|
||||||
<Icon @name="edit" @color="var(--link-color)" />
|
<Icon @name="edit" @color="#007bff" />
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export default class PlacesSidebar extends Component {
|
|||||||
class="btn btn-outline create-place"
|
class="btn btn-outline create-place"
|
||||||
{{on "click" this.createNewPlace}}
|
{{on "click" this.createNewPlace}}
|
||||||
>
|
>
|
||||||
<Icon @name="plus" @size={{18}} @color="var(--link-color)" />
|
<Icon @name="plus" @size={{18}} @color="#007bff" />
|
||||||
Create new place
|
Create new place
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
128
app/components/settings-pane.gjs
Normal file
128
app/components/settings-pane.gjs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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 {
|
||||||
|
@service settings;
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateApi(event) {
|
||||||
|
this.settings.updateOverpassApi(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleKinetic(event) {
|
||||||
|
this.settings.updateMapKinetic(event.target.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar settings-pane">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h2>
|
||||||
|
<img src="/icons/icon-rounded.svg" alt="" width="32" height="32" />
|
||||||
|
Marco
|
||||||
|
</h2>
|
||||||
|
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
||||||
|
<Icon @name="x" @size={{20}} @color="#333" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-content">
|
||||||
|
<section class="settings-section">
|
||||||
|
<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={{unless this.settings.mapKinetic "selected"}}
|
||||||
|
>
|
||||||
|
Off
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<p>
|
||||||
|
<strong>Marco</strong>
|
||||||
|
(as in
|
||||||
|
<a
|
||||||
|
href="https://en.wikipedia.org/wiki/Marco_Polo"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>Marco Polo</a>) is an unhosted maps application that respects your
|
||||||
|
privacy and choices.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Connect your own
|
||||||
|
<a
|
||||||
|
href="https://remotestorage.io/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>remote storage</a>
|
||||||
|
to sync place bookmarks across apps and devices.
|
||||||
|
</p>
|
||||||
|
<ul class="link-list">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://gitea.kosmos.org/raucao/marco"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
Source Code
|
||||||
|
</a>
|
||||||
|
(<a
|
||||||
|
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>AGPL</a>)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://openstreetmap.org/copyright"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
Map Data © OpenStreetMap
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -101,23 +101,6 @@ 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) {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import Service, { service } from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import { getPlaceType } from '../utils/osm';
|
import { getPlaceType } from '../utils/osm';
|
||||||
import { humanizeOsmTag } from '../utils/format-text';
|
import { humanizeOsmTag } from '../utils/format-text';
|
||||||
|
|
||||||
export default class PhotonService extends Service {
|
export default class PhotonService extends Service {
|
||||||
@service settings;
|
baseUrl = 'https://photon.komoot.io/api/';
|
||||||
|
|
||||||
get baseUrl() {
|
|
||||||
return this.settings.photonApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
async search(query, lat, lon, limit = 10) {
|
async search(query, lat, lon, limit = 10) {
|
||||||
if (!query || query.length < 2) return [];
|
if (!query || query.length < 2) return [];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { tracked } from '@glimmer/tracking';
|
|||||||
export default class SettingsService extends Service {
|
export default class SettingsService extends Service {
|
||||||
@tracked overpassApi = 'https://overpass-api.de/api/interpreter';
|
@tracked overpassApi = 'https://overpass-api.de/api/interpreter';
|
||||||
@tracked mapKinetic = true;
|
@tracked mapKinetic = true;
|
||||||
@tracked photonApi = 'https://photon.komoot.io/api/';
|
|
||||||
|
|
||||||
overpassApis = [
|
overpassApis = [
|
||||||
{
|
{
|
||||||
@@ -25,13 +24,6 @@ export default class SettingsService extends Service {
|
|||||||
// },
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
photonApis = [
|
|
||||||
{
|
|
||||||
name: 'photon.komoot.io',
|
|
||||||
url: 'https://photon.komoot.io/api/',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
@@ -67,8 +59,4 @@ export default class SettingsService extends Service {
|
|||||||
this.mapKinetic = enabled;
|
this.mapKinetic = enabled;
|
||||||
localStorage.setItem('marco:map-kinetic', String(enabled));
|
localStorage.setItem('marco:map-kinetic', String(enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhotonApi(url) {
|
|
||||||
this.photonApi = url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Service, { service } from '@ember/service';
|
import 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,10 +7,8 @@ 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 = [];
|
||||||
@@ -368,82 +366,6 @@ 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;
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--default-list-color: #fc3;
|
--default-list-color: #fc3;
|
||||||
--hover-bg: #f8f9fa;
|
|
||||||
--sidebar-width: 350px;
|
|
||||||
--link-color: #2a7fff;
|
|
||||||
--link-color-visited: #6a4fbf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -188,7 +184,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: var(--link-color);
|
color: #007bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-danger {
|
.text-danger {
|
||||||
@@ -205,7 +201,7 @@ body {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: var(--sidebar-width);
|
width: 300px;
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 3100; /* Higher than Header (3000) */
|
z-index: 3100; /* Higher than Header (3000) */
|
||||||
box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
|
box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
|
||||||
@@ -214,12 +210,12 @@ body {
|
|||||||
overflow: hidden; /* Ensure flex children are contained */
|
overflow: hidden; /* Ensure flex children are contained */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.app-menu-pane {
|
.settings-pane.sidebar {
|
||||||
z-index: 3200; /* Higher than Places Sidebar (3100) */
|
z-index: 3200; /* Higher than Places Sidebar (3100) */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 768px) {
|
@media (width <= 768px) {
|
||||||
.sidebar.app-menu-pane {
|
.settings-pane.sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
@@ -255,111 +251,10 @@ body {
|
|||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-menu-pane .sidebar-content {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 -1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu button {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.8rem;
|
|
||||||
padding: 1rem;
|
|
||||||
padding-left: 1.4rem;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-family: inherit;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu button:hover {
|
|
||||||
background-color: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu .icon {
|
|
||||||
color: #666;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details {
|
|
||||||
margin: 0 -1rem; /* Top margin, negative side margins to span full width */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details summary {
|
|
||||||
list-style: none; /* Hide default triangle */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.8rem;
|
|
||||||
padding: 1rem;
|
|
||||||
padding-left: 1.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #333;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details summary::-webkit-details-marker {
|
|
||||||
display: none; /* Hide default triangle in WebKit */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details summary:hover {
|
|
||||||
background-color: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details summary .icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details summary::after {
|
|
||||||
content: '';
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='9 18 15 12 9 6'/%3E%3C/svg%3E");
|
|
||||||
background-size: 20px 20px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
margin-left: auto;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details[open] summary::after {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content details .details-content {
|
|
||||||
padding: 0 1.4rem 1rem;
|
|
||||||
animation: details-slide-down 0.2s ease-out;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes details-slide-down {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-form {
|
.edit-form {
|
||||||
margin: -1rem;
|
margin: -1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--hover-bg);
|
background: #f8f9fa;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
@@ -383,25 +278,12 @@ body {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
box-sizing: border-box; /* Ensure padding doesn't overflow width */
|
box-sizing: border-box; /* Ensure padding doesn't overflow width */
|
||||||
color: #333;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--link-color);
|
border-color: #007bff;
|
||||||
box-shadow: 0 0 0 2px rgb(42 127 255 / 10%);
|
box-shadow: 0 0 0 2px rgb(0 123 255 / 10%);
|
||||||
}
|
|
||||||
|
|
||||||
select.form-control {
|
|
||||||
appearance: none;
|
|
||||||
background-color: #fff;
|
|
||||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.75rem center;
|
|
||||||
background-size: 16px 16px;
|
|
||||||
padding-right: 2.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-actions {
|
.edit-actions {
|
||||||
@@ -412,27 +294,27 @@ select.form-control {
|
|||||||
|
|
||||||
.settings-section {
|
.settings-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
font-size: 0.95rem;
|
}
|
||||||
|
|
||||||
|
.settings-section h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-section .form-group {
|
.settings-section .form-group {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-section {
|
.settings-section p a {
|
||||||
margin-bottom: 2rem;
|
color: #007bff;
|
||||||
}
|
|
||||||
|
|
||||||
.about-section a {
|
|
||||||
color: var(--link-color);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-section a:visited {
|
.settings-section p a:hover {
|
||||||
color: var(--link-color-visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-section a:hover {
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +323,7 @@ select.form-control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--link-color);
|
background: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
@@ -470,7 +352,7 @@ select.form-control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.meta-info a {
|
.meta-info a {
|
||||||
color: var(--link-color);
|
color: #007bff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,34 +360,24 @@ select.form-control {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content table {
|
.link-list {
|
||||||
width: 100%;
|
list-style: none;
|
||||||
border-collapse: collapse;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content table th,
|
.link-list li {
|
||||||
.sidebar-content table td {
|
margin-bottom: 0.5rem;
|
||||||
padding: 0.5rem 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content table th {
|
.link-list a {
|
||||||
font-size: 0.75rem;
|
color: #007bff;
|
||||||
font-weight: bold;
|
text-decoration: none;
|
||||||
text-transform: uppercase;
|
font-size: 0.95rem;
|
||||||
color: #898989;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content table td {
|
.link-list a:hover {
|
||||||
border-bottom: 1px solid #f9f9f9;
|
text-decoration: underline;
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
abbr[title] {
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.places-list {
|
.places-list {
|
||||||
@@ -528,7 +400,7 @@ abbr[title] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.place-item:hover {
|
.place-item:hover {
|
||||||
background: var(--hover-bg);
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-name {
|
.place-name {
|
||||||
@@ -624,7 +496,7 @@ abbr[title] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-blue {
|
.btn-blue {
|
||||||
background: var(--link-color);
|
background: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -734,17 +606,6 @@ abbr[title] {
|
|||||||
|
|
||||||
/* Icons */
|
/* Icons */
|
||||||
|
|
||||||
.app-logo-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-logo-icon svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon {
|
span.icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -869,14 +730,15 @@ span.icon {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar is open (Desktop: Left var(--sidebar-width)) */
|
/* Sidebar is open (Desktop: Left 300px) */
|
||||||
|
|
||||||
/* We want to center in the remaining space (width - var(--sidebar-width)) */
|
/* We want to center in the remaining space (width - 300px) */
|
||||||
|
|
||||||
/* Center X = var(--sidebar-width) + (width - var(--sidebar-width)) / 2 = var(--sidebar-width)/2 + 50% */
|
/* Center X = 300 + (width - 300) / 2 = 300 + width/2 - 150 = width/2 + 150 */
|
||||||
|
|
||||||
|
/* So shift left by 150px from center */
|
||||||
.map-container.sidebar-open .map-crosshair {
|
.map-container.sidebar-open .map-crosshair {
|
||||||
left: calc(50% + var(--sidebar-width) / 2);
|
left: calc(50% + 150px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 768px) {
|
@media (width <= 768px) {
|
||||||
@@ -1084,7 +946,7 @@ button.create-place {
|
|||||||
|
|
||||||
.search-result-item:hover,
|
.search-result-item:hover,
|
||||||
.search-result-item:focus {
|
.search-result-item:focus {
|
||||||
background: var(--hover-bg);
|
background: #f5f5f5;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1149,7 +1011,7 @@ button.create-place {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.place-lists-manager .list-item:hover {
|
.place-lists-manager .list-item:hover {
|
||||||
background: var(--hover-bg);
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-lists-manager label {
|
.place-lists-manager label {
|
||||||
@@ -1164,7 +1026,7 @@ button.create-place {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.place-lists-manager input[type='checkbox'] {
|
.place-lists-manager input[type='checkbox'] {
|
||||||
accent-color: var(--link-color);
|
accent-color: #007bff;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Component from '@glimmer/component';
|
|||||||
import { pageTitle } from 'ember-page-title';
|
import { pageTitle } from 'ember-page-title';
|
||||||
import Map from '#components/map';
|
import Map from '#components/map';
|
||||||
import AppHeader from '#components/app-header';
|
import AppHeader from '#components/app-header';
|
||||||
import AppMenu from '#components/app-menu/index';
|
import SettingsPane from '#components/settings-pane';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
@@ -14,7 +14,7 @@ export default class ApplicationComponent extends Component {
|
|||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
@tracked isAppMenuOpen = false;
|
@tracked isSettingsOpen = false;
|
||||||
|
|
||||||
get isSidebarOpen() {
|
get isSidebarOpen() {
|
||||||
// We consider the sidebar "open" if we are in search or place routes.
|
// We consider the sidebar "open" if we are in search or place routes.
|
||||||
@@ -34,19 +34,19 @@ export default class ApplicationComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleAppMenu() {
|
toggleSettings() {
|
||||||
this.isAppMenuOpen = !this.isAppMenuOpen;
|
this.isSettingsOpen = !this.isSettingsOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
closeAppMenu() {
|
closeSettings() {
|
||||||
this.isAppMenuOpen = false;
|
this.isSettingsOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleOutsideClick() {
|
handleOutsideClick() {
|
||||||
if (this.isAppMenuOpen) {
|
if (this.isSettingsOpen) {
|
||||||
this.closeAppMenu();
|
this.closeSettings();
|
||||||
} else if (this.router.currentRouteName === 'search') {
|
} else if (this.router.currentRouteName === 'search') {
|
||||||
this.router.transitionTo('index');
|
this.router.transitionTo('index');
|
||||||
} else if (this.router.currentRouteName === 'place') {
|
} else if (this.router.currentRouteName === 'place') {
|
||||||
@@ -65,7 +65,7 @@ export default class ApplicationComponent extends Component {
|
|||||||
<template>
|
<template>
|
||||||
{{pageTitle "Marco"}}
|
{{pageTitle "Marco"}}
|
||||||
|
|
||||||
<AppHeader @onToggleMenu={{this.toggleAppMenu}} />
|
<AppHeader @onToggleMenu={{this.toggleSettings}} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="rs-widget-container"
|
id="rs-widget-container"
|
||||||
@@ -81,12 +81,12 @@ export default class ApplicationComponent extends Component {
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<Map
|
<Map
|
||||||
@isSidebarOpen={{or this.isSidebarOpen this.isAppMenuOpen}}
|
@isSidebarOpen={{or this.isSidebarOpen this.isSettingsOpen}}
|
||||||
@onOutsideClick={{this.handleOutsideClick}}
|
@onOutsideClick={{this.handleOutsideClick}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if this.isAppMenuOpen}}
|
{{#if this.isSettingsOpen}}
|
||||||
<AppMenu @onClose={{this.closeAppMenu}} />
|
<SettingsPane @onClose={{this.closeSettings}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
|||||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||||
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
||||||
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
||||||
import gift from 'feather-icons/dist/icons/gift.svg?raw';
|
|
||||||
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
||||||
import heart from 'feather-icons/dist/icons/heart.svg?raw';
|
|
||||||
import home from 'feather-icons/dist/icons/home.svg?raw';
|
import home from 'feather-icons/dist/icons/home.svg?raw';
|
||||||
import info from 'feather-icons/dist/icons/info.svg?raw';
|
|
||||||
import instagram from 'feather-icons/dist/icons/instagram.svg?raw';
|
import instagram from 'feather-icons/dist/icons/instagram.svg?raw';
|
||||||
import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
||||||
import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
|
import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
|
||||||
@@ -37,11 +34,8 @@ const ICONS = {
|
|||||||
clock,
|
clock,
|
||||||
edit,
|
edit,
|
||||||
facebook,
|
facebook,
|
||||||
gift,
|
|
||||||
globe,
|
globe,
|
||||||
heart,
|
|
||||||
home,
|
home,
|
||||||
info,
|
|
||||||
instagram,
|
instagram,
|
||||||
'log-in': logIn,
|
'log-in': logIn,
|
||||||
'log-out': logOut,
|
'log-out': logOut,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.16.0",
|
"version": "1.13.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build --outDir release/",
|
"build": "vite build --outDir release/",
|
||||||
"build:icons": "cp public/icons/icon-rounded.svg app/icons/icon-rounded.svg && for size in 32 48 144 180 192 512; do if [ \"$size\" -le 64 ]; then magick public/icons/icon.svg -define svg:remove-groups=map-grid -resize ${size}x${size} public/icons/icon-${size}.png; else rsvg-convert -w $size -h $size public/icons/icon.svg -o public/icons/icon-${size}.png; fi; done && rsvg-convert -w 512 -h 512 public/icons/icon.svg -o public/icons/icon-maskable.png",
|
"build:icons": "for size in 32 48 144 180 192 512; do if [ \"$size\" -le 64 ]; then magick public/icons/icon.svg -define svg:remove-groups=map-grid -resize ${size}x${size} public/icons/icon-${size}.png; else rsvg-convert -w $size -h $size public/icons/icon.svg -o public/icons/icon-${size}.png; fi; done && rsvg-convert -w 512 -h 512 public/icons/icon.svg -o public/icons/icon-maskable.png",
|
||||||
"format": "prettier . --cache --write",
|
"format": "prettier . --cache --write",
|
||||||
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
|
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
|
||||||
"lint:css": "stylelint \"**/*.css\"",
|
"lint:css": "stylelint \"**/*.css\"",
|
||||||
@@ -35,7 +35,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 lint && pnpm test",
|
"preversion": "pnpm test",
|
||||||
"version": "pnpm build && git add release/"
|
"version": "pnpm build && git add release/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -104,5 +104,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ember-concurrency": "^5.2.0",
|
"ember-concurrency": "^5.2.0",
|
||||||
"ember-lifeline": "^7.0.0"
|
"ember-lifeline": "^7.0.0"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
1
release/assets/main-DAo4Q0R2.css
Normal file
1
release/assets/main-DAo4Q0R2.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
2
release/assets/main-gjk9d6Ld.js
Normal file
2
release/assets/main-gjk9d6Ld.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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-gEUnNw-L.js"></script>
|
<script type="module" crossorigin src="/assets/main-gjk9d6Ld.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-BOfcjRke.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-DAo4Q0R2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -125,60 +125,4 @@ 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'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
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'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user