746 lines
23 KiB
Vue
746 lines
23 KiB
Vue
<script>
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
import { Map, NavigationControl, Marker, LngLatBounds, LngLat, Popup } from 'maplibre-gl';
|
|
import { createApp, ref, provide, inject } from 'vue';
|
|
import Simplebar from 'simplebar-vue';
|
|
|
|
import lightbox from '../scripts/lightbox.js';
|
|
import { getOuterWidth } from '../scripts/common.js';
|
|
|
|
import SpotIcon from './spotIcon.vue';
|
|
import SpotIconStack from './spotIconStack.vue';
|
|
import SpotButton from './spotButton.vue';
|
|
import ProjectPost from './projectPost.vue';
|
|
import ProjectPopup from './projectPopup.vue';
|
|
|
|
export default {
|
|
components: {
|
|
SpotIcon,
|
|
SpotButton,
|
|
ProjectPost,
|
|
Simplebar
|
|
},
|
|
data() {
|
|
return {
|
|
server: this.consts.server,
|
|
feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true},
|
|
refreshRate: 60,
|
|
lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''},
|
|
feedPanelOpen: false,
|
|
settingsPanelOpen: false,
|
|
markers: {messages: null, medias: null},
|
|
markerSize: {width: 32, height: 32},
|
|
currProject: {},
|
|
modeHisto: false,
|
|
posts: [],
|
|
nlFeedbacks: [],
|
|
nlLoading: false,
|
|
baseMaps: {},
|
|
baseMap: null,
|
|
map: null,
|
|
hikes: {
|
|
colors:{'main':'#00ff78', 'off-track':'#0000ff', 'hitchhiking':'#FF7814'},
|
|
width: 4
|
|
},
|
|
popup: {content: null, element: null}
|
|
};
|
|
},
|
|
computed: {
|
|
projectClasses() {
|
|
return [
|
|
this.feedPanelOpen?'with-feed':'',
|
|
this.settingsPanelOpen?'with-settings':''
|
|
].filter(n => n).join(' ');
|
|
},
|
|
nlClasses() {
|
|
return [
|
|
this.nlAction,
|
|
this.nlLoading?'loading':''
|
|
].filter(n => n).join(' ');
|
|
},
|
|
subscribed() {
|
|
return this.user.id_user > 0;
|
|
},
|
|
nlAction() {
|
|
return this.subscribed?'unsubscribe':'subscribe';
|
|
}
|
|
},
|
|
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');
|
|
}
|
|
},
|
|
'hash.items.0'(newProjectCodename, oldProjectCodename) {
|
|
if(newProjectCodename && newProjectCodename != oldProjectCodename) {
|
|
this.hash.items = [newProjectCodename];
|
|
this.init();
|
|
}
|
|
}
|
|
},
|
|
provide() {
|
|
return {
|
|
map: {
|
|
panToBetweenPanels: this.panToBetweenPanels,
|
|
openMarkerPopup: this.openMarkerPopup,
|
|
closeMarkerPopup: this.closeMarkerPopup,
|
|
isMarkerVisible: this.isMarkerVisible
|
|
},
|
|
project: this
|
|
};
|
|
},
|
|
inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'],
|
|
beforeMount() {
|
|
if(this.hash.items.length == 0) this.hash.items[0] = this.projects.getDefaultCodeName();
|
|
},
|
|
mounted() {
|
|
window.addEventListener('resize', this.handleResize);
|
|
this.init();
|
|
},
|
|
beforeUnmount() {
|
|
window.removeEventListener('resize', this.handleResize);
|
|
this.quit();
|
|
},
|
|
methods: {
|
|
handleResize() {
|
|
//this.spot.tmp('map_offset', -1 * (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0) / getOuterWidth(window));
|
|
|
|
/* TODO
|
|
if(typeof this.spot.tmp('elev') != 'undefined' && this.spot.tmp('elev')._showState) {
|
|
this.spot.tmp('elev').resize({width:this.getElevWidth()});
|
|
}
|
|
*/
|
|
},
|
|
async init() {
|
|
let bFirstLoad = (typeof this.currProject.codename == 'undefined');
|
|
this.initProject();
|
|
if(bFirstLoad) this.initLightbox();
|
|
|
|
await Promise.all([
|
|
this.initFeed(),
|
|
this.initMap()
|
|
]);
|
|
|
|
//Direct link: Scroll to post
|
|
if(this.hash.items.length == 3) this.findPost({type: this.hash.items[1], id: this.hash.items[2]});
|
|
},
|
|
quit() {
|
|
lightbox.end();
|
|
this.$refs.feedSimpleBar.scrollElement.removeEventListener('scroll', this.onFeedScroll);
|
|
this.setFeedUpdateTimer(-1);
|
|
this.map.remove();
|
|
},
|
|
getNaturalDuration(iHours) {
|
|
var iTimeMinutes = 0, iTimeHours = 0, iTimeDays = Math.floor(iHours/8); //8 hours a day
|
|
if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day
|
|
else {
|
|
iTimeDays = 0;
|
|
iTimeHours = Math.floor(iHours);
|
|
iHours -= iTimeHours;
|
|
|
|
iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes
|
|
}
|
|
return '~ '
|
|
+(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+this.lang.get(iTimeDays>1?'unit.days':'unit.day')):'') //Days
|
|
+((iTimeHours>0 || iTimeDays==0)?iTimeHours+this.lang.get('unit.hour'):'') //Hours
|
|
+((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes); //Minutes
|
|
|
|
},
|
|
initProject() {
|
|
this.currProject = this.projects[this.hash.items[0]];
|
|
this.modeHisto = (this.currProject.mode == this.consts.modes.histo);
|
|
this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
|
|
this.posts = [];
|
|
//this.baseMap = null;
|
|
this.baseMaps = {};
|
|
},
|
|
initLightbox() {
|
|
lightbox.option({
|
|
alwaysShowNavOnTouchDevices: true,
|
|
albumLabel: 'Media %1 / %2',
|
|
fadeDuration: 300,
|
|
imageFadeDuration: 400,
|
|
positionFromTop: 0,
|
|
resizeDuration: 400,
|
|
hasVideo: true,
|
|
onMediaChange: (oMedia) => {
|
|
this.hash.items = [this.currProject.codename, 'media', oMedia.id];
|
|
if(oMedia.set == 'post-medias') this.goToPost({type: 'media', id: oMedia.id});
|
|
},
|
|
onClosing: () => {this.hash.items = [this.hash.items[0]];}
|
|
});
|
|
},
|
|
async initFeed() {
|
|
//Simplebar event
|
|
this.$refs.feedSimpleBar?.scrollElement.addEventListener('scroll', this.onFeedScroll);
|
|
|
|
//Mobile Touchscreen Events
|
|
//TODO
|
|
|
|
//Add post Event handling
|
|
//TODO
|
|
|
|
//Get first posts batch
|
|
await this.getNextFeed();
|
|
this.$refs.feedSimpleBar.scrollElement.scrollTop = 0;
|
|
|
|
//Start auto-update
|
|
if(!this.modeHisto) this.setFeedUpdateTimer(this.refreshRate);
|
|
},
|
|
async initMap() {
|
|
//Start async calls
|
|
const oMarkersPromise = this.api.get('markers', {id_project: this.currProject.id});
|
|
const oTrackPromise = this.api.get('geojson', {id_project: this.currProject.id});
|
|
|
|
//Build Map
|
|
if(this.map) this.map.remove();
|
|
this.map = new Map({
|
|
container: 'map',
|
|
style: {
|
|
version: 8,
|
|
sources: {},
|
|
layers: []
|
|
},
|
|
attributionControl: false
|
|
});
|
|
|
|
//Parse Map Info
|
|
const aoMarkers = await oMarkersPromise;
|
|
this.baseMaps = aoMarkers.maps;
|
|
this.baseMap = this.baseMaps.find((asBM) => asBM.default_map)?.codename ?? null;
|
|
this.markers.messages = aoMarkers.messages;
|
|
this.markers.medias = aoMarkers.medias;
|
|
this.lastUpdate = aoMarkers.last_update;
|
|
|
|
//Force wait for load event
|
|
await new Promise((resolve) => {
|
|
if(this.map.loaded()) resolve();
|
|
else this.map.once('load', resolve);
|
|
});
|
|
|
|
//Base maps (raster tiles)
|
|
for(const asBaseMap of this.baseMaps) {
|
|
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.codename == this.baseMap ? 'visible' : 'none'},
|
|
minZoom: asBaseMap.min_zoom,
|
|
maxZoom: asBaseMap.max_zoom
|
|
});
|
|
}
|
|
|
|
//Add track
|
|
const oTrack = await oTrackPromise;
|
|
this.addTrack(oTrack);
|
|
|
|
//Centering map
|
|
await this.positionMap(oTrack);
|
|
|
|
//Add Markers
|
|
this.addMarkers();
|
|
|
|
//Force wait for idle event
|
|
await new Promise((resolve) => {
|
|
this.map.once('idle', resolve);
|
|
});
|
|
},
|
|
addTrack(oTrack) {
|
|
this.map.addSource('track', {
|
|
'type': 'geojson',
|
|
'data': oTrack
|
|
});
|
|
|
|
//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
|
|
}
|
|
});
|
|
},
|
|
async addMarkers() {
|
|
this.map.addImage('markerIcon', (await this.map.loadImage('images/footprint_mapbox.png')).data);
|
|
this.map.addSource('markers', {
|
|
type:'geojson',
|
|
data: {
|
|
type: 'FeatureCollection',
|
|
features: this.convertMsgToFeatures(this.markers.messages)
|
|
}
|
|
});
|
|
this.map.addLayer({
|
|
'id': 'markers',
|
|
'type': 'symbol',
|
|
'source': 'markers',
|
|
'layout': {'icon-image': 'markerIcon'}
|
|
});
|
|
this.map.on('click', 'markers', (e) => {
|
|
this.openMarkerPopup(e.features[0]);
|
|
});
|
|
|
|
/*
|
|
this.markers.messages.forEach(msg => {
|
|
const el = document.createElement('div');
|
|
|
|
const app = createApp(SpotIconStack, {iconMain: 'message', iconSub:'message-in', iconSubRotation: 270});
|
|
app.mount(el);
|
|
|
|
new Marker({element: el, anchor: 'bottom'})
|
|
.setLngLat([msg.longitude, msg.latitude])
|
|
.addTo(this.map);
|
|
});
|
|
*/
|
|
|
|
//Medias
|
|
//TODO Use same way of displaying markers (so that openMarkerPopup works on all markers)
|
|
this.markers.medias.forEach(msg => {
|
|
const $Marker = document.createElement('div');
|
|
createApp(SpotIconStack, {mainClasses: 'media', iconMain: 'marker', iconSub: msg.subtype}).mount($Marker);
|
|
|
|
const marker = new Marker({element: $Marker, anchor: 'bottom'})
|
|
.setLngLat([msg.longitude, msg.latitude])
|
|
.addTo(this.map)
|
|
.getElement().addEventListener('click', (oEvent) => {
|
|
oEvent.preventDefault();
|
|
oEvent.stopPropagation();
|
|
this.openMediaPopup(msg);
|
|
});
|
|
});
|
|
},
|
|
async positionMap(oTrack) {
|
|
let bOpenFeedPanel = !this.isMobile();
|
|
let oBounds = new LngLatBounds();
|
|
|
|
if( //Blog Mode: Fit to last message
|
|
this.currProject.mode == this.consts.modes.blog &&
|
|
this.markers.messages.length > 0 &&
|
|
this.hash.items[2] != 'message'
|
|
) {
|
|
|
|
let oLastMsg = this.markers.messages[this.markers.messages.length - 1];
|
|
oBounds.extend(new LngLat(oLastMsg.longitude, oLastMsg.latitude));
|
|
}
|
|
else { //Pre/Histo Mode: Fit to track
|
|
|
|
for(const iFeatureId in oTrack.features) {
|
|
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
|
|
(bounds, coord) => {
|
|
return bounds.extend(coord);
|
|
},
|
|
oBounds
|
|
);
|
|
}
|
|
}
|
|
|
|
const iFeedPanelPadding = bOpenFeedPanel?(getOuterWidth(this.$refs.feed)/2):0;
|
|
await this.map.fitBounds(
|
|
oBounds,
|
|
{
|
|
padding: {
|
|
top: 20,
|
|
bottom: 20,
|
|
left: (20 + iFeedPanelPadding),
|
|
right: (20 + iFeedPanelPadding)
|
|
},
|
|
animate: false,
|
|
maxZoom: 15
|
|
}
|
|
);
|
|
|
|
//Toggle only when map is ready, for the tilt effet
|
|
this.toggleFeedPanel(bOpenFeedPanel);
|
|
},
|
|
convertMsgToFeatures(oMsg) {
|
|
return oMsg.map(oMsg => ({
|
|
'type': 'Feature',
|
|
'geometry': {
|
|
'type': 'Point',
|
|
'coordinates': [oMsg.longitude, oMsg.latitude]
|
|
},
|
|
'properties': {
|
|
...oMsg,
|
|
...{'description': ''}
|
|
}
|
|
}));
|
|
},
|
|
openMediaPopup(oMedia) {
|
|
this.closeMarkerPopup();
|
|
|
|
const $Popup = document.createElement('div');
|
|
this.popup.element = new Popup({
|
|
anchor: 'bottom',
|
|
offset: [0, this.markerSize.height * -1],
|
|
closeButton: false
|
|
})
|
|
.setDOMContent($Popup)
|
|
.setLngLat([oMedia.longitude, oMedia.latitude])
|
|
.setMaxWidth(300)
|
|
.addTo(this.map);
|
|
|
|
this.popup.content = createApp(ProjectPopup, {
|
|
type: 'media',
|
|
options: oMedia,
|
|
medias: [oMedia],
|
|
project: this.currProject
|
|
});
|
|
this.popup.content
|
|
.provide('spot', this.spot)
|
|
.provide('lang', this.lang)
|
|
.provide('consts', this.consts)
|
|
.mount($Popup);
|
|
},
|
|
openMarkerPopup(oFeature) {
|
|
this.closeMarkerPopup();
|
|
|
|
//Convert ID Message to feature
|
|
if(typeof oFeature == 'number') {
|
|
const oMatchingFeatures = this.map.querySourceFeatures('markers', {
|
|
filter: ['==', ['get', 'id_message'], oFeature]
|
|
});
|
|
|
|
if(!oMatchingFeatures.length) {
|
|
console.warn('Marker not found: ', oFeature);
|
|
return;
|
|
}
|
|
else oFeature = oMatchingFeatures[0];
|
|
}
|
|
|
|
const $Popup = document.createElement('div');
|
|
this.popup.element = new Popup({
|
|
anchor: 'bottom',
|
|
offset: [0, this.markerSize.height * -1],
|
|
closeButton: false
|
|
})
|
|
.setDOMContent($Popup)
|
|
.setLngLat(oFeature.geometry.coordinates)
|
|
.setMaxWidth(300)
|
|
.addTo(this.map);
|
|
|
|
this.popup.content = createApp(ProjectPopup, {
|
|
type: 'message',
|
|
options: oFeature.properties,
|
|
medias: JSON.parse(oFeature.properties.medias || '[]'),
|
|
project: this.currProject
|
|
});
|
|
this.popup.content
|
|
.provide('spot', this.spot)
|
|
.provide('lang', this.lang)
|
|
.provide('consts', this.consts)
|
|
.mount($Popup);
|
|
},
|
|
closeMarkerPopup() {
|
|
if(this.popup.content) {
|
|
this.popup.content.unmount();
|
|
this.popup.content = null;
|
|
}
|
|
if(this.popup.element) {
|
|
this.popup.element.remove();
|
|
this.popup.element = null;
|
|
}
|
|
},
|
|
async getNextFeed() {
|
|
if(!this.feed.outOfData && !this.feed.loading) {
|
|
//Get next chunk
|
|
this.feed.loading = true;
|
|
let aoData = await this.api.get('next_feed', {id_project: this.currProject.id, id: this.feed.refIdLast});
|
|
let iPostCount = Object.keys(aoData.feed).length;
|
|
|
|
//Update pointers
|
|
this.feed.outOfData = (iPostCount < this.consts.chunk_size);
|
|
if(iPostCount > 0) {
|
|
this.feed.refIdLast = aoData.ref_id_last;
|
|
if(this.feed.firstChunk) this.feed.refIdFirst = aoData.ref_id_first;
|
|
}
|
|
|
|
//Add posts
|
|
this.posts.push(...aoData.feed);
|
|
|
|
this.feed.loading = false;
|
|
this.feed.firstChunk = false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
onFeedScroll(oEvent) {
|
|
const box = oEvent.currentTarget
|
|
const content = box.querySelector('.simplebar-content')
|
|
|
|
if ((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
|
|
},
|
|
setFeedUpdateTimer(iSeconds) {
|
|
if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
|
|
if(iSeconds >= 0) this.feedTimer = setTimeout(this.checkNewFeed, iSeconds * 1000);
|
|
},
|
|
async checkNewFeed() {
|
|
let aoData = await this.api.get('new_feed', {id_project: this.currProject.id, id: this.feed.refIdFirst});
|
|
|
|
if(Object.keys(aoData.feed).length > 0) {
|
|
//Update pointer
|
|
this.feed.refIdFirst = aoData.ref_id_first;
|
|
|
|
//Add new posts
|
|
this.posts.unshift(...aoData.feed);
|
|
}
|
|
|
|
//Add new Markers
|
|
if(Object.keys(aoData.messages).length > 0) {
|
|
const oMarkerSource = this.map.getSource('markers');
|
|
oMarkerSource.setData({
|
|
type: 'FeatureCollection',
|
|
features: [...oMarkerSource._data.features, ...this.convertMsgToFeatures(aoData.messages)]
|
|
});
|
|
}
|
|
|
|
//TODO medias
|
|
|
|
//Message Last Update
|
|
this.lastUpdate = aoData.last_update;
|
|
|
|
//Reschedule
|
|
this.setFeedUpdateTimer(this.refreshRate);
|
|
},
|
|
panToBetweenPanels(oLngLat, iZoom, fCallback) {
|
|
const iXOffset = (this.settingsPanelOpen?getOuterWidth(this.$refs.settings):0) - (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0);
|
|
|
|
this.map.once('moveend', fCallback);
|
|
this.map.easeTo({
|
|
center: oLngLat,
|
|
zoom: iZoom,
|
|
offset: [iXOffset / 2, 0],
|
|
duration: 500
|
|
});
|
|
},
|
|
isMarkerVisible(oLngLat){
|
|
return this.map.getBounds().contains(oLngLat);
|
|
},
|
|
getGoogleMapsLink(asInfo) {
|
|
return $('<a>', {
|
|
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
|
|
title: this.lang.get('map.see_on_google'),
|
|
target: '_blank',
|
|
rel: 'noreferrer noopener'
|
|
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
|
|
},
|
|
async manageSubs() {
|
|
var regexEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
if(!regexEmail.test(this.user.email)) this.nlFeedbacks.push({type:'error', 'msg':this.lang.get('newsletter.invalid_email')});
|
|
else {
|
|
this.api.get(this.nlAction, {'email': this.user.email, 'name': this.user.name})
|
|
.then((asUser, sDesc) => {
|
|
this.nlFeedbacks.push('success', sDesc);
|
|
Object.assign(this.user, asUser);
|
|
})
|
|
.catch((sDesc) => {this.nlFeedbacks.push('error', sDesc);});
|
|
}
|
|
},
|
|
toggleFeedPanel(bShow, sMapAction) {
|
|
let bOldValue = this.feedPanelOpen;
|
|
this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow;
|
|
|
|
if(bOldValue != this.feedPanelOpen && !this.isMobile()) {
|
|
this.handleResize();
|
|
|
|
sMapAction = sMapAction || 'panTo';
|
|
switch(sMapAction) {
|
|
case 'none':
|
|
break;
|
|
case 'panTo':
|
|
this.map.panBy(
|
|
[(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0],
|
|
{duration: 500}
|
|
);
|
|
break;
|
|
case 'panToInstant':
|
|
this.map.panBy([(this.feedPanelOpen?1:-1) * getOuterWidth(this.$refs.feed) / 2, 0]);
|
|
break;
|
|
case 'fitBounds':
|
|
/*
|
|
this.map.fitBounds(
|
|
this.spot.tmp('track').getBounds(),
|
|
{
|
|
paddingTopLeft: L.point(5, this.spot.tmp('marker_size').height + 5),
|
|
paddingBottomRight: L.point(this.spot.tmp('$Feed').outerWidth(true) + 5, 5)
|
|
}
|
|
);
|
|
break;
|
|
*/
|
|
}
|
|
}
|
|
},
|
|
toggleSettingsPanel(bShow, sMapAction) {
|
|
let bOldValue = this.settingsPanelOpen;
|
|
this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow;
|
|
|
|
if(bOldValue != this.settingsPanelOpen && !this.isMobile()) {
|
|
this.handleResize();
|
|
|
|
sMapAction = sMapAction || 'panTo';
|
|
switch(sMapAction) {
|
|
case 'none':
|
|
break;
|
|
case 'panTo':
|
|
this.map.panBy(
|
|
[(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) / 2, 0],
|
|
{duration: 500}
|
|
);
|
|
break;
|
|
case 'panToInstant':
|
|
this.map.panBy([(this.settingsPanelOpen?-1:1) * getOuterWidth(this.$refs.settings) /2, 0]);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
async findPost(oPost) {
|
|
let oRef = this.goToPost(oPost);
|
|
if(oRef) {
|
|
oRef.executeMainAction();
|
|
}
|
|
else if(!this.feed.outOfData) {
|
|
await this.getNextFeed();
|
|
this.findPost(oPost);
|
|
}
|
|
else console.log('Missing element ID "'+oPost.id+'" of type "'+oPost.type+'"');
|
|
},
|
|
goToPost(oPost) {
|
|
let bFound = false;
|
|
let aoRefs = this.$refs.posts.filter((post) => {return post.postId == oPost.type+'-'+oPost.id;});
|
|
if(aoRefs.length == 1) {
|
|
let oRef = aoRefs[0];
|
|
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
|
|
oRef.$el.getBoundingClientRect().top
|
|
+ window.pageYOffset
|
|
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
|
|
);
|
|
|
|
//this.hash.items = [this.hash.items[0]];
|
|
|
|
return oRef;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div id="projects" :class="projectClasses">
|
|
<div id="background"></div>
|
|
<div id="submap">
|
|
<div class="loader">
|
|
<SpotIcon :icon="'map'" :classes="'flicker'" fixed-width />
|
|
</div>
|
|
</div>
|
|
<div id="map"></div>
|
|
<div id="settings" class="map-container map-container-left" ref="settings">
|
|
<div id="settings-panel" class="map-panel">
|
|
<div class="settings-header">
|
|
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
|
|
<div id="last_update" v-if="this.currProject.mode == this.consts.modes.blog && lastUpdate.unix_time > 0">
|
|
<p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr :title="lastUpdate.formatted_time">{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}</abbr></p>
|
|
</div>
|
|
</div>
|
|
<div class="settings-sections">
|
|
<Simplebar id="settings-sections-scrollbox">
|
|
<div class="settings-section">
|
|
<h1><SpotIcon :icon="'project'" fixed-width :text="lang.get('project.hikes')" /></h1>
|
|
<div class="settings-section-body">
|
|
<div class="radio" v-for="project in projects" :key="'project-'+project.id">
|
|
<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="$parent.hash.items[0]" />
|
|
<label :for="'project-'+project.id">
|
|
<span>{{ project.name }}</span>
|
|
<a class="download" :href="project.gpxfilepath" :download="project.codename + '.gpx'" :title="lang.get('track.download')" @click.stop="()=>{}">
|
|
<SpotIcon :icon="'download'" margin="left" />
|
|
</a>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-section">
|
|
<h1><SpotIcon :icon="'map'" fixed-width :text="lang.get('map.title')" /></h1>
|
|
<div class="settings-section-body">
|
|
<div class="radio" v-for="bm in baseMaps" :key="'map-'+bm.id_map">
|
|
<input type="radio" :id="'map-'+bm.id_map" :value="bm.codename" v-model="baseMap" />
|
|
<label :for="'map-'+bm.id_map">{{ lang.get('map.'+bm.codename) }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-section newsletter">
|
|
<h1><SpotIcon :icon="'newsletter'" fixed-width :text="lang.get('newsletter.title')" /></h1>
|
|
<div class="newsletter-form">
|
|
<input type="email" name="email" id="email" :placeholder="lang.get('newsletter.email_placeholder')" v-model="user.email" :disabled="nlLoading || subscribed" />
|
|
<SpotButton id="nl_btn" :classes="nlClasses" :title="lang.get('newsletter.'+nlAction)" :icon="nlAction" @click="manageSubs" />
|
|
</div>
|
|
<div id="settings-feedback" class="feedback">
|
|
<p v-for="feedback in nlFeedbacks" :class="feedback.type">
|
|
<SpotIcon :icon="feedback.type" :text="feedback.msg" />
|
|
</p>
|
|
</div>
|
|
{{ lang.get('newsletter.'+(subscribed?'subscribed':'unsubscribed')+'_desc') }}
|
|
</div>
|
|
<div class="settings-section admin" v-if="user.hasClearance(consts.clearances.admin)">
|
|
<h1><SpotIcon :icon="'admin'" fixed-width :text="lang.get('admin.title')" /></h1>
|
|
<div class="admin-actions">
|
|
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="lang.get('admin.config')" /></a>
|
|
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="lang.get('admin.upload')" /></a>
|
|
</div>
|
|
</div>
|
|
</Simplebar>
|
|
</div>
|
|
<div class="settings-footer">
|
|
<a href="https://git.lutran.fr/franzz/spot" :title="lang.get('credits.git')" target="_blank" rel="noopener">
|
|
<SpotIcon :icon="'credits'" :text="lang.get('credits.project')" />
|
|
</a> {{ lang.get('credits.license') }}</div>
|
|
</div>
|
|
<div :class="'map-control map-control-icon settings-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleSettingsPanel">
|
|
<SpotIcon :icon="settingsPanelOpen?'prev':'menu'" />
|
|
</div>
|
|
<div v-if="!isMobile()" id="legend" class="map-control settings-control map-control-bottom">
|
|
<div v-for="(color, hikeType) in hikes.colors" class="track">
|
|
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
|
|
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
|
|
</div>
|
|
</div>
|
|
<div id="title" :class="'map-control settings-control map-control-'+(isMobile()?'bottom':'top')">
|
|
<span>{{ currProject.name }}</span>
|
|
</div>
|
|
</div>
|
|
<div id="feed" class="map-container map-container-right" ref="feed">
|
|
<Simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
|
|
<div id="feed-header">
|
|
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
|
|
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
|
|
</div>
|
|
<div id="feed-posts">
|
|
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
|
|
</div>
|
|
<div id="feed-footer" v-if="feed.loading">
|
|
<ProjectPost :options="{type: 'loading', headerless: true}" />
|
|
</div>
|
|
</Simplebar>
|
|
<div :class="'map-control map-control-icon feed-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleFeedPanel">
|
|
<SpotIcon :icon="feedPanelOpen?'next':'post'" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|