Draw outlines/areas for ways and relations on map

This commit is contained in:
2026-02-24 11:22:57 +04:00
parent 1926e2b20c
commit d827fe263b
5 changed files with 298 additions and 3 deletions

View File

@@ -13,6 +13,7 @@ import LayerGroup from 'ol/layer/Group.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import Feature from 'ol/Feature.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import Point from 'ol/geom/Point.js';
import Geolocation from 'ol/Geolocation.js';
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
@@ -27,6 +28,7 @@ export default class MapComponent extends Component {
mapInstance;
bookmarkSource;
selectedShapeSource;
searchOverlay;
searchOverlayElement;
selectedPinOverlay;
@@ -40,6 +42,22 @@ export default class MapComponent extends Component {
const openfreemap = new LayerGroup();
// Create a vector source and layer for the selected shape (outline)
this.selectedShapeSource = new VectorSource();
const selectedShapeLayer = new VectorLayer({
source: this.selectedShapeSource,
style: new Style({
stroke: new Stroke({
color: '#3388ff',
width: 4,
}),
fill: new Fill({
color: 'rgba(51, 136, 255, 0.1)',
}),
}),
zIndex: 5, // Below bookmarks (10) but above tiles
});
// Create a vector source and layer for bookmarks
this.bookmarkSource = new VectorSource();
const bookmarkLayer = new VectorLayer({
@@ -99,7 +117,7 @@ export default class MapComponent extends Component {
this.mapInstance = new Map({
target: element,
layers: [openfreemap, bookmarkLayer],
layers: [openfreemap, selectedShapeLayer, bookmarkLayer],
view: view,
controls: defaultControls({
zoom: true,
@@ -426,6 +444,11 @@ export default class MapComponent extends Component {
if (!this.selectedPinOverlay || !this.selectedPinElement) return;
// Clear any previous shape
if (this.selectedShapeSource) {
this.selectedShapeSource.clear();
}
if (selected && selected.lat && selected.lon) {
const coords = fromLonLat([selected.lon, selected.lat]);
this.selectedPinOverlay.setPosition(coords);
@@ -436,6 +459,18 @@ export default class MapComponent extends Component {
void this.selectedPinElement.offsetWidth;
this.selectedPinElement.classList.add('active');
// Draw GeoJSON shape if available
if (selected.geojson && this.selectedShapeSource) {
try {
const feature = new GeoJSON().readFeature(selected.geojson, {
featureProjection: 'EPSG:3857',
});
this.selectedShapeSource.addFeature(feature);
} catch (e) {
console.warn('Failed to render selected place shape:', e);
}
}
if (selected.bbox) {
this.zoomToBbox(selected.bbox);
} else {

View File

@@ -48,7 +48,28 @@ export default class PlaceRoute extends Route {
}
}
afterModel(model) {
async afterModel(model) {
// If the model comes from a search result (e.g. Photon), it might lack detailed geometry.
// We want to ensure we have the full OSM object (with polygon/linestring) for display.
if (
model &&
model.osmId &&
model.osmType &&
model.osmType !== 'node' &&
!model.geojson
) {
// Only fetch if it's NOT a node (nodes don't have interesting geometry anyway, just a point)
// Although fetching nodes again ensures we have the latest tags too.
console.debug('Model missing geometry, fetching full OSM details...');
const fullDetails = await this.loadOsmPlace(model.osmId, model.osmType);
if (fullDetails) {
// Update the model in-place with the fuller details
Object.assign(model, fullDetails);
console.debug('Enriched model with full OSM details', model);
}
}
// Notify the Map UI to show the pin
if (model) {
this.mapUi.selectPlace(model);

View File

@@ -218,6 +218,7 @@ out center;
let lat = displayElement.lat;
let lon = displayElement.lon;
let bbox = null;
let geojson = null;
// If it's a way, calculate center from nodes
if (targetType === 'way' && mainElement.nodes) {
@@ -250,6 +251,25 @@ out center;
minLon: Math.min(...lons),
maxLon: Math.max(...lons),
};
// Construct GeoJSON
if (coords.length > 1) {
const first = coords[0];
const last = coords[coords.length - 1];
const isClosed = first[0] === last[0] && first[1] === last[1];
if (isClosed) {
geojson = {
type: 'Polygon',
coordinates: [coords],
};
} else {
geojson = {
type: 'LineString',
coordinates: coords,
};
}
}
}
} else if (targetType === 'relation' && mainElement.members) {
// Find all nodes that are part of this relation (directly or via ways)
@@ -261,6 +281,8 @@ out center;
}
});
const segments = [];
mainElement.members.forEach((member) => {
if (member.type === 'node') {
const node = nodeMap.get(member.ref);
@@ -270,10 +292,17 @@ out center;
(el) => el.type === 'way' && el.id === member.ref
);
if (way && way.nodes) {
const wayCoords = [];
way.nodes.forEach((nodeId) => {
const node = nodeMap.get(nodeId);
if (node) allNodes.push(node);
if (node) {
allNodes.push(node);
wayCoords.push([node.lon, node.lat]);
}
});
if (wayCoords.length > 1) {
segments.push(wayCoords);
}
}
}
});
@@ -297,6 +326,13 @@ out center;
maxLon: Math.max(...lons),
};
}
if (segments.length > 0) {
geojson = {
type: 'MultiLineString',
coordinates: segments,
};
}
}
const tags = displayElement.tags || {};
@@ -307,6 +343,7 @@ out center;
lat,
lon,
bbox,
geojson,
url: tags.website,
osmId: String(displayElement.id),
osmType: displayElement.type,