• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-nuxt3 / e295f783-924d-482f-957e-d7dd24de88dd

05 Feb 2026 11:03PM UTC coverage: 44.827% (+0.02%) from 44.812%
e295f783-924d-482f-957e-d7dd24de88dd

push

circleci

CircleCI Auto-merge
Auto-merge master to production after successful tests - Original commit: fix: Restore scrollable prop on MessageEditModal to keep footer visible

3691 of 8388 branches covered (44.0%)

Branch coverage included in aggregate %.

1673 of 3578 relevant lines covered (46.76%)

76.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.15
/components/PostMap.vue
1
<template>
2
  <div v-if="initialBounds">
7!
3
    <div v-if="!mapHidden">
7✔
4
      <div
5
        ref="mapcont"
6
        :style="'height: ' + mapHeight + 'px'"
7
        class="w-100 position-relative mb-1"
8
      >
9
        <div class="mapbox">
10
          <l-map
11
            ref="map"
12
            v-model:bounds="bounds"
13
            v-model:center="center"
14
            v-model:zoom="zoom"
15
            :style="'width: 100%; height: ' + mapHeight + 'px'"
16
            :min-zoom="minZoom"
17
            :max-zoom="maxZoom"
18
            :options="mapOptions"
19
            @ready="ready"
20
            @update:bounds="idle"
21
            @zoomend="idle"
22
            @moveend="idle"
23
            @dragend="dragEnd"
24
          >
25
            <l-tile-layer :url="osmtile()" :attribution="attribution()" />
2✔
26
            <div v-if="showMessages">
2!
27
              <ClusterMarker
28
                v-if="messagesForMap.length"
×
29
                :markers="messagesForMap"
30
                :map="mapObject"
31
                tag="post"
32
                @click="clusterClick"
33
              />
34
              <ClusterMarker
35
                v-if="!moved"
×
36
                :markers="secondaryMessagesForMap"
37
                :map="mapObject"
38
                tag="post"
39
                css-class="fadedMarker"
40
                @click="clusterClick"
×
41
              />
42
              <l-marker
43
                v-if="me?.settings?.mylocation && (me.lat || me.lng)"
44
                :lat-lng="[me.lat, me.lng]"
45
                @click="goHome"
46
              >
47
                <l-icon>
48
                  <BrowseHomeIcon />
49
                </l-icon>
50
                <l-tooltip>
51
                  This is where your postcode is. You can change your postcode
52
                  from Settings.
53
                </l-tooltip>
×
54
              </l-marker>
55
            </div>
56
            <div v-else-if="showGroups">
2✔
57
              <GroupMarker
58
                v-for="g in groupsInBounds"
59
                :key="'marker-' + g.id + '-' + zoom"
60
                :group="g"
61
                :size="largeGroupMarkers ? 'rich' : 'poor'"
1!
62
              />
63
            </div>
64
            <div v-if="showIsochrones">
2!
65
              <l-geo-json
66
                v-for="g in isochroneGEOJSONs"
67
                :key="'isochrone' + g.id"
68
                :geojson="g.json"
69
                :options="isochroneOptions"
70
              />
71
            </div>
72
          </l-map>
73
        </div>
74
      </div>
75
    </div>
76
  </div>
77
</template>
78
<script setup>
79
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
80
import { useRuntimeConfig } from 'nuxt/app'
81
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
82
import cloneDeep from 'lodash.clonedeep'
83
import { storeToRefs } from 'pinia'
84
import Wkt from 'wicket'
85
import { LGeoJson, LTooltip } from '@vue-leaflet/vue-leaflet'
86
import GroupMarker from './GroupMarker'
87
import BrowseHomeIcon from './BrowseHomeIcon'
88
import ClusterMarker from './ClusterMarker'
89
import { useGroupStore } from '~/stores/group'
90
import { useMessageStore } from '~/stores/message'
91
import {
92
  calculateMapHeight,
93
  loadLeaflet,
94
  attribution,
95
  osmtile,
96
} from '~/composables/useMap'
97
import { useMiscStore } from '~/stores/misc'
98
import { useIsochroneStore } from '~/stores/isochrone'
99
import { useAuthorityStore } from '~/stores/authority'
100
import { useAuthStore } from '~/stores/auth'
101
import 'leaflet-control-geocoder/dist/Control.Geocoder.css'
102
import 'leaflet-gesture-handling/dist/leaflet-gesture-handling.css'
103
import { useMe } from '~/composables/useMe'
104

