diff --git a/components.d.ts b/components.d.ts index 3834bf1..001762d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -11,3 +11,21 @@ declare module '@vue/runtime-core' { NInput: (typeof import('naive-ui'))['NInput']; } } + +// types/leaflet-src-dom-DomEvent.d.ts +declare module 'leaflet/src/dom/DomEvent' { + export type DomEventHandler = (e?: any) => void; + + // Attach event listeners. `obj` can be any DOM node or object with event handling. + export function on(obj: any, types: string, fn: DomEventHandler, context?: any): void; + + // Detach event listeners. + export function off(obj: any, types: string, fn?: DomEventHandler, context?: any): void; + + // Prevent default on native events + export function preventDefault(ev?: Event | undefined): void; + + // Optional: other helpers you might need later + export function stopPropagation(ev?: Event | undefined): void; + export function stop(ev?: Event | undefined): void; +} \ No newline at end of file diff --git a/resources/js/Components/Map/MapOptions.ts b/resources/js/Components/Map/MapOptions.ts index 8a2de5a..ede8d87 100644 --- a/resources/js/Components/Map/MapOptions.ts +++ b/resources/js/Components/Map/MapOptions.ts @@ -1,7 +1,11 @@ -import type { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds'; -import type { LatLngExpression } from 'leaflet/src/geo/LatLng'; -import type { Layer } from 'leaflet/src/layer/Layer'; -import type { CRS } from 'leaflet/src/geo/crs/CRS'; +// import type { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds'; +// import type { LatLngExpression } from 'leaflet/src/geo/LatLng'; +// import type { Layer } from 'leaflet/src/layer/Layer'; +// import type { CRS } from 'leaflet/src/geo/crs/CRS'; +import type { LatLngBoundsExpression } from 'leaflet'; +import type { LatLngExpression } from 'leaflet'; +import type { Layer } from 'leaflet'; +import type { CRS } from 'leaflet'; export interface MapOptions { preferCanvas?: boolean | undefined; diff --git a/resources/js/Components/Map/SearchMap.vue b/resources/js/Components/Map/SearchMap.vue index aac036e..7c02269 100644 --- a/resources/js/Components/Map/SearchMap.vue +++ b/resources/js/Components/Map/SearchMap.vue @@ -8,7 +8,6 @@ import { svg } from 'leaflet/src/layer/vector/SVG'; import axios from 'axios'; import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds'; import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS'; -// import { TileLayer } from 'leaflet/src/layer/tile/TileLayer'; import { Attribution } from 'leaflet/src/control/Control.Attribution'; import DrawControlComponent from '@/Components/Map/draw.component.vue'; import ZoomControlComponent from '@/Components/Map/zoom.component.vue'; @@ -17,14 +16,7 @@ import { LayerGroup } from 'leaflet/src/layer/LayerGroup'; import { OpensearchDocument } from '@/Dataset'; Map.include({ - // @namespace Map; @method getRenderer(layer: Path): Renderer - // Returns the instance of `Renderer` that should be used to render the given - // `Path`. It will ensure that the `renderer` options of the map and paths - // are respected, and that the renderers do exist on the map. getRenderer: function (layer) { - // @namespace Path; @option renderer: Renderer - // Use this specific instance of `Renderer` for this path. Takes - // precedence over the map's [default renderer](#map-renderer). var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer; if (!renderer) { @@ -51,21 +43,18 @@ Map.include({ }, _createRenderer: function (options) { - // @namespace Map; @option preferCanvas: Boolean = false - // Whether `Path`s should be rendered on a `Canvas` renderer. - // By default, all `Path`s are rendered in a `SVG` renderer. return (this.options.preferCanvas && canvas(options)) || svg(options); }, }); + const DEFAULT_BASE_LAYER_NAME = 'BaseLayer'; const DEFAULT_BASE_LAYER_ATTRIBUTION = '© OpenStreetMap contributors'; const OPENSEARCH_HOST = 'https://catalog.geosphere.at'; -// const OPENSEARCH_HOST = `${process.env.OPENSEARCH_HOST}`; -// const OPENSEARCH_HOST = `http://${process.env.OPENSEARCH_PUBLIC_HOST}`; + let map: Map; const props = defineProps({ - dheckable: Boolean, + checkable: Boolean, datasets: { type: Array, default: () => [], @@ -89,10 +78,7 @@ const items = computed({ get() { return props.datasets; }, - // setter set(value) { - // Note: we are using destructuring assignment syntax here. - props.datasets.length = 0; props.datasets.push(...value); }, @@ -103,15 +89,13 @@ const fitBounds: LatLngBoundsExpression = [ [49.0390742051, 16.9796667823], ]; -// const mapId = 'map'; const drawControl: Ref = ref(null); const southWest = ref(null); const northEast = ref(null); const mapService = MapService(); +const isLoading = ref(false); const filterLayerGroup = new LayerGroup(); -// Replace with your actual data -// const datasets: Ref = ref([]); onMounted(() => { initMap(); @@ -122,7 +106,6 @@ onUnmounted(() => { }); const initMap = async () => { - // init leaflet map map = new Map('map', props.mapOptions); mapService.setMap(props.mapId, map); map.scrollWheelZoom.disable(); @@ -140,11 +123,6 @@ const initMap = async () => { layers: 'OSM-WMS', }); - // let baseAt = new TileLayer('https://{s}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png', { - // subdomains: ['maps', 'maps1', 'maps2', 'maps3', 'maps4'], - // attribution: DEFAULT_BASE_LAYER_ATTRIBUTION, - // }); - let layerOptions = { label: DEFAULT_BASE_LAYER_NAME, visible: true, @@ -153,62 +131,15 @@ const initMap = async () => { layerOptions.layer.addTo(map); map.on('Draw.Event.CREATED', handleDrawEventCreated); - - // // const query = { - // // query: { - // // term: { - // // id: "103" - // // } - // // } - // // }; - // // to do : call extra method: - // const query = { - // // q: 'id:103' - // // q: 'author:"Iglseder, Christoph" OR title:"Datensatz"', - // // q: 'author:"Iglseder"', - // q: '*', - // _source: 'author,bbox_xmin,bbox_xmax,bbox_ymin,bbox_ymax,abstract,title', - // size: 1000 - // // qf:"title^3 author^2 subject^1", - // } - // try { - // let response = await axios({ - // method: 'GET', - // url: OPEN_SEARCH_HOST + '/tethys-records/_search', - // headers: { 'Content-Type': 'application/json' }, - // params: query - // }); - // // Loop through the hits in the response - // response.data.hits.hits.forEach(hit => { - // // Get the geo_location attribute - // // var geo_location = hit._source.geo_location; - // let xMin = hit._source.bbox_xmin; - // let xMax = hit._source.bbox_xmax; - // let yMin = hit._source.bbox_ymin; - // let yMax = hit._source.bbox_ymax; - // var bbox: LatLngBoundsExpression = [[yMin, xMin], [yMax, xMax]]; - // // Parse the WKT string to get the bounding box coordinates - // // var bbox = wktToBbox(geo_location); - - // // // Add the bounding box to the map as a rectangle - // new Rectangle(bbox, { color: "#ff7800", weight: 1 }).addTo(map); - // // console.log(hit._source); - // }); - // } catch (error) { - // console.error(error); - // } }; const handleDrawEventCreated = async (event) => { + isLoading.value = true; filterLayerGroup.clearLayers(); items.value = []; let layer = event.layer; let bounds = layer.getBounds(); - // coverage.x_min = bounds.getSouthWest().lng; - // coverage.y_min = bounds.getSouthWest().lat; - // coverage.x_max = bounds.getNorthEast().lng; - // coverage.y_max = bounds.getNorthEast().lat; try { let response = await axios({ @@ -225,7 +156,6 @@ const handleDrawEventCreated = async (event) => { filter: { geo_shape: { geo_location: { - // replace 'location' with your geo-point field name shape: { type: 'envelope', coordinates: [ @@ -237,16 +167,12 @@ const handleDrawEventCreated = async (event) => { }, }, }, - // _source: 'author,bbox_xmin,bbox_xmax,bbox_ymin,bbox_ymax,abstract,title', - // "size": 1000 }, }, }, }); - // Loop through the hits in the response + response.data.hits.hits.forEach((hit) => { - // Get the geo_location attribute - // var geo_location = hit._source.geo_location; let xMin = hit._source.bbox_xmin; let xMax = hit._source.bbox_xmax; let yMin = hit._source.bbox_ymin; @@ -255,46 +181,255 @@ const handleDrawEventCreated = async (event) => { [yMin, xMin], [yMax, xMax], ]; - // Parse the WKT string to get the bounding box coordinates - // var bbox = wktToBbox(geo_location); - // // Add the bounding box to the map as a rectangle - let rect = new Rectangle(bbox, { color: '#ff7800', weight: 1 }); + let rect = new Rectangle(bbox, { + color: '#65DC21', + weight: 2, + fillColor: '#65DC21', + fillOpacity: 0.2, + className: 'animated-rectangle', + }); filterLayerGroup.addLayer(rect); - // add to result list items.value.push(hit._source); }); } catch (error) { console.error(error); + } finally { + isLoading.value = false; } }; - - \ No newline at end of file + +/* Map Instructions Banner */ +.map-instructions { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.5rem; + background: linear-gradient(135deg, rgba(101, 220, 33, 0.1) 0%, rgba(53, 124, 6, 0.1) 100%); + border-bottom: 2px solid #e5e7eb; +} + +.dark .map-instructions { + background: linear-gradient(135deg, rgba(101, 220, 33, 0.2) 0%, rgba(53, 124, 6, 0.2) 100%); + border-bottom-color: #374151; +} + +.instruction-icon { + width: 1.5rem; + height: 1.5rem; + color: #65dc21; + flex-shrink: 0; +} + +.instruction-text { + font-size: 0.875rem; + color: #4b5563; + margin: 0; +} + +.dark .instruction-text { + color: #d1d5db; +} + +.instruction-text strong { + color: #65dc21; + font-weight: 600; +} + +/* Loading Overlay */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.95); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.dark .loading-overlay { + background: rgba(31, 41, 55, 0.95); +} + +.loading-spinner { + width: 3rem; + height: 3rem; + border: 4px solid #e5e7eb; + border-top-color: #65dc21; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.dark .loading-spinner { + border-color: #374151; + border-top-color: #65dc21; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-text { + margin-top: 1rem; + font-size: 0.875rem; + font-weight: 600; + color: #65dc21; +} + +/* Map Container */ +.map-container { + position: relative; + height: 600px; + width: 100%; + background: #f9fafb; +} + +.dark .map-container { + background: #111827; +} + +/* Leaflet Overrides */ +:deep(.leaflet-container) { + height: 600px; + width: 100%; + background: transparent; + font-family: inherit; +} + +:deep(.leaflet-container .leaflet-pane) { + z-index: 30 !important; +} + +/* Enhanced Rectangle Styling */ +:deep(.animated-rectangle) { + animation: pulseRectangle 2s ease-in-out infinite; +} + +@keyframes pulseRectangle { + 0%, + 100% { + opacity: 0.6; + } + 50% { + opacity: 1; + } +} + +/* Control Enhancements */ +:deep(.leaflet-control) { + border-radius: 0.5rem; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border: none; +} + +:deep(.leaflet-bar a) { + border-radius: 0.5rem; + transition: all 0.2s ease; +} + +:deep(.leaflet-bar a:hover) { + background: #65dc21; + color: white; +} + +:deep(.leaflet-draw-toolbar a) { + background: white; + transition: all 0.2s ease; +} + +.dark :deep(.leaflet-draw-toolbar a) { + background: #374151; + color: #d1d5db; +} + +:deep(.leaflet-draw-toolbar a:hover) { + background: #65dc21; +} + +/* Popup Enhancements */ +:deep(.leaflet-popup-content-wrapper) { + border-radius: 0.75rem; + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +:deep(.leaflet-popup-tip) { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .map-container { + height: 400px; + } + + .map-instructions { + padding: 0.75rem 1rem; + } + + .instruction-text { + font-size: 0.8125rem; + } +} + +@media (max-width: 640px) { + .map-container { + height: 350px; + } +} + diff --git a/resources/js/Components/Map/draw.component.vue b/resources/js/Components/Map/draw.component.vue index 1bfb9af..2638213 100644 --- a/resources/js/Components/Map/draw.component.vue +++ b/resources/js/Components/Map/draw.component.vue @@ -1,14 +1,29 @@ @@ -17,16 +32,14 @@ import { Component, Vue, Prop } from 'vue-facing-decorator'; import BaseIcon from '@/Components/BaseIcon.vue'; -import { mdiDrawPen } from '@mdi/js'; +import { mdiVectorRectangle, mdiClose } from '@mdi/js'; import { MapService } from '@/Stores/map.service'; -import { Map } from 'leaflet/src/map/index'; -// import { LayerGroup } from 'leaflet/src/layer/LayerGroup'; -// import { LatLngBounds, Rectangle } from 'leaflet'; +import { Map } from 'leaflet'; import { on, off, preventDefault } from 'leaflet/src/dom/DomEvent'; -import { Rectangle } from 'leaflet/src/layer/vector/Rectangle'; -import { LatLngBounds } from 'leaflet/src/geo/LatLngBounds'; + +import { Rectangle } from 'leaflet'; +import { LatLngBounds } from 'leaflet'; import { LatLng } from 'leaflet'; -import { LeafletMouseEvent } from 'leaflet'; @Component({ name: 'draw-control', @@ -34,19 +47,19 @@ import { LeafletMouseEvent } from 'leaflet'; BaseIcon, }, }) -export default class DrawControlComponent extends Vue { +export class DrawControlComponent extends Vue { public TYPE = 'rectangle'; - mdiDrawPen = mdiDrawPen; - // private featuresLayer; + mdiVectorRectangle = mdiVectorRectangle; + mdiClose = mdiClose; options = { shapeOptions: { stroke: true, - color: '#22C55E', + color: '#65DC21', weight: 4, opacity: 0.5, fill: true, - fillColor: '#22C55E', //same as color by default + fillColor: '#65DC21', fillOpacity: 0.2, clickable: true, }, @@ -56,7 +69,6 @@ export default class DrawControlComponent extends Vue { }; @Prop() public mapId: string; - // @Prop() public map: Map; @Prop public southWest: LatLng; @Prop public northEast: LatLng; @Prop({ @@ -65,13 +77,17 @@ export default class DrawControlComponent extends Vue { public preserve: boolean; mapService = MapService(); - public _enabled: boolean; + private _enabled: boolean; private _map: Map; private _isDrawing: boolean = false; private _startLatLng: LatLng; private _mapDraggable: boolean; private _shape: Rectangle | undefined; + get enabled() { + return this._enabled; + } + enable() { if (this._enabled) { return this; @@ -93,49 +109,35 @@ export default class DrawControlComponent extends Vue { return this; } - enabled() { - return !!this._enabled; - } - - // @Ref('inputDraw') private _inputDraw: HTMLElement; + // enabled() { + // return !!this._enabled; + // } private addHooks() { - // L.Draw.Feature.prototype.addHooks.call(this); this._map = this.mapService.getMap(this.mapId); if (this._map) { this._mapDraggable = this._map.dragging.enabled(); if (this._mapDraggable) { this._map.dragging.disable(); } - //TODO refactor: move cursor to styles - // this._map.domElement.style.cursor = 'crosshair'; - this._map._container.style.cursor = 'crosshair'; - // this._tooltip.updateContent({text: this._initialLabelText}); + this._map.getContainer().style.cursor = 'crosshair'; this._map .on('mousedown', this._onMouseDown, this) .on('mousemove', this._onMouseMove, this) .on('touchstart', this._onMouseDown, this) - .on('touchmove', this._onMouseMove, this); - // we should prevent default, otherwise default behavior (scrolling) will fire, - // and that will cause document.touchend to fire and will stop the drawing - // (circle, rectangle) in touch mode. - // (update): we have to send passive now to prevent scroll, because by default it is {passive: true} now, which means, - // handler can't event.preventDefault - // check the news https://developers.google.com/web/updates/2016/06/passive-event-listeners - // document.addEventListener('touchstart', preventDefault, { passive: false }); + .on('touchmove', this._onMouseMove, this); + } } private removeHooks() { - // L.Draw.Feature.prototype.removeHooks.call(this); if (this._map) { if (this._mapDraggable) { this._map.dragging.enable(); } - //TODO refactor: move cursor to styles - this._map._container.style.cursor = ''; + this._map.getContainer().style.cursor = ''; this._map .off('mousedown', this._onMouseDown, this) @@ -146,46 +148,36 @@ export default class DrawControlComponent extends Vue { off(document, 'mouseup', this._onMouseUp, this); off(document, 'touchend', this._onMouseUp, this); - // document.removeEventListener('touchstart', preventDefault); - - // If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return if (this._shape && this.preserve == false) { this._map.removeLayer(this._shape); - // delete this._shape; this._shape = undefined; } } this._isDrawing = false; } - private _onMouseDown(e: LeafletMouseEvent) { + // private _onMouseDown(e: LeafletMouseEvent) { + private _onMouseDown(e: any) { this._isDrawing = true; this._startLatLng = e.latlng; - // DomEvent.on(document, 'mouseup', this._onMouseUp, this) - // .on(document, 'touchend', this._onMouseUp, this) - // .preventDefault(e.originalEvent); on(document, 'mouseup', this._onMouseUp, this); on(document, 'touchend', this._onMouseUp, this); preventDefault(e.originalEvent); } - - private _onMouseMove(e: LeafletMouseEvent) { + // private _onMouseMove(e: LeafletMouseEvent) { + private _onMouseMove(e: any) { var latlng = e.latlng; - // this._tooltip.updatePosition(latlng); if (this._isDrawing) { - // this._tooltip.updateContent(this._getTooltipText()); this._drawShape(latlng); } } - private _onMouseUp() { if (this._shape) { this._fireCreatedEvent(this._shape); } - // this.removeHooks(); this.disable(); if (this.options.repeatMode) { this.enable(); @@ -194,14 +186,12 @@ export default class DrawControlComponent extends Vue { private _fireCreatedEvent(shape: Rectangle) { var rectangle = new Rectangle(shape.getBounds(), this.options.shapeOptions); - // L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle); this._map.fire('Draw.Event.CREATED', { layer: rectangle, type: this.TYPE }); } public removeShape() { if (this._shape) { this._map.removeLayer(this._shape); - // delete this._shape; this._shape = undefined; } } @@ -210,7 +200,6 @@ export default class DrawControlComponent extends Vue { if (!this._shape) { const bounds = new LatLngBounds(southWest, northEast); this._shape = new Rectangle(bounds, this.options.shapeOptions); - // this._map.addLayer(this._shape); this._map = this.mapService.getMap(this.mapId); this._shape.addTo(this._map); } else { @@ -218,12 +207,10 @@ export default class DrawControlComponent extends Vue { } } - // from Draw Rectangle private _drawShape(latlng: LatLng) { if (!this._shape) { const bounds = new LatLngBounds(this._startLatLng, latlng); this._shape = new Rectangle(bounds, this.options.shapeOptions); - // this._map.addLayer(this._shape); this._shape.addTo(this._map); } else { this._shape.setBounds(new LatLngBounds(this._startLatLng, latlng)); @@ -237,44 +224,336 @@ export default class DrawControlComponent extends Vue { this.enable(); } } - - // private enable() { - // //if (this.map.mapTool) this.map.mapTool.on('editable:drawing:start', this.disable.bind(this)); - // // dom.addClass(this.map.container, 'measure-enabled'); - // //this.fireAndForward('showmeasure'); - // this._startMarker(this.southWest, this.options); - // } - - // private disable() { - // //if (this.map.mapTool) this.map.mapTool.off('editable:drawing:start', this.disable.bind(this)); - // // dom.removeClass(this.map.container, 'measure-enabled'); - // // this.featuresLayer.clearLayers(); - // // //this.fireAndForward('hidemeasure'); - // // if (this._drawingEditor) { - // // this._drawingEditor.cancelDrawing(); - // // } - // } } +export default DrawControlComponent; - + + \ No newline at end of file diff --git a/resources/js/Components/Map/map.component.vue b/resources/js/Components/Map/map.component.vue index d6d6f29..353162b 100644 --- a/resources/js/Components/Map/map.component.vue +++ b/resources/js/Components/Map/map.component.vue @@ -1,21 +1,72 @@