This commit is contained in:
@@ -0,0 +1,710 @@
|
||||
<script>
|
||||
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl, NavigationControl, setWorkerUrl } from 'maplibre-gl';
|
||||
import maplibreWorkerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import Lightbox from '@scripts/lightbox';
|
||||
|
||||
import AppIcon from '@components/AppIcon';
|
||||
import AppIconStack from '@components/AppIconStack';
|
||||
import ProjectPopup from '@components/ProjectPopup';
|
||||
import ProjectFeed from '@components/ProjectFeed';
|
||||
import ProjectSettings from '@components/ProjectSettings';
|
||||
|
||||
setWorkerUrl(maplibreWorkerUrl);
|
||||
|
||||
class GroupedScaleControl {
|
||||
constructor(options) {
|
||||
this.scale = new ScaleControl(options);
|
||||
}
|
||||
|
||||
onAdd(map) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
|
||||
|
||||
const scaleElement = this.scale.onAdd(map);
|
||||
scaleElement.classList.remove('maplibregl-ctrl');
|
||||
this.container.appendChild(scaleElement);
|
||||
|
||||
return this.container;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this.scale.onRemove();
|
||||
this.container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AppIcon,
|
||||
ProjectFeed,
|
||||
ProjectSettings
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
panels: {
|
||||
leftOpen: false,
|
||||
rightOpen: false
|
||||
},
|
||||
feed: null,
|
||||
settings: null,
|
||||
track: null,
|
||||
markers: [],
|
||||
markerProps: {
|
||||
project: {mainClasses: 'project', iconMain: 'marker', iconSub: 'project'},
|
||||
image: {mainClasses: 'media', iconMain: 'marker', iconSub: 'image'},
|
||||
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
|
||||
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
|
||||
},
|
||||
project: null,
|
||||
modeHisto: null,
|
||||
baseMaps: [],
|
||||
baseMap: null,
|
||||
terrainEnabled: false,
|
||||
map: null,
|
||||
mapInitializing: false,
|
||||
markerHeight: 32, //FIXME
|
||||
mapPadding: 16 + 32, //1rem + marker height
|
||||
maxZoom: 15,
|
||||
initialPitch: 45,
|
||||
lightbox: null,
|
||||
hikes: {
|
||||
colors: {},
|
||||
width: null,
|
||||
transitions: {
|
||||
lineWidthTransition: {duration:null}
|
||||
}
|
||||
},
|
||||
popup: {content: null, element: null},
|
||||
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
projectOptions() {
|
||||
return [
|
||||
this.overview,
|
||||
...Object.values(this.projects)
|
||||
];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
baseMap(sNewBaseMap, sOldBaseMap) {
|
||||
if(this.map?.isStyleLoaded()) {
|
||||
if(sOldBaseMap && this.map.getLayer(sOldBaseMap)) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
||||
if(sNewBaseMap && this.map.getLayer(sNewBaseMap)) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
||||
}
|
||||
},
|
||||
terrainEnabled(bEnabled) {
|
||||
if(!this.map?.isStyleLoaded()) return;
|
||||
if(bEnabled) this.addTerrain();
|
||||
else this.removeTerrain();
|
||||
},
|
||||
'hash.items.0'(newProjectCodename, oldProjectCodename) { //hash.items.0 = Project Code Name
|
||||
if(newProjectCodename != oldProjectCodename) {
|
||||
this.hash.items = [newProjectCodename]; //Force removal of direct link
|
||||
this.settings.toggle(false, 0);
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
map: {
|
||||
panToBetweenPanels: this.panToBetweenPanels,
|
||||
openMarkerPopup: this.openMarkerPopup,
|
||||
closePopup: this.closePopup,
|
||||
isMarkerVisible: this.isMarkerVisible
|
||||
},
|
||||
project: this
|
||||
};
|
||||
},
|
||||
inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'],
|
||||
mounted() {
|
||||
//Starts default project init() through watcher
|
||||
if(this.hash.items.length == 0) {
|
||||
this.hash.items[0] = (this.projects.getDefaultProject().mode == this.consts.modes.blog)?this.projects.getDefaultCodeName():this.overview.codename;
|
||||
}
|
||||
else this.init();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.quit();
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.initLightbox();
|
||||
this.hikes.colors = {
|
||||
'main': this.getStyleProperty('--track-main'),
|
||||
'off-track': this.getStyleProperty('--track-off-track'),
|
||||
'hitchhiking': this.getStyleProperty('--track-hitchhiking')
|
||||
};
|
||||
this.hikes.width = parseFloat(this.getStyleProperty('--track-width'));
|
||||
this.hikes.transitions.lineWidthTransition.duration = parseFloat(this.getStyleProperty('--trans-quick'));
|
||||
|
||||
//Reset values
|
||||
this.track = null;
|
||||
this.project = null;
|
||||
this.removeMapContent();
|
||||
|
||||
//Build Map
|
||||
this.mapInitializing = true;
|
||||
if(this.hash.items[0] && this.projects[this.hash.items[0]]) await this.initProject(this.hash.items[0]);
|
||||
else await this.initOverview();
|
||||
this.mapInitializing = false;
|
||||
},
|
||||
quit() {
|
||||
this.lightbox.end(true);
|
||||
this.lightbox = null;
|
||||
this.removeMap();
|
||||
},
|
||||
async initOverview() {
|
||||
this.modeHisto = true;
|
||||
this.hash.items = [this.overview.codename];
|
||||
this.feed.toggle(false, 0);
|
||||
|
||||
await this.initOverviewMap();
|
||||
},
|
||||
async initProject(sProjectCodeName) {
|
||||
this.project = this.projects[sProjectCodeName];
|
||||
this.modeHisto = (this.project.mode == this.consts.modes.histo);
|
||||
|
||||
await this.$nextTick();
|
||||
const pMapReady = this.initProjectMap();
|
||||
await this.feed.init(pMapReady);
|
||||
},
|
||||
initLightbox() {
|
||||
if(!this.lightbox) {
|
||||
this.lightbox = new Lightbox({
|
||||
alwaysShowNavOnTouchDevices: true,
|
||||
fadeDuration: parseFloat(this.getStyleProperty('--trans-quick')),
|
||||
imageFadeDuration: parseFloat(this.getStyleProperty('--trans-quick')),
|
||||
positionFromTop: 0,
|
||||
resizeDuration: parseFloat(this.getStyleProperty('--trans-slow')),
|
||||
hasVideo: true,
|
||||
onMediaChange: async (oMedia) => {
|
||||
this.hash.items = [this.project.codename, 'media', oMedia.id];
|
||||
if(oMedia.set == 'post-medias') {
|
||||
(await this.feed.goToPost('media', oMedia.id))?.panMapToMarker();
|
||||
if(!this.lightbox.hasMediaAfterCurrent()) {
|
||||
await this.feed.getNextFeed();
|
||||
await this.$nextTick();
|
||||
this.lightbox.refreshAlbum();
|
||||
}
|
||||
}
|
||||
},
|
||||
onClosing: () => {this.hash.items = [this.hash.items[0]];}
|
||||
});
|
||||
}
|
||||
},
|
||||
async initProjectMap() {
|
||||
[
|
||||
{maps: this.baseMaps, markers: this.markers},
|
||||
this.track
|
||||
] = await Promise.all([
|
||||
this.api.get('markers', {id_project: this.project.id}),
|
||||
this.api.getAsset(this.project.geofilepath)
|
||||
]);
|
||||
|
||||
await this.initMap();
|
||||
},
|
||||
async initOverviewMap() {
|
||||
this.baseMaps = this.consts.default_maps;
|
||||
this.markers = Object.values(this.projects).map((asProject) => ({
|
||||
type: 'project',
|
||||
subtype: 'project',
|
||||
...asProject,
|
||||
opacityWhenCovered: 0.3
|
||||
}));
|
||||
|
||||
await this.initMap();
|
||||
},
|
||||
async initMap() {
|
||||
//Build map
|
||||
if(!this.map) this.addMap();
|
||||
this.updateMapPadding();
|
||||
|
||||
//Force wait for load event
|
||||
await new Promise((resolve) => {
|
||||
if(this.map.isStyleLoaded()) resolve();
|
||||
else this.map.once('load', resolve);
|
||||
});
|
||||
|
||||
this.map.resize();
|
||||
this.setInitialProjectCamera();
|
||||
|
||||
//Add content: Base Maps, Tracks, Markers
|
||||
this.addMapContent();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
if(this.map.loaded() && this.map.areTilesLoaded()) resolve();
|
||||
else this.map.once('idle', resolve);
|
||||
});
|
||||
},
|
||||
addMap() {
|
||||
this.map = new Map({
|
||||
container: 'map',
|
||||
aroundCenter: true,
|
||||
style: {
|
||||
version: 8,
|
||||
projection: {type: 'globe'},
|
||||
sky: {
|
||||
'sky-color': this.getStyleProperty('--space'),
|
||||
'horizon-color': this.getStyleProperty('--horizon'),
|
||||
'sky-horizon-blend': 0.35,
|
||||
'atmosphere-blend': 0.8
|
||||
},
|
||||
sources: {},
|
||||
layers: []
|
||||
},
|
||||
attributionControl: false
|
||||
});
|
||||
this.map.addControl(new GroupedScaleControl({unit: 'metric'}), 'bottom-right');
|
||||
this.map.addControl(new NavigationControl({showZoom: false, visualizePitch: true}), 'bottom-right');
|
||||
},
|
||||
removeMap() {
|
||||
this.removeMapContent();
|
||||
this.map?.remove();
|
||||
this.map = null;
|
||||
},
|
||||
addMapContent() {
|
||||
this.baseMaps.forEach(this.addBaseMap);
|
||||
if(this.terrainEnabled) this.addTerrain();
|
||||
this.addTrack();
|
||||
this.markers.forEach(this.addMarker);
|
||||
},
|
||||
removeMapContent() {
|
||||
if(!this.map) return;
|
||||
|
||||
this.closePopup();
|
||||
this.removeTrack();
|
||||
this.markers.forEach(this.removeMarker);
|
||||
this.removeTerrain();
|
||||
this.baseMaps.forEach(this.removeBaseMap);
|
||||
},
|
||||
addTerrain() {
|
||||
// MapLibre terrain's fog matrix is only implemented for Mercator.
|
||||
this.map.setProjection({type: 'mercator'});
|
||||
if(!this.map.getSource('terrain-dem')) {
|
||||
this.map.addSource('terrain-dem', {
|
||||
type: 'raster-dem',
|
||||
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
maxzoom: 15,
|
||||
encoding: 'terrarium'
|
||||
});
|
||||
}
|
||||
if(!this.map.getSource('hillshade-dem')) {
|
||||
this.map.addSource('hillshade-dem', {
|
||||
type: 'raster-dem',
|
||||
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
maxzoom: 13,
|
||||
encoding: 'terrarium'
|
||||
});
|
||||
}
|
||||
if(!this.map.getLayer('terrain-hillshade')) {
|
||||
this.map.addLayer({
|
||||
id: 'terrain-hillshade',
|
||||
type: 'hillshade',
|
||||
source: 'hillshade-dem',
|
||||
paint: {
|
||||
'hillshade-exaggeration': 0.35,
|
||||
'hillshade-shadow-color': '#2d342b',
|
||||
'hillshade-highlight-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
}
|
||||
this.map.setTerrain({source: 'terrain-dem', exaggeration: 1.25});
|
||||
},
|
||||
removeTerrain() {
|
||||
if(!this.map) return;
|
||||
if(this.map.getTerrain()) this.map.setTerrain(null);
|
||||
if(this.map.getLayer('terrain-hillshade')) this.map.removeLayer('terrain-hillshade');
|
||||
if(this.map.getSource('hillshade-dem')) this.map.removeSource('hillshade-dem');
|
||||
if(this.map.getSource('terrain-dem')) this.map.removeSource('terrain-dem');
|
||||
this.map.setProjection({type: 'globe'});
|
||||
},
|
||||
addBaseMap(asBaseMap) {
|
||||
if(asBaseMap.default_map) this.baseMap = asBaseMap.codename;
|
||||
if(this.map.getSource(asBaseMap.codename) && this.map.getLayer(asBaseMap.codename)) return;
|
||||
this.map.addSource(asBaseMap.codename, {
|
||||
type: 'raster',
|
||||
tiles: [asBaseMap.pattern],
|
||||
tileSize: asBaseMap.tile_size
|
||||
});
|
||||
this.map.addLayer({
|
||||
id: asBaseMap.codename,
|
||||
type: 'raster',
|
||||
source: asBaseMap.codename,
|
||||
'layout': {'visibility': asBaseMap.default_map?'visible':'none'},
|
||||
minZoom: asBaseMap.min_zoom,
|
||||
maxZoom: asBaseMap.max_zoom
|
||||
});
|
||||
},
|
||||
removeBaseMap(asBaseMap) {
|
||||
if(this.map.getLayer(asBaseMap.codename)) this.map.removeLayer(asBaseMap.codename);
|
||||
if(this.map.getSource(asBaseMap.codename)) this.map.removeSource(asBaseMap.codename);
|
||||
},
|
||||
addTrack() {
|
||||
if(!this.track) return;
|
||||
|
||||
this.track.features.forEach((oFeature, iFeatureId) => {
|
||||
oFeature.properties.track_id = iFeatureId;
|
||||
});
|
||||
this.map.addSource('track', {
|
||||
'type': 'geojson',
|
||||
'data': this.track
|
||||
});
|
||||
|
||||
//Color mapping
|
||||
let asColorMapping = ['match', ['get', 'type']];
|
||||
for(const [sHikeType, sColor] of Object.entries(this.hikes.colors)) {
|
||||
asColorMapping.push(sHikeType);
|
||||
asColorMapping.push(sColor);
|
||||
}
|
||||
asColorMapping.push('black'); //fallback value
|
||||
|
||||
//Track layer
|
||||
this.map.addLayer({
|
||||
'id': 'track',
|
||||
'type': 'line',
|
||||
'source': 'track',
|
||||
'layout': {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
'paint': {
|
||||
'line-color': asColorMapping,
|
||||
'line-width': this.hikes.width
|
||||
}
|
||||
});
|
||||
|
||||
//Enlarged track (click hit box)
|
||||
this.map.addLayer({
|
||||
'id': 'track-hitbox',
|
||||
'type': 'line',
|
||||
'source': 'track',
|
||||
'paint': {
|
||||
'line-opacity': 0,
|
||||
'line-width': this.hikes.width + this.mapPadding
|
||||
}
|
||||
});
|
||||
this.map.on('click', 'track-hitbox', this.openTrackPopup);
|
||||
this.map.on('mouseenter', 'track-hitbox', this.onTrackHover);
|
||||
this.map.on('mouseleave', 'track-hitbox', this.onTrackHover);
|
||||
},
|
||||
removeTrack() {
|
||||
//Over clickable track
|
||||
if(this.map.getLayer('track-hitbox')) {
|
||||
this.map.off('click', 'track-hitbox', this.openTrackPopup);
|
||||
this.map.off('mouseenter', 'track-hitbox', this.onTrackHover);
|
||||
this.map.off('mouseleave', 'track-hitbox', this.onTrackHover);
|
||||
this.map.removeLayer('track-hitbox');
|
||||
}
|
||||
|
||||
//Actual track
|
||||
if(this.map.getLayer('track')) this.map.removeLayer('track');
|
||||
|
||||
//Track source
|
||||
if(this.map.getSource('track')) this.map.removeSource('track');
|
||||
},
|
||||
addMarker(oMarker) {
|
||||
const $Marker = document.createElement('div');
|
||||
oMarker.app = createApp(AppIconStack, this.markerProps[oMarker.subtype]);
|
||||
oMarker.app.mount($Marker);
|
||||
|
||||
oMarker.marker = new Marker({element: $Marker, anchor: 'bottom', opacityWhenCovered: oMarker.opacityWhenCovered ?? 0})
|
||||
.setLngLat([oMarker.longitude, oMarker.latitude])
|
||||
.addTo(this.map);
|
||||
|
||||
const $MarkerElement = oMarker.marker.getElement();
|
||||
$MarkerElement.addEventListener('click', (oEvent) => {this.onMarkerClick(oEvent, oMarker);});
|
||||
$MarkerElement.addEventListener('mouseenter', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
|
||||
$MarkerElement.addEventListener('mouseleave', (oEvent) => {this.onMarkerHover(oEvent, oMarker);});
|
||||
},
|
||||
removeMarker(oMarker) {
|
||||
if(oMarker.app) {
|
||||
oMarker.app.unmount();
|
||||
delete oMarker.app;
|
||||
}
|
||||
if(oMarker.marker) {
|
||||
oMarker.marker.remove();
|
||||
delete oMarker.marker;
|
||||
}
|
||||
},
|
||||
onTrackHover(oEvent) {
|
||||
this.map.getCanvas().style.cursor = (oEvent.type == 'mouseenter')?'pointer':'';
|
||||
},
|
||||
onMarkerClick(oEvent, oMarker) {
|
||||
oEvent.preventDefault();
|
||||
oEvent.stopPropagation();
|
||||
switch (oMarker.type) {
|
||||
case 'project':
|
||||
this.hash.items = [oMarker.codename];
|
||||
break;
|
||||
default:
|
||||
this.openMarkerPopup(oMarker.id, oMarker.type);
|
||||
}
|
||||
},
|
||||
onMarkerHover(oEvent, oMarker) {
|
||||
switch (oMarker.type) {
|
||||
case 'project':
|
||||
if(oEvent.type == 'mouseenter') this.openProjectPopup(oMarker);
|
||||
else this.closePopup();
|
||||
break;
|
||||
}
|
||||
},
|
||||
openProjectPopup(oProject) {
|
||||
this.openPopup({
|
||||
lnglat: [oProject.longitude, oProject.latitude],
|
||||
options: oProject,
|
||||
offset: [0, -1 * this.markerHeight * this.getStyleProperty('--zoom-scale')]
|
||||
});
|
||||
},
|
||||
openMarkerPopup(iMarkerId, sMarkerType) {
|
||||
let oMarker = this.markers.find((oCandidate) => oCandidate.id == iMarkerId && oCandidate.type == sMarkerType);
|
||||
this.openPopup({
|
||||
lnglat: [oMarker.longitude, oMarker.latitude],
|
||||
options: oMarker,
|
||||
offset: [0, -1 * this.markerHeight * (this.isMobile?this.getStyleProperty('--zoom-scale'):1)]
|
||||
});
|
||||
},
|
||||
openTrackPopup(oEvent) {
|
||||
this.openPopup({
|
||||
lnglat: oEvent.lngLat,
|
||||
options: this.projects.getTrackInfo(oEvent.features[0], this.track, this.lang),
|
||||
});
|
||||
},
|
||||
openPopup({lnglat, options={}, offset=[0, 0]} = {}) {
|
||||
this.closePopup();
|
||||
const $Popup = document.createElement('div');
|
||||
this.popup.element = new Popup({
|
||||
anchor: 'bottom',
|
||||
offset: offset,
|
||||
closeButton: false
|
||||
})
|
||||
.setDOMContent($Popup)
|
||||
.setLngLat(lnglat)
|
||||
.addTo(this.map);
|
||||
|
||||
this.popup.content = createApp(ProjectPopup, {
|
||||
options: options,
|
||||
project: this.project,
|
||||
hikes: this.hikes
|
||||
});
|
||||
this.popup.content
|
||||
.provide('lang', this.lang)
|
||||
.provide('consts', this.consts)
|
||||
.provide('isMobile', this.isMobile)
|
||||
.mount($Popup);
|
||||
},
|
||||
closePopup() {
|
||||
if(this.popup.content) {
|
||||
this.popup.content.unmount();
|
||||
this.popup.content = null;
|
||||
}
|
||||
if(this.popup.element) {
|
||||
this.popup.element.remove();
|
||||
this.popup.element = null;
|
||||
}
|
||||
},
|
||||
setInitialProjectCamera() {
|
||||
let oHashMarker;
|
||||
if(this.hash.items.length == 3) {
|
||||
oHashMarker = this.markers.find((oMarker) => (
|
||||
oMarker.type == this.hash.items[1] &&
|
||||
oMarker.id == this.hash.items[2] &&
|
||||
oMarker.longitude != null &&
|
||||
oMarker.latitude != null
|
||||
)) || null;
|
||||
}
|
||||
|
||||
let oLastMarker = this.markers.at(-1);
|
||||
|
||||
//Overview map: Center on default project
|
||||
if(!this.project) {
|
||||
//Center on default project
|
||||
const oDefaultProject = this.projects.getDefaultProject();
|
||||
|
||||
//Get Map / Canvas size
|
||||
const $Canvas = this.map.getCanvas();
|
||||
const oMapBounds = this.map.getContainer().getBoundingClientRect();
|
||||
|
||||
//Adapt zoom to see whole planet
|
||||
const iTargetRadius = Math.max(1, Math.min(oMapBounds.width || $Canvas.clientWidth, oMapBounds.height || $Canvas.clientHeight) / 2);
|
||||
const iWorldSize = iTargetRadius * 2 * Math.PI * Math.cos(oDefaultProject.latitude * Math.PI / 180);
|
||||
|
||||
this.map.jumpTo({
|
||||
center: new LngLat(oDefaultProject.longitude, oDefaultProject.latitude),
|
||||
zoom: Math.log2(iWorldSize / 512),
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
});
|
||||
}
|
||||
//Direct link to marker
|
||||
else if(oHashMarker) {
|
||||
this.map.jumpTo({
|
||||
center: new LngLat(oHashMarker.longitude, oHashMarker.latitude),
|
||||
zoom: 13,
|
||||
pitch: this.initialPitch
|
||||
});
|
||||
}
|
||||
//Blog Mode: Fit to last marker
|
||||
else if(this.project.mode == this.consts.modes.blog && oLastMarker) {
|
||||
this.map.jumpTo({
|
||||
center: new LngLat(oLastMarker.longitude, oLastMarker.latitude),
|
||||
zoom: this.maxZoom,
|
||||
pitch: this.initialPitch,
|
||||
bearing: 0
|
||||
});
|
||||
}
|
||||
//Pre Mode, Histo Mode, Blog Mode without markers or missing direct link marker: Fit to track
|
||||
else {
|
||||
let oBounds = new LngLatBounds();
|
||||
const aoTrackCoordinates = [];
|
||||
for(const iFeatureId in this.track.features) {
|
||||
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
|
||||
(bounds, coord) => {
|
||||
aoTrackCoordinates.push(coord);
|
||||
return bounds.extend(coord);
|
||||
},
|
||||
oBounds
|
||||
);
|
||||
}
|
||||
|
||||
this.map.fitBounds(oBounds, {
|
||||
padding: this.mapPadding,
|
||||
animate: false,
|
||||
maxZoom: this.maxZoom,
|
||||
pitch: this.initialPitch,
|
||||
bearing: 0
|
||||
});
|
||||
|
||||
this.fixPitchedCameraCenter(aoTrackCoordinates);
|
||||
}
|
||||
},
|
||||
fixPitchedCameraCenter(aoTrackCoordinates) {
|
||||
//Project min/max coords (lat, lng) onto map rectangle corner points (x, y)
|
||||
const oScreenBounds = aoTrackCoordinates.reduce((oBounds, coord) => {
|
||||
const oPoint = this.map.project(coord);
|
||||
return {
|
||||
minX: Math.min(oBounds.minX, oPoint.x),
|
||||
minY: Math.min(oBounds.minY, oPoint.y),
|
||||
maxX: Math.max(oBounds.maxX, oPoint.x),
|
||||
maxY: Math.max(oBounds.maxY, oPoint.y)
|
||||
};
|
||||
}, {
|
||||
minX: Infinity,
|
||||
minY: Infinity,
|
||||
maxX: -Infinity,
|
||||
maxY: -Infinity
|
||||
});
|
||||
|
||||
//Current Rectangle center
|
||||
const oTrackCenter = {
|
||||
x: (oScreenBounds.minX + oScreenBounds.maxX) / 2,
|
||||
y: (oScreenBounds.minY + oScreenBounds.maxY) / 2
|
||||
};
|
||||
|
||||
//Convert back center point (x, y) to coords and Move map to the track center
|
||||
this.map.jumpTo({
|
||||
center: this.map.unproject([
|
||||
oTrackCenter.x,
|
||||
oTrackCenter.y
|
||||
])
|
||||
});
|
||||
},
|
||||
addNewMarkers(aoMarkers) { //FIXME Use its own marker update API
|
||||
this.markers.push(...aoMarkers);
|
||||
aoMarkers.forEach(this.addMarker);
|
||||
},
|
||||
panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
|
||||
return new Promise((resolve) => {
|
||||
if(!this.map) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.map.once('moveend', resolve);
|
||||
this.map.easeTo({
|
||||
center: oLngLat,
|
||||
zoom: iZoom,
|
||||
padding: this.getMapPadding(),
|
||||
duration: iAnimDuration
|
||||
});
|
||||
});
|
||||
},
|
||||
getMapPadding() {
|
||||
let bIsMobile = this.isMobile();
|
||||
return {
|
||||
top: this.mapPadding,
|
||||
bottom: this.mapPadding,
|
||||
left: this.mapPadding + ((!bIsMobile && this.panels.leftOpen && this.settings)?this.settings.getWidth():0),
|
||||
right: this.mapPadding + ((!bIsMobile && this.panels.rightOpen && this.feed)?this.feed.getWidth():0)
|
||||
};
|
||||
},
|
||||
updateMapPadding(iDuration=0) {
|
||||
const asPadding = this.getMapPadding();
|
||||
if(iDuration > 0) this.map.easeTo({padding: asPadding, duration: iDuration});
|
||||
else this.map.jumpTo({padding: asPadding});
|
||||
},
|
||||
getStyleProperty(sProperty) {
|
||||
return getComputedStyle(this.$el).getPropertyValue(sProperty).trim();
|
||||
},
|
||||
isMarkerVisible(oLngLat){
|
||||
return !!this.map && this.map.getBounds().contains(oLngLat);
|
||||
},
|
||||
onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
|
||||
const sPanelKey = sPanel + 'Open';
|
||||
let bOldValue = this.panels[sPanelKey];
|
||||
this.panels[sPanelKey] = bNewValue;
|
||||
|
||||
if(bOldValue != bNewValue) {
|
||||
//Adjust map center
|
||||
if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);
|
||||
|
||||
//Open Close panels
|
||||
this.$el.classList.toggle('with-'+sPanel+'-panel');
|
||||
}
|
||||
},
|
||||
setFeed(vPanel) {
|
||||
this.feed = vPanel;
|
||||
},
|
||||
setSettings(vPanel) {
|
||||
this.settings = vPanel;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="projects">
|
||||
<div id="space"></div>
|
||||
<div id="submap">
|
||||
<div class="loader">
|
||||
<AppIcon :icon="'map'" :classes="'flicker'" width="fixed" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<ProjectSettings
|
||||
:ref="setSettings"
|
||||
:projects="projectOptions"
|
||||
v-model:project-code-name="hash.items[0]"
|
||||
:base-maps="baseMaps"
|
||||
v-model:base-map="baseMap"
|
||||
v-model:terrain-enabled="terrainEnabled"
|
||||
:map-initializing="mapInitializing"
|
||||
:hikes="hikes"
|
||||
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('left', bIsOpen, iAnimDuration)"
|
||||
/>
|
||||
<ProjectFeed
|
||||
:ref="setFeed"
|
||||
:project="project"
|
||||
:mode-histo="modeHisto"
|
||||
@request-last-update="settings?.setLastUpdate"
|
||||
@new-markers="addNewMarkers"
|
||||
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('right', bIsOpen, iAnimDuration)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user