Structures.vue 5.1 KB

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