105
const props = defineProps({
4✔
106
  showIsochrones: {
107
    type: Boolean,
108
    required: false,
109
    default: false,
110
  },
111
  initialBounds: {
112
    type: Array,
113
    required: true,
114
  },
115
  heightFraction: {
116
    type: Number,
117
    required: false,
118
    default: 3,
119
  },
120
  minZoom: {
121
    type: Number,
122
    required: false,
123
    default: 5,
124
  },
125
  maxZoom: {
126
    type: Number,
127
    required: false,
128
    default: 15,
129
  },
130
  postZoom: {
131
    type: Number,
132
    required: false,
133
    default: 10,
134
  },
135
  forceMessages: {
136
    type: Boolean,
137
    required: false,
138
    default: false,
139
  },
140
  groupid: {
141
    type: Number,
142
    required: false,
143
    default: null,
144
  },
145
  type: {
146
    type: String,
147
    required: false,
148
    default: 'All',
149
  },
150
  search: {
151
    type: String,
152
    required: false,
153
    default: null,
154
  },
155
  showMany: {
156
    type: Boolean,
157
    required: false,
158
    default: true,
159
  },
160
  region: {
161
    type: String,
162
    required: false,
163
    default: null,
164
  },
165
  canHide: {
166
    type: Boolean,
167
    required: false,
168
    default: false,
169
  },
170
  isochroneOverride: {
171
    type: Object,
172
    required: false,
173
    default: null,
174
  },
175
  authorityid: {
176
    type: Number,
177
    required: false,
178
    default: null,
179
  },
180
})
181

182
const { myGroups, myGroupsBoundingBox, myGroupIds } = useMe()
183

184
const emit = defineEmits([
185
  'update:ready',
186
  'update:showGroups',
187
  'update:bounds',
188
  'update:zoom',
189
  'update:centre',
190
  'update:loading',
191
  'update:moved',
192
  'groups',
193
  'messages',
194
  'idle',
195
  'minzoom',
196
  'searched',
197
])
198

199
const miscStore = useMiscStore()
200
const groupStore = useGroupStore()
201
const messageStore = useMessageStore()
202
const isochroneStore = useIsochroneStore()
203
const authorityStore = useAuthorityStore()
204
const authStore = useAuthStore()
205
const me = authStore.user
206

207
// Data properties as refs
208
const messageList = ref([])
209
const secondaryMessageList = ref([])
210
const moved = ref(false)
211
const mapObject = ref(null)
212
const manyToShow = ref(20)
213
const shownMany = ref(false)
214
const lastBounds = ref(null)
215
const lastBoundsFetch = ref(null)
216
const zoom = ref(5)
217
const destroyed = ref(false)
218
const mapIdle = ref(0)
219
const center = ref(null)
220
const bounds = ref(null)
221
const map = ref(null)
222
const mapcont = ref(null)
223

224
// Get isochroneBounds from the store
225
const { bounds: isochroneBounds } = storeToRefs(useIsochroneStore())
226

227
// Computed properties
228
const mapHeight = computed(() => {
229
  return calculateMapHeight(props.heightFraction)
230
})
231

232
const mapOptions = computed(() => {
233
  return {
3✔
234
    zoomControl: true,
235
    dragging: process.client && window?.L?.Browser?.mobile,
6!
236
    touchZoom: true,
237
    scrollWheelZoom: false,
238
    bounceAtZoomLimits: true,
239
    gestureHandling: true,
240
  }
241
})
242

243
const mapHidden = computed(() => {
244
  return props.canHide && miscStore?.get('hidepostmap')
6!
245
})
246

247
const showMessages = computed(() => {
248
  // We're zoomed in far enough or we're forcing ourselves to show them (but not so that it's silly)
249
  return (
250
    mapIdle.value > 0 &&
4!
251
    (zoom.value >= props.postZoom || (props.forceMessages && zoom.value >= 7))
252
  )
253
})
254

