Skip to content

Commit

Permalink
- CHG: Added GPS trait to cow trial.
Browse files Browse the repository at this point in the history
- ADD: Added GPS trait visualization.
-
  • Loading branch information
sebastian-raubach committed Jul 9, 2024
1 parent fe2e890 commit 497834d
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 8 deletions.
3 changes: 2 additions & 1 deletion components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ declare module 'vue' {
ChangelogModal: typeof import('./src/components/modals/ChangelogModal.vue')['default']
ColumnHeader: typeof import('./src/components/canvas/ColumnHeader.vue')['default']
ConfirmModal: typeof import('./src/components/modals/ConfirmModal.vue')['default']
copy: typeof import('./src/components/modals/TraitImportExportTabularModal.vue')['default']
copy: typeof import('./src/components/MapComponent copy.vue')['default']
DataCalendarHeatmapChart: typeof import('./src/components/charts/DataCalendarHeatmapChart.vue')['default']
DataCanvas: typeof import('./src/components/canvas/DataCanvas.vue')['default']
DataGridComponent: typeof import('./src/components/canvas/DataGridComponent.vue')['default']
Expand All @@ -91,6 +91,7 @@ declare module 'vue' {
GermplasmPerformanceBarChart: typeof import('./src/components/charts/GermplasmPerformanceBarChart.vue')['default']
GermplasmRepHeatmap: typeof import('./src/components/charts/GermplasmRepHeatmap.vue')['default']
GpsInput: typeof import('./src/components/GpsInput.vue')['default']
GpsTraitMap: typeof import('./src/components/GpsTraitMap.vue')['default']
GuidedWalkSelectorModal: typeof import('./src/components/modals/GuidedWalkSelectorModal.vue')['default']
GuideOrderSelector: typeof import('./src/components/GuideOrderSelector.vue')['default']
HomeBanners: typeof import('./src/components/HomeBanners.vue')['default']
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/assets/data/cows.json

Large diffs are not rendered by default.

260 changes: 260 additions & 0 deletions src/components/GpsTraitMap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
<template>
<div>
<div class="data-map" ref="map" />

<b-modal ref="plotModal" :title="$t('modalTitleVizPlotDataDetails')" ok-only :ok-title="$t('buttonClose')" v-if="selectedFeature">
<PlotDataSection :trial="trial" :cell="selectedFeature" :traits="[trait]" />
</b-modal>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
import { getTrialDataCached } from '@/plugins/datastore'
import PlotDataSection from '@/components/PlotDataSection.vue'
import { categoricalColors } from '@/plugins/color'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import emitter from 'tiny-emitter/instance'
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png'
import iconUrl from 'leaflet/dist/images/marker-icon.png'
import shadowUrl from 'leaflet/dist/images/marker-shadow.png'
// Set the leaflet marker icon
delete L.Icon.Default.prototype._getIconUrl
L.Icon.Default.mergeOptions({
iconRetinaUrl: iconRetinaUrl,
iconUrl: iconUrl,
shadowUrl: shadowUrl
})
export default {
components: {
PlotDataSection
},
computed: {
...mapGetters([
'storeDarkMode',
'storeMapLayer',
'storeHighlightControls'
])
},
props: {
trial: {
type: Object,
default: () => null
},
trait: {
type: Object,
default: () => null
},
selectedGermplasm: {
type: Array,
default: () => []
}
},
data: function () {
return {
trial: null,
selectedFeature: null
}
},
watch: {
trait: function () {
this.update()
},
selectedGermplasm: function () {
this.update()
},
storeDarkMode: function () {
this.updateThemeLayer()
}
},
methods: {
updateThemeLayer: function () {
if (this.themeLayer) {
this.themeLayer.setUrl(`//services.arcgisonline.com/arcgis/rest/services/Canvas/${this.storeDarkMode ? 'World_Dark_Gray_Base' : 'World_Light_Gray_Base'}/MapServer/tile/{z}/{y}/{x}`)
}
},
initMap: function () {
this.map = L.map(this.$refs.map)
this.map.setView([22.5937, 2.1094], 3)
// Add OSM as the default
const openstreetmap = L.tileLayer('//tile.openstreetmap.org/{z}/{x}/{y}.png', {
id: 'OpenStreetMap',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 21,
maxNativeZoom: 19
})
this.themeLayer = L.tileLayer(`//services.arcgisonline.com/arcgis/rest/services/Canvas/${this.storeDarkMode ? 'World_Dark_Gray_Base' : 'World_Light_Gray_Base'}/MapServer/tile/{z}/{y}/{x}`, {
id: this.storeDarkMode ? 'Esri Dark Gray Base' : 'Esri Light Gray Base',
attribution: 'Esri, HERE, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community',
maxZoom: 21,
maxNativeZoom: 15
})
// Add an additional satellite layer
const satellite = L.tileLayer('//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
id: 'Esri WorldImagery',
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
maxZoom: 21,
maxNativeZoom: 19
})
switch (this.storeMapLayer) {
case 'theme':
this.map.addLayer(this.themeLayer)
break
case 'satellite':
this.map.addLayer(satellite)
break
case 'osm':
default:
this.map.addLayer(openstreetmap)
break
}
const baseMaps = {
'Theme-based': this.themeLayer,
OpenStreetMap: openstreetmap,
'Esri WorldImagery': satellite
}
L.control.layers(baseMaps).addTo(this.map)
// Listen for layer changes and store the user selection in the store
this.map.on('baselayerchange', e => {
switch (e.name) {
case 'Theme-based':
this.$store.dispatch('setMapLayer', 'theme')
break
case 'OpenStreetMap':
this.$store.dispatch('setMapLayer', 'osm')
break
case 'Esri WorldImagery':
this.$store.dispatch('setMapLayer', 'satellite')
break
}
})
this.layerGroup = L.layerGroup().addTo(this.map)
// Disable zoom until focus gained, disable when blur
this.map.scrollWheelZoom.disable()
this.map.on('focus', () => this.map.scrollWheelZoom.enable())
this.map.on('blur', () => this.map.scrollWheelZoom.disable())
this.map.on('click', e => console.log(e.latlng))
},
update: async function () {
if (this.layerGroup) {
this.layerGroup.clearLayers()
}
// Extract all the individual polygons from the data
if (this.trialData && this.trait) {
const bounds = L.latLngBounds()
Object.keys(this.trialData).forEach((k, kIndex) => {
const cell = this.trialData[k]
const gps = []
if (cell.measurements && cell.measurements[this.trait.id]) {
cell.measurements[this.trait.id].sort((a, b) => a.timestamp.localeCompare(b.timestamp)).forEach(m => {
m.values.forEach(v => gps.push({
timestamp: m.timestamp,
displayName: cell.displayName,
latLng: v.split(';').map(Number)
}))
})
}
const color = (this.selectedGermplasm.length < 1 || this.selectedGermplasm.includes(cell.displayName)) ? categoricalColors.D3schemeCategory10[kIndex % categoricalColors.D3schemeCategory10.length] : 'gray'
const line = L.polyline(gps.map(g => g.latLng), { weight: 3, color: color }).addTo(this.layerGroup)
line.bindTooltip(cell.displayName)
line.on('mouseover', e => {
const l = e.target
l.setStyle({
weight: 10
})
})
line.on('mouseout', e => {
const l = e.target
l.setStyle({
weight: 3
})
})
line.on('mousemove', e => {
const l = e.target
l._tooltip.setLatLng(e.latlng)
})
gps.forEach(g => {
const m = L.circleMarker(g.latLng, { radius: 5, stroke: false, fillColor: color, fillOpacity: 0.8 })
m.bindTooltip(`${g.displayName}: ${new Date(g.timestamp).toLocaleDateString()}`)
m.on('click', e => {
this.selectedFeature = cell
this.$nextTick(() => this.$refs.plotModal.show())
})
m.addTo(this.layerGroup)
bounds.extend(g.latLng)
})
})
if (bounds.isValid()) {
const padding = Math.min(this.$refs.map.offsetWidth, this.$refs.map.offsetHeight) * 0.05
this.map.fitBounds(bounds, { padding: [padding, padding] })
}
}
},
updateTrialDataCache: function () {
this.trialData = getTrialDataCached()
this.update()
}
},
mounted: function () {
this.initMap()
emitter.on('trial-data-loaded', this.updateTrialDataCache)
this.updateTrialDataCache()
},
beforeUnmount: function () {
emitter.off('trial-data-loaded', this.updateTrialDataCache)
}
}
</script>

<style scoped>
.data-map {
height: 50vh;
}
.map-measurement-list {
max-height: 60vh;
overflow-y: auto;
}
</style>
<style>
.leaflet-popup-content-wrapper {
border-radius: 0;
}
.leaflet-popup-content {
min-width: 200px!important;
max-height: 50vh;
line-height: 1em;
overflow-x: hidden;
height: auto !important;
}
.leaflet-popup-content dl {
margin-bottom: 0;
}
.dark-mode .leaflet-control-locate a .leaflet-control-locate-location-arrow {
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="white" d="M445 4 29 195c-48 23-32 93 19 93h176v176c0 51 70 67 93 19L508 67c16-38-25-79-63-63z"/></svg>');
}
</style>
15 changes: 15 additions & 0 deletions src/plugins/changelog/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,25 @@
"title": "Zeilen-/Spaltenbeschriftung",
"text": "Die Zeilen-/Spaltenbeschriftungen des Versuchsaufbaus können jetzt weiter angepasst werden. Dies erlaubt das Erstellen von Versuchen bei welchem z.B. die Nummerierung bei 2 anfängt oder Zahlen übersprungen werden."
},
{
"type": "new",
"title": "GPS Merkmalstyp",
"text": "Ein neuer Merkmalstyp wurde in Form einer GPS-Verfolgung hinzugefügt. Dies erlaubt das Aufzeichnen der aktuellen Position als Merkmal."
},
{
"type": "new",
"title": "Neuer Beispielversuch",
"text": "Wir haben einen neuen Beispielversuch zum Erkunden hinzugefügt. Um hervorzuheben, dass GridScore nicht auf Pflanzenphänotypisierung beschränkt ist zeigt der neue Versuch die Nutzung von Merkmalssammlung für Kühe inklusive der Milchproduktion, Gewicht und GPS-Standort."
},
{
"type": "bugfix",
"title": "Service-worker-Bug",
"text": "Ein kritischer Fehler wurde behoben der GridScore davon abgehalten hat bestimmte Serverressourcen zu nutzen wie z.B. der Export in Germinate Data Templates."
},
{
"type": "bugfix",
"title": "Automatischer Eingabefortschritt",
"text": "Behebt einen Fehler bei dem der Fokus der Eingabe nicht automatisch auf die nächste Eingabe weitergegeben wurde beim Auswählen eines kategorischen Wertes."
}
]
}
Expand Down
15 changes: 15 additions & 0 deletions src/plugins/changelog/en_GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,25 @@
"title": "Row/column labels",
"text": "You can now further customise the row and column labels of your trial design. This will allow you to define trials, e.g. starting from number 2 or skipping numbers in between."
},
{
"type": "new",
"title": "GPS trait type",
"text": "A new trait type in the form of GPS tracking has been added. This will allow you to record your current position as a trait."
},
{
"type": "new",
"title": "New example trial",
"text": "We added a new example trial for you to explore. To highlight that GridScore is not restricted to plant phenotyping, we added a trial showcasing its use for trait data collection on cows including milk production, weight and GPS location."
},
{
"type": "bugfix",
"title": "Service worker bug",
"text": "Fixed a critical bug that prevented GridScore from accessing certain server resources including export to Germinate Data Templates."
},
{
"type": "bugfix",
"title": "Automatic input traversal",
"text": "Fixed an issue where the input focus didn't automatically move to the next trait input after selecting a categorical trait value."
}
]
}
Expand Down
11 changes: 7 additions & 4 deletions src/views/VizStatisticsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@
<b-col cols=12 lg=6 v-for="(t, tIndex) in selectedTraits" :key="`trait-heading-${t.trait.id}`">
<div class="d-flex flex-row justify-content-between align-items-center flex-wrap">
<h2><TraitHeading :short="true" :trait="t.trait" :traitIndex="t.index" /></h2>
<b-form-checkbox switch v-model="chartInteractionEnabled[tIndex]" @input="toggleChartInteraction(tIndex)"> {{ $t(chartInteractionEnabled[tIndex] ? 'formCheckboxChartInteractEnabled' : 'formCheckboxChartInteractDisabled') }}</b-form-checkbox>
<b-form-checkbox v-if="t.trait.dataType !== 'gps'" switch v-model="chartInteractionEnabled[tIndex]" @input="toggleChartInteraction(tIndex)"> {{ $t(chartInteractionEnabled[tIndex] ? 'formCheckboxChartInteractEnabled' : 'formCheckboxChartInteractDisabled') }}</b-form-checkbox>
</div>
<p v-if="t.trait.description">{{ t.trait.description }}</p>

