11 Commits

Author SHA1 Message Date
55aecbd699 Apply same button-press effect to both header buttons 2026-02-10 18:51:26 +04:00
ccaa56b78f Remove remaining default tap highlights on mobiles 2026-02-10 18:44:41 +04:00
d30375707a Prevent map search when zoomed out too much
It's usually an accidental click, and if not, the search radius/pulse
wouldn't be clearly visible.
2026-02-10 18:33:44 +04:00
53300b92f5 Re-add zoom controls 2026-02-10 17:47:03 +04:00
c37f794eea Auto-locate user on first app launch
closes #17
2026-02-10 17:18:59 +04:00
4bc92bb7cc Run tests before versioning 2026-02-08 17:01:56 +04:00
9f48d7b264 1.11.2 2026-02-08 17:01:01 +04:00
bbd3bf47c6 Merge pull request 'Fix back button behavior' (#14) from bugfix/back_button into master
Reviewed-on: #14
2026-02-08 13:00:07 +00:00
59e3d91071 Fix back button behavior
fixes #12
2026-02-08 16:59:53 +04:00
348b721876 1.11.1 2026-01-27 15:05:08 +07:00
3d982a6a7c More kinetic panning optimizations 2026-01-27 15:04:25 +07:00
13 changed files with 192 additions and 31 deletions

View File

@@ -24,7 +24,7 @@ export default class AppHeaderComponent extends Component {
<header class="app-header"> <header class="app-header">
<div class="header-left"> <div class="header-left">
<button <button
class="icon-btn" class="menu-btn btn-press"
type="button" type="button"
aria-label="Menu" aria-label="Menu"
{{on "click" @onToggleMenu}} {{on "click" @onToggleMenu}}
@@ -36,7 +36,7 @@ export default class AppHeaderComponent extends Component {
<div class="header-right"> <div class="header-right">
<div class="user-menu-container"> <div class="user-menu-container">
<button <button
class="user-btn" class="user-btn btn-press"
type="button" type="button"
aria-label="User Menu" aria-label="User Menu"
{{on "click" this.toggleUserMenu}} {{on "click" this.toggleUserMenu}}

View File

@@ -68,6 +68,7 @@ export default class MapComponent extends Component {
// Default view settings // Default view settings
let center = [14.21683569, 27.060114248]; let center = [14.21683569, 27.060114248];
let zoom = 2.661; let zoom = 2.661;
let restoredFromStorage = false;
// Try to restore from localStorage // Try to restore from localStorage
try { try {
@@ -82,6 +83,7 @@ export default class MapComponent extends Component {
) { ) {
center = parsed.center; center = parsed.center;
zoom = parsed.zoom; zoom = parsed.zoom;
restoredFromStorage = true;
} }
} }
} catch (e) { } catch (e) {
@@ -99,7 +101,7 @@ export default class MapComponent extends Component {
layers: [openfreemap, bookmarkLayer], layers: [openfreemap, bookmarkLayer],
view: view, view: view,
controls: defaultControls({ controls: defaultControls({
zoom: false, zoom: true,
rotate: true, rotate: true,
attribution: true, attribution: true,
}), }),
@@ -243,6 +245,7 @@ export default class MapComponent extends Component {
const coordinates = geolocation.getPosition(); const coordinates = geolocation.getPosition();
const accuracyGeometry = geolocation.getAccuracyGeometry(); const accuracyGeometry = geolocation.getAccuracyGeometry();
const accuracy = geolocation.getAccuracy(); const accuracy = geolocation.getAccuracy();
console.debug('Geolocation change:', { coordinates, accuracy });
if (!coordinates) return; if (!coordinates) return;
@@ -307,7 +310,8 @@ export default class MapComponent extends Component {
this.mapInstance.getView().animate(viewOptions); this.mapInstance.getView().animate(viewOptions);
}; };
locateBtn.addEventListener('click', () => { const startLocating = () => {
console.debug('Getting current geolocation...')
// 1. Clear any previous session // 1. Clear any previous session
stopLocating(); stopLocating();
@@ -331,7 +335,9 @@ export default class MapComponent extends Component {
locateTimeout = setTimeout(() => { locateTimeout = setTimeout(() => {
stopLocating(); stopLocating();
}, 10000); }, 10000);
}); };
locateBtn.addEventListener('click', startLocating);
const locateControl = new Control({ const locateControl = new Control({
element: locateElement, element: locateElement,
@@ -340,6 +346,11 @@ export default class MapComponent extends Component {
this.mapInstance.addLayer(geolocationLayer); this.mapInstance.addLayer(geolocationLayer);
this.mapInstance.addControl(locateControl); this.mapInstance.addControl(locateControl);
// Auto-locate on first visit (if not restored from storage and on home page)
if (!restoredFromStorage && this.router.currentRouteName === 'index') {
startLocating();
}
this.mapInstance.on('singleclick', this.handleMapClick); this.mapInstance.on('singleclick', this.handleMapClick);
// Load places when map moves // Load places when map moves
@@ -374,6 +385,16 @@ export default class MapComponent extends Component {
? new Kinetic(-0.005, 0.05, 100) ? new Kinetic(-0.005, 0.05, 100)
: false; : false;
// Fix for "sticky" touches on mobile:
// If we're on mobile (width <= 768) AND using kinetic,
// we increase the minimum velocity required to trigger kinetic panning.
// This prevents slow drags from being interpreted as a "throw"
if (this.settings.mapKinetic && window.innerWidth <= 768) {
// Default minVelocity is 0.05. We bump it up significantly.
// This means the user has to really "flick" the map to get inertia.
kinetic.minVelocity_ = 0.25;
}
this.mapInstance.addInteraction( this.mapInstance.addInteraction(
new DragPan({ new DragPan({
kinetic: kinetic, kinetic: kinetic,
@@ -723,6 +744,13 @@ export default class MapComponent extends Component {
return; return;
} }
// Require Zoom >= 17 for generic map searches
// This prevents accidental searches when interacting with the map at a high level
const currentZoom = this.mapInstance.getView().getZoom();
if (currentZoom < 16) {
return;
}
const coords = toLonLat(event.coordinate); const coords = toLonLat(event.coordinate);
const [lon, lat] = coords; const [lon, lat] = coords;

View File

@@ -56,6 +56,8 @@ export default class PlaceRoute extends Route {
deactivate() { deactivate() {
// Clear the pin when leaving the route // Clear the pin when leaving the route
this.mapUi.clearSelection(); this.mapUi.clearSelection();
// Reset the "return to search" flag so it doesn't persist to subsequent navigations
this.mapUi.returnToSearch = false;
} }
async loadOsmPlace(id, type = null) { async loadOsmPlace(id, type = null) {

View File

@@ -6,6 +6,7 @@ export default class MapUiService extends Service {
@tracked isSearching = false; @tracked isSearching = false;
@tracked isCreating = false; @tracked isCreating = false;
@tracked creationCoordinates = null; @tracked creationCoordinates = null;
@tracked returnToSearch = false;
selectPlace(place) { selectPlace(place) {
this.selectedPlace = place; this.selectedPlace = place;

View File

@@ -28,13 +28,8 @@ export default class SettingsService extends Service {
const savedKinetic = localStorage.getItem('marco:map-kinetic'); const savedKinetic = localStorage.getItem('marco:map-kinetic');
if (savedKinetic !== null) { if (savedKinetic !== null) {
this.mapKinetic = savedKinetic === 'true'; 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;
}
} }
// Default is true (initialized in class field)
} }
updateOverpassApi(url) { updateOverpassApi(url) {

View File

@@ -5,6 +5,11 @@ body {
height: 100%; height: 100%;
overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */ overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
button {
-webkit-tap-highlight-color: transparent;
} }
body { body {
@@ -69,7 +74,15 @@ body {
pointer-events: auto; /* Re-enable clicks for buttons */ pointer-events: auto; /* Re-enable clicks for buttons */
} }
.icon-btn { .btn-press {
transition: transform 0.1s;
}
.btn-press:active {
transform: scale(0.95);
}
.menu-btn {
background: white; background: white;
border: none; border: none;
border-radius: 50%; border-radius: 50%;
@@ -80,11 +93,6 @@ body {
justify-content: center; justify-content: center;
box-shadow: 0 2px 5px rgb(0 0 0 / 20%); box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
cursor: pointer; cursor: pointer;
transition: transform 0.1s;
}
.icon-btn:active {
transform: scale(0.95);
} }
.user-btn { .user-btn {
@@ -539,22 +547,40 @@ body {
} }
} }
/* Locate Control */ /* Zoom Control - Moved to bottom right above attribution */
.ol-zoom {
top: auto !important;
left: auto !important;
bottom: 2.5em;
right: 0.5em;
}
.ol-touch .ol-zoom {
bottom: 3.5em;
}
/* Locate Control - Above Zoom */
.ol-control.ol-locate { .ol-control.ol-locate {
inset: auto 0.5em 2.5em auto; top: auto !important;
left: auto !important;
bottom: 6.5em;
right: 0.5em;
} }
.ol-touch .ol-control.ol-locate { .ol-touch .ol-control.ol-locate {
inset: auto 0.5em 3.5em auto; bottom: 8.5em;
} }
/* Rotate Control */ /* Rotate Control - Above Locate */
.ol-rotate { .ol-rotate {
inset: auto 0.5em 5em auto; top: auto !important;
left: auto !important;
bottom: 9em;
right: 0.5em;
} }
.ol-touch .ol-rotate { .ol-touch .ol-rotate {
inset: auto 0.5em 6em auto; bottom: 11.5em;
} }
span.icon { span.icon {

View File

@@ -77,11 +77,11 @@ export default class PlaceTemplate extends Component {
navigateBack(place) { navigateBack(place) {
// The sidebar calls this with null when "Back" is clicked. // The sidebar calls this with null when "Back" is clicked.
if (place === null) { if (place === null) {
// If we have history, go back (preserves search state) // If we came from search results, go back in history
if (window.history.length > 1) { if (this.mapUi.returnToSearch) {
window.history.back(); window.history.back();
} else { } else {
// Fallback if opened directly // Otherwise just close the sidebar (return to map index)
this.router.transitionTo('index'); this.router.transitionTo('index');
} }
} else { } else {

View File

@@ -5,10 +5,12 @@ import { action } from '@ember/object';
export default class SearchTemplate extends Component { export default class SearchTemplate extends Component {
@service router; @service router;
@service mapUi;
@action @action
selectPlace(place) { selectPlace(place) {
if (place) { if (place) {
this.mapUi.returnToSearch = true;
this.router.transitionTo('place', place); this.router.transitionTo('place', place);
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.11.0", "version": "1.11.2",
"private": true, "private": true,
"description": "Unhosted maps app", "description": "Unhosted maps app",
"repository": { "repository": {
@@ -34,6 +34,7 @@
"lint:js:fix": "eslint . --fix", "lint:js:fix": "eslint . --fix",
"start": "vite", "start": "vite",
"test": "vite build --mode development && testem ci --port 0", "test": "vite build --mode development && testem ci --port 0",
"preversion": "pnpm test",
"version": "pnpm build && git add release/" "version": "pnpm build && git add release/"
}, },
"devDependencies": { "devDependencies": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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-y8e9Z0x2.js"></script> <script type="module" crossorigin src="/assets/main-CYFdUlXN.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>

View File

@@ -0,0 +1,106 @@
import { module, test } from 'qunit';
import { visit, currentURL, click, settled } from '@ember/test-helpers';
import { setupApplicationTest } from 'marco/tests/helpers';
import Service from '@ember/service';
import sinon from 'sinon';
class MockOsmService extends Service {
async getNearbyPois() {
return [
{
osmId: '123',
lat: 1,
lon: 1,
osmTags: { name: 'Test Place', amenity: 'cafe' },
osmType: 'node',
},
];
}
async getPoiById() {
return {
osmId: '123',
lat: 1,
lon: 1,
osmTags: { name: 'Test Place', amenity: 'cafe' },
osmType: 'node',
};
}
}
class MockStorageService extends Service {
savedPlaces = [];
findPlaceById() {
return null;
}
loadPlacesInBounds() {
return [];
}
get placesInView() {
return [];
}
rs = {
on: () => {},
};
}
module('Acceptance | navigation', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
this.owner.register('service:osm', MockOsmService);
this.owner.register('service:storage', MockStorageService);
});
test('navigating from search results to place and back uses history', async function (assert) {
const mapUi = this.owner.lookup('service:map-ui');
const backStub = sinon.stub(window.history, 'back');
try {
await visit('/search?lat=1&lon=1');
assert.strictEqual(currentURL(), '/search?lat=1&lon=1');
await click('.place-item');
assert.ok(currentURL().includes('/place/'), 'Navigated to place');
assert.true(mapUi.returnToSearch, 'Flag returnToSearch is set');
// Click the back button in the sidebar
await click('.back-btn');
assert.true(backStub.calledOnce, 'window.history.back() was called');
} finally {
backStub.restore();
}
});
test('closing the sidebar resets the returnToSearch flag', async function (assert) {
const mapUi = this.owner.lookup('service:map-ui');
await visit('/search?lat=1&lon=1');
await click('.place-item'); // Sets returnToSearch = true
assert.true(mapUi.returnToSearch, 'Flag is set upon entering place');
// Click the Close (X) button
await click('.close-btn');
await settled();
assert.strictEqual(currentURL(), '/', 'Returned to index');
assert.false(mapUi.returnToSearch, 'Flag is reset after closing sidebar');
});
test('navigating directly to place and back closes sidebar', async function (assert) {
const backStub = sinon.stub(window.history, 'back');
try {
await visit('/place/osm:node:123');
assert.ok(currentURL().includes('/place/'), 'Visited place directly');
await click('.back-btn');
await settled();
assert.strictEqual(currentURL(), '/', 'Returned to index/map');
assert.true(backStub.notCalled, 'window.history.back() was NOT called');
} finally {
backStub.restore();
}
});
});