255
const showGroups = computed(() => {
256
  // Don't show until the map has been idle - there is an issue with markers not destroying properly which this
257
  // provokes.
258
  return mapIdle.value > 0 && !showMessages.value
6✔
259
})
260

261
const groups = computed(() => {
262
  const ret = []
5✔
263

264
  if (messageList.value) {
5✔
265
    messageList.value.forEach((m) => {
266
      if (!ret.includes(m.groupid)) {
3✔
267
        ret.push(m.groupid)
268
      }
269
    })
270
  }
271

272
  return ret
273
})
274

275
const largeGroupMarkers = computed(() => {
276
  // Can't get this to look sane.
277
  return false
278
})
279

280
const allGroups = computed(() => {
281
  return groupStore?.list
1!
282
})
283

284
const groupsInBounds = computed(() => {
285
  const ret = []
5✔
286

287
  try {
5✔
288
    // Reference map idle so that we recalc.
289
    const groups = mapIdle.value ? allGroups.value : []
5✔
290
    const boundsObj = mapObject.value ? mapObject.value.getBounds() : null
5✔
291

292
    if (!process.client && boundsObj) {
293
      // SSR - return all for SEO.
294
      for (const ix in groups) {
295
        const group = groups[ix]
296

297
        if (
298
          group.onmap &&
299
          (!props.region ||
300
            group.region.trim().toLowerCase() ===
301
              props.region.trim().toLowerCase())
302
        ) {
303
          ret.push(group)
304
        }
305
      }
306
    } else if (boundsObj) {
307
      for (const ix in groups) {
1✔
308
        const group = groups[ix]
2✔
309

310
        if (group.lat || group.lng) {
2✔
311
          try {
1✔
312
            if (
313
              group.onmap &&
1!
314
              group.publish &&
315
              boundsObj.contains([group.lat, group.lng]) &&
316
              (!props.region ||
317
                props.region.toLowerCase() === group.region.toLowerCase())
318
            ) {
319
              ret.push(group)
320
            }
321
          } catch (e) {
322
            console.log('Problem group', e)
×
323
          }
324
        }
325
      }
326
    }
327
  } catch (e) {
328
    console.log('Groups in bounds exception', e)
×
329
  }
330

331
  const sorted = ret.sort((a, b) => {
332
    return a.namedisplay
333
      .toLowerCase()
334
      .localeCompare(b.namedisplay.toLowerCase())
335
  })
336

337
  return sorted
338
})
339

340
const messagesForMap = computed(() => {
341
  return mapObject.value && messageList.value && messageList.value.length
×
342
    ? messageList.value
343
    : []
344
})
345

346
const isochrones = computed(() => {
347
  return props.isochroneOverride
2!
348
    ? [props.isochroneOverride]
349
    : isochroneStore?.list
2!
350
})
351

352
const isochroneGEOJSONs = computed(() => {
353
  const ret = []
1✔
354

355
  isochrones.value.forEach((i) => {
1✔
356
    const wkt = new Wkt.Wkt()
×
357
    try {
×
358
      wkt.read(i.polygon)
×
359
      ret.push({
360
        id: i.id,
361
        json: wkt.toJson(),
362
      })
363
    } catch (e) {
364
      console.log('WKT error', location, e)
×
365
    }
366
  })
367

368
  return ret
369
})
370

371
const isochroneOptions = computed(() => {
372
  return {
373
    fillColor: 'darkblue',
374
    fill: true,
375
    fillOpacity: 0.2,
376
    color: 'darkblue',
377
  }
378
})
379

380
const messageIds = computed(() => {
381
  return messageList.value.map((m) => m.id)
382
})
383

384
const secondaryMessagesForMap = computed(() => {
385
  if (secondaryMessageList.value?.length > 200) {
×
386
    // So many posts that the precise numbers no longer matter that much.  So return all the ones we have fetched
387
    // rather than spend CPU on filtering (which is a significant issue on slow browsers).
388
    return secondaryMessageList.value
389
  } else {
390
    // Return anything relevant we have fetched which is not already in the primary one.
391
    return secondaryMessageList.value.filter((m) => {
392
      return (
393
        !messageIds.value[m.id] &&
×
394
        (!props.groupid || m.groupid === props.groupid) &&
395
        (props.type === 'All' || m.type === props.type)
396
      )
397
    })
398
  }
399
})
400
// Watchers
401
watch(bounds, (newVal, oldVal) => {
4✔
402
  if (!showGroups.value) {
×
403
    getMessages()
404
  }
405
})
406

