mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-06 01:25:31 +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
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const additionalUsers: any[] = []
|
const additionalUsers: any[] = []
|
||||||
for (let i = 0; i < 1000; i++) {
|
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')
|
const user = await Factory.build('user')
|
||||||
await jennyRostock.relateTo(user, 'following')
|
await jennyRostock.relateTo(user, 'following')
|
||||||
await user.relateTo(jennyRostock, '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)
|
additionalUsers.push(user)
|
||||||
|
|
||||||
const userObj = await user.toJson()
|
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
|
// Jenny's first 99 additional users all redeemed code ABCDEF
|
||||||
// eslint-disable-next-line no-console
|
// 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: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 86,
|
lines: 87,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
coverageProvider: 'v8',
|
coverageProvider: 'v8',
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Veranstaltung",
|
"event": "Veranstaltung",
|
||||||
"group": "Gruppe",
|
"group": "Gruppe",
|
||||||
"theUser": "Meine Position",
|
"theUser": "Meine Position",
|
||||||
|
"title": "Legende",
|
||||||
"user": "Nutzer"
|
"user": "Nutzer"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Dunkel",
|
"dark": "Dunkel",
|
||||||
"outdoors": "Landschaft",
|
"outdoors": "Landschaft",
|
||||||
"satellite": "Satellit",
|
"satellite": "Satellit",
|
||||||
"streets": "Straßen"
|
"streets": "Straßen",
|
||||||
|
"title": "Kartenstil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Event",
|
"event": "Event",
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
"theUser": "My position",
|
"theUser": "My position",
|
||||||
|
"title": "Legend",
|
||||||
"user": "User"
|
"user": "User"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"outdoors": "Outdoors",
|
"outdoors": "Outdoors",
|
||||||
"satellite": "Satellite",
|
"satellite": "Satellite",
|
||||||
"streets": "Streets"
|
"streets": "Streets",
|
||||||
|
"title": "Map style"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Evento",
|
"event": "Evento",
|
||||||
"group": "Grupo",
|
"group": "Grupo",
|
||||||
"theUser": "Mi posición",
|
"theUser": "Mi posición",
|
||||||
|
"title": "Leyenda",
|
||||||
"user": "Usuario"
|
"user": "Usuario"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Oscuro",
|
"dark": "Oscuro",
|
||||||
"outdoors": "Exterior",
|
"outdoors": "Exterior",
|
||||||
"satellite": "Satélite",
|
"satellite": "Satélite",
|
||||||
"streets": "Calles"
|
"streets": "Calles",
|
||||||
|
"title": "Estilo de mapa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Événement",
|
"event": "Événement",
|
||||||
"group": "Groupe",
|
"group": "Groupe",
|
||||||
"theUser": "Ma position",
|
"theUser": "Ma position",
|
||||||
|
"title": "Légende",
|
||||||
"user": "Utilisateur"
|
"user": "Utilisateur"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Sombre",
|
"dark": "Sombre",
|
||||||
"outdoors": "Plein air",
|
"outdoors": "Plein air",
|
||||||
"satellite": "Satellite",
|
"satellite": "Satellite",
|
||||||
"streets": "Rues"
|
"streets": "Rues",
|
||||||
|
"title": "Style de carte"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Evento",
|
"event": "Evento",
|
||||||
"group": "Gruppo",
|
"group": "Gruppo",
|
||||||
"theUser": "La mia posizione",
|
"theUser": "La mia posizione",
|
||||||
|
"title": "Legenda",
|
||||||
"user": "Utente"
|
"user": "Utente"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Scuro",
|
"dark": "Scuro",
|
||||||
"outdoors": "All'aperto",
|
"outdoors": "All'aperto",
|
||||||
"satellite": "Satellite",
|
"satellite": "Satellite",
|
||||||
"streets": "Strade"
|
"streets": "Strade",
|
||||||
|
"title": "Stile mappa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Evenement",
|
"event": "Evenement",
|
||||||
"group": "Groep",
|
"group": "Groep",
|
||||||
"theUser": "Mijn positie",
|
"theUser": "Mijn positie",
|
||||||
|
"title": "Legenda",
|
||||||
"user": "Gebruiker"
|
"user": "Gebruiker"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Donker",
|
"dark": "Donker",
|
||||||
"outdoors": "Buiten",
|
"outdoors": "Buiten",
|
||||||
"satellite": "Satelliet",
|
"satellite": "Satelliet",
|
||||||
"streets": "Straten"
|
"streets": "Straten",
|
||||||
|
"title": "Kaartstijl"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Wydarzenie",
|
"event": "Wydarzenie",
|
||||||
"group": "Grupa",
|
"group": "Grupa",
|
||||||
"theUser": "Moja pozycja",
|
"theUser": "Moja pozycja",
|
||||||
|
"title": "Legenda",
|
||||||
"user": "Użytkownik"
|
"user": "Użytkownik"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Ciemny",
|
"dark": "Ciemny",
|
||||||
"outdoors": "Na zewnątrz",
|
"outdoors": "Na zewnątrz",
|
||||||
"satellite": "Satelita",
|
"satellite": "Satelita",
|
||||||
"streets": "Ulice"
|
"streets": "Ulice",
|
||||||
|
"title": "Styl mapy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Evento",
|
"event": "Evento",
|
||||||
"group": "Grupo",
|
"group": "Grupo",
|
||||||
"theUser": "Minha posição",
|
"theUser": "Minha posição",
|
||||||
|
"title": "Legenda",
|
||||||
"user": "Usuário"
|
"user": "Usuário"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Escuro",
|
"dark": "Escuro",
|
||||||
"outdoors": "Ao ar livre",
|
"outdoors": "Ao ar livre",
|
||||||
"satellite": "Satélite",
|
"satellite": "Satélite",
|
||||||
"streets": "Ruas"
|
"streets": "Ruas",
|
||||||
|
"title": "Estilo do mapa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Событие",
|
"event": "Событие",
|
||||||
"group": "Группа",
|
"group": "Группа",
|
||||||
"theUser": "Моя позиция",
|
"theUser": "Моя позиция",
|
||||||
|
"title": "Легенда",
|
||||||
"user": "Пользователь"
|
"user": "Пользователь"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Тёмная",
|
"dark": "Тёмная",
|
||||||
"outdoors": "Природа",
|
"outdoors": "Природа",
|
||||||
"satellite": "Спутник",
|
"satellite": "Спутник",
|
||||||
"streets": "Улицы"
|
"streets": "Улицы",
|
||||||
|
"title": "Стиль карты"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Ngjarje",
|
"event": "Ngjarje",
|
||||||
"group": "Grup",
|
"group": "Grup",
|
||||||
"theUser": "Pozicioni im",
|
"theUser": "Pozicioni im",
|
||||||
|
"title": "Legjenda",
|
||||||
"user": "Përdorues"
|
"user": "Përdorues"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "E errët",
|
"dark": "E errët",
|
||||||
"outdoors": "Jashtë",
|
"outdoors": "Jashtë",
|
||||||
"satellite": "Satelit",
|
"satellite": "Satelit",
|
||||||
"streets": "Rrugë"
|
"streets": "Rrugë",
|
||||||
|
"title": "Stili i hartës"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
|
|||||||
@ -754,6 +754,7 @@
|
|||||||
"event": "Подія",
|
"event": "Подія",
|
||||||
"group": "Група",
|
"group": "Група",
|
||||||
"theUser": "Моя позиція",
|
"theUser": "Моя позиція",
|
||||||
|
"title": "Легенда",
|
||||||
"user": "Користувач"
|
"user": "Користувач"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
@ -767,7 +768,8 @@
|
|||||||
"dark": "Темна",
|
"dark": "Темна",
|
||||||
"outdoors": "На відкритому повітрі",
|
"outdoors": "На відкритому повітрі",
|
||||||
"satellite": "Супутник",
|
"satellite": "Супутник",
|
||||||
"streets": "Вулиці"
|
"streets": "Вулиці",
|
||||||
|
"title": "Стиль карти"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"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 -->
|
<!-- Example Reference: https://codesandbox.io/s/v-mapbox-with-nuxt-lbrt6?file=/pages/index.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="map-page">
|
<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)">
|
<client-only v-if="!isEmpty($env.MAPBOX_TOKEN)">
|
||||||
<map-styles-buttons
|
|
||||||
v-if="isMobile"
|
|
||||||
:styles="styles"
|
|
||||||
:actualStyle="mapOptions.style"
|
|
||||||
:setStyle="setStyle"
|
|
||||||
/>
|
|
||||||
<mgl-map
|
<mgl-map
|
||||||
:mapbox-gl="mapboxgl"
|
:mapbox-gl="mapboxgl"
|
||||||
:access-token="mapOptions.accessToken"
|
:access-token="mapOptions.accessToken"
|
||||||
@ -39,16 +17,37 @@
|
|||||||
:max-pitch="60"
|
:max-pitch="60"
|
||||||
@load="onMapLoad"
|
@load="onMapLoad"
|
||||||
>
|
>
|
||||||
<map-styles-buttons
|
|
||||||
v-if="!isMobile"
|
|
||||||
:styles="styles"
|
|
||||||
:actualStyle="mapOptions.style"
|
|
||||||
:setStyle="setStyle"
|
|
||||||
/>
|
|
||||||
<MglFullscreenControl />
|
<MglFullscreenControl />
|
||||||
<MglNavigationControl position="top-right" />
|
<MglNavigationControl position="top-right" />
|
||||||
<MglGeolocateControl position="top-right" />
|
<MglGeolocateControl position="top-right" />
|
||||||
<MglScaleControl />
|
<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>
|
</mgl-map>
|
||||||
</client-only>
|
</client-only>
|
||||||
<empty v-else icon="alert" :message="$t('map.alertMessage')" />
|
<empty v-else icon="alert" :message="$t('map.alertMessage')" />
|
||||||
@ -57,7 +56,7 @@
|
|||||||
|
|
||||||
<!-- eslint-disable vue/no-reserved-component-names -->
|
<!-- eslint-disable vue/no-reserved-component-names -->
|
||||||
<script>
|
<script>
|
||||||
import { isEmpty, toArray } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import mapboxgl from 'mapbox-gl'
|
import mapboxgl from 'mapbox-gl'
|
||||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
||||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
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 { mapQuery } from '~/graphql/MapQuery'
|
||||||
import mobile from '~/mixins/mobile'
|
import mobile from '~/mixins/mobile'
|
||||||
import Empty from '~/components/Empty/Empty'
|
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
|
const maxMobileWidth = 639 // on this width and smaller the mapbox 'MapboxGeocoder' search gets bigger
|
||||||
|
|
||||||
@ -75,7 +73,6 @@ export default {
|
|||||||
mixins: [mobile(maxMobileWidth)],
|
mixins: [mobile(maxMobileWidth)],
|
||||||
components: {
|
components: {
|
||||||
Empty,
|
Empty,
|
||||||
MapStylesButtons,
|
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
return {
|
return {
|
||||||
@ -87,6 +84,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isEmpty,
|
isEmpty,
|
||||||
mapboxgl,
|
mapboxgl,
|
||||||
|
legendOpen: false,
|
||||||
activeStyle: null,
|
activeStyle: null,
|
||||||
defaultCenter: [10.452764, 51.165707], // center of Germany: https://www.gpskoordinaten.de/karte/land/DE
|
defaultCenter: [10.452764, 51.165707], // center of Germany: https://www.gpskoordinaten.de/karte/land/DE
|
||||||
currentUserLocation: null,
|
currentUserLocation: null,
|
||||||
@ -140,12 +138,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.updateMapPosition()
|
||||||
|
window.addEventListener('resize', this.updateMapPosition)
|
||||||
|
|
||||||
this.currentUserLocation = await this.getUserLocation(this.currentUser.id)
|
this.currentUserLocation = await this.getUserLocation(this.currentUser.id)
|
||||||
this.currentUserCoordinates = this.currentUserLocation
|
this.currentUserCoordinates = this.currentUserLocation
|
||||||
? this.getCoordinates(this.currentUserLocation)
|
? this.getCoordinates(this.currentUserLocation)
|
||||||
: null
|
: null
|
||||||
this.addMarkersOnCheckPrepared()
|
this.addMarkersOnCheckPrepared()
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.updateMapPosition)
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'auth/user',
|
currentUser: 'auth/user',
|
||||||
@ -160,9 +164,6 @@ export default {
|
|||||||
this.posts
|
this.posts
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
styles() {
|
|
||||||
return toArray(this.availableStyles)
|
|
||||||
},
|
|
||||||
availableStyles() {
|
availableStyles() {
|
||||||
// https://docs.mapbox.com/api/maps/styles/
|
// https://docs.mapbox.com/api/maps/styles/
|
||||||
const availableStyles = {
|
const availableStyles = {
|
||||||
@ -181,7 +182,7 @@ export default {
|
|||||||
url: 'mapbox://styles/mapbox/dark-v10?optimize=true',
|
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)
|
availableStyles[key].title = this.$t('map.styles.' + key)
|
||||||
})
|
})
|
||||||
return availableStyles
|
return availableStyles
|
||||||
@ -211,6 +212,19 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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 }) {
|
onMapLoad({ map }) {
|
||||||
this.map = map
|
this.map = map
|
||||||
|
|
||||||
@ -230,85 +244,203 @@ export default {
|
|||||||
accessToken: this.$env.MAPBOX_TOKEN,
|
accessToken: this.$env.MAPBOX_TOKEN,
|
||||||
mapboxgl: this.mapboxgl,
|
mapboxgl: this.mapboxgl,
|
||||||
marker: false,
|
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
|
// create a popup, but don't add it to the map yet
|
||||||
this.markers.popup = new mapboxgl.Popup({
|
this.markers.popup = new mapboxgl.Popup({
|
||||||
closeButton: false,
|
closeButton: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
|
maxWidth: '300px',
|
||||||
})
|
})
|
||||||
|
|
||||||
this.map.on('mouseenter', 'markers', (e) => {
|
// show popup for given features at coordinates
|
||||||
// if (e.features[0].properties.type !== 'theUser') {}
|
const showPopup = (features, lngLat) => {
|
||||||
if (this.popupOnLeaveTimeoutId) {
|
if (this.popupOnLeaveTimeoutId) {
|
||||||
clearTimeout(this.popupOnLeaveTimeoutId)
|
clearTimeout(this.popupOnLeaveTimeoutId)
|
||||||
this.popupOnLeaveTimeoutId = null
|
this.popupOnLeaveTimeoutId = null
|
||||||
}
|
}
|
||||||
if (this.markers.popup.isOpen()) {
|
if (this.markers.popup.isOpen()) {
|
||||||
this.map.getCanvas().style.cursor = ''
|
|
||||||
this.markers.popup.remove()
|
this.markers.popup.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the cursor style as a UI indicator.
|
|
||||||
this.map.getCanvas().style.cursor = 'pointer'
|
this.map.getCanvas().style.cursor = 'pointer'
|
||||||
|
|
||||||
// Copy coordinates array.
|
const coordinates = features[0].geometry.coordinates.slice()
|
||||||
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>`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// Ensure that if the map is zoomed out such that multiple
|
// Ensure popup appears over the correct copy when map is zoomed out
|
||||||
// copies of the feature are visible, the popup appears
|
while (Math.abs(lngLat.lng - coordinates[0]) > 180) {
|
||||||
// over the copy being pointed to.
|
coordinates[0] += lngLat.lng > coordinates[0] ? 360 : -360
|
||||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
|
||||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the popup and set its coordinates
|
// Build popup content safely using DOM nodes (no raw HTML interpolation)
|
||||||
// based on the feature found.
|
const container = document.createElement('div')
|
||||||
this.markers.popup.setLngLat(coordinates).setHTML(description).addTo(this.map)
|
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()) {
|
if (this.markers.popup.isOpen()) {
|
||||||
this.popupOnLeaveTimeoutId = setTimeout(() => {
|
this.popupOnLeaveTimeoutId = setTimeout(() => {
|
||||||
this.map.getCanvas().style.cursor = ''
|
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()
|
this.loadMarkersIconsAndAddMarkers()
|
||||||
},
|
},
|
||||||
language(map) {
|
language(map) {
|
||||||
@ -370,6 +511,7 @@ export default {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
slug: user.slug,
|
slug: user.slug,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
locationName: user.location.name,
|
||||||
description: user.about ? user.about : undefined,
|
description: user.about ? user.about : undefined,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
@ -390,6 +532,7 @@ export default {
|
|||||||
id: this.currentUser.id,
|
id: this.currentUser.id,
|
||||||
slug: this.currentUser.slug,
|
slug: this.currentUser.slug,
|
||||||
name: this.currentUser.name,
|
name: this.currentUser.name,
|
||||||
|
locationName: this.currentUserLocation.name,
|
||||||
description: this.currentUser.about ? this.currentUser.about : undefined,
|
description: this.currentUser.about ? this.currentUser.about : undefined,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
@ -409,6 +552,7 @@ export default {
|
|||||||
id: group.id,
|
id: group.id,
|
||||||
slug: group.slug,
|
slug: group.slug,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
|
locationName: group.location.name,
|
||||||
description: group.about ? group.about : undefined,
|
description: group.about ? group.about : undefined,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
@ -428,6 +572,7 @@ export default {
|
|||||||
id: post.id,
|
id: post.id,
|
||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
name: post.title,
|
name: post.title,
|
||||||
|
locationName: post.eventLocation.name,
|
||||||
description: this.$filters.removeHtml(post.content),
|
description: this.$filters.removeHtml(post.content),
|
||||||
},
|
},
|
||||||
geometry: {
|
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
|
this.markers.isGeoJSON = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,15 +713,15 @@ export default {
|
|||||||
@import 'v-mapbox/dist/v-mapbox.css';
|
@import 'v-mapbox/dist/v-mapbox.css';
|
||||||
|
|
||||||
.map-page {
|
.map-page {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100vh - 4rem - #{$space-x-small});
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
z-index: 1;
|
||||||
|
|
||||||
.map-header {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 0 0 $space-xx-small;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mgl-map-wrapper {
|
.mgl-map-wrapper {
|
||||||
@ -558,9 +729,230 @@ export default {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 811px) {
|
.mgl-map-wrapper {
|
||||||
.map-page {
|
overflow: hidden;
|
||||||
height: calc(100vh - 6rem - 8rem);
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user