Skip to content

Commit 007c46d

Browse files
- ADD: Added person area map to trial statistics page.
-
1 parent b54610a commit 007c46d

File tree

10 files changed

+437
-46
lines changed

10 files changed

+437
-46
lines changed

src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ declare module 'vue' {
7878
TrialImport: typeof import('./components/trial/TrialImport.vue')['default']
7979
TrialLayout: typeof import('./components/setup/TrialLayout.vue')['default']
8080
TrialOptionsDropdown: typeof import('./components/trial/TrialOptionsDropdown.vue')['default']
81+
TrialPersonDataMap: typeof import('./components/trial/TrialPersonDataMap.vue')['default']
8182
TrialPersonSelectModal: typeof import('./components/modals/TrialPersonSelectModal.vue')['default']
8283
TrialPreviewCanvas: typeof import('./components/data/TrialPreviewCanvas.vue')['default']
8384
TrialSelector: typeof import('./components/trial/TrialSelector.vue')['default']

src/components/chart/BarChart.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import bar from 'plotly.js/lib/bar'
2828
import { useI18n } from 'vue-i18n'
2929
import { coreStore } from '@/stores/app'
30+
import BaseChart from '@/components/chart/BaseChart.vue'
3031
3132
// Only register the chart types we're actually using to reduce the final bundle size
3233
Plotly.register([

src/components/chart/LineChart.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import scatter from 'plotly.js/lib/scatter'
2828
import { useI18n } from 'vue-i18n'
2929
import { coreStore } from '@/stores/app'
30+
import BaseChart from '@/components/chart/BaseChart.vue'
3031
3132
// Only register the chart types we're actually using to reduce the final bundle size
3233
Plotly.register([
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<template>
2+
<div class="person-data-map" ref="mapElement" />
3+
</template>
4+
5+
<script setup lang="ts">
6+
import type { TrialPlus } from '@/plugins/types/client'
7+
import { coreStore } from '@/stores/app'
8+
import L, { type Polygon, type Map, type TileLayer } from 'leaflet'
9+
import 'leaflet/dist/leaflet.css'
10+
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png'
11+
import iconUrl from 'leaflet/dist/images/marker-icon.png'
12+
import shadowUrl from 'leaflet/dist/images/marker-shadow.png'
13+
import { categoricalColors } from '@/plugins/color'
14+
import type { PersonLocationData } from '@/pages/visualization/trial-statistics.vue'
15+
import { mapAreaTypeMap } from '@/plugins/constants'
16+
17+
// Set the leaflet marker icon
18+
// @ts-ignore
19+
delete L.Icon.Default.prototype._getIconUrl
20+
L.Icon.Default.mergeOptions({
21+
iconRetinaUrl: iconRetinaUrl,
22+
iconUrl: iconUrl,
23+
shadowUrl: shadowUrl,
24+
})
25+
26+
const compProps = defineProps<{
27+
trial: TrialPlus
28+
personData: { [index: string]: PersonLocationData }
29+
overall: PersonLocationData
30+
areaType: string
31+
}>()
32+
33+
const store = coreStore()
34+
35+
const mapElement = useTemplateRef('mapElement')
36+
let themeLayer: TileLayer
37+
let map: Map
38+
let polygons: Polygon[] = []
39+
40+
watch(() => store.storeIsDarkMode, async () => updateThemeLayer())
41+
42+
function initMap () {
43+
if (!mapElement.value) {
44+
return
45+
}
46+
47+
map = L.map(mapElement.value)
48+
map.setView([22.5937, 2.1094], 3)
49+
50+
themeLayer = L.tileLayer(`//services.arcgisonline.com/arcgis/rest/services/Canvas/${store.storeIsDarkMode ? 'World_Dark_Gray_Base' : 'World_Light_Gray_Base'}/MapServer/tile/{z}/{y}/{x}`, {
51+
id: store.storeIsDarkMode ? 'Esri Dark Gray Base' : 'Esri Light Gray Base',
52+
attribution: 'Esri, HERE, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community',
53+
maxZoom: 21,
54+
maxNativeZoom: 15,
55+
})
56+
57+
const openstreetmap = L.tileLayer('//tile.openstreetmap.org/{z}/{x}/{y}.png', {
58+
id: 'OpenStreetMap',
59+
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
60+
maxZoom: 21,
61+
maxNativeZoom: 19,
62+
})
63+
64+
// Add an additional satellite layer
65+
const satellite = L.tileLayer('//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
66+
id: 'Esri WorldImagery',
67+
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
68+
maxZoom: 21,
69+
maxNativeZoom: 19,
70+
})
71+
72+
switch (store.storeMapLayer) {
73+
case 'theme': {
74+
map.addLayer(themeLayer)
75+
break
76+
}
77+
case 'satellite': {
78+
map.addLayer(satellite)
79+
break
80+
}
81+
default: {
82+
map.addLayer(openstreetmap)
83+
break
84+
}
85+
}
86+
87+
const baseMaps = {
88+
'Theme-based': themeLayer,
89+
OpenStreetMap: openstreetmap,
90+
'Esri WorldImagery': satellite,
91+
}
92+
93+
map.on('baselayerchange', e => {
94+
switch (e.name) {
95+
case 'Theme-based': {
96+
store.setMapLayer('theme')
97+
break
98+
}
99+
case 'OpenStreetMap': {
100+
store.setMapLayer('osm')
101+
break
102+
}
103+
case 'Esri WorldImagery': {
104+
store.setMapLayer('satellite')
105+
break
106+
}
107+
}
108+
})
109+
110+
L.control.layers(baseMaps).addTo(map)
111+
112+
// Disable zoom until focus gained, disable when blur
113+
map.scrollWheelZoom.disable()
114+
map.on('focus', () => map.scrollWheelZoom.enable())
115+
map.on('blur', () => map.scrollWheelZoom.disable())
116+
117+
nextTick(() => updateData())
118+
}
119+
120+
async function updateData () {
121+
if (polygons) {
122+
polygons.forEach(p => map.removeLayer(p))
123+
polygons = []
124+
}
125+
126+
const bounds = new L.LatLngBounds([])
127+
128+
if (compProps.overall && compProps.overall.bounds.length > 0) {
129+
const polygon = L.polygon(compProps.overall.bounds.map(ll => [ll.lat || 0, ll.lng || 0]), undefined)
130+
131+
// @ts-ignore
132+
polygon.getLatLngs().forEach(point => bounds.extend(point))
133+
134+
const areaType = mapAreaTypeMap[compProps.areaType]
135+
const area = areaType?.convert(compProps.overall.area) || 0
136+
const displayValue = `Overall: ${area.toLocaleString()}`
137+
polygon.bindTooltip(`${displayValue} ${areaType?.unit}`, {
138+
permanent: true,
139+
direction: 'center',
140+
sticky: false,
141+
})
142+
143+
polygon.addTo(map)
144+
polygons.push(polygon)
145+
}
146+
147+
if (compProps.personData) {
148+
Object.keys(compProps.personData).forEach(p => {
149+
if (compProps.personData[p] && compProps.personData[p].bounds.length > 0) {
150+
const personIndex = compProps.trial.people.findIndex(pp => pp.id === p)
151+
const personObject = compProps.trial.people[personIndex]
152+
153+
const polygon = L.polygon(compProps.personData[p].bounds.map(ll => [ll.lat || 0, ll.lng || 0]), personObject ? { color: categoricalColors.D3schemeCategory10[personIndex % categoricalColors.D3schemeCategory10.length] } : undefined)
154+
155+
// @ts-ignore
156+
polygon.getLatLngs().forEach(point => bounds.extend(point))
157+
158+
const areaType = mapAreaTypeMap[compProps.areaType]
159+
const area = areaType?.convert(compProps.personData[p].area) || 0
160+
const displayValue = `${personObject ? personObject.name : 'Overall'}: ${area.toLocaleString()}`
161+
polygon.bindTooltip(`${displayValue} ${areaType?.unit}`, {
162+
permanent: false,
163+
direction: 'top',
164+
sticky: true,
165+
})
166+
167+
polygon.addTo(map)
168+
polygons.push(polygon)
169+
}
170+
})
171+
}
172+
173+
if (bounds.isValid()) {
174+
const size = map.getSize()
175+
map.fitBounds(bounds, { padding: [size.x / 4, size.y / 4] })
176+
}
177+
}
178+
179+
function updateThemeLayer () {
180+
if (themeLayer) {
181+
themeLayer.setUrl(`//services.arcgisonline.com/arcgis/rest/services/Canvas/${store.storeIsDarkMode ? 'World_Dark_Gray_Base' : 'World_Light_Gray_Base'}/MapServer/tile/{z}/{y}/{x}`)
182+
}
183+
}
184+
185+
function invalidateSize () {
186+
nextTick(() => map?.invalidateSize())
187+
}
188+
189+
defineExpose({
190+
invalidateSize,
191+
})
192+
193+
onMounted(() => initMap())
194+
195+
watch(() => compProps.overall, async () => updateData())
196+
watch(() => compProps.personData, async () => updateData())
197+
watch(() => compProps.areaType, async () => updateData())
198+
</script>
199+
200+
<style scoped>
201+
.person-data-map {
202+
height: 50vh;
203+
}
204+
</style>

src/components/util/HelpCard.vue

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,23 @@
1919
<template #subtitle>
2020
<span class="text-wrap">{{ $t('widgetHelpInformationText') }}</span>
2121
</template>
22-
<template #text>
23-
<v-row>
24-
<v-col v-if="showInstall">
25-
<v-list-item slim min-width="200" @click="install" :title="$t('widgetHelpToolbarInstall')" :prepend-icon="mdiCellphoneArrowDown" />
26-
</v-col>
27-
<v-col v-else-if="!isInstalledAndroid">
28-
<v-list-item slim min-width="200" @click="showInstallInfo = true" :title="$t('widgetHelpToolbarInstall')" :prepend-icon="mdiCellphoneArrowDown" />
29-
</v-col>
30-
<v-col>
31-
<v-list-item slim min-width="200" href="mailto:[email protected]?subject=GridScore" :title="$t('widgetHelpToolbarSupport')" :prepend-icon="mdiAccountQuestion" />
32-
</v-col>
33-
<v-col>
34-
<v-list-item slim min-width="200" href="https://cropgeeks.github.io/gridscore-next-client" target="_blank" :title="$t('widgetHelpToolbarDocumentation')" :prepend-icon="mdiInformation" />
35-
</v-col>
36-
<v-col>
37-
<v-list-item slim min-width="200" href="https://github.com/cropgeeks/gridscore-next-client/issues/new/choose" target="_blank" :title="$t('widgetHelpToolbarSuggestions')" :prepend-icon="mdiGithub" />
38-
</v-col>
39-
</v-row>
40-
</template>
22+
<v-row class="mb-0">
23+
<v-col v-if="showInstall">
24+
<v-list-item slim min-width="200" @click="install" :title="$t('widgetHelpToolbarInstall')" :prepend-icon="mdiCellphoneArrowDown" />
25+
</v-col>
26+
<v-col v-else-if="!isInstalledAndroid">
27+
<v-list-item slim min-width="200" @click="showInstallInfo = true" :title="$t('widgetHelpToolbarInstall')" :prepend-icon="mdiCellphoneArrowDown" />
28+
</v-col>
29+
<v-col>
30+
<v-list-item slim min-width="200" href="mailto:[email protected]?subject=GridScore" :title="$t('widgetHelpToolbarSupport')" :prepend-icon="mdiAccountQuestion" />
31+
</v-col>
32+
<v-col>
33+
<v-list-item slim min-width="200" href="https://cropgeeks.github.io/gridscore-next-client" target="_blank" :title="$t('widgetHelpToolbarDocumentation')" :prepend-icon="mdiInformation" />
34+
</v-col>
35+
<v-col>
36+
<v-list-item slim min-width="200" href="https://github.com/cropgeeks/gridscore-next-client/issues/new/choose" target="_blank" :title="$t('widgetHelpToolbarSuggestions')" :prepend-icon="mdiGithub" />
37+
</v-col>
38+
</v-row>
4139

4240
<v-dialog
4341
v-model="showInstallInfo"

src/pages/collect/grid.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
:breakpoint="mdAndUp"
1111
/>
1212
<v-menu activator="#grid-media-mode">
13-
<v-list slim density="compact" min-width="300" max-width="min(500px, 75vw)">
14-
<v-list-subheader :title="$t('menuItemMediaModeHeading')" />
13+
<v-list slim density="compact" min-width="300" max-width="min(500px, 75vw)" id="media-mode-dropdown">
14+
<v-list-subheader>
15+
<div class="d-flex justify-space-between">
16+
<span>{{ $t('menuItemMediaModeHeading') }}</span>
17+
<span v-tooltip:top="$t('menuItemHelpMediaModeHeading')"><v-icon :icon="mdiHelpCircle" color="primary" /></span>
18+
</div>
19+
</v-list-subheader>
1520
<v-list-item :title="$t('menuItemMediaModeDisabled')" :prepend-icon="mdiCancel" :append-icon="store.storeMediaMode === undefined ? mdiCheck : undefined" @click="store.setMediaMode(undefined)" />
1621
<v-list-item :title="$t('menuItemMediaModeImage')" :prepend-icon="mdiImage" :append-icon="store.storeMediaMode === 'image' ? mdiCheck : undefined" @click="store.setMediaMode('image')" />
1722
<v-list-item :title="$t('menuItemMediaModeVideo')" :prepend-icon="mdiVideo" :append-icon="store.storeMediaMode === 'video' ? mdiCheck : undefined" @click="store.setMediaMode('video')" />
@@ -119,7 +124,7 @@
119124
import { getTrialById } from '@/plugins/idb'
120125
import { MainDisplayMode, type MiniCell, NavigationMode, type CellPlus, type Geolocation, type TrialPlus } from '@/plugins/types/client'
121126
import { coreStore } from '@/stores/app'
122-
import { mdiAccountMultiple, mdiCameraBurst, mdiCancel, mdiCheck, mdiCheckboxMarked, mdiCloudUpload, mdiCursorMove, mdiFormatListNumbered, mdiImage, mdiMarker, mdiMarkerCancel, mdiSprinklerFire, mdiSprout, mdiVideo } from '@mdi/js'
127+
import { mdiAccountMultiple, mdiCameraBurst, mdiCancel, mdiCheck, mdiCheckboxMarked, mdiCloudUpload, mdiCursorMove, mdiFormatListNumbered, mdiHelpCircle, mdiImage, mdiMarker, mdiMarkerCancel, mdiSprinklerFire, mdiSprout, mdiVideo } from '@mdi/js'
123128
import { watchIgnorable } from '@vueuse/core'
124129
125130
import emitter from 'tiny-emitter/instance'
@@ -450,4 +455,8 @@
450455
justify-content: space-between;
451456
flex-wrap: wrap;
452457
}
458+
459+
#media-mode-dropdown .v-list-subheader__text {
460+
flex-grow: 1;
461+
}
453462
</style>

0 commit comments

Comments
 (0)