407
watch(showGroups, (newVal) => {
408
  if (!newVal && !props.authorityid) {
1!
409
    getMessages()
410
  }
411
})
412

413
watch(zoom, (newVal) => {
414
  if (newVal < props.postZoom && !props.forceMessages) {
×
415
    emit('update:showGroups', true)
416
  } else {
417
    emit('update:showGroups', false)
418
  }
419
})
420

421
watch(isochroneBounds, (newVal) => {
422
  if (newVal && mapObject.value) {
×
423
    // Make the map show the isochrone view.
424
    try {
×
425
      mapObject.value.fitBounds(newVal)
×
426
    } catch (e) {
427
      // This happens when leaflet is destroyed.
428
      console.log('Ignore flyToBounds exception', e)
×
429
    }
430
  }
431
  getMessages()
×
432
})
433

434
watch(
435
  groups,
436
  (newval) => {
437
    emit('groups', newval)
5✔
438
  },
439
  { immediate: true }
440
)
441

442
watch(
443
  () => props.type,
444
  () => {
445
    lastBounds.value = null
×
446

447
    if (zoom.value >= props.postZoom || props.search) {
448
      getMessages()
449
    }
450
  }
451
)
452

453
watch(
454
  () => props.search,
455
  () => {
456
    lastBounds.value = null
×
457
    getMessages()
458
  }
459
)
460

461
watch(
462
  () => props.groupid,
463
  (groupid) => {
464
    lastBounds.value = null
465

466
    if (groupid) {
×
467
      // Use the bounding box for the group.
468
      const group = myGroup(groupid)
×
469
      console.log('Got group', group)
470

471
      if (group.bbox) {
×
472
        const wkt = new Wkt.Wkt()
×
473
        try {
×
474
          wkt.read(group.bbox)
×
475
          const obj = wkt.toObject()
476
          const thisbounds = obj.getBounds()
×
477
          const sw = thisbounds.getSouthWest()
478
          const ne = thisbounds.getNorthEast()
479

480
          const latLngBounds = new window.L.LatLngBounds([
481
            [sw.lat, sw.lng],
482
            [ne.lat, ne.lng],
483
          ]).pad(0.1)
484

485
          // For reasons I don't understand, leaflet throws errors if we don't make these local here.
486
          const swlat = latLngBounds.getSouthWest().lat
487
          const swlng = latLngBounds.getSouthWest().lng
488
          const nelat = latLngBounds.getNorthEast().lat
489
          const nelng = latLngBounds.getNorthEast().lng
490

491
          mapObject.value.flyToBounds([
×
492
            [swlat, swlng],
493
            [nelat, nelng],
494
          ])
495

496
          moved.value = true
497
        } catch (e) {
498
          console.log('WKT error', location, e)
×
499
        }
500
      }
501
    }
502
  }
503
)
504

505
watch(groupsInBounds, (newval) => {
506
  emit(
1✔
507
    'groups',
508
    groupsInBounds.value.map((g) => g.id)
509
  )
510
})
511
// Add missing myGroup function
512
function myGroup(groupId) {
×
513
  return groupStore.list?.find((g) => g.id === groupId) || {}
×
514
}
515

516
// Lifecycle hooks
517
onMounted(async () => {
4✔
518
  if (mapHidden.value) {
4✔
519
    // Say we're ready so the parent can crack on.
520
    emit('update:ready', true)
521

522
    // Fetch the messages.
523
    getMessages()
524
  }
525

526
  await loadLeaflet()
527
})
528

529
onBeforeUnmount(() => {
530
  destroyed.value = true
×
531
  if (markerFixInterval.value) {
×
532
    clearInterval(markerFixInterval.value)
533
  }
534
})
535

536
const markerFixInterval = ref(null)
4✔
537

