tethys.backend/resources/js/Components/Map/SearchMap.vue
Arno Kaimbacher 4229001572
Some checks failed
build.yaml / Enhance Map Zoom Control and Improve Map Page Layout (push) Failing after 0s
Enhance Map Zoom Control and Improve Map Page Layout
- Refactored zoom control component for better accessibility and styling.
- Added hover effects and improved button states for zoom in/out buttons.
- Updated map page layout with enhanced dataset card design and responsive styles.
- Introduced empty state for no datasets found and improved results header.
- Added icons for dataset cards and improved author display.
2025-11-05 13:15:23 +01:00

435 lines
11 KiB
Vue

<script setup lang="ts">
import { onMounted, onUnmounted, ref, Ref, defineProps, computed } from 'vue';
import SectionMain from '@/Components/SectionMain.vue';
import { Map } from 'leaflet/src/map/index';
import { Rectangle } from 'leaflet';
import { canvas } from 'leaflet/src/layer/vector/Canvas';
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 { Attribution } from 'leaflet/src/control/Control.Attribution';
import DrawControlComponent from '@/Components/Map/draw.component.vue';
import ZoomControlComponent from '@/Components/Map/zoom.component.vue';
import { MapService } from '@/Stores/map.service';
import { LayerGroup } from 'leaflet/src/layer/LayerGroup';
import { OpensearchDocument } from '@/Dataset';
Map.include({
getRenderer: function (layer) {
var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
if (!renderer) {
renderer = this._renderer = this._createRenderer();
}
if (!this.hasLayer(renderer)) {
this.addLayer(renderer);
}
return renderer;
},
_getPaneRenderer: function (name) {
if (name === 'overlayPane' || name === undefined) {
return false;
}
var renderer = this._paneRenderers[name];
if (renderer === undefined) {
renderer = this._createRenderer({ pane: name });
this._paneRenderers[name] = renderer;
}
return renderer;
},
_createRenderer: function (options) {
return (this.options.preferCanvas && canvas(options)) || svg(options);
},
});
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
const OPENSEARCH_HOST = 'https://catalog.geosphere.at';
let map: Map;
const props = defineProps({
checkable: Boolean,
datasets: {
type: Array<OpensearchDocument>,
default: () => [],
},
mapId: {
type: String,
default: 'map',
},
mapOptions: {
type: Object,
default: () => ({
center: [48.208174, 16.373819],
zoom: 3,
zoomControl: false,
attributionControl: false,
}),
},
});
const items = computed({
get() {
return props.datasets;
},
set(value) {
props.datasets.length = 0;
props.datasets.push(...value);
},
});
const fitBounds: LatLngBoundsExpression = [
[46.4318173285, 9.47996951665],
[49.0390742051, 16.9796667823],
];
const drawControl: Ref<DrawControlComponent | null> = ref(null);
const southWest = ref(null);
const northEast = ref(null);
const mapService = MapService();
const isLoading = ref(false);
const filterLayerGroup = new LayerGroup();
onMounted(() => {
initMap();
});
onUnmounted(() => {
map.off('zoomend zoomlevelschange');
});
const initMap = async () => {
map = new Map('map', props.mapOptions);
mapService.setMap(props.mapId, map);
map.scrollWheelZoom.disable();
map.fitBounds(fitBounds);
drawControl.value?.toggleDraw();
map.addLayer(filterLayerGroup);
const attributionControl = new Attribution().addTo(map);
attributionControl.setPrefix(false);
let osmGgray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
format: 'image/png',
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
layers: 'OSM-WMS',
});
let layerOptions = {
label: DEFAULT_BASE_LAYER_NAME,
visible: true,
layer: osmGgray,
};
layerOptions.layer.addTo(map);
map.on('Draw.Event.CREATED', handleDrawEventCreated);
};
const handleDrawEventCreated = async (event) => {
isLoading.value = true;
filterLayerGroup.clearLayers();
items.value = [];
let layer = event.layer;
let bounds = layer.getBounds();
try {
let response = await axios({
method: 'POST',
url: OPENSEARCH_HOST + '/tethys-records/_search',
headers: { 'Content-Type': 'application/json' },
data: {
size: 1000,
query: {
bool: {
must: {
match_all: {},
},
filter: {
geo_shape: {
geo_location: {
shape: {
type: 'envelope',
coordinates: [
[bounds.getSouthWest().lng, bounds.getNorthEast().lat],
[bounds.getNorthEast().lng, bounds.getSouthWest().lat],
],
},
relation: 'within',
},
},
},
},
},
},
});
response.data.hits.hits.forEach((hit) => {
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],
];
let rect = new Rectangle(bbox, {
color: '#65DC21',
weight: 2,
fillColor: '#65DC21',
fillOpacity: 0.2,
className: 'animated-rectangle',
});
filterLayerGroup.addLayer(rect);
items.value.push(hit._source);
});
} catch (error) {
console.error(error);
} finally {
isLoading.value = false;
}
};
</script>
<template>
<SectionMain>
<div class="map-container-wrapper">
<!-- Loading Overlay -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<p class="loading-text">Searching datasets...</p>
</div>
<!-- Map Instructions Banner -->
<div class="map-instructions">
<svg class="instruction-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" />
</svg>
<p class="instruction-text">
<strong>Tip:</strong> Use the drawing tool to select an area on the map and discover datasets
</p>
</div>
<div id="map" class="map-container">
<ZoomControlComponent ref="zoomControl" :mapId="mapId"></ZoomControlComponent>
<DrawControlComponent ref="drawControl" :preserve="false" :mapId="mapId" :southWest="southWest" :northEast="northEast">
</DrawControlComponent>
</div>
</div>
</SectionMain>
</template>
<style scoped>
.map-container-wrapper {
position: relative;
border-radius: 1rem;
overflow: hidden;
background: white;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.dark .map-container-wrapper {
background: #1f2937;
}
/* 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;
}
}
</style>