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;
|
void this.selectedPinElement.offsetWidth;
|
||||||
this.selectedPinElement.classList.add('active');
|
this.selectedPinElement.classList.add('active');
|
||||||
|
|
||||||
this.handlePinVisibility(coords);
|
if (selected.bbox) {
|
||||||
|
this.zoomToBbox(selected.bbox);
|
||||||
|
} else {
|
||||||
|
this.handlePinVisibility(coords);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.selectedPinElement.classList.remove('active');
|
this.selectedPinElement.classList.remove('active');
|
||||||
// Hide it effectively by moving it away or just relying on display:none in CSS
|
// 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) {
|
handlePinVisibility(coords) {
|
||||||
if (!this.mapInstance) return;
|
if (!this.mapInstance) return;
|
||||||
|
|
||||||
|
|||||||
@@ -171,8 +171,12 @@ out center;
|
|||||||
|
|
||||||
if (!mainElement) return null;
|
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
|
// 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) {
|
if (targetType === 'relation' && mainElement.members) {
|
||||||
const labelMember = mainElement.members.find(
|
const labelMember = mainElement.members.find(
|
||||||
(m) => m.role === 'label' && m.type === 'node'
|
(m) => m.role === 'label' && m.type === 'node'
|
||||||
@@ -189,13 +193,14 @@ out center;
|
|||||||
String(el.id) === String(targetMember.ref) && el.type === 'node'
|
String(el.id) === String(targetMember.ref) && el.type === 'node'
|
||||||
);
|
);
|
||||||
if (targetNode) {
|
if (targetNode) {
|
||||||
mainElement = targetNode;
|
displayElement = targetNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lat = mainElement.lat;
|
let lat = displayElement.lat;
|
||||||
let lon = mainElement.lon;
|
let lon = displayElement.lon;
|
||||||
|
let bbox = null;
|
||||||
|
|
||||||
// If it's a way, calculate center from nodes
|
// If it's a way, calculate center from nodes
|
||||||
if (targetType === 'way' && mainElement.nodes) {
|
if (targetType === 'way' && mainElement.nodes) {
|
||||||
@@ -211,11 +216,23 @@ out center;
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (coords.length > 0) {
|
if (coords.length > 0) {
|
||||||
// Simple average center
|
// Only override lat/lon if we haven't switched to a specific display node
|
||||||
const sumLat = coords.reduce((sum, c) => sum + c[1], 0);
|
if (displayElement === mainElement) {
|
||||||
const sumLon = coords.reduce((sum, c) => sum + c[0], 0);
|
const sumLat = coords.reduce((sum, c) => sum + c[1], 0);
|
||||||
lat = sumLat / coords.length;
|
const sumLon = coords.reduce((sum, c) => sum + c[0], 0);
|
||||||
lon = sumLon / coords.length;
|
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) {
|
} else if (targetType === 'relation' && mainElement.members) {
|
||||||
// Find all nodes that are part of this relation (directly or via ways)
|
// Find all nodes that are part of this relation (directly or via ways)
|
||||||
@@ -245,23 +262,37 @@ out center;
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (allNodes.length > 0) {
|
if (allNodes.length > 0) {
|
||||||
const sumLat = allNodes.reduce((sum, n) => sum + n.lat, 0);
|
// Only override lat/lon if we haven't switched to a specific display node
|
||||||
const sumLon = allNodes.reduce((sum, n) => sum + n.lon, 0);
|
if (displayElement === mainElement) {
|
||||||
lat = sumLat / allNodes.length;
|
const sumLat = allNodes.reduce((sum, n) => sum + n.lat, 0);
|
||||||
lon = sumLon / allNodes.length;
|
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';
|
const type = getPlaceType(tags) || 'Point of Interest';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: getLocalizedName(tags),
|
title: getLocalizedName(tags),
|
||||||
lat,
|
lat,
|
||||||
lon,
|
lon,
|
||||||
|
bbox,
|
||||||
url: tags.website,
|
url: tags.website,
|
||||||
osmId: String(mainElement.id),
|
osmId: String(displayElement.id),
|
||||||
osmType: mainElement.type,
|
osmType: displayElement.type,
|
||||||
osmTags: tags,
|
osmTags: tags,
|
||||||
description: tags.description,
|
description: tags.description,
|
||||||
source: 'osm',
|
source: 'osm',
|
||||||
|
|||||||
@@ -98,6 +98,41 @@ module('Unit | Service | osm', function (hooks) {
|
|||||||
assert.strictEqual(result.osmType, 'node');
|
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) {
|
test('normalizeOsmApiData calculates centroid for relations with member ways', function (assert) {
|
||||||
let service = this.owner.lookup('service:osm');
|
let service = this.owner.lookup('service:osm');
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user