538
function fixDefaultMarkers() {
×
539
  if (!mapcont.value) return
×
540

541
  // Find any default Leaflet marker icons and replace with our custom icon
542
  const defaultMarkers = mapcont.value.querySelectorAll(
×
543
    'img[src*="marker-icon"]'
544
  )
545

546
  defaultMarkers.forEach((img) => {
×
547
    img.src = '/mapmarker.gif'
×
548
    img.style.width = '15px'
549
    img.style.height = '19px'
550
    img.style.marginLeft = '-7px'
551
    img.style.marginTop = '-19px'
552
  })
553

554
  // Stop checking once no default markers found for a while
555
  if (defaultMarkers.length === 0) {
×
556
    markerFixCount.value++
557
    if (markerFixCount.value > 10) {
×
558
      clearInterval(markerFixInterval.value)
559
      markerFixInterval.value = null
560
    }
561
  } else {
562
    markerFixCount.value = 0
563
  }
564
}
565

566
const markerFixCount = ref(0)
4✔
567

568
function startMarkerFix() {
1✔
569
  if (!markerFixInterval.value) {
1✔
570
    markerFixCount.value = 0
571
    markerFixInterval.value = setInterval(fixDefaultMarkers, 500)
572
  }
573
}
574

575
// Methods
576
async function ready() {
1✔
577
  emit('update:ready', true)
578
  mapObject.value = map.value.leafletObject
579

580
  if (process.client && mapObject.value) {
1!
581
    try {
1✔
582
      mapObject.value.fitBounds(props.initialBounds)
1✔
583

584
      // Start checking for and fixing default markers
585
      startMarkerFix()
586

587
      const runtimeConfig = useRuntimeConfig()
1✔
588

589
      const { Geocoder } = await import('leaflet-control-geocoder/src/control')
1✔
590
      const { Photon } = await import(
×
591
        'leaflet-control-geocoder/src/geocoders/photon'
592
      )
593

594
      new Geocoder({
×
595
        placeholder: 'Search for a place...',
596
        defaultMarkGeocode: false,
597
        geocoder: new Photon({
598
          geocodingQueryParams: {
599
            bbox: '-7.57216793459, 49.959999905, 1.68153079591, 58.6350001085',
600
          },
601
          nameProperties: [
602
            'name',
603
            'street',
604
            'suburb',
605
            'hamlet',
606
            'town',
607
            'city',
608
          ],
609
          serviceUrl: runtimeConfig.public.GEOCODE,
610
        }),
611
        collapsed: false,
612
      })
613
        .on('markgeocode', async function (e) {
614
          if (e && e.geocode && e.geocode.bbox) {
×
615
            // Empty out the query box so that the dropdown closes.  Note that "this" is the control object,
616
            // which is why this isn't in a separate method.
617
            console.log('Search for place', e)
×
618
            this.moved = true
619
            this.setQuery('')
620

621
            // If we don't find anything at this location we will want to zoom out.
622
            shownMany.value = false
623

624
            // For some reason we need to take a copy of the latlng bounds in the event before passing it to
625
            // flyToBounds.
626
            const flyTo = e.geocode.bbox
×
627
            const L = await import('leaflet/dist/leaflet-src.esm')
628
            const newBounds = new L.LatLngBounds(
629
              new L.LatLng(flyTo.getSouthWest().lat, flyTo.getSouthWest().lng),
630
              new L.LatLng(flyTo.getNorthEast().lat, flyTo.getNorthEast().lng)
631
            )
632
            // Move the map to the location we've found.
633
            map.value.leafletObject.flyToBounds(newBounds)
×
634
            emit('searched')
635
          }
636
        })
637
        .addTo(mapObject.value)
638
    } catch (e) {
639
      // This is usually caused by leaflet.
640
      console.log('Ignore leaflet exception', e)
×
641
    }
642
  }
643
}
644
function clusterClick() {
×
645
  moved.value = true
×
646
  idle()
647
}
648

649
function idle() {
1✔
650
  mapIdle.value++
1✔
651

652
  try {
1✔
653
    if (mapObject.value) {
1!
654
      // We need to update the parent about our zoom level and whether we are showing the posts or groups.
655
      const newBounds = mapObject.value.getBounds().toBBoxString()
1✔
656

657
      if (newBounds !== lastBounds.value) {
1✔
658
        lastBounds.value = newBounds
659

660
        if (showMessages.value) {
1!
661
          getMessages()
662
        }
663
      }
664

665
      emit('update:bounds', mapObject.value.getBounds())
666
      emit('update:zoom', mapObject.value.getZoom())
667
      emit('update:centre', mapObject.value.getCenter())
668
      emit('idle', mapObject.value)
669
    }
670
  } catch (e) {
671
    console.error('Error in map idle', e)
×
672
  }
673
}
674

