| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- <template>
- <LayoutContainer>
- <v-responsive :aspect-ratio="1" width="100%">
- <!-- /!\ We do not use the nuxt-leaflet library here,
- because in its current version it's way too slow to deal with hundreds of markers -->
- <div id="map" />
- </v-responsive>
- <div class="advice">
- {{ $t("click_on_land_to_go_there") }}
- </div>
- <v-row class="map-shortcuts">
- <v-col
- v-for="shortcut in shortcuts"
- :key="shortcut.src"
- cols="4"
- lg="2"
- class="px-1"
- >
- <div
- class="map-shortcut"
- @click="setMapBounds(shortcut.bounds)"
- >
- <v-img
- :src="shortcut.src"
- :alt="shortcut.alt"
- :title="shortcut.alt"
- width="85"
- height="85"
- />
- <div class="map-shortcut-overlay">
- <font-awesome-icon class="icon" icon="search" />
- </div>
- </div>
- </v-col>
- </v-row>
- </LayoutContainer>
- </template>
- <script lang="ts">
- import 'leaflet/dist/leaflet.css'
- import Vue from 'vue'
- let L: any
- if (process.client) { // only require leaflet on client-side
- L = require('leaflet')
- }
- declare module 'vue/types/vue' {
- interface Vue {
- structures: Array<Structure>,
- zoomRequired: boolean,
- map: any,
- defaultBounds: L.LatLngBounds,
- shortcuts: Array<{src: string, alt: string, bounds: number[][] }>
- }
- }
- const METROPOLE_BOUNDS = new L.LatLngBounds([51.03, -5.78], [41.2, 9.70])
- export default Vue.extend({
- props: {
- structures: {
- type: Array as () => Array<Structure>,
- required: false,
- default: () => []
- }
- },
- data () {
- return {
- map: null,
- defaultBounds: METROPOLE_BOUNDS,
- shortcuts: [
- { src: '/images/metropole.png', alt: 'Metropole', bounds: [[51.03, -5.78], [41.2, 9.70]] },
- { src: '/images/guadeloupe.png', alt: 'Guadeloupe', bounds: [[16.62, -62.03], [15.74, -60.97]] },
- { src: '/images/martinique.png', alt: 'Martinique', bounds: [[14.95, -61.43], [14.28, -60.60]] },
- { src: '/images/mayotte.png', alt: 'Mayotte', bounds: [[-12.51, 44.86], [-13.19, 45.45]] },
- { src: '/images/la_reunion.png', alt: 'La Réunion', bounds: [[-20.65, 54.92], [-21.65, 56.15]] },
- { src: '/images/guyane.png', alt: 'Guyane', bounds: [[6.24, -54.62], [1.87, -50.59]] }
- ],
- zoomRequired: false,
- nextZoomIsDefault: false,
- firstPopulate: true
- }
- },
- watch: {
- structures (oldVal: Array<Structure>, newVal: Array<Structure>): void {
- if (oldVal !== newVal) {
- this.populateMarkers()
- }
- if (this.zoomRequired) {
- this.fitBoundsToMarkers()
- }
- }
- },
- async mounted () {
- const defaultCenter: L.LatLngTuple = [46.71, 1.94]
- const defaultZoom: number = 5.5
- const layerSource: string = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png'
- const attribution: string = '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
- const options: L.MapOptions = { scrollWheelZoom: false, zoomSnap: 0.25 }
- this.map = L.map('map', options)
- this.map.setView(defaultCenter, defaultZoom)
- L.tileLayer(layerSource, { attribution }).addTo(this.map)
- await this.populateMarkers()
- this.map.on('zoomend moveend', () => {
- this.$emit('boundsUpdated', this.map.getBounds())
- })
- if (this.zoomRequired) {
- this.fitBoundsToMarkers()
- }
- },
- beforeDestroy (): void {
- if (this.map) {
- this.map.remove()
- }
- },
- methods: {
- populateMarkers () {
- // remove any existant marker layers
- this.map.eachLayer((layer: L.Layer) => {
- if (layer instanceof L.MarkerClusterGroup) {
- this.map.removeLayer(layer)
- }
- })
- // populate new layer
- const clusters = L.markerClusterGroup()
- for (const s of this.structures) {
- if (!s.latitude || !s.longitude) { continue }
- const marker = L.marker([s.latitude, s.longitude])
- marker.bindPopup(`<b>${s.name}</b><br/>${s.postalCode} ${s.addressCity}<br/><a href="${s.website}" target="_blank">${s.website}</a>`)
- clusters.addLayer(marker)
- }
- this.map.addLayer(clusters)
- if (this.firstPopulate) {
- if (this.structures.length > 0) { // map is considered as mounted only when the first results are diplayed on it
- this.$emit('mounted')
- this.firstPopulate = false
- }
- } else {
- this.$emit('populated')
- }
- },
- setMapBounds (bounds: L.LatLngBoundsExpression, padding: [number, number] = [0, 0]): void {
- this.map.fitBounds(bounds, { padding }) // << without this, the new bounds may not be properly set when the current view overlaps the new bounds.
- },
- resetBounds (): void {
- this.setMapBounds(
- this.defaultBounds,
- (this.defaultBounds === METROPOLE_BOUNDS ? [0, 0] : [80, 80]) as [number, number]
- )
- },
- fitNextResults (setAsDefault = false) {
- this.zoomRequired = true
- this.nextZoomIsDefault = setAsDefault
- },
- fitBoundsToMarkers (): boolean {
- const coords: L.LatLngBoundsLiteral = this.structures.filter(
- (s: Structure) => {
- return (
- s.latitude && typeof s.latitude !== 'undefined' &&
- s.longitude && typeof s.latitude !== 'undefined'
- )
- }
- ).map(
- (s: Structure) => { return [s.latitude as number, s.longitude as number] as L.LatLngTuple }
- )
- if (!(coords.length > 0)) {
- return false
- }
- this.setMapBounds(coords, [80, 80])
- if (this.nextZoomIsDefault) {
- this.defaultBounds = coords
- }
- return true
- }
- }
- })
- </script>
- <style scoped lang="scss">
- #map {
- height: 100%;
- width: 100%;
- }
- .advice {
- margin: 1rem 0;
- color: #262626;
- font-weight: 750;
- text-align: center;
- font-size: 0.9rem;
- width: 100%;
- }
- .map-shortcuts {
- padding: 12px 6px;
- }
- .map-shortcut {
- position: relative;
- border: solid 1px #000;
- height: 87px;
- width: 87px;
- }
- .map-shortcut-overlay {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- height: 100%;
- width: 100%;
- opacity: 0;
- cursor: pointer;
- }
- .map-shortcut:hover .map-shortcut-overlay {
- opacity: 0.5;
- }
- .map-shortcut-overlay>.icon {
- color: #595959;
- font-size: 38px;
- position: absolute;
- top: 50%;
- left: 50%;
- -webkit-transform: translate(-50%, -50%);
- -ms-transform: translate(-50%, -50%);
- transform: translate(-50%, -50%);
- text-align: center;
- }
- </style>
|