From b7d6672d96cc40b2583d5f1e92aebc427313706f Mon Sep 17 00:00:00 2001 From: Jonathan Thiry Date: Mon, 2 Feb 2026 10:57:05 +0100 Subject: [PATCH 1/8] Use variables URIs instead of labels --- .../cp/meta/services/UploadDtoReader.scala | 2 +- .../meta/services/upload/VarMetaLookup.scala | 4 ++- src/main/twirl/views/UploadGuiPage.scala.html | 27 +++++++++++++------ .../se/lu/nateko/cp/meta/upload/Dtos.scala | 2 +- .../nateko/cp/meta/upload/PubSubEvent.scala | 1 + .../nateko/cp/meta/upload/SparqlQueries.scala | 3 ++- .../lu/nateko/cp/meta/upload/UploadApp.scala | 8 +++--- .../se/lu/nateko/cp/meta/upload/Utils.scala | 6 ++--- .../meta/upload/formcomponents/DataList.scala | 4 +-- .../upload/formcomponents/FileInput.scala | 2 +- .../formcomponents/FormComponents.scala | 12 ++++----- .../formcomponents/GenericTextInput.scala | 2 +- .../upload/formcomponents/L3VarInfoForm.scala | 18 ++++++++----- .../cp/meta/upload/formcomponents/Radio.scala | 2 +- .../meta/upload/formcomponents/Select.scala | 4 +-- .../upload/subforms/CollectionPanel.scala | 9 +++---- .../cp/meta/upload/subforms/DataPanel.scala | 5 ++++ .../meta/upload/subforms/PanelSubform.scala | 10 +++++++ .../upload/subforms/SpatioTemporalPanel.scala | 27 ++++++++++++++++--- .../subforms/StationTimeSeriesPanel.scala | 9 +++---- 20 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala b/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala index 5527dbb59..d37ebc102 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala @@ -47,7 +47,7 @@ object UploadDtoReader{ samplingHeight = l3.samplingHeight, production = dataProductionToDto(l3.productionInfo), customLandingPage = dobj.accessUrl.filterNot(uri => uri.getPath.endsWith(dobj.hash.id)), - variables = l3.variables.map(_.map(_.label)) + variables = l3.variables.map(_.map(_.model.uri.toString.split('/').last)) )) case Right(l2) => Right(StationTimeSeriesDto( station = l2.acquisition.station.org.self.uri, diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/upload/VarMetaLookup.scala b/src/main/scala/se/lu/nateko/cp/meta/services/upload/VarMetaLookup.scala index 1422a4394..d53991d23 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/upload/VarMetaLookup.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/upload/VarMetaLookup.scala @@ -23,7 +23,9 @@ class VarMetaLookup(varDefs: Seq[DatasetVariable]): val plainMandatory = varDefs.filterNot(_.isOptional).flatMap(_.plain) - private val plainLookup: Map[String, VarMeta] = varDefs.flatMap(_.plain).map(vm => vm.label -> vm).toMap + private val plainLookup: Map[String, VarMeta] = varDefs.flatMap(_.plain).map{vm => + vm.model.uri.toString.split('/').last -> vm + }.toMap private val regexes = varDefs.filter(_.isRegex).sortBy(_.isOptional).map{ dv => new Regex(dv.title) -> dv diff --git a/src/main/twirl/views/UploadGuiPage.scala.html b/src/main/twirl/views/UploadGuiPage.scala.html index 086fdb411..b951e757c 100644 --- a/src/main/twirl/views/UploadGuiPage.scala.html +++ b/src/main/twirl/views/UploadGuiPage.scala.html @@ -113,7 +113,7 @@

About

- +
@@ -147,7 +147,7 @@

About

- +
@@ -217,7 +217,7 @@

About

Data

- +
@@ -390,11 +390,13 @@

Spatiotemporal dataset

@geoCovSelect("spattemp"){ }
- +
@@ -422,7 +424,7 @@

- +
- +
} + +@l3varInputs(selectId: String) = { +
+ + + + +
+} diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Dtos.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Dtos.scala index 8ced15e33..6d48f8645 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Dtos.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Dtos.scala @@ -36,4 +36,4 @@ case class SamplingPoint(uri: URI, latitude: Double, longitude: Double, name: St class SpatialCoverage(val uri: Option[URI], val label: String) -case class DatasetVar(label: String, title: String, valueType: String, unit: String, isOptional: Boolean, isRegex: Boolean) +case class DatasetVar(uri: URI, label: String, title: String, valueType: String, unit: String, isOptional: Boolean, isRegex: Boolean) diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/PubSubEvent.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/PubSubEvent.scala index 46544ba2a..7b06baa33 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/PubSubEvent.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/PubSubEvent.scala @@ -11,6 +11,7 @@ final case class LevelSelected(level: Int) extends PubSubEvent final case class ObjSpecSelected(spec: ObjSpec) extends PubSubEvent final case class GotStationsList(stations: IndexedSeq[Station]) extends PubSubEvent +final case class GotVariableList(variables: IndexedSeq[DatasetVar]) extends PubSubEvent final case class GotUploadDto(dto: UploadDto) extends PubSubEvent case object FormInputUpdated extends PubSubEvent diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/SparqlQueries.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/SparqlQueries.scala index a7f1a7fe0..77d2bad5f 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/SparqlQueries.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/SparqlQueries.scala @@ -179,7 +179,7 @@ object SparqlQueries { private def datasetVarOrColQuery(dataset: URI, typ: String) = s"""prefix rdfs: |prefix cpmeta: - |select ?label ?title ?valueType ?unit ?optional ?regex + |select ?var ?label ?title ?valueType ?unit ?optional ?regex |where{ | <$dataset> cpmeta:has${typ} ?var . | ?var rdfs:label ?label; cpmeta:has${typ}Title ?title ; cpmeta:hasValueType ?valueTypeRes . @@ -190,6 +190,7 @@ object SparqlQueries { |}""".stripMargin def toDatasetVar(b: Binding) = DatasetVar( + new URI(b("var")), b("label"), b("title"), b("valueType"), diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/UploadApp.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/UploadApp.scala index 892896854..6eeea7393 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/UploadApp.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/UploadApp.scala @@ -25,13 +25,13 @@ object UploadApp { private val loginBlock = new HtmlElements("#login-block") private val formBlock = new HtmlElements("#form-block") private val headerButtons = new HtmlElements("#header-buttons-container") - private val modal = getElementById[html.Div]("upload-help-modal").get + private val modal = getElementById[html.Div]("upload-help-modal") val progressBar = new ProgressBar("#progress-bar") - private val alert = getElementById[html.Div]("alert-placeholder").get + private val alert = getElementById[html.Div]("alert-placeholder") private def setIframeSrc(src: String): Unit = val iframe = getElementById[dom.html.IFrame]("help-modal-iframe") - iframe.foreach(_.src = src) + iframe.src = src modal.addEventListener("show.bs.modal", { _ => setIframeSrc("https://www.youtube.com/embed/8TpbRZPaTuU") @@ -73,7 +73,7 @@ object UploadApp { private def displayLoginButton(authHost: String): Unit = { val url = URIUtils.encodeURI(dom.window.location.href) val href = s"https://$authHost/login/?targetUrl=$url" - getElementById[html.Anchor]("login-button").get.setAttribute("href", href) + getElementById[html.Anchor]("login-button").setAttribute("href", href) loginBlock.show() } diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Utils.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Utils.scala index 65427a357..0e8277350 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Utils.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Utils.scala @@ -12,9 +12,9 @@ import org.scalajs.dom.Element object Utils { - def getElementById[T <: html.Element : ClassTag](id: String): Option[T] = document.getElementById(id) match{ - case input: T => Some(input) - case _ => None + def getElementById[T <: html.Element : ClassTag](id: String): T = document.getElementById(id) match{ + case input: T => input + case _ => throw new Exception(s"Missing #$id element") } def querySelector[T <: html.Element : ClassTag](parent: html.Element, selector: String): Option[T] = parent.querySelector(selector) match { diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/DataList.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/DataList.scala index 255f35d2f..23d79df87 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/DataList.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/DataList.scala @@ -21,7 +21,7 @@ object DataListInput { } class DataList[T](elemId: String, val labeller: T => String) { - private val list = getElementById[html.DataList](elemId).get + private val list = getElementById[html.DataList](elemId) private var _values = IndexedSeq.empty[T] private val valLookup = mutable.Map.empty[String, T] protected val lookupIsActive: Boolean = true @@ -65,7 +65,7 @@ class DataListForm[T](elemId: String, list: DataList[T], notifyUpdate: () => Uni } } - private val formDiv = getElementById[html.Div](elemId).get + private val formDiv = getElementById[html.Div](elemId) private val template = querySelector[html.Div](formDiv, ".data-list").get private var _ordId: Long = 0L private val addButton = querySelector[html.Button](formDiv, "#add-element").get diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FileInput.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FileInput.scala index 85ec52cc5..9ad53cb27 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FileInput.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FileInput.scala @@ -16,7 +16,7 @@ import se.lu.nateko.cp.meta.upload.Utils.* import se.lu.nateko.cp.meta.upload.UploadApp class FileInput(elemId: String, cb: () => Unit){ - private val fileInput = getElementById[html.Input](elemId).get + private val fileInput = getElementById[html.Input](elemId) private var _hash: Try[Sha256Sum] = file.flatMap(_ => fail("hashsum computing has not started yet")) private var _lastModified: Double = 0 diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FormComponents.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FormComponents.scala index 1f0058458..a21763f76 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FormComponents.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/FormComponents.scala @@ -18,7 +18,7 @@ import se.lu.nateko.cp.meta.core.data.OneOrSeq class FormElement(elemId: String) { - private val form = getElementById[html.Form](elemId).get + private val form = getElementById[html.Form](elemId) def reset() = { form.reset() @@ -117,7 +117,7 @@ class DescriptionInput(elemId: String, cb: () => Unit) extends GenericOptionalIn class TextOptInput(elemId: String, cb: () => Unit) extends GenericOptionalInput[String](elemId, cb)(s => Try(Some(s)), _.toString()) class Button(elemId: String, onClick: () => Unit){ - private val button = getElementById[html.Button](elemId).get + private val button = getElementById[html.Button](elemId) private var popover = initializeBootstrapPopover(button.parentElement) def enable(): Unit = @@ -173,7 +173,7 @@ class HtmlElements(selector: String) { } class TagCloud(elemId: String) { - private val div = getElementById[html.Div](elemId).get + private val div = getElementById[html.Div](elemId) def setList(keywords: Seq[String]): Unit = { div.innerHTML = @@ -185,7 +185,7 @@ class TagCloud(elemId: String) { } class Modal(elemId: String) { - private val modal = getElementById[html.Div](elemId).get + private val modal = getElementById[html.Div](elemId) private val title = querySelector[html.Heading](modal, ".modal-title").get private val body = querySelector[html.Div](modal, ".modal-body").get @@ -199,7 +199,7 @@ class Modal(elemId: String) { } class Checkbox(elemId: String, cb: (Boolean) => Unit) { - private val checkbox = getElementById[html.Input](elemId).get + private val checkbox = getElementById[html.Input](elemId) def checked: Boolean = checkbox.checked def check(): Unit = { @@ -218,7 +218,7 @@ class Checkbox(elemId: String, cb: (Boolean) => Unit) { } class Text(elemId: String): - private val label = getElementById[html.Span](elemId).get + private val label = getElementById[html.Span](elemId) def setText(text: String): Unit = label.innerHTML = text diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/GenericTextInput.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/GenericTextInput.scala index 64ce979a2..23878fa29 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/GenericTextInput.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/GenericTextInput.scala @@ -14,7 +14,7 @@ private abstract class TextInputElement extends html.Object{ } abstract class GenericTextInput[T](elemId: String, cb: () => Unit, init: Try[T])(parser: String => Try[T], serializer: T => String) { - private val input: TextInputElement = getElementById[html.Element](elemId).get.asInstanceOf[TextInputElement] + private val input: TextInputElement = getElementById[html.Element](elemId).asInstanceOf[TextInputElement] private var _value: Try[T] = init def value: Try[T] = _value diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala index a00f3ab79..bf3265456 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala @@ -4,14 +4,17 @@ import scala.util.{Try, Success} import org.scalajs.dom.html import se.lu.nateko.cp.meta.upload.Utils.* import scala.collection.mutable +import se.lu.nateko.cp.meta.upload.DatasetVar class L3VarInfoForm(elemId: String, notifyUpdate: () => Unit) { - def varInfos: Try[Option[Seq[String]]] = if(elems.isEmpty) Success(None) else Try{ + var list: IndexedSeq[DatasetVar] = IndexedSeq.empty + + def values: Try[Option[Seq[DatasetVar]]] = if(elems.isEmpty) Success(None) else Try{ Some(elems.map(_.varInfo.get).toIndexedSeq) } - def setValues(vars: Option[Seq[String]]): Unit = { + def setValues(vars: Option[Seq[DatasetVar]]): Unit = { elems.foreach(_.remove()) elems.clear() vars.foreach{vdtos => @@ -23,7 +26,7 @@ class L3VarInfoForm(elemId: String, notifyUpdate: () => Unit) { } } - private val formDiv = getElementById[html.Div](elemId).get + private val formDiv = getElementById[html.Div](elemId) private val template = querySelector[html.Div](formDiv, ".l3varinfo-element").get private var _ordId: Long = 0L @@ -45,9 +48,9 @@ class L3VarInfoForm(elemId: String, notifyUpdate: () => Unit) { private class L3VarInfoInput{ - def varInfo: Try[String] = varNameInput.value + def varInfo: Try[DatasetVar] = varNameInput.value.withMissingError("Missing variable name") - def setValue(varName: String): Unit = { + def setValue(varName: DatasetVar): Unit = { varNameInput.value = varName } @@ -66,10 +69,11 @@ class L3VarInfoForm(elemId: String, notifyUpdate: () => Unit) { } Seq("varnameInput").foreach{inputClass => - querySelector[html.Input](div, s".$inputClass").foreach{_.id = s"${inputClass}_$id"} + querySelector[html.Select](div, s".$inputClass").foreach{_.id = s"${inputClass}_$id"} } - private val varNameInput = new TextInput(s"varnameInput_$id", notifyUpdate, "variable name") + private val varNameInput = new Select[DatasetVar](s"varnameInput_$id", _.label, _.uri.toString, false, notifyUpdate) + varNameInput.setOptions(list) div.style.display = "" diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Radio.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Radio.scala index 87fbb14df..c9c4a3b74 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Radio.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Radio.scala @@ -4,7 +4,7 @@ import se.lu.nateko.cp.meta.upload.Utils.* import org.scalajs.dom.html class Radio[T](elemId: String, cb: T => Unit, parser: String => Option[T], serializer: T => String) { - private val inputBlock: html.Element = getElementById[html.Element](elemId).get + private val inputBlock: html.Element = getElementById[html.Element](elemId) private val inputs: Seq[html.Input] = querySelectorAll[html.Input](inputBlock, "input[type=radio]") def value: Option[T] = selectedInput.flatMap(si => parser(si.value)) diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Select.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Select.scala index 7b8d477ba..f217353b7 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Select.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/Select.scala @@ -4,11 +4,11 @@ import org.scalajs.dom.{html, document} import se.lu.nateko.cp.meta.upload.Utils.* class Select[T](elemId: String, labeller: T => String, titleMaker: T => String, autoselect: Boolean = false, cb: () => Unit = () => ()){ - private val select = getElementById[html.Select](elemId).get + private val select = getElementById[html.Select](elemId) private var _values: IndexedSeq[T] = IndexedSeq.empty select.onchange = _ => cb() - getElementById[html.Form]("form-block").get.onreset = _ => { + getElementById[html.Form]("form-block").onreset = _ => { select.selectedIndex = -1 cb() } diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/CollectionPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/CollectionPanel.scala index 343204888..899571e51 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/CollectionPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/CollectionPanel.scala @@ -21,10 +21,9 @@ class CollectionPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSubBus private val collectionDoc = new HashOptInput("colldoc", notifyUpdate) private val spatialCovSelect = new GeoCoverageSelector(covs, "coll") - getElementById[html.Button]("rmCollGeoSelection").foreach: button => - button.onclick = event => - event.preventDefault() - spatialCovSelect.unselect() + getElementById[html.Button]("rmCollGeoSelection").onclick = event => + event.preventDefault() + spatialCovSelect.unselect() def resetForm(): Unit = collectionTitle.reset() @@ -48,4 +47,4 @@ class CollectionPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSubBus case _ => hide() -end CollectionPanel \ No newline at end of file +end CollectionPanel diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/DataPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/DataPanel.scala index 363779490..3f5be26a0 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/DataPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/DataPanel.scala @@ -87,6 +87,11 @@ class DataPanel( if(objSpec.isStationTimeSer && isNotAtcTimeSeries && isNotWdcgg && isNotNetCDF) nRowsInput.enable() else nRowsInput.disable() if(objSpec.dataset.nonEmpty) varInfoButton.enable() else disableVarInfoButton() dataTypeKeywords.setList(objSpec.keywords) + objSpec.dataset.foreach(dataset => { + whenDone(getVariables(dataset)) { variables => + bus.publish(GotVariableList(variables)) + } + }) bus.publish(ObjSpecSelected(objSpec)) } notifyUpdate() diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/PanelSubform.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/PanelSubform.scala index 7f909b354..35bc668f0 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/PanelSubform.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/PanelSubform.scala @@ -5,12 +5,14 @@ import se.lu.nateko.cp.meta.upload.* import eu.icoscp.envri.Envri import scala.concurrent.Future +import java.net.URI abstract class PanelSubform(selector: String)(using bus: PubSubBus) { protected val htmlElements = new HtmlElements(selector) protected def notifyUpdate(): Unit = bus.publish(FormInputUpdated) private type AgentList = IndexedSeq[NamedUri] private var peepsOrgsFut: Option[Future[(AgentList, AgentList)]] = None + private var variablesFut: Option[Future[IndexedSeq[DatasetVar]]] = None def resetForm(): Unit @@ -23,4 +25,12 @@ abstract class PanelSubform(selector: String)(using bus: PubSubBus) { result else peepsOrgsFut.get + + protected def getVariables(dataset: URI) = if variablesFut.isEmpty then + val result = Backend.getDatasetVariables(dataset) + variablesFut = Some(result) + result + else + variablesFut.get + } diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala index 16c0d8f32..7ba41373f 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala @@ -7,11 +7,13 @@ import se.lu.nateko.cp.meta.SpatioTemporalDto import se.lu.nateko.cp.meta.UploadDto import se.lu.nateko.cp.meta.core.data.TemporalCoverage import se.lu.nateko.cp.meta.upload.* +import se.lu.nateko.cp.meta.upload.UploadApp.whenDone import scala.util.Try import formcomponents.* import Utils.* +import java.net.URI class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSubBus) extends PanelSubform(".l3-section"){ @@ -26,7 +28,7 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu prod <- productionDto; customLanding <- externalPageInput.value; height <- samplingHeightInput.value; - varInfo <- varInfoForm.varInfos + varInfo <- varInfoForm.values yield SpatioTemporalDto( title = title, description = descr, @@ -36,10 +38,10 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu samplingHeight = height, production = prod, customLandingPage = customLanding, - variables = varInfo + variables = varInfo.map(_.map(_.uri.toString.split('/').last)) ) - def varnames: Try[Option[Seq[String]]] = varInfoForm.varInfos + def varnames: Try[Option[Seq[String]]] = varInfoForm.values.map(_.map(_.map(_.label))) private val titleInput = new TextInput("l3title", notifyUpdate, "elaborated product title") private val descriptionInput = new DescriptionInput("l3descr", notifyUpdate) @@ -52,6 +54,8 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu private val spatialCovSelect = new GeoCoverageSelector(covs, "spattemp") private val varInfoForm = new L3VarInfoForm("l3varinfo-form", notifyUpdate) private val externalPageInput = new UriOptInput("l3landingpage", notifyUpdate) + private var datasetSpec: Option[URI] = None + private var selsectedVars: Option[Seq[String]] = None def resetForm(): Unit = { Iterable( @@ -68,7 +72,14 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu case ObjSpecSelected(spec) => if(spec.isSpatiotemporal) show() else hide() if(spec.isNetCDF) varInfoForm.show() else varInfoForm.hide() + datasetSpec = spec.dataset case GotStationsList(stations) => stationSelect.setOptions(stations) + case GotVariableList(variables) => + varInfoForm.list = variables + selsectedVars.map { variables => + varInfoForm.setValues(Some(variables.flatMap(uri => varInfoForm.list.find(_.uri.toString.split('/').last == uri)))) + } + } private def handleDto(upDto: UploadDto): Unit = upDto match { @@ -86,7 +97,15 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu ) stationSelect.value = stat samplingHeightInput.value = spatTemp.samplingHeight externalPageInput.value = spatTemp.customLandingPage - varInfoForm.setValues(spatTemp.variables) + selsectedVars = spatTemp.variables + spatTemp.variables.map { varUris => + datasetSpec.map { dataset => + whenDone(getVariables(dataset)) { variables => + varInfoForm.list = variables + varInfoForm.setValues(Some(varUris.flatMap(uri => varInfoForm.list.find(_.uri.toString.split('/').last == uri)))) + } + } + } spatialCovSelect.handleReceivedSpatialCoverage(Some(spatTemp.spatial)) show() case _ => diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/StationTimeSeriesPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/StationTimeSeriesPanel.scala index 10e4fb639..dc5bdb7a5 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/StationTimeSeriesPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/StationTimeSeriesPanel.scala @@ -57,10 +57,9 @@ class StationTimeSeriesPanel(covs: IndexedSeq[SpatialCoverage]) (using bus: PubS private val customSamplingPoint = SamplingPoint(new URI(""), 0, 0, "Custom") - getElementById[html.Button]("rmL2GeoSelection").foreach: button => - button.onclick = event => - event.preventDefault() - spatialCovSelect.unselect() + getElementById[html.Button]("rmL2GeoSelection").onclick = event => + event.preventDefault() + spatialCovSelect.unselect() def resetForm(): Unit = { resetPlaceInfo() @@ -192,4 +191,4 @@ object StationTimeSeriesPanel{ } } } -} \ No newline at end of file +} From b798aacfe744a6c66a8aed3392f4bab87a01e9d2 Mon Sep 17 00:00:00 2001 From: Jonathan Thiry Date: Thu, 19 Feb 2026 11:35:12 +0100 Subject: [PATCH 2/8] Send variable titles to data ingestion --- .../lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala | 2 +- .../lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala index bf3265456..f5a2a0626 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala @@ -72,7 +72,7 @@ class L3VarInfoForm(elemId: String, notifyUpdate: () => Unit) { querySelector[html.Select](div, s".$inputClass").foreach{_.id = s"${inputClass}_$id"} } - private val varNameInput = new Select[DatasetVar](s"varnameInput_$id", _.label, _.uri.toString, false, notifyUpdate) + private val varNameInput = new Select[DatasetVar](s"varnameInput_$id", s => s"${s.label} (${s.title})", _.uri.toString, false, notifyUpdate) varNameInput.setOptions(list) div.style.display = "" diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala index 7ba41373f..147de154b 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala @@ -41,7 +41,7 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu variables = varInfo.map(_.map(_.uri.toString.split('/').last)) ) - def varnames: Try[Option[Seq[String]]] = varInfoForm.values.map(_.map(_.map(_.label))) + def varnames: Try[Option[Seq[String]]] = varInfoForm.values.map(_.map(_.map(_.title))) private val titleInput = new TextInput("l3title", notifyUpdate, "elaborated product title") private val descriptionInput = new DescriptionInput("l3descr", notifyUpdate) From b95dda0057769a25bdd111c67df2ba5702cfede6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valter=20Sundstr=C3=B6m?= Date: Wed, 25 Feb 2026 00:10:25 +0100 Subject: [PATCH 3/8] Simplify varnames from Option[Seq] to just Seq --- .../scala/se/lu/nateko/cp/meta/upload/Backend.scala | 12 ++++-------- .../meta/upload/subforms/SpatioTemporalPanel.scala | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala index 523de560f..2187165d7 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala @@ -96,18 +96,14 @@ object Backend { sparqlSelect(datasetVariableQuery(dataset)).map(_.map(toDatasetVar)) def tryIngestion( - file: File, spec: ObjSpec, nRows: Option[Int], varnames: Option[Seq[String]] + file: File, spec: ObjSpec, nRows: Option[Int], varnames: Seq[String] )(implicit envriConfig: EnvriConfig): Future[Unit] = { - val hasVars: Boolean = varnames.flatMap(_.headOption).isDefined - - if (spec.dataset.isDefined && (spec.isStationTimeSer || hasVars)) || spec.isZip || spec.isNetCDF then + if (spec.dataset.isDefined && (spec.isStationTimeSer || varnames.nonEmpty)) || spec.isZip || spec.isNetCDF then val nRowsQ = nRows.fold("")(nr => s"&nRows=$nr") - val varsQ = varnames.fold(""){vns => - val varsJson = encodeURIComponent(Json.toJson(vns).toString) - s"&varnames=$varsJson" - } + val varsJson = encodeURIComponent(Json.toJson(varnames).toString) + val varsQ = s"&varnames=$varsJson" val url = s"https://${envriConfig.dataHost}/tryingest?specUri=${spec.uri}$nRowsQ$varsQ" fetchOk("validate data object", url, new RequestInit{ diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala index 147de154b..0c0e6a8c9 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala @@ -41,7 +41,7 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu variables = varInfo.map(_.map(_.uri.toString.split('/').last)) ) - def varnames: Try[Option[Seq[String]]] = varInfoForm.values.map(_.map(_.map(_.title))) + def varnames: Try[Seq[String]] = varInfoForm.values.map(_.getOrElse(Seq.empty).map(_.title)) private val titleInput = new TextInput("l3title", notifyUpdate, "elaborated product title") private val descriptionInput = new DescriptionInput("l3descr", notifyUpdate) From a55f960698a80be30b6ccc4edf60b870e5f2f762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valter=20Sundstr=C3=B6m?= Date: Wed, 25 Feb 2026 00:16:39 +0100 Subject: [PATCH 4/8] Preserve original behaviour of empty string when empty list --- .../main/scala/se/lu/nateko/cp/meta/upload/Backend.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala index 2187165d7..96ea9d215 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala @@ -102,8 +102,10 @@ object Backend { if (spec.dataset.isDefined && (spec.isStationTimeSer || varnames.nonEmpty)) || spec.isZip || spec.isNetCDF then val nRowsQ = nRows.fold("")(nr => s"&nRows=$nr") - val varsJson = encodeURIComponent(Json.toJson(varnames).toString) - val varsQ = s"&varnames=$varsJson" + val varsQ = if (varnames.nonEmpty) { + val varsJson = encodeURIComponent(Json.toJson(varnames).toString) + s"&varnames=$varsJson" + } else { "" } val url = s"https://${envriConfig.dataHost}/tryingest?specUri=${spec.uri}$nRowsQ$varsQ" fetchOk("validate data object", url, new RequestInit{ From 05e8152ddbea9554c72d0232f69f858cd56f40c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valter=20Sundstr=C3=B6m?= Date: Wed, 25 Feb 2026 10:51:47 +0100 Subject: [PATCH 5/8] Change SpatioTemporalDto.variables from Option[Seq] to Seq --- .../se/lu/nateko/cp/meta/UploadDtos.scala | 2 +- .../cp/meta/services/UploadDtoReader.scala | 2 +- .../services/upload/StatementsProducer.scala | 2 +- .../upload/validation/UploadValidator.scala | 18 ++++++----- .../cp/meta/upload/L3UpdateWorkbench.scala | 2 +- .../upload/formcomponents/L3VarInfoForm.scala | 16 +++++----- .../upload/subforms/SpatioTemporalPanel.scala | 30 +++++++++++-------- 7 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/main/scala/se/lu/nateko/cp/meta/UploadDtos.scala b/src/main/scala/se/lu/nateko/cp/meta/UploadDtos.scala index d42b5b1b8..be4ece372 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/UploadDtos.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/UploadDtos.scala @@ -86,7 +86,7 @@ case class SpatioTemporalDto( forStation: Option[URI], samplingHeight: Option[Float], customLandingPage: Option[URI], - variables: Option[Seq[String]] + variables: Seq[String] ) case class DataProductionDto( diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala b/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala index d37ebc102..821942158 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala @@ -47,7 +47,7 @@ object UploadDtoReader{ samplingHeight = l3.samplingHeight, production = dataProductionToDto(l3.productionInfo), customLandingPage = dobj.accessUrl.filterNot(uri => uri.getPath.endsWith(dobj.hash.id)), - variables = l3.variables.map(_.map(_.model.uri.toString.split('/').last)) + variables = l3.variables.map(_.map(_.model.uri.toString.split('/').last)).getOrElse(Nil) )) case Right(l2) => Right(StationTimeSeriesDto( station = l2.acquisition.station.org.self.uri, diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/upload/StatementsProducer.scala b/src/main/scala/se/lu/nateko/cp/meta/services/upload/StatementsProducer.scala index ecec21ca0..f00a40f89 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/upload/StatementsProducer.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/upload/StatementsProducer.scala @@ -201,7 +201,7 @@ class StatementsProducer(vocab: CpVocab, metaVocab: CpmetaVocab) { makeSt(acq, metaVocab.prov.wasAssociatedWith, meta.forStation.map(_.toRdf)) ++ makeSt(acq, metaVocab.hasSamplingHeight, meta.samplingHeight.map(vocab.lit)) ++ makeSt(objUri, RDFS.SEEALSO, meta.customLandingPage.map(_.toRdf)) ++ - meta.variables.toSeq.flatten.flatMap(getL3VarInfoStatements(objUri, hash, _)) + meta.variables.flatMap(getL3VarInfoStatements(objUri, hash, _)) } private def getStationDataStatements(hash: Sha256Sum, meta: StationTimeSeriesDto)(using Envri): Seq[Statement] = { diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/upload/validation/UploadValidator.scala b/src/main/scala/se/lu/nateko/cp/meta/services/upload/validation/UploadValidator.scala index b6594bb58..060273216 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/upload/validation/UploadValidator.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/upload/validation/UploadValidator.scala @@ -338,14 +338,16 @@ class UploadValidator(servers: DataObjectInstanceServers): if spec.specificDatasetType != DatasetType.SpatioTemporal then errors += "Wrong type of dataset for this object spec (must be spatiotemporal)" else - for vars <- spTempMeta.variables do spec.datasetSpec.fold( - errors += s"Data object specification ${spec.self.uri} lacks a dataset specification; cannot accept variable info." - ): dsSpec => - val valTypeLookupV = metaReader.getValTypeLookup(dsSpec.self.uri.toRdf) - errors ++= valTypeLookupV.errors - for valTypeLookup <- valTypeLookupV; varName <- vars do - if valTypeLookup.lookup(varName).isEmpty then errors += - s"Variable name '$varName' is not compatible with dataset specification ${dsSpec.self.uri}" + if (spTempMeta.variables.nonEmpty) { + spec.datasetSpec.fold( + errors += s"Data object specification ${spec.self.uri} lacks a dataset specification; cannot accept variable info." + ): dsSpec => + val valTypeLookupV = metaReader.getValTypeLookup(dsSpec.self.uri.toRdf) + errors ++= valTypeLookupV.errors + for valTypeLookup <- valTypeLookupV; varName <- spTempMeta.variables do + if valTypeLookup.lookup(varName).isEmpty then errors += + s"Variable name '$varName' is not compatible with dataset specification ${dsSpec.self.uri}" + } case Right(stationMeta) => if spec.specificDatasetType != DatasetType.StationTimeSeries diff --git a/src/test/scala/se/lu/nateko/cp/meta/upload/L3UpdateWorkbench.scala b/src/test/scala/se/lu/nateko/cp/meta/upload/L3UpdateWorkbench.scala index 852aef671..6e80638c2 100644 --- a/src/test/scala/se/lu/nateko/cp/meta/upload/L3UpdateWorkbench.scala +++ b/src/test/scala/se/lu/nateko/cp/meta/upload/L3UpdateWorkbench.scala @@ -33,7 +33,7 @@ object L3UpdateWorkbench extends CpmetaJsonProtocol{ val l3 = dto.specificInfo.left.getOrElse(???) val l3updated = l3.copy( spatial = new URI("http://meta.icos-cp.eu/resources/latlonboxes/globalLatLonBox"), - variables = Some(Seq("emission")), + variables = Seq("emission"), ) dto.copy( submitterId = "CP", diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala index f5a2a0626..5ef1f214d 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/formcomponents/L3VarInfoForm.scala @@ -10,19 +10,17 @@ class L3VarInfoForm(elemId: String, notifyUpdate: () => Unit) { var list: IndexedSeq[DatasetVar] = IndexedSeq.empty - def values: Try[Option[Seq[DatasetVar]]] = if(elems.isEmpty) Success(None) else Try{ - Some(elems.map(_.varInfo.get).toIndexedSeq) + def values: Try[Seq[DatasetVar]] = if(elems.isEmpty) Success(Nil) else Try{ + elems.map(_.varInfo.get).toIndexedSeq } - def setValues(vars: Option[Seq[DatasetVar]]): Unit = { + def setValues(vars: Seq[DatasetVar]): Unit = { elems.foreach(_.remove()) elems.clear() - vars.foreach{vdtos => - vdtos.foreach{vdto => - val input = new L3VarInfoInput - input.setValue(vdto) - elems.append(input) - } + vars.foreach{vdto => + val input = new L3VarInfoInput + input.setValue(vdto) + elems.append(input) } } diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala index 0c0e6a8c9..25eb6787b 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/subforms/SpatioTemporalPanel.scala @@ -38,10 +38,10 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu samplingHeight = height, production = prod, customLandingPage = customLanding, - variables = varInfo.map(_.map(_.uri.toString.split('/').last)) + variables = varInfo.map(_.uri.toString.split('/').last) ) - def varnames: Try[Seq[String]] = varInfoForm.values.map(_.getOrElse(Seq.empty).map(_.title)) + def varnames: Try[Seq[String]] = varInfoForm.values.map(_.map(_.title)) private val titleInput = new TextInput("l3title", notifyUpdate, "elaborated product title") private val descriptionInput = new DescriptionInput("l3descr", notifyUpdate) @@ -55,14 +55,14 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu private val varInfoForm = new L3VarInfoForm("l3varinfo-form", notifyUpdate) private val externalPageInput = new UriOptInput("l3landingpage", notifyUpdate) private var datasetSpec: Option[URI] = None - private var selsectedVars: Option[Seq[String]] = None + private var selsectedVars: Seq[String] = Nil def resetForm(): Unit = { Iterable( titleInput, descriptionInput, timeStartInput, timeStopInput, temporalResInput, externalPageInput ).foreach(_.reset()) - varInfoForm.setValues(None) + varInfoForm.setValues(Nil) spatialCovSelect.resetForm() } @@ -76,9 +76,8 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu case GotStationsList(stations) => stationSelect.setOptions(stations) case GotVariableList(variables) => varInfoForm.list = variables - selsectedVars.map { variables => - varInfoForm.setValues(Some(variables.flatMap(uri => varInfoForm.list.find(_.uri.toString.split('/').last == uri)))) - } + varInfoForm.setValues(selsectedVars.flatMap(uri => + varInfoForm.list.find(_.uri.toString.split('/').last == uri))) } @@ -98,14 +97,19 @@ class SpatioTemporalPanel(covs: IndexedSeq[SpatialCoverage])(implicit bus: PubSu samplingHeightInput.value = spatTemp.samplingHeight externalPageInput.value = spatTemp.customLandingPage selsectedVars = spatTemp.variables - spatTemp.variables.map { varUris => - datasetSpec.map { dataset => - whenDone(getVariables(dataset)) { variables => - varInfoForm.list = variables - varInfoForm.setValues(Some(varUris.flatMap(uri => varInfoForm.list.find(_.uri.toString.split('/').last == uri)))) + if (selsectedVars.nonEmpty) { + datasetSpec match { + case None => () + case Some(dataset) => { + whenDone(getVariables(dataset)) { variables => + varInfoForm.list = variables + varInfoForm.setValues(selsectedVars.flatMap(uri => + varInfoForm.list.find(_.uri.toString.split('/').last == uri) + )) + } + } } } - } spatialCovSelect.handleReceivedSpatialCoverage(Some(spatTemp.spatial)) show() case _ => From 2e52fb4d1da5e61b8dbf6b100c8bd49292799202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valter=20Sundstr=C3=B6m?= Date: Wed, 25 Feb 2026 11:36:21 +0100 Subject: [PATCH 6/8] Change SpatioTemporalMeta.variables from Option[Seq] to Seq --- .../scala/se/lu/nateko/cp/meta/core/data/DataObject.scala | 4 ++-- .../scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala | 2 +- .../se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala | 2 +- .../se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala | 2 +- src/main/twirl/views/DataLandingPage.scala.html | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala b/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala index 47e77a683..5214ce1d6 100644 --- a/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala +++ b/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala @@ -108,7 +108,7 @@ case class SpatioTemporalMeta( station: Option[Station], samplingHeight: Option[Float], productionInfo: DataProduction, - variables: Option[Seq[VarMeta]] + variables: Seq[VarMeta] ){ def acquisition: Option[DataAcquisition] = station.map{ DataAcquisition(_, None, Some(temporal.interval), None, None, samplingHeight) @@ -166,7 +166,7 @@ case class DataObject( val varsAndPosits = for cols <- specificInfo.fold( - l3 => l3.variables, + l3 => Some(l3.variables), l2 => l2.columns ).toSeq col <- cols diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala b/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala index 821942158..6e2e96a71 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/UploadDtoReader.scala @@ -47,7 +47,7 @@ object UploadDtoReader{ samplingHeight = l3.samplingHeight, production = dataProductionToDto(l3.productionInfo), customLandingPage = dobj.accessUrl.filterNot(uri => uri.getPath.endsWith(dobj.hash.id)), - variables = l3.variables.map(_.map(_.model.uri.toString.split('/').last)).getOrElse(Nil) + variables = l3.variables.map(_.model.uri.toString.split('/').last) )) case Right(l2) => Right(StationTimeSeriesDto( station = l2.acquisition.station.org.self.uri, diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala b/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala index 6f6811576..121597a16 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala @@ -183,7 +183,7 @@ class SchemaOrg(handleProxies: HandleProxiesConfig)(using envri: Envri, envriCon ) } - val variableMeasured = asOptArray(dobj.specificInfo.fold(_.variables, _.columns))( + val variableMeasured = asOptArray(dobj.specificInfo.fold(spatio => Some(spatio.variables), timeSeries => timeSeries.columns))( variable => JsObject( "@type" -> JsString("PropertyValue"), "name" -> JsString(variable.label), diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala b/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala index 0ef04e06e..805554a1d 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala @@ -325,7 +325,7 @@ trait DobjMetaReader(val vocab: CpVocab) extends CpmetaReader: station = station, samplingHeight = samplingHeightOpt.flatten, productionInfo = prod, - variables = Some(variables.flatten).filterNot(_.isEmpty) + variables = variables.flatten() ) def getContributors(objIri: IRI, contribPredicate: IRI)(using conn: DobjConn | DocConn): Validated[IndexedSeq[Agent]] = diff --git a/src/main/twirl/views/DataLandingPage.scala.html b/src/main/twirl/views/DataLandingPage.scala.html index 1934cb3c4..e983cb926 100644 --- a/src/main/twirl/views/DataLandingPage.scala.html +++ b/src/main/twirl/views/DataLandingPage.scala.html @@ -398,7 +398,7 @@

Technical information

} @varMetas = @{ - dobj.specificInfo.fold(_.variables, _.columns) + dobj.specificInfo.fold( spatio => Some(spatio.variables), timeSeries => timeSeries.columns) } @instrumentDeploymentsPresent = @{ From 6e4271f23d4a0f5b77f5739ca5bcece087c9efaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valter=20Sundstr=C3=B6m?= Date: Wed, 25 Feb 2026 12:12:38 +0100 Subject: [PATCH 7/8] Change StationTimeSeriesMeta.columns from Option[Seq] to Seq --- .../se/lu/nateko/cp/meta/core/data/DataObject.scala | 11 ++++++----- .../meta/services/citation/AttributionProvider.scala | 5 ++--- .../cp/meta/services/citation/CitationMaker.scala | 8 +++++--- .../cp/meta/services/metaexport/SchemaOrg.scala | 2 +- .../cp/meta/services/upload/DobjMetaReader.scala | 2 +- src/main/twirl/views/DataLandingPage.scala.html | 8 ++++---- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala b/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala index 5214ce1d6..1142977c2 100644 --- a/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala +++ b/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala @@ -86,7 +86,7 @@ case class StationTimeSeriesMeta( productionInfo: Option[DataProduction], nRows: Option[Int], coverage: Option[GeoFeature], - columns: Option[Seq[VarMeta]] + columns: Seq[VarMeta] ) case class ValueType(self: UriResource, quantityKind: Option[UriResource], unit: Option[String]) @@ -164,11 +164,12 @@ case class DataObject( l2 => l2.coverage.orElse(l2.acquisition.coverage) ) + val cols = specificInfo.fold( + l3 => l3.variables, + l2 => l2.columns + ) + val varsAndPosits = for - cols <- specificInfo.fold( - l3 => Some(l3.variables), - l2 => l2.columns - ).toSeq col <- cols deps <- col.instrumentDeployments.toSeq dep <- deps diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/citation/AttributionProvider.scala b/src/main/scala/se/lu/nateko/cp/meta/services/citation/AttributionProvider.scala index cdca67efb..a1dcb2329 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/citation/AttributionProvider.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/citation/AttributionProvider.scala @@ -65,10 +65,9 @@ final class AttributionProvider(vocab: CpVocab, val metaVocab: CpmetaVocab) exte if(dobj.specification.theme.self.uri === vocab.atmoTheme) memb => (memb.role.weight.isDefined && { val speciesOk = for( extra <- memb.role.extra; - l2 <- dobj.specificInfo.toOption; - cols <- l2.columns + l2 <- dobj.specificInfo.toOption ) yield{ - val colLabels = cols.map(_.label.toLowerCase) + val colLabels = l2.columns.map(_.label.toLowerCase) extra.split(',').map(_.trim.toLowerCase).exists(species => colLabels.exists(_.contains(species)) || dobj.specification.self.label.getOrElse("").toLowerCase.contains(species) diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/citation/CitationMaker.scala b/src/main/scala/se/lu/nateko/cp/meta/services/citation/CitationMaker.scala index dc3f5e42d..d9f5205ac 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/citation/CitationMaker.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/citation/CitationMaker.scala @@ -177,9 +177,11 @@ class CitationMaker( val height = acq.samplingHeight.fold("")(sh => s" ($sh m)") val vars = if dobj.specification.self.uri === vocab.atmGhgProdSpec then - stationTs.columns.fold("")(_.collect{ - case v if v.valueType.unit.isDefined => v.label - }.mkString(" (", ", ", ")")) + if (stationTs.columns.nonEmpty) + stationTs.columns.collect{ + case v if v.valueType.unit.isDefined => v.label + }.mkString(" (", ", ", ")") + else "" else "" s"$spec$vars from $station$height" diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala b/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala index 121597a16..c7850dcb0 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/metaexport/SchemaOrg.scala @@ -183,7 +183,7 @@ class SchemaOrg(handleProxies: HandleProxiesConfig)(using envri: Envri, envriCon ) } - val variableMeasured = asOptArray(dobj.specificInfo.fold(spatio => Some(spatio.variables), timeSeries => timeSeries.columns))( + val variableMeasured = asOptArray(dobj.specificInfo.fold((_.variables), _.columns))( variable => JsObject( "@type" -> JsString("PropertyValue"), "name" -> JsString(variable.label), diff --git a/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala b/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala index 805554a1d..d8b9b4bda 100644 --- a/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala +++ b/src/main/scala/se/lu/nateko/cp/meta/services/upload/DobjMetaReader.scala @@ -272,7 +272,7 @@ trait DobjMetaReader(val vocab: CpVocab) extends CpmetaReader: case Some(interval) => addInstrDeplInfo(stationUri, interval, columns) columnsOptV.sinkOption.map: columnsOpt => - StationTimeSeriesMeta(acq, prod, nRows, lblCoverage, columnsOpt) + StationTimeSeriesMeta(acq, prod, nRows, lblCoverage, columnsOpt.getOrElse(Nil)) resV.flatMap(identity) end getStationTimeSerMeta diff --git a/src/main/twirl/views/DataLandingPage.scala.html b/src/main/twirl/views/DataLandingPage.scala.html index e983cb926..d0a42f5c9 100644 --- a/src/main/twirl/views/DataLandingPage.scala.html +++ b/src/main/twirl/views/DataLandingPage.scala.html @@ -133,7 +133,7 @@

Production


- @for(variables <- varMetas){ + @if(varMetas.nonEmpty){

Previewable variables

@@ -155,7 +155,7 @@

Previewable variables

- @for(varInfo <- variables){ + @for(varInfo <- varMetas){ @@ -398,11 +398,11 @@

Technical information

} @varMetas = @{ - dobj.specificInfo.fold( spatio => Some(spatio.variables), timeSeries => timeSeries.columns) + dobj.specificInfo.fold(_.variables, _.columns) } @instrumentDeploymentsPresent = @{ - varMetas.exists(_.exists(_.instrumentDeployments.isDefined)) + varMetas.exists(_.instrumentDeployments.isDefined) } @moratoriumText = @{ From 8de9db91fd33c44502cb690ff608bcf27de2909b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valter=20Sundstr=C3=B6m?= Date: Wed, 25 Feb 2026 12:21:21 +0100 Subject: [PATCH 8/8] Improve diff --- .../src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala index 96ea9d215..0742c2506 100644 --- a/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala +++ b/uploadgui/src/main/scala/se/lu/nateko/cp/meta/upload/Backend.scala @@ -99,7 +99,9 @@ object Backend { file: File, spec: ObjSpec, nRows: Option[Int], varnames: Seq[String] )(implicit envriConfig: EnvriConfig): Future[Unit] = { - if (spec.dataset.isDefined && (spec.isStationTimeSer || varnames.nonEmpty)) || spec.isZip || spec.isNetCDF then + val hasVars = varnames.nonEmpty + + if (spec.dataset.isDefined && (spec.isStationTimeSer || hasVars)) || spec.isZip || spec.isNetCDF then val nRowsQ = nRows.fold("")(nr => s"&nRows=$nr") val varsQ = if (varnames.nonEmpty) {
@(varInfo.label) @(varInfo.valueType.self.label.getOrElse(""))