mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-03 08:05:33 +00:00
959 lines
28 KiB
Vue
959 lines
28 KiB
Vue
<!-- Example Reference: https://codesandbox.io/s/v-mapbox-with-nuxt-lbrt6?file=/pages/index.vue -->
|
|
<template>
|
|
<div class="map-page">
|
|
<client-only v-if="!isEmpty($env.MAPBOX_TOKEN)">
|
|
<mgl-map
|
|
:mapbox-gl="mapboxgl"
|
|
:access-token="mapOptions.accessToken"
|
|
:map-style.sync="mapOptions.style"
|
|
:center="mapOptions.center"
|
|
:zoom="mapOptions.zoom"
|
|
:max-zoom="mapOptions.maxZoom"
|
|
:cross-source-collisions="false"
|
|
:fail-if-major-performance-caveat="false"
|
|
:preserve-drawing-buffer="true"
|
|
:hash="false"
|
|
:min-pitch="0"
|
|
:max-pitch="60"
|
|
@load="onMapLoad"
|
|
>
|
|
<MglFullscreenControl />
|
|
<MglNavigationControl position="top-right" />
|
|
<MglGeolocateControl position="top-right" />
|
|
<MglScaleControl />
|
|
<div class="map-legend" :class="{ 'map-legend--open': legendOpen }">
|
|
<button
|
|
class="map-legend-toggle"
|
|
:aria-expanded="String(legendOpen)"
|
|
aria-controls="map-legend-content"
|
|
@click="legendOpen = !legendOpen"
|
|
>
|
|
{{ $t('map.legend.title') }}
|
|
<span class="map-legend-arrow" aria-hidden="true">{{ legendOpen ? '▼' : '▲' }}</span>
|
|
</button>
|
|
<div
|
|
id="map-legend-content"
|
|
v-show="legendOpen || !isMobile"
|
|
class="map-legend-content"
|
|
role="region"
|
|
:aria-label="$t('map.legend.title')"
|
|
>
|
|
<div v-for="type in markers.types" :key="type.id" class="map-legend-item">
|
|
<img
|
|
:alt="$t('map.legend.' + type.id)"
|
|
:src="'/img/mapbox/marker-icons/' + type.icon.legendName"
|
|
width="15"
|
|
/>
|
|
{{ $t('map.legend.' + type.id) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</mgl-map>
|
|
</client-only>
|
|
<empty v-else icon="alert" :message="$t('map.alertMessage')" />
|
|
</div>
|
|
</template>
|
|
|
|
<!-- eslint-disable vue/no-reserved-component-names -->
|
|
<script>
|
|
import { isEmpty } from 'lodash'
|
|
import mapboxgl from 'mapbox-gl'
|
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
|
import { mapGetters } from 'vuex'
|
|
import { profileUserQuery } from '~/graphql/User'
|
|
import { mapQuery } from '~/graphql/MapQuery'
|
|
import mobile from '~/mixins/mobile'
|
|
import Empty from '~/components/Empty/Empty'
|
|
|
|
const maxMobileWidth = 639 // on this width and smaller the mapbox 'MapboxGeocoder' search gets bigger
|
|
|
|
export default {
|
|
name: 'Map',
|
|
mixins: [mobile(maxMobileWidth)],
|
|
components: {
|
|
Empty,
|
|
},
|
|
head() {
|
|
return {
|
|
title: this.$t('map.pageTitle'),
|
|
}
|
|
},
|
|
data() {
|
|
mapboxgl.accessToken = this.$env.MAPBOX_TOKEN
|
|
return {
|
|
isEmpty,
|
|
mapboxgl,
|
|
legendOpen: false,
|
|
activeStyle: null,
|
|
defaultCenter: [10.452764, 51.165707], // center of Germany: https://www.gpskoordinaten.de/karte/land/DE
|
|
currentUserLocation: null,
|
|
currentUserCoordinates: null,
|
|
users: null,
|
|
groups: null,
|
|
posts: null,
|
|
markers: {
|
|
types: [
|
|
{
|
|
id: 'theUser',
|
|
icon: {
|
|
id: 'marker-orange',
|
|
legendName: 'mapbox-marker-icon-orange.svg',
|
|
mapName: 'mapbox-marker-icon-20px-orange.png',
|
|
},
|
|
},
|
|
{
|
|
id: 'user',
|
|
icon: {
|
|
id: 'marker-green',
|
|
legendName: 'mapbox-marker-icon-green.svg',
|
|
mapName: 'mapbox-marker-icon-20px-green.png',
|
|
},
|
|
},
|
|
{
|
|
id: 'group',
|
|
icon: {
|
|
id: 'marker-red',
|
|
legendName: 'mapbox-marker-icon-red.svg',
|
|
mapName: 'mapbox-marker-icon-20px-red.png',
|
|
},
|
|
},
|
|
{
|
|
id: 'event',
|
|
icon: {
|
|
id: 'marker-purple',
|
|
legendName: 'mapbox-marker-icon-purple.svg',
|
|
mapName: 'mapbox-marker-icon-20px-purple.png',
|
|
},
|
|
},
|
|
],
|
|
isImagesLoaded: false,
|
|
geoJSON: [],
|
|
isGeoJSON: false,
|
|
isSourceAndLayerAdded: false,
|
|
isFlyToCenter: false,
|
|
popup: null,
|
|
popupOnLeaveTimeoutId: null,
|
|
},
|
|
}
|
|
},
|
|
async mounted() {
|
|
this.updateMapPosition()
|
|
window.addEventListener('resize', this.updateMapPosition)
|
|
|
|
this.currentUserLocation = await this.getUserLocation(this.currentUser.id)
|
|
this.currentUserCoordinates = this.currentUserLocation
|
|
? this.getCoordinates(this.currentUserLocation)
|
|
: null
|
|
this.addMarkersOnCheckPrepared()
|
|
},
|
|
beforeDestroy() {
|
|
window.removeEventListener('resize', this.updateMapPosition)
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
currentUser: 'auth/user',
|
|
}),
|
|
isPreparedForMarkers() {
|
|
return (
|
|
!this.markers.isGeoJSON &&
|
|
this.markers.isImagesLoaded &&
|
|
this.currentUser &&
|
|
this.users &&
|
|
this.groups &&
|
|
this.posts
|
|
)
|
|
},
|
|
availableStyles() {
|
|
// https://docs.mapbox.com/api/maps/styles/
|
|
const availableStyles = {
|
|
outdoors: {
|
|
url: 'mapbox://styles/mapbox/outdoors-v12?optimize=true',
|
|
},
|
|
streets: {
|
|
url: 'mapbox://styles/mapbox/streets-v11?optimize=true',
|
|
// use the newest version?
|
|
// url: 'mapbox://styles/mapbox/streets-v12',
|
|
},
|
|
satellite: {
|
|
url: 'mapbox://styles/mapbox/satellite-streets-v11?optimize=true',
|
|
},
|
|
dark: {
|
|
url: 'mapbox://styles/mapbox/dark-v10?optimize=true',
|
|
},
|
|
}
|
|
Object.keys(availableStyles).forEach((key) => {
|
|
availableStyles[key].title = this.$t('map.styles.' + key)
|
|
})
|
|
return availableStyles
|
|
},
|
|
mapOptions() {
|
|
return {
|
|
// accessToken: this.$env.MAPBOX_TOKEN, // is set already above
|
|
style: !this.activeStyle ? this.availableStyles.outdoors.url : this.activeStyle,
|
|
center: this.mapCenter,
|
|
zoom: this.mapZoom,
|
|
maxZoom: 22,
|
|
// projection: 'globe', // the package is probably to old, because of Vue2: https://docs.mapbox.com/mapbox-gl-js/example/globe/
|
|
}
|
|
},
|
|
mapCenter() {
|
|
return this.currentUserCoordinates ? this.currentUserCoordinates : this.defaultCenter
|
|
},
|
|
mapZoom() {
|
|
return this.currentUserCoordinates ? 10 : 4
|
|
},
|
|
},
|
|
watch: {
|
|
isPreparedForMarkers(newValue) {
|
|
if (newValue) {
|
|
this.addMarkersOnCheckPrepared()
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
updateMapPosition() {
|
|
const navbar = document.getElementById('navbar')
|
|
const footer = document.getElementById('footer')
|
|
const el = this.$el
|
|
if (navbar) {
|
|
el.style.top = navbar.offsetHeight + 'px'
|
|
}
|
|
if (footer && window.getComputedStyle(footer).display !== 'none') {
|
|
el.style.bottom = footer.offsetHeight + 'px'
|
|
} else {
|
|
el.style.bottom = '0px'
|
|
}
|
|
},
|
|
onMapLoad({ map }) {
|
|
this.map = map
|
|
|
|
// set the default atmosphere style
|
|
// this.map.setFog({}) // the package is probably to old, because of Vue2: https://docs.mapbox.com/mapbox-gl-js/example/globe/
|
|
|
|
this.map.on('style.load', (value) => {
|
|
// Triggered when `setStyle` is called.
|
|
this.markers.isImagesLoaded = false
|
|
this.markers.isSourceAndLayerAdded = false
|
|
this.loadMarkersIconsAndAddMarkers()
|
|
})
|
|
|
|
// add search field for locations
|
|
this.map.addControl(
|
|
new MapboxGeocoder({
|
|
accessToken: this.$env.MAPBOX_TOKEN,
|
|
mapboxgl: this.mapboxgl,
|
|
marker: false,
|
|
collapsed: window.innerWidth <= 810,
|
|
}),
|
|
'top-right',
|
|
)
|
|
|
|
// add style switcher control
|
|
let closePopoverHandler = null
|
|
const styleSwitcher = {
|
|
onAdd: () => {
|
|
const container = document.createElement('div')
|
|
container.className = 'mapboxgl-ctrl map-style-switcher'
|
|
|
|
// Icon button (layers icon as SVG)
|
|
const styleLabel = this.$t('map.styles.title') || 'Map style'
|
|
const toggle = document.createElement('button')
|
|
toggle.type = 'button'
|
|
toggle.className = 'map-style-switcher-toggle'
|
|
toggle.title = styleLabel
|
|
toggle.setAttribute('aria-label', styleLabel)
|
|
toggle.setAttribute('aria-expanded', 'false')
|
|
toggle.innerHTML =
|
|
'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true">' +
|
|
'<path d="M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27-7.38 5.74zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"/>' +
|
|
'</svg>'
|
|
toggle.addEventListener('click', (e) => {
|
|
e.stopPropagation()
|
|
const isOpen = popover.classList.toggle('map-style-popover--open')
|
|
toggle.setAttribute('aria-expanded', String(isOpen))
|
|
})
|
|
container.appendChild(toggle)
|
|
|
|
// Popover with style options
|
|
const popover = document.createElement('div')
|
|
popover.className = 'map-style-popover'
|
|
popover.setAttribute('role', 'listbox')
|
|
popover.setAttribute('aria-label', styleLabel)
|
|
|
|
Object.entries(this.availableStyles).forEach(([key, style]) => {
|
|
const btn = document.createElement('button')
|
|
btn.type = 'button'
|
|
btn.title = style.title
|
|
btn.textContent = style.title
|
|
btn.className = 'map-style-popover-btn'
|
|
btn.setAttribute('role', 'option')
|
|
if (this.mapOptions.style === style.url) {
|
|
btn.classList.add('map-style-popover-btn--active')
|
|
btn.setAttribute('aria-selected', 'true')
|
|
}
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation()
|
|
this.setStyle(style.url)
|
|
popover.querySelectorAll('.map-style-popover-btn').forEach((b) => {
|
|
b.classList.remove('map-style-popover-btn--active')
|
|
b.setAttribute('aria-selected', 'false')
|
|
})
|
|
btn.classList.add('map-style-popover-btn--active')
|
|
btn.setAttribute('aria-selected', 'true')
|
|
popover.classList.remove('map-style-popover--open')
|
|
toggle.setAttribute('aria-expanded', 'false')
|
|
})
|
|
popover.appendChild(btn)
|
|
})
|
|
container.appendChild(popover)
|
|
|
|
// Close popover when clicking elsewhere on the map
|
|
closePopoverHandler = () => {
|
|
popover.classList.remove('map-style-popover--open')
|
|
toggle.setAttribute('aria-expanded', 'false')
|
|
}
|
|
this.map.getContainer().addEventListener('click', closePopoverHandler)
|
|
|
|
return container
|
|
},
|
|
onRemove: () => {
|
|
if (closePopoverHandler) {
|
|
this.map.getContainer().removeEventListener('click', closePopoverHandler)
|
|
closePopoverHandler = null
|
|
}
|
|
},
|
|
}
|
|
this.map.addControl(styleSwitcher, 'top-right')
|
|
|
|
// create a popup, but don't add it to the map yet
|
|
this.markers.popup = new mapboxgl.Popup({
|
|
closeButton: true,
|
|
closeOnClick: true,
|
|
maxWidth: '300px',
|
|
})
|
|
|
|
// show popup for given features at coordinates
|
|
const showPopup = (features, lngLat) => {
|
|
if (this.popupOnLeaveTimeoutId) {
|
|
clearTimeout(this.popupOnLeaveTimeoutId)
|
|
this.popupOnLeaveTimeoutId = null
|
|
}
|
|
if (this.markers.popup.isOpen()) {
|
|
this.markers.popup.remove()
|
|
}
|
|
|
|
this.map.getCanvas().style.cursor = 'pointer'
|
|
|
|
const coordinates = features[0].geometry.coordinates.slice()
|
|
|
|
// Ensure popup appears over the correct copy when map is zoomed out
|
|
while (Math.abs(lngLat.lng - coordinates[0]) > 180) {
|
|
coordinates[0] += lngLat.lng > coordinates[0] ? 360 : -360
|
|
}
|
|
|
|
// Build popup content safely using DOM nodes (no raw HTML interpolation)
|
|
const container = document.createElement('div')
|
|
container.className = 'map-popup-container'
|
|
|
|
const locationName = features[0].properties.locationName
|
|
if (locationName) {
|
|
const header = document.createElement('div')
|
|
header.className = 'map-popup-header'
|
|
header.textContent = locationName
|
|
container.appendChild(header)
|
|
}
|
|
|
|
const body = document.createElement('div')
|
|
body.className = 'map-popup-body'
|
|
|
|
features.forEach((feature, index) => {
|
|
if (index > 0) {
|
|
body.appendChild(document.createElement('hr'))
|
|
}
|
|
|
|
const markerTypeLabel = this.$t(`map.markerTypes.${feature.properties.type}`)
|
|
const markerProfile = {
|
|
theUser: {
|
|
linkTitle: '@' + feature.properties.slug,
|
|
link: `/profile/${feature.properties.id}/${feature.properties.slug}`,
|
|
},
|
|
user: {
|
|
linkTitle: '@' + feature.properties.slug,
|
|
link: `/profile/${feature.properties.id}/${feature.properties.slug}`,
|
|
},
|
|
group: {
|
|
linkTitle: '&' + feature.properties.slug,
|
|
link: `/groups/${feature.properties.id}/${feature.properties.slug}`,
|
|
},
|
|
event: {
|
|
linkTitle: feature.properties.slug,
|
|
link: `/post/${feature.properties.id}/${feature.properties.slug}`,
|
|
},
|
|
}
|
|
const profile = markerProfile[feature.properties.type]
|
|
|
|
const item = document.createElement('div')
|
|
|
|
const nameRow = document.createElement('div')
|
|
const nameB = document.createElement('b')
|
|
nameB.textContent = feature.properties.name
|
|
const typeI = document.createElement('i')
|
|
typeI.textContent = ` (${markerTypeLabel})`
|
|
nameRow.appendChild(nameB)
|
|
nameRow.appendChild(typeI)
|
|
item.appendChild(nameRow)
|
|
|
|
const linkRow = document.createElement('div')
|
|
const link = document.createElement('a')
|
|
link.href = profile.link
|
|
link.target = '_blank'
|
|
link.rel = 'noopener noreferrer'
|
|
link.textContent = profile.linkTitle
|
|
linkRow.appendChild(link)
|
|
item.appendChild(linkRow)
|
|
|
|
body.appendChild(item)
|
|
|
|
if (feature.properties.description && feature.properties.description.length > 0) {
|
|
const desc = document.createElement('div')
|
|
desc.style.marginTop = '4px'
|
|
desc.textContent = feature.properties.description
|
|
body.appendChild(desc)
|
|
}
|
|
})
|
|
|
|
container.appendChild(body)
|
|
this.markers.popup.setLngLat(coordinates).setDOMContent(container).addTo(this.map)
|
|
}
|
|
|
|
// Query all features at the clicked/hovered point
|
|
const getFeaturesAtPoint = (point) => {
|
|
return this.map.queryRenderedFeatures(point, { layers: ['markers'] })
|
|
}
|
|
|
|
// Desktop: show popup on hover
|
|
this.map.on('mouseenter', 'markers', (e) => {
|
|
const features = getFeaturesAtPoint(e.point)
|
|
if (features.length > 0) {
|
|
showPopup(features, e.lngLat)
|
|
}
|
|
})
|
|
|
|
this.map.on('mouseleave', 'markers', () => {
|
|
if (this.markers.popup.isOpen()) {
|
|
this.popupOnLeaveTimeoutId = setTimeout(() => {
|
|
this.map.getCanvas().style.cursor = ''
|
|
this.markers.popup.remove()
|
|
}, 3000)
|
|
}
|
|
})
|
|
|
|
// Mobile: show popup on click/tap
|
|
this.map.on('click', 'markers', (e) => {
|
|
const features = getFeaturesAtPoint(e.point)
|
|
if (features.length > 0) {
|
|
showPopup(features, e.lngLat)
|
|
e.originalEvent.stopPropagation()
|
|
}
|
|
})
|
|
|
|
this.loadMarkersIconsAndAddMarkers()
|
|
},
|
|
language(map) {
|
|
// example in mapbox-gl-language: https://github.com/mapbox/mapbox-gl-language/blob/master/index.js
|
|
map.getStyle().layers.forEach(function (thisLayer) {
|
|
if (thisLayer.id.indexOf('-label') > 0) {
|
|
// seems to use user language. specific language would be `name_de`, but is not compatible with all maps
|
|
// variant sets all 'text-field' layers to languages of their countries
|
|
map.setLayoutProperty(thisLayer.id, 'text-field', ['get', 'name'])
|
|
}
|
|
})
|
|
},
|
|
setStyle(url) {
|
|
this.map.setStyle(url)
|
|
this.activeStyle = url
|
|
},
|
|
loadMarkersIconsAndAddMarkers() {
|
|
Promise.all(
|
|
this.markers.types.map(
|
|
(marker) =>
|
|
new Promise((resolve, reject) => {
|
|
// our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL
|
|
this.map.loadImage(
|
|
'img/mapbox/marker-icons/' + marker.icon.mapName,
|
|
(error, image) => {
|
|
if (error) throw error
|
|
this.map.addImage(marker.icon.id, image)
|
|
resolve()
|
|
},
|
|
)
|
|
}),
|
|
),
|
|
).then(() => {
|
|
this.markers.isImagesLoaded = true
|
|
this.language(this.map)
|
|
this.addMarkersOnCheckPrepared()
|
|
})
|
|
},
|
|
addMarkersOnCheckPrepared() {
|
|
// set geoJSON for markers
|
|
if (this.isPreparedForMarkers) {
|
|
// add markers for "users"
|
|
this.users.forEach((user) => {
|
|
if (user.id !== this.currentUser.id) {
|
|
this.markers.geoJSON.push({
|
|
type: 'Feature',
|
|
properties: {
|
|
type: 'user',
|
|
iconName: 'marker-green',
|
|
iconRotate: 0.0,
|
|
id: user.id,
|
|
slug: user.slug,
|
|
name: user.name,
|
|
locationName: user.location.name,
|
|
description: user.about ? user.about : undefined,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: this.getCoordinates(user.location),
|
|
},
|
|
})
|
|
}
|
|
})
|
|
// add marker for "currentUser"
|
|
if (this.currentUserCoordinates) {
|
|
this.markers.geoJSON.push({
|
|
type: 'Feature',
|
|
properties: {
|
|
type: 'theUser',
|
|
iconName: 'marker-orange',
|
|
iconRotate: 45.0,
|
|
id: this.currentUser.id,
|
|
slug: this.currentUser.slug,
|
|
name: this.currentUser.name,
|
|
locationName: this.currentUserLocation.name,
|
|
description: this.currentUser.about ? this.currentUser.about : undefined,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: this.currentUserCoordinates,
|
|
},
|
|
})
|
|
}
|
|
// add markers for "groups"
|
|
this.groups.forEach((group) => {
|
|
this.markers.geoJSON.push({
|
|
type: 'Feature',
|
|
properties: {
|
|
type: 'group',
|
|
iconName: 'marker-red',
|
|
iconRotate: 0.0,
|
|
id: group.id,
|
|
slug: group.slug,
|
|
name: group.name,
|
|
locationName: group.location.name,
|
|
description: group.about ? group.about : undefined,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: this.getCoordinates(group.location),
|
|
},
|
|
})
|
|
})
|
|
// add markers for "posts", post type "Event" with location coordinates
|
|
this.posts.forEach((post) => {
|
|
this.markers.geoJSON.push({
|
|
type: 'Feature',
|
|
properties: {
|
|
type: 'event',
|
|
iconName: 'marker-purple',
|
|
iconRotate: 0.0,
|
|
id: post.id,
|
|
slug: post.slug,
|
|
name: post.title,
|
|
locationName: post.eventLocation.name,
|
|
description: this.$filters.removeHtml(post.content),
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: this.getCoordinates(post.eventLocation),
|
|
},
|
|
})
|
|
})
|
|
|
|
// Nudge markers of different types sharing the same coordinates
|
|
const coordGroups = {}
|
|
this.markers.geoJSON.forEach((feature) => {
|
|
const key = feature.geometry.coordinates.join(',')
|
|
if (!coordGroups[key]) coordGroups[key] = []
|
|
coordGroups[key].push(feature)
|
|
})
|
|
const lngOffset = 0.0002 // small longitude offset (~15m at mid-latitudes)
|
|
Object.values(coordGroups).forEach((group) => {
|
|
// Deduplicate by type — only offset distinct types
|
|
const uniqueTypes = [...new Set(group.map((f) => f.properties.type))]
|
|
if (uniqueTypes.length <= 1) return
|
|
const totalWidth = (uniqueTypes.length - 1) * lngOffset
|
|
uniqueTypes.forEach((type, index) => {
|
|
const offset = -totalWidth / 2 + index * lngOffset
|
|
group
|
|
.filter((f) => f.properties.type === type)
|
|
.forEach((feature) => {
|
|
feature.geometry.coordinates = [
|
|
feature.geometry.coordinates[0] + offset,
|
|
feature.geometry.coordinates[1],
|
|
]
|
|
})
|
|
})
|
|
})
|
|
|
|
this.markers.isGeoJSON = true
|
|
}
|
|
|
|
// add source and layer
|
|
if (!this.markers.isSourceAndLayerAdded && this.markers.isGeoJSON && this.map) {
|
|
this.map.addSource('markers', {
|
|
type: 'geojson',
|
|
data: {
|
|
type: 'FeatureCollection',
|
|
features: this.markers.geoJSON,
|
|
},
|
|
})
|
|
this.map.addLayer({
|
|
id: 'markers',
|
|
type: 'symbol',
|
|
source: 'markers',
|
|
layout: {
|
|
'icon-image': ['get', 'iconName'], // get the "icon-image" from the source's "iconName" property
|
|
'icon-allow-overlap': true,
|
|
'icon-size': 1.0,
|
|
'icon-rotate': ['get', 'iconRotate'], // get the "icon-rotate" from the source's "iconRotate" property
|
|
// 'text-field': ['get', 'name'], // get the "text-field" from the source's "name" property
|
|
// 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
|
|
// 'text-offset': [0, 0],
|
|
// 'text-anchor': 'top',
|
|
// 'text-allow-overlap': true,
|
|
},
|
|
})
|
|
|
|
this.markers.isSourceAndLayerAdded = true
|
|
}
|
|
|
|
// fly to center if never done
|
|
if (!this.markers.isFlyToCenter && this.markers.isSourceAndLayerAdded) {
|
|
this.mapFlyToCenter()
|
|
this.markers.isFlyToCenter = true
|
|
}
|
|
},
|
|
mapFlyToCenter() {
|
|
if (this.map) {
|
|
// example: https://docs.mapbox.com/mapbox-gl-js/example/center-on-feature/
|
|
this.map.flyTo({
|
|
center: this.mapCenter,
|
|
zoom: this.mapZoom,
|
|
})
|
|
}
|
|
},
|
|
getCoordinates(location) {
|
|
return [location.lng, location.lat]
|
|
},
|
|
async getUserLocation(id) {
|
|
try {
|
|
const {
|
|
data: { User: users },
|
|
} = await this.$apollo.query({
|
|
query: profileUserQuery(this.$i18n),
|
|
variables: {
|
|
id,
|
|
followedByCount: 0,
|
|
followingCount: 0,
|
|
},
|
|
})
|
|
return users && users[0] && users[0].location ? users[0].location : null
|
|
} catch (err) {
|
|
this.$toast.error(err.message)
|
|
return null
|
|
}
|
|
},
|
|
},
|
|
apollo: {
|
|
mapData: {
|
|
query() {
|
|
return mapQuery(this.$i18n)
|
|
},
|
|
variables() {
|
|
return {
|
|
userFilter: { hasLocation: true },
|
|
groupHasLocation: true,
|
|
postFilter: {
|
|
postType_in: ['Event'],
|
|
eventStart_gte: new Date(),
|
|
hasLocation: true,
|
|
},
|
|
}
|
|
},
|
|
update({ User, Group, Post }) {
|
|
this.users = User
|
|
this.groups = Group
|
|
this.posts = Post
|
|
this.addMarkersOnCheckPrepared()
|
|
},
|
|
fetchPolicy: 'cache-and-network',
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
<!-- eslint-enable vue/no-reserved-component-names -->
|
|
|
|
<style lang="scss">
|
|
// description: https: //github.com/geospoc/v-mapbox/tree/v1.11.2/docs
|
|
// code example: https: //codesandbox.io/embed/v-mapbox-map-demo-k1l1n?autoresize=1&fontsize=14&hidenavigation=1&theme=dark
|
|
@import 'mapbox-gl/dist/mapbox-gl.css';
|
|
@import 'v-mapbox/dist/v-mapbox.css';
|
|
|
|
.map-page {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
z-index: 1;
|
|
}
|
|
|
|
.mgl-map-wrapper {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.mgl-map-wrapper {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mapboxgl-popup-content {
|
|
max-height: 40vh;
|
|
overflow: hidden;
|
|
padding: 10px;
|
|
}
|
|
|
|
.map-popup-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: calc(40vh - 20px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mapboxgl-popup-close-button {
|
|
font-size: 1.2rem;
|
|
padding: 2px 6px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.map-popup-header {
|
|
font-weight: bold;
|
|
font-size: 1.1em;
|
|
margin-bottom: 4px;
|
|
padding-right: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.map-popup-body {
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
// Smaller geocoder on mobile (expanded)
|
|
@media (max-width: 810px) {
|
|
.mapboxgl-ctrl-geocoder {
|
|
font-size: 13px;
|
|
line-height: 18px;
|
|
min-width: 180px;
|
|
max-width: 240px;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder--input {
|
|
height: 29px;
|
|
padding: 4px 28px;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder--icon-search {
|
|
width: 15px;
|
|
height: 15px;
|
|
top: 7px;
|
|
left: 7px;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder--button {
|
|
width: 22px;
|
|
height: 22px;
|
|
top: 4px;
|
|
right: 4px;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder--icon-close {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed {
|
|
width: 29px;
|
|
height: 29px;
|
|
min-width: 29px;
|
|
background-color: white;
|
|
border-radius: 4px;
|
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
|
|
.mapboxgl-ctrl-geocoder--icon-search {
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 17px;
|
|
height: 17px;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder--input {
|
|
display: none;
|
|
}
|
|
|
|
.mapboxgl-ctrl-geocoder--pin-right > * {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.map-legend {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
left: 10px;
|
|
background: rgba(255, 255, 255, 0.75);
|
|
backdrop-filter: blur(4px);
|
|
border-radius: 4px;
|
|
z-index: 1;
|
|
font-size: 0.8rem;
|
|
color: $color-neutral-10;
|
|
}
|
|
|
|
.map-legend-toggle {
|
|
display: none;
|
|
width: 100%;
|
|
padding: 4px 8px;
|
|
border: none;
|
|
background: rgba(0, 0, 0, 0.05);
|
|
color: $color-neutral-10;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
text-align: left;
|
|
order: 1;
|
|
|
|
&:hover,
|
|
&:active {
|
|
background: rgba(0, 0, 0, 0.1);
|
|
}
|
|
}
|
|
|
|
.map-legend-arrow {
|
|
float: right;
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.map-legend-content {
|
|
padding: 4px 8px 2px;
|
|
}
|
|
|
|
.map-legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
@media (max-width: 639px) {
|
|
.map-legend {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.map-legend-toggle {
|
|
display: block;
|
|
}
|
|
|
|
.map-legend-content {
|
|
order: 0;
|
|
}
|
|
|
|
.map-legend--open .map-legend-content {
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
}
|
|
|
|
.map-style-switcher {
|
|
position: relative;
|
|
background: white;
|
|
border-radius: 4px;
|
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.map-style-switcher-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 29px;
|
|
height: 29px;
|
|
padding: 0;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
color: #333;
|
|
|
|
&:hover {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
}
|
|
|
|
.map-style-popover {
|
|
display: none;
|
|
position: absolute;
|
|
top: 0;
|
|
right: 100%;
|
|
margin-right: 6px;
|
|
background: white;
|
|
border-radius: 4px;
|
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
|
|
&--open {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.map-style-popover-btn {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 6px 12px;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
text-align: left;
|
|
|
|
&:not(:last-child) {
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
&:hover {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
&--active {
|
|
font-weight: bold;
|
|
background: rgba(0, 0, 0, 0.08);
|
|
}
|
|
}
|
|
</style>
|