Zoom to fit ways and relations into map view
This commit is contained in:
@@ -436,7 +436,11 @@ export default class MapComponent extends Component {
|
||||
void this.selectedPinElement.offsetWidth;
|
||||
this.selectedPinElement.classList.add('active');
|
||||
|
||||
this.handlePinVisibility(coords);
|
||||
if (selected.bbox) {
|
||||
this.zoomToBbox(selected.bbox);
|
||||
} else {
|
||||
this.handlePinVisibility(coords);
|
||||
}
|
||||
} else {
|
||||
this.selectedPinElement.classList.remove('active');
|
||||
// Hide it effectively by moving it away or just relying on display:none in CSS
|
||||
@@ -444,6 +448,55 @@ export default class MapComponent extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
zoomToBbox(bbox) {
|
||||
if (!this.mapInstance || !bbox) return;
|
||||
|
||||
const view = this.mapInstance.getView();
|
||||
const size = this.mapInstance.getSize();
|
||||
|
||||
// Convert bbox to extent: [minx, miny, maxx, maxy]
|
||||
const min = fromLonLat([bbox.minLon, bbox.minLat]);
|
||||
const max = fromLonLat([bbox.maxLon, bbox.maxLat]);
|
||||
const extent = [...min, ...max];
|
||||
|
||||
// Default padding for full screen: 15% on all sides (70% visible)
|
||||
let padding = [
|
||||
size[1] * 0.15, // Top
|
||||
size[0] * 0.15, // Right
|
||||
size[1] * 0.15, // Bottom
|
||||
size[0] * 0.15, // Left
|
||||
];
|
||||
|
||||
// Mobile: Bottom sheet covers 50% of the screen height
|
||||
if (size[0] <= 768) {
|
||||
// We want the geometry to be centered in the top 50% of the screen.
|
||||
// Top padding: 15% of the VISIBLE height (size[1] * 0.5)
|
||||
const visibleHeight = size[1] * 0.5;
|
||||
const topPadding = visibleHeight * 0.15;
|
||||
const bottomPadding = (size[1] * 0.5) + (visibleHeight * 0.15); // Sheet + padding
|
||||
|
||||
padding[0] = topPadding;
|
||||
padding[2] = bottomPadding;
|
||||
}
|
||||
// Desktop: Sidebar covers left side (approx 400px)
|
||||
else if (this.args.isSidebarOpen) {
|
||||
const sidebarWidth = 400;
|
||||
const visibleWidth = size[0] - sidebarWidth;
|
||||
|
||||
// Left padding: Sidebar + 15% of visible width
|
||||
padding[3] = sidebarWidth + (visibleWidth * 0.15);
|
||||
// Right padding: 15% of visible width
|
||||
padding[1] = visibleWidth * 0.15;
|
||||
}
|
||||
|
||||
view.fit(extent, {
|
||||
padding: padding,
|
||||
duration: 1000,
|
||||
easing: (t) => t * (2 - t),
|
||||
maxZoom: 19,
|
||||
});
|
||||
}
|
||||
|
||||
handlePinVisibility(coords) {
|
||||
if (!this.mapInstance) return;
|
||||
|
||||
|
||||
@@ -171,8 +171,12 @@ out center;
|
||||
|
||||
if (!mainElement) return null;
|
||||
|
||||
// Use a separate variable for the element we want to display (tags, id, specific coords)
|
||||
// vs the element we use for geometry calculation (bbox).
|
||||
let displayElement = mainElement;
|
||||
|
||||
// If it's a boundary relation, try to find the label or admin_centre node
|
||||
// and use that as the main element (better coordinates and tags).
|
||||
// and use that as the display element (better coordinates and tags).
|
||||
if (targetType === 'relation' && mainElement.members) {
|
||||
const labelMember = mainElement.members.find(
|
||||
(m) => m.role === 'label' && m.type === 'node'
|
||||
@@ -189,13 +193,14 @@ out center;
|
||||
String(el.id) === String(targetMember.ref) && el.type === 'node'
|
||||
);
|
||||
if (targetNode) {
|
||||
mainElement = targetNode;
|
||||
displayElement = targetNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lat = mainElement.lat;
|
||||
let lon = mainElement.lon;
|
||||
let lat = displayElement.lat;
|
||||
let lon = displayElement.lon;
|
||||
let bbox = null;
|
||||
|
||||
// If it's a way, calculate center from nodes
|
||||
if (targetType === 'way' && mainElement.nodes) {
|
||||
@@ -211,11 +216,23 @@ out center;
|
||||
.filter(Boolean);
|
||||
|
||||
if (coords.length > 0) {
|
||||
// Simple average center
|
||||
const sumLat = coords.reduce((sum, c) => sum + c[1], 0);
|
||||
const sumLon = coords.reduce((sum, c) => sum + c[0], 0);
|
||||
lat = sumLat / coords.length;
|
||||
lon = sumLon / coords.length;
|
||||
// Only override lat/lon if we haven't switched to a specific display node
|
||||
if (displayElement === mainElement) {
|
||||
const sumLat = coords.reduce((sum, c) => sum + c[1], 0);
|
||||
const sumLon = coords.reduce((sum, c) => sum + c[0], 0);
|
||||
lat = sumLat / coords.length;
|
||||
lon = sumLon / coords.length;
|
||||
}
|
||||
|
||||
// Calculate BBox
|
||||
const lats = coords.map((c) => c[1]);
|
||||
const lons = coords.map((c) => c[0]);
|
||||
bbox = {
|
||||
minLat: Math.min(...lats),
|
||||
maxLat: Math.max(...lats),
|
||||
minLon: Math.min(...lons),
|
||||
maxLon: Math.max(...lons),
|
||||
};
|
||||
}
|
||||
} else if (targetType === 'relation' && mainElement.members) {
|
||||
// Find all nodes that are part of this relation (directly or via ways)
|
||||
@@ -245,23 +262,37 @@ out center;
|
||||
});
|
||||
|
||||
if (allNodes.length > 0) {
|
||||
const sumLat = allNodes.reduce((sum, n) => sum + n.lat, 0);
|
||||
const sumLon = allNodes.reduce((sum, n) => sum + n.lon, 0);
|
||||
lat = sumLat / allNodes.length;
|
||||
lon = sumLon / allNodes.length;
|
||||
// Only override lat/lon if we haven't switched to a specific display node
|
||||
if (displayElement === mainElement) {
|
||||
const sumLat = allNodes.reduce((sum, n) => sum + n.lat, 0);
|
||||
const sumLon = allNodes.reduce((sum, n) => sum + n.lon, 0);
|
||||
lat = sumLat / allNodes.length;
|
||||
lon = sumLon / allNodes.length;
|
||||
}
|
||||
|
||||
// Calculate BBox
|
||||
const lats = allNodes.map((n) => n.lat);
|
||||
const lons = allNodes.map((n) => n.lon);
|
||||
bbox = {
|
||||
minLat: Math.min(...lats),
|
||||
maxLat: Math.max(...lats),
|
||||
minLon: Math.min(...lons),
|
||||
maxLon: Math.max(...lons),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tags = mainElement.tags || {};
|
||||
const tags = displayElement.tags || {};
|
||||
const type = getPlaceType(tags) || 'Point of Interest';
|
||||
|
||||
return {
|
||||
title: getLocalizedName(tags),
|
||||
lat,
|
||||
lon,
|
||||
bbox,
|
||||
url: tags.website,
|
||||
osmId: String(mainElement.id),
|
||||
osmType: mainElement.type,
|
||||
osmId: String(displayElement.id),
|
||||
osmType: displayElement.type,
|
||||
osmTags: tags,
|
||||
description: tags.description,
|
||||
source: 'osm',
|
||||
|
||||
@@ -98,6 +98,41 @@ module('Unit | Service | osm', function (hooks) {
|
||||
assert.strictEqual(result.osmType, 'node');
|
||||
});
|
||||
|
||||
test('normalizeOsmApiData calculates bbox for relations', function (assert) {
|
||||
let service = this.owner.lookup('service:osm');
|
||||
const elements = [
|
||||
{
|
||||
id: 789,
|
||||
type: 'relation',
|
||||
members: [
|
||||
{ type: 'node', ref: 1, role: 'label' },
|
||||
{ type: 'node', ref: 2, role: 'border' },
|
||||
{ type: 'node', ref: 3, role: 'border' },
|
||||
],
|
||||
tags: { name: 'Test Relation' },
|
||||
},
|
||||
{ id: 1, type: 'node', lat: 10, lon: 10, tags: { name: 'Label' } },
|
||||
{ id: 2, type: 'node', lat: 0, lon: 0 },
|
||||
{ id: 3, type: 'node', lat: 20, lon: 20 },
|
||||
];
|
||||
|
||||
const result = service.normalizeOsmApiData(elements, 789, 'relation');
|
||||
|
||||
// Should prioritize admin centre for ID/Title/Center
|
||||
assert.strictEqual(result.title, 'Label');
|
||||
assert.strictEqual(result.lat, 10);
|
||||
assert.strictEqual(result.lon, 10);
|
||||
assert.strictEqual(result.osmId, '1');
|
||||
assert.strictEqual(result.osmType, 'node');
|
||||
|
||||
// BUT should calculate BBox from ALL members (0,0 to 20,20)
|
||||
assert.ok(result.bbox, 'BBox should be present');
|
||||
assert.strictEqual(result.bbox.minLat, 0);
|
||||
assert.strictEqual(result.bbox.minLon, 0);
|
||||
assert.strictEqual(result.bbox.maxLat, 20);
|
||||
assert.strictEqual(result.bbox.maxLon, 20);
|
||||
});
|
||||
|
||||
test('normalizeOsmApiData calculates centroid for relations with member ways', function (assert) {
|
||||
let service = this.owner.lookup('service:osm');
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user