Structures.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <template>
  2. <LayoutContainer>
  3. <v-responsive :aspect-ratio="1" width="100%">
  4. <!-- /!\ We do not use the nuxt-leaflet library here,
  5. because in its current version it's way too slow to deal with hundreds of markers -->
  6. <div id="map" />
  7. </v-responsive>
  8. <div class="advice">
  9. {{ $t("click_on_land_to_go_there") }}
  10. </div>
  11. <v-row class="map-shortcuts">
  12. <v-col v-for="shortcut in shortcuts" :key="shortcut.src" cols="2">
  13. <div @click="setMapBounds(shortcut.bounds)">
  14. <nuxt-img
  15. :src="shortcut.src"
  16. :alt="shortcut.alt"
  17. />
  18. </div>
  19. </v-col>
  20. </v-row>
  21. </LayoutContainer>
  22. </template>
  23. <script lang="ts">
  24. import 'leaflet/dist/leaflet.css'
  25. import {
  26. Layer,
  27. LatLngBounds,
  28. LatLngBoundsExpression, LatLngBoundsLiteral,
  29. LatLngTuple,
  30. MapOptions,
  31. Map
  32. } from 'leaflet'
  33. import Vue from 'vue'
  34. let L: any
  35. if (typeof window !== 'undefined') {
  36. // only require leaflet on client-side
  37. L = require('leaflet')
  38. }
  39. declare module 'vue/types/vue' {
  40. interface Vue {
  41. structures: Array<Structure>,
  42. zoomRequired: boolean,
  43. map: Map,
  44. defaultBounds: LatLngBounds
  45. }
  46. }
  47. export default Vue.extend({
  48. props: {
  49. structures: {
  50. type: Array as () => Array<Structure>,
  51. required: false,
  52. default: () => []
  53. }
  54. },
  55. data (): object {
  56. const options: MapOptions = { scrollWheelZoom: false, zoomSnap: 0.25 }
  57. return {
  58. map: L.map('map', options),
  59. defaultBounds: new LatLngBounds([51.03, -5.78], [41.2, 9.70]),
  60. shortcuts: [
  61. { src: '/images/metropole.png', alt: 'Metropole', bounds: [[51.03, -5.78], [41.2, 9.70]] },
  62. { src: '/images/guadeloupe.png', alt: 'Guadeloupe', bounds: [[16.62, -62.03], [15.74, -60.97]] },
  63. { src: '/images/martinique.png', alt: 'Martinique', bounds: [[14.95, -61.43], [14.28, -60.60]] },
  64. { src: '/images/mayotte.png', alt: 'Mayotte', bounds: [[-12.51, 44.86], [-13.19, 45.45]] },
  65. { src: '/images/la_reunion.png', alt: 'La Réunion', bounds: [[-20.65, 54.92], [-21.65, 56.15]] },
  66. { src: '/images/guyane.png', alt: 'Guyane', bounds: [[6.24, -54.62], [1.87, -50.59]] }
  67. ],
  68. zoomRequired: false
  69. }
  70. },
  71. watch: {
  72. structures (oldVal: Array<Structure>, newVal: Array<Structure>): void {
  73. if (oldVal !== newVal) {
  74. this.populateMarkers()
  75. }
  76. if (this.zoomRequired) {
  77. this._fitBoundsToMarkers()
  78. this.zoomRequired = false
  79. }
  80. }
  81. },
  82. mounted (): void {
  83. const defaultCenter: LatLngTuple = [46.71, 1.94]
  84. const defaultZoom: number = 5.75
  85. const layerSource: string = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png'
  86. const attribution: string = '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
  87. this.map.setView(defaultCenter, defaultZoom)
  88. L.tileLayer(layerSource, { attribution }).addTo(this.map)
  89. this.populateMarkers()
  90. this.map.on('zoomend moveend', () => {
  91. this.$emit('boundsUpdated', this.map.getBounds())
  92. })
  93. },
  94. beforeDestroy (): void {
  95. if (this.map) {
  96. this.map.remove()
  97. }
  98. },
  99. methods: {
  100. populateMarkers (): void {
  101. // remove any existant marker layers
  102. this.map.eachLayer((layer: Layer) => {
  103. if (layer instanceof L.MarkerClusterGroup) {
  104. this.map.removeLayer(layer)
  105. }
  106. })
  107. // populate new layer
  108. const clusters = L.markerClusterGroup()
  109. for (const s of this.structures) {
  110. if (!s.latitude || !s.longitude) { continue }
  111. const marker = L.marker([s.latitude, s.longitude])
  112. marker.bindPopup(`<b>${s.name}</b><br/>${s.postalCode} ${s.addressCity}<br/><a href="${s.website}" target="_blank">${s.website}</a>`)
  113. clusters.addLayer(marker)
  114. }
  115. this.map.addLayer(clusters)
  116. },
  117. setMapBounds (bounds: LatLngBoundsExpression): void {
  118. this.map.fitBounds(bounds) // << without this, the new bounds may not be properly set when the current view overlaps the new bounds.
  119. },
  120. resetBounds (): void {
  121. this.setMapBounds(this.defaultBounds)
  122. },
  123. zoomOnResults (): void {
  124. this.zoomRequired = true
  125. },
  126. _fitBoundsToMarkers (): void {
  127. const coords: LatLngBoundsLiteral = this.structures.filter(
  128. (s) => {
  129. return (
  130. s.latitude && typeof s.latitude !== 'undefined' &&
  131. s.longitude && typeof s.latitude !== 'undefined'
  132. )
  133. }
  134. ).map(
  135. (s) => { return [s.latitude as number, s.longitude as number] as LatLngTuple }
  136. )
  137. if (!coords) {
  138. return
  139. }
  140. this.setMapBounds(coords)
  141. }
  142. }
  143. })
  144. </script>
  145. <style scoped>
  146. #map {
  147. height: 100%;
  148. width: 100%;
  149. }
  150. .advice {
  151. margin: 1rem 0;
  152. color: #262626;
  153. font-weight: 750;
  154. text-align: center;
  155. font-size: 0.9rem;
  156. width: 100%;
  157. }
  158. .map-shortcuts {
  159. padding: 12px 6px;
  160. }
  161. .map-shortcuts img {
  162. border: solid 1px #000;
  163. width: 75px;
  164. height: 75px;
  165. }
  166. </style>