mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-02 23:55:28 +00:00
fix(webapp): fix & improve map substantially (#9481)
This commit is contained in:
parent
4c539406bc
commit
0bf724f0c0
@ -1301,13 +1301,123 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('seed', 'users additional')
|
||||
console.log('seed', 'users additional with map locations around Zwingenberg')
|
||||
|
||||
// Region Hessen (Mapbox-compatible hierarchy: place -> region -> country)
|
||||
const Hessen = await Factory.build('location', {
|
||||
id: 'region.8967011281068080',
|
||||
name: 'Hessen',
|
||||
type: 'region',
|
||||
lng: 8.6528,
|
||||
lat: 50.6521,
|
||||
nameDE: 'Hessen',
|
||||
nameEN: 'Hesse',
|
||||
nameES: 'Hesse',
|
||||
nameFR: 'Hesse',
|
||||
nameIT: 'Assia',
|
||||
namePT: 'Hessen',
|
||||
nameNL: 'Hessen',
|
||||
namePL: 'Hesja',
|
||||
nameRU: 'Гессен',
|
||||
})
|
||||
await Hessen.relateTo(Germany, 'isIn')
|
||||
|
||||
// 50 villages around Zwingenberg (64673), Zwingenberg excluded
|
||||
// Mapbox-compatible: type 'place', realistic IDs
|
||||
const zwingenbergVillages = [
|
||||
// Bergstraße (west)
|
||||
{ id: 'place.8652241', name: 'Alsbach-Hähnlein', lat: 49.7389, lng: 8.6331 },
|
||||
{ id: 'place.8652242', name: 'Bickenbach', lat: 49.7567, lng: 8.6178 },
|
||||
{ id: 'place.8652243', name: 'Seeheim-Jugenheim', lat: 49.7631, lng: 8.6506 },
|
||||
{ id: 'place.8652244', name: 'Bensheim', lat: 49.6812, lng: 8.6167 },
|
||||
{ id: 'place.8652245', name: 'Auerbach', lat: 49.7053, lng: 8.6389 },
|
||||
{ id: 'place.8652246', name: 'Heppenheim', lat: 49.6428, lng: 8.6392 },
|
||||
{ id: 'place.8652247', name: 'Lorsch', lat: 49.6539, lng: 8.5678 },
|
||||
{ id: 'place.8652248', name: 'Einhausen', lat: 49.6775, lng: 8.5578 },
|
||||
{ id: 'place.8652249', name: 'Gernsheim', lat: 49.7528, lng: 8.4906 },
|
||||
{ id: 'place.8652250', name: 'Pfungstadt', lat: 49.8056, lng: 8.6042 },
|
||||
// Odenwald (east)
|
||||
{ id: 'place.8652251', name: 'Reichenbach', lat: 49.725, lng: 8.67 },
|
||||
{ id: 'place.8652252', name: 'Lautertal', lat: 49.7253, lng: 8.6914 },
|
||||
{ id: 'place.8652253', name: 'Lindenfels', lat: 49.6836, lng: 8.7781 },
|
||||
{ id: 'place.8652254', name: 'Modautal', lat: 49.7736, lng: 8.7258 },
|
||||
{ id: 'place.8652255', name: 'Mühltal', lat: 49.8003, lng: 8.6917 },
|
||||
{ id: 'place.8652256', name: 'Ober-Ramstadt', lat: 49.8306, lng: 8.7486 },
|
||||
{ id: 'place.8652257', name: 'Reinheim', lat: 49.8289, lng: 8.8356 },
|
||||
{ id: 'place.8652258', name: 'Groß-Bieberau', lat: 49.7906, lng: 8.8281 },
|
||||
{ id: 'place.8652259', name: 'Fränkisch-Crumbach', lat: 49.745, lng: 8.8444 },
|
||||
{ id: 'place.8652260', name: 'Brensbach', lat: 49.7742, lng: 8.8819 },
|
||||
// Ried (west/southwest)
|
||||
{ id: 'place.8652261', name: 'Bürstadt', lat: 49.6433, lng: 8.4506 },
|
||||
{ id: 'place.8652262', name: 'Lampertheim', lat: 49.5978, lng: 8.47 },
|
||||
{ id: 'place.8652263', name: 'Biblis', lat: 49.6878, lng: 8.4531 },
|
||||
{ id: 'place.8652264', name: 'Groß-Rohrheim', lat: 49.7228, lng: 8.4822 },
|
||||
{ id: 'place.8652265', name: 'Riedstadt', lat: 49.835, lng: 8.4944 },
|
||||
{ id: 'place.8652266', name: 'Stockstadt am Rhein', lat: 49.8094, lng: 8.4656 },
|
||||
{ id: 'place.8652267', name: 'Biebesheim', lat: 49.7806, lng: 8.4672 },
|
||||
{ id: 'place.8652268', name: 'Trebur', lat: 49.9211, lng: 8.4081 },
|
||||
{ id: 'place.8652269', name: 'Nauheim', lat: 49.9456, lng: 8.4494 },
|
||||
{ id: 'place.8652270', name: 'Griesheim', lat: 49.8619, lng: 8.5722 },
|
||||
// Darmstadt area (north)
|
||||
{ id: 'place.8652271', name: 'Roßdorf', lat: 49.8572, lng: 8.7578 },
|
||||
{ id: 'place.8652272', name: 'Messel', lat: 49.9333, lng: 8.75 },
|
||||
{ id: 'place.8652273', name: 'Eppertshausen', lat: 49.95, lng: 8.85 },
|
||||
{ id: 'place.8652274', name: 'Münster', lat: 49.9253, lng: 8.8653 },
|
||||
{ id: 'place.8652275', name: 'Dieburg', lat: 49.8983, lng: 8.8467 },
|
||||
{ id: 'place.8652276', name: 'Babenhausen', lat: 49.965, lng: 8.9511 },
|
||||
{ id: 'place.8652277', name: 'Schaafheim', lat: 49.9244, lng: 8.9703 },
|
||||
{ id: 'place.8652278', name: 'Groß-Umstadt', lat: 49.8667, lng: 8.9333 },
|
||||
{ id: 'place.8652279', name: 'Otzberg', lat: 49.82, lng: 8.91 },
|
||||
{ id: 'place.8652280', name: 'Höchst im Odenwald', lat: 49.7994, lng: 8.9986 },
|
||||
// Further south
|
||||
{ id: 'place.8652281', name: 'Mörlenbach', lat: 49.5969, lng: 8.7378 },
|
||||
{ id: 'place.8652282', name: 'Rimbach', lat: 49.6256, lng: 8.7611 },
|
||||
{ id: 'place.8652283', name: 'Fürth', lat: 49.6522, lng: 8.7789 },
|
||||
{ id: 'place.8652284', name: 'Grasellenbach', lat: 49.6353, lng: 8.8531 },
|
||||
{ id: 'place.8652285', name: 'Wald-Michelbach', lat: 49.57, lng: 8.83 },
|
||||
{ id: 'place.8652286', name: 'Abtsteinach', lat: 49.5536, lng: 8.78 },
|
||||
{ id: 'place.8652287', name: 'Gorxheimertal', lat: 49.5322, lng: 8.7322 },
|
||||
{ id: 'place.8652288', name: 'Viernheim', lat: 49.5403, lng: 8.5783 },
|
||||
{ id: 'place.8652289', name: 'Weinheim', lat: 49.5489, lng: 8.6639 },
|
||||
{ id: 'place.8652290', name: 'Hemsbach', lat: 49.59, lng: 8.65 },
|
||||
]
|
||||
|
||||
// Create village location nodes (one per village, shared by all users in that village)
|
||||
const villageLocationNodes: (typeof Hamburg)[] = []
|
||||
for (const village of zwingenbergVillages) {
|
||||
const location = await Factory.build('location', {
|
||||
id: village.id,
|
||||
name: village.name,
|
||||
type: 'place',
|
||||
lng: village.lng,
|
||||
lat: village.lat,
|
||||
nameDE: village.name,
|
||||
nameEN: village.name,
|
||||
nameES: village.name,
|
||||
nameFR: village.name,
|
||||
nameIT: village.name,
|
||||
namePT: village.name,
|
||||
nameNL: village.name,
|
||||
namePL: village.name,
|
||||
nameRU: village.name,
|
||||
})
|
||||
await location.relateTo(Hessen, 'isIn')
|
||||
villageLocationNodes.push(location)
|
||||
}
|
||||
|
||||
// Create 1000 additional users with locations assigned during creation
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const additionalUsers: any[] = []
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
if (i % 100 === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('seed', `additional users ${i}/1000`)
|
||||
}
|
||||
const user = await Factory.build('user')
|
||||
await jennyRostock.relateTo(user, 'following')
|
||||
await user.relateTo(jennyRostock, 'following')
|
||||
// Assign village location (round-robin across 50 villages = ~20 users per village)
|
||||
await user.relateTo(villageLocationNodes[i % villageLocationNodes.length], 'isIn')
|
||||
additionalUsers.push(user)
|
||||
|
||||
const userObj = await user.toJson()
|
||||
@ -1321,6 +1431,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('seed', 'additional users 1000/1000 done')
|
||||
|
||||
// Jenny's first 99 additional users all redeemed code ABCDEF
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<os-button
|
||||
v-for="style in styles"
|
||||
:key="style.title"
|
||||
:appearance="actualStyle === style.url ? 'filled' : 'outline'"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="map-style-button"
|
||||
@click="setStyle(style.url)"
|
||||
>
|
||||
{{ style.title }}
|
||||
</os-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
name: 'MapStylesButtons',
|
||||
props: {
|
||||
styles: { type: Array, required: true },
|
||||
actualStyle: { type: String, required: true },
|
||||
setStyle: { type: Function, required: true },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// Outline (nicht ausgewählt) button styles
|
||||
button.map-style-button.bg-transparent {
|
||||
background-color: $background-color-softer !important;
|
||||
color: $text-color-base !important;
|
||||
}
|
||||
|
||||
button.map-style-button.bg-transparent:hover {
|
||||
background-color: $color-primary-light !important;
|
||||
border-color: $color-primary-light !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
button.map-style-button.bg-transparent:active {
|
||||
background-color: $color-primary-dark !important;
|
||||
border-color: $color-primary-dark !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.map-style-button {
|
||||
position: relative;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
@ -18,7 +18,7 @@ module.exports = {
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 86,
|
||||
lines: 87,
|
||||
},
|
||||
},
|
||||
coverageProvider: 'v8',
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Veranstaltung",
|
||||
"group": "Gruppe",
|
||||
"theUser": "Meine Position",
|
||||
"title": "Legende",
|
||||
"user": "Nutzer"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Dunkel",
|
||||
"outdoors": "Landschaft",
|
||||
"satellite": "Satellit",
|
||||
"streets": "Straßen"
|
||||
"streets": "Straßen",
|
||||
"title": "Kartenstil"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Event",
|
||||
"group": "Group",
|
||||
"theUser": "My position",
|
||||
"title": "Legend",
|
||||
"user": "User"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Dark",
|
||||
"outdoors": "Outdoors",
|
||||
"satellite": "Satellite",
|
||||
"streets": "Streets"
|
||||
"streets": "Streets",
|
||||
"title": "Map style"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Evento",
|
||||
"group": "Grupo",
|
||||
"theUser": "Mi posición",
|
||||
"title": "Leyenda",
|
||||
"user": "Usuario"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Oscuro",
|
||||
"outdoors": "Exterior",
|
||||
"satellite": "Satélite",
|
||||
"streets": "Calles"
|
||||
"streets": "Calles",
|
||||
"title": "Estilo de mapa"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Événement",
|
||||
"group": "Groupe",
|
||||
"theUser": "Ma position",
|
||||
"title": "Légende",
|
||||
"user": "Utilisateur"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Sombre",
|
||||
"outdoors": "Plein air",
|
||||
"satellite": "Satellite",
|
||||
"streets": "Rues"
|
||||
"streets": "Rues",
|
||||
"title": "Style de carte"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Evento",
|
||||
"group": "Gruppo",
|
||||
"theUser": "La mia posizione",
|
||||
"title": "Legenda",
|
||||
"user": "Utente"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Scuro",
|
||||
"outdoors": "All'aperto",
|
||||
"satellite": "Satellite",
|
||||
"streets": "Strade"
|
||||
"streets": "Strade",
|
||||
"title": "Stile mappa"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Evenement",
|
||||
"group": "Groep",
|
||||
"theUser": "Mijn positie",
|
||||
"title": "Legenda",
|
||||
"user": "Gebruiker"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Donker",
|
||||
"outdoors": "Buiten",
|
||||
"satellite": "Satelliet",
|
||||
"streets": "Straten"
|
||||
"streets": "Straten",
|
||||
"title": "Kaartstijl"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Wydarzenie",
|
||||
"group": "Grupa",
|
||||
"theUser": "Moja pozycja",
|
||||
"title": "Legenda",
|
||||
"user": "Użytkownik"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Ciemny",
|
||||
"outdoors": "Na zewnątrz",
|
||||
"satellite": "Satelita",
|
||||
"streets": "Ulice"
|
||||
"streets": "Ulice",
|
||||
"title": "Styl mapy"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Evento",
|
||||
"group": "Grupo",
|
||||
"theUser": "Minha posição",
|
||||
"title": "Legenda",
|
||||
"user": "Usuário"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Escuro",
|
||||
"outdoors": "Ao ar livre",
|
||||
"satellite": "Satélite",
|
||||
"streets": "Ruas"
|
||||
"streets": "Ruas",
|
||||
"title": "Estilo do mapa"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Событие",
|
||||
"group": "Группа",
|
||||
"theUser": "Моя позиция",
|
||||
"title": "Легенда",
|
||||
"user": "Пользователь"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Тёмная",
|
||||
"outdoors": "Природа",
|
||||
"satellite": "Спутник",
|
||||
"streets": "Улицы"
|
||||
"streets": "Улицы",
|
||||
"title": "Стиль карты"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Ngjarje",
|
||||
"group": "Grup",
|
||||
"theUser": "Pozicioni im",
|
||||
"title": "Legjenda",
|
||||
"user": "Përdorues"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "E errët",
|
||||
"outdoors": "Jashtë",
|
||||
"satellite": "Satelit",
|
||||
"streets": "Rrugë"
|
||||
"streets": "Rrugë",
|
||||
"title": "Stili i hartës"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@ -754,6 +754,7 @@
|
||||
"event": "Подія",
|
||||
"group": "Група",
|
||||
"theUser": "Моя позиція",
|
||||
"title": "Легенда",
|
||||
"user": "Користувач"
|
||||
},
|
||||
"markerTypes": {
|
||||
@ -767,7 +768,8 @@
|
||||
"dark": "Темна",
|
||||
"outdoors": "На відкритому повітрі",
|
||||
"satellite": "Супутник",
|
||||
"streets": "Вулиці"
|
||||
"streets": "Вулиці",
|
||||
"title": "Стиль карти"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,29 +1,7 @@
|
||||
<!-- Example Reference: https://codesandbox.io/s/v-mapbox-with-nuxt-lbrt6?file=/pages/index.vue -->
|
||||
<template>
|
||||
<div class="map-page">
|
||||
<div class="map-header">
|
||||
<h1 class="ds-heading ds-heading-h1">{{ $t('map.pageTitle') }}</h1>
|
||||
<small>
|
||||
<div>
|
||||
<span v-for="type in markers.types" :key="type.id">
|
||||
<img
|
||||
:alt="$t('map.legend.' + type.id)"
|
||||
:src="'/img/mapbox/marker-icons/' + type.icon.legendName"
|
||||
width="15"
|
||||
/>
|
||||
{{ $t('map.legend.' + type.id) }}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</small>
|
||||
</div>
|
||||
<client-only v-if="!isEmpty($env.MAPBOX_TOKEN)">
|
||||
<map-styles-buttons
|
||||
v-if="isMobile"
|
||||
:styles="styles"
|
||||
:actualStyle="mapOptions.style"
|
||||
:setStyle="setStyle"
|
||||
/>
|
||||
<mgl-map
|
||||
:mapbox-gl="mapboxgl"
|
||||
:access-token="mapOptions.accessToken"
|
||||
@ -39,16 +17,37 @@
|
||||
:max-pitch="60"
|
||||
@load="onMapLoad"
|
||||
>
|
||||
<map-styles-buttons
|
||||
v-if="!isMobile"
|
||||
:styles="styles"
|
||||
:actualStyle="mapOptions.style"
|
||||
:setStyle="setStyle"
|
||||
/>
|
||||
<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')" />
|
||||
@ -57,7 +56,7 @@
|
||||
|
||||
<!-- eslint-disable vue/no-reserved-component-names -->
|
||||
<script>
|
||||
import { isEmpty, toArray } from 'lodash'
|
||||
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'
|
||||
@ -66,7 +65,6 @@ import { profileUserQuery } from '~/graphql/User'
|
||||
import { mapQuery } from '~/graphql/MapQuery'
|
||||
import mobile from '~/mixins/mobile'
|
||||
import Empty from '~/components/Empty/Empty'
|
||||
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
||||
|
||||
const maxMobileWidth = 639 // on this width and smaller the mapbox 'MapboxGeocoder' search gets bigger
|
||||
|
||||
@ -75,7 +73,6 @@ export default {
|
||||
mixins: [mobile(maxMobileWidth)],
|
||||
components: {
|
||||
Empty,
|
||||
MapStylesButtons,
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
@ -87,6 +84,7 @@ export default {
|
||||
return {
|
||||
isEmpty,
|
||||
mapboxgl,
|
||||
legendOpen: false,
|
||||
activeStyle: null,
|
||||
defaultCenter: [10.452764, 51.165707], // center of Germany: https://www.gpskoordinaten.de/karte/land/DE
|
||||
currentUserLocation: null,
|
||||
@ -140,12 +138,18 @@ export default {
|
||||
}
|
||||
},
|
||||
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',
|
||||
@ -160,9 +164,6 @@ export default {
|
||||
this.posts
|
||||
)
|
||||
},
|
||||
styles() {
|
||||
return toArray(this.availableStyles)
|
||||
},
|
||||
availableStyles() {
|
||||
// https://docs.mapbox.com/api/maps/styles/
|
||||
const availableStyles = {
|
||||
@ -181,7 +182,7 @@ export default {
|
||||
url: 'mapbox://styles/mapbox/dark-v10?optimize=true',
|
||||
},
|
||||
}
|
||||
Object.keys(availableStyles).map((key) => {
|
||||
Object.keys(availableStyles).forEach((key) => {
|
||||
availableStyles[key].title = this.$t('map.styles.' + key)
|
||||
})
|
||||
return availableStyles
|
||||
@ -211,6 +212,19 @@ export default {
|
||||
},
|
||||
},
|
||||
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
|
||||
|
||||
@ -230,85 +244,203 @@ export default {
|
||||
accessToken: this.$env.MAPBOX_TOKEN,
|
||||
mapboxgl: this.mapboxgl,
|
||||
marker: false,
|
||||
collapsed: window.innerWidth <= 810,
|
||||
}),
|
||||
'top-right',
|
||||
)
|
||||
|
||||
// example for popup: https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/
|
||||
// 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: false,
|
||||
closeButton: true,
|
||||
closeOnClick: true,
|
||||
maxWidth: '300px',
|
||||
})
|
||||
|
||||
this.map.on('mouseenter', 'markers', (e) => {
|
||||
// if (e.features[0].properties.type !== 'theUser') {}
|
||||
// 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.map.getCanvas().style.cursor = ''
|
||||
this.markers.popup.remove()
|
||||
}
|
||||
|
||||
// Change the cursor style as a UI indicator.
|
||||
this.map.getCanvas().style.cursor = 'pointer'
|
||||
|
||||
// Copy coordinates array.
|
||||
const coordinates = e.features[0].geometry.coordinates.slice()
|
||||
const markerTypeLabel = this.$t(`map.markerTypes.${e.features[0].properties.type}`)
|
||||
const markerProfile = {
|
||||
theUser: {
|
||||
linkTitle: '@' + e.features[0].properties.slug,
|
||||
link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||
},
|
||||
user: {
|
||||
linkTitle: '@' + e.features[0].properties.slug,
|
||||
link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||
},
|
||||
group: {
|
||||
linkTitle: '&' + e.features[0].properties.slug,
|
||||
link: `/groups/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||
},
|
||||
event: {
|
||||
linkTitle: e.features[0].properties.slug,
|
||||
link: `/post/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||
},
|
||||
}
|
||||
const markerProfileLinkTitle = markerProfile[e.features[0].properties.type].linkTitle
|
||||
const markerProfileLink = markerProfile[e.features[0].properties.type].link
|
||||
let description = `
|
||||
<div>
|
||||
<div>
|
||||
<b>${e.features[0].properties.name}</b> <i>(${markerTypeLabel})</i>
|
||||
</div>
|
||||
<div>
|
||||
<a href="${markerProfileLink}" target="_blank">${markerProfileLinkTitle}</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
description +=
|
||||
e.features[0].properties.description && e.features[0].properties.description.length > 0
|
||||
? `
|
||||
<hr>
|
||||
<div>
|
||||
${e.features[0].properties.description}
|
||||
</div>`
|
||||
: ''
|
||||
const coordinates = features[0].geometry.coordinates.slice()
|
||||
|
||||
// Ensure that if the map is zoomed out such that multiple
|
||||
// copies of the feature are visible, the popup appears
|
||||
// over the copy being pointed to.
|
||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360
|
||||
// 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
|
||||
}
|
||||
|
||||
// Populate the popup and set its coordinates
|
||||
// based on the feature found.
|
||||
this.markers.popup.setLngLat(coordinates).setHTML(description).addTo(this.map)
|
||||
// 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', (e) => {
|
||||
this.map.on('mouseleave', 'markers', () => {
|
||||
if (this.markers.popup.isOpen()) {
|
||||
this.popupOnLeaveTimeoutId = setTimeout(() => {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
@ -317,6 +449,15 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
// 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) {
|
||||
@ -370,6 +511,7 @@ export default {
|
||||
id: user.id,
|
||||
slug: user.slug,
|
||||
name: user.name,
|
||||
locationName: user.location.name,
|
||||
description: user.about ? user.about : undefined,
|
||||
},
|
||||
geometry: {
|
||||
@ -390,6 +532,7 @@ export default {
|
||||
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: {
|
||||
@ -409,6 +552,7 @@ export default {
|
||||
id: group.id,
|
||||
slug: group.slug,
|
||||
name: group.name,
|
||||
locationName: group.location.name,
|
||||
description: group.about ? group.about : undefined,
|
||||
},
|
||||
geometry: {
|
||||
@ -428,6 +572,7 @@ export default {
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
name: post.title,
|
||||
locationName: post.eventLocation.name,
|
||||
description: this.$filters.removeHtml(post.content),
|
||||
},
|
||||
geometry: {
|
||||
@ -437,6 +582,32 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -542,15 +713,15 @@ export default {
|
||||
@import 'v-mapbox/dist/v-mapbox.css';
|
||||
|
||||
.map-page {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 4rem - #{$space-x-small});
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-header {
|
||||
flex-shrink: 0;
|
||||
padding: 0 0 $space-xx-small;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mgl-map-wrapper {
|
||||
@ -558,9 +729,230 @@ export default {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 811px) {
|
||||
.map-page {
|
||||
height: calc(100vh - 6rem - 8rem);
|
||||
.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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user