diff --git a/app/components/map.gjs b/app/components/map.gjs index 0812a42..5090375 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -15,7 +15,6 @@ import Point from 'ol/geom/Point.js'; import Geolocation from 'ol/Geolocation.js'; import { Style, Circle, Fill, Stroke } from 'ol/style.js'; import { apply } from 'ol-mapbox-style'; -import { getDistance } from '../utils/geo'; export default class MapComponent extends Component { @service osm; @@ -29,6 +28,8 @@ export default class MapComponent extends Component { searchOverlayElement; selectedPinOverlay; selectedPinElement; + crosshairElement; // New crosshair + crosshairOverlay; // New crosshair overlay setupMap = modifier((element) => { if (this.mapInstance) return; @@ -140,6 +141,29 @@ export default class MapComponent extends Component { }); this.mapInstance.addOverlay(this.selectedPinOverlay); + // Crosshair Overlay (for Creating New Place) + this.crosshairElement = document.createElement('div'); + this.crosshairElement.className = 'map-crosshair'; + // Use an SVG or simple CSS cross + this.crosshairElement.innerHTML = ` + + + + + `; + // We attach it to the map control container OR keep it as an overlay centered on map center? + // Actually, a fixed center overlay is trickier in OpenLayers because Overlays move with the map. + // If we want it FIXED in the center of the VIEWPORT, it should be a Control or just an absolute HTML element on top of the map div. + // Adding it as a Control is cleaner. + + // HOWEVER, the request says "cross hair drawn on the map... which should be removed when saving". + // A fixed element in the center of the screen is best for "choose location by dragging map". + // So let's append it to the map container directly via Glimmer template or JS. + + // We'll append it to the map target element (this.element is the target). + element.appendChild(this.crosshairElement); + + // Geolocation Pulse Overlay this.locationOverlayElement = document.createElement('div'); this.locationOverlayElement.className = 'search-pulse blue'; @@ -522,9 +546,128 @@ export default class MapComponent extends Component { } }); + // Sync the creation mode (Crosshair) + syncCreationMode = modifier(() => { + if (!this.crosshairElement || !this.mapInstance) return; + + if (this.mapUi.isCreating) { + this.crosshairElement.classList.add('visible'); + + // If we have initial coordinates from the route (e.g. reload or link), + // we need to pan the map so those coordinates are UNDER the crosshair. + const coords = this.mapUi.creationCoordinates; + if (coords && coords.lat && coords.lon) { + // We only animate if the map center isn't already "roughly" correct + // But actually, updateCreationCoordinates is called by handleMapMove too. + // We need to distinguish "initial set" vs "drag update". + // The Service doesn't distinguish, but if we are just entering mode, + // we can check if the current map center aligns. + + // Better approach: + // We calculate where the map center *should* be to put the target coords + // under the crosshair. + const targetCoords = fromLonLat([coords.lon, coords.lat]); + this.animateToCrosshair(targetCoords); + } + } else { + this.crosshairElement.classList.remove('visible'); + } + }); + + animateToCrosshair(targetCoords) { + if (!this.mapInstance || !this.crosshairElement) return; + + // 1. Get current visual position of the crosshair + const mapRect = this.mapInstance.getTargetElement().getBoundingClientRect(); + const crosshairRect = this.crosshairElement.getBoundingClientRect(); + + const crosshairPixelX = + crosshairRect.left + crosshairRect.width / 2 - mapRect.left; + const crosshairPixelY = + crosshairRect.top + crosshairRect.height / 2 - mapRect.top; + + // 2. Get the center pixel of the map viewport + const size = this.mapInstance.getSize(); + const mapCenterX = size[0] / 2; + const mapCenterY = size[1] / 2; + + // 3. Calculate the offset (how far the crosshair is from the geometric center) + const offsetX = crosshairPixelX - mapCenterX; + const offsetY = crosshairPixelY - mapCenterY; + + // 4. Calculate the new map center + // We want 'targetCoords' to be at [crosshairPixelX, crosshairPixelY]. + // If we center the map on 'targetCoords', it will be at [mapCenterX, mapCenterY]. + // So we need to shift the map center by the OPPOSITE of the offset. + // Wait. + // If crosshair is to the right (+X), we need to move the camera LEFT (-X) to bring the point there? + // Let's think in map units. + const view = this.mapInstance.getView(); + const resolution = view.getResolution(); + + const offsetMapUnitsX = offsetX * resolution; + const offsetMapUnitsY = -offsetY * resolution; // Y is inverted in pixel vs map coords usually? + // In Web Mercator: Y increases North (Up). + // In Pixels: Y increases South (Down). + // So +PixelY (Down) = -MapY (South). Correct. + + // If crosshair is at +100px (Right), we want the target to be there. + // If we center on target, it is at 0px. + // To make it appear at +100px, we must shift the camera center by -100px (Left). + // So CenterX_new = TargetX - offsetMapUnitsX. + + const targetX = targetCoords[0]; + const targetY = targetCoords[1]; + + const newCenterX = targetX - offsetMapUnitsX; + const newCenterY = targetY - offsetMapUnitsY; + + // Only animate if the difference is significant (avoid micro-jitters/loops) + const currentCenter = view.getCenter(); + const dist = Math.sqrt( + Math.pow(currentCenter[0] - newCenterX, 2) + + Math.pow(currentCenter[1] - newCenterY, 2) + ); + + // 1 meter is approx 1 unit in Mercator near equator, varies by latitude. + // Resolution at zoom 18 is approx 0.6m/pixel. + // Let's use a small pixel threshold. + if (dist > resolution * 5) { + view.animate({ + center: [newCenterX, newCenterY], + duration: 800, + easing: (t) => t * (2 - t), // Ease-out + }); + } + } + handleMapMove = async () => { if (!this.mapInstance) return; + // If in creation mode, update the coordinates in the service AND the URL + if (this.mapUi.isCreating) { + // Calculate coordinates under the crosshair element + // We need the pixel position of the crosshair relative to the map viewport + // The crosshair is positioned via CSS, so we can use getBoundingClientRect + const mapRect = this.mapInstance.getTargetElement().getBoundingClientRect(); + const crosshairRect = this.crosshairElement.getBoundingClientRect(); + + const centerX = crosshairRect.left + crosshairRect.width / 2 - mapRect.left; + const centerY = crosshairRect.top + crosshairRect.height / 2 - mapRect.top; + + const coordinate = this.mapInstance.getCoordinateFromPixel([centerX, centerY]); + const center = toLonLat(coordinate); + + const lat = parseFloat(center[1].toFixed(6)); + const lon = parseFloat(center[0].toFixed(6)); + + this.mapUi.updateCreationCoordinates(lat, lon); + + // Update URL without triggering a full refresh + // We use replaceWith to avoid cluttering history + this.router.replaceWith('place.new', { queryParams: { lat, lon } }); + } + const size = this.mapInstance.getSize(); const extent = this.mapInstance.getView().calculateExtent(size); const [minLon, minLat] = toLonLat([extent[0], extent[1]]); @@ -640,11 +783,12 @@ export default class MapComponent extends Component { } diff --git a/app/components/place-details.gjs b/app/components/place-details.gjs index 1b7f279..c6b99b0 100644 --- a/app/components/place-details.gjs +++ b/app/components/place-details.gjs @@ -3,19 +3,13 @@ import { fn } from '@ember/helper'; import { on } from '@ember/modifier'; import { humanizeOsmTag } from '../utils/format-text'; import Icon from '../components/icon'; +import PlaceEditForm from './place-edit-form'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; export default class PlaceDetails extends Component { @tracked isEditing = false; - @tracked editTitle = ''; - @tracked editDescription = ''; - - constructor() { - super(...arguments); - this.resetEditFields(); - } get place() { return this.args.place || {}; @@ -34,16 +28,9 @@ export default class PlaceDetails extends Component { ); } - @action - resetEditFields() { - this.editTitle = this.name; - this.editDescription = this.place.description || ''; - } - @action startEditing() { if (!this.place.createdAt) return; // Only allow editing saved places - this.resetEditFields(); this.isEditing = true; } @@ -53,28 +40,16 @@ export default class PlaceDetails extends Component { } @action - async saveChanges(event) { - event.preventDefault(); + async saveChanges(changes) { if (this.args.onSave) { await this.args.onSave({ ...this.place, - title: this.editTitle, - description: this.editDescription, + ...changes, }); } this.isEditing = false; } - @action - updateTitle(e) { - this.editTitle = e.target.value; - } - - @action - updateDescription(e) { - this.editDescription = e.target.value; - } - get type() { const rawType = this.tags.amenity || @@ -171,32 +146,11 @@ export default class PlaceDetails extends Component {