<template>
<Header :name="$route.name" :title="title ?? $t($route.name)" :backButton="backButton" />
<Toolbar ref="toolbar" :open="toolbarOpen || !toolbarCloseable" width="100%" :toolbarCloseable="toolbarCloseable"
@close="toolbarOpen = false">
<component :is="toolbarComponent" :parent="this" />
</Toolbar>
<q-btn v-if="toolbar" style="position:absolute; top:40px; right:5px; z-index: 9999;" @click="toolbarOpen = true"
padding="xs" icon="settings" size="sm" />
<div id="map-container"
:style="{ top: (35 + toolbarHeight) + 'px', height: ($q.screen.height - 75 - toolbarHeight) + 'px' }"
tabIndex="0">
<ContextMenu ref="clickMenu" :contextMenu="false" :options="[
{ label: $t('History'), callback: showHistory, visible: !activeAction && selectedTitle != null && selectedFeature == null },
//{ label: $t('Frequencies'), callback: showFrequencies, visible: !activeAction && selectedIndicator != null },
//{ label: $t('Properties'), callback: editProps, visible: !activeAction, runIfSingle: true },
///{ label: $t('Statistics'), callback: showFeatureStats, visible: !activeAction && selectedFeature != null && !selectedFeature.get('custom_geometry_id') && selectedCatalog != null }
]" />
<!-- <ContextMenu ref="contextmenu" :options="[
{ label: $t('New polygon'), callback: newObject, options: { type: 'Polygon' } },
{ label: $t('New point'), callback: newObject, options: { type: 'Point' } },
{ label: $t('New line'), callback: newObject, options: { type: 'LineString' } },
{ label: $t('Delete'), callback: deleteObject, visible: selectedFeature != null },
{ label: $t('End editing'), callback: endObject, visible: activeAction != null },
]" /> -->
<div id="map" ref="map" />
<i v-if="crosshair" id="crosshair" class="crosshair material-icons">close</i>
<q-btn-group flat class="btnGroup">
<q-btn size="sm" round flat dense icon="content_copy" @click="exportMap(false)">
<q-tooltip anchor="center left" self="center right">{{ $t("Copy to clipboard") }}</q-tooltip>
</q-btn>
<q-btn size="sm" round flat dense icon="download" @click="exportMap(true)">
<q-tooltip anchor="center left" self="center right">{{ $t("Download as image") }}</q-tooltip>
</q-btn>
<q-btn v-if="rotation != 0" size="sm" round flat dense icon="north" @click="map.getView().setRotation(0)">
<q-tooltip anchor="center left" self="center right">{{ $t("North orientation") }}</q-tooltip>
</q-btn>
<q-btn v-if="trackPosition" size="sm" round flat dense :icon="$icons.my_location"
@click="map.getView().setCenter(geolocation.getPosition())">
<q-tooltip anchor="center left" self="center right">{{ $t("Center at GPS") }}</q-tooltip>
</q-btn>
</q-btn-group>
<OlMapLegend v-if="selectedCatalog || selectedIndicator" :options="legend"
:unit="selectedIndicator ? selectedIndicator.unit : null" />
<ImageViewer ref="imageViewerMap" @compassChanged="compassChanged" />
<div id="infoTip" />
<div id="info" v-if="info">{{ `zoom ${zoom.toFixed(1)}, center
${center[0].toFixed(3)},
${center[1].toFixed(3)}` }} '{{ activeAction }}' pixel: {{ valueAtPixel }}
ll: {{ cursorPosition[0].toFixed(3) }} {{ cursorPosition[1].toFixed(3) }}
xy: {{ cursorXY[0].toFixed(0) }} {{ cursorXY[1].toFixed(0) }} c: {{ ($store.compass ?? 0).toFixed(0) }}
</div>
</div>
</template>
<script>
/**
* OpenLayers map component.
*
* @component
* @name OlMap
* @example
* <OlMap />
*/
// #region imports
import "ol/ol.css";
import { Map, View, Feature, Overlay } from "ol";
import GeoJSON from 'ol/format/GeoJSON';
import GeoTIFFSource from 'ol/source/GeoTIFF.js';
import { OSM, Vector as VectorSource, Cluster as ClusterSource, BingMaps } from "ol/source";
import { Vector as VectorLayer } from "ol/layer";
import TileLayer from 'ol/layer/WebGLTile.js';
import { defaults as defaultInteractions, DragRotateAndZoom, Extent, Draw, Modify, Select, Snap } from "ol/interaction";
//import { defaults as defaultControls } from 'ol/control/defaults';
import { fromLonLat } from "ol/proj";
import { Point } from "ol/geom";
import { get as getProjection, useGeographic } from 'ol/proj';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4.js';
import { defineAsyncComponent } from 'vue';
import { MapMixin } from '../mixins/ol-map.js';
import { MapEditingMixin } from '../mixins/ol-map-editing.js';
import { MapStylesMixin } from '../mixins/ol-map-styles.js';
import { MapEventsMixin } from '../mixins/ol-map-events.js';
import { MapLayersMixin } from '../mixins/ol-map-layers.js';
import OlMapPopup from './ol-map-popup.vue';
import OlMapLegend from './ol-map-legend.vue';
import OlMapProps from './ol-map-props.vue';
import Toolbar from './toolbar.vue';
import ImageViewer from './image-viewer.vue';
import ContextMenu from './context-menu.vue';
import { click, shiftKeyOnly, singleClick } from "ol/events/condition";
import { Style, Circle, Fill, Stroke } from "ol/style";
// #endregion
export default {
name: 'OlMap',
mixins: [MapMixin, MapEditingMixin, MapStylesMixin, MapEventsMixin, MapLayersMixin],
components: {
OlMapPopup,
OlMapLegend,
OlMapProps,
Toolbar,
ContextMenu,
ImageViewer,
},
beforeRouteEnter(to, from, next) {
next(vm => { vm.init(to.name); });
},
beforeRouteUpdate(to, from, next) {
this.routeKey++;
this.init(to.name);
next();
},
beforeRouteLeave(to, from, next) {
if (this.activeAction != null) {
this.endObject();
}
console.log("Leaving map");
this.geolocation.setTracking(false);
this.saveStorable(this, from.name);
next();
},
computed: {
toolbarComponent: function () {
if (!this.toolbar) return null;
return defineAsyncComponent(() => import(`../components/${this.toolbar}.vue`));
},
},
watch: {
//"$q.screen.width": "calcToolbarHeight",
//toolbarOpen: "calcToolbarHeight",
valueAtPixel: function (val) {
this.valueAtPixelStyle = {
position: "absolute",
top: (this.cursorPixel[1] - 30) + "px",
left: this.cursorPixel[0] + "px",
backgroundColor: "white",
padding: "3px",
borderRadius: "5px",
}
},
selectedShape: function (val) {
this.setShapeLayer();
},
selectedBaseLayer: function (val) {
this.setBaseLayer();
},
showEditableSource: function (val) {
this.editableLayer.setVisible(val);
},
nutsLevel: function (val) {
this.setBoundariesLayer();
},
showPoints: function (val) {
this.setPointLayer();
},
srid: function (val) {
this.setSrid(val)
},
trackPosition: function (val) {
this.geolocationLayer.setVisible(val);
this.geolocation.setTracking(val);
},
opacity: function (val) {
this.tiffLayer.setOpacity(val);
},
clustered: function (val) {
if (this.pointSource) {
this.pointLayer.setSource(this.clustered ? this.clusteredSource : this.pointSource);
}
},
selectedIndicator: async function (val) {
this.setPointLayer();
this.setBoundariesLayer();
if (this.selectedIndicator) {
this.legend = await this.get("Home/GetIndicatorLegend", { indicatorId: this.selectedIndicator.value });
if (!this.legend) this.legend = [];
} else {
this.legend = [];
}
},
selectedDataSource: function (val) {
this.selectedIndicator = null;
this.setPointLayer();
this.setBoundariesLayer();
},
selectedCatalog: function (val) {
if (this.selectedTitle) {
this.createLegend(this.selectedTitle.color_map);
}
this.setTileLayer();
},
},
data: function () {
return {
routeKey: 0,
storable: ["toolbarOpen", "srid", "center", "zoom", "rotation", "opacity", "trackPosition", "clustered", "showEditableSource", "selectedBaseLayer", "selectedDataSource", "info"],
dialog: false,
srid: null,
toolbar: null,
toolbarOpen: false,
toolbarCloseable: true,
center: [12, 51],
cursorPosition: [12, 51],
cursorXY: [0, 0],
cursorPixel: [0, 0],
pointFeatures: [],
zoom: 5,
map: null,
view: null,
vectorSource: null,
overlay: null,
projectionForSave: null,
projection: getProjection("EPSG:3857"),
extent: null,
format: new GeoJSON(),
trackPosition: false,
rotation: 0,
clustered: true,
type: null,
geom: null,
source: null,
opacity: 0.5,
styleCache: {},
nutsLevel: null,
showPoints: true,
selectedIndicator: null,
selectedTitle: null,
sources: [],
selectedCatalog: null,
legend: [],
activeAction: null,
showEditableSource: false,
select: null,
modify: null,
snap: null,
draw: null,
selectedFeature: null,
toolbarHeight: 0,
valueAtPixel: null,
tiffLayer: null,
pixelRatio: 1,
moveTimeout: null,
selectedBaseLayer: { value: 1, label: "OSM" },
selectedDataSource: { value: 1, label: "LUCAS 2018" },
selectedShape: null,
crosshairOverlay: null,
crosshair: false,
params: null,
baseLayers: [
{ value: 1, label: "OSM" },
{ value: 2, label: "Bing satellite" }
],
pointSource: null,
pointLayer: null,
clusteredSource: null,
editableLayer: null,
editableSource: null,
boundariesSource: null,
boundariesLayer: null,
shapeSource: null,
shapeLayer: null,
geolocation: null,
accuracyFeature: null,
positionFeature: null,
geolocationSource: null,
geolocationLayer: null,
editStack: [],
drawVertexByVertex: false,
infoTip: null,
info: false,
title: null,
backButton: false,
};
},
methods: {
exportMap(download) {
const mapCanvas = document.createElement('canvas');
const size = this.map.getSize();
mapCanvas.width = size[0];
mapCanvas.height = size[1];
const mapContext = mapCanvas.getContext('2d');
Array.prototype.forEach.call(
this.map.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer'),
function (canvas) {
if (canvas.width > 0) {
const opacity =
canvas.parentNode.style.opacity || canvas.style.opacity;
mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
let matrix;
const transform = canvas.style.transform;
if (transform) {
// Get the transform parameters from the style's transform matrix
matrix = transform
.match(/^matrix\(([^\(]*)\)$/)[1]
.split(',')
.map(Number);
} else {
matrix = [
parseFloat(canvas.style.width) / canvas.width,
0,
0,
parseFloat(canvas.style.height) / canvas.height,
0,
0,
];
}
// Apply the transform to the export map context
CanvasRenderingContext2D.prototype.setTransform.apply(
mapContext,
matrix,
);
const backgroundColor = canvas.parentNode.style.backgroundColor;
if (backgroundColor) {
mapContext.fillStyle = backgroundColor;
mapContext.fillRect(0, 0, canvas.width, canvas.height);
}
mapContext.drawImage(canvas, 0, 0);
}
},
);
mapContext.globalAlpha = 1;
mapContext.setTransform(1, 0, 0, 1, 0, 0);
if (download) {
var link = document.createElement('a');
link.href = mapCanvas.toDataURL();
link.download = 'map.png';
link.click();
} else {
mapCanvas.toBlob((blob) => {
const item = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([item])
.then(() => {
this.$q.notify({
message: this.$t("Image copied to clipboard"),
color: "positive",
timeout: 1000,
position: "bottom"
});
})
.catch((err) => {
this.showError("Copy failed: ", err);
});
}, 'image/png');
}
},
// async calcToolbarHeight() {
// await this.$nextTick(() => {
// this.toolbarHeight = this.toolbarOpen ? this.$refs.toolbar.$el.clientHeight : 0;
// });
// },
/**
* Handles the click event on a popup.
*
* @param {Object} feature - The feature object associated with the clicked popup.
*/
async popupClicked(feature) {
this.$refs.popup.hide();
let osm_id = feature.get('osm_id');
let result = await this.get(this.api2, { OsmId: osm_id });
this.vectorSource.addFeatures(this.format.readFeatures(result));
},
/**
* Initializes the component.
*
* @param {string} routeName - The name of the route.
*/
async init(routeName) {
proj4.defs("EPSG:4258", "+proj=longlat +ellps=GRS80 +no_defs +type=crs");
proj4.defs("EPSG:3035", "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs");
register(proj4);
let self = this;
this.infoTip = document.getElementById('infoTip');
this.selectedCatalog = null;
this.selectedIndicator = null;
this.selectedTitle = null;
this.selectedShape = null;
this.crosshair = false;
this.toolbarCloseable = true;
this.editStack = [];
this.params = null;
this.backButton = false;
this.projectionForSave = getProjection("EPSG:4326");
this.createGeolocation();
console.log("route", this.$route.name, this.$store.props[this.$route.name]);
this.copyObject(this.$store.props[this.$route.name], this, true);
// if this.srid is not an object, make it one
this.srid = this.srid ?? 3035;
this.projection = this.projection ?? "EPSG:" + this.srid;
this.loadStorable(this, routeName);
this.projection = "EPSG:" + this.srid;
this.pointSource = new VectorSource({ format: this.format });
this.editableSource = new VectorSource({ format: this.format });
this.boundariesSource = new VectorSource({ format: this.format });
this.shapeSource = new VectorSource({ format: this.format });
this.clusteredSource = new ClusterSource({
format: this.format,
projection: this.projection,
distance: 7, //parseInt(distanceInput.value, 10),
minDistance: 7, // parseInt(minDistanceInput.value, 10),
source: this.pointSource
});
this.geolocationSource = new VectorSource({
format: this.format,
projection: this.projection,
features: [this.accuracyFeature, this.positionFeature]
});
this.baseLayer = new TileLayer({
properties: { visible: true, zIndex: 0, source: null },
});
this.baseLayer.on("precompose", e => {
this.pixelRatio = e.frameState.pixelRatio;
});
this.tiffLayer = new TileLayer({
properties: {
type: "GeoTIFF",
}, visible: false, opacity: this.opacity, zIndex: 1, source: null,
});
this.editableLayer = new VectorLayer({
source: this.editableSource,
zIndex: 100,
style: feature => {
if (feature.getGeometry().getType() === 'Point') {
return this.getObservationPointStyle(feature, this.zoom);
} else {
return this.shapeStyle;
}
}
});
this.shapeLayer = new VectorLayer({ source: this.shapeSource, zIndex: 2 });
this.boundariesLayer = new VectorLayer({
source: this.boundariesSource, opacity: 0.5, zIndex: 3, style: feature => {
return this.getBoundaryStyle(feature);
}
});
this.pointLayer = new VectorLayer({
source: this.clustered ? this.clusteredSource : this.pointSource,
type: 'Point',
zIndex: 4,
style: feature => {
return this.getPointStyle(feature, this.clustered, this.zoom);
}
});
this.geolocationLayer = new VectorLayer({
source: this.geolocationSource,
zIndex: 5000,
});
this.geolocationLayer.set('interactive', false);
if (this.map) this.map.dispose();
this.map = new Map({
controls: [],
interactions: defaultInteractions().extend([new DragRotateAndZoom()]),
target: "map",
layers: [],
view: new View({
projection: this.projection,
center: fromLonLat(this.center, this.projection),
zoom: this.zoom,
})
// controls: defaultControls({ rotateOptions: { autoHide: false } }),
});
this.map.addLayer(this.baseLayer);
this.map.addLayer(this.tiffLayer);
this.map.addLayer(this.editableLayer);
this.map.addLayer(this.shapeLayer);
this.map.addLayer(this.boundariesLayer);
this.map.addLayer(this.pointLayer);
this.map.addLayer(this.geolocationLayer);
this.editableLayer.setVisible(this.showEditableSource);
this.$store.map = this.map;
// this.select = new Select({
// condition: singleClick,
// toggleCondition: shiftKeyOnly,
// hitTolerance: 5,
// style: this.selectedStyleFunction,
// });
// this.map.addInteraction(this.select);
this.modify = new Modify({
source: this.editableSource,
});
this.map.addInteraction(this.modify);
this.createEvents();
this.setBaseLayer();
this.setTileLayer();
if (this.params && this.params.id) {
await this.readCustomFeatures(this.params.id);
} else if (this.customFeatures) {
await this.readCustomFeatures(null);
}
if (this.shapeAPI) {
let shapes = await this.get(this.shapeAPI);
console.log(shapes);
this.shapeSource.addFeatures(this.format.readFeatures(shapes));
this.animateToExtent(this.shapeSource);
}
if (this.editObservation) {
this.trackPosition = true;
this.showEditableSource = true;
}
this.map.updateSize();
if (this.showEditableSource) {
this.$nextTick(() => {
// count features in editable source with id starting with 'g'
let count = 0;
for (let f of this.editableSource.getFeatures()) {
if (f.getId().startsWith('g')) {
count++;
}
}
if (count > 0) {
let f = this.editableSource.getFeatures()[0];
if (count == 1 && f.getGeometry().getType() === 'Point') {
this.map.getView().setCenter(f.getGeometry().getCoordinates());
} else {
this.animateToExtent(this.editableSource);
}
}
});
}
}
}
}
</script>
<style scoped>
#map-container {
position: absolute;
width: 100%;
/*height: calc(100vh - 75px);*/
}
#map {
width: 100%;
height: 100%;
}
#info {
position: absolute;
bottom: 0px;
left: 150px;
padding: 5px;
z-index: 1000;
}
#toolbar {
padding: 0;
margin: 0;
}
.btnGroup {
position: absolute;
bottom: 10px;
right: 5px;
padding: 5px;
z-index: 1000;
flex-direction: column;
}
.crosshair {
font-size: 30px;
font-weight: 50;
color: blue;
position: absolute;
top: calc(50% - 15px);
left: calc(50% - 15px);
pointer-events: none;
}
#infoTip {
position: absolute;
display: inline-block;
height: 30px;
width: auto;
z-index: 100;
background-color: #fff;
color: #000;
text-align: center;
border-radius: 4px;
padding: 5px;
transform: translate(-60%, -80%);
/* Center the tip horizontally */
visibility: hidden;
pointer-events: none;
}
</style>