675
async function getMessages() {
1✔
676
  let messages = []
1✔
677
  secondaryMessageList.value = []
1✔
678

679
  emit('update:loading', true)
680

681
  let bounds = new window.L.LatLngBounds(props.initialBounds)
1✔
682

683
  if (mapObject.value) {
1!
684
    // Get the messages from the server which are in the bounds of the map.
685
    bounds = mapObject.value.getBounds()
686

687
    if (mapObject.value.getZoom() < props.minZoom) {
×
688
      // The parent may replace us with something else at this point, e.g. with a group map.  But maybe not.
689
      // Their call.
690
      emit('minzoom', mapObject.value.getZoom())
691
    }
692
  }
693

694
  const swlat = bounds.getSouthWest().lat
1✔
695
  const swlng = bounds.getSouthWest().lng
696
  const nelat = bounds.getNorthEast().lat
697
  const nelng = bounds.getNorthEast().lng
698
  let ret = null
1✔
699

700
  if (moved.value) {
1!
701
    // The map has been moved.
702
    if (props.search) {
×
703
      // Search within the bounds of the map.
704
      console.log('GetMessages - moved, search within map bounds')
705
      ret = await messageStore.search({
706
        messagetype: props.type,
707
        search: props.search,
708
        swlat,
709
        swlng,
710
        nelat,
711
        nelng,
712
      })
713
    } else {
714
      // Just fetch the bounds of the map.
715
      console.log('GetMessages - moved, fetch within map bounds')
716
      ret = await messageStore.fetchInBounds(swlat, swlng, nelat, nelng)
1!
717
    }
718
  } else if (props.groupid) {
719
    // We have been asked to show a specific group.
720
    if (props.search) {
×
721
      // So search within that group.
722
      console.log('GetMessages - search on specific group')
723
      ret = await messageStore.search({
724
        messagetype: props.type,
725
        search: props.search,
726
        groupids: [props.groupid],
727
      })
728
    } else {
729
      // Just fetch that the messages on that group.
730
      console.log('GetMessages - fetch on specific group')
731
      ret = await messageStore.fetchMyGroups(props.groupid)
732

733
      if (!mapHidden.value) {
×
734
        // Fetch all the messages in the map bounds too, so that we can show others as secondary.
735
        // No need to bother if the map isn't showing - they don't appear in the post list.
736
        secondaryMessageList.value = await messageStore.fetchInBounds(
737
          swlat,
738
          swlng,
739
          nelat,
740
          nelng
1!
741
        )
742
      }
743
    }
744
  } else if (props.authorityid) {
745
    // We are trying to show posts within a specific authority
746
    console.log('Get messages within authority')
×
747
    ret = await authorityStore.fetchMessages(props.authorityid)
1!
748

749
    // Don't fetch the other messages - this may return so many it's too much load on the client.
750
  } else if (props.showIsochrones) {
751
    // We are trying to show posts nearby.
752
    if (isochrones.value?.length) {
1!
753
      // We have isochrones.
754
      if (props.search) {
1!
755
        // We don't have a search-within-isochones call.  But we can fetch all the messages in the isochrones,
756
        // and also search within the map, and take the intersection.
757
        console.log('GetMessages - search in isochrones')
×
758
        const isoret = await isochroneStore.fetchMessages()
×
759
        const searchret = await messageStore.search({
760
          messagetype: props.type,
761
          search: props.search,
762
          swlat,
763
          swlng,
764
          nelat,
765
          nelng,
766
        })
767

768
        const ids = {}
769

770
        ret = searchret.filter((i) => {
×
771
          if (isoret.find((el) => el.id === i.id) && !ids[i.id]) {
×
772
            ids[i.id] = true
773
            return true
774
          } else {
775
            return false
776
          }
777
        })
778

779
        secondaryMessageList.value = searchret
780
      } else {
781
        // Fetch the messages in our isochrones.
782
        console.log('GetMessages - fetch in isochrones')
1✔
783
        ret = await isochroneStore.fetchMessages()
784

785
        // Fetch the messages in bounds too, so that we can show those as secondary.
786
        secondaryMessageList.value = await messageStore.fetchInBounds(
787
          swlat,
788
          swlng,
789
          nelat,
790
          nelng
×
791
        )
792
      }
793
    } else if (myGroups.value?.length) {
794
      // We don't, which will be because we don't have a location.
795
      // Use the bounding boxes of the groups we are in.
796
      const groupbounds = myGroupsBoundingBox.value
×
797

798
      if (props.search) {
×
799
        console.log('GetMessages - search within group bounds')
800
        ret = await messageStore.search({
801
          messagetype: props.type,
802
          search: props.search,
803
          swlat: groupbounds[0][0],
804
          swlng: groupbounds[0][1],
805
          nelat: groupbounds[1][0],
806
          nelng: groupbounds[1][1],
807
        })
808
      } else {
809
        // Just fetch the messages within those bounds.    This will show a bit more than the strict
810
        // "all my groups" option, but not as much as we might show using the map bounds.
811
        console.log(
812
          'GetMessages - fetch in group bounds',
813
          JSON.stringify(groupbounds)
814
        )
815

816
        if (lastBoundsFetch.value !== JSON.stringify(groupbounds)) {
×
817
          lastBoundsFetch.value = JSON.stringify(groupbounds)
818

819
          ret = await messageStore.fetchInBounds(
820
            groupbounds[0][0],
821
            groupbounds[0][1],
822
            groupbounds[1][0],
823
            groupbounds[1][1],
824
            props.groupid
825
          )
826
        } else {
827
          console.log('Already fetched that.')
828
        }
829
      }
830
    } else if (props.search) {
×
831
      // We have no isochrones and no groups.  Do nothing - we expect code elsewhere to prompt for a location.
832
      // Search within the bounds of the map.
833
      console.log(
834
        'GetMessages - no isochrones, no groups, search within map bounds'
835
      )
836
      ret = await messageStore.search({
837
        messagetype: props.type,
838
        search: props.search,
839
        swlat,
840
        swlng,
841
        nelat,
842
        nelng,
843
      })
844
    } else {
845
      // Just fetch the bounds of the map.
846
      console.log(
847
        'GetMessages - no isochrones, no groups, fetch within map bounds'
848
      )
849
      ret = await messageStore.fetchInBounds(swlat, swlng, nelat, nelng)
×
850
    }
851
  } else if (myGroups.value?.length) {
852
    if (props.search) {
×
853
      const groupbounds = myGroupsBoundingBox.value
×
854

855
      console.log(
×
856
        'GetMessages - some groups, search within group bounds',
857
        groupbounds,
858
        myGroupIds
859
      )
860
      ret = await messageStore.search({
861
        messagetype: props.type,
862
        search: props.search,
863
        swlat: groupbounds[0][0],
864
        swlng: groupbounds[0][1],
865
        nelat: groupbounds[1][0],
866
        nelng: groupbounds[1][1],
867
        groupids: myGroupIds,
868
      })
869
    } else {
870
      // We have groups, so fetch the messages in those groups.
871
      console.log('GetMessages - some groups, fetch groups')
×
872
      ret = await messageStore.fetchMyGroups()
873

874
      // Get the messages in the map bounds too, so that we can show others as secondary.
875
      secondaryMessageList.value = await messageStore.fetchInBounds(
876
        swlat,
877
        swlng,
878
        nelat,
879
        nelng
880
      )
881
    }
882
  } else {
883
    // We have no groups, so fetch the messages in the map bounds.
884
    console.log('GetMessages - no groups, fetch in map bounds')
×
885
    ret = await messageStore.fetchInBounds(swlat, swlng, nelat, nelng)
886
  }
887

888
  if (ret && !destroyed.value) {
1✔
889
    messages = ret
890
  }
891

892
  if (messages?.length) {
3!
893
    if (props.groupid) {
894
      messages = messages.filter((m) => {
895
        return m.groupid === props.groupid
896
      })
897
    }
898

899
    if (props.type !== 'All') {
1!
900
      messages = messages.filter((m) => {
901
        return m.type === props.type
902
      })
903
    }
904
  }
905

906
  let countInBounds = 0
1✔
907

908
  messages.forEach((m) => {
909
    if (swlat <= m.lat && m.lat <= nelat && swlng <= m.lng && m.lng <= nelng) {
3✔
910
      countInBounds++
911
    }
912
  })
913

914
  if (props.isochroneOverride) {
1!
915
    // Don't want to autozoom out in this case - stay where we're put.
916
    shownMany.value = true
1!
917
  } else if (countInBounds >= manyToShow.value) {
918
    // We have seen lots, so we don't need to do the auto zoom out thing now.
919
    shownMany.value = true
1!
920
  } else if (
921
    !props.search &&
2!
922
    props.showMany &&
923
    countInBounds < manyToShow.value &&
924
    !shownMany.value
925
  ) {
926
    // If we haven't got more than 1 message at this zoom level, zoom out.  That means we'll always show at
927
    // least something.  This is useful when we search for a specific place.
928
    const currzoom = mapObject.value.getZoom()
×
929
    if (currzoom > props.minZoom) {
×
930
      console.log(
931
        'Not enough showing, zoom out',
932
        countInBounds,
933
        manyToShow.value,
934
        currzoom,
935
        props.minZoom
936
      )
937
      mapObject.value.setZoom(currzoom - 1)
938
      moved.value = true
939
    } else {
940
      shownMany.value = true
941
    }
942
  }
943

944
  messageList.value = messages || []
1!
945
  emit('messages', messageList.value)
946
  emit('update:loading', false)
947

948
  return cloneDeep(messages)
949
}
950

