Skip to content

Commit e6c0f10

Browse files
haithcoatjclaudem-mohr
authored
Add 1° download grid layer and download modal (#256)
* Add 1° download grid layer and download modal Adds a toggleable 1-degree grid overlay in Global Predictions mode that lets users click any cell to download its field boundaries as a GeoParquet file from Source Cooperative. - `Download-Grid-Layer.ts`: vector layer loading the grid manifest (`ftw-download-grid.geojson`) from Source Cooperative, with normal/hover/selected styles. - `useDownloadGrid.ts`: composable managing layer visibility, hover, click handling, and download modal state; idempotent per-map initialization avoids duplicate listeners across mode switches. - `DownloadModal.vue`: Vuetify dialog showing tile id, bounding box, feature count / size, and a direct download link for the currently selected year. - `useMap.ts`: initializes the grid layer when entering global mode and delegates grid-cell clicks from `handleMapClick`. - `GlobalDataPanel.vue`: adds a "Show 1° download grid" toggle to the side panel. - `MapComponent.vue`: mounts the modal and registers `handleMapClick`. - Unit tests covering URL construction, feature-to-cell mapping, and composable state transitions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Store download state in settings, persist download state to URL. * Zoom out when enabling the download grid, rename fieldBoundaryOpacity to opacity * Add a help button for downloads that directs to Source cooperative. * Download directly and other fixes * Code review * Download directly instead of via popup --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Matthias Mohr <m.mohr@moregeo.it>
1 parent 212f4be commit e6c0f10

10 files changed

Lines changed: 752 additions & 200 deletions

File tree

src/components/GlobalDataPanel.vue

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<script setup lang="ts">
2+
import { mdiHelpCircleOutline } from '@mdi/js'
23
import useSettings from '../composables/useSettings'
34
import useMap from '../composables/useMap'
45
import useAreaOfInterest from '../composables/useAreaOfInterest'
56
import type { PlaceResult } from '../composables/useAreaOfInterest'
67
import useNotifier from '../composables/useNotifier'
8+
import useDownloadGrid from '../composables/useDownloadGrid'
79
import { transformExtent } from 'ol/proj'
810
import GeocodingSearch from './GeocodingSearch.vue'
911
import MapLegend from './MapLegend.vue'
@@ -12,6 +14,7 @@ const { settings } = useSettings()
1214
const { map } = useMap()
1315
const { fitToExtent } = useAreaOfInterest()
1416
const { showError } = useNotifier()
17+
useDownloadGrid()
1518
1619
const handleLocationSelected = (place: PlaceResult) => {
1720
if (!map.value) return
@@ -68,9 +71,9 @@ const handleLocationSelected = (place: PlaceResult) => {
6871
thumb-color="teal"
6972
hide-details
7073
/>
71-
<h3 class="group">Opacity: {{ settings.fieldBoundariesOpacity }}%</h3>
74+
<h3 class="group">Opacity: {{ settings.opacity }}%</h3>
7275
<v-slider
73-
v-model.number="settings.fieldBoundariesOpacity"
76+
v-model.number="settings.opacity"
7477
:min="0"
7578
:max="100"
7679
:step="1"
@@ -79,6 +82,34 @@ const handleLocationSelected = (place: PlaceResult) => {
7982
thumb-color="teal"
8083
hide-details
8184
/>
85+
<div class="group group-with-help">
86+
<h3>Download Data</h3>
87+
<v-menu open-on-hover :close-on-content-click="false" max-width="400">
88+
<template #activator="{ props }">
89+
<v-icon :icon="mdiHelpCircleOutline" size="x-small" v-bind="props"></v-icon>
90+
</template>
91+
<v-sheet class="pa-3 text-body-2">
92+
<p class="pb-2">
93+
After activation, click a tile to download the agricultural field boundary
94+
predictions for that 1° cell as a GeoParquet file.
95+
</p>
96+
<p>
97+
You can also download the entire dataset in various variants from
98+
<a href="https://source.coop/ftw/global-data/" target="_blank" rel="noopener"
99+
>our Source Cooperative repository</a
100+
>.
101+
</p>
102+
</v-sheet>
103+
</v-menu>
104+
</div>
105+
<v-switch
106+
v-model="settings.downloads"
107+
color="teal"
108+
density="compact"
109+
hide-details
110+
label="Show download grid"
111+
class="mb-1"
112+
/>
82113
<h3 class="group legend">Legend</h3>
83114
<MapLegend />
84115
</v-col>
@@ -92,6 +123,16 @@ const handleLocationSelected = (place: PlaceResult) => {
92123
font-weight: 500;
93124
font-size: 1.1rem;
94125
}
126+
.group-with-help {
127+
display: flex;
128+
align-items: center;
129+
gap: 0.25rem;
130+
}
131+
.group-with-help h3 {
132+
margin: 0;
133+
font-weight: inherit;
134+
font-size: inherit;
135+
}
95136
.group.legend {
96137
margin-top: 1rem;
97138
border-top: 1px solid rgba(136, 136, 136, 0.65);

src/components/MapComponent.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import useAreaOfInterest from '../composables/useAreaOfInterest'
1212
import usePermalink from '../composables/usePermalink'
1313
import useMap from '../composables/useMap'
1414
import useSettings from '../composables/useSettings'
15+
import useDownloadGrid from '../composables/useDownloadGrid'
1516
import { createXYZ } from 'ol/tilegrid'
1617
1718
const {
@@ -22,11 +23,14 @@ const {
2223
propertiesBoxPosition,
2324
originalClickPosition,
2425
hidePropertiesBox,
26+
handleMapClick,
2527
geoJsonResults,
2628
initCloudlessLayer,
2729
updateLayers,
2830
} = useMap()
2931
32+
useDownloadGrid()
33+
3034
const { addMapClickHandler } = useAreaOfInterest()
3135
const { setAvailableModels, settings } = useSettings()
3236
@@ -91,6 +95,7 @@ onMounted(async () => {
9195
updateLayers()
9296
9397
addMapClickHandler(map.value as Map, areaValues.value)
98+
map.value.on('singleclick', handleMapClick)
9499
// Add scale bar
95100
map.value.addControl(new ScaleLine())
96101
// Setup permalink functionality
@@ -158,7 +163,6 @@ defineExpose({
158163
<PropertiesDisplay :properties="selectedFeature.getProperties()" />
159164
</div>
160165
</div>
161-
162166
<header v-if="critical" id="critical">
163167
<v-alert closable type="error" :text="critical"></v-alert>
164168
</header>

src/components/ProcessingResults.vue

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
</template>
9494

9595
<script setup lang="ts">
96-
import { ref, onMounted, onUnmounted, onBeforeUnmount, computed } from 'vue'
96+
import { ref, onBeforeUnmount, computed } from 'vue'
9797
import useMap from '../composables/useMap'
9898
import useNotifier from '../composables/useNotifier'
9999
import useAreaOfInterest from '../composables/useAreaOfInterest'
@@ -109,7 +109,7 @@ const emit = defineEmits<{
109109
(e: 'clearResults'): void
110110
}>()
111111
112-
const { map, handleMapClick, vectorLayer, selectedFeature, hidePropertiesBox } = useMap()
112+
const { map, vectorLayer, selectedFeature, hidePropertiesBox } = useMap()
113113
const { clearResults, returnToResults, fitToExtent } = useAreaOfInterest()
114114
const { showInfo, showError } = useNotifier()
115115
@@ -261,24 +261,10 @@ const fitMapToResult = (result: Feature) => {
261261
fitToExtent(map.value!, extent, null)
262262
}
263263
264-
onMounted(() => {
265-
// Add map click handler to detect feature clicks and show properties
266-
if (map.value) {
267-
map.value.on('singleclick', handleMapClick)
268-
}
269-
})
270-
271264
onBeforeUnmount(() => {
272265
selectedFeature.value = null
273266
})
274267
275-
// Clean up map click handler when component is unmounted
276-
onUnmounted(() => {
277-
if (map.value) {
278-
map.value.un('singleclick', handleMapClick)
279-
}
280-
})
281-
282268
const clearResultsHandler = () => {
283269
// Clear results and zoom back to S2 grid
284270
clearResults(map.value!)

0 commit comments

Comments
 (0)