<div :ref="`trait-stats-chart-${t.trait.id}`" />
<GpsTraitMap v-if="t.trait.dataType === 'gps'" :trait="t.trait" :trial="trial" :selectedGermplasm="selectedGermplasm" />
<div :ref="`trait-stats-chart-${t.trait.id}`" v-else />
</b-col>
</b-row>
</div>
Expand All @@ -63,7 +64,8 @@ import { getTrialDataCached } from '@/plugins/datastore'
import { getTrialById } from '@/plugins/idb'
import { hexToRgba, invertHex, toLocalDateString } from '@/plugins/misc'
import PlotDataSection from '@/components/PlotDataSection.vue'
import { CELL_CATEGORIES, CELL_CATEGORY_CONTROL, DISPLAY_ORDER_BOTTOM_TO_TOP, DISPLAY_ORDER_RIGHT_TO_LEFT } from '@/plugins/constants'
import GpsTraitMap from '@/components/GpsTraitMap.vue'
import { CELL_CATEGORIES, CELL_CATEGORY_CONTROL } from '@/plugins/constants'
import emitter from 'tiny-emitter/instance'
Expand All @@ -82,7 +84,8 @@ const GENERIC_TRACE = 'GENERIC_TRACE'
export default {
components: {
TraitHeading,
PlotDataSection
PlotDataSection,
GpsTraitMap
},
computed: {
...mapGetters([
Expand Down

0 comments on commit 497834d

Please sign in to comment.