951
async function goHome() {
×
952
  await loadLeaflet()
×
953

954
  if (me.lat || me.lng) {
955
    mapObject.value.flyTo(new window.L.LatLng(me.lat, me.lng))
956
  }
957
}
958

959
function dragEnd(e) {
×
960
  moved.value = true
×
961
  emit('update:moved', true)
962
  idle()
963
}
964
</script>
965
<style scoped lang="scss">
966
@import 'bootstrap/scss/functions';
967
@import 'bootstrap/scss/variables';
968
@import 'bootstrap/scss/mixins/_breakpoints';
969

970
/* Hide default Leaflet markers until our fix replaces them */
971
:deep(img[src*='marker-icon']) {
972
  display: none !important;
973
}
974

975
.mapbox {
976
  width: 100%;
977
  top: 0px;
978
  left: 0;
979
  border: 1px solid $color-gray--light;
980
}
981

982
:deep(.leaflet-control-geocoder) {
983
  right: 30px;
984
}
985

986
@media screen and (max-width: 360px) {
987
  :deep(.leaflet-control-geocoder-form input) {
988
    max-width: 200px;
989
  }
990
}
991

992
@include media-breakpoint-up(md) {
993
  :deep(.leaflet-control-geocoder-form input) {
994
    height: calc(1.25em + 1rem + 2px);
995
    padding: 0.5rem 1rem;
996
    font-size: 1rem !important;
997
    line-height: 1.25;
998
    border-radius: 0.3rem;
999
  }
1000
}
1001

1002
:deep(.handle) {
1003
  content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAB3RJTUUH5AoLCyYQDowQNQAAAHRJREFUOMtjYBhMQJ6BgSGLgYHhP5TPzMDA4AXlG0DFuBkYGDKQ1KAAmGZ9KB+muY6BgUEYqjkaKuaAzYD/DAwM6mg212Cx2Z2QV5A1CxBjMwMWP9PXZuQwIMtmGDAj12ZkQJbNyJrJtpmBEpuRA9GBYcgBALMUJBS9QtP6AAAAAElFTkSuQmCC');
1004
}
1005

1006
:deep(.top) {
1007
  z-index: 1000 !important;
1008
}
1009

1010
.pauto {
1011
  pointer-events: auto;
1012
}
1013

1014
:deep(.fadedMarker) {
1015
  filter: grayscale(100%);
1016
  z-index: -1 !important;
1017

1018
  &.icon,
1019
  .icon {
1020
    border: 5px solid $color-gray--light;
1021
    opacity: 0.5;
1022
  }
1023
}
1024
</style>
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc