Enhance Map Zoom Control and Improve Map Page Layout
Some checks failed
build.yaml / Enhance Map Zoom Control and Improve Map Page Layout (push) Failing after 0s
Some checks failed
build.yaml / Enhance Map Zoom Control and Improve Map Page Layout (push) Failing after 0s
- 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.
This commit is contained in:
parent
88e37bfee8
commit
4229001572
7 changed files with 1520 additions and 452 deletions
|
|
@ -1,21 +1,72 @@
|
|||
<template>
|
||||
<div style="position: relative">
|
||||
<!-- <Map className="h-36" :center="state.center" :zoom="state.zoom"> // map component content </Map> -->
|
||||
<div :id="mapId" class="rounded">
|
||||
<div class="dark:bg-slate-900 bg-slate flex flex-col">
|
||||
<div class="relative w-full">
|
||||
<!-- Map Container -->
|
||||
<div
|
||||
:id="mapId"
|
||||
class="relative h-[600px] w-full bg-gray-50 dark:bg-gray-900 rounded-xl overflow-hidden"
|
||||
>
|
||||
<div class="relative w-full h-full">
|
||||
<ZoomControlComponent ref="zoom" :mapId="mapId" />
|
||||
<DrawControlComponent ref="draw" :mapId="mapId" :southWest="southWest" :northEast="northEast" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="gba-control-validate btn-group-vertical">
|
||||
|
||||
<!-- Validate Button -->
|
||||
<div class="absolute left-4 top-44 z-[1000] select-none">
|
||||
<button
|
||||
class="min-w-27 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black text-sm p-1"
|
||||
class="group flex items-center justify-center relative overflow-visible outline-none font-semibold text-sm transition-all duration-300 ease-in-out
|
||||
w-10 h-10 rounded-xl border-2 shadow-md
|
||||
focus-visible:outline focus-visible:outline-3 focus-visible:outline-offset-2"
|
||||
:class="[
|
||||
validBoundingBox
|
||||
? 'bg-gradient-to-br from-lime-500 to-lime-700 border-lime-700 text-white shadow-lime-500/40 cursor-default gap-2 w-auto px-4 focus-visible:outline-lime-500/50'
|
||||
: 'bg-white dark:bg-gray-800 border-red-500 text-red-600 dark:text-red-400 gap-0 hover:bg-red-50 dark:hover:bg-gray-900 hover:border-red-500 hover:text-red-700 dark:hover:text-red-300 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-red-500/30 hover:w-auto hover:px-4 hover:gap-2 focus-visible:outline-red-500/50'
|
||||
]"
|
||||
type="button"
|
||||
@click.stop.prevent="validateBoundingBox"
|
||||
:class="[validBoundingBox ? 'cursor-not-allowed bg-green-500 is-active' : 'bg-red-500 ']"
|
||||
:aria-label="validBoundingBox ? 'Bounding box is valid' : 'Validate bounding box'"
|
||||
>
|
||||
<!-- <BaseIcon v-if="mdiMapCheckOutline" :path="mdiMapCheckOutline" /> -->
|
||||
{{ label }}
|
||||
<!-- Icon -->
|
||||
<BaseIcon
|
||||
v-if="mdiMapCheckOutline"
|
||||
:path="mdiMapCheckOutline"
|
||||
:size="20"
|
||||
:class="[
|
||||
'transition-transform duration-300',
|
||||
validBoundingBox && 'animate-[checkPulse_2s_ease-in-out_infinite]'
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Status badge -->
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider whitespace-nowrap transition-all duration-300 overflow-hidden"
|
||||
:class="[
|
||||
validBoundingBox
|
||||
? 'max-w-[100px] opacity-100 text-white drop-shadow'
|
||||
: 'max-w-0 opacity-0 group-hover:max-w-[100px] group-hover:opacity-100'
|
||||
]"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
|
||||
<!-- Pulse animation for valid state -->
|
||||
<span
|
||||
v-if="validBoundingBox"
|
||||
class="absolute top-1/2 left-1/2 w-full h-full bg-white/30 rounded-xl -translate-x-1/2 -translate-y-1/2 animate-[pulse_2s_ease-out_infinite] pointer-events-none"
|
||||
></span>
|
||||
|
||||
<!-- Ripple effect on click -->
|
||||
<span
|
||||
class="absolute top-1/2 left-1/2 w-0 h-0 rounded-full bg-white/30 -translate-x-1/2 -translate-y-1/2 transition-all duration-[600ms] active:w-[300px] active:h-[300px]"
|
||||
></span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<span
|
||||
v-if="!validBoundingBox"
|
||||
class="absolute left-[calc(100%+0.5rem)] top-1/2 -translate-y-1/2 px-3 py-1.5 bg-gray-800 text-white text-xs rounded-md whitespace-nowrap opacity-0 pointer-events-none transition-opacity duration-200 z-[1001] group-hover:opacity-100 group-hover:animate-[fadeInTooltip_0.2s_ease_0.5s_forwards]"
|
||||
>
|
||||
Click to validate
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -27,8 +78,7 @@ import { Component, Vue, Prop, Ref } from 'vue-facing-decorator';
|
|||
import { Map } from 'leaflet/src/map/index';
|
||||
import { Control } from 'leaflet/src/control/Control';
|
||||
import { LatLngBoundsExpression, LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||
// import { toLatLng } from 'leaflet/src/geo/LatLng';
|
||||
import { LatLng } from 'leaflet'; //'leaflet/src/geo/LatLng';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
||||
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
||||
import { mdiMapCheckOutline } from '@mdi/js';
|
||||
|
|
@ -37,22 +87,15 @@ import BaseIcon from '@/Components/BaseIcon.vue';
|
|||
import { MapOptions } from './MapOptions';
|
||||
import { LayerOptions, LayerMap } from './LayerOptions';
|
||||
import { MapService } from '@/Stores/map.service';
|
||||
import ZoomControlComponent from './zoom.component.vue';
|
||||
import DrawControlComponent from './draw.component.vue';
|
||||
import { ZoomControlComponent } from './zoom.component.vue';
|
||||
import { DrawControlComponent } from './draw.component.vue';
|
||||
import { Coverage } from '@/Dataset';
|
||||
import { canvas } from 'leaflet/src/layer/vector/Canvas';
|
||||
import { svg } from 'leaflet/src/layer/vector/SVG';
|
||||
import Notification from '@/utils/toast';
|
||||
|
||||
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) {
|
||||
|
|
@ -79,15 +122,11 @@ 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_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
||||
|
||||
@Component({
|
||||
|
|
@ -98,33 +137,19 @@ const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://o
|
|||
BaseIcon,
|
||||
},
|
||||
})
|
||||
export default class MapComponent extends Vue {
|
||||
/**
|
||||
* A map with the given ID is created inside this component.
|
||||
* This ID can be used the get the map instance over the map cache service.
|
||||
*/
|
||||
export class MapComponent extends Vue {
|
||||
@Prop()
|
||||
public mapId: string;
|
||||
|
||||
/**
|
||||
* The corresponding leaflet map options (see: https://leafletjs.com/reference-1.3.4.html#map-option)
|
||||
*/
|
||||
@Prop()
|
||||
public mapOptions: MapOptions;
|
||||
|
||||
@Prop()
|
||||
public coverage: Coverage;
|
||||
|
||||
// markerService: MarkerService
|
||||
/**
|
||||
* Bounds for the map
|
||||
*/
|
||||
@Prop({ default: null })
|
||||
public fitBounds: LatLngBoundsExpression;
|
||||
|
||||
/**
|
||||
* Describes the the zoom control options (see: https://leafletjs.com/reference-1.3.4.html#control-zoom)
|
||||
*/
|
||||
@Prop()
|
||||
public zoomControlOptions: Control.ZoomOptions;
|
||||
|
||||
|
|
@ -132,7 +157,7 @@ export default class MapComponent extends Vue {
|
|||
public baseMaps: LayerMap;
|
||||
|
||||
get label(): string {
|
||||
return this.validBoundingBox ? ' valid' : 'invalid';
|
||||
return this.validBoundingBox ? 'Valid' : 'Invalid';
|
||||
}
|
||||
|
||||
get validBoundingBox(): boolean {
|
||||
|
|
@ -144,35 +169,31 @@ export default class MapComponent extends Vue {
|
|||
|
||||
let isBoundValid = true;
|
||||
if (isValidNumber) {
|
||||
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
let _southWest: LatLng = new LatLng(this.coverage.y_min!, this.coverage.x_min!);
|
||||
let _northEast: LatLng = new LatLng(this.coverage.y_max!, this.coverage.x_max!);
|
||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||
// this.draw.removeShape();
|
||||
// Notification.showTemporary('Bounds are not valid.');
|
||||
isBoundValid = false;
|
||||
}
|
||||
}
|
||||
return isValidNumber && isBoundValid;
|
||||
}
|
||||
|
||||
@Ref('zoom') private zoom: ZoomControlComponent;
|
||||
@Ref('draw') private draw: DrawControlComponent;
|
||||
@Ref('zoom')
|
||||
private zoom: ZoomControlComponent;
|
||||
|
||||
@Ref('draw')
|
||||
private draw: DrawControlComponent;
|
||||
|
||||
// services:
|
||||
mapService = MapService();
|
||||
|
||||
mdiMapCheckOutline = mdiMapCheckOutline;
|
||||
southWest: LatLng;
|
||||
northEast: LatLng;
|
||||
|
||||
/**
|
||||
* Informs when initialization is done with map id.
|
||||
*/
|
||||
public onMapInitializedEvent: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
public map!: Map;
|
||||
// protected drawnItems!: FeatureGroup<any>;
|
||||
|
||||
validateBoundingBox() {
|
||||
if (this.validBoundingBox == false) {
|
||||
|
|
@ -182,53 +203,22 @@ export default class MapComponent extends Vue {
|
|||
}
|
||||
this.map.control && this.map.control.disable();
|
||||
var _this = this;
|
||||
// // _this.locationErrors.length = 0;
|
||||
// this.drawnItems.clearLayers();
|
||||
// //var xmin = document.getElementById("xmin").value;
|
||||
// var xmin = (<HTMLInputElement>document.getElementById("xmin")).value;
|
||||
// // var ymin = document.getElementById("ymin").value;
|
||||
// var ymin = (<HTMLInputElement>document.getElementById("ymin")).value;
|
||||
// //var xmax = document.getElementById("xmax").value;
|
||||
// var xmax = (<HTMLInputElement>document.getElementById("xmax")).value;
|
||||
// //var ymax = document.getElementById("ymax").value;
|
||||
// var ymax = (<HTMLInputElement>document.getElementById("ymax")).value;
|
||||
// var bounds = [[ymin, xmin], [ymax, xmax]];
|
||||
|
||||
// let _southWest: LatLng;
|
||||
// let _northEast: LatLng;
|
||||
// if (this.coverage.x_min && this.coverage.y_min) {
|
||||
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
// }
|
||||
// if (this.coverage.x_max && this.coverage.y_max) {
|
||||
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
// }
|
||||
|
||||
let _southWest: LatLng = new LatLng(this.coverage.y_min!, this.coverage.x_min!);
|
||||
let _northEast: LatLng = new LatLng(this.coverage.y_max!, this.coverage.x_max!);
|
||||
|
||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||
this.draw.removeShape();
|
||||
Notification.showTemporary('Bounds are not valid.');
|
||||
} else {
|
||||
// this.draw.drawShape(_southWest, _northEast);
|
||||
try {
|
||||
this.draw.drawShape(_southWest, _northEast);
|
||||
_this.map.fitBounds(bounds);
|
||||
|
||||
// var boundingBox = L.rectangle(bounds, { color: "#005F6A", weight: 1 });
|
||||
// // this.geolocation.xmin = xmin;
|
||||
// // this.geolocation.ymin = ymin;
|
||||
// // this.geolocation.xmax = xmax;
|
||||
// // this.geolocation.ymax = ymax;
|
||||
|
||||
// _this.drawnItems.addLayer(boundingBox);
|
||||
// _this.map.fitBounds(bounds);
|
||||
// this.options.message = "valid bounding box";
|
||||
// this.$toast.success("valid bounding box", this.options);
|
||||
Notification.showSuccess('valid bounding box');
|
||||
Notification.showSuccess('Valid bounding box');
|
||||
} catch (err) {
|
||||
// this.options.message = e.message;
|
||||
// // _this.errors.push(e);
|
||||
// this.$toast.error(e.message, this.options);
|
||||
Notification.showTemporary('An error occurred while drawing bounding box');
|
||||
// generatingCodes.value = false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
@ -242,16 +232,11 @@ export default class MapComponent extends Vue {
|
|||
this.map.off('zoomend zoomlevelschange');
|
||||
}
|
||||
|
||||
// @Emit(this.onMapInitializedEvent)
|
||||
protected initMap(): void {
|
||||
// let map: Map = (this.map = this.mapService.getMap(this.mapId));
|
||||
|
||||
let map: Map = (this.map = new Map(this.mapId, this.mapOptions));
|
||||
this.mapService.setMap(this.mapId, map);
|
||||
map.scrollWheelZoom.disable();
|
||||
|
||||
// return this.mapId;
|
||||
// this.$emit("onMapInitializedEvent", this.mapId);
|
||||
this.onMapInitializedEvent.emit(this.mapId);
|
||||
this.addBaseMap();
|
||||
|
||||
|
|
@ -260,45 +245,28 @@ export default class MapComponent extends Vue {
|
|||
|
||||
map.on(
|
||||
'Draw.Event.CREATED',
|
||||
function (event) {
|
||||
// drawnItems.clearLayers();
|
||||
// var type = event.type;
|
||||
(event: any) => {
|
||||
var layer = event.layer;
|
||||
|
||||
// if (type === "rectancle") {
|
||||
// layer.bindPopup("A popup!" + layer.getBounds().toBBoxString());
|
||||
var bounds = layer.getBounds();
|
||||
this.coverage.x_min = bounds.getSouthWest().lng;
|
||||
this.coverage.y_min = bounds.getSouthWest().lat;
|
||||
// console.log(this.geolocation.xmin);
|
||||
this.coverage.x_max = bounds.getNorthEast().lng;
|
||||
this.coverage.y_max = bounds.getNorthEast().lat;
|
||||
// }
|
||||
|
||||
// drawnItems.addLayer(layer);
|
||||
},
|
||||
this,
|
||||
);
|
||||
|
||||
// Initialise the FeatureGroup to store editable layers
|
||||
// let drawnItems = (this.drawnItems = new FeatureGroup());
|
||||
// map.addLayer(drawnItems);
|
||||
|
||||
this.map.on('zoomend zoomlevelschange', this.zoom.updateDisabled, this.zoom);
|
||||
|
||||
// if (this.fitBounds) {
|
||||
// this.map.fitBounds(this.fitBounds);
|
||||
// }
|
||||
if (this.coverage.x_min && this.coverage.y_min) {
|
||||
this.southWest = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
this.southWest = new LatLng(this.coverage.y_min!, this.coverage.x_min!);
|
||||
} else {
|
||||
this.southWest = new LatLng(46.5, 9.9);
|
||||
}
|
||||
if (this.coverage.x_max && this.coverage.y_max) {
|
||||
this.northEast = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
this.northEast = new LatLng(this.coverage.y_max!, this.coverage.x_max!);
|
||||
} else {
|
||||
this.northEast = new LatLng(48.9, 16.9);
|
||||
} // this.northEast = toLatLng(48.9, 16.9);
|
||||
}
|
||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
|
|
@ -318,10 +286,6 @@ export default class MapComponent extends Vue {
|
|||
private addBaseMap(layerOptions?: LayerOptions): void {
|
||||
if (this.map) {
|
||||
if (!this.baseMaps || this.baseMaps.size === 0) {
|
||||
// let bmapgrau = tileLayer('https://{s}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png', {
|
||||
// subdomains: ['maps', 'maps1', 'maps2', 'maps3', 'maps4'],
|
||||
// attribution: 'Datenquelle: <a href="http://www.basemap.at/">basemap.at</a>',
|
||||
// });
|
||||
let osmGgray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
|
||||
format: 'image/png',
|
||||
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
|
||||
|
|
@ -337,45 +301,61 @@ export default class MapComponent extends Vue {
|
|||
}
|
||||
}
|
||||
}
|
||||
export default MapComponent;
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
/* .leaflet-container {
|
||||
<style scoped>
|
||||
/* Leaflet container - only what can't be done with Tailwind */
|
||||
:deep(.leaflet-container) {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
outline-offset: 1px;
|
||||
} */
|
||||
.leaflet-container {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
background: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.gba-control-validate {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 150px;
|
||||
z-index: 999;
|
||||
:deep(.leaflet-container .leaflet-pane) {
|
||||
z-index: 30 !important;
|
||||
}
|
||||
|
||||
.btn-group-vertical button {
|
||||
display: block;
|
||||
|
||||
margin-left: 0;
|
||||
margin-top: 0.5em;
|
||||
/* Custom animations */
|
||||
@keyframes checkPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-container .leaflet-pane {
|
||||
z-index: 30!important;
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
/* .leaflet-pane {
|
||||
z-index: 30;
|
||||
} */
|
||||
</style>
|
||||
|
||||
@keyframes fadeInTooltip {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
:deep(.leaflet-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
:deep(.leaflet-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue