diff --git a/build.gradle b/build.gradle index 8f4711fa8..a7ae8dc89 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ plugins { id "com.gorylenko.gradle-git-properties" version "2.4.1" } -version "4.4" +version "4.5-SPECIES-SNAPSHOT" group "au.org.ala" description "Ecodata" diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index 324357ad7..3a77d5315 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -106,6 +106,9 @@ if (!google.geocode.url) { if (!temp.file.cleanup.days) { temp.file.cleanup.days = 1 } +if(!paratoo.location.excluded) { + paratoo.location.excluded = ['location.vegetation-association-nvis'] +} access.expiry.maxEmails=500 @@ -531,6 +534,7 @@ ecodata.documentation.exampleProjectUrl = 'http://ecodata-test.ala.org.au/ws/act // Used by ParatooService to sync available protocols paratoo.core.baseUrl = 'https://dev.core-api.monitor.tern.org.au/api' paratoo.excludeInterventionProtocols = true +paratoo.core.documentationUrl = '/documentation/swagger.json' auth.baseUrl = 'https://auth-test.ala.org.au' userDetails.web.url = "${auth.baseUrl}/userdetails/" @@ -1391,3 +1395,94 @@ elasticsearch { username = 'elastic' password = 'password' } + +// paratoo / monitor + +paratoo.defaultPlotLayoutDataModels = [ + [ + dataType: "geoMap", + name: "plot_layout", + validate: "required" + ], + [ + dataType: "list", + name: "plot_visit", + validate: "required", + columns: [ + [ + dataType: "date", + name: "end_date", + dwcAttribute: "eventDate" + ], + [ + dataType: "text", + name: "visit_field_name" + ], + [ + dataType: "date", + name: "start_date", + dwcAttribute: "eventDate" + ] + ] + ] + ] + +paratoo.defaultPlotLayoutViewModels = [ + [ + type: "row", + items: [ + [ + type: "col", + items: [ + [ + type: "section", + title: "Plot Visit", + preLabel: "Plot Visit", + boxed: true, + items: [ + [ + type: "repeat", + source: "plot_visit", + userAddedRows: false, + items: [ + [ + type: "row", + class: "output-section", + items: [ + [ + type: "col", + items: [ + [ + type: "date", + source: "end_date", + preLabel: "End Date" + ], + [ + type: "text", + source: "visit_field_name", + preLabel: "Visit Field Name" + ], + [ + type: "date", + source: "start_date", + preLabel: "Start Date" + ] + ] + ] + ] + ] + ] + ] + ] + ], + [ + type: "geoMap", + source: "plot_layout", + orientation: "vertical" + ] + ] + ] + ] + ] + ] + diff --git a/grails-app/conf/data/mapping.json b/grails-app/conf/data/mapping.json index 980d85eb1..39e8c094a 100644 --- a/grails-app/conf/data/mapping.json +++ b/grails-app/conf/data/mapping.json @@ -60,6 +60,10 @@ "fundingType": { "type" : "keyword" }, + "fundingVerificationDate": { + "type": "date", + "ignore_malformed": true + }, "status": { "type" : "keyword" }, diff --git a/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy b/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy index 99c9a8dd8..82fcd699a 100644 --- a/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy @@ -1,5 +1,8 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.paratoo.ParatooCollection +import au.org.ala.ecodata.paratoo.ParatooProject +import au.org.ala.ecodata.paratoo.ParatooProtocolConfig import au.org.ala.web.AlaSecured import grails.converters.JSON import grails.util.Environment @@ -725,4 +728,58 @@ class AdminController { render errors as JSON } + /** + * Re-fetch data from Paratoo. Helpful when data could not be parsed correctly the first time. + * + * @return + */ + @AlaSecured(["ROLE_ADMIN"]) + def reSubmitDataSet() { + String projectId = params.id + String dataSetId = params.dataSetId + String userId = params.userId ?: userService.getCurrentUser().userId + if (!projectId || !dataSetId || !userId) { + render text: [message: "Bad request"] as JSON, status: HttpStatus.SC_BAD_REQUEST + return + } + + ParatooCollection collection = new ParatooCollection(orgMintedUUID: dataSetId, coreProvenance: [:]) + List projects = paratooService.userProjects(userId) + ParatooProject project = projects.find {it.project.projectId == projectId } + if (project) { + paratooService.submitCollection(collection, project, userId) + render text: [message: "Submitted request to fetch data for dataSet $dataSetId in project $projectId by user $userId"] as JSON, status: HttpStatus.SC_OK, contentType: 'application/json' + } + else { + render text: [message: "Project not found"] as JSON, status: HttpStatus.SC_NOT_FOUND + } + } + + + /** + * Helper function to check the form generated for a protocol during the sync operation. + * Usual step is to update Paratoo config in DB. Use this function to check the form generated. + * @return + */ + @AlaSecured(["ROLE_ADMIN"]) + def checkActivityFormForProtocol() { + String protocolId = params.id + List protocols = paratooService.getProtocolsFromParatoo() + Map protocol = protocols.find { it.attributes.identifier == protocolId } + if (!protocol) { + render text: [message: "Protocol not found"] as JSON, status: HttpStatus.SC_NOT_FOUND, contentType: 'application/json' + return + } + + Map documentation = paratooService.getParatooSwaggerDocumentation() + ParatooProtocolConfig config = paratooService.getProtocolConfig(protocolId) + if (!config) { + render text: [message: "Protocol config not found"] as JSON, status: HttpStatus.SC_NOT_FOUND, contentType: 'application/json' + return + } + + Map template = paratooService.buildTemplateForProtocol(protocol, documentation, config) + render text: template as JSON, status: HttpStatus.SC_OK, contentType: 'application/json' + } + } diff --git a/grails-app/controllers/au/org/ala/ecodata/ParatooController.groovy b/grails-app/controllers/au/org/ala/ecodata/ParatooController.groovy index 0042a736f..577908d99 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ParatooController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ParatooController.groovy @@ -1,12 +1,7 @@ package au.org.ala.ecodata import au.ala.org.ws.security.SkipApiKeyCheck -import au.org.ala.ecodata.paratoo.ParatooCollection -import au.org.ala.ecodata.paratoo.ParatooCollectionId -import au.org.ala.ecodata.paratoo.ParatooPlotSelection -import au.org.ala.ecodata.paratoo.ParatooPlotSelectionData -import au.org.ala.ecodata.paratoo.ParatooProject -import au.org.ala.ecodata.paratoo.ParatooToken +import au.org.ala.ecodata.paratoo.* import groovy.util.logging.Slf4j import io.swagger.v3.oas.annotations.OpenAPIDefinition import io.swagger.v3.oas.annotations.Operation @@ -33,7 +28,7 @@ import javax.ws.rs.PUT import javax.ws.rs.Path // Requiring these scopes will guarantee we can get a valid userId out of the process. @Slf4j -@au.ala.org.ws.security.RequireApiKey(scopes=["profile", "openid"]) +@au.ala.org.ws.security.RequireApiKey(scopes = ["profile", "openid"]) @OpenAPIDefinition( info = @Info( title = "Ecodata APIs", @@ -55,17 +50,17 @@ import javax.ws.rs.Path type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows( clientCredentials = @OAuthFlow( - authorizationUrl = "https://auth-test.ala.org.au/cas/oidc/authorize", - tokenUrl = "https://auth-test.ala.org.au/cas/oidc/token", - refreshUrl = "https://auth-test.ala.org.au/cas/oidc/refresh", - scopes = [ - @OAuthScope(name="openid"), - @OAuthScope(name="profile"), - @OAuthScope(name="ala", description = "CAS scope"), - @OAuthScope(name="roles", description = "CAS scope"), - @OAuthScope(name="ala/attrs", description = "Cognito scope"), - @OAuthScope(name="ala/roles", description = "Cognito scope") - ] + authorizationUrl = "https://auth-test.ala.org.au/cas/oidc/authorize", + tokenUrl = "https://auth-test.ala.org.au/cas/oidc/token", + refreshUrl = "https://auth-test.ala.org.au/cas/oidc/refresh", + scopes = [ + @OAuthScope(name = "openid"), + @OAuthScope(name = "profile"), + @OAuthScope(name = "ala", description = "CAS scope"), + @OAuthScope(name = "roles", description = "CAS scope"), + @OAuthScope(name = "ala/attrs", description = "Cognito scope"), + @OAuthScope(name = "ala/roles", description = "Cognito scope") + ] ) ), scheme = "bearer" @@ -250,7 +245,7 @@ class ParatooController { error(collectionId.errors) } else { String userId = userService.currentUserDetails.userId - boolean hasProtocol = paratooService.protocolWriteCheck(userId, collectionId.surveyId.projectId, collectionId.surveyId.protocol.id) + boolean hasProtocol = paratooService.protocolWriteCheck(userId, collectionId.projectId, collectionId.protocolId) if (hasProtocol) { Map mintResults = paratooService.mintCollectionId(userId, collectionId) if (mintResults.error) { @@ -282,24 +277,28 @@ class ParatooController { if (log.isDebugEnabled()) { log.debug("ParatooController::submitCollection") - log.debug(request.JSON.toString()) } if (collection.hasErrors()) { error(collection.errors) } else { String userId = userService.currentUserDetails.userId - Map dataSet = paratooService.findDataSet(userId, collection.orgMintedIdentifier) - - boolean hasProtocol = paratooService.protocolWriteCheck(userId, dataSet.project.id, collection.protocol.id) - if (hasProtocol) { - Map result = paratooService.submitCollection(collection, dataSet.project) - if (!result.error) { - respond([success: true]) + Map dataSet = paratooService.findDataSet(userId, collection.orgMintedUUID) + if (dataSet?.dataSet?.surveyId) { + ParatooCollectionId collectionId = ParatooCollectionId.fromMap(dataSet.dataSet.surveyId) + boolean hasProtocol = paratooService.protocolWriteCheck(userId, dataSet.project.id, collectionId.protocolId) + if (hasProtocol) { + Map result = paratooService.submitCollection(collection, dataSet.project) + if (!result.updateResult.error) { + respond([success: true]) + } else { + error(HttpStatus.SC_INTERNAL_SERVER_ERROR, result.updateResult.error) + } } else { - error(HttpStatus.SC_INTERNAL_SERVER_ERROR, result.error) + error(HttpStatus.SC_FORBIDDEN, "Project / protocol combination not available") } + } else { - error(HttpStatus.SC_FORBIDDEN, "Project / protocol combination not available") + error(HttpStatus.SC_NOT_FOUND, "No data set found with orgMintedUUID=${collection.orgMintedUUID}") } } } @@ -394,8 +393,8 @@ class ParatooController { plotSelections.addAll(it.plots) } } - plotSelections = plotSelections.unique {it.siteId} ?: [] - respond plots:plotSelections + plotSelections = plotSelections.unique { it.siteId } ?: [] + respond plots: plotSelections } private def addOrUpdatePlotSelection(ParatooPlotSelection plotSelection) { @@ -440,7 +439,7 @@ class ParatooController { ), tags = "Org Interface" ) - def updateProjectSites(@Parameter(name = "id", description = "Project id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string"))String id) { + def updateProjectSites(@Parameter(name = "id", description = "Project id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "string")) String id) { String userId = userService.currentUserDetails.userId List projects = paratooService.userProjects(userId) ParatooProject project = projects?.find { it.id == id } @@ -453,20 +452,19 @@ class ParatooController { Map result = paratooService.updateProjectSites(project, data.data, projects) if (result?.error) { - respond([message:result.error], status:HttpStatus.SC_INTERNAL_SERVER_ERROR) - } - else { - respond(buildUpdateProjectSitesResponse(id, data.data), status:HttpStatus.SC_OK) + respond([message: result.error], status: HttpStatus.SC_INTERNAL_SERVER_ERROR) + } else { + respond(buildUpdateProjectSitesResponse(id, data.data), status: HttpStatus.SC_OK) } } private static Map buildUpdateProjectSitesResponse(String id, Map data) { [ - "data": [ - "id": id, - "attributes": data - ], - meta: [:] + "data": [ + "id" : id, + "attributes": data + ], + meta : [:] ] } diff --git a/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy b/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy index 571e3fcf7..79736aca7 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy @@ -366,6 +366,17 @@ class ProjectController { } } + @RequireApiKey + def fetchDataSetRecords (String projectId, String dataSetId) { + if (projectId && dataSetId) { + List records = projectService.fetchDataSetRecords(projectId, dataSetId) + render text: records as JSON, contentType: 'application/json' + } + else { + render status: 400, text: "projectId and dataSetId are required parameters" + } + } + def importProjectsFromSciStarter(){ Integer count = projectService.importProjectsFromSciStarter()?:0 render(text: [count: count] as JSON, contentType: 'application/json'); diff --git a/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy b/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy index 9ce1abbd7..7f85d0b43 100644 --- a/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy @@ -194,6 +194,7 @@ class UrlMappings { "/ws/project/getDataCollectionWhiteList"(controller: "project"){ action = [GET:"getDataCollectionWhiteList"] } "/ws/project/getBiocollectFacets"(controller: "project"){ action = [GET:"getBiocollectFacets"] } "/ws/project/getDefaultFacets"(controller: "project", action: "getDefaultFacets") + "/ws/project/$projectId/dataSet/$dataSetId/records"(controller: "project", action: "fetchDataSetRecords") "/ws/admin/initiateSpeciesRematch"(controller: "admin", action: "initiateSpeciesRematch") "/ws/document/download"(controller:"document", action:"download") diff --git a/grails-app/controllers/au/org/ala/ecodata/graphql-test.http b/grails-app/controllers/au/org/ala/ecodata/graphql-test.http new file mode 100644 index 000000000..6ba0e69e8 --- /dev/null +++ b/grails-app/controllers/au/org/ala/ecodata/graphql-test.http @@ -0,0 +1,43 @@ +### +POST https://ecodata-test.ala.org.au/ws/graphql +Authorization: Bearer eyJraWQiOiI2UEpOaFwvdU5EYlBIWlk4Y2xmTHJvMnBKUnJhTFRXTnpaU0tOcVdka3Y0az0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiOTMyNTY0Zi1hNjQxLTQ3MTYtYTAwMS0wMTRkODA2NzU0MmQiLCJjb2duaXRvOmdyb3VwcyI6WyJ1c2VyIiwiZWNvZGF0YV9hcGkiXSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLmFwLXNvdXRoZWFzdC0yLmFtYXpvbmF3cy5jb21cL2FwLXNvdXRoZWFzdC0yX09PWFU5R1czOSIsInZlcnNpb24iOjIsImNsaWVudF9pZCI6IjRyaGtvcDl0bDMwbHJ0OThjcWpwY3UzZTF0Iiwib3JpZ2luX2p0aSI6IjMxYmYzYTg0LThiMDktNDVhNi05OTE3LTY5ODE5N2Y3N2EzYiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYWxhXC9hdHRycyBhbGFcL3JvbGVzIG9wZW5pZCBwcm9maWxlIGVtYWlsIiwiYXV0aF90aW1lIjoxNzA3MDgwMTM5LCJleHAiOjE3MDcwODczNDgsImlhdCI6MTcwNzA4Mzc0OCwianRpIjoiNjNhZTJjNTItYzUxZC00ZmJjLWFkMDAtODQ4NGIyOTQ1ZDI0IiwidXNlcm5hbWUiOiI2NDUwIn0.crLqFuvY9JLmxM88GI3ukYTZGlcYHmXTlJ8AFJHSkB2bkPoSJ8F7OnGnJuZlpzlBHLcyMM91JpVoNNdLlVFYptnafX279Q2oUXzAe_iiwPI2-JPPVEp4bDSYd7x9Yl488EYFBNGJ5cbEaSGyMh9WHggWAFG8BLXXUdWM8GfS2-PdqUwcFkPQAu7DDgSh0k6BDviZaOuA0-2AtLdx4c6yWA9dzfRlzMinbTnR3at0QAJ4Dv7xJhEsgvXaR2z4Pe9JrcIuoTZlJx360UQtuDgNLWA-fzDHfmsSOIuBBYB8E0gFkdKt7f7TE-M6KvIUKhp6z30xI4_OwGC52U0D11LISg +Content-Type: application/json + +{ + "query": "query {\n project(projectId:\"30b6e207-51fb-4286-881c-9ca31959010c\") {\n projectId\n name\n description\n \n program {\n name\n programId\n acronym\n }\n organisation {\n \torganisationId\n name\n abn\n }\n managementUnit {\n \tmanagementUnitId\n name\n shortName\n }\n }\n}" +} + +### +POST http://localhost:8080/ws/graphql +Authorization: Bearer eyJraWQiOiI2UEpOaFwvdU5EYlBIWlk4Y2xmTHJvMnBKUnJhTFRXTnpaU0tOcVdka3Y0az0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiOTMyNTY0Zi1hNjQxLTQ3MTYtYTAwMS0wMTRkODA2NzU0MmQiLCJjb2duaXRvOmdyb3VwcyI6WyJ1c2VyIiwiZWNvZGF0YV9hcGkiXSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLmFwLXNvdXRoZWFzdC0yLmFtYXpvbmF3cy5jb21cL2FwLXNvdXRoZWFzdC0yX09PWFU5R1czOSIsInZlcnNpb24iOjIsImNsaWVudF9pZCI6IjRyaGtvcDl0bDMwbHJ0OThjcWpwY3UzZTF0Iiwib3JpZ2luX2p0aSI6IjMxYmYzYTg0LThiMDktNDVhNi05OTE3LTY5ODE5N2Y3N2EzYiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYWxhXC9hdHRycyBhbGFcL3JvbGVzIG9wZW5pZCBwcm9maWxlIGVtYWlsIiwiYXV0aF90aW1lIjoxNzA3MDgwMTM5LCJleHAiOjE3MDcwODczNDgsImlhdCI6MTcwNzA4Mzc0OCwianRpIjoiNjNhZTJjNTItYzUxZC00ZmJjLWFkMDAtODQ4NGIyOTQ1ZDI0IiwidXNlcm5hbWUiOiI2NDUwIn0.crLqFuvY9JLmxM88GI3ukYTZGlcYHmXTlJ8AFJHSkB2bkPoSJ8F7OnGnJuZlpzlBHLcyMM91JpVoNNdLlVFYptnafX279Q2oUXzAe_iiwPI2-JPPVEp4bDSYd7x9Yl488EYFBNGJ5cbEaSGyMh9WHggWAFG8BLXXUdWM8GfS2-PdqUwcFkPQAu7DDgSh0k6BDviZaOuA0-2AtLdx4c6yWA9dzfRlzMinbTnR3at0QAJ4Dv7xJhEsgvXaR2z4Pe9JrcIuoTZlJx360UQtuDgNLWA-fzDHfmsSOIuBBYB8E0gFkdKt7f7TE-M6KvIUKhp6z30xI4_OwGC52U0D11LISg +Content-Type: application/json + +{ + "query": "query {\n project(projectId:\"5e416e05-cb4d-4f7e-84ec-e29291ce492a\") {\n projectId\n name\n description\n \n program {\n name\n programId\n acronym\n }\n organisation {\n \torganisationId\n name\n abn\n }\n managementUnit {\n \tmanagementUnitId\n name\n shortName\n }\n }\n}" +} + +### +GET http://localhost:8080/ws/paratoo/user-projects +Authorization: Bearer eyJraWQiOiI2UEpOaFwvdU5EYlBIWlk4Y2xmTHJvMnBKUnJhTFRXTnpaU0tOcVdka3Y0az0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2ODMyZDkyNy03ZTE0LTRkMTYtODk0ZS00N2I4NGM3YjdkNWYiLCJjb2duaXRvOmdyb3VwcyI6WyJ1c2VyIiwiZWNvZGF0YV9hcGkiLCJhZG1pbiJdLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtc291dGhlYXN0LTIuYW1hem9uYXdzLmNvbVwvYXAtc291dGhlYXN0LTJfT09YVTlHVzM5IiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiNHJoa29wOXRsMzBscnQ5OGNxanBjdTNlMXQiLCJvcmlnaW5fanRpIjoiOGMxNmM3NmItNTA3Ny00OWRmLWI2YTAtY2Y1Y2NjMmExMTRmIiwiZXZlbnRfaWQiOiIzYzk1NmYzYS0yMGFhLTQxMjAtYjM1Zi0yMzhhMTkzMWE1NDUiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImFsYVwvYXR0cnMgYWxhXC9yb2xlcyBvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImF1dGhfdGltZSI6MTcwNjU2NTg5NSwiZXhwIjoxNzA2NTc2NzY3LCJpYXQiOjE3MDY1NzMxNjgsImp0aSI6IjE5NWQ0ODkwLWM1YWUtNDJmNy05NjA2LTVmNjVlZGI5NmQ4YyIsInVzZXJuYW1lIjoiNTY1MDEifQ.yoePlhuO22-CqSiMh9rM41El_bI-gohdRk7KQWphxo9GUAMba4Kf8efT8bnxzLzuuQ5bM4egie0iRdmIcHPNzuZfoLidvn9Q_ItjSYkBeIaxozFKyy3uGl5n6_BOuzQdXN2J831Qj_me_38mGKbRRp1W-cjOJu7xH-lYudXiyzSjdv02MpRdAQEfKG47BW_r2FFXBdDCKrhZ_pSQjjFQng4NLFefeMPxmErR6CYDEwBG9_u5m_WQea9P_gy6mjsuQ-bXZbZq4BSNswJgd1pmayNz-65gX-hewA2eE6VVOWwzgmwakJNVEF1FX9tMcYWd0wRKJBvBOzz2Dii-5Xd3gg +Content-Type: */* + +### +GET https://ecodata-staging.ala.org.au/ws/paratoo/user-projects +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIm9yZy5hcGVyZW8uY2FzLnNlcnZpY2VzLlJlZ2lzdGVyZWRTZXJ2aWNlIjoiMTY3MTE1MjI5MTY4OCIsImtpZCI6InNpZy0xNjU1OTU2NDkzIn0.eyJzdWIiOiIxMDkxNDYwNjc2NzgyNDkyNzg1ODQiLCJyb2xlIjpbIlJPTEVfQURNSU4iLCJST0xFX0FQSV9FRElUT1IiLCJST0xFX0NPTExFQ1RJT05fQURNSU4iLCJST0xFX0VDT0RBVEFfQVBJIiwiUk9MRV9FRElUT1IiLCJST0xFX1VTRVIiLCJST0xFX1ZQX0FETUlOIiwiUk9MRV9WUF9URVNUX0FETUlOIl0sIm9hdXRoQ2xpZW50SWQiOiIyWHZsZ0pYWHVzc3R4aFZkTlJaY0J0aUpIeFdiRlNUZVJDNWYiLCJjbGllbnROYW1lIjoiR29vZ2xlIiwiaXNzIjoiaHR0cHM6XC9cL2F1dGguYWxhLm9yZy5hdVwvY2FzXC9vaWRjIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2hyaXMuZ29kd2luLmFsYUBnbWFpbC5jb20iLCJjbGllbnRfaWQiOiIyWHZsZ0pYWHVzc3R4aFZkTlJaY0J0aUpIeFdiRlNUZVJDNWYiLCJ1cGRhdGVkX2F0IjoiMjAyMi0wNy0xNCAxMDoyMzoyMyIsImdyYW50X3R5cGUiOiJBVVRIT1JJWkFUSU9OX0NPREUiLCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIl0sInNlcnZlcklwQWRkcmVzcyI6IjEyNy4wLjAuMSIsImxvbmdUZXJtQXV0aGVudGljYXRpb25SZXF1ZXN0VG9rZW5Vc2VkIjpmYWxzZSwic3RhdGUiOiI1NDg4YTdiZTkxOTk0ZDQ5OWJiMzEyMGUxNjhkY2MzZiIsImV4cCI6MTcxMDUzNTg2MywiaWF0IjoxNzEwNDQ5NDYzLCJqdGkiOiJBVC0zMjM2Ny1NOTFnRkFYdXRHaFlESkp6LTRlMm1nQVNKQXRIN1N3TCIsImVtYWlsIjoiY2hyaXMuZ29kd2luLmFsYUBnbWFpbC5jb20iLCJjbGllbnRJcEFkZHJlc3MiOiIxNDAuNzkuNzkuOTYiLCJpc0Zyb21OZXdMb2dpbiI6dHJ1ZSwiZW1haWxfdmVyaWZpZWQiOiIxIiwiYXV0aGVudGljYXRpb25EYXRlIjoiMjAyNC0wMy0xNFQyMDo1MTowMi45MzE0OTFaIiwic3VjY2Vzc2Z1bEF1dGhlbnRpY2F0aW9uSGFuZGxlcnMiOiJEZWxlZ2F0ZWRDbGllbnRBdXRoZW50aWNhdGlvbkhhbmRsZXIiLCJ1c2VyQWdlbnQiOiJNb3ppbGxhXC81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXRcLzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZVwvMTIyLjAuMC4wIFNhZmFyaVwvNTM3LjM2IiwiZ2l2ZW5fbmFtZSI6IkNocmlzIiwibm9uY2UiOiIiLCJjcmVkZW50aWFsVHlwZSI6IkNsaWVudENyZWRlbnRpYWwiLCJzYW1sQXV0aGVudGljYXRpb25TdGF0ZW1lbnRBdXRoTWV0aG9kIjoidXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOmFtOnVuc3BlY2lmaWVkIiwiYXVkIjoiaHR0cHM6XC9cL3Rva2Vucy5hbGEub3JnLmF1XC9sb2dpbiIsImF1dGhlbnRpY2F0aW9uTWV0aG9kIjoiRGVsZWdhdGVkQ2xpZW50QXV0aGVudGljYXRpb25IYW5kbGVyIiwiZ2VvTG9jYXRpb24iOiJ1bmtub3duIiwibmFtZSI6IkNocmlzIEdvZHdpbiIsInNjb3BlcyI6WyJvcGVuaWQiLCJwcm9maWxlIl0sImZhbWlseV9uYW1lIjoiR29kd2luIn0.jazpxAZPTWJ3vzEPnrHAZkmxllLK64Pk-mRmddv5k_IhSTHiEvvrA6IP9GTd7BDWSnBtEfmUFKqv5wk3Igz3m7oQR97aAvqA8t1ORqGhrdNUJu4dU5V5l7EG8wLuwcT7k668sAa_kpxH1YFpDzXp_tG4xIKtk3tpAZOSnjYEzCPy3MVCiw5f4Tm-7CGS125T_JOIyST6_nh4HHygYHoXDdjy-MvRJqrvWFR7tKMN99zscvCBuOBjHy5fyzj16dSPY67q9-J0DuGiHR0-okwtev4rajr8LNXGsX40N-CwtAlzXYJK6ThJcYtRM7KNlImtW6Jyyx7TqRKC-IRWCE8L7g +Content-Type: application/json + +### +GET http://localhost:8080/ws/paratoo/user-projects +Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIm9yZy5hcGVyZW8uY2FzLnNlcnZpY2VzLlJlZ2lzdGVyZWRTZXJ2aWNlIjoiMTY5NTg3MjAzMTM4MyIsImtpZCI6InNpZy0xNjU1OTU2NDkzIn0.eyJzdWIiOiIxMDkxNDYwNjc2NzgyNDkyNzg1ODQiLCJyb2xlIjpbIlJPTEVfQURNSU4iLCJST0xFX0FQSV9FRElUT1IiLCJST0xFX0NPTExFQ1RJT05fQURNSU4iLCJST0xFX0VDT0RBVEFfQVBJIiwiUk9MRV9FRElUT1IiLCJST0xFX1VTRVIiLCJST0xFX1ZQX0FETUlOIiwiUk9MRV9WUF9URVNUX0FETUlOIl0sIm9hdXRoQ2xpZW50SWQiOiJvUEJFZEYwNW5WWGNWamJjZ3JlY3B3cVd4cFdzeEtUQnRmRVYiLCJjbGllbnROYW1lIjoiR29vZ2xlIiwiaXNzIjoiaHR0cHM6XC9cL2F1dGguYWxhLm9yZy5hdVwvY2FzXC9vaWRjIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiY2hyaXMuZ29kd2luLmFsYUBnbWFpbC5jb20iLCJ1c2VyaWQiOiIxNDkzIiwiY2xpZW50X2lkIjoib1BCRWRGMDVuVlhjVmpiY2dyZWNwd3FXeHBXc3hLVEJ0ZkVWIiwidXBkYXRlZF9hdCI6IjIwMjItMDctMTQgMTA6MjM6MjMiLCJncmFudF90eXBlIjoiQVVUSE9SSVpBVElPTl9DT0RFIiwic2NvcGUiOlsiYWxhIiwib3BlbmlkIiwicHJvZmlsZSIsInJvbGVzIl0sInNlcnZlcklwQWRkcmVzcyI6IjEyNy4wLjAuMSIsImxvbmdUZXJtQXV0aGVudGljYXRpb25SZXF1ZXN0VG9rZW5Vc2VkIjpmYWxzZSwic3RhdGUiOiI3Njc0OWRjMmI2Y2U0N2Y2YmJiMGI1ZDdhOTk5Y2QwMSIsImV4cCI6MTcwODEzMDE1MCwiaWF0IjoxNzA4MDQzNzUwLCJqdGkiOiJBVC00MDU4MC1lOHczT1YwZUgtbkF1VlRwbk1vc2NkUHNBOXlsdXFyQiIsImVtYWlsIjoiY2hyaXMuZ29kd2luLmFsYUBnbWFpbC5jb20iLCJjbGllbnRJcEFkZHJlc3MiOiIxNDAuMjUzLjIyNC4yNDciLCJpc0Zyb21OZXdMb2dpbiI6ZmFsc2UsImVtYWlsX3ZlcmlmaWVkIjoiMSIsImF1dGhlbnRpY2F0aW9uRGF0ZSI6IjIwMjQtMDItMTVUMjI6MTc6MzEuNjkwMTU3WiIsInN1Y2Nlc3NmdWxBdXRoZW50aWNhdGlvbkhhbmRsZXJzIjoiRGVsZWdhdGVkQ2xpZW50QXV0aGVudGljYXRpb25IYW5kbGVyIiwidXNlckFnZW50IjoiTW96aWxsYVwvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0XC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWVcLzEyMS4wLjAuMCBTYWZhcmlcLzUzNy4zNiIsImdpdmVuX25hbWUiOiJDaHJpcyIsIm5vbmNlIjoiIiwiY3JlZGVudGlhbFR5cGUiOiJDbGllbnRDcmVkZW50aWFsIiwic2FtbEF1dGhlbnRpY2F0aW9uU3RhdGVtZW50QXV0aE1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphbTp1bnNwZWNpZmllZCIsImF1ZCI6Imh0dHBzOlwvXC90b2tlbnMuYWxhLm9yZy5hdVwvbG9naW4iLCJhdXRoZW50aWNhdGlvbk1ldGhvZCI6IkRlbGVnYXRlZENsaWVudEF1dGhlbnRpY2F0aW9uSGFuZGxlciIsImdlb0xvY2F0aW9uIjoidW5rbm93biIsIm5hbWUiOiJDaHJpcyBHb2R3aW4iLCJzY29wZXMiOlsib3BlbmlkIiwicHJvZmlsZSIsImFsYSIsInJvbGVzIl0sImZhbWlseV9uYW1lIjoiR29kd2luIn0.M74V6ibistW_VGeTQhUdDpn_h7rk8G5QoCm56EPOgpmnyxAeZK3Kz_omMELPI5p2cdvaAttJ6ZMvd268jtHju9VfDmm7IP668ikpDQsWoaFVaXiFi1X0FPvucilDm3Aa1Sd3rqeEH9fNDO_OEJe4NvE4JaKUAXGp4ZjboV5xZZniuczlZyPXpQ8p-YqIIZRtEfYnbBrSJt5xA8GzVXIhXCbuUfb6AjH1qkG5qebHGh3LYZ2BEx_X9nfew8WD3wP6k8OebLJecvxm-u3ELKV48c5LAf4RFHjNSEcR9-NM3Yg7r5y4PP-wGuikh1cFjWMEPgwtB6T3Bq2xmPnLtkuZAQ +Content-Type: application/json + +### +GET https://dev.core-api.monitor.tern.org.au/api/cover-point-intercept-species-intercepts?populate=deep +Authorization: Bearer eyJraWQiOiI2UEpOaFwvdU5EYlBIWlk4Y2xmTHJvMnBKUnJhTFRXTnpaU0tOcVdka3Y0az0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2ODMyZDkyNy03ZTE0LTRkMTYtODk0ZS00N2I4NGM3YjdkNWYiLCJjb2duaXRvOmdyb3VwcyI6WyJ1c2VyIiwiZWNvZGF0YV9hcGkiLCJhZG1pbiJdLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtc291dGhlYXN0LTIuYW1hem9uYXdzLmNvbVwvYXAtc291dGhlYXN0LTJfT09YVTlHVzM5IiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiN2E5MGMzNjB2MXEwazY4cjFwYXM5MmRka3AiLCJvcmlnaW5fanRpIjoiMWY0MDIzMmEtNzAyMy00NDMyLWE1NTAtMTNkMjA5MTEzYTYzIiwidG9rZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJhbGFcL2F0dHJzIGFsYVwvcm9sZXMgb3BlbmlkIHByb2ZpbGUgZW1haWwiLCJhdXRoX3RpbWUiOjE3MDgzODI4MzAsImV4cCI6MTcwODM4NjQzMCwiaWF0IjoxNzA4MzgyODMxLCJqdGkiOiI3YTVhN2I4MS1kOWQ3LTRlMjUtOWVmMy03M2M0OTk5Zjg2YzUiLCJ1c2VybmFtZSI6IjU2NTAxIn0.hHOysWUwZer0JzpDWJZtWWAXebEbzAkLz3Q74wg0LuGCyWMFonredGZ6fA2NxwCwX3FB-4ppTA6QdyUiNAnz846dK7pIN6Tfv1shG6whqYvBDvpmHblgRl6MFnIHiFJVoCUoRxWM6A9nArwRAvPqv9fOPQzxeIWDGn0f2sQkfPdCCvYsTFYqC2qhF_GqjaXxb2Wsf2dDq0RhldNRmBGMuuu-BiloPGzVl9tZAsH7kyiNsZD8UdCzUkIBQhYmgWlhYBRUzbcAeJ7vrvQMdHQ-vO7xGT-s6xxudDwt24zRsjXt83YDTHdMxY8roc_3ZEn0cGo3gBaoMq4HBpXUo_8w-Q +Content-Type: application/json + +### +GET https://dev.core-api.monitor.tern.org.au/api/protocols/reverse-lookup +Authorization: Bearer eyJraWQiOiI2UEpOaFwvdU5EYlBIWlk4Y2xmTHJvMnBKUnJhTFRXTnpaU0tOcVdka3Y0az0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2ODMyZDkyNy03ZTE0LTRkMTYtODk0ZS00N2I4NGM3YjdkNWYiLCJjb2duaXRvOmdyb3VwcyI6WyJ1c2VyIiwiZWNvZGF0YV9hcGkiLCJhZG1pbiJdLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtc291dGhlYXN0LTIuYW1hem9uYXdzLmNvbVwvYXAtc291dGhlYXN0LTJfT09YVTlHVzM5IiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiN2E5MGMzNjB2MXEwazY4cjFwYXM5MmRka3AiLCJvcmlnaW5fanRpIjoiMWY0ZDljOTYtNDBkOC00YzNmLTkxMDctNDQ1YjdiOTEzOGIzIiwiZXZlbnRfaWQiOiJhZWVmYzEwNi1lYjc1LTQ1OTEtYjE3OC02MjM2NWU3NWU4NDAiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImFsYVwvYXR0cnMgYWxhXC9yb2xlcyBvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImF1dGhfdGltZSI6MTcwOTA2Njg1MSwiZXhwIjoxNzA5MDcwNDUxLCJpYXQiOjE3MDkwNjY4NTIsImp0aSI6IjFlZGYyNzQ2LTJlY2ItNDRhOC05NjNlLTAwZTM1ODNkYjllMyIsInVzZXJuYW1lIjoiNTY1MDEifQ.x5h1Hw1fbomORk_jVxL8-fdjeO4uqbG6UoCKfeGQo_CJV02ZS5OhYmEolZkVSL5MkdytHBqY6Vojh5MrP9738He8exlbBRh8OnaA6QCb4Iasio0xWJcbysxt5Y98TGOC1cfcUAbjaZpwMH7r6ifQVoiK0RcztZJWlcSBp5IE2rZeSZ2tawg-Xl_JqR0d69wukn6gcNgrg5gPGPRqGbfrFwqciIKaj2KbZ8axZZi1kakPRGPH8FB2nxvPp4XGsjs-hV__8KxeNMRD2G0-paI0LyqWbvNOJnUfKYYNY2G8AgH-CJWLEA0Llo3kHORqnm3PyzcXvr82MhAn1ujdGlxDYQ + +userId=1493 \ No newline at end of file diff --git a/grails-app/domain/au/org/ala/ecodata/Project.groovy b/grails-app/domain/au/org/ala/ecodata/Project.groovy index e78edf691..0bfd23021 100644 --- a/grails-app/domain/au/org/ala/ecodata/Project.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Project.groovy @@ -70,6 +70,8 @@ class Project { List ecoScienceType = [] List tags = [] double funding + /** The most recent date the correctness of the project funding was checked */ + Date fundingVerificatonDate String orgIdGrantee, orgIdSponsor, orgIdSvcProvider String userCreated, userLastModified boolean isExternal = false // An external project only has a listing with the ALA and is not using data capture capabilities @@ -236,6 +238,7 @@ class Project { mapDisplays nullable: true terminationReason nullable: true fundingType nullable: true + fundingVerificatonDate nullable: true electionCommitmentYear nullable: true geographicInfo nullable:true portfolio nullable: true diff --git a/grails-app/domain/au/org/ala/ecodata/Record.groovy b/grails-app/domain/au/org/ala/ecodata/Record.groovy index 377c91f2c..0dfe00481 100644 --- a/grails-app/domain/au/org/ala/ecodata/Record.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Record.groovy @@ -14,23 +14,29 @@ class Record { activityId index: true projectActivityId index: true lastUpdated index: true + dataSetId index: true + outputId index: true version false } ObjectId id String projectId //ID of the project within ecodata String projectActivityId + String dataSetId String activityId String occurrenceID String outputSpeciesId // reference to output species outputSpeciesId. String userId String eventDate //should be a date in "yyyy-MM-dd" or "2014-11-24T04:55:48+11:00" format + String scientificName + String name + String vernacularName Double decimalLatitude Double decimalLongitude Double generalizedDecimalLatitude Double generalizedDecimalLongitude Integer coordinateUncertaintyInMeters - Integer individualCount + Integer individualCount = 1 Integer numberOfOrganisms Date dateCreated Date lastUpdated @@ -66,6 +72,10 @@ class Record { outputItemId nullable: true status nullable: true outputSpeciesId nullable: true + dataSetId nullable: true + name nullable: true + vernacularName nullable: true + scientificName nullable: true } String getRecordNumber(sightingsUrl){ diff --git a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy index 93a914eb7..0f34079b0 100644 --- a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy @@ -1317,7 +1317,7 @@ class ElasticSearchService { * @param path * @return */ - List getDataFromPath(output, List path){ + static List getDataFromPath(output, List path){ def temp = output List result = [] List navigatedPath = [] diff --git a/grails-app/services/au/org/ala/ecodata/MetadataService.groovy b/grails-app/services/au/org/ala/ecodata/MetadataService.groovy index 423b86642..ff5ff114c 100644 --- a/grails-app/services/au/org/ala/ecodata/MetadataService.groovy +++ b/grails-app/services/au/org/ala/ecodata/MetadataService.groovy @@ -844,13 +844,23 @@ class MetadataService { data } - Map autoPopulateSpeciesData(Map data){ - if (!data?.guid && data?.scientificName) { - def result = speciesReMatchService.searchBie(data.scientificName, 10) - // only if there is a single match - if (result?.autoCompleteList?.size() == 1) { - data.guid = result?.autoCompleteList[0]?.guid - data.commonName = data.commonName ?: result?.autoCompleteList[0]?.commonName + Map autoPopulateSpeciesData (Map data, int limit = 10) { + String searchName = (data?.scientificName)?.trim() + if (!data?.guid && (searchName)) { + def result = speciesReMatchService.searchBie(searchName, limit) + // find the name that exactly matches the search name + def bestMatch = result?.autoCompleteList?.find { + it.matchedNames?.findResult { String name -> + name.equalsIgnoreCase(searchName) + || name.equalsIgnoreCase(data.name) + || name.equalsIgnoreCase(data.commonName) + } + } + + if (bestMatch) { + data.guid = bestMatch?.guid + data.commonName = data.commonName ?: bestMatch?.commonName + data.scientificName = data.scientificName ?: bestMatch?.name } } diff --git a/grails-app/services/au/org/ala/ecodata/OutputService.groovy b/grails-app/services/au/org/ala/ecodata/OutputService.groovy index 6f049cec1..5e3597b67 100644 --- a/grails-app/services/au/org/ala/ecodata/OutputService.groovy +++ b/grails-app/services/au/org/ala/ecodata/OutputService.groovy @@ -3,7 +3,6 @@ package au.org.ala.ecodata import au.org.ala.ecodata.converter.RecordConverter import au.org.ala.ecodata.metadata.OutputMetadata -import static au.org.ala.ecodata.Status.ACTIVE import static au.org.ala.ecodata.Status.DELETED class OutputService { @@ -155,8 +154,12 @@ class OutputService { props.data = saveImages(props.data, props.name, output.outputId, props.activityId); props.data = saveAudio(props.data, props.name, output.outputId, props.activityId); - - createOrUpdateRecordsForOutput(activity, output, props) + try { + createOrUpdateRecordsForOutput(activity, output, props) + } + catch (Exception ex) { + log.error("Error creating records for activity - ${activity.activityId}", ex) + } commonService.updateProperties(output, props) return [status: 'ok', outputId: output.outputId] @@ -241,7 +244,12 @@ class OutputService { List statusUpdate = recordService.updateRecordStatusByOutput(outputId, Status.DELETED) if (!statusUpdate) { - createOrUpdateRecordsForOutput(activity, output, props) + try { + createOrUpdateRecordsForOutput(activity, output, props) + } + catch (Exception ex) { + log.error("Error creating records for activity - ${activity.activityId}", ex) + } commonService.updateProperties(output, props) result = [status: 'ok'] } else { diff --git a/grails-app/services/au/org/ala/ecodata/ParatooService.groovy b/grails-app/services/au/org/ala/ecodata/ParatooService.groovy index 8177dcce5..c0d745bf9 100644 --- a/grails-app/services/au/org/ala/ecodata/ParatooService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ParatooService.groovy @@ -1,28 +1,27 @@ package au.org.ala.ecodata - -import au.org.ala.ecodata.paratoo.ParatooCollection -import au.org.ala.ecodata.paratoo.ParatooCollectionId -import au.org.ala.ecodata.paratoo.ParatooMintedIdentifier -import au.org.ala.ecodata.paratoo.ParatooPlotSelectionData -import au.org.ala.ecodata.paratoo.ParatooProject -import au.org.ala.ecodata.paratoo.ParatooProtocolConfig -import au.org.ala.ecodata.paratoo.ParatooProtocolId -import au.org.ala.ecodata.paratoo.ParatooSurveyId +import au.org.ala.ecodata.metadata.PropertyAccessor +import au.org.ala.ecodata.paratoo.* import au.org.ala.ws.tokens.TokenService +import grails.async.Promise import grails.converters.JSON import grails.core.GrailsApplication import groovy.util.logging.Slf4j +import javassist.NotFoundException -import java.net.http.HttpHeaders +import java.util.regex.Matcher +import java.util.regex.Pattern +import static grails.async.Promises.task /** * Supports the implementation of the paratoo "org" interface */ @Slf4j class ParatooService { - + static final String DATASET_DATABASE_TABLE = 'Database Table' + static final int PARATOO_MAX_RETRIES = 3 static final String PARATOO_PROTOCOL_PATH = '/protocols' + static final String PARATOO_DATA_PATH = '/protocols/reverse-lookup' static final String PARATOO_PROTOCOL_FORM_TYPE = 'EMSA' static final String PARATOO_PROTOCOLS_KEY = 'paratoo.protocols' static final String PARATOO_PROTOCOL_DATA_MAPPING_KEY = 'paratoo.surveyData.mapping' @@ -33,6 +32,37 @@ class ParatooService { ['Plot Selection and Layout', 'Plot Description', 'Opportune'] static final List ADMIN_ONLY_PROTOCOLS = ['Plot Selection'] static final String INTERVENTION_PROTOCOL_TAG = 'intervention' + static final String PARATOO_UNIT_FIELD_NAME = "x-paratoo-unit" + static final String PARATOO_HINT = "x-paratoo-hint" + static final String PARATOO_MODEL_REF = "x-model-ref" + static final int PARATOO_FLOAT_DECIMAL_PLACES = 6 + static final String PARATOO_SCHEMA_POSTFIX = "Request" + static final String PARATOO_FILE_SCHEMA_NAME = "UploadFile" + static final String PARATOO_FILE_MODEL_NAME = "file" + static final String PARATOO_LUT_REF = "x-lut-ref" + static final String PARATOO_FILE_TYPE = "x-paratoo-file-type" + static final String PARATOO_IMAGE_FILE_TYPE = "images" + static final String PARATOO_SPECIES_TYPE = "x-paratoo-csv-list-taxa" + static final String PARATOO_FIELD_LABEL = "x-paratoo-rename" + static final List PARATOO_IGNORE_MODEL_LIST = [ + 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'created_by', 'createdBy', 'updated_by', 'updatedBy', + 'published_at', 'publishedAt', 'x-paratoo-file-type', PARATOO_DATAMODEL_PLOT_LAYOUT, PARATOO_DATAMODEL_PLOT_SELECTION, + PARATOO_DATAMODEL_PLOT_VISIT, 'plot-visit', 'plot-selection', 'plot-layout', 'survey_metadata' + ] + static final List PARATOO_IGNORE_X_MODEL_REF_LIST_MINIMUM = [ + 'file', 'admin::user' + ] + static final List PARATOO_IGNORE_X_MODEL_REF_LIST = [ + 'plot-visit', 'plot-selection', 'plot-layout' + ] + PARATOO_IGNORE_X_MODEL_REF_LIST_MINIMUM + static final String PARATOO_WORKFLOW_PLOT_LAYOUT = "plot-layout" + static final String PARATOO_COMPONENT = "x-paratoo-component" + static final String PARATOO_TYPE_ARRAY = "array" + static final String PARATOO_LOCATION_COMPONENT = "location.location" + static final String PARATOO_LOCATION_COMPONENT_STARTS_WITH = "location." + static final String PARATOO_DATAMODEL_PLOT_LAYOUT = "plot_layout" + static final String PARATOO_DATAMODEL_PLOT_SELECTION = "plot_selection" + static final String PARATOO_DATAMODEL_PLOT_VISIT = "plot_visit" GrailsApplication grailsApplication SettingService settingService @@ -41,8 +71,11 @@ class ParatooService { SiteService siteService PermissionService permissionService TokenService tokenService - MetadataService metadataService + CacheService cacheService ActivityService activityService + RecordService recordService + MetadataService metadataService + UserService userService /** * The rules we use to find projects eligible for use by paratoo are: @@ -64,7 +97,7 @@ class ParatooService { project.protocols = findProjectProtocols(project) } - projects.findAll{it.protocols} + projects.findAll { it.protocols } } private List findProjectProtocols(ParatooProject project) { @@ -76,11 +109,11 @@ class ParatooService { List categoriesWithDefaults = monitoringProtocolCategories + DEFAULT_MODULES protocols += findProtocolsByCategories(categoriesWithDefaults.unique()) if (!project.isParaooAdmin()) { - protocols = protocols.findAll{!(it.name in ADMIN_ONLY_PROTOCOLS)} + protocols = protocols.findAll { !(it.name in ADMIN_ONLY_PROTOCOLS) } } // Temporarily exclude intervention protocols until they are ready if (grailsApplication.config.getProperty('paratoo.excludeInterventionProtocols', Boolean.class, true)) { - protocols = protocols.findAll{!(INTERVENTION_PROTOCOL_TAG in it.tags)} + protocols = protocols.findAll { !(INTERVENTION_PROTOCOL_TAG in it.tags) } } } @@ -106,8 +139,7 @@ class ParatooService { permission = permissionService.findParentPermission(permission) projectAccessLevels[projectId] = permission?.accessLevel } - } - else { + } else { // Update the map of projectId to accessLevel projectAccessLevels[projectId] = permission.accessLevel } @@ -138,13 +170,17 @@ class ParatooService { } Map mintCollectionId(String userId, ParatooCollectionId paratooCollectionId) { - String projectId = paratooCollectionId.surveyId.projectId + String projectId = paratooCollectionId.projectId Project project = Project.findByProjectId(projectId) + // Update the identifier properties as per the org contract and save the data to the data set + String dataSetId = Identifiers.getNew(true, '') + paratooCollectionId.eventTime = new Date() + paratooCollectionId.survey_metadata.orgMintedUUID = dataSetId + paratooCollectionId.userId = project.organisationId // using the organisation as the owner of the data Map dataSet = mapParatooCollectionId(paratooCollectionId, project) - dataSet.progress = Activity.PLANNED - String dataSetName = buildName(paratooCollectionId.surveyId, project) - dataSet.name = dataSetName + + dataSet.surveyId = paratooCollectionId.toMap() // No codec to save this to mongo if (!project.custom) { project.custom = [:] @@ -152,18 +188,12 @@ class ParatooService { if (!project.custom.dataSets) { project.custom.dataSets = [] } - ParatooMintedIdentifier orgMintedIdentifier = new ParatooMintedIdentifier( - surveyId: paratooCollectionId.surveyId, - eventTime: new Date(), - userId: userId, - projectId: projectId, - system: "MERIT", - version: metadataService.getVersion() - ) - dataSet.orgMintedIdentifier = orgMintedIdentifier.encodeAsMintedCollectionId() + + dataSet.orgMintedIdentifier = paratooCollectionId.encodeAsOrgMintedIdentifier() + log.info "Minting identifier for Monitor collection: ${paratooCollectionId}: ${dataSet.orgMintedIdentifier}" project.custom.dataSets << dataSet - Map result = projectService.update([custom:project.custom], projectId, false) + Map result = projectService.update([custom: project.custom], projectId, false) if (!result.error) { result.orgMintedIdentifier = dataSet.orgMintedIdentifier @@ -171,74 +201,203 @@ class ParatooService { result } - private static String buildName(ParatooSurveyId surveyId, Project project) { - ActivityForm protocolForm = ActivityForm.findByExternalId(surveyId.protocol.id) - String dataSetName = protocolForm?.name + " - " + surveyId.timeAsDisplayDate() + " (" + project.name + ")" + private static String buildName(String protocolId, String displayDate, Project project) { + ActivityForm protocolForm = ActivityForm.findByExternalId(protocolId) + String dataSetName = protocolForm?.name + " - " + displayDate + " (" + project.name + ")" dataSetName } - Map submitCollection(ParatooCollection collection, ParatooProject project) { - - Map dataSet = project.project.custom?.dataSets?.find{it.orgMintedIdentifier == collection.orgMintedIdentifier} + /** + * Stores a collection created by monitor. This method will create a site, activity and output. + * If species are recorded, record of it are automatically generated. + * @param collection + * @param project + * @param userId - user making the data submission + * @return + */ + Map submitCollection(ParatooCollection collection, ParatooProject project, String userId = null) { + userId = userId ?: userService.currentUserDetails?.userId + Map dataSet = project.project.custom?.dataSets?.find{it.dataSetId == collection.orgMintedUUID} if (!dataSet) { - throw new RuntimeException("Unable to find data set with orgMintedIdentifier: "+collection.orgMintedIdentifier) + throw new RuntimeException("Unable to find data set with orgMintedUUID: "+collection.orgMintedUUID) } dataSet.progress = Activity.STARTED + dataSet.surveyId.coreSubmitTime = new Date() + dataSet.surveyId.survey_metadata.provenance.putAll(collection.coreProvenance) + + Map authHeader = getAuthHeader() + Promise promise = task { + asyncFetchCollection(collection, authHeader, userId, project) + } + promise.onError { Throwable e -> + log.error("An error occurred feching ${collection.orgMintedUUID}: ${e.message}", e) + } + def result = projectService.update([custom: project.project.custom], project.id, false) + [updateResult: result, promise: promise] + } - ParatooSurveyId surveyId = ParatooSurveyId.fromMap(dataSet.surveyId) + Map asyncFetchCollection(ParatooCollection collection, Map authHeader, String userId, ParatooProject project) { + Activity.withSession { session -> + int counter = 0 + Map surveyDataAndObservations = null + Map response = null + Map dataSet = project.project.custom?.dataSets?.find{it.dataSetId == collection.orgMintedUUID} - ParatooProtocolConfig config = getProtocolConfig(surveyId.protocol.id) - Map surveyData = retrieveSurveyData(surveyId, config) + if (!dataSet) { + throw new RuntimeException("Unable to find data set with orgMintedUUID: "+collection.orgMintedUUID) + } - if (surveyData) { - // If we are unable to create a site, null will be returned - assigning a null siteId is valid. - dataSet.siteId = createSiteFromSurveyData(surveyData, collection, surveyId, project.project, config) - dataSet.startDate = config.getStartDate(surveyData) - dataSet.endDate = config.getEndDate(surveyData) + // wait for 5 seconds before fetching data + while(response == null && counter < PARATOO_MAX_RETRIES) { + sleep(5 * 1000) + try { + response = retrieveSurveyAndObservations(collection, authHeader) + surveyDataAndObservations = response?.collections + } catch (Exception e) { + log.error("Error fetching collection data for ${collection.orgMintedUUID}: ${e.message}") + } - createActivityFromSurveyData(surveyId, collection.orgMintedIdentifier, surveyData, config, project) - } - else { - log.warn("Unable to retrieve survey data for: "+collection.orgMintedIdentifier) - } + counter++ + } - Map result = projectService.update([custom:project.project.custom], project.id, false) + if (surveyDataAndObservations == null) { + log.error("Unable to fetch collection data for ${collection.orgMintedUUID}") + return + } else { + ParatooCollectionId surveyId = ParatooCollectionId.fromMap(dataSet.surveyId) + ParatooProtocolConfig config = getProtocolConfig(surveyId.protocolId) + config.surveyId = surveyId + ActivityForm form = ActivityForm.findByExternalId(surveyId.protocolId) + // add plot data to survey observations + addPlotDataToObservations(surveyDataAndObservations, config) + rearrangeSurveyData(surveyDataAndObservations, surveyDataAndObservations, form.sections[0].template.relationships.ecodata, form.sections[0].template.relationships.apiOutput) + // transform data to make it compatible with data model + surveyDataAndObservations = recursivelyTransformData(form.sections[0].template.dataModel, surveyDataAndObservations, form.name, 1, config) + // If we are unable to create a site, null will be returned - assigning a null siteId is valid. + + if (!dataSet.siteId) { + dataSet.siteId = createSiteFromSurveyData(surveyDataAndObservations, collection, surveyId, project.project, config, form) + } - result - } + // plot layout is of type geoMap. Therefore, expects a site id. + if (surveyDataAndObservations.containsKey(PARATOO_DATAMODEL_PLOT_LAYOUT) && dataSet.siteId) { + surveyDataAndObservations[PARATOO_DATAMODEL_PLOT_LAYOUT] = dataSet.siteId + } + + // Delete previously created activity so that duplicate species records are not created. + // Updating existing activity will also create duplicates since it relies on outputSpeciesId to determine + // if a record is new and new ones are created by code. + if (dataSet.activityId) { + activityService.delete(dataSet.activityId, true) + } - private void createActivityFromSurveyData(ParatooSurveyId paratooSurveyId, String mintedCollectionId, Map surveyData, ParatooProtocolConfig config, ParatooProject project) { - ActivityForm form = ActivityForm.findByExternalId(paratooSurveyId.protocol.id) - if (!form) { - log.error("No activity form found for protocol: "+paratooSurveyId.protocol.id) + String activityId = createActivityFromSurveyData(form, surveyDataAndObservations, surveyId, dataSet.siteId, userId) + List records = recordService.getAllByActivity(activityId) + dataSet.areSpeciesRecorded = records?.size() > 0 + dataSet.activityId = activityId + + dataSet.startDate = config.getStartDate(surveyDataAndObservations) + dataSet.endDate = config.getEndDate(surveyDataAndObservations) + dataSet.format = DATASET_DATABASE_TABLE + dataSet.sizeUnknown = true + + projectService.update([custom: project.project.custom], project.id, false) + } } - else { - Map activity = mapActivity(mintedCollectionId, paratooSurveyId, surveyData, form, config, project) - activityService.create(activity) + } + + /** + * Rearrange survey data to match the data model. + * e.g. [a: [b: [c: 1, d: 2], d: 1], b: [c: 1, d: 2]] => [b: [c: 1, d: 2, a: [d: 1]]] + * where relationship [b: [a: [:]]] + * @param properties + * @param rootProperties + * @param relationship + * @param nodesToRemove + * @param ancestors + * @param isChild + * @return + */ + Map rearrangeSurveyData (Map properties, Map rootProperties, Map relationship, Map apiOutputRelationship, List nodesToRemove = [], List ancestors = [] , Boolean isChild = false) { + if (relationship instanceof Map) { + relationship.each { String nodeName, Map children -> + ancestors.add(nodeName) + def nodeObject = rootProperties[nodeName] + if (nodeObject != null) { + properties[nodeName] = nodeObject + // don't add root properties to remove list + if (properties != rootProperties) { + nodesToRemove.add(nodeName) + } + } + else { + def result = findObservationDataFromAPIOutput(nodeName, apiOutputRelationship, rootProperties) + if (result.data instanceof List && (result.data.size() > 0)) { + nodeObject = result.data.first() + } + + if (nodeObject != null) { + properties[nodeName] = nodeObject + nodesToRemove.add(result.path) + } + } + + + if (children) { + if (nodeObject instanceof Map) { + rearrangeSurveyData(nodeObject, rootProperties, children, apiOutputRelationship, nodesToRemove, ancestors, true ) + } + else if (nodeObject instanceof List) { + nodeObject.each { Map node -> + rearrangeSurveyData(node, rootProperties, children, apiOutputRelationship, nodesToRemove, ancestors, true ) + } + } + } + ancestors.removeLast() + } } - } + // remove nodes that have been rearranged. removing during iteration will cause exception. + if (!isChild) { + // sort based on depth of nesting so that child nodes are removed first before ancestors. + nodesToRemove = nodesToRemove.sort { a, b -> b.split('\\.').size() <=> a.split('\\.').size() } + nodesToRemove.each { String path -> + removeProperty(properties, path) + } - private static Map mapActivity(String mintedCollectionId, ParatooSurveyId surveyId, Map surveyData, ActivityForm activityForm, ParatooProtocolConfig config, ParatooProject project) { - Map activity = [:] - activity.projectId = project.id - activity.startDate = config.getStartDate(surveyData) - activity.endDate = config.getEndDate(surveyData) - activity.type = activityForm.name - activity.description = activityForm.name + " - " + surveyId.timeAsDisplayDate() + nodesToRemove.clear() + } - Map output = [ - name: 'Unstructured', - data: surveyData - ] - activity.outputs = [output] - activity.externalIds = [new ExternalId(idType:ExternalId.IdType.MONITOR_MINTED_COLLECTION_ID, externalId: mintedCollectionId)] + properties + } - activity + /** + * Extract plot selection, layout and visit data from survey and copy it to the observations + * @param surveyData + * @param surveyDataAndObservations + * @param config + */ + static void addPlotDataToObservations(Map surveyDataAndObservations, ParatooProtocolConfig config) { + if (surveyDataAndObservations && config.usesPlotLayout) { + Map plotSelection = config.getPlotSelection(surveyDataAndObservations) + Map plotLayout = config.getPlotLayout(surveyDataAndObservations) + Map plotVisit = config.getPlotVisit(surveyDataAndObservations) + + if (plotSelection) { + surveyDataAndObservations[PARATOO_DATAMODEL_PLOT_SELECTION] = plotSelection + surveyDataAndObservations[PARATOO_DATAMODEL_PLOT_LAYOUT] = plotLayout + surveyDataAndObservations[PARATOO_DATAMODEL_PLOT_VISIT] = plotVisit + } + } } - private ParatooProtocolConfig getProtocolConfig(String protocolId) { + /** + * Get the protocol config for the given protocol id. + * @param protocolId + * @return + */ + ParatooProtocolConfig getProtocolConfig(String protocolId) { String result = settingService.getSetting(PARATOO_PROTOCOL_DATA_MAPPING_KEY) Map protocolDataConfig = JSON.parse(result ?: '{}') Map config = protocolDataConfig[protocolId] @@ -255,46 +414,175 @@ class ParatooService { private boolean protocolCheck(String userId, String projectId, String protocolId, boolean read) { List projects = userProjects(userId) - ParatooProject project = projects.find{it.id == projectId} - boolean protocol = project?.protocols?.find{it.externalIds.find{it.externalId == protocolId}} + ParatooProject project = projects.find { it.id == projectId } + boolean protocol = project?.protocols?.find { it.externalIds.find { it.externalId == protocolId } } int minimumAccess = read ? AccessLevel.projectParticipant.code : AccessLevel.editor.code protocol && project.accessLevel.code >= minimumAccess } - Map findDataSet(String userId, String collectionId) { + Map findDataSet(String userId, String orgMintedUUID) { List projects = findUserProjects(userId) Map dataSet = null ParatooProject project = projects?.find { - dataSet = it.dataSets?.find { it.orgMintedIdentifier == collectionId } + dataSet = it.dataSets?.find { it.dataSetId == orgMintedUUID } dataSet } - [dataSet:dataSet, project:project] + [dataSet: dataSet, project: project] + } + + /** + * Create an activity from survey data. + * @param activityForm + * @param surveyObservations + * @param collection + * @param siteId + * @return + */ + private String createActivityFromSurveyData(ActivityForm activityForm, Map surveyObservations, ParatooCollectionId collection, String siteId, String userId) { + Map activityProps = [ + type : activityForm.name, + formVersion : activityForm.formVersion, + description : "Activity submitted by monitor", + projectId : collection.projectId, + publicationStatus: "published", + siteId : siteId, + userId : userId, + outputs : [[ + data: surveyObservations, + name: activityForm.name + ]] + ] + + Map result = activityService.create(activityProps) + result.activityId + } + + /** + * Converts species, feature, document, image and list to appropriate formats. + * @param dataModel + * @param output + * @param path + * @return + */ + def recursivelyTransformData(List dataModel, Map output, String formName = "", int featureId = 1, ParatooProtocolConfig config = null) { + dataModel?.each { Map model -> + switch (model.dataType) { + case "list": + String updatedPath = model.name + def rows =[] + try { + rows = getProperty(output, updatedPath, true, false) + } + catch (Exception e) { + log.info("Error getting list for ${model.name}: ${e.message}") + } + + if (rows instanceof Map) { + output[updatedPath] = rows = [rows] + } + + rows?.each { row -> + if (row != null) { + recursivelyTransformData(model.columns, row, formName, featureId, config) + } + } + break + case "species": + String speciesName + try { + if(model.containsKey(PARATOO_LUT_REF)) { + speciesName = getProperty(output, model.name)?.label?.first() + } else { + speciesName = getProperty(output, model.name)?.first() + } + + output[model.name] = transformSpeciesName(speciesName) + } catch (Exception e) { + log.info("Error getting species name for ${model.name}: ${e.message}") + } + break + case "feature": + // used by protocols like bird survey where a point represents a sight a bird has been observed in a + // bird survey plot + def location = output[model.name] + if (location instanceof Map) { + output[model.name] = [ + type : 'Feature', + geometry : [ + type : 'Point', + coordinates: [location.lng, location.lat] + ], + properties: [ + name : "Point ${formName}-${featureId}", + externalId: location.id, + id: "${formName}-${featureId}" + ] + ] + } + else if (location instanceof List) { + String name + switch (config?.geometryType) { + case "LineString": + name = "LineString ${formName}-${featureId}" + output[model.name] = ParatooProtocolConfig.createLineStringFeatureFromGeoJSON (location, name, null, name) + break + default: + name = "Polygon ${formName}-${featureId}" + output[model.name] = ParatooProtocolConfig.createFeatureFromGeoJSON (location, name, null, name) + break + } + } + + featureId ++ + break + case "image": + case "document": + // backup a copy of multimedia to another attribute and remove it from existing attribute since it interferes with existing logic + String backupAttributeName = "${model.name}_backup" + output[backupAttributeName] = output[model.name] + output.remove(model.name) + break + } + } + + output } - private String createSiteFromSurveyData(Map surveyData, ParatooCollection collection, ParatooSurveyId surveyId, Project project, ParatooProtocolConfig config) { + private String createSiteFromSurveyData(Map observation, ParatooCollection collection, ParatooCollectionId surveyId, Project project, ParatooProtocolConfig config, ActivityForm form) { String siteId = null // Create a site representing the location of the collection - Map geoJson = config.getGeoJson(surveyData) + Map geoJson = config.getGeoJson(observation, form) if (geoJson) { Map siteProps = siteService.propertiesFromGeoJson(geoJson, 'upload') + List features = geoJson?.features ?: [] + geoJson.remove('features') + siteProps.features = features siteProps.type = Site.TYPE_SURVEY_AREA siteProps.publicationStatus = PublicationStatus.PUBLISHED siteProps.projects = [project.projectId] String externalId = geoJson.properties?.externalId if (externalId) { - siteProps.externalIds = [new ExternalId(idType:ExternalId.IdType.MONITOR_PLOT_GUID, externalId: externalId)] + siteProps.externalIds = [new ExternalId(idType: ExternalId.IdType.MONITOR_PLOT_GUID, externalId: externalId)] } - Site site = Site.findByExternalId(ExternalId.IdType.MONITOR_PLOT_GUID, externalId) + Site site + // create new site for every non-plot submission + if (config.usesPlotLayout) { + site = Site.findByExternalId(ExternalId.IdType.MONITOR_PLOT_GUID, externalId) + if (site?.features) { + siteProps.features?.addAll(site.features) + } + } + Map result if (!site) { result = siteService.create(siteProps) + } else { + result = [siteId: site.siteId] } - else { - result = [siteId:site.siteId] - } - if (result.error) { // Don't treat this as a fatal error for the purposes of responding to the paratoo request - log.error("Error creating a site for survey "+collection.orgMintedIdentifier+", project "+project.projectId+": "+result.error) + if (result.error) { + // Don't treat this as a fatal error for the purposes of responding to the paratoo request + log.error("Error creating a site for survey " + collection.orgMintedUUID + ", project " + project.projectId + ": " + result.error) } siteId = result.siteId } @@ -302,52 +590,68 @@ class ParatooService { } private Map syncParatooProtocols(List protocols) { - - Map result = [errors:[], messages:[]] + Map result = [errors: [], messages: []] List guids = [] protocols.each { Map protocol -> + String message String id = protocol.id String guid = protocol.attributes.identifier guids << guid String name = protocol.attributes.name - ParatooProtocolConfig protocolConfig = getProtocolConfig(guid) - ActivityForm form = ActivityForm.findByExternalId(guid) - if (!form) { - form = new ActivityForm() - form.externalIds = [] - form.externalIds << new ExternalId(idType: ExternalId.IdType.MONITOR_PROTOCOL_INTERNAL_ID, externalId: id) - form.externalIds << new ExternalId(idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID, externalId: guid) - - String message = "Creating form with id: "+id+", name: "+name - result.messages << message - log.info message - } - else { - ExternalId paratooInternalId = form.externalIds.find{it.idType == ExternalId.IdType.MONITOR_PROTOCOL_INTERNAL_ID} - - // Paratoo internal protocol ids are not stable so if we match the guid, we may need to update - // the id as that is used in other API methods. - if (paratooInternalId) { - String message = "Updating form with id: "+paratooInternalId.externalId+", guid: "+guid+", name: "+name+", new id: "+id - paratooInternalId.externalId = id + if (guid) { + ParatooProtocolConfig protocolConfig = getProtocolConfig(guid) + ActivityForm form = ActivityForm.findByExternalId(guid) + if (!form) { + form = new ActivityForm() + form.externalIds = [] + form.externalIds << new ExternalId(idType: ExternalId.IdType.MONITOR_PROTOCOL_INTERNAL_ID, externalId: id) + form.externalIds << new ExternalId(idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID, externalId: guid) + + message = "Creating form with id: " + id + ", name: " + name result.messages << message log.info message + } else { + ExternalId paratooInternalId = form.externalIds.find { it.idType == ExternalId.IdType.MONITOR_PROTOCOL_INTERNAL_ID } + + // Paratoo internal protocol ids are not stable so if we match the guid, we may need to update + // the id as that is used in other API methods. + if (paratooInternalId) { + message = "Updating form with id: " + paratooInternalId.externalId + ", guid: " + guid + ", name: " + name + ", new id: " + id + paratooInternalId.externalId = id + result.messages << message + log.info message + } else { + String error = "Error: Missing internal id for form with id: " + id + ", name: " + name + result.errors << error + log.error error + } + } - else { - String error = "Error: Missing internal id for form with id: "+id+", name: "+name + + try { + mapProtocolToActivityForm(protocol, form, protocolConfig) + form.save() + + if (form.hasErrors()) { + result.errors << form.errors + log.warn "Error saving form with id: " + id + ", name: " + name + } + } + catch (NotFoundException e) { + String error = "Error: No protocol definition found in swagger documentation for protocol: " + name + result.errors << error + log.error error + } + catch (Exception e) { + String error = "Error: Unable to save form for protocol: " + name result.errors << error log.error error } - } - - List tags = protocolConfig?.tags ?: [ActivityForm.SURVEY_TAG] - mapProtocolToActivityForm(protocol, form, tags) - form.save() - - if (form.hasErrors()) { - result.errors << form.errors - log.warn "Error saving form with id: "+id+", name: "+name + } else { + String error = "Error: No valid guid found for protocol: " + name + result.errors << error + log.error error } } @@ -358,10 +662,12 @@ class ParatooService { status != Status.DELETED } - List deletions = allProtocolForms.findAll{it.externalIds.find{it.idType == ExternalId.IdType.MONITOR_PROTOCOL_GUID && !(it.externalId in guids)}} + List deletions = allProtocolForms.findAll { it.externalIds.find { it.idType == ExternalId.IdType.MONITOR_PROTOCOL_GUID && !(it.externalId in guids) } } deletions.each { ActivityForm activityForm -> - result.messages << "Form ${activityForm.name} with guid: ${activityForm.externalIds.find{it.idType == ExternalId.IdType.MONITOR_PROTOCOL_GUID}.externalId} has been deleted" + result.messages << "Form ${activityForm.name} with guid: ${activityForm.externalIds.find { it.idType == ExternalId.IdType.MONITOR_PROTOCOL_GUID }.externalId} has been deleted" } + + log.debug("Completed syncing paratoo protocols") result } @@ -376,19 +682,37 @@ class ParatooService { grailsApplication.config.getProperty('paratoo.core.baseUrl') } + private String getDocumentationEndpoint() { + grailsApplication.config.getProperty('paratoo.core.documentationUrl') + } - Map syncProtocolsFromParatoo() { - String url = paratooBaseUrl+PARATOO_PROTOCOL_PATH + List getProtocolsFromParatoo() { + (List) cacheService.get("paratoo-protocols", { -> + String url = paratooBaseUrl + PARATOO_PROTOCOL_PATH + Map authHeader = getAuthHeader() + webService.getJson(url, null, authHeader, false)?.data + }) + } + + Map getAuthHeader() { String accessToken = tokenService.getAuthToken(true) if (!accessToken?.startsWith('Bearer')) { - accessToken = 'Bearer '+accessToken + accessToken = 'Bearer ' + accessToken } - Map authHeader = [(MONITOR_AUTH_HEADER):accessToken] - Map response = webService.getJson(url, null, authHeader, false) - syncParatooProtocols(response?.data) + + if (!accessToken) { + throw new RuntimeException("Unable to get access token") + } + + [(MONITOR_AUTH_HEADER): accessToken] + } + + Map syncProtocolsFromParatoo() { + List protocols = getProtocolsFromParatoo() + syncParatooProtocols(protocols) } - private static void mapProtocolToActivityForm(Map protocol, ActivityForm form, List tags) { + private void mapProtocolToActivityForm(Map protocol, ActivityForm form, ParatooProtocolConfig config) { form.name = protocol.attributes.name form.formVersion = protocol.attributes.version form.type = PARATOO_PROTOCOL_FORM_TYPE @@ -396,90 +720,80 @@ class ParatooService { form.external = true form.publicationStatus = PublicationStatus.PUBLISHED form.description = protocol.attributes.description - form.tags = tags + form.tags = config.tags form.externalIds + form.sections = [getFormSectionForProtocol(protocol, config)] } - private ParatooProject mapProject(Project project, AccessLevel accessLevel, List sites) { - Site projectArea = sites.find{it.type == Site.TYPE_PROJECT_AREA} + ParatooProject mapProject(Project project, AccessLevel accessLevel, List sites) { + Site projectArea = sites.find { it.type == Site.TYPE_PROJECT_AREA } Map projectAreaGeoJson = null if (projectArea) { projectAreaGeoJson = siteService.geometryAsGeoJson(projectArea) } - List plotSelections = sites.findAll{it.type == Site.TYPE_SURVEY_AREA} + // Monitor has users selecting a point as an approximate survey location then + // laying out the plot using GPS when at the site. We only want to return the approximate planning + // sites from this call + List plotSelections = sites.findAll{it.type == Site.TYPE_SURVEY_AREA && it.extent?.geometry?.type == 'Point'} Map attributes = [ id:project.projectId, name:project.name, + grantID:project.grantId, accessLevel: accessLevel, project:project, projectArea: projectAreaGeoJson, projectAreaSite: projectArea, - plots: plotSelections] + plots : plotSelections] new ParatooProject(attributes) } - private static Map mapParatooCollectionId(ParatooCollectionId collectionId, Project project) { + private static Map mapParatooCollectionId(ParatooCollectionId paratooCollectionId, Project project) { Map dataSet = [:] - dataSet.dataSetId = Identifiers.getNew(true, '') - dataSet.surveyId = collectionId.surveyId.toMap() // No codec to save this to mongo - dataSet.protocol = collectionId.surveyId.protocol.id + dataSet.dataSetId = paratooCollectionId.survey_metadata.orgMintedUUID + dataSet.protocol = paratooCollectionId.protocolId dataSet.grantId = project.grantId dataSet.collectionApp = PARATOO_APP_NAME dataSet.dateCreated = DateUtil.format(new Date()) dataSet.lastUpdated = DateUtil.format(new Date()) + + dataSet.progress = Activity.PLANNED + String dataSetName = buildName( + paratooCollectionId.protocolId, + DateUtil.formatAsDisplayDateTime(paratooCollectionId.eventTime), project) + dataSet.name = dataSetName + dataSet } - private static String buildSurveyQueryString(int start, int limit) { - "?populate=deep&sort=updatedAt&pagination[start]=$start&pagination[limit]=$limit" + private static String buildSurveyQueryString(int start, int limit, String createdAt) { + "?populate=deep&sort=updatedAt&pagination[start]=$start&pagination[limit]=$limit&filters[createdAt][\$eq]=$createdAt" } - Map retrieveSurveyData(ParatooSurveyId surveyId, ParatooProtocolConfig config) { - - String apiEndpoint = config.getApiEndpoint(surveyId) - - String accessToken = tokenService.getAuthToken(true) - if (!accessToken?.startsWith('Bearer')) { - accessToken = 'Bearer '+accessToken - } - Map authHeader = [(MONITOR_AUTH_HEADER):accessToken] - - if (!accessToken) { - throw new RuntimeException("Unable to get access token") - } - int start = 0 - int limit = 10 - - - String url = paratooBaseUrl+'/'+apiEndpoint - String query = buildSurveyQueryString(start, limit) - Map response = webService.getJson(url+query, null, authHeader, false) - Map survey = findMatchingSurvey(surveyId, response.data, config) - int total = response.meta?.pagination?.total ?: 0 - while (!survey && start+limit < total) { - start += limit + Map retrieveSurveyAndObservations(ParatooCollection collection, Map authHeader = null) { + String apiEndpoint = PARATOO_DATA_PATH + Map payload = [ + org_minted_uuid: collection.orgMintedUUID + ] - query = buildSurveyQueryString(start, limit) - response = webService.getJson(url+query, null, authHeader, false) - survey = findMatchingSurvey(surveyId, response.data, config) + if (!authHeader) { + authHeader = getAuthHeader() } - survey - } + String url = paratooBaseUrl + apiEndpoint + Map response = webService.doPost(url, payload, false, authHeader) + log.debug((response as JSON).toString()) - - private static Map findMatchingSurvey(ParatooSurveyId surveyId, List data, ParatooProtocolConfig config) { - data?.find { config.matches(it, surveyId) } + response?.resp } Map addOrUpdatePlotSelections(String userId, ParatooPlotSelectionData plotSelectionData) { List projects = userProjects(userId) if (!projects) { - return [error:'User has no projects eligible for Monitor site data'] + return [error: 'User has no projects eligible for Monitor site data'] } Map siteData = mapPlotSelection(plotSelectionData) @@ -490,8 +804,7 @@ class ParatooService { Map result if (site) { result = siteService.update(siteData, site.siteId) - } - else { + } else { result = siteService.create(siteData) } @@ -501,10 +814,12 @@ class ParatooService { private static Map mapPlotSelection(ParatooPlotSelectionData plotSelectionData) { Map geoJson = ParatooProtocolConfig.plotSelectionToGeoJson(plotSelectionData) Map site = SiteService.propertiesFromGeoJson(geoJson, 'point') - site.projects = [] // get all projects for the user I suppose - not sure why this isn't in the payload as it's in the UI... + site.projects = [] + // get all projects for the user I suppose - not sure why this isn't in the payload as it's in the UI... site.type = Site.TYPE_SURVEY_AREA - site.externalIds = [new ExternalId(idType:ExternalId.IdType.MONITOR_PLOT_GUID, externalId:geoJson.properties.externalId)] - site.publicationStatus = PublicationStatus.PUBLISHED // Mark the plot as read only as it is managed by the Monitor app + site.externalIds = [new ExternalId(idType: ExternalId.IdType.MONITOR_PLOT_GUID, externalId: geoJson.properties.externalId)] + site.publicationStatus = PublicationStatus.PUBLISHED + // Mark the plot as read only as it is managed by the Monitor app site } @@ -512,7 +827,7 @@ class ParatooService { Map updateProjectSites(ParatooProject project, Map siteData, List userProjects) { if (siteData.plot_selections) { List siteExternalIds = siteData.plot_selections - siteExternalIds = siteExternalIds.findAll{it} // Remove null / empty ids + siteExternalIds = siteExternalIds.findAll { it } // Remove null / empty ids if (siteExternalIds) { linkProjectToSites(project, siteExternalIds, userProjects) } @@ -536,7 +851,7 @@ class ParatooService { // Validate that the user has permission to link the site to the project by checking // if the user has permission on any other projects this site is linked to. if (site.projects) { - if (!userProjects.collect{it.id}.containsAll(site.projects)) { + if (!userProjects.collect { it.id }.containsAll(site.projects)) { errors << "User does not have permission to link site ${site.externalId} to project ${project.id}" return } @@ -547,12 +862,11 @@ class ParatooService { errors << site.errors } } - } - else { + } else { errors << "No site exists with externalId = ${siteExternalId}" } } - [success:!errors, error:errors] + [success: !errors, error: errors] } private Map updateProjectArea(ParatooProject project, String type, List coordinates) { @@ -562,11 +876,10 @@ class ParatooService { projectArea.extent.geometry.type = geometry.type projectArea.extent.geometry.coordinates = geometry.coordinates siteService.update(projectArea.extent, projectArea.siteId) - } - else { + } else { Map site = [ - name:'Monitor project area', + name:'Monitor Project Extent', type:Site.TYPE_PROJECT_AREA, extent: [ source:'drawn', @@ -577,4 +890,1144 @@ class ParatooService { siteService.create(site) } } + + Map getParatooSwaggerDocumentation() { + (Map) cacheService.get('paratoo-swagger-documentation', { + String url = "${getParatooBaseUrl()}${getDocumentationEndpoint()}" + webService.getJson(url, null, null, false) + }) + } + + FormSection getFormSectionForProtocol(Map protocol, ParatooProtocolConfig config) { + Map documentation = getParatooSwaggerDocumentation() + Map template = buildTemplateForProtocol(protocol, documentation, config) + + new FormSection(template: template, templateName: template.modelName, name: protocol.attributes.name) + } + + Map buildPathToModel(Map properties) { + Map relationship = buildChildParentRelationship(properties) + buildTreeFrom2DRelationship(relationship) + } + + def getModelStructureFromDefinition(def definition) { + if (definition instanceof Map) + definition?.properties?.data?.properties ?: definition?.items?.properties?.data?.properties ?: definition?.items?.properties ?: definition?.properties ?: definition?.items + } + + def getRequiredModels(Map properties) { + properties.properties?.data?.required ?: properties.items?.properties?.data?.required ?: properties.items?.required ?: properties.properties?.required ?: properties?.required ?: [] + } + + Map simplifyModelStructure(Map definition) { + Map simplifiedDefinition = [:] + if ((definition.type == PARATOO_TYPE_ARRAY) && definition.items) { + definition << definition.items + } + + Map properties = getModelStructureFromDefinition(definition) + List required = getRequiredModels(definition) + String componentName = definition[PARATOO_COMPONENT] + if (properties) { + simplifiedDefinition.type = definition.type ?: "object" + simplifiedDefinition.properties = properties + } else { + simplifiedDefinition << definition + } + + if (required) { + simplifiedDefinition.required = required + } + + if (componentName) { + simplifiedDefinition[PARATOO_COMPONENT] = componentName + } + + simplifiedDefinition + } + + def cleanSwaggerDefinition(def definition) { + def cleanedDefinition + if (definition instanceof Map) { + cleanedDefinition = simplifyModelStructure(definition) + if (cleanedDefinition.properties) { + cleanedDefinition.properties?.each { String name, def value -> + cleanedDefinition.properties[name] = cleanSwaggerDefinition(value) + } + } else { + cleanedDefinition?.each { String name, def value -> + def cleanedValue = value instanceof Map ? simplifyModelStructure(value) : value + cleanedDefinition[name] = cleanSwaggerDefinition(cleanedValue) + } + } + } else if (definition instanceof List) { + cleanedDefinition = [] + definition.each { def value -> + cleanedDefinition << cleanSwaggerDefinition(value) + } + } else { + try { + cleanedDefinition = definition?.clone() + } + catch (CloneNotSupportedException e) { + // if not cloneable, then it is a primitive type + cleanedDefinition = definition + } + } + + cleanedDefinition + } + + Map buildChildParentRelationship(def definition, ArrayDeque currentNodes = null, Map relationship = null) { + currentNodes = currentNodes ?: new ArrayDeque() + relationship = relationship != null ? relationship : [:].withDefault { [] } as Map + if (definition instanceof Map) { + // model references are usually has the following representation + // { + // "type": "integer", + // "x-model-ref": "bird-survey" + // } + if (definition[PARATOO_MODEL_REF] && (definition["type"] == "integer")) { + String modelName = definition[PARATOO_MODEL_REF] + String parentNode = currentNodes.first() + String grandParentNode = currentNodes.size() > 1 ? currentNodes.getAt(1) : null + if (!PARATOO_IGNORE_X_MODEL_REF_LIST_MINIMUM.contains(modelName)) { + // no circular references + List nodes = [grandParentNode, parentNode, modelName].unique() + if ((nodes.size() == 3) && + !currentNodes.contains(modelName) && + !(relationship.containsKey(grandParentNode) && relationship[grandParentNode].contains(modelName))) { + String nodeListToParent = parentNode ? currentNodes.toList().reverse().join(".") : null + // make sure there are no duplicate child entries + if (nodeListToParent && (!relationship.containsKey(nodeListToParent) + || !relationship[nodeListToParent].contains(modelName))) { + relationship[nodeListToParent].add(modelName) + } + } + } + } else if (definition.properties && definition.properties[PARATOO_MODEL_REF]) { + // For representation like + // { + // "quad" : { + // "type" : "array", + // "properties" : { + // "type" : "integer" + // "x-model-ref" : "quadrant" + // } + // } + // } + buildChildParentRelationship(definition.properties, currentNodes, relationship) + } else { + if (definition instanceof Map) { + definition?.each { String name, model -> + currentNodes.push(name) + buildChildParentRelationship(model, currentNodes, relationship) + currentNodes.pop() + } + } + } + } + + relationship + } + + Map buildTreeRelationshipOfModels(Map properties) { + def twoDRelationships = buildParentChildRelationship(properties) + buildTreeFrom2DRelationship(twoDRelationships) + } + + def buildParentChildRelationship(def definition, ArrayDeque ancestorNodes = null, Map relationships = null) { + ancestorNodes = ancestorNodes ?: new ArrayDeque() + relationships = relationships != null ? relationships : [:].withDefault { [] } + if (definition instanceof Map) { + // model references are usually has the following representation + // { + // "type": "integer", + // "x-model-ref": "bird-survey" + // } + if (definition[PARATOO_MODEL_REF] && (definition["type"] == "integer")) { + String modelName = definition[PARATOO_MODEL_REF] + String currentNode = ancestorNodes.size() ? ancestorNodes.last() : name + if (!PARATOO_IGNORE_X_MODEL_REF_LIST.contains(modelName)) { + if (!relationships.containsKey(modelName) || !relationships[modelName].contains(currentNode)) { + // prevent circular references + if (modelName != currentNode + && !ancestorNodes.contains(modelName) + && (!relationships.containsKey(currentNode) || !relationships[currentNode].contains(modelName))) { + relationships[modelName].add(currentNode) + } + } + } + } else if (definition.properties && definition.properties[PARATOO_MODEL_REF]) { + // For representation like + // { + // "quad" : { + // "type" : "array", + // "properties" : { + // "type" : "integer" + // "x-model-ref" : "quadrant" + // } + // } + // } + buildParentChildRelationship(definition.properties, ancestorNodes, relationships) + } else { + def iteratingObject = definition.properties ?: definition + if (iteratingObject instanceof Map) { + iteratingObject?.each { String modelName, model -> + ancestorNodes.push(modelName) + buildParentChildRelationship(model, ancestorNodes, relationships) + ancestorNodes.pop() + } + } + } + } + + relationships + } + + /** + * Builds a tree representation from a list of two dimensional relationships. + * @param twoDRelationships + * e.g. + * [ + * "bird-survey": ["bird-observation"], + * "fauna-survey": ["fauna-observation"], + * "plot-visit": ["bird-survey", "fauna-survey"] + * ] + * @return + */ + Map buildTreeFrom2DRelationship(Map twoDRelationships) { + Map treeRepresentation = [:] + + twoDRelationships.each { String name, List children -> + treeRepresentation[name] = addChildrenToNode(children) + } + + List nodesToRemove = iterateTreeAndAddChildren(treeRepresentation) + + nodesToRemove?.each { + treeRepresentation.remove(it) + } + + treeRepresentation + } + + List iterateTreeAndAddChildren(Map treeRepresentation, Map root = null, HashSet nodesToRemove = null, depth = 0, ArrayDeque visited = null) { + nodesToRemove = nodesToRemove ?: new HashSet() + root = root ?: treeRepresentation + visited = visited ?: new ArrayDeque() + treeRepresentation.each { String parent, Map children -> + visited.push(parent) + children?.each { String child, Map grandChildren -> + if (!visited.contains(child)) { + visited.push(child) + if (root.containsKey(child)) { + grandChildren << deepCopy(root[child]) + nodesToRemove.add(child) + } + + iterateTreeAndAddChildren(children, root, nodesToRemove, depth + 1, visited) + visited.pop() + } + } + visited.pop() + } + + nodesToRemove.toList() + } + + Map addChildrenToNode(List children) { + Map childrenNode = [:] + children?.each { String child -> + if (!childrenNode.containsKey(child)) + childrenNode.put(child, [:]) + } + + childrenNode + } + + static def deepCopy(def original) { + def copy + + if (original instanceof Map) { + copy = [:] + original.each { String key, Object value -> + copy.put(key, deepCopy(value)) + } + } else if (original instanceof List) { + copy = original.collect { deepCopy(it) } + } else { + copy = original + } + + copy + } + + Map buildTemplateForProtocol(Map protocol, Map documentation, ParatooProtocolConfig config) { + ArrayDeque modelVisitStack = new ArrayDeque<>() + documentation = deepCopy(documentation) + Map components = deepCopy(getComponents(documentation)) + + Map template = [dataModel: [], viewModel: [], modelName: capitalizeModelName(protocol.attributes.name), record: true, relationships: [ecodata: [:], apiOutput: [:]]] + Map properties = deepCopy(findProtocolEndpointDefinition(protocol, documentation)) + if (properties == null) { + throw new NotFoundException("No protocol endpoint found for ${protocol.attributes.endpointPrefix}/bulk") + } + + resolveReferences(properties, components) + Map cleanedProperties = cleanSwaggerDefinition(properties) + cleanedProperties = deepCopy(cleanedProperties) +// rearrange models not working for protocols multiple relationship between models. Disabling it for now. +// template.relationships.ecodata = buildTreeRelationshipOfModels(cleanedProperties) +// template.relationships.apiOutput = buildPathToModel(cleanedProperties) +// println((template.relationships.apiOutput as JSON).toString(true)) +// println((template.relationships.ecodata as JSON).toString(true)) + resolveModelReferences(cleanedProperties, components) +// cleanedProperties = rearrangePropertiesAccordingToModelRelationship(cleanedProperties, template.relationships.apiOutput, template.relationships.ecodata) + cleanedProperties = deepCopy(cleanedProperties) + log.debug((properties as JSON).toString()) + + if (isPlotLayoutNeededByProtocol(protocol)) { + template.dataModel.addAll(grailsApplication.config.getProperty("paratoo.defaultPlotLayoutDataModels", List)) + template.viewModel.addAll(grailsApplication.config.getProperty("paratoo.defaultPlotLayoutViewModels", List)) + } + cleanedProperties.each { String name, def definition -> + if (definition instanceof Map) { + modelVisitStack.push(name) + Map result = convertToDataModelAndViewModel(definition, documentation, name, null, modelVisitStack, 0, name, config) + modelVisitStack.pop() + if (result) { + template.dataModel.addAll(result.dataModel) + template.viewModel.addAll(result.viewModel) + } + } + } + + template + } + + def resolveModelReferences(def model, Map components, ArrayDeque modelVisitStack = null) { + modelVisitStack = modelVisitStack ?: new ArrayDeque() + boolean modelNameStacked = false + if (model instanceof Map) { + if (model.containsKey(PARATOO_MODEL_REF)) { + String modelName = model[PARATOO_MODEL_REF] + String componentName = getSchemaNameFromModelName(modelName) + Map referencedComponent = components[componentName] + + if (referencedComponent) { + if (modelVisitStack.contains(modelName)) + log.error("Circular dependency - ignoring model resolution ${modelName}") + else { + modelNameStacked = true + modelVisitStack.push(modelName) + model << cleanSwaggerDefinition(components[componentName]) + } + } + } + + model.each { String modelName, def value -> + resolveModelReferences(value, components, modelVisitStack) + } + } else if (model instanceof List) { + model.each { def value -> + resolveModelReferences(value, components, modelVisitStack) + } + } + + if (modelNameStacked) { + modelVisitStack.pop() + } + + model + } + + List findPathFromRelationship(String nameToFind, def relationship, List ancestors = null) { + ancestors = ancestors ?: [] + List results = [] + relationship?.each { String path, def models -> + if (path == nameToFind) { + results << ancestors.join('.') + } else if (models instanceof Map) { + ancestors.add(path) + results.addAll(findPathFromRelationship(nameToFind, models, ancestors)) + ancestors.removeLast() + } + } + + results + } + + /** + * Check if protocol requires a plot + * @param protocol + * @return boolean + */ + boolean isPlotLayoutNeededByProtocol(Map protocol) { + List modelNames = protocol.attributes.workflow?.collect { it.modelName } + modelNames.contains(PARATOO_WORKFLOW_PLOT_LAYOUT) + } + + /** + * Find the models associated with a protocol. Endpoint is of the format Protocol's END_POINT_PREFIX/bulk. + * + * @param protocol + * @param documentation + * @return + */ + Map findProtocolEndpointDefinition (Map protocol, Map documentation) { + (Map) documentation.paths.findResult { String pathName, Map path -> + if (pathName == "${protocol.attributes.endpointPrefix}/bulk") { + return path.post.requestBody.content['application/json'].schema.properties + .data.properties.collections.items.properties + } + } + } + + Map getComponents(Map documentation) { + documentation.components.schemas + } + + Map resolveReferences(Map schema, Map components) { + String componentName + schema.each { String model, def value -> + if (!(value instanceof Map)) + return + + if (value['$ref']) { + componentName = getModelNameFromRef(value['$ref']) + if (components[componentName]) + value.putAll(components[componentName]) + else + log.debug("No component definition found for ${componentName}") + value.remove('$ref') + resolveReferences(value, components) + } else if (value.items) { + resolveReferences(value, components) + } else if (value.properties && value.properties.data && value.properties.data.properties) { + resolveReferences(value.properties.data, components) + } else if (value.anyOf) { + def definition = value.anyOf.find { + it['$ref'] !== null + } + + + value.putAll(definition ?: [:]) + value.remove('anyOf') + resolveReferences(value, components) + } else { + resolveReferences(value, components) + } + + } + + schema + } + + String getModelNameFromRef(String ref) { + ref.replace("#/components/schemas/", "") + } + + Map convertToDataModelAndViewModel(Map component, Map documentation, String name, List required = null, Deque modelVisitStack = null, int depth = 0, String path = "", ParatooProtocolConfig config = null) { + boolean modelNameStacked = false + Map model = [dataModel: [], viewModel: []], dataType, viewModel, template + modelVisitStack = modelVisitStack ?: new ArrayDeque() + String componentName, modelName = component[PARATOO_MODEL_REF] + + /** + * Some time component definition can be like + { + "type": "array", + "items": { + "type": "integer", + "x-paratoo-file-type": ["images"], + "x-model-ref": "file" + } + } + */ + if (!modelName && (component.properties?.getAt(PARATOO_MODEL_REF) == PARATOO_FILE_MODEL_NAME)) { + component = component.properties + modelName = component[PARATOO_MODEL_REF] + } + + if (PARATOO_IGNORE_MODEL_LIST.contains(name)) + return + else if (component[PARATOO_SPECIES_TYPE]) { + component.type = "species" + } else if (modelName == PARATOO_FILE_MODEL_NAME) { + if (component[PARATOO_FILE_TYPE] == [PARATOO_IMAGE_FILE_TYPE]) { + component.type = "image" + } else { + component.type = "document" + } + } + + switch (component.type) { + case "object": + if (isLocationObject(component)) { + // complex object here represents a point with lat,lng attributes + dataType = getFeatureDataType(component, documentation, name) + viewModel = getFeatureViewModel(dataType, component, documentation, name) + } else { + template = getColumns(component, documentation, modelVisitStack, depth, path, config) + dataType = getListDataType(component, documentation, name, template.dataModel, true) + viewModel = getListViewModel(dataType, component, documentation, name, template.viewModel) + } + break + case "array": + template = getColumns(component, documentation, modelVisitStack, depth, path, config) + dataType = getListDataType(component, documentation, name, template.dataModel) + viewModel = getListViewModel(dataType, component, documentation, name, template.viewModel) + break + case "integer": + dataType = getIntegerDataType(component, documentation, name) + viewModel = getIntegerViewModel(dataType, component, documentation, name) + break + case "number": + dataType = getNumberDataType(component, documentation, name) + viewModel = getNumberViewModel(dataType, component, documentation, name) + break + case "string": + dataType = getStringDataType(component, documentation, name) + viewModel = getStringViewModel(dataType, component, documentation, name) + break + case "boolean": + dataType = getBooleanDataType(component, documentation, name) + viewModel = getBooleanViewModel(dataType, component, documentation, name) + break + case "document": + dataType = getDocumentDataType(component, documentation, name) + viewModel = getDocumentViewModel(dataType, component, documentation, name) + break + case "image": + dataType = getImageDataType(component, documentation, name) + viewModel = getImageViewModel(dataType, component, documentation, name) + break + case "species": + dataType = getSpeciesDataType(component, documentation, name) + viewModel = getSpeciesViewModel(dataType, component, documentation, name) + break + default: + log.error("Cannot convert Paratoo component to dataModel - ${component.type}") + break + } + + addOverrides(dataType, viewModel, path, config) + + if (dataType) { + addRequiredFlag(component, dataType, required) + model.dataModel.add(dataType) + } + + if (viewModel) { + model.viewModel.add(viewModel) + } + + if (modelNameStacked) { + modelVisitStack.pop() + } + + model + } + + Map addOverrides(Map dataModel, Map viewModel, String path, ParatooProtocolConfig config) { + Map overrides = config.overrides + if (overrides?.dataModel?.containsKey(path)) { + Map override = overrides?.dataModel[path] + if (override) { + dataModel.putAll(override) + } + } + + if (overrides?.viewModel?.containsKey(path)) { + Map override = overrides?.viewModel[path] + if (override) { + viewModel.putAll(override) + } + } + } + + boolean isLocationObject(Map input) { + ((input[PARATOO_COMPONENT] == PARATOO_LOCATION_COMPONENT) || + input[PARATOO_COMPONENT]?.startsWith(PARATOO_LOCATION_COMPONENT_STARTS_WITH)) && + !grailsApplication.config.getProperty("paratoo.location.excluded", List)?.contains(input[PARATOO_COMPONENT]) + } + + static Map addRequiredFlag(Map component, Map dataType, List required) { + if (required?.contains(component.name)) { + dataType["validate"] = "required" + } + + dataType + } + + Map getStringDataType(Map component, Map documentation, String name) { + Map dataType + switch (component.format) { + case "date-time": + dataType = addUnitsAndDescription([ + "dataType" : "date", + "name" : name, + "dwcAttribute": "eventDate" + ], component) + break + default: + dataType = addUnitsAndDescription([ + "dataType": "text", + "name" : name + ], component) + + if (component[PARATOO_LUT_REF]) { + List items = getLutValues(component[PARATOO_LUT_REF]) + if (items) + dataType << transFormLutValuesToDataModel(items) + else + dataType.constraints = component.enum + dataType[PARATOO_LUT_REF] = component[PARATOO_LUT_REF] + } + break + } + + dataType + } + + Map transFormLutValuesToDataModel(List items) { + Map dataModel = [ + constraints : [ + "textProperty" : "label", + "type" : "literal", + "valueProperty": "value" + ], + "displayOptions": [ + "placeholder": "Select an option", + "tags" : true + ] + ] + + dataModel.constraints.literal = items.collect { Map item -> + [label: item.attributes.label, value: item.attributes.symbol] + } + + dataModel + } + + /** + * Get display names for symbols. Swagger documentation does not have display names but only list of symbols. + * @param lutRef + * @return [ + * [ + * "id": 1, + * "attributes": [ + * "symbol": "E", + * "label": "Estuary", + * "description": "", + * "uri": "", + * "createdAt": "2024-03-05T07:21:46.070Z", + * "updatedAt": "2024-03-12T07:30:12.903Z" + * ] + * ] + * ] + */ + List getLutValues(String lutRef) { + cacheService.get("paratoo-$lutRef", { + String url = "${getParatooBaseUrl()}/${getPluralizedName(lutRef)}" + int start = 0 + int limit = 20 + String query = "?" + buildPaginationQuery(start, limit) + Map authHeader = getAuthHeader() + Map response = webService.getJson(url + query, null, authHeader, false) + List items = response.data ?: [] + if (!response.error) { + int total = response.meta?.pagination?.total ?: 0 + while (items && start + limit < total) { + start += limit + query = "?" + buildPaginationQuery(start, limit) + response = webService.getJson(url + query, null, authHeader, false) + if (!response.error) { + items.addAll(response.data) + } + } + } + + items + }) + } + + String buildPaginationQuery(int start, int limit) { + "pagination[start]=$start&pagination[limit]=$limit" + } + + /** + * Get pluralized name of an enumeration. This is used to get the correct endpoint to get display name of an enumeration. + * @param name + * @return + */ + String getPluralizedName(String name) { + Map documentation = getParatooSwaggerDocumentation() + // strapi uses plural names for endpoints + List suffixes = ["s", "es", "ies"] + String winner = name + suffixes.each { String suffix -> + String tempName + if (suffix.equals("ies") && name.endsWith("y")) { + // Apply special rule for words ending with 'y' + tempName = name.substring(0, name.length() - 1) + "ies" + } else { + tempName = "${name}${suffix}" + } + + // check if the endpoint exists in swagger documentation to decide if the plural value is correct. + if (documentation.paths["/${tempName}"] != null) + winner = tempName + } + + winner + } + + /** + * Get model definitions according to model relationship so that parent will come first and children will come under it. + * If relationship is [b:[a:[:]] then rearranged output will be + * [ type: "object", + * properties: [ + * b: [ + * type: "object", + * properties: [ + * a: [...] + * ] + * ] + * ] + * ] + * @param properties + * @param apiOutput + * @param relationship + * @param newOrder + * @return + */ + Map rearrangePropertiesAccordingToModelRelationship(properties, apiOutput, relationship, Map newOrder = null) { + newOrder = newOrder ?: [type: "object", properties: [:]] + if (relationship instanceof Map) { + relationship.each { String parent, Map children -> + List paths = findPathFromRelationship(parent, apiOutput) + String path = paths.size() ? paths.first() : null + if (path) { + newOrder["type"] = "object" + // get model definition for the parent + def value = [:] + try { + value = getProperty(properties, path)?.first() ?: [:] + // remove parent from children + paths?.each { String propertyPath -> + removeProperty(properties, propertyPath) + } + + value = deepCopy(value) + + } + catch (Exception e) { + log.info("Error getting property for path: ${path}") + } + + // reorder + newOrder.properties = newOrder.properties ?: [:] + newOrder.properties[parent] = newOrder.properties[parent] ?: [:] + newOrder.properties[parent].putAll(value ?: [:]) + removeProperty(properties, path) + } else { + // if path is not found, then check if parent is in root model + newOrder.properties = newOrder.properties ?: [:] + newOrder.properties[parent] = newOrder.properties[parent] ?: [:] + properties = properties ?: [:] + newOrder.properties[parent].putAll((properties[parent] ?: [:])) + } + + if (children) { + // if children are present, then recurse the process through each children + newOrder.properties[parent] = newOrder.properties[parent] ?: [type: "object", properties: [:]] + newOrder.properties[parent].properties.putAll(rearrangePropertiesAccordingToModelRelationship(properties, apiOutput, children, newOrder.properties[parent]).properties) + } + } + } + + newOrder + } + + def findObservationDataFromAPIOutput(String modelToFind, Map apiOutputRelationship, Map data) { + List paths = findPathFromRelationship(modelToFind, apiOutputRelationship) + String path = paths.size() ? paths.first() : null + if (path) { + path = path.replaceAll(".properties", "") + // get model definition for the parent + try { + def result = getProperty(data, path) + return [path: path, data: result] + } + catch (Exception e) { + log.info("Error getting property for path: ${path}") + } + } + else { + return [path: modelToFind, data: [data[modelToFind]]] + } + } + + /** + * Remove a property at a given path. + * i.e. if path is a.b.c and object is [a: [b: [c: [:]]]] then after removing the property, properties will be [a: [b: [:]]] + * @param object + * @param key + * @param parts + */ + void removeProperty(def object, String key, List parts = null) { + parts = parts ?: key.split(/\./) + String part = parts.remove(0) + if (parts.size() == 0) { + if (object instanceof Map) + object.remove(part) + else if (object instanceof List) { + object.each { def item -> + item.remove(part) + } + } + } else if (object instanceof Map) + removeProperty(object[part], key, parts) + else if (object instanceof List) { + object.each { def item -> + removeProperty(item[part], key, parts.clone()) + } + } + } + + Map getStringViewModel(Map dataModel, Map component, Map documentation, String name) { + Map viewModel + switch (component.format) { + case "date-time": + viewModel = addLabel([ + "type" : "date", + "source": name + ], component, name) + break + default: + if (dataModel.constraints) { + viewModel = addLabel([ + "type" : "selectOne", + "source": name + ], component, name) + } else { + viewModel = addLabel([ + "type" : "text", + "source": name + ], component, name) + } + + break + } + + viewModel + } + + Map getTimeDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType" : "time", + "name" : name, + "dwcAttribute": "eventTime" + ], component) + } + + Map getTimeViewModel(Map dataModel, Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType" : "time", + "name" : name, + "dwcAttribute": "eventTime" + ], component) + } + + Map getListDataType(Map component, Map documentation, String name, List columns, Boolean isObject = false) { + addUnitsAndDescription([ + "dataType": "list", + "name" : name, + "isObject": isObject, + "columns" : columns + ], component) + } + + Map getListViewModel(Map dataModel, Map component, Map documentation, String name, List columns) { + Map viewModel = addLabel([ + type : "section", + title: getLabel(component, name), + boxed: true, + items: [[ + type : "repeat", + source: dataModel.name, + items : [[ + type : "row", + "class": "output-section", + items : [[ + type : "col", + items: columns + ]] + ]] + ]] + ], component, name) + + if (dataModel.isObject && viewModel.items) { + viewModel.items.first().userAddedRows = false + } + + viewModel + } + + Map getSpeciesDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType" : "species", + "name" : name, + "dwcAttribute": "scientificName" + ], component) + } + + Map getSpeciesViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "speciesSelect", + source: dataModel.name + ], component, name) + } + + Map getImageDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType": "image", + "name" : name + ], component) + } + + Map getImageViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "image", + source: dataModel.name + ], component, name) + } + + Map getDocumentDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType": "boolean", + "name" : name + ], component) + } + + Map getDocumentViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "document", + source: dataModel.name + ], component, name) + } + + Map getBooleanDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType": "boolean", + "name" : name + ], component) + } + + Map getBooleanViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "boolean", + source: dataModel.name + ], component, name) + } + + Map getFeatureDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType": "feature", + "name" : name + ], component) + } + + Map getFeatureViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "feature", + source: dataModel.name + ], component, name) + } + + Map getIntegerDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType" : "number", + "name" : name, + "decimalPlaces": 0 + ], component) + } + + Map getIntegerViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "number", + source: dataModel.name + ], component, name) + } + + Map getNumberDataType(Map component, Map documentation, String name) { + addUnitsAndDescription([ + "dataType" : "number", + "name" : name, + "decimalPlaces": PARATOO_FLOAT_DECIMAL_PLACES + ], component) + } + + Map getNumberViewModel(Map dataModel, Map component, Map documentation, String name) { + addLabel([ + type : "number", + source: dataModel.name + ], component, name) + } + + Map addLabel(Map viewModel, Map component, String name) { + viewModel.preLabel = getLabel(component, name) + viewModel + } + + /** + * Get label from swagger definition or from property name + * @param component + * @param name + * @return + */ + String getLabel(Map component, String name) { + String label + if (component[PARATOO_FIELD_LABEL]) { + label = component[PARATOO_FIELD_LABEL] + } else { + label = getLabelFromPropertyName(name) + } + + label + } + + /** + * Get label from property name. + * e.g. "bird-survey" will be converted to "Bird Survey" + * @param name + * @return + */ + String getLabelFromPropertyName(String name) { + String out = '' + out = name.replaceAll(/(-|_)/, '-') + .split('-') + .findAll { it } + .collect { it.capitalize() } + .join(' ') + + return out.trim() + } + + Map addUnitsAndDescription(Map dataType, Map component) { + if (component[PARATOO_UNIT_FIELD_NAME]) { + dataType.units = component[PARATOO_UNIT_FIELD_NAME] + } + + if (component[PARATOO_HINT]) { + dataType.description = component[PARATOO_HINT] + } + + dataType + } + + Map getColumns(Map component, Map documentation, Deque modelVisitStack, int depth = 0, String path, ParatooProtocolConfig config = null) { + Map template = [dataModel: [], viewModel: []] + Map properties = getModelStructureFromDefinition(component) + List required = getRequiredModels(component) + properties?.each { String name, def model -> + if (!model) + return + + if (PARATOO_IGNORE_MODEL_LIST.contains(name)) + return + + if(model instanceof Map) { + Map sections = convertToDataModelAndViewModel(model, documentation, name, required, modelVisitStack, depth, "${path}.${name}", config) + if (sections?.dataModel) + template.dataModel.addAll(sections.dataModel) + + if (sections?.viewModel) + template.viewModel.addAll(sections.viewModel) + } + } + + template + } + + /** + * Converts a model name to a name used in swagger documentation. + * e.g. a-model-name will be converted to AModelNameRequest + * @param modelName + * @return + */ + String getSchemaNameFromModelName(String modelName) { + capitalizeModelName(modelName) + PARATOO_SCHEMA_POSTFIX + } + + /** + * Capitalize a model name. + * e.g. a-model-name will be converted to AModelName + * @param modelName + * @return + */ + String capitalizeModelName(String modelName) { + modelName?.toLowerCase()?.replaceAll("[^a-zA-Z0-9]+", ' ')?.tokenize(' ')?.collect { it.capitalize() }?.join() + } + + def getProperty(def surveyData, String path, boolean useAccessor = false, boolean isDeepCopy = true) { + if (!path || surveyData == null) { + return null + } + + def result + if (useAccessor) { + result = new PropertyAccessor(path).get(surveyData) + } + else { + List parts = path.split(/\./) + result = ElasticSearchService.getDataFromPath(surveyData, parts) + } + + return isDeepCopy ? deepCopy(result) : result + } + + /** + * Transforms a species name to species object used by ecodata. + * e.g. Acacia glauca [Species] (scientific: Acacia glauca Willd.) + * [name: "Acacia glauca Willd.", scientificName: "Acacia glauca Willd.", guid: "A_GUID"] + * Guid is necessary to generate species occurrence record. Guid is found by searching the species name with BIE. If not found, then a default value is added. + * @param name + * @return + */ + Map transformSpeciesName(String name) { + if (!name) { + return null + } + + String regex = "([^\\[\\(]*)(?:\\[(.*)\\])?\\s*(?:\\(scientific:\\s*(.*?)\\))?" + Pattern pattern = Pattern.compile(regex) + Matcher matcher = pattern.matcher(name) + Map result = [name: name, scientificName: name, commonName: name, outputSpeciesId: UUID.randomUUID().toString()] + + if (matcher.find()) { + String commonName = matcher.group(1)?.trim() + String scientificName = matcher.group(3)?.trim() + result.commonName = commonName ?: result.commonName + result.taxonRank = matcher.group(2)?.trim() + result.scientificName = scientificName ?: commonName ?: result.scientificName + result.name = scientificName ?: commonName ?: result.name + } + + metadataService.autoPopulateSpeciesData(result) + // try again with common name + if ((result.guid == null) && result.commonName) { + def speciesObject = [scientificName: result.commonName] + metadataService.autoPopulateSpeciesData(speciesObject) + result.guid = speciesObject.guid + result.scientificName = result.scientificName ?: speciesObject.scientificName + } + + // record is only created if guid is present + result.guid = result.guid ?: "A_GUID" + result + } } + diff --git a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy index a0fbfcdc4..b1ee890db 100644 --- a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy @@ -44,6 +44,7 @@ class ProjectService { OrganisationService organisationService UserService userService ActivityFormService activityFormService + RecordService recordService LockService lockService /* def getCommonService() { @@ -1039,4 +1040,18 @@ class ProjectService { count > 0 } + List fetchDataSetRecords (String projectId, String dataSetId) { + int batchSize = 10, count = 10, offset = 0 + List records = [] + while (batchSize == count) { + def response = Record.findAllByProjectIdAndDataSetId(projectId, dataSetId, [max: batchSize, offset: offset]) + count = records.size() + response = response.collect { recordService.toMap(it) } + records.addAll(response) + offset += count + } + + records + } + } \ No newline at end of file diff --git a/grails-app/services/au/org/ala/ecodata/WebService.groovy b/grails-app/services/au/org/ala/ecodata/WebService.groovy index 8bf425498..6f02db8dd 100644 --- a/grails-app/services/au/org/ala/ecodata/WebService.groovy +++ b/grails-app/services/au/org/ala/ecodata/WebService.groovy @@ -227,7 +227,7 @@ class WebService { } } - Map doPost(String url, Map postBody) { + Map doPost(String url, Map postBody, boolean addUserId = true, Map headers = null) { HttpURLConnection conn = null def charEncoding = 'utf-8' try { @@ -237,11 +237,20 @@ class WebService { conn.setRequestProperty("Content-Type", "application/json;charset=${charEncoding}"); conn.setRequestProperty("Authorization", "${grailsApplication.config.getProperty('api_key')}"); - def user = getUserService().getCurrentUserDetails() - if (user && user.userId) { - conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId) - conn.setRequestProperty("Cookie", "ALA-Auth="+java.net.URLEncoder.encode(user.userName, charEncoding)) + if (addUserId) { + def user = getUserService().getCurrentUserDetails() + if (user && user.userId) { + conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId) + conn.setRequestProperty("Cookie", "ALA-Auth=" + java.net.URLEncoder.encode(user.userName, charEncoding)) + } } + + if (headers) { + headers.each {key, value -> + conn.setRequestProperty(key, value) + } + } + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(), charEncoding) wr.write((postBody as JSON).toString()) wr.flush() diff --git a/grails-app/views/paratoo/_paratooProject.gson b/grails-app/views/paratoo/_paratooProject.gson index 703c836e5..81adbf773 100644 --- a/grails-app/views/paratoo/_paratooProject.gson +++ b/grails-app/views/paratoo/_paratooProject.gson @@ -7,6 +7,7 @@ model { json { id project.id name project.name + grantID project.grantID protocols tmpl.paratooProtocol('protocol', project.protocols) project_area project.projectArea ? tmpl.projectArea(projectArea:project.projectArea) : null plot_selections tmpl.plot('plot', project.plots) diff --git a/src/main/groovy/au/org/ala/ecodata/DateUtil.groovy b/src/main/groovy/au/org/ala/ecodata/DateUtil.groovy index 4e8ad3499..8c5831423 100644 --- a/src/main/groovy/au/org/ala/ecodata/DateUtil.groovy +++ b/src/main/groovy/au/org/ala/ecodata/DateUtil.groovy @@ -1,5 +1,7 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.converter.ISODateBindingConverter + import java.math.MathContext import java.math.RoundingMode import java.text.DecimalFormat @@ -21,6 +23,7 @@ class DateUtil { static DateTimeFormatter ISO_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") static DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd") + static DateTimeFormatter DISPLAY_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a") static Date parse(String dateStr) { SimpleDateFormat format = new SimpleDateFormat(dateFormat) return format.parse(dateStr.replace("Z", "+0000")) @@ -46,6 +49,11 @@ class DateUtil { dateTime.format(DATE_FORMATTER) } + static String formatAsDisplayDateTime(Date date) { + ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()) + dateTime.format(DISPLAY_DATE_TIME_FORMATTER) + } + /** * Returns a formatted string representing the financial year a report or activity falls into, based on * the end date. This method won't necessarily work for start dates as it will subtract a day from the value @@ -127,4 +135,9 @@ class DateUtil { } return timeZone } + + static String convertUTCDateToStringInTimeZone (String dateStr, TimeZone clientTimezone = TimeZone.default, String format = "dd/MM/yyyy HH:mm:ss Z z") { + Date date = new ISODateBindingConverter().convert(dateStr, ISODateBindingConverter.FORMAT) + date.format(format, clientTimezone) + } } diff --git a/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy b/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy index 72d89ec59..a299e4a8f 100644 --- a/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy +++ b/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy @@ -1,22 +1,22 @@ package au.org.ala.ecodata -import org.locationtech.jts.geom.* -import org.locationtech.jts.io.WKTReader -import org.locationtech.jts.io.WKTWriter -import org.locationtech.jts.simplify.TopologyPreservingSimplifier -import org.locationtech.jts.util.GeometricShapeFactory import grails.converters.JSON import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory import org.geotools.geojson.geom.GeometryJSON import org.geotools.geometry.jts.JTS +import org.geotools.geometry.jts.JTSFactoryFinder import org.geotools.referencing.CRS import org.geotools.referencing.GeodeticCalculator +import org.locationtech.jts.geom.* +import org.locationtech.jts.io.WKTReader +import org.locationtech.jts.io.WKTWriter +import org.locationtech.jts.simplify.TopologyPreservingSimplifier +import org.locationtech.jts.util.GeometricShapeFactory import org.opengis.referencing.crs.CoordinateReferenceSystem import org.opengis.referencing.operation.MathTransform import java.awt.geom.Point2D - /** * Helper class for working with site geometry. */ @@ -39,6 +39,19 @@ class GeometryUtils { return result } + static Geometry getFeatureCollectionConvexHull (List features) { + // Extract geometries from features + List geometries = [] + features.each { feature -> geometries.add( geoJsonMapToGeometry(feature) ) } + + GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory() + GeometryCollection geometryCollection = geometryFactory.createGeometryCollection(geometries.toArray(new Geometry[0])) + Geometry unionGeometry = geometryCollection.union() + + // Compute convex hull + unionGeometry.convexHull() + } + private static MultiPolygon convertToMultiPolygon(Geometry geom) { MultiPolygon result switch (geom.geometryType) { diff --git a/src/main/groovy/au/org/ala/ecodata/converter/FeatureConverter.groovy b/src/main/groovy/au/org/ala/ecodata/converter/FeatureConverter.groovy new file mode 100644 index 000000000..03165ef20 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/converter/FeatureConverter.groovy @@ -0,0 +1,47 @@ +package au.org.ala.ecodata.converter + +class FeatureConverter implements RecordFieldConverter { + + List convert(Map data, Map metadata = [:]) { + Map record = [:] + + + Double latitude = getDecimalLatitude(data[metadata.name]) + Double longitude = getDecimalLongitude(data[metadata.name]) + + // Don't override decimalLongitud or decimalLatitude in case they are null, site info could've already set them + if(latitude != null) { + record.decimalLatitude = latitude + } + + if (longitude != null) { + record.decimalLongitude = longitude + } + + + Map dwcMappings = extractDwcMapping(metadata) + + record << getDwcAttributes(data, dwcMappings) + + if (data.dwcAttribute) { + record[data.dwcAttribute] = data.value + } + + [record] + } + + static Double getDecimalLatitude (Map data) { + switch (data.type) { + case 'Point': + return data.coordinates[1] + } + } + + static Double getDecimalLongitude (Map data) { + switch (data.type) { + case 'Point': + return data.coordinates[0] + } + + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/converter/GenericFieldConverter.groovy b/src/main/groovy/au/org/ala/ecodata/converter/GenericFieldConverter.groovy index b8fe8633b..23e07b096 100644 --- a/src/main/groovy/au/org/ala/ecodata/converter/GenericFieldConverter.groovy +++ b/src/main/groovy/au/org/ala/ecodata/converter/GenericFieldConverter.groovy @@ -23,7 +23,7 @@ class GenericFieldConverter implements RecordFieldConverter { record << getDwcAttributes(data, dwcMappings) - if (data.dwcAttribute) { + if (data?.dwcAttribute) { record[data.dwcAttribute] = data.value } @@ -34,9 +34,9 @@ class GenericFieldConverter implements RecordFieldConverter { private Double getLatitude(Map data) { Double latitude = null - if (data.decimalLatitude) { + if (data?.decimalLatitude) { latitude = toDouble(data.decimalLatitude) - } else if (data.locationLatitude) { + } else if (data?.locationLatitude) { latitude = toDouble(data.locationLatitude) } @@ -46,9 +46,9 @@ class GenericFieldConverter implements RecordFieldConverter { private Double getLongitude(Map data) { Double longitude = null - if (data.decimalLongitude) { + if (data?.decimalLongitude) { longitude = toDouble(data.decimalLongitude) - } else if (data.locationLongitude) { + } else if (data?.locationLongitude) { longitude = toDouble(data.locationLongitude) } diff --git a/src/main/groovy/au/org/ala/ecodata/converter/ListConverter.groovy b/src/main/groovy/au/org/ala/ecodata/converter/ListConverter.groovy index a4d24c835..024d3a97d 100644 --- a/src/main/groovy/au/org/ala/ecodata/converter/ListConverter.groovy +++ b/src/main/groovy/au/org/ala/ecodata/converter/ListConverter.groovy @@ -13,7 +13,17 @@ class ListConverter implements RecordFieldConverter { // delegate the conversion of each column in each row to a specific converter for the column type data[outputMetadata.name].each { row -> + if (row == null) { + return + } + Map baseRecord = [:] + List singleItemModels + List multiItemModels + (singleItemModels, multiItemModels) = outputMetadata?.columns?.split { + //check if dataType is null + !RecordConverter.MULTI_ITEM_DATA_TYPES.contains(it.dataType?.toLowerCase()) + } //Use the same approach as au.org.ala.ecodata.converter.RecordConverter.convertRecords() to convert // singleItemModels, ie, iterate over the datatype definitions rather than the values (data) @@ -26,7 +36,7 @@ class ListConverter implements RecordFieldConverter { // All records will share the same base record data List baseRecordModels List speciesModels - (baseRecordModels, speciesModels) = outputMetadata?.columns?.split { + (baseRecordModels, speciesModels) = singleItemModels?.split { it.dataType.toLowerCase() != "species" } @@ -36,22 +46,45 @@ class ListConverter implements RecordFieldConverter { baseRecordModels?.each { Map dataModel -> RecordFieldConverter converter = RecordConverter.getFieldConverter(dataModel.dataType) List recordFieldSets = converter.convert(row, dataModel) - baseRecord << recordFieldSets[0] + if (recordFieldSets[0]) + baseRecord << recordFieldSets[0] } // For each species dataType, where present we will generate a new record speciesModels?.each { Map dataModel -> RecordFieldConverter converter = RecordConverter.getFieldConverter(dataModel.dataType) List recordFieldSets = converter.convert(row, dataModel) - Map speciesRecord = RecordConverter.overrideFieldValues(baseRecord, recordFieldSets[0]) - - // We want to create a record in the DB only if species information is present - if(speciesRecord.outputSpeciesId) { - speciesRecord.outputItemId = index++ - records << speciesRecord - } else { - log.warn("Record [${speciesRecord}] does not contain full species information. " + - "This is most likely a bug.") + if (recordFieldSets) { + Map speciesRecord = RecordConverter.overrideFieldValues(baseRecord, recordFieldSets[0]) + + // We want to create a record in the DB only if species information is present + if (speciesRecord.outputSpeciesId) { + speciesRecord.outputItemId = index++ + records << speciesRecord + } else { + log.warn("Record [${speciesRecord}] does not contain full species information. " + + "This is most likely a bug.") + } + } + } + + if (multiItemModels) { + // For each multiItemModel, get the appropriate field converter for the data type and generate the list of field + // sets which will be converted into Records. For each field set, add a copy of the skeleton Record so it has + // all the common fields + multiItemModels?.each { Map dataModel -> + RecordFieldConverter converter = RecordConverter.getFieldConverter(dataModel.dataType) + List recordFieldSets = converter.convert(row, dataModel) + + recordFieldSets.each { + Map rowRecord = RecordConverter.overrideFieldValues(baseRecord, it) + if(rowRecord.guid && rowRecord.guid != "") { + records << rowRecord + } else { + log.warn("Multi item Record [${rowRecord}] does not contain species information, " + + "was the form intended to work like that?") + } + } } } } diff --git a/src/main/groovy/au/org/ala/ecodata/converter/RecordConverter.groovy b/src/main/groovy/au/org/ala/ecodata/converter/RecordConverter.groovy index be1764602..c0482bd08 100644 --- a/src/main/groovy/au/org/ala/ecodata/converter/RecordConverter.groovy +++ b/src/main/groovy/au/org/ala/ecodata/converter/RecordConverter.groovy @@ -151,8 +151,8 @@ class RecordConverter { public static Map overrideFieldValues(Map source, Map additional) { Map result = [:] - result << source - result << additional + result << (source ?: [:]) + result << (additional ?: [:]) result } diff --git a/src/main/groovy/au/org/ala/ecodata/converter/SpeciesConverter.groovy b/src/main/groovy/au/org/ala/ecodata/converter/SpeciesConverter.groovy index 5438e2327..5cf5b43f5 100644 --- a/src/main/groovy/au/org/ala/ecodata/converter/SpeciesConverter.groovy +++ b/src/main/groovy/au/org/ala/ecodata/converter/SpeciesConverter.groovy @@ -6,6 +6,10 @@ class SpeciesConverter implements RecordFieldConverter { ] List convert(Map data, Map metadata = [:]) { + if ((data == null) || (data[metadata.name] == null) ) { + return [] + } + Map record = [:] record.scientificNameID = record.guid = data[metadata.name].guid diff --git a/src/main/groovy/au/org/ala/ecodata/metadata/OutputMetadata.groovy b/src/main/groovy/au/org/ala/ecodata/metadata/OutputMetadata.groovy index 16fb4931b..f9c0ff1e5 100644 --- a/src/main/groovy/au/org/ala/ecodata/metadata/OutputMetadata.groovy +++ b/src/main/groovy/au/org/ala/ecodata/metadata/OutputMetadata.groovy @@ -241,7 +241,7 @@ class OutputMetadata { } def isNestedDataModelType(node) { - return (node.columns != null && node.dataType != "geoMap") + return ((node.columns != null) && (node.columns.size() != 0) && node.dataType != "geoMap") } def isNestedViewModelType(node) { return (node.items != null || node.columns != null) @@ -256,7 +256,7 @@ class OutputMetadata { * ] * */ - Map getNamesForDataType(String type, context){ + Map getNamesForDataType(String type, context, int depth = 0, String path = ""){ Map names = [:], childrenNames if(!context && metadata){ @@ -265,8 +265,9 @@ class OutputMetadata { context?.each { data -> if(isNestedDataModelType(data)){ + String contextPath = "${path}.${data.name}" // recursive call for nested data model - childrenNames = getNamesForDataType(type, getNestedDataModelNodes(data)); + childrenNames = getNamesForDataType(type, getNestedDataModelNodes(data), depth + 1, contextPath); if(childrenNames?.size()){ names[data.name] = childrenNames } diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollection.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollection.groovy index fff87f0cd..76338dc8c 100644 --- a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollection.groovy +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollection.groovy @@ -1,23 +1,11 @@ package au.org.ala.ecodata.paratoo -import grails.databinding.BindingFormat -import grails.validation.Validateable import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import grails.validation.Validateable @JsonIgnoreProperties(['metaClass', 'errors', 'expandoMetaClass']) class ParatooCollection implements Validateable { - String orgMintedIdentifier - ParatooProtocolId protocol - String projectId - String userId - - @BindingFormat("iso8601") - Date eventTime - static constraints = { - protocol validator: { val, obj -> val.validate() } - projectId nullable: true - userId nullable: true - eventTime nullable: true - } + String orgMintedUUID + Map coreProvenance } diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollectionId.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollectionId.groovy index a64ae1494..4f0f6d527 100644 --- a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollectionId.groovy +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooCollectionId.groovy @@ -1,14 +1,60 @@ package au.org.ala.ecodata.paratoo +import au.org.ala.ecodata.DateUtil import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import grails.converters.JSON +import grails.databinding.BindingFormat import grails.validation.Validateable @JsonIgnoreProperties(['metaClass', 'errors', 'expandoMetaClass']) class ParatooCollectionId implements Validateable { - ParatooSurveyId surveyId + ParatooSurveyMetadata survey_metadata + String userId + @BindingFormat("iso8601") + Date eventTime + + Date coreSubmitTime static constraints = { - surveyId validator: { val, obj -> val.validate() } + userId nullable: true + eventTime nullable: true + coreSubmitTime nullable: true + survey_metadata validator: { val, obj -> val.validate() } + } + + String getProjectId() { + survey_metadata?.survey_details?.project_id + } + + String getProtocolId() { + survey_metadata?.survey_details?.protocol_id + } + + Map toMap() { + [ + survey_metadata: survey_metadata.toMap(), + userId: userId, + eventTime: eventTimeAsISOString() + ] + } + + String eventTimeAsISOString() { + eventTime ? DateUtil.formatWithMilliseconds(eventTime) : null + } + + String encodeAsOrgMintedIdentifier() { + Map data = toMap() + String jsonString = (data as JSON).toString() + jsonString.encodeAsBase64() + } + + static ParatooCollectionId fromMap(Map map) { + + Date eventTime = map.eventTime ? DateUtil.parseWithMilliseconds(map.eventTime) : null + new ParatooCollectionId( + eventTime: eventTime, + survey_metadata: ParatooSurveyMetadata.fromMap(map.survey_metadata), + ) } } diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooPlotSelection.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooPlotSelection.groovy index 9a0146f9c..7796caf93 100644 --- a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooPlotSelection.groovy +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooPlotSelection.groovy @@ -13,7 +13,7 @@ class ParatooPlotSelectionData { String plot_label ParatooPlotSelectionLocation recommended_location String uuid - String comment + String comments } @JsonIgnoreProperties(['metaClass', 'errors', 'expandoMetaClass']) diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProject.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProject.groovy index 585958555..c046e3356 100644 --- a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProject.groovy +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProject.groovy @@ -17,6 +17,8 @@ class ParatooProject { String id String name + // Used when creating voucher labels + String grantID AccessLevel accessLevel Project project List protocols diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy index 657c0eaa3..e6b0be669 100644 --- a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy @@ -1,10 +1,12 @@ package au.org.ala.ecodata.paratoo -import au.org.ala.ecodata.DateUtil +import au.org.ala.ecodata.* +import au.org.ala.ecodata.converter.ISODateBindingConverter +import au.org.ala.ecodata.metadata.OutputMetadata import au.org.ala.ecodata.metadata.PropertyAccessor -import groovy.util.logging.Slf4j import com.fasterxml.jackson.annotation.JsonIgnoreProperties - +import groovy.util.logging.Slf4j +import org.locationtech.jts.geom.Geometry /** * Configuration about how to work with a Paratoo/Monitor protocol */ @@ -19,17 +21,27 @@ class ParatooProtocolConfig { String geometryType = 'Polygon' String geometryPath - String startDatePath = 'attributes.start_date_time' - String endDatePath = 'attributes.end_date_time' - String surveyIdPath = 'attributes.surveyId' - String plotLayoutIdPath = 'attributes.plot_visit.data.attributes.plot_layout.data.id' - String plotLayoutPointsPath = 'attributes.plot_visit.data.attributes.plot_layout.data.attributes.plot_points' - String plotSelectionPath = 'attributes.plot_visit.data.attributes.plot_layout.data.attributes.plot_selection.data.attributes' - String plotLayoutDimensionLabelPath = 'attributes.plot_visit.data.attributes.plot_layout.data.attributes.plot_dimensions.data.attributes.label' - String plotLayoutTypeLabelPath = 'attributes.plot_visit.data.attributes.plot_layout.data.attributes.plot_type.data.attributes.label' - String getApiEndpoint(ParatooSurveyId surveyId) { + String startDatePath = 'start_date_time' + String endDatePath = 'end_date_time' + String surveyIdPath = 'survey_metadata' + String plotVisitPath = 'plot_visit' + String plotProtocolObservationDatePath = "date_time" + String plotVisitStartDatePath = "${plotVisitPath}.start_date" + String plotVisitEndDatePath = "${plotVisitPath}.end_date" + String plotLayoutPath = "${plotVisitPath}.plot_layout" + String plotLayoutIdPath = "${plotLayoutPath}.id" + String plotLayoutPointsPath = "${plotLayoutPath}.plot_points" + String plotSelectionPath = "${plotLayoutPath}.plot_selection" + String plotLayoutDimensionLabelPath = "${plotLayoutPath}.plot_dimensions.label" + String plotLayoutTypeLabelPath = "${plotLayoutPath}.plot_type.label" + + String getApiEndpoint(ParatooCollectionId surveyId) { apiEndpoint ?: defaultEndpoint(surveyId) } + Map overrides = [dataModel: [:], viewModel: [:]] + + ParatooCollectionId surveyId + TimeZone clientTimeZone private static String removeMilliseconds(String isoDateWithMillis) { if (!isoDateWithMillis) { @@ -39,97 +51,323 @@ class ParatooProtocolConfig { } String getStartDate(Map surveyData) { - removeMilliseconds(getProperty(surveyData, startDatePath)) + if(startDatePath == null || surveyData == null) { + return null + } + + def date + if (usesPlotLayout) { + List dates = getDatesFromObservation(surveyData) + date = dates ? DateUtil.format(dates.first()) : null + return date + } + else { + date = getProperty(surveyData, startDatePath) + if (!date) { + date = getPropertyFromSurvey(surveyData, startDatePath) + } + + date = getFirst(date) + return removeMilliseconds(date) + } + } + + /** + * Get date from plotProtocolObservationDatePath and sort them. + * @param surveyData - reverse lookup output which includes survey and observation data + * @return + */ + List getDatesFromObservation(Map surveyData) { + Map surveysData = surveyData.findAll { key, value -> + ![ getSurveyAttributeName(), ParatooService.PARATOO_DATAMODEL_PLOT_SELECTION, + ParatooService.PARATOO_DATAMODEL_PLOT_VISIT, ParatooService.PARATOO_DATAMODEL_PLOT_LAYOUT].contains(key) + } + List result = [] + ISODateBindingConverter converter = new ISODateBindingConverter() + surveysData.each { key, value -> + def dates = getProperty(value, plotProtocolObservationDatePath) + dates = dates instanceof List ? dates : [dates] + + result.addAll(dates.collect { String date -> + date ? converter.convert(date, ISODateBindingConverter.FORMAT) : null + }) + } + + result = result.findAll { it != null } + result.sort() + } + + def getPropertyFromSurvey(Map surveyData, String path) { + surveyData = getSurveyData(surveyData) + getProperty(surveyData, path) } String getEndDate(Map surveyData) { - removeMilliseconds(getProperty(surveyData, endDatePath)) + if(endDatePath == null || surveyData == null) { + return null + } + + def date + if (usesPlotLayout) { + def dates = getDatesFromObservation(surveyData) + date = dates ? DateUtil.format(dates.last()) : null + return date + } + else { + date = getProperty(surveyData, endDatePath) + if (!date) { + date = getPropertyFromSurvey(surveyData, endDatePath) + } + + date = getFirst(date) + return removeMilliseconds(date) + } } Map getSurveyId(Map surveyData) { - getProperty(surveyData, surveyIdPath) + if(surveyIdPath == null || surveyData == null) { + return null + } + + def result = getProperty(surveyData, surveyIdPath) + if (result == null) { + result = getPropertyFromSurvey(surveyData, surveyIdPath) + } + + result = getFirst(result) + result } - private Map extractSiteDataFromPath(Map surveyData) { - Map geometry = null + private Map extractSiteDataFromPath(Map survey) { + Map surveyData = getSurveyData(survey) def geometryData = getProperty(surveyData, geometryPath) + geometryData = getFirst(geometryData) + extractGeometryFromSiteData(geometryData) + } + + String getSurveyAttributeName() { + String surveyAttribute = apiEndpoint + if(surveyAttribute?.endsWith('s')) { + surveyAttribute = surveyAttribute.substring(0, surveyAttribute.length() - 1) + } + + surveyAttribute + } + + Map getSurveyDataFromObservation (Map observation) { + String surveyAttribute = getSurveyAttributeName() + + def survey = observation[surveyAttribute] + if (survey instanceof List) { + return survey[0] + } + + survey + } + + private List extractFeatures (Map observation, ActivityForm form) { + List features = [] + form.sections.each { FormSection section -> + OutputMetadata om = new OutputMetadata(section.template) + Map paths = om.getNamesForDataType("feature", null ) + features.addAll(getFeaturesFromPath(observation, paths)) + } + + features + } + + private List getFeaturesFromPath (Map output, Map paths) { + List features = [] + paths.each { String name, node -> + if (node instanceof Boolean) { + features.add(output[name]) + // todo later: add featureIds and modelId for compliance with feature behaviour of reports + } + + // recursive check for feature + if (node instanceof Map) { + if (output[name] instanceof Map) { + features.addAll(getFeaturesFromPath(output[name], node)) + } + + if (output[name] instanceof List) { + output[name].eachWithIndex { row, index -> + features.addAll(getFeaturesFromPath(row, node)) + } + } + } + } + + features + } + + private Map extractGeometryFromSiteData(geometryData) { + Map geometry = null if (geometryData) { switch (geometryType) { case 'Point': - geometry = [type:'Point', coordinates:[geometryData.lng, geometryData.lat]] + geometry = [type: 'Point', coordinates: [geometryData.lng, geometryData.lat]] break } + } else { + log.warn("Unable to get spatial data from survey: " + apiEndpoint + ", " + geometryPath) } - else { - log.warn("Unable to get spatial data from survey: "+apiEndpoint+", "+geometryPath) - } + geometry } - private static String defaultEndpoint(ParatooSurveyId surveyId) { - String apiEndpoint = surveyId.surveyType + private static String defaultEndpoint(ParatooCollectionId surveyId) { + String apiEndpoint = surveyId.survey_metadata?.survey_details?.survey_model if (!apiEndpoint.endsWith('s')) { apiEndpoint += 's' } // strapi makes the endpoint plural sometimes? apiEndpoint } - private static def getProperty(Map surveyData, String path) { + def getProperty(def surveyData, String path) { if (!path) { return null } - new PropertyAccessor(path).get(surveyData) + + def result = new PropertyAccessor(path).get(surveyData) + if ((result == null) && (surveyId != null)) { + path = surveyId.survey_metadata.survey_details.survey_model+'.'+path + result = new PropertyAccessor(path).get(surveyData) + } + + result } - Map getGeoJson(Map survey) { - if (!survey) { + Map getGeoJson(Map output, ActivityForm form = null) { + if (!output) { return null } Map geoJson = null if (usesPlotLayout) { - geoJson = extractSiteDataFromPlotVisit(survey) + geoJson = extractSiteDataFromPlotVisit(output) + // get list of all features associated with observation + if (geoJson && form && output) { + geoJson.features = extractFeatures(output, form) + } } else if (geometryPath) { - geoJson = extractSiteDataFromPath(survey) + geoJson = extractSiteDataFromPath(output) } + else if (form && output) { + List features = extractFeatures(output, form) + if (features) { + List featureGeometries = features.collect { it.geometry } + Geometry geometry = GeometryUtils.getFeatureCollectionConvexHull(featureGeometries) + String startDateInString = getStartDate(output) + startDateInString = DateUtil.convertUTCDateToStringInTimeZone(startDateInString, clientTimeZone?:TimeZone.default) + String name = "${form.name} site - ${startDateInString}" + geoJson = [ + type: 'Feature', + geometry: GeometryUtils.geometryToGeoJsonMap(geometry), + properties: [ + name: name, + description: "${name} (convex hull of all features)", + ], + features: features + ] + } + } + geoJson } - boolean matches(Map surveyData, ParatooSurveyId surveyId) { + Map getPlotVisit (Map surveyData) { + def result = getProperty(surveyData, plotVisitPath) + Map plotVisit = getFirst(result) + copyWithExcludedProperty(plotVisit, ParatooService.PARATOO_DATAMODEL_PLOT_LAYOUT) + } + + Map getPlotLayout (Map surveyData) { + def result = getProperty(surveyData, plotLayoutPath) + Map plotLayout = getFirst(result) + copyWithExcludedProperty(plotLayout, ParatooService.PARATOO_DATAMODEL_PLOT_SELECTION) + } + + Map getPlotSelection (Map surveyData) { + def result = getProperty(surveyData, plotSelectionPath) + Map plotSelection = getFirst(result) + copyWithExcludedProperty(plotSelection) + } + + private Map copyWithExcludedProperty(Map map, String property = null) { + if (!map) { + return [:] + } + List keys = map.keySet().toList() + if (property) { + keys = keys.minus(property) + } + Map result = [:] + keys.each { key -> + result[key] = map[key] + } + + result + } + + + boolean matches(Map surveyData, ParatooCollectionId collectionId) { Map tmpSurveyId = getSurveyId(surveyData) - tmpSurveyId.surveyType == surveyId.surveyType && - tmpSurveyId.time == surveyId.timeAsISOString() && - tmpSurveyId.uuid == surveyId.uuid + if (!tmpSurveyId) { + log.error("Cannot find surveyId:") + log.debug(surveyData.toString()) + return false + } + + surveyEqualityTest(tmpSurveyId, collectionId) + } + + static boolean surveyEqualityTest(Map tmpSurveyId, ParatooCollectionId collectionId) { + tmpSurveyId?.survey_details?.survey_model == collectionId.survey_metadata?.survey_details.survey_model && + tmpSurveyId?.survey_details?.time == collectionId.survey_metadata?.survey_details.time && + tmpSurveyId?.survey_details?.uuid == collectionId.survey_metadata?.survey_details.uuid + } + + static def getFirst (def value) { + if (value instanceof List) { + try { + value = value.first() + } + catch (NoSuchElementException e) { + log.warn("List is empty", e) + value = null + } + } + + value + } + + def getSurveyData (Map survey) { + if (surveyId) { + def path = surveyId.survey_metadata.survey_details.survey_model + getFirst(survey[path]) + } } private Map extractSiteDataFromPlotVisit(Map survey) { - def plotLayoutId = getProperty(survey, plotLayoutIdPath) // Currently an int, may become uuid? + Map surveyData = getSurveyData(survey) + def plotLayoutId = getProperty(surveyData, plotLayoutIdPath) // Currently an int, may become uuid? if (!plotLayoutId) { log.warn("No plot_layout found in survey at path ${plotLayoutIdPath}") return null } - List plotLayoutPoints = getProperty(survey, plotLayoutPointsPath) - Map plotSelection = getProperty(survey, plotSelectionPath) + List plotLayoutPoints = getProperty(surveyData, plotLayoutPointsPath) + Map plotSelection = getProperty(surveyData, plotSelectionPath) Map plotSelectionGeoJson = plotSelectionToGeoJson(plotSelection) - String plotLayoutDimensionLabel = getProperty(survey, plotLayoutDimensionLabelPath) - String plotLayoutTypeLabel = getProperty(survey, plotLayoutTypeLabelPath) + String plotLayoutDimensionLabel = getProperty(surveyData, plotLayoutDimensionLabelPath) + String plotLayoutTypeLabel = getProperty(surveyData, plotLayoutTypeLabelPath) String name = plotSelectionGeoJson.properties.name + ' - ' + plotLayoutTypeLabel + ' (' + plotLayoutDimensionLabel + ')' - Map plotGeometory = toGeometry(plotLayoutPoints) - Map plotGeoJson = [ - type: 'Feature', - geometry: plotGeometory, - properties: [ - name: name, - externalId: plotLayoutId, - description: name, - notes: plotSelectionGeoJson?.properties?.notes - ] - ] + Map plotGeoJson = createFeatureFromGeoJSON(plotLayoutPoints, name, plotLayoutId, plotSelectionGeoJson?.properties?.notes) //Map faunaPlotGeoJson = toGeometry(plotLayout.fauna_plot_point) @@ -139,6 +377,24 @@ class ParatooProtocolConfig { plotGeoJson } + static Map createFeatureFromGeoJSON(List plotLayoutPoints, String name, def plotLayoutId, String notes = "") { + Map plotGeometry = toGeometry(plotLayoutPoints) + createFeatureObject(plotGeometry, name, plotLayoutId, notes) + } + + static Map createFeatureObject(Map plotGeometry, String name, plotLayoutId, String notes = "") { + [ + type : 'Feature', + geometry : plotGeometry, + properties: [ + name : name, + externalId : plotLayoutId, + description: name, + notes : notes + ] + ] + } + static Map toGeometry(List points) { List coords = points?.findAll { !exclude(it) }.collect { [it.lng, it.lat] @@ -151,6 +407,23 @@ class ParatooProtocolConfig { plotGeometry } + static Map createLineStringFeatureFromGeoJSON (List plotLayoutPoints, String name, def plotLayoutId, String notes = "") { + Map plotGeometry = toLineStringGeometry(plotLayoutPoints) + createFeatureObject(plotGeometry, name, plotLayoutId, notes) + } + + static Map toLineStringGeometry(List points) { + List coords = points?.collect { + [it.lng, it.lat] + } + Map plotGeometry = coords ? [ + type : 'LineString', + coordinates: coords + ] : null + + plotGeometry + } + private static List closePolygonIfRequired(List points) { if (points[0][0] != points[-1][0] || points[0][1] != points[-1][1]) { points << points[0] @@ -159,7 +432,7 @@ class ParatooProtocolConfig { } private static boolean exclude(Map point) { - point.name?.data?.attributes?.symbol == "C" // The plot layout has a centre point that we don't want + point.name?.data?.attributes?.symbol == "C" || point.name?.symbol == "C"// The plot layout has a centre point that we don't want } // Accepts a Map or ParatooPlotSelectionData as this is used by two separate calls. @@ -174,7 +447,7 @@ class ParatooProtocolConfig { name : plotSelectionData.plot_label, externalId: plotSelectionData.uuid, description: plotSelectionData.plot_label, - notes: plotSelectionData.comment + notes: plotSelectionData.comments ] geoJson } diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProvenance.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProvenance.groovy new file mode 100644 index 000000000..5d33f64c0 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProvenance.groovy @@ -0,0 +1,38 @@ +package au.org.ala.ecodata.paratoo + +class ParatooProvenance { + + String system_app + String version_app + String version_core_documentation + + String system_core + String version_core + + String system_org + String version_org + + Map toMap() { + [ + version_app: version_app, + version_core_documentation: version_core_documentation, + system_app: system_app, + system_org: system_org, + version_org: version_org, + system_core: system_core, + version_core: version_core + ] + } + + static ParatooProvenance fromMap(Map data) { + new ParatooProvenance( + version_app: data.version_app, + version_core_documentation: data.version_core_documentation, + system_app: data.system_app, + system_org: data.system_org, + version_org: data.version_org, + system_core: data.system_core, + version_core: data.version_core + ) + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooSurveyDetails.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooSurveyDetails.groovy new file mode 100644 index 000000000..566c3ce8e --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooSurveyDetails.groovy @@ -0,0 +1,33 @@ +package au.org.ala.ecodata.paratoo + +class ParatooSurveyDetails { + String survey_model + /** ISO time, stored as a String as we don't need to use it as a Date */ + String time + String uuid + String project_id + String protocol_id + Integer protocol_version + + Map toMap() { + [ + survey_model: survey_model, + time: time, + uuid: uuid, + project_id: project_id, + protocol_id: protocol_id, + protocol_version: protocol_version + ] + } + + static ParatooSurveyDetails fromMap(Map data) { + new ParatooSurveyDetails( + survey_model: data.survey_model, + time: data.time, + uuid: data.uuid, + project_id: data.project_id, + protocol_id: data.protocol_id, + protocol_version: data.protocol_version + ) + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooSurveyMetadata.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooSurveyMetadata.groovy new file mode 100644 index 000000000..98e5dfafe --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooSurveyMetadata.groovy @@ -0,0 +1,33 @@ +package au.org.ala.ecodata.paratoo + +import grails.validation.Validateable + +class ParatooSurveyMetadata implements Validateable { + + static constraints = { + orgMintedUUID nullable: true + } + + ParatooSurveyDetails survey_details + ParatooProvenance provenance + + /** Added by the ParatooService, other fields are supplied as input */ + String orgMintedUUID + + Map toMap() { + [ + survey_details: survey_details.toMap(), + provenance: provenance.toMap(), + orgMintedUUID: orgMintedUUID + ] + } + + static ParatooSurveyMetadata fromMap(Map data) { + ParatooSurveyMetadata surveyMetadata = new ParatooSurveyMetadata([ + survey_details: ParatooSurveyDetails.fromMap(data.survey_details), + provenance: ParatooProvenance.fromMap(data.provenance), + orgMintedUUID: data.orgMintedUUID + ]) + } + +} diff --git a/src/test/groovy/au/org/ala/ecodata/DateUtilSpec.groovy b/src/test/groovy/au/org/ala/ecodata/DateUtilSpec.groovy index 4378f7d72..00887738d 100644 --- a/src/test/groovy/au/org/ala/ecodata/DateUtilSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/DateUtilSpec.groovy @@ -37,4 +37,12 @@ class DateUtilSpec extends Specification { expect: DateUtil.formatWithMilliseconds(DateUtil.parseWithMilliseconds(date)) == date } + + def "A date can be parsed and displayed in provided timezone"(){ + given: + String date = "2021-06-30T00:12:33Z" + + expect: + DateUtil.convertUTCDateToStringInTimeZone(date, TimeZone.getTimeZone("Australia/Sydney"), "dd/MM/yyyy") == "30/06/2021" + } } diff --git a/src/test/groovy/au/org/ala/ecodata/ParatooControllerSpec.groovy b/src/test/groovy/au/org/ala/ecodata/ParatooControllerSpec.groovy index 50ab32e42..e1be30204 100644 --- a/src/test/groovy/au/org/ala/ecodata/ParatooControllerSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/ParatooControllerSpec.groovy @@ -3,6 +3,7 @@ package au.org.ala.ecodata import au.org.ala.ecodata.converter.ISODateBindingConverter import au.org.ala.ecodata.paratoo.ParatooProject import grails.testing.web.controllers.ControllerUnitTest +import groovy.json.JsonSlurper import org.apache.http.HttpStatus import spock.lang.Specification @@ -163,6 +164,11 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< setup: String userId = 'u1' Map collection = buildCollectionJson() + Map collectionId = buildCollectionIdJson() + collectionId.eventTime = DateUtil.formatWithMilliseconds(new Date()) + collectionId.userId = 'system' + Map dataSet = [surveyId:collectionId] + when: request.method = "POST" @@ -171,7 +177,7 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< then: 1 * userService.currentUserDetails >> [userId:userId] - 1 * paratooService.findDataSet(userId, collection.orgMintedIdentifier) >> [project:new ParatooProject(id:'p1'), dataSet:[:]] + 1 * paratooService.findDataSet(userId, collection.orgMintedUUID) >> [project:new ParatooProject(id:'p1'), dataSet:dataSet] 1 * paratooService.protocolWriteCheck(userId, 'p1', "guid-1") >> false and: @@ -184,7 +190,10 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< setup: String userId = 'u1' Map collection = buildCollectionJson() - Map searchResults = [project:new ParatooProject(id:'p1'), dataSet:[:]] + Map collectionId = buildCollectionIdJson() + collectionId.eventTime = DateUtil.formatWithMilliseconds(new Date()) + collectionId.userId = 'system' + Map searchResults = [project:new ParatooProject(id:'p1'), dataSet:[surveyId:collectionId]] when: request.method = "POST" @@ -193,9 +202,9 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< then: 1 * userService.currentUserDetails >> [userId:userId] - 1 * paratooService.findDataSet(userId, collection.orgMintedIdentifier) >> searchResults + 1 * paratooService.findDataSet(userId, collection.orgMintedUUID) >> searchResults 1 * paratooService.protocolWriteCheck(userId, 'p1', "guid-1") >> true - 1 * paratooService.submitCollection({it.orgMintedIdentifier == "c1"}, searchResults.project) >> [:] + 1 * paratooService.submitCollection({it.orgMintedUUID == "c1"}, searchResults.project) >> [updateResult: [:], promise: null] and: response.status == HttpStatus.SC_OK @@ -207,7 +216,10 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< setup: String userId = 'u1' Map collection = buildCollectionJson() - Map searchResults = [project:new ParatooProject(id:'p1'), dataSet:[:]] + Map collectionId = buildCollectionIdJson() + collectionId.eventTime = DateUtil.formatWithMilliseconds(new Date()) + collectionId.userId = 'system' + Map searchResults = [project:new ParatooProject(id:'p1'), dataSet:[surveyId:collectionId]] when: request.method = "POST" @@ -216,9 +228,9 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< then: 1 * userService.currentUserDetails >> [userId:userId] - 1 * paratooService.findDataSet(userId, collection.orgMintedIdentifier) >> searchResults + 1 * paratooService.findDataSet(userId, collection.orgMintedUUID) >> searchResults 1 * paratooService.protocolWriteCheck(userId, 'p1', "guid-1") >> true - 1 * paratooService.submitCollection({it.orgMintedIdentifier == "c1"}, searchResults.project) >> [error:"Error"] + 1 * paratooService.submitCollection({it.orgMintedUUID == "c1"}, searchResults.project) >> [updateResult: [error:"Error"], promise: null] and: response.status == HttpStatus.SC_INTERNAL_SERVER_ERROR @@ -299,31 +311,22 @@ class ParatooControllerSpec extends Specification implements ControllerUnitTest< } private Map buildCollectionIdJson() { - [ - "surveyId": [ - surveyType: "Bird", - time: "2023-01-01T00:00:00Z", - uuid: "1234", - "projectId":"p1", - "protocol": [ - "id": "guid-1", - "version": 1 - ] - ] - ] + readData("mintCollectionIdPayload") } private Map buildCollectionJson() { [ - "orgMintedIdentifier":"c1", - "projectId":"p1", - "userId": "u1", - "protocol": [ - "id": "guid-1", - "version": 1 - ], - "eventTime":"2023-01-01T00:00:00Z" + "orgMintedUUID":"c1", + "coreProvenance": [ + "system_core": "Monitor-test", + "version_core": "1" + ] ] } + private Map readData(String name) { + URL url = getClass().getResource("/paratoo/${name}.json") + new JsonSlurper().parse(url) + } + } diff --git a/src/test/groovy/au/org/ala/ecodata/ParatooJsonViewSpec.groovy b/src/test/groovy/au/org/ala/ecodata/ParatooJsonViewSpec.groovy index 37eacd044..a772d479b 100644 --- a/src/test/groovy/au/org/ala/ecodata/ParatooJsonViewSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/ParatooJsonViewSpec.groovy @@ -12,7 +12,7 @@ class ParatooJsonViewSpec extends Specification implements JsonViewTest { int[][] projectSpec = [[3, 1, 0], [0, 0, 1], [1, 0, 0]] as int[][] Map expectedResult = [ projects: [[ - id:"p1", name:"Project 1", protocols: [ + id:"p1", name:"Project 1", grantID:"g1", protocols: [ [id:1, identifier: "guid-1", name: "Protocol 1", version: 1, module: "module-1"], [id:2, identifier: "guid-2", name: "Protocol 2", version: 1, module: "module-2"], [id:3, identifier: "guid-3", name: "Protocol 3", version: 1, module: "module-3"]], @@ -22,11 +22,11 @@ class ParatooJsonViewSpec extends Specification implements JsonViewTest { ], role:"project_admin" ],[ - id:"p2", name:"Project 2", protocols:[], plot_selections:[], + id:"p2", name:"Project 2", grantID:"g2", protocols:[], plot_selections:[], project_area:[type:"Polygon", coordinates: DUMMY_POLYGON[0].collect{[lat:it[1], lng:it[0]]}], role:"authenticated" ],[ - id:"p3", name:"Project 3", protocols:[ + id:"p3", name:"Project 3", grantID:"g3", protocols:[ [id:1, identifier: "guid-1", name: "Protocol 1", version: 1, module: 'module-1'] ], project_area:null, plot_selections:[], role:'authenticated' ] @@ -70,7 +70,7 @@ class ParatooJsonViewSpec extends Specification implements JsonViewTest { Site tmp = buildSite(numberOfPlots+2) projectArea = [type:tmp.extent.geometry.type, coordinates:tmp.extent.geometry.coordinates] } - new ParatooProject(id:"p$projectIndex", name:"Project $projectIndex", protocols: protocols, projectArea: projectArea, plots:plots, accessLevel: AccessLevel.admin) + new ParatooProject(id:"p$projectIndex", name:"Project $projectIndex", grantID:"g$projectIndex", protocols: protocols, projectArea: projectArea, plots:plots, accessLevel: AccessLevel.admin) } private ActivityForm buildActivityForm(int i) { diff --git a/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy b/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy index 0f661525a..a57dc2dfe 100644 --- a/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy @@ -5,16 +5,19 @@ import au.org.ala.ws.tokens.TokenService import com.nimbusds.oauth2.sdk.token.AccessToken import grails.converters.JSON import grails.test.mongodb.MongoSpec +import grails.testing.gorm.DataTest import grails.testing.services.ServiceUnitTest import groovy.json.JsonSlurper +import org.codehaus.jackson.map.ObjectMapper import org.grails.web.converters.marshaller.json.CollectionMarshaller import org.grails.web.converters.marshaller.json.MapMarshaller +import static grails.async.Promises.waitAll /** * Tests for the ParatooService. * The tests are incomplete as some of the behaviour needs to be specified. */ -class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest{ +class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest, DataTest { String userId = 'u1' SiteService siteService = Mock(SiteService) @@ -23,14 +26,20 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> DUMMY_POLYGON + 1 * siteService.geometryAsGeoJson({ it.siteId == 's1' }) >> DUMMY_POLYGON } @@ -86,7 +106,7 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest projects = service.userProjects(userId) @@ -95,8 +115,8 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest projects = service.userProjects(userId) + when: + List projects = service.userProjects(userId) - then: - projects.size() == 1 - projects[0].accessLevel == AccessLevel.admin + then: + projects.size() == 1 + projects[0].accessLevel == AccessLevel.admin } void "The service can create a data set from a submitted collection"() { setup: - + ParatooCollectionId collectionId = buildCollectionId() String projectId = 'p1' - ParatooProtocolId protocol = new ParatooProtocolId(id:"guid-2", version: 1) - ParatooSurveyId surveyId = new ParatooSurveyId(projectId:projectId, protocol:protocol, surveyType:"api", time:new Date(), uuid:"1l") - ParatooCollectionId collectionId = new ParatooCollectionId(surveyId:surveyId) when: Map result = service.mintCollectionId('u1', collectionId) then: - 1 * projectService.update(_, projectId, false) >> {data, pId, updateCollectory -> + 1 * projectService.update(_, projectId, false) >> { data, pId, updateCollectory -> Map dataSet = data.custom.dataSets[1] // The stubbed project already has a dataSet, so the new one will be index=1 - assert dataSet.surveyId.time == surveyId.timeAsISOString() - assert dataSet.surveyId.uuid == surveyId.uuid - assert dataSet.surveyId.surveyType == surveyId.surveyType - assert dataSet.protocol == surveyId.protocol.id + assert dataSet.surveyId != null + assert dataSet.surveyId.eventTime != null + assert dataSet.surveyId.userId == 'org1' + assert dataSet.surveyId.survey_metadata.orgMintedUUID == dataSet.dataSetId + assert dataSet.protocol == collectionId.protocolId assert dataSet.grantId == "g1" assert dataSet.progress == 'planned' - assert dataSet.name == "aParatooForm 1 - ${DateUtil.formatAsDisplayDate(surveyId.time)} (Project 1)" + assert dataSet.name == "aParatooForm 1 - ${DateUtil.formatAsDisplayDateTime(collectionId.eventTime)} (Project 1)" - [status:'ok'] + [status: 'ok'] } and: @@ -151,32 +169,61 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest", + "version_core": "" + ] + ) + ParatooCollectionId paratooCollectionId = buildCollectionId() + Map dataSet = [dataSetId:'d1', grantId:'g1', surveyId:paratooCollectionId.toMap(), activityId: "123"] + dataSet.surveyId.survey_metadata.orgMintedUUID = orgMintedId + Map expectedDataSetSync = dataSet + [progress: Activity.STARTED] + Map expectedDataSetAsync = dataSet + [progress: Activity.STARTED, startDate: "2023-09-01T00:00:00Z", endDate: "2023-09-01T00:00:00Z", areSpeciesRecorded: false, activityId: '123', siteId: null, format: "Database Table", sizeUnknown: true] + ParatooProject project = new ParatooProject(id: projectId, project: new Project(projectId: projectId, custom: [dataSets: [dataSet]])) + when: Map result = service.submitCollection(collection, project) + waitAll(result.promise) then: - 1 * webService.getJson({it.indexOf('/s1s') >= 0}, null, _, false) >> [data:[], meta:[pagination:[total:0]]] + 1 * webService.doPost(*_) >> [resp: [collections: ["coarse-woody-debris-survey": [uuid: "1", createdAt: "2023-09-01T00:00:00.123Z", start_date_time: "2023-09-01T00:00:00.123Z", end_date_time: "2023-09-01T00:00:00.123Z"]]]] 1 * tokenService.getAuthToken(true) >> Mock(AccessToken) - 1 * projectService.update([custom:[dataSets:[expectedDataSet]]], 'p1', false) >> [status:'ok'] + 1 * projectService.update([custom: [dataSets: [expectedDataSetAsync]]], 'p1', false) >> [status: 'ok'] + 1 * projectService.update([custom: [dataSets: [expectedDataSetSync]]], 'p1', false) >> [status: 'ok'] + 1 * activityService.create(_) >> [activityId: '123'] + 1 * activityService.delete("123", true) >> [status: 'ok'] + 1 * recordService.getAllByActivity('123') >> [] + 1 * settingService.getSetting('paratoo.surveyData.mapping') >> { + (["guid-2": [ + "name" : "coarse woody debris", + "usesPlotLayout": false, + "tags" : ["survey"], + "apiEndpoint" : "coarse-woody-debris-surveys", + "overrides" : [ + "dataModel": null, + "viewModel": null + ] + ]] as JSON).toString() + } + 1 * userService.getCurrentUserDetails() >> [userId: userId] and: - result == [status:'ok'] + result.updateResult == [status: 'ok'] } - + void "The service can create a site from a submitted plot-selection"() { setup: Map data = [ - "plot_label":"CTMAUA2222", - "recommended_location":["lat":-35.2592424,"lng":149.0651439], - "uuid":"lmpisy5p9g896lad4ut", - "comment":"Test"] + "plot_label" : "CTMAUA2222", + "recommended_location": ["lat": -35.2592424, "lng": 149.0651439], + "uuid" : "lmpisy5p9g896lad4ut", + "comments" : "Test"] - Map expected = ['name':'CTMAUA2222', 'description':'CTMAUA2222', publicationStatus:'published', 'externalIds':[new ExternalId(externalId:'lmpisy5p9g896lad4ut', idType:ExternalId.IdType.MONITOR_PLOT_GUID)], 'notes':'Test', 'extent':['geometry':['type':'Point', 'coordinates':[149.0651439, -35.2592424], 'decimalLatitude':-35.2592424, 'decimalLongitude':149.0651439], 'source':'point'], 'projects':[], 'type':'surveyArea'] + Map expected = ['name': 'CTMAUA2222', 'description': 'CTMAUA2222', publicationStatus: 'published', 'externalIds': [new ExternalId(externalId: 'lmpisy5p9g896lad4ut', idType: ExternalId.IdType.MONITOR_PLOT_GUID)], 'notes': 'Test', 'extent': ['geometry': ['type': 'Point', 'coordinates': [149.0651439, -35.2592424], 'decimalLatitude': -35.2592424, 'decimalLongitude': 149.0651439], 'source': 'point'], 'projects': [], 'type': 'surveyArea'] String userId = 'u1' @@ -190,8 +237,8 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest", + "version_core": "" + ] + ) + ParatooCollectionId paratooCollectionId = buildCollectionId("mintCollectionIdBasalAreaPayload","guid-3") + Map dataSet = [dataSetId:'d1', grantId:'g1', surveyId:paratooCollectionId.toMap()] + ParatooProject project = new ParatooProject(id: projectId, project: new Project(projectId: projectId, custom: [dataSets: [dataSet]])) + Map surveyData = readSurveyData('basalAreaDbhReverseLookup') Map site when: Map result = service.submitCollection(collection, project) + waitAll(result.promise) then: - 1 * webService.getJson({it.indexOf('/basal-area-dbh-measure-survey') >= 0}, null, _, false) >> [data:[surveyData], meta:[pagination:[total:0]]] + 1 * webService.doPost(*_) >> [resp: surveyData] 1 * tokenService.getAuthToken(true) >> Mock(AccessToken) - 1 * projectService.update(_, projectId, false) >> [status:'ok'] - 1 * siteService.create(_) >> {site = it[0]; [siteId:'s1']} + 2 * projectService.update(_, projectId, false) >> [status: 'ok'] + 1 * siteService.create(_) >> { site = it[0]; [siteId: 's1'] } + 1 * activityService.create(_) >> [activityId: '123'] + 1 * recordService.getAllByActivity('123') >> [] + 1 * settingService.getSetting('paratoo.surveyData.mapping') >> { + (["guid-3": [ + "name" : "Basal Area - DBH", + "usesPlotLayout": true, + "tags" : ["survey"], + "apiEndpoint" : "basal-area-dbh-measure-surveys", + "overrides" : [ + "dataModel": null, + "viewModel": null + ] + ]] as JSON).toString() + } + 1 * userService.getCurrentUserDetails() >> [userId: userId] and: site.name == "SATFLB0001 - Control (100 x 100)" @@ -254,50 +325,1037 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> [projectArea, plot] - Program program = new Program(programId: "prog1", name:"A program", config:[(ParatooService.PROGRAM_CONFIG_PARATOO_ITEM):true]) - program.save(failOnError:true, flush:true) + Program program = new Program(programId: "prog1", name: "A program", config: [(ParatooService.PROGRAM_CONFIG_PARATOO_ITEM): true]) + program.save(failOnError: true, flush: true) + + Service service = new Service(name: "S1", serviceId: '1', legacyId: 1, outputs: [new ServiceForm(externalId: "guid-2", formName: "aParatooForm", sectionName: null)]) + service.save(failOnError: true, flush: true) + + ActivityForm activityForm = new ActivityForm(name: "aParatooForm 1", type: 'EMSA', category: 'protocol category 1', external: true, + sections: [ + new FormSection(name: "section 1", type: "section", template: [ + dataModel : [ + [ + dataType: "list", + name : "coarse-woody-debris-survey", + columns : [ + [ + dataType: "list", + name : "coarse-woody-debris-survey-observation" + ] + ] + ] + ], + viewModel : [], + relationships: [ + ecodata : ["coarse-woody-debris-survey":["coarse-woody-debris-survey-observation": [:]]], + apiOutput: [:] + ] + ] + ) + ] + ) + activityForm.externalIds = [new ExternalId(externalId: "guid-2", idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID)] + activityForm.save(failOnError: true, flush: true) + + activityForm = new ActivityForm(name: "aParatooForm 2 ", type: 'EMSA', category: 'protocol category 2', external: true, + sections: [ + new FormSection(name: "section 1", type: "section", template: [ + dataModel : [ + [ + dataType: "list", + name : "basal-area-dbh-measure-survey", + columns : [ + [ + dataType: "list", + name : "basal-area-dbh-measure-observation", + columns : [ + [ + dataType: "feature", + name : "location" + ] + ] + ] + ] + ] + ], + viewModel : [], + relationships: [ + ecodata : ["basal-area-dbh-measure-survey":["basal-area-dbh-measure-observation": [:]]], + apiOutput: [:] + ] + ] + ) + ]) + activityForm.externalIds = [new ExternalId(externalId: "guid-3", idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID)] + activityForm.save(failOnError: true, flush: true) + + activityForm = new ActivityForm(name: "aParatooForm 3", type: 'EMSA', category: 'protocol category 3', external: true) + activityForm.externalIds = [new ExternalId(externalId: "guid-4", idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID)] + activityForm.save(failOnError: true, flush: true) + + activityForm = new ActivityForm(name: "aParatooForm 4", type: 'EMSA', category: 'protocol category 4', external: true, + sections: [ + new FormSection(name: "section 1", type: "section", template: [ + dataModel : [ + [ + dataType: "list", + name : "bird-survey", + columns : [ + [ + dataType: "integer", + name : "bird-observation" + ] + ] + ] + ], + viewModel : [], + relationships: [ + ecodata : ["bird-survey": ["bird-observation": [:]]], + apiOutput: ["bird-survey.bird-observation": ["bird-observation": [:]]] + ] + ] + ) + ] + ) + activityForm.externalIds = [new ExternalId(externalId: "guid-4", idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID)] + activityForm.save(failOnError: true, flush: true) + + } + + void "capitalizeModelName should convert hyphenated name to capitalised name"() { + when: + String result = service.capitalizeModelName("paratoo-protocol") + + then: + result == "ParatooProtocol" + + when: + result = service.capitalizeModelName("paratoo-protocol-id") + + then: + result == "ParatooProtocolId" - Service service = new Service(name:"S1", serviceId:'1', legacyId: 1, outputs:[new ServiceForm(externalId:"guid-2", formName:"aParatooForm", sectionName:null)]) - service.save(failOnError:true, flush:true) + when: + result = service.capitalizeModelName(null) - ActivityForm activityForm = new ActivityForm(name:"aParatooForm 1", type:'EMSA', category:'protocol category 1', external: true) - activityForm.externalIds = [new ExternalId(externalId: "guid-2", idType:ExternalId.IdType.MONITOR_PROTOCOL_GUID)] - activityForm.save(failOnError:true, flush:true) + then: + result == null - activityForm = new ActivityForm(name:"aParatooForm 2 ", type:'EMSA', category:'protocol category 2', external: true) - activityForm.externalIds = [new ExternalId(externalId: "guid-3", idType:ExternalId.IdType.MONITOR_PROTOCOL_GUID)] - activityForm.save(failOnError:true, flush:true) + when: + result = service.capitalizeModelName("") - activityForm = new ActivityForm(name:"aParatooForm 3", type:'EMSA', category:'protocol category 3', external: true) - activityForm.externalIds = [new ExternalId(externalId: "guid-4", idType:ExternalId.IdType.MONITOR_PROTOCOL_GUID)] - activityForm.save(failOnError:true, flush:true) + then: + result == "" } + + void "transformSpeciesName should convert paratoo species name to object correctly"() { + when: + Map result = service.transformSpeciesName("Acacia glauca [Species] (scientific: Acacia glauca Willd.)") + String outputSpeciesId = result.remove("outputSpeciesId") + then: + outputSpeciesId != null + result == [name: "Acacia glauca Willd.", scientificName: "Acacia glauca Willd.", guid: "A_GUID", commonName: "Acacia glauca", taxonRank: "Species"] + 2 * metadataService.autoPopulateSpeciesData(_) >> null + + when: // no scientific name + result = service.transformSpeciesName("Frogs [Class] (scientific: )") + outputSpeciesId = result.remove("outputSpeciesId") + + then: + outputSpeciesId != null + result == [name: "Frogs", scientificName: "Frogs", guid: "A_GUID", commonName: "Frogs", taxonRank: "Class"] + 2 * metadataService.autoPopulateSpeciesData(_) >> null + } + + void "buildRelationshipTree should build relationship tree correctly"() { + given: + def properties = [ + "bird-observation" : [ + "type" : "integer", + "x-model-ref": "bird-survey" + ], + "bird-survey" : [ + "type" : "integer", + "x-model-ref": "plot-visit" + ], + "fauna-survey" : [ + "type" : "integer", + "x-model-ref": "plot-visit" + ], + "fauna-observation": [ + "type" : "integer", + "x-model-ref": "fauna-survey" + ], + "plot-visit" : [ + "type" : "integer", + "x-model-ref": "plot-layout" + ], + "plot-layout" : [ + "type" : "integer", + "x-model-ref": "plot-selection" + ], + "plot-selection" : [ + "type" : "object", + "properties": [ + "column": [ + "type": "string" + ] + ] + ] + ] + + + when: + def relationships = service.buildParentChildRelationship(properties) + + then: + relationships.size() == 2 + relationships["bird-survey"] != null + relationships["bird-survey"].size() == 1 + relationships["bird-survey"].contains("bird-observation") + relationships["fauna-survey"].size() == 1 + relationships["fauna-survey"].contains("fauna-observation") + } + + void "buildTreeFromParentChildRelationships should build tree correctly"() { + given: + def relationships = [ + "plot-layout" : ["plot-visit"], + "bird-survey" : ["bird-observation"], + "plot-visit" : ["bird-survey", "fauna-survey"], + "plot-selection": ["plot-layout"], + "fauna-survey" : ["fauna-observation"] + ] + + when: + def tree = service.buildTreeFrom2DRelationship(relationships) + + then: + tree.size() == 1 + tree["bird-survey"] == null + tree["plot-selection"].size() != 0 + tree["plot-selection"]["plot-layout"]["plot-visit"]["bird-survey"]["bird-observation"] != null + } + + void "findPathFromRelationship should find path to model from relationship"() { + setup: + Map relationship = ["aerial-observation": [survey: ["aerial-survey": [setup_ID: ["aerial-setup-desktop": [survey: [:]]]]]], "aerial-survey": [survey: [:]], "aerial-setup": [survey: ["aerial-survey": [setup_ID: ["aerial-setup-desktop": [survey: [:]]]]]]] + + when: + List paths = service.findPathFromRelationship("aerial-setup-desktop", relationship) + + then: + paths.size() == 2 + paths[0] == "aerial-observation.survey.aerial-survey.setup_ID" + paths[1] == "aerial-setup.survey.aerial-survey.setup_ID" + } + + void "removeProperty should remove property from nested object"() { + setup: "nested map" + def object = [a: 1, b: [d: [e: [f: ["toRemove"]]]], c: 3] + + when: + service.removeProperty(object, "b.d.e.f") + + then: + object.size() == 3 + object.b.d.e.f == null + + when: "nested list" + object = [a: 1, b: [[d: [[e: [[f: ["toRemove"]]]]]]], c: 3] + service.removeProperty(object, "b.d.e.f") + + then: + object.size() == 3 + object.b[0].d[0].e[0].f == null + + } + + void "rearrangePropertiesAccordingToModelRelationship should rearrange properties according to model relationship"() { + setup: + def relationship = '{\n' + + ' "ecodata": {\n' + + ' "vegetation-mapping-survey": {\n' + + ' "vegetation-mapping-observation": {}\n' + + ' }\n' + + ' },\n' + + ' "apiOutput": {\n' + + ' "vegetation-mapping-observation.properties.vegetation_mapping_survey": {\n' + + ' "vegetation-mapping-survey": {}\n' + + ' }\n' + + ' }\n' + + ' }' + ObjectMapper mapper = new ObjectMapper() + relationship = mapper.readValue(relationship, Map.class) + + def output = """ +{ + "vegetation-mapping-observation": { + "type": "object", + "properties": { + "vegetation_mapping_survey": { + "type": "object", + "properties": { + "test": { + "type": "string" + } + } + }, + "observation": { + "type": "object", + "properties": { + "survey": { + "type": "object", + "properties": {} + } + } + } + } + } +} +""" + + output = mapper.readValue(output, Map.class) + + when: + output = service.rearrangePropertiesAccordingToModelRelationship(output, relationship.apiOutput, relationship.ecodata) + + then: + output.size() == 2 + output["properties"]["vegetation-mapping-survey"]["properties"].size() == 2 + output["properties"]["vegetation-mapping-survey"]["properties"]["vegetation-mapping-observation"]["properties"].size() == 1 + output["properties"]["vegetation-mapping-survey"]["properties"]["test"] == ["type": "string"] + output["properties"]["vegetation-mapping-survey"]["properties"]["vegetation-mapping-observation"]["properties"]["observation"]["properties"]["survey"]["properties"].size() == 0 + } + + void "cleanSwaggerDefinition should standardize the swagger definition"() { + setup: + def definition = [ + "type" : "object", + "required" : ["data"], + "properties" : [ + "data": [ + "required" : ["transect_number", "date_time"], + "type" : "object", + "properties": [ + "plot" : [ + "type" : "integer", + "x-model-ref": "setup" + ], + "number" : ["type": "string"], + "photo" : [ + "type" : "integer", + "x-paratoo-file-type": ["images"], + "x-model-ref" : "file" + ], + "quad" : [ + "type" : "array", + "items": [ + "type" : "integer", + "x-model-ref": "quadrant" + ], + ], + "date_time": ["type": "string", "format": "date-time"] + ] + ] + ], + "additionalProperties": false + ] + + when: + def result = service.cleanSwaggerDefinition(definition) + + then: + result.size() == 3 + + result.properties.size() == 5 + result.properties.plot.size() == 2 + result.properties.number.type == "string" + result.properties.quad.properties.type == "integer" + result.required.size() == 2 + result.required[0] == "transect_number" + } + + void "buildChildParentRelationship should build child parent relationship from simplified data model"() { + setup: + def output = ' {\n' + + ' "type" : "object",\n' + + ' "properties" : {\n' + + ' "plot" : {\n' + + ' "type" : "integer",\n' + + ' "x-model-ref" : "setup"\n' + + ' },\n' + + ' "number" : {\n' + + ' "type" : "string"\n' + + ' },\n' + + ' "photo" : {\n' + + ' "type" : "integer",\n' + + ' "x-paratoo-file-type" : [ "images" ],\n' + + ' "x-model-ref" : "file"\n' + + ' },\n' + + ' "quad" : {\n' + + ' "type" : "array",\n' + + ' "properties" : {\n' + + ' "type" : "integer",\n' + + ' "x-model-ref" : "quadrant"\n' + + ' }\n' + + ' },\n' + + ' "date_time" : {\n' + + ' "type" : "string",\n' + + ' "format" : "date-time"\n' + + ' }\n' + + ' },\n' + + ' "required" : [ "transect_number", "date_time" ]\n' + + '}' + ObjectMapper mapper = new ObjectMapper() + def input = mapper.readValue(output, Map.class) + + when: + def result = service.buildChildParentRelationship(input) + + then: + result.size() == 2 + result["properties.plot"] == ["setup"] + result["properties.quad"] == ["quadrant"] + } + + void "buildParentChildRelationship should build parent child relationship from simplified data model"() { + setup: + def output = ' {\n' + + ' "type" : "object",\n' + + ' "properties" : {\n' + + ' "plot" : {\n' + + ' "type" : "integer",\n' + + ' "x-model-ref" : "setup"\n' + + ' },\n' + + ' "number" : {\n' + + ' "type" : "string"\n' + + ' },\n' + + ' "photo" : {\n' + + ' "type" : "integer",\n' + + ' "x-paratoo-file-type" : [ "images" ],\n' + + ' "x-model-ref" : "file"\n' + + ' },\n' + + ' "quad" : {\n' + + ' "type" : "array",\n' + + ' "properties" : {\n' + + ' "type" : "integer",\n' + + ' "x-model-ref" : "quadrant"\n' + + ' }\n' + + ' },\n' + + ' "date_time" : {\n' + + ' "type" : "string",\n' + + ' "format" : "date-time"\n' + + ' }\n' + + ' },\n' + + ' "required" : [ "transect_number", "date_time" ]\n' + + '}' + ObjectMapper mapper = new ObjectMapper() + def input = mapper.readValue(output, Map.class) + + when: + def result = service.buildParentChildRelationship(input) + + then: + result.size() == 2 + result["setup"] == ["plot"] + result["quadrant"] == ["quad"] + } + + void "rearrangeSurveyData should reorder api output according to provided relationship"() { + setup: + def mapper = new ObjectMapper() + def output = '{"a": {}, "f": 4, "b": {"c": 1, "d": {}}, "e": {"g": 3}}' + output = mapper.readValue(output, Map.class) + def relationship = '{"d": {"b": {"e": {"a": {} } }}, "c": {} }' + relationship = mapper.readValue(relationship, Map.class) + def apiOutputRelationship = '{ "e": {"e": {} }, "a": "a", "b.c": {"c": {}}, "b.d": {"d": {}} }' + apiOutputRelationship = mapper.readValue(apiOutputRelationship, Map.class) + when: + def result = service.rearrangeSurveyData(output, output, relationship, apiOutputRelationship) + then: + result.size() == 3 + result == ["f": 4, "d": ["b": ["e": ["g": 3, "a": [:]]]], "c": 1] + } + + void "resolveModelReferences should swap model with definitions"() { + setup: + def dataModel = [ + "type" : "object", + "properties": [ + "plot" : [ + "type" : "integer", + "x-model-ref": "setup" + ], + "number" : ["type": "string"], + "photo" : [ + "type" : "integer", + "x-paratoo-file-type": ["images"], + "x-model-ref" : "file" + ], + "quad" : [ + "type" : "array", + "properties": [ + "type" : "integer", + "x-model-ref": "quadrant" + ], + ], + "date_time": ["type": "string", "format": "date-time"] + ] + ] + + def components = [ + "SetupRequest" : [ + "type" : "object", + "properties": [ + "transect_number": ["type": "string"], + "date_time" : ["type": "string", "format": "date-time"] + ] + ], + "QuadrantRequest": [ + "type" : "object", + "properties": [ + "transect": ["type": "string"], + "date" : ["type": "string", "format": "date"] + ] + ] + ] + + when: + def result = service.resolveModelReferences(dataModel, components) + + then: + result.size() == 2 + result.properties.plot.properties.transect_number.type == "string" + result.properties.plot.properties.date_time.format == "date-time" + result.properties.quad.properties.properties.date.format == "date" + result.properties.quad.properties.properties.transect.type == "string" + result.properties.photo.type == "integer" + result.properties.photo["x-model-ref"] == "file" + } + + void "isPlotLayoutNeededByProtocol checks if plot is required by protocol"() { + given: + def protocol1 = [attributes: [workflow: [[modelName: 'plot-layout'], [modelName: 'other-model']]]] + def expected1 = true + + when: + def result = service.isPlotLayoutNeededByProtocol(protocol1) + + then: + result == expected1 + + when: + def protocol2 = [attributes: [workflow: [[modelName: 'other-model'], [modelName: 'another-model']]]] + def expected2 = false + result = service.isPlotLayoutNeededByProtocol(protocol2) + + then: + result == expected2 + } + + void "findProtocolEndpointDefinition should find the protocol endpoint definition"() { + given: + def protocol = [attributes: [endpointPrefix: "opportunes", workflow: [[modelName: 'plot-layout'], [modelName: 'other-model']]]] + def documentation = [ + paths: [ + "opportunes/bulk": [ + post: [ + requestBody: + [ + content: [ + "application/json": [ + schema: [ + "properties": [ + "data": [ + "properties": [ + "collections": [ + "items": [ + "properties": [ + "surveyId": [ + "properties": [ + "projectId": [ + "type": "string" + ], + "protocol" : [ + "properties": [ + "id" : [ + "type": "string" + ], + "version": [ + "type": "integer" + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + + when: + def result = service.findProtocolEndpointDefinition(protocol, documentation) + + then: + result != null + result.surveyId != null + + when: // no endpoint path is found should return null + documentation = [ + paths: [ + "test/bulk": [ + post: [ + requestBody: + [ + content: [ + "application/json": [ + schema: [ + "properties": [ + "data": [ + "properties": [ + "collections": [ + "items": [ + "properties": [ + "surveyId": [ + "properties": [ + "projectId": [ + "type": "string" + ], + "protocol" : [ + "properties": [ + "id" : [ + "type": "string" + ], + "version": [ + "type": "integer" + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + result = service.findProtocolEndpointDefinition(protocol, documentation) + + then: + result == null + + when: // schema has changed should throw exception + documentation = [ + paths: [ + "opportunes/bulk": [ + post: [ + requestBody: + [ + content: [ + "application/json": [:] + ] + ] + ] + ] + ] + ] + service.findProtocolEndpointDefinition(protocol, documentation) + + then: + thrown(NullPointerException) + } + + void "resolveReferences should resolve \$ref variables in schema"() { + given: + def schema = [ + "plot-layout" : [ + "\$ref": "#/components/schemas/PlotLayoutRequest" + ], + "basal-area-dbh-measure-observation": [ + "type" : "array", + "items": [ + "\$ref": "#/components/schemas/BasalAreaDbhMeasureObservationRequest" + ] + ] + ] + def components = [ + "PlotLayoutRequest" : [ + "type" : "object", + "properties": [ + "transect_number": ["type": "string"], + "date_time" : ["type": "string", "format": "date-time"] + ] + ], + "BasalAreaDbhMeasureObservationRequest": [ + "type" : "object", + "properties": [ + "transect": ["type": "string"], + "date" : ["type": "string", "format": "date"] + ] + ] + ] + + when: + def result = service.resolveReferences(schema, components) + + then: + result.size() == 2 + result["plot-layout"].size() == 2 + result["plot-layout"].properties.transect_number.type == "string" + result["plot-layout"].properties.date_time.format == "date-time" + result["basal-area-dbh-measure-observation"].size() == 2 + result["basal-area-dbh-measure-observation"].items.properties.date.format == "date" + result["basal-area-dbh-measure-observation"].items.properties.transect.type == "string" + } + + void "convertToDataModelAndViewModel should return correct data model"() { + when:// when integer type is provided + String name = "height" + def component = [ + "type" : "integer", + "x-paratoo-unit": "m", + "x-paratoo-hint": "height" + ] + def config = new ParatooProtocolConfig( + "name" : "Opportune", + "usesPlotLayout": false, + "tags" : ["survey"], + "apiEndpoint" : "s1s", + "overrides" : [ + "dataModel": null, + "viewModel": null + ] + ) + def result = service.convertToDataModelAndViewModel(component, [:], name, null, null, 0, "", config) + service.cacheService.clear() + then: + result.dataModel[0] == [ + "dataType" : "number", + "units" : "m", + "name" : "height", + "description" : "height", + "decimalPlaces": 0 + ] + result.viewModel[0] == [ + "type" : "number", + "source" : "height", + "preLabel": "Height" + ] + when:// when number type is provided + component = [ + "type" : "number", + "x-paratoo-unit": "m", + "x-paratoo-hint": "height" + ] + result = service.convertToDataModelAndViewModel(component, [:], name, null, null, 0, "", config) + then: + result.dataModel[0] == [ + "dataType" : "number", + "units" : "m", + "name" : "height", + "description" : "height", + "decimalPlaces": 6 + ] + result.viewModel[0] == [ + "type" : "number", + "source" : "height", + "preLabel": "Height" + ] + + when:// when string type is provided + service.cacheService.clear() + component = [ + "type" : "string", + "x-paratoo-unit": "m", + "x-paratoo-hint": "height", + "x-lut-ref" : "lut1" + ] + result = service.convertToDataModelAndViewModel(component, [:], name, null, null, 0, "", config) + then: + 1 * webService.getJson( {it.indexOf('/lut1s') >= 0}, _, _, _) >> [data: [ + [ + attributes: [ + "symbol": "1", + "label" : "one" + ] + ], + [ + attributes: [ + "symbol": "2", + "label" : "two" + ] + ] + ], meta : [pagination: [total: 0]]] + 1 * webService.getJson({ it.indexOf('/documentation/swagger.json') >= 0 }, _, _, _) >> [ + paths: ["/lut1s": [:]] + ] + result.dataModel[0] == [ + "dataType" : "text", + "units" : "m", + "name" : "height", + "description" : "height", + "constraints" : [ + "textProperty" : "label", + "type" : "literal", + "valueProperty": "value", + "literal" : [ + [ + label: "one", + value: "1" + ], + [ + label: "two", + value: "2" + ] + ] + ], + "displayOptions": [ + "placeholder": "Select an option", + "tags" : true + ], + "x-lut-ref":"lut1" + ] + result.viewModel[0] == [ + "type" : "selectOne", + "source" : "height", + "preLabel": "Height" + ] + } + + def "isLocationObject should identify object with 'location' name in component name" (Map input, boolean expected) { + when: + boolean result = service.isLocationObject(input) + + then: + result == expected + + where: + input | expected + [(service.PARATOO_COMPONENT): "location.location"] | true + [(service.PARATOO_COMPONENT): "location.plot-location-point"] | true + [(service.PARATOO_COMPONENT): "location.fauna-plot-points"] | true + [(service.PARATOO_COMPONENT): "location.vegetation-association-nvis"] | false + [(service.PARATOO_COMPONENT): "location-observation-3"] | false + } + + def "cleanSwaggerDefinition should clean and standardise given definitions" () { + given: + def definition = getNormalDefinition() + when: + def result = service.cleanSwaggerDefinition(definition.input) + then: + result == definition.output + } + + def "recursivelyTransformData should transform species data"() { + + def dataModel = [ + [ + "dataType" : "species", + "name" : "lut", + "x-lut-ref" : "lut1", + ] + ] + def output = [ + lut: [ + "id": 8, + "symbol": "Cat", + "label": "Cat", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:32.116Z", + "updatedAt": "2024-03-26T02:39:32.116Z" + ] + ] + String formName = "form name" + + when: + def result = service.recursivelyTransformData(dataModel, output, formName, 1, null) + result.lut.remove('outputSpeciesId') + + then: + result == [ + lut: [ + commonName: "Cat", + name: "Cat", + taxonRank: null, + scientificName: "Cat", + guid: "A_GUID" + ] + ] + + when: + dataModel = [ + [ + "dataType" : "species", + "name" : "lut" + ] + ] + output = [ + lut: "Cat" + ] + result = service.recursivelyTransformData(dataModel, output, formName, 1, null) + result.lut.remove('outputSpeciesId') + then: + result == [ + lut: [ + commonName: "Cat", + name: "Cat", + taxonRank: null, + scientificName: "Cat", + guid: "A_GUID" + ] + ] + } + + def "recursivelyTransformData should transform feature object based on provided protocol config"() { + given: + def dataModel = [ + [ + "dataType" : "feature", + "name" : "line" + ] + ] + def output = [ + line: [[lat: 1, lng: 2], [lat: 3, lng: 4]] + ] + String formName = "form name" + ParatooProtocolConfig config = new ParatooProtocolConfig(geometryType: "LineString") + + when: + def result = service.recursivelyTransformData(dataModel, output, formName, 1, config) + + then: + result == [ + line: [ + type: "Feature", + geometry: [ + type: "LineString", + coordinates: [[2, 1], [4, 3]] + ], + properties: [ + name: "LineString form name-1", + description: "LineString form name-1", + externalId: null, + notes: "LineString form name-1" + ] + ] + ] + + when: + output = [ + line: [[lat: 1, lng: 2], [lat: 3, lng: 4]] + ] + config = new ParatooProtocolConfig(geometryType: "Polygon") + result = service.recursivelyTransformData(dataModel, output, formName, 1, config) + + then: + result == [ + line: [ + type: "Feature", + geometry: [ + type: "Polygon", + coordinates: [[[2, 1], [4, 3], [2, 1]]] + ], + properties: [ + name: "Polygon form name-1", + description: "Polygon form name-1", + externalId: null, + notes: "Polygon form name-1" + ] + ] + ] + } + + private Map getNormalDefinition() { + def input = """ +{ + "plot_points": { + "type": "array", + "items": { + "properties": { + "lat": { + "type": "number", + "format": "float" + }, + "lng": { + "type": "number", + "format": "float" + }, + "name": { + "type": "string", + "format": "string" + } + }, + "type": "object", + "x-paratoo-component": "location.plot-location-point" + }, + "maxItems": 25 + } +} +""" + Map inputObject = getGroovyObject(input) + Map output = ["plot_points": [type: "object", properties: ["lat": [type: "number", format: "float"], "lng": [type: "number", format: "float"], "name": [type: "string", format: "string"]], "x-paratoo-component": "location.plot-location-point"]] + [input: inputObject, output: output] + } + + private getGroovyObject(String input, Class clazz = Map.class){ + ObjectMapper mapper = new ObjectMapper() + mapper.readValue(input, clazz) + } + } diff --git a/src/test/groovy/au/org/ala/ecodata/SpeciesReMatchServiceSpec.groovy b/src/test/groovy/au/org/ala/ecodata/SpeciesReMatchServiceSpec.groovy index 70bcb4af8..c848689b5 100644 --- a/src/test/groovy/au/org/ala/ecodata/SpeciesReMatchServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/SpeciesReMatchServiceSpec.groovy @@ -10,7 +10,7 @@ class SpeciesReMatchServiceSpec extends Specification implements ServiceUnitTest void setup() { service.cacheService = new CacheService() service.webService = Mock(WebService) - grailsApplication.config.bie.url = "http://localhost:8080" + grailsApplication.config.bie.url = "http://localhost:8080/" } void "test searchBie"() { diff --git a/src/test/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfigSpec.groovy b/src/test/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfigSpec.groovy index 5ee59f757..dbfbf87fa 100644 --- a/src/test/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfigSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfigSpec.groovy @@ -1,6 +1,10 @@ package au.org.ala.ecodata.paratoo - +import au.org.ala.ecodata.ActivityForm +import au.org.ala.ecodata.DateUtil +import au.org.ala.ecodata.ExternalId +import au.org.ala.ecodata.FormSection +import au.org.ala.ecodata.ParatooService import grails.converters.JSON import groovy.json.JsonSlurper import org.grails.web.converters.marshaller.json.CollectionMarshaller @@ -8,10 +12,12 @@ import org.grails.web.converters.marshaller.json.MapMarshaller import spock.lang.Specification class ParatooProtocolConfigSpec extends Specification { + ParatooService paratooService def setup() { JSON.registerObjectMarshaller(new MapMarshaller()) JSON.registerObjectMarshaller(new CollectionMarshaller()) + paratooService = new ParatooService() } private Map readSurveyData(String name) { @@ -22,61 +28,286 @@ class ParatooProtocolConfigSpec extends Specification { def "The vegetation-mapping-survey can be used with this config"() { setup: - Map surveyData = readSurveyData('vegetationMappingSurvey') + Map surveyData = readSurveyData('vegetationMappingObservationReverseLookup') + Map observation = surveyData?.collections Map vegetationMappingConfig = [ - apiEndpoint:'vegetation-mapping-observations', - usesPlotLayout:false, - geometryType:'Point', - geometryPath:'attributes.position', - startDatePath:'attributes.vegetation_mapping_survey.data.attributes.start_date_time', - endDatePath: null + apiEndpoint : 'vegetation-mapping-surveys', + usesPlotLayout : false, + geometryType : 'Point' ] ParatooProtocolConfig config = new ParatooProtocolConfig(vegetationMappingConfig) + config.surveyId = ParatooCollectionId.fromMap([survey_metadata: surveyData.survey_metadata]) + ActivityForm activityForm = new ActivityForm( + name: "aParatooForm 1", + type: 'EMSA', + category: 'protocol category 1', + external: true, + sections: [ + [ + name : "section 1", + template: [ + dataModel : [ + [ + name : "vegetation-mapping-survey", + dataType: "list", + columns : [ - expect: - config.getStartDate(surveyData) == '2023-09-08T23:39:00Z' - config.getEndDate(surveyData) == null - config.getGeoJson(surveyData) == [type:'Point', coordinates:[149.0651536, -35.2592398]] + ] + ], + [ + name : "vegetation-mapping-observation", + dataType: "list", + columns : [ + [ + dataType: "feature", + name : "position" + ] + ] + ] + ], + relationships: [ + ecodata : [:], + apiOutput: [:] + ] + ] + ] + ] + ) + activityForm.externalIds = [new ExternalId(externalId: "guid-3", idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID)] + + when: + transformData(observation, activityForm, config) + + then: + config.getStartDate(observation) == '2023-09-08T23:39:00Z' + config.getEndDate(observation) == null + config.getGeoJson(observation, activityForm).features == [ + [ + type : "Feature", + geometry : [type: 'Point', coordinates: [149.0651536, -35.2592398]], + properties: [ + name : "Point aParatooForm 1-1", + externalId: 44, + id : "aParatooForm 1-1" + ] + ] + ] } def "The floristics-standard survey can be used with this config"() { setup: - Map surveyData = readSurveyData('floristicsStandard') + Map apiOutput = readSurveyData('floristicsStandardReverseLookup') + Map observation = apiOutput.collections Map floristicsSurveyConfig = [ apiEndpoint:'floristics-veg-survey-lites', usesPlotLayout:true, - startDatePath: 'attributes.start_date_time', - endDatePath: 'attributes.end_date_time', - surveyIdPath: 'attributes.surveyId' + startDatePath: 'plot_visit.start_date', + endDatePath: 'plot_visit.end_date' ] ParatooProtocolConfig config = new ParatooProtocolConfig(floristicsSurveyConfig) + config.setSurveyId(ParatooCollectionId.fromMap([survey_metadata: apiOutput.survey_metadata])) + ActivityForm activityForm = new ActivityForm( + name: "aParatooForm 1", + type: 'EMSA', + category: 'protocol category 1', + external: true, + sections: [ + [ + name : "section 1", + template: [ + dataModel : [[ + name : "floristics-veg-survey-lite", + dataType: "list", + columns : [] + ]], + relationships: [ + ecodata : [:], + apiOutput: [:] + ] + ] + ] + ] + ) - expect: - config.getStartDate(surveyData) == '2022-09-21T01:55:44Z' - config.getEndDate(surveyData) == "2022-09-21T01:55:44Z" - config.getGeoJson(surveyData) == [type:"Feature", geometry:[type:"Polygon", coordinates:[[[152.880694, -27.388252], [152.880651, -27.388336], [152.880518, -27.388483], [152.880389, -27.388611], [152.88028, -27.388749], [152.880154, -27.388903], [152.880835, -27.389463], [152.880644, -27.389366], [152.880525, -27.389248], [152.88035, -27.389158], [152.880195, -27.389021], [152.880195, -27.389373], [152.880797, -27.388316], [152.881448, -27.388909], [152.881503, -27.388821], [152.881422, -27.388766], [152.881263, -27.388644], [152.881107, -27.388549], [152.880939, -27.388445], [152.881314, -27.389035], [152.88122, -27.389208], [152.881089, -27.389343], [152.880973, -27.389472], [152.880916, -27.389553], [152.880694, -27.388252]]]], properties:[name:"QDASEQ0001 - Control (100 x 100)", externalId:1, description:"QDASEQ0001 - Control (100 x 100)", notes:"some comment"]] + + when: + transformData(observation, activityForm, config) + + then: + config.getStartDate(observation) == "2024-04-08T01:23:28Z" + config.getEndDate(observation) == "2024-04-10T01:23:28Z" + config.getGeoJson(observation) == [type: "Feature", geometry: [type: "Polygon", coordinates: [[[152.880694, -27.388252], [152.880651, -27.388336], [152.880518, -27.388483], [152.880389, -27.388611], [152.88028, -27.388749], [152.880154, -27.388903], [152.880835, -27.389463], [152.880644, -27.389366], [152.880525, -27.389248], [152.88035, -27.389158], [152.880195, -27.389021], [152.880195, -27.389373], [152.880797, -27.388316], [152.881448, -27.388909], [152.881503, -27.388821], [152.881422, -27.388766], [152.881263, -27.388644], [152.881107, -27.388549], [152.880939, -27.388445], [152.881314, -27.389035], [152.88122, -27.389208], [152.881089, -27.389343], [152.880973, -27.389472], [152.880916, -27.389553], [152.880694, -27.388252]]]], properties: [name: "QDASEQ0001 - Control (100 x 100)", externalId: 1, description: "QDASEQ0001 - Control (100 x 100)", notes: "some comment"]] } def "The basal-area-dbh-measure-survey can be used with this config"() { setup: - Map surveyData = readSurveyData('basalAreaDbh') + Map surveyData = readSurveyData('basalAreaDbhReverseLookup') Map basalAreaDbhMeasureSurveyConfig = [ apiEndpoint:'basal-area-dbh-measure-surveys', usesPlotLayout:true, - startDatePath: 'attributes.start_date', - endDatePath: 'attributes.start_date', + startDatePath: 'start_date', + endDatePath: 'start_date', ] ParatooProtocolConfig config = new ParatooProtocolConfig(basalAreaDbhMeasureSurveyConfig) + config.setSurveyId(ParatooCollectionId.fromMap([survey_metadata: surveyData.survey_metadata])) + Map observation = surveyData?.collections + ActivityForm activityForm = new ActivityForm(name: "aParatooForm 2", type: 'EMSA', category: 'protocol category 2', external: true, + sections: [ + new FormSection(name: "section 1", type: "section", template: [ + dataModel : [ + [ + dataType: "list", + name : "basal-area-dbh-measure-survey", + columns : [ + + ] + ], + [ + dataType: "list", + name : "basal-area-dbh-measure-observation", + columns : [ + [ + dataType: "feature", + name : "location" + ] + ] + ] + ], + viewModel : [], + relationships: [ + ecodata : [:], + apiOutput: [:] + ] + ] + ) + ]) + activityForm.externalIds = [new ExternalId(externalId: "guid-3", idType: ExternalId.IdType.MONITOR_PROTOCOL_GUID)] + transformData(observation, activityForm, config) expect: - config.getStartDate(surveyData) == '2023-09-22T00:59:47Z' - config.getEndDate(surveyData) == "2023-09-22T00:59:47Z" - config.getGeoJson(surveyData) == [ - type:"Feature", - geometry:[ - type:"Polygon", - coordinates:[[[138.63720760798054, -34.97222197296049], [138.63720760798054, -34.97204230990367], [138.63720760798054, -34.971862646846844], [138.63720760798054, -34.97168298379002], [138.63720760798054, -34.9715033207332], [138.63720760798054, -34.971413489204785], [138.63731723494544, -34.971413489204785], [138.6375364888752, -34.971413489204785], [138.63775574280498, -34.971413489204785], [138.63797499673475, -34.971413489204785], [138.63819425066453, -34.971413489204785], [138.63830387762943, -34.971413489204785], [138.63830387762943, -34.9715033207332], [138.63830387762943, -34.97168298379002], [138.63830387762943, -34.971862646846844], [138.63830387762943, -34.97204230990367], [138.63830387762943, -34.97222197296049], [138.63830387762943, -34.9723118044889], [138.63819425066453, -34.9723118044889], [138.63797499673475, -34.9723118044889], [138.63775574280498, -34.9723118044889], [138.6375364888752, -34.9723118044889], [138.63731723494544, -34.9723118044889], [138.63720760798054, -34.9723118044889], [138.63720760798054, -34.97222197296049]]]], - properties:["name":"SATFLB0001 - Control (100 x 100)", externalId:4, description:"SATFLB0001 - Control (100 x 100)", notes:"some comment"]] + config.getStartDate(observation) == "2024-03-28T03:17:01Z" + config.getEndDate(observation) == "2024-03-28T03:17:01Z" + config.getGeoJson(observation) == [ + type : "Feature", + geometry : [ + type : "Polygon", + coordinates: [[[138.63720760798054, -34.97222197296049], [138.63720760798054, -34.97204230990367], [138.63720760798054, -34.971862646846844], [138.63720760798054, -34.97168298379002], [138.63720760798054, -34.9715033207332], [138.63720760798054, -34.971413489204785], [138.63731723494544, -34.971413489204785], [138.6375364888752, -34.971413489204785], [138.63775574280498, -34.971413489204785], [138.63797499673475, -34.971413489204785], [138.63819425066453, -34.971413489204785], [138.63830387762943, -34.971413489204785], [138.63830387762943, -34.9715033207332], [138.63830387762943, -34.97168298379002], [138.63830387762943, -34.971862646846844], [138.63830387762943, -34.97204230990367], [138.63830387762943, -34.97222197296049], [138.63830387762943, -34.9723118044889], [138.63819425066453, -34.9723118044889], [138.63797499673475, -34.9723118044889], [138.63775574280498, -34.9723118044889], [138.6375364888752, -34.9723118044889], [138.63731723494544, -34.9723118044889], [138.63720760798054, -34.9723118044889], [138.63720760798054, -34.97222197296049]]]], + properties: ["name": "SATFLB0001 - Control (100 x 100)", externalId: 2, description: "SATFLB0001 - Control (100 x 100)", notes: "some comment"] + ] + config.getGeoJson(observation, activityForm).features == [[ + type : "Feature", + geometry : [ + type : "Point", + coordinates: [149.0651491, -35.2592444] + ], + properties: ["name": "Point aParatooForm 2-1", externalId: 37, id: "aParatooForm 2-1"] + ]] + } + + def "The observations from opportunistic-survey can be filtered"() { + setup: + Map response = readSurveyData('opportunisticSurveyObservationsReverseLookup') + Map surveyObservations = response?.collections + Map opportunisticSurveyConfig = [ + apiEndpoint : 'opportunistic-surveys', + usesPlotLayout: false, + geometryType : 'Point', + startDatePath : 'start_date_time', + endDatePath : 'end_date_time' + ] + ActivityForm activityForm = new ActivityForm( + name: "aParatooForm 1", + type: 'EMSA', + category: 'protocol category 1', + external: true, + sections: [ + [ + name : "section 1", + template: [ + dataModel : [ + [ + name : "opportunistic-survey", + dataType: "list", + columns: [] + ], + [ + name : "opportunistic-observation", + dataType: "list", + columns : [ + [ + name : "location", + dataType: "feature", + required: true, + external: true + ] + ] + ] + ], + relationships: [ + ecodata : [:], + apiOutput: [:] + ] + ] + ] + ] + ) + + when: + ParatooProtocolConfig config = new ParatooProtocolConfig(opportunisticSurveyConfig) + config.clientTimeZone = TimeZone.getTimeZone("Australia/Sydney") + config.surveyId = ParatooCollectionId.fromMap([survey_metadata: response.survey_metadata]) + transformData(surveyObservations, activityForm, config) + String start_date = config.getStartDate(surveyObservations) + String startDateInDefaultTimeZone = DateUtil.convertUTCDateToStringInTimeZone(start_date, config.clientTimeZone) + String end_date = config.getEndDate(surveyObservations) + Map geoJson = config.getGeoJson(surveyObservations, activityForm) + + then: + start_date == "2024-04-03T03:37:54Z" + end_date == "2024-04-03T03:39:40Z" + geoJson == [ + type : "Feature", + geometry : [type: "Point", coordinates: [138.63, -35.0005]], + features : [[type: "Feature", geometry: [type: "Point", coordinates: [138.63, -35.0005]], properties:[name:"Point aParatooForm 1-1", externalId:40, id:"aParatooForm 1-1"]]], + properties: [ + name : "aParatooForm 1 site - ${startDateInDefaultTimeZone}", + description: "aParatooForm 1 site - ${startDateInDefaultTimeZone} (convex hull of all features)" + ] + ] + } + + def "Should create line GeoJSON objects" () { + when: + def result = ParatooProtocolConfig.toLineStringGeometry([[lat: 1, lng: 2], [lat: 3, lng: 4]]) + + then: + result == [ + type : 'LineString', + coordinates: [[2, 1], [4, 3]] + ] + + when: + result = ParatooProtocolConfig.createLineStringFeatureFromGeoJSON([[lat: 1, lng: 2], [lat: 3, lng: 4], [lat: 5, lng: 6]], "test name", 1, "test notes") + + then: + result == [ + "type" : "Feature", + "geometry" : [ + type : 'LineString', + coordinates: [[2, 1], [4, 3], [6, 5]] + ], + "properties": [ + "name" : "test name", + "externalId" : 1, + "description": "test name", + "notes" : "test notes" + ] + ] + } + def transformData(Map surveyDataAndObservations, ActivityForm form, ParatooProtocolConfig config) { + ParatooService.addPlotDataToObservations(surveyDataAndObservations, config) + paratooService.rearrangeSurveyData(surveyDataAndObservations, surveyDataAndObservations, form.sections[0].template.relationships.ecodata, form.sections[0].template.relationships.apiOutput) + paratooService.recursivelyTransformData(form.sections[0].template.dataModel, surveyDataAndObservations, form.name) } } diff --git a/src/test/resources/paratoo/basalAreaDbh.json b/src/test/resources/paratoo/basalAreaDbh.json deleted file mode 100644 index 39c2c3fa4..000000000 --- a/src/test/resources/paratoo/basalAreaDbh.json +++ /dev/null @@ -1,766 +0,0 @@ -{ - "id": 2, - "attributes": { - "surveyId": { - "time": "2023-09-22T01:03:15.556Z", - "uuid": "43389075", - "protocol": { - "id": "1", - "version": 1 - }, - "projectId": "p1", - "surveyType": "basal-area-dbh-measure-survey" - }, - "start_date": "2023-09-22T00:59:47.807Z", - "createdAt": "2023-09-22T01:05:19.981Z", - "updatedAt": "2023-09-22T01:05:19.981Z", - "plot_size": { - "data": { - "id": 1, - "attributes": { - "symbol": "P40", - "label": "40x40 m", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:43.261Z", - "updatedAt": "2023-09-14T05:59:43.261Z", - "survey_method": { - "data": { - "id": 1, - "attributes": { - "symbol": "T", - "label": "Transects", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/ba3bc90d-ebed-5e4e-a2ef-8e5d4d485fef", - "createdAt": "2023-09-14T05:59:34.972Z", - "updatedAt": "2023-09-14T05:59:34.972Z" - } - } - }, - "transect_number": { - "data": null - } - } - } - }, - "basal_dbh_instrument": { - "data": { - "id": 1, - "attributes": { - "symbol": "DIA", - "label": "Diameter Tape Measure", - "description": "A diameter tape was used to measure DBH.", - "uri": "https://linked.data.gov.au/def/nrm/237b799a-a1f3-5a1e-b88a-69429d05523a", - "createdAt": "2023-09-14T05:59:37.957Z", - "updatedAt": "2023-09-14T05:59:37.957Z" - } - } - }, - "plot_visit": { - "data": { - "id": 9, - "attributes": { - "start_date": "2023-09-22T00:54:22.157Z", - "end_date": "2023-09-23T00:00:00.000Z", - "visit_field_name": "Plot test", - "createdAt": "2023-09-22T00:55:48.249Z", - "updatedAt": "2023-09-22T00:55:48.249Z", - "plot_layout": { - "data": { - "id": 4, - "attributes": { - "replicate": 1, - "is_the_plot_permanently_marked": true, - "orientation": 1, - "createdAt": "2023-09-14T06:00:11.996Z", - "updatedAt": "2023-09-14T06:00:11.996Z", - "plot_selection": { - "data": { - "id": 4, - "attributes": { - "plot_label": "SATFLB0001", - "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f4", - "comment": "some comment", - "createdAt": "2023-09-14T05:59:34.907Z", - "updatedAt": "2023-09-14T05:59:34.907Z", - "plot_name": { - "id": 4, - "unique_digits": "0001", - "state": { - "data": { - "id": 5, - "attributes": { - "symbol": "SA", - "label": "South Australia", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", - "createdAt": "2023-09-14T05:59:33.984Z", - "updatedAt": "2023-09-14T05:59:33.984Z" - } - } - }, - "program": { - "data": { - "id": 7, - "attributes": { - "symbol": "S", - "label": "Super Sites", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:34.035Z", - "updatedAt": "2023-09-14T05:59:34.035Z" - } - } - }, - "bioregion": { - "data": { - "id": 27, - "attributes": { - "symbol": "FLB", - "label": "Flinders Lofty Block", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:34.233Z", - "updatedAt": "2023-09-14T05:59:38.572Z", - "states": { - "data": [ - { - "id": 5, - "attributes": { - "symbol": "SA", - "label": "South Australia", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", - "createdAt": "2023-09-14T05:59:33.984Z", - "updatedAt": "2023-09-14T05:59:33.984Z" - } - } - ] - } - } - } - } - }, - "recommended_location": { - "id": 4, - "lat": -35.0004723472134, - "lng": 138.66067886352542 - }, - "recommended_location_point": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "description": "Centre", - "label": "Centre", - "uri": "", - "createdAt": "2023-09-14T05:59:34.718Z", - "updatedAt": "2023-09-14T05:59:34.718Z" - } - } - }, - "plot_selection_survey": { - "data": null - } - } - } - }, - "plot_type": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "label": "Control", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:53.672Z", - "updatedAt": "2023-09-14T05:59:53.672Z" - } - } - }, - "permanently_marked": { - "id": 3, - "NW": true, - "NE": false, - "SW": false, - "SE": false, - "Centre": false, - "marker": { - "data": { - "id": 2, - "attributes": { - "symbol": "SSS", - "label": "Survey stake - steel", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:53.465Z", - "updatedAt": "2023-09-14T05:59:53.465Z" - } - } - } - }, - "plot_dimensions": { - "data": { - "id": 1, - "attributes": { - "symbol": "100m", - "label": "100 x 100", - "description": "100 m by 100 m (1 hectare)", - "uri": "", - "createdAt": "2023-09-14T05:59:53.412Z", - "updatedAt": "2023-09-14T05:59:53.412Z" - } - } - }, - "plot_points": [ - { - "id": 76, - "lat": -34.97222197296049, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 12, - "attributes": { - "symbol": "W1", - "description": "West 1", - "label": "West 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.771Z", - "updatedAt": "2023-09-14T05:59:34.771Z" - } - } - } - }, - { - "id": 77, - "lat": -34.97204230990367, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 13, - "attributes": { - "symbol": "W2", - "description": "West 2", - "label": "West 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.775Z", - "updatedAt": "2023-09-14T05:59:34.775Z" - } - } - } - }, - { - "id": 78, - "lat": -34.971862646846844, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 14, - "attributes": { - "symbol": "W3", - "description": "West 3", - "label": "West 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.779Z", - "updatedAt": "2023-09-14T05:59:34.779Z" - } - } - } - }, - { - "id": 79, - "lat": -34.97168298379002, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 15, - "attributes": { - "symbol": "W4", - "description": "West 4", - "label": "West 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.783Z", - "updatedAt": "2023-09-14T05:59:34.783Z" - } - } - } - }, - { - "id": 80, - "lat": -34.9715033207332, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 16, - "attributes": { - "symbol": "W5", - "description": "West 5", - "label": "West 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.787Z", - "updatedAt": "2023-09-14T05:59:34.787Z" - } - } - } - }, - { - "id": 81, - "lat": -34.971413489204785, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 24, - "attributes": { - "symbol": "NW", - "description": "North West", - "label": "North West", - "uri": "", - "createdAt": "2023-09-14T05:59:34.827Z", - "updatedAt": "2023-09-14T05:59:34.827Z" - } - } - } - }, - { - "id": 83, - "lat": -34.971413489204785, - "lng": 138.63731723494544, - "name": { - "data": { - "id": 17, - "attributes": { - "symbol": "N1", - "description": "North 1", - "label": "North 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.795Z", - "updatedAt": "2023-09-14T05:59:34.795Z" - } - } - } - }, - { - "id": 82, - "lat": -34.971413489204785, - "lng": 138.6375364888752, - "name": { - "data": { - "id": 18, - "attributes": { - "symbol": "N2", - "description": "North 2", - "label": "North 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.799Z", - "updatedAt": "2023-09-14T05:59:34.799Z" - } - } - } - }, - { - "id": 84, - "lat": -34.971413489204785, - "lng": 138.63775574280498, - "name": { - "data": { - "id": 19, - "attributes": { - "symbol": "N3", - "description": "North 3", - "label": "North 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.803Z", - "updatedAt": "2023-09-14T05:59:34.803Z" - } - } - } - }, - { - "id": 85, - "lat": -34.971413489204785, - "lng": 138.63797499673475, - "name": { - "data": { - "id": 20, - "attributes": { - "symbol": "N4", - "description": "North 4", - "label": "North 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.808Z", - "updatedAt": "2023-09-14T05:59:34.808Z" - } - } - } - }, - { - "id": 86, - "lat": -34.971413489204785, - "lng": 138.63819425066453, - "name": { - "data": { - "id": 21, - "attributes": { - "symbol": "N5", - "description": "North 5", - "label": "North 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.811Z", - "updatedAt": "2023-09-14T05:59:34.811Z" - } - } - } - }, - { - "id": 87, - "lat": -34.971413489204785, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 25, - "attributes": { - "symbol": "NE", - "description": "North East", - "label": "North East", - "uri": "", - "createdAt": "2023-09-14T05:59:34.831Z", - "updatedAt": "2023-09-14T05:59:34.831Z" - } - } - } - }, - { - "id": 88, - "lat": -34.9715033207332, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 6, - "attributes": { - "symbol": "E5", - "description": "East 5", - "label": "East 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.744Z", - "updatedAt": "2023-09-14T05:59:34.744Z" - } - } - } - }, - { - "id": 89, - "lat": -34.97168298379002, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 5, - "attributes": { - "symbol": "E4", - "description": "East 4", - "label": "East 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.740Z", - "updatedAt": "2023-09-14T05:59:34.740Z" - } - } - } - }, - { - "id": 90, - "lat": -34.971862646846844, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 4, - "attributes": { - "symbol": "E3", - "description": "East 3", - "label": "East 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.736Z", - "updatedAt": "2023-09-14T05:59:34.736Z" - } - } - } - }, - { - "id": 91, - "lat": -34.97204230990367, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 3, - "attributes": { - "symbol": "E2", - "description": "East 2", - "label": "East 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.731Z", - "updatedAt": "2023-09-14T05:59:34.731Z" - } - } - } - }, - { - "id": 92, - "lat": -34.97222197296049, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 2, - "attributes": { - "symbol": "E1", - "description": "East 1", - "label": "East 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.726Z", - "updatedAt": "2023-09-14T05:59:34.726Z" - } - } - } - }, - { - "id": 93, - "lat": -34.9723118044889, - "lng": 138.63830387762943, - "name": { - "data": { - "id": 23, - "attributes": { - "symbol": "SE", - "description": "South East", - "label": "South East", - "uri": "", - "createdAt": "2023-09-14T05:59:34.823Z", - "updatedAt": "2023-09-14T05:59:34.823Z" - } - } - } - }, - { - "id": 94, - "lat": -34.9723118044889, - "lng": 138.63819425066453, - "name": { - "data": { - "id": 11, - "attributes": { - "symbol": "S5", - "description": "South 5", - "label": "South 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.767Z", - "updatedAt": "2023-09-14T05:59:34.767Z" - } - } - } - }, - { - "id": 95, - "lat": -34.9723118044889, - "lng": 138.63797499673475, - "name": { - "data": { - "id": 10, - "attributes": { - "symbol": "S4", - "description": "South 4", - "label": "South 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.763Z", - "updatedAt": "2023-09-14T05:59:34.763Z" - } - } - } - }, - { - "id": 96, - "lat": -34.9723118044889, - "lng": 138.63775574280498, - "name": { - "data": { - "id": 9, - "attributes": { - "symbol": "S3", - "description": "South 3", - "label": "South 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.760Z", - "updatedAt": "2023-09-14T05:59:34.760Z" - } - } - } - }, - { - "id": 97, - "lat": -34.9723118044889, - "lng": 138.6375364888752, - "name": { - "data": { - "id": 8, - "attributes": { - "symbol": "S2", - "description": "South 2", - "label": "South 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.756Z", - "updatedAt": "2023-09-14T05:59:34.756Z" - } - } - } - }, - { - "id": 98, - "lat": -34.9723118044889, - "lng": 138.63731723494544, - "name": { - "data": { - "id": 7, - "attributes": { - "symbol": "S1", - "description": "South 1", - "label": "South 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.751Z", - "updatedAt": "2023-09-14T05:59:34.751Z" - } - } - } - }, - { - "id": 99, - "lat": -34.97186264684684, - "lng": 138.63775574280498, - "name": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "description": "Centre", - "label": "Centre", - "uri": "", - "createdAt": "2023-09-14T05:59:34.718Z", - "updatedAt": "2023-09-14T05:59:34.718Z" - } - } - } - }, - { - "id": 100, - "lat": -34.9723118044889, - "lng": 138.63720760798054, - "name": { - "data": { - "id": 22, - "attributes": { - "symbol": "SW", - "description": "South West", - "label": "South West", - "uri": "", - "createdAt": "2023-09-14T05:59:34.815Z", - "updatedAt": "2023-09-14T05:59:34.815Z" - } - } - } - } - ], - "fauna_plot_point": [ - { - "id": 6, - "lat": -34.971864821496084, - "lng": 138.63764986019743, - "name": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "description": "Centre", - "label": "Centre", - "uri": "", - "createdAt": "2023-09-14T05:59:44.422Z", - "updatedAt": "2023-09-14T05:59:44.422Z" - } - } - } - }, - { - "id": 7, - "lat": -34.971403261821905, - "lng": 138.6371026907952, - "name": { - "data": { - "id": 2, - "attributes": { - "symbol": "NW", - "description": "North-West", - "label": "North-West", - "uri": "", - "createdAt": "2023-09-14T05:59:44.427Z", - "updatedAt": "2023-09-14T05:59:44.427Z" - } - } - } - }, - { - "id": 9, - "lat": -34.972304399720215, - "lng": 138.63709732396242, - "name": { - "data": { - "id": 3, - "attributes": { - "symbol": "SW", - "description": "South-West", - "label": "South-West", - "uri": "", - "createdAt": "2023-09-14T05:59:44.434Z", - "updatedAt": "2023-09-14T05:59:44.434Z" - } - } - } - }, - { - "id": 10, - "lat": -34.972304399720215, - "lng": 138.6381916652405, - "name": { - "data": { - "id": 4, - "attributes": { - "symbol": "SE", - "description": "South-East", - "label": "South-East", - "uri": "", - "createdAt": "2023-09-14T05:59:44.438Z", - "updatedAt": "2023-09-14T05:59:44.438Z" - } - } - } - }, - { - "id": 8, - "lat": -34.9714076576406, - "lng": 138.63819166764344, - "name": { - "data": { - "id": 5, - "attributes": { - "symbol": "NE", - "description": "North-East", - "label": "North-East", - "uri": "", - "createdAt": "2023-09-14T05:59:44.442Z", - "updatedAt": "2023-09-14T05:59:44.442Z" - } - } - } - } - ] - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/paratoo/basalAreaDbhReverseLookup.json b/src/test/resources/paratoo/basalAreaDbhReverseLookup.json new file mode 100644 index 000000000..27145c2ab --- /dev/null +++ b/src/test/resources/paratoo/basalAreaDbhReverseLookup.json @@ -0,0 +1,1185 @@ +{ + "survey_metadata": { + "orgMintedUUID": "d1", + "survey_details": { + "survey_model": "basal-area-dbh-measure-survey", + "time": "2023-09-22T01:03:15.556Z", + "uuid": "43389075", + "project_id": "p1", + "protocol_id": "guid-3", + "protocol_version": "1" + }, + "provenance": { + "version_app": "0.0.1-xxxxx", + "version_core": "0.1.0-1fb53f81", + "version_core_documentation": "0.0.1-xxxxx", + "version_org": "4.4-SNAPSHOT", + "system_app": "monitor", + "system_core": "Monitor-dummy-data-production", + "system_org": "MERIT" + } + }, + "collections": { + "basal-area-dbh-measure-survey": { + "survey_metadata": { + "orgMintedUUID": "d1", + "survey_details": { + "survey_model": "basal-area-dbh-measure-survey", + "time": "2023-09-22T01:03:15.556Z", + "uuid": "43389075", + "project_id": "p1", + "protocol_id": "guid-3", + "protocol_version": "1" + }, + "provenance": { + "version_app": "0.0.1-xxxxx", + "version_core": "0.1.0-1fb53f81", + "version_core_documentation": "0.0.1-xxxxx", + "version_org": "4.4-SNAPSHOT", + "system_app": "monitor", + "system_core": "Monitor-dummy-data-production", + "system_org": "MERIT" + } + }, + "start_date": "2023-09-22T00:59:47.807Z", + "createdAt": "2023-09-22T01:05:19.981Z", + "updatedAt": "2023-09-22T01:05:19.981Z", + "plot_size": { + "symbol": "P40", + "label": "40x40 m", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:43.261Z", + "updatedAt": "2023-09-14T05:59:43.261Z", + "survey_method": { + "symbol": "T", + "label": "Transects", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/ba3bc90d-ebed-5e4e-a2ef-8e5d4d485fef", + "createdAt": "2023-09-14T05:59:34.972Z", + "updatedAt": "2023-09-14T05:59:34.972Z" + }, + "transect_number": { + "data": null + } + }, + "basal_dbh_instrument": { + "symbol": "DIA", + "label": "Diameter Tape Measure", + "description": "A diameter tape was used to measure DBH.", + "uri": "https://linked.data.gov.au/def/nrm/237b799a-a1f3-5a1e-b88a-69429d05523a", + "createdAt": "2023-09-14T05:59:37.957Z", + "updatedAt": "2023-09-14T05:59:37.957Z" + }, + "plot_visit": { + "id": 1, + "start_date": "2023-09-22T00:54:22.157Z", + "end_date": "2023-09-23T00:00:00.000Z", + "visit_field_name": "Plot test", + "createdAt": "2023-09-22T00:55:48.249Z", + "updatedAt": "2023-09-22T00:55:48.249Z", + "plot_layout": { + "id": 2, + "replicate": 1, + "is_the_plot_permanently_marked": true, + "orientation": 1, + "createdAt": "2023-09-14T06:00:11.996Z", + "updatedAt": "2023-09-14T06:00:11.996Z", + "plot_selection": { + "plot_label": "SATFLB0001", + "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f4", + "comments": "some comment", + "createdAt": "2023-09-14T05:59:34.907Z", + "updatedAt": "2023-09-14T05:59:34.907Z", + "plot_name": { + "id": 4, + "unique_digits": "0001", + "state": { + "symbol": "SA", + "label": "South Australia", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2023-09-14T05:59:33.984Z", + "updatedAt": "2023-09-14T05:59:33.984Z" + }, + "program": { + "symbol": "S", + "label": "Super Sites", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.035Z", + "updatedAt": "2023-09-14T05:59:34.035Z" + }, + "bioregion": { + "symbol": "FLB", + "label": "Flinders Lofty Block", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.233Z", + "updatedAt": "2023-09-14T05:59:38.572Z", + "states": [ + { + "symbol": "SA", + "label": "South Australia", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2023-09-14T05:59:33.984Z", + "updatedAt": "2023-09-14T05:59:33.984Z" + } + ] + } + }, + "recommended_location": { + "id": 4, + "lat": -35.0004723472134, + "lng": 138.66067886352542 + }, + "recommended_location_point": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + }, + "plot_selection_survey": { + "data": null + } + }, + "plot_type": { + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.672Z", + "updatedAt": "2023-09-14T05:59:53.672Z" + }, + "permanently_marked": { + "id": 3, + "NW": true, + "NE": false, + "SW": false, + "SE": false, + "Centre": false, + "marker": { + "symbol": "SSS", + "label": "Survey stake - steel", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.465Z", + "updatedAt": "2023-09-14T05:59:53.465Z" + } + }, + "plot_dimensions": { + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2023-09-14T05:59:53.412Z", + "updatedAt": "2023-09-14T05:59:53.412Z" + }, + "plot_points": [ + { + "id": 76, + "lat": -34.97222197296049, + "lng": 138.63720760798054, + "name": { + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.771Z", + "updatedAt": "2023-09-14T05:59:34.771Z" + } + }, + { + "id": 77, + "lat": -34.97204230990367, + "lng": 138.63720760798054, + "name": { + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.775Z", + "updatedAt": "2023-09-14T05:59:34.775Z" + } + }, + { + "id": 78, + "lat": -34.971862646846844, + "lng": 138.63720760798054, + "name": { + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.779Z", + "updatedAt": "2023-09-14T05:59:34.779Z" + } + }, + { + "id": 79, + "lat": -34.97168298379002, + "lng": 138.63720760798054, + "name": { + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.783Z", + "updatedAt": "2023-09-14T05:59:34.783Z" + } + }, + { + "id": 80, + "lat": -34.9715033207332, + "lng": 138.63720760798054, + "name": { + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.787Z", + "updatedAt": "2023-09-14T05:59:34.787Z" + } + }, + { + "id": 81, + "lat": -34.971413489204785, + "lng": 138.63720760798054, + "name": { + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.827Z", + "updatedAt": "2023-09-14T05:59:34.827Z" + } + }, + { + "id": 83, + "lat": -34.971413489204785, + "lng": 138.63731723494544, + "name": { + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.795Z", + "updatedAt": "2023-09-14T05:59:34.795Z" + } + }, + { + "id": 82, + "lat": -34.971413489204785, + "lng": 138.6375364888752, + "name": { + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.799Z", + "updatedAt": "2023-09-14T05:59:34.799Z" + } + }, + { + "id": 84, + "lat": -34.971413489204785, + "lng": 138.63775574280498, + "name": { + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.803Z", + "updatedAt": "2023-09-14T05:59:34.803Z" + } + }, + { + "id": 85, + "lat": -34.971413489204785, + "lng": 138.63797499673475, + "name": { + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.808Z", + "updatedAt": "2023-09-14T05:59:34.808Z" + } + }, + { + "id": 86, + "lat": -34.971413489204785, + "lng": 138.63819425066453, + "name": { + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.811Z", + "updatedAt": "2023-09-14T05:59:34.811Z" + } + }, + { + "id": 87, + "lat": -34.971413489204785, + "lng": 138.63830387762943, + "name": { + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.831Z", + "updatedAt": "2023-09-14T05:59:34.831Z" + } + }, + { + "id": 88, + "lat": -34.9715033207332, + "lng": 138.63830387762943, + "name": { + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.744Z", + "updatedAt": "2023-09-14T05:59:34.744Z" + } + }, + { + "id": 89, + "lat": -34.97168298379002, + "lng": 138.63830387762943, + "name": { + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.740Z", + "updatedAt": "2023-09-14T05:59:34.740Z" + } + }, + { + "id": 90, + "lat": -34.971862646846844, + "lng": 138.63830387762943, + "name": { + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.736Z", + "updatedAt": "2023-09-14T05:59:34.736Z" + } + }, + { + "id": 91, + "lat": -34.97204230990367, + "lng": 138.63830387762943, + "name": { + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.731Z", + "updatedAt": "2023-09-14T05:59:34.731Z" + } + }, + { + "id": 92, + "lat": -34.97222197296049, + "lng": 138.63830387762943, + "name": { + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.726Z", + "updatedAt": "2023-09-14T05:59:34.726Z" + } + }, + { + "id": 93, + "lat": -34.9723118044889, + "lng": 138.63830387762943, + "name": { + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.823Z", + "updatedAt": "2023-09-14T05:59:34.823Z" + } + }, + { + "id": 94, + "lat": -34.9723118044889, + "lng": 138.63819425066453, + "name": { + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.767Z", + "updatedAt": "2023-09-14T05:59:34.767Z" + } + }, + { + "id": 95, + "lat": -34.9723118044889, + "lng": 138.63797499673475, + "name": { + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.763Z", + "updatedAt": "2023-09-14T05:59:34.763Z" + } + }, + { + "id": 96, + "lat": -34.9723118044889, + "lng": 138.63775574280498, + "name": { + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.760Z", + "updatedAt": "2023-09-14T05:59:34.760Z" + } + }, + { + "id": 97, + "lat": -34.9723118044889, + "lng": 138.6375364888752, + "name": { + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.756Z", + "updatedAt": "2023-09-14T05:59:34.756Z" + } + }, + { + "id": 98, + "lat": -34.9723118044889, + "lng": 138.63731723494544, + "name": { + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.751Z", + "updatedAt": "2023-09-14T05:59:34.751Z" + } + }, + { + "id": 99, + "lat": -34.97186264684684, + "lng": 138.63775574280498, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + } + }, + { + "id": 100, + "lat": -34.9723118044889, + "lng": 138.63720760798054, + "name": { + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.815Z", + "updatedAt": "2023-09-14T05:59:34.815Z" + } + } + ], + "fauna_plot_point": [ + { + "id": 6, + "lat": -34.971864821496084, + "lng": 138.63764986019743, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:44.422Z", + "updatedAt": "2023-09-14T05:59:44.422Z" + } + }, + { + "id": 7, + "lat": -34.971403261821905, + "lng": 138.6371026907952, + "name": { + "symbol": "NW", + "description": "North-West", + "label": "North-West", + "uri": "", + "createdAt": "2023-09-14T05:59:44.427Z", + "updatedAt": "2023-09-14T05:59:44.427Z" + } + }, + { + "id": 9, + "lat": -34.972304399720215, + "lng": 138.63709732396242, + "name": { + "symbol": "SW", + "description": "South-West", + "label": "South-West", + "uri": "", + "createdAt": "2023-09-14T05:59:44.434Z", + "updatedAt": "2023-09-14T05:59:44.434Z" + } + }, + { + "id": 10, + "lat": -34.972304399720215, + "lng": 138.6381916652405, + "name": { + "symbol": "SE", + "description": "South-East", + "label": "South-East", + "uri": "", + "createdAt": "2023-09-14T05:59:44.438Z", + "updatedAt": "2023-09-14T05:59:44.438Z" + } + }, + { + "id": 8, + "lat": -34.9714076576406, + "lng": 138.63819166764344, + "name": { + "symbol": "NE", + "description": "North-East", + "label": "North-East", + "uri": "", + "createdAt": "2023-09-14T05:59:44.442Z", + "updatedAt": "2023-09-14T05:59:44.442Z" + } + } + ] + } + } + }, + "basal-area-dbh-measure-observation": [ + { + "id": 2, + "dead": false, + "multi_stemmed": false, + "ellipse": false, + "buttresses": false, + "location": { + "id": 37, + "lat": -35.2592444, + "lng": 149.0651491 + }, + "floristics_voucher": { + "id": 17, + "voucher_full": null, + "voucher_lite": null + }, + "survey_metadata": { + "orgMintedUUID": "d1", + "survey_details": { + "survey_model": "basal-area-dbh-measure-survey", + "time": "2023-09-22T01:03:15.556Z", + "uuid": "43389075", + "project_id": "p1", + "protocol_id": "guid-3", + "protocol_version": "1" + }, + "provenance": { + "version_app": "0.0.1-xxxxx", + "version_core": "0.1.0-1fb53f81", + "version_core_documentation": "0.0.1-xxxxx", + "version_org": "4.4-SNAPSHOT", + "system_app": "monitor", + "system_core": "Monitor-dummy-data-production", + "system_org": "MERIT" + } + }, + "date_time": "2024-03-28T03:17:01.764Z", + "createdAt": "2023-09-22T01:05:19.981Z", + "updatedAt": "2023-09-22T01:05:19.981Z", + "stem_1": { + "id": 2, + "DBH": 3, + "POM_single": 1.3, + "circumference_at_breast_height": null, + "ellipse_DBH_measurement": null + }, + "stem": [], + "buttressed_tree": null, + "basal_area_dbh_measure_survey_full": { + "survey_metadata": { + "orgMintedUUID": "d1", + "survey_details": { + "survey_model": "basal-area-dbh-measure-survey", + "time": "2023-09-22T01:03:15.556Z", + "uuid": "43389075", + "project_id": "p1", + "protocol_id": "guid-3", + "protocol_version": "1" + }, + "provenance": { + "version_app": "0.0.1-xxxxx", + "version_core": "0.1.0-1fb53f81", + "version_core_documentation": "0.0.1-xxxxx", + "version_org": "4.4-SNAPSHOT", + "system_app": "monitor", + "system_core": "Monitor-dummy-data-production", + "system_org": "MERIT" + } + }, + "start_date": "2023-09-22T00:59:47.807Z", + "createdAt": "2023-09-22T01:05:19.981Z", + "updatedAt": "2023-09-22T01:05:19.981Z", + "plot_size": { + "symbol": "P40", + "label": "40x40 m", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:43.261Z", + "updatedAt": "2023-09-14T05:59:43.261Z", + "survey_method": { + "symbol": "T", + "label": "Transects", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/ba3bc90d-ebed-5e4e-a2ef-8e5d4d485fef", + "createdAt": "2023-09-14T05:59:34.972Z", + "updatedAt": "2023-09-14T05:59:34.972Z" + }, + "transect_number": { + "data": null + } + }, + "basal_dbh_instrument": { + "symbol": "DIA", + "label": "Diameter Tape Measure", + "description": "A diameter tape was used to measure DBH.", + "uri": "https://linked.data.gov.au/def/nrm/237b799a-a1f3-5a1e-b88a-69429d05523a", + "createdAt": "2023-09-14T05:59:37.957Z", + "updatedAt": "2023-09-14T05:59:37.957Z" + }, + "plot_visit": { + "id": 1, + "start_date": "2023-09-22T00:54:22.157Z", + "end_date": "2023-09-23T00:00:00.000Z", + "visit_field_name": "Plot test", + "createdAt": "2023-09-22T00:55:48.249Z", + "updatedAt": "2023-09-22T00:55:48.249Z", + "plot_layout": { + "id": 2, + "replicate": 1, + "is_the_plot_permanently_marked": true, + "orientation": 1, + "createdAt": "2023-09-14T06:00:11.996Z", + "updatedAt": "2023-09-14T06:00:11.996Z", + "plot_selection": { + "plot_label": "SATFLB0001", + "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f4", + "comments": "some comment", + "createdAt": "2023-09-14T05:59:34.907Z", + "updatedAt": "2023-09-14T05:59:34.907Z", + "plot_name": { + "id": 4, + "unique_digits": "0001", + "state": { + "symbol": "SA", + "label": "South Australia", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2023-09-14T05:59:33.984Z", + "updatedAt": "2023-09-14T05:59:33.984Z" + }, + "program": { + "symbol": "S", + "label": "Super Sites", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.035Z", + "updatedAt": "2023-09-14T05:59:34.035Z" + }, + "bioregion": { + "symbol": "FLB", + "label": "Flinders Lofty Block", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.233Z", + "updatedAt": "2023-09-14T05:59:38.572Z", + "states": [ + { + "symbol": "SA", + "label": "South Australia", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2023-09-14T05:59:33.984Z", + "updatedAt": "2023-09-14T05:59:33.984Z" + } + ] + } + }, + "recommended_location": { + "id": 4, + "lat": -35.0004723472134, + "lng": 138.66067886352542 + }, + "recommended_location_point": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + }, + "plot_selection_survey": { + "data": null + } + }, + "plot_type": { + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.672Z", + "updatedAt": "2023-09-14T05:59:53.672Z" + }, + "permanently_marked": { + "id": 3, + "NW": true, + "NE": false, + "SW": false, + "SE": false, + "Centre": false, + "marker": { + "symbol": "SSS", + "label": "Survey stake - steel", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.465Z", + "updatedAt": "2023-09-14T05:59:53.465Z" + } + }, + "plot_dimensions": { + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2023-09-14T05:59:53.412Z", + "updatedAt": "2023-09-14T05:59:53.412Z" + }, + "plot_points": [ + { + "id": 76, + "lat": -34.97222197296049, + "lng": 138.63720760798054, + "name": { + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.771Z", + "updatedAt": "2023-09-14T05:59:34.771Z" + } + }, + { + "id": 77, + "lat": -34.97204230990367, + "lng": 138.63720760798054, + "name": { + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.775Z", + "updatedAt": "2023-09-14T05:59:34.775Z" + } + }, + { + "id": 78, + "lat": -34.971862646846844, + "lng": 138.63720760798054, + "name": { + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.779Z", + "updatedAt": "2023-09-14T05:59:34.779Z" + } + }, + { + "id": 79, + "lat": -34.97168298379002, + "lng": 138.63720760798054, + "name": { + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.783Z", + "updatedAt": "2023-09-14T05:59:34.783Z" + } + }, + { + "id": 80, + "lat": -34.9715033207332, + "lng": 138.63720760798054, + "name": { + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.787Z", + "updatedAt": "2023-09-14T05:59:34.787Z" + } + }, + { + "id": 81, + "lat": -34.971413489204785, + "lng": 138.63720760798054, + "name": { + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.827Z", + "updatedAt": "2023-09-14T05:59:34.827Z" + } + }, + { + "id": 83, + "lat": -34.971413489204785, + "lng": 138.63731723494544, + "name": { + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.795Z", + "updatedAt": "2023-09-14T05:59:34.795Z" + } + }, + { + "id": 82, + "lat": -34.971413489204785, + "lng": 138.6375364888752, + "name": { + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.799Z", + "updatedAt": "2023-09-14T05:59:34.799Z" + } + }, + { + "id": 84, + "lat": -34.971413489204785, + "lng": 138.63775574280498, + "name": { + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.803Z", + "updatedAt": "2023-09-14T05:59:34.803Z" + } + }, + { + "id": 85, + "lat": -34.971413489204785, + "lng": 138.63797499673475, + "name": { + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.808Z", + "updatedAt": "2023-09-14T05:59:34.808Z" + } + }, + { + "id": 86, + "lat": -34.971413489204785, + "lng": 138.63819425066453, + "name": { + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.811Z", + "updatedAt": "2023-09-14T05:59:34.811Z" + } + }, + { + "id": 87, + "lat": -34.971413489204785, + "lng": 138.63830387762943, + "name": { + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.831Z", + "updatedAt": "2023-09-14T05:59:34.831Z" + } + }, + { + "id": 88, + "lat": -34.9715033207332, + "lng": 138.63830387762943, + "name": { + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.744Z", + "updatedAt": "2023-09-14T05:59:34.744Z" + } + }, + { + "id": 89, + "lat": -34.97168298379002, + "lng": 138.63830387762943, + "name": { + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.740Z", + "updatedAt": "2023-09-14T05:59:34.740Z" + } + }, + { + "id": 90, + "lat": -34.971862646846844, + "lng": 138.63830387762943, + "name": { + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.736Z", + "updatedAt": "2023-09-14T05:59:34.736Z" + } + }, + { + "id": 91, + "lat": -34.97204230990367, + "lng": 138.63830387762943, + "name": { + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.731Z", + "updatedAt": "2023-09-14T05:59:34.731Z" + } + }, + { + "id": 92, + "lat": -34.97222197296049, + "lng": 138.63830387762943, + "name": { + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.726Z", + "updatedAt": "2023-09-14T05:59:34.726Z" + } + }, + { + "id": 93, + "lat": -34.9723118044889, + "lng": 138.63830387762943, + "name": { + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.823Z", + "updatedAt": "2023-09-14T05:59:34.823Z" + } + }, + { + "id": 94, + "lat": -34.9723118044889, + "lng": 138.63819425066453, + "name": { + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.767Z", + "updatedAt": "2023-09-14T05:59:34.767Z" + } + }, + { + "id": 95, + "lat": -34.9723118044889, + "lng": 138.63797499673475, + "name": { + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.763Z", + "updatedAt": "2023-09-14T05:59:34.763Z" + } + }, + { + "id": 96, + "lat": -34.9723118044889, + "lng": 138.63775574280498, + "name": { + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.760Z", + "updatedAt": "2023-09-14T05:59:34.760Z" + } + }, + { + "id": 97, + "lat": -34.9723118044889, + "lng": 138.6375364888752, + "name": { + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.756Z", + "updatedAt": "2023-09-14T05:59:34.756Z" + } + }, + { + "id": 98, + "lat": -34.9723118044889, + "lng": 138.63731723494544, + "name": { + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.751Z", + "updatedAt": "2023-09-14T05:59:34.751Z" + } + }, + { + "id": 99, + "lat": -34.97186264684684, + "lng": 138.63775574280498, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + } + }, + { + "id": 100, + "lat": -34.9723118044889, + "lng": 138.63720760798054, + "name": { + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.815Z", + "updatedAt": "2023-09-14T05:59:34.815Z" + } + } + ], + "fauna_plot_point": [ + { + "id": 6, + "lat": -34.971864821496084, + "lng": 138.63764986019743, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:44.422Z", + "updatedAt": "2023-09-14T05:59:44.422Z" + } + }, + { + "id": 7, + "lat": -34.971403261821905, + "lng": 138.6371026907952, + "name": { + "symbol": "NW", + "description": "North-West", + "label": "North-West", + "uri": "", + "createdAt": "2023-09-14T05:59:44.427Z", + "updatedAt": "2023-09-14T05:59:44.427Z" + } + }, + { + "id": 9, + "lat": -34.972304399720215, + "lng": 138.63709732396242, + "name": { + "symbol": "SW", + "description": "South-West", + "label": "South-West", + "uri": "", + "createdAt": "2023-09-14T05:59:44.434Z", + "updatedAt": "2023-09-14T05:59:44.434Z" + } + }, + { + "id": 10, + "lat": -34.972304399720215, + "lng": 138.6381916652405, + "name": { + "symbol": "SE", + "description": "South-East", + "label": "South-East", + "uri": "", + "createdAt": "2023-09-14T05:59:44.438Z", + "updatedAt": "2023-09-14T05:59:44.438Z" + } + }, + { + "id": 8, + "lat": -34.9714076576406, + "lng": 138.63819166764344, + "name": { + "symbol": "NE", + "description": "North-East", + "label": "North-East", + "uri": "", + "createdAt": "2023-09-14T05:59:44.442Z", + "updatedAt": "2023-09-14T05:59:44.442Z" + } + } + ] + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/floristicsStandard.json b/src/test/resources/paratoo/floristicsStandard.json deleted file mode 100644 index 24891a6ae..000000000 --- a/src/test/resources/paratoo/floristicsStandard.json +++ /dev/null @@ -1,620 +0,0 @@ -{ - "id": 1, - "attributes": { - "surveyId": { - "time": "2022-09-21T01:55:45.107Z", - "uuid": "28027518", - "surveyType": "floristics-veg-survey-lite" - }, - "start_date_time": "2022-09-21T01:55:44.186Z", - "end_date_time": "2022-09-21T01:55:44.186Z", - "createdAt": "2023-09-14T06:00:17.099Z", - "updatedAt": "2023-09-14T06:00:17.099Z", - "plot_visit": { - "data": { - "id": 1, - "attributes": { - "start_date": "2021-08-26T12:26:54.317Z", - "end_date": "2021-08-26T13:26:54.317Z", - "visit_field_name": "QLD winter survey", - "createdAt": "2023-09-14T06:00:12.120Z", - "updatedAt": "2023-09-14T06:00:17.882Z", - "plot_layout": { - "data": { - "id": 1, - "attributes": { - "replicate": 1, - "is_the_plot_permanently_marked": false, - "orientation": 1, - "createdAt": "2023-09-14T06:00:11.473Z", - "updatedAt": "2023-09-14T06:00:11.473Z", - "plot_selection": { - "data": { - "id": 1, - "attributes": { - "plot_label": "QDASEQ0001", - "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f1", - "comment": "some comment", - "createdAt": "2023-09-14T05:59:34.851Z", - "updatedAt": "2023-09-14T05:59:34.851Z", - "plot_name": { - "id": 1, - "unique_digits": "0001", - "state": { - "data": { - "id": 4, - "attributes": { - "symbol": "QD", - "label": "Queensland", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", - "createdAt": "2023-09-14T05:59:33.978Z", - "updatedAt": "2023-09-14T05:59:33.978Z" - } - } - }, - "program": { - "data": { - "id": 1, - "attributes": { - "symbol": "NLP", - "label": "National Landcare Program", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:34.006Z", - "updatedAt": "2023-09-14T05:59:34.006Z" - } - } - }, - "bioregion": { - "data": { - "id": 84, - "attributes": { - "symbol": "SEQ", - "label": "South Eastern Queensland", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:34.695Z", - "updatedAt": "2023-09-14T05:59:39.355Z", - "states": { - "data": [ - { - "id": 2, - "attributes": { - "symbol": "NS", - "label": "New South Wales", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/14b8c3fe-3a60-5f3a-bb7a-b9cf5df49b4e", - "createdAt": "2023-09-14T05:59:33.966Z", - "updatedAt": "2023-09-14T05:59:33.966Z" - } - }, - { - "id": 4, - "attributes": { - "symbol": "QD", - "label": "Queensland", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", - "createdAt": "2023-09-14T05:59:33.978Z", - "updatedAt": "2023-09-14T05:59:33.978Z" - } - } - ] - } - } - } - } - }, - "recommended_location": { - "id": 1, - "lat": -35.0004723472134, - "lng": 138.66067886352542 - }, - "recommended_location_point": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "description": "Centre", - "label": "Centre", - "uri": "", - "createdAt": "2023-09-14T05:59:34.718Z", - "updatedAt": "2023-09-14T05:59:34.718Z" - } - } - }, - "plot_selection_survey": { - "data": null - } - } - } - }, - "plot_type": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "label": "Control", - "description": "", - "uri": "", - "createdAt": "2023-09-14T05:59:53.672Z", - "updatedAt": "2023-09-14T05:59:53.672Z" - } - } - }, - "permanently_marked": null, - "plot_dimensions": { - "data": { - "id": 1, - "attributes": { - "symbol": "100m", - "label": "100 x 100", - "description": "100 m by 100 m (1 hectare)", - "uri": "", - "createdAt": "2023-09-14T05:59:53.412Z", - "updatedAt": "2023-09-14T05:59:53.412Z" - } - } - }, - "plot_points": [ - { - "id": 2, - "lat": -27.388252, - "lng": 152.880694, - "name": { - "data": { - "id": 16, - "attributes": { - "symbol": "W5", - "description": "West 5", - "label": "West 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.787Z", - "updatedAt": "2023-09-14T05:59:34.787Z" - } - } - } - }, - { - "id": 1, - "lat": -27.388336, - "lng": 152.880651, - "name": { - "data": { - "id": 15, - "attributes": { - "symbol": "W4", - "description": "West 4", - "label": "West 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.783Z", - "updatedAt": "2023-09-14T05:59:34.783Z" - } - } - } - }, - { - "id": 3, - "lat": -27.388483, - "lng": 152.880518, - "name": { - "data": { - "id": 14, - "attributes": { - "symbol": "W3", - "description": "West 3", - "label": "West 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.779Z", - "updatedAt": "2023-09-14T05:59:34.779Z" - } - } - } - }, - { - "id": 4, - "lat": -27.388611, - "lng": 152.880389, - "name": { - "data": { - "id": 13, - "attributes": { - "symbol": "W2", - "description": "West 2", - "label": "West 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.775Z", - "updatedAt": "2023-09-14T05:59:34.775Z" - } - } - } - }, - { - "id": 5, - "lat": -27.388749, - "lng": 152.88028, - "name": { - "data": { - "id": 12, - "attributes": { - "symbol": "W1", - "description": "West 1", - "label": "West 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.771Z", - "updatedAt": "2023-09-14T05:59:34.771Z" - } - } - } - }, - { - "id": 6, - "lat": -27.388903, - "lng": 152.880154, - "name": { - "data": { - "id": 22, - "attributes": { - "symbol": "SW", - "description": "South West", - "label": "South West", - "uri": "", - "createdAt": "2023-09-14T05:59:34.815Z", - "updatedAt": "2023-09-14T05:59:34.815Z" - } - } - } - }, - { - "id": 7, - "lat": -27.389463, - "lng": 152.880835, - "name": { - "data": { - "id": 23, - "attributes": { - "symbol": "SE", - "description": "South East", - "label": "South East", - "uri": "", - "createdAt": "2023-09-14T05:59:34.823Z", - "updatedAt": "2023-09-14T05:59:34.823Z" - } - } - } - }, - { - "id": 8, - "lat": -27.389366, - "lng": 152.880644, - "name": { - "data": { - "id": 11, - "attributes": { - "symbol": "S5", - "description": "South 5", - "label": "South 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.767Z", - "updatedAt": "2023-09-14T05:59:34.767Z" - } - } - } - }, - { - "id": 9, - "lat": -27.389248, - "lng": 152.880525, - "name": { - "data": { - "id": 10, - "attributes": { - "symbol": "S4", - "description": "South 4", - "label": "South 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.763Z", - "updatedAt": "2023-09-14T05:59:34.763Z" - } - } - } - }, - { - "id": 10, - "lat": -27.389158, - "lng": 152.88035, - "name": { - "data": { - "id": 9, - "attributes": { - "symbol": "S3", - "description": "South 3", - "label": "South 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.760Z", - "updatedAt": "2023-09-14T05:59:34.760Z" - } - } - } - }, - { - "id": 11, - "lat": -27.389021, - "lng": 152.880195, - "name": { - "data": { - "id": 8, - "attributes": { - "symbol": "S2", - "description": "South 2", - "label": "South 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.756Z", - "updatedAt": "2023-09-14T05:59:34.756Z" - } - } - } - }, - { - "id": 12, - "lat": -27.389373, - "lng": 152.880195, - "name": { - "data": { - "id": 7, - "attributes": { - "symbol": "S1", - "description": "South 1", - "label": "South 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.751Z", - "updatedAt": "2023-09-14T05:59:34.751Z" - } - } - } - }, - { - "id": 13, - "lat": -27.388316, - "lng": 152.880797, - "name": { - "data": { - "id": 24, - "attributes": { - "symbol": "NW", - "description": "North West", - "label": "North West", - "uri": "", - "createdAt": "2023-09-14T05:59:34.827Z", - "updatedAt": "2023-09-14T05:59:34.827Z" - } - } - } - }, - { - "id": 14, - "lat": -27.388909, - "lng": 152.881448, - "name": { - "data": { - "id": 25, - "attributes": { - "symbol": "NE", - "description": "North East", - "label": "North East", - "uri": "", - "createdAt": "2023-09-14T05:59:34.831Z", - "updatedAt": "2023-09-14T05:59:34.831Z" - } - } - } - }, - { - "id": 15, - "lat": -27.388821, - "lng": 152.881503, - "name": { - "data": { - "id": 21, - "attributes": { - "symbol": "N5", - "description": "North 5", - "label": "North 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.811Z", - "updatedAt": "2023-09-14T05:59:34.811Z" - } - } - } - }, - { - "id": 16, - "lat": -27.388766, - "lng": 152.881422, - "name": { - "data": { - "id": 20, - "attributes": { - "symbol": "N4", - "description": "North 4", - "label": "North 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.808Z", - "updatedAt": "2023-09-14T05:59:34.808Z" - } - } - } - }, - { - "id": 17, - "lat": -27.388644, - "lng": 152.881263, - "name": { - "data": { - "id": 19, - "attributes": { - "symbol": "N3", - "description": "North 3", - "label": "North 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.803Z", - "updatedAt": "2023-09-14T05:59:34.803Z" - } - } - } - }, - { - "id": 18, - "lat": -27.388549, - "lng": 152.881107, - "name": { - "data": { - "id": 18, - "attributes": { - "symbol": "N2", - "description": "North 2", - "label": "North 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.799Z", - "updatedAt": "2023-09-14T05:59:34.799Z" - } - } - } - }, - { - "id": 19, - "lat": -27.388445, - "lng": 152.880939, - "name": { - "data": { - "id": 17, - "attributes": { - "symbol": "N1", - "description": "North 1", - "label": "North 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.795Z", - "updatedAt": "2023-09-14T05:59:34.795Z" - } - } - } - }, - { - "id": 20, - "lat": -27.389035, - "lng": 152.881314, - "name": { - "data": { - "id": 6, - "attributes": { - "symbol": "E5", - "description": "East 5", - "label": "East 5", - "uri": "", - "createdAt": "2023-09-14T05:59:34.744Z", - "updatedAt": "2023-09-14T05:59:34.744Z" - } - } - } - }, - { - "id": 21, - "lat": -27.389208, - "lng": 152.88122, - "name": { - "data": { - "id": 5, - "attributes": { - "symbol": "E4", - "description": "East 4", - "label": "East 4", - "uri": "", - "createdAt": "2023-09-14T05:59:34.740Z", - "updatedAt": "2023-09-14T05:59:34.740Z" - } - } - } - }, - { - "id": 22, - "lat": -27.388974, - "lng": 152.880106, - "name": { - "data": { - "id": 1, - "attributes": { - "symbol": "C", - "description": "Centre", - "label": "Centre", - "uri": "", - "createdAt": "2023-09-14T05:59:34.718Z", - "updatedAt": "2023-09-14T05:59:34.718Z" - } - } - } - }, - { - "id": 23, - "lat": -27.389343, - "lng": 152.881089, - "name": { - "data": { - "id": 4, - "attributes": { - "symbol": "E3", - "description": "East 3", - "label": "East 3", - "uri": "", - "createdAt": "2023-09-14T05:59:34.736Z", - "updatedAt": "2023-09-14T05:59:34.736Z" - } - } - } - }, - { - "id": 24, - "lat": -27.389472, - "lng": 152.880973, - "name": { - "data": { - "id": 3, - "attributes": { - "symbol": "E2", - "description": "East 2", - "label": "East 2", - "uri": "", - "createdAt": "2023-09-14T05:59:34.731Z", - "updatedAt": "2023-09-14T05:59:34.731Z" - } - } - } - }, - { - "id": 25, - "lat": -27.389553, - "lng": 152.880916, - "name": { - "data": { - "id": 2, - "attributes": { - "symbol": "E1", - "description": "East 1", - "label": "East 1", - "uri": "", - "createdAt": "2023-09-14T05:59:34.726Z", - "updatedAt": "2023-09-14T05:59:34.726Z" - } - } - } - } - ], - "fauna_plot_point": [] - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/paratoo/floristicsStandardReverseLookup.json b/src/test/resources/paratoo/floristicsStandardReverseLookup.json new file mode 100644 index 000000000..f89b84506 --- /dev/null +++ b/src/test/resources/paratoo/floristicsStandardReverseLookup.json @@ -0,0 +1,2153 @@ +{ + "survey_metadata": { + "id": 8, + "org_minted_uuid": "9beeba20-374a-470a-b4f4-caed34e07572", + "createdAt": "2024-03-28T03:17:25.302Z", + "updatedAt": "2024-03-28T03:17:25.302Z", + "survey_details": { + "id": 59, + "survey_model": "floristics-veg-survey-lite", + "time": "2024-03-28T03:17:01.727Z", + "uuid": "263f710d-9af5-456a-9c68-b2b675763220", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 59, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": "0.1.0-a2ba7a48", + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": "Monitor FDCP API-production", + "system_org": null + } + }, + "collections": { + "floristics-veg-survey-lite": { + "survey_metadata": { + "id": 8, + "org_minted_uuid": "9beeba20-374a-470a-b4f4-caed34e07572", + "createdAt": "2024-03-28T03:17:25.302Z", + "updatedAt": "2024-03-28T03:17:25.302Z", + "survey_details": { + "id": 59, + "survey_model": "floristics-veg-survey-lite", + "time": "2024-03-28T03:17:01.727Z", + "uuid": "263f710d-9af5-456a-9c68-b2b675763220", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 59, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": "0.1.0-a2ba7a48", + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": "Monitor FDCP API-production", + "system_org": null + } + }, + "start_date_time": "2022-09-21T01:55:44.186Z", + "end_date_time": "2022-09-21T01:55:44.186Z", + "createdAt": "2023-09-14T06:00:17.099Z", + "updatedAt": "2023-09-14T06:00:17.099Z", + "plot_visit": { + "start_date": "2021-08-26T11:26:54.317Z", + "end_date": "2021-08-26T13:26:54.317Z", + "visit_field_name": "QLD winter survey", + "createdAt": "2023-09-14T06:00:12.120Z", + "updatedAt": "2023-09-14T06:00:17.882Z", + "plot_layout": { + "id": 1, + "replicate": 1, + "is_the_plot_permanently_marked": false, + "orientation": 1, + "createdAt": "2023-09-14T06:00:11.473Z", + "updatedAt": "2023-09-14T06:00:11.473Z", + "plot_selection": { + "plot_label": "QDASEQ0001", + "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f1", + "comments": "some comment", + "createdAt": "2023-09-14T05:59:34.851Z", + "updatedAt": "2023-09-14T05:59:34.851Z", + "plot_name": { + "id": 1, + "unique_digits": "0001", + "state": { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + }, + "program": { + "symbol": "NLP", + "label": "National Landcare Program", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.006Z", + "updatedAt": "2023-09-14T05:59:34.006Z" + }, + "bioregion": { + "symbol": "SEQ", + "label": "South Eastern Queensland", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.695Z", + "updatedAt": "2023-09-14T05:59:39.355Z", + "states": [ + { + "symbol": "NS", + "label": "New South Wales", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/14b8c3fe-3a60-5f3a-bb7a-b9cf5df49b4e", + "createdAt": "2023-09-14T05:59:33.966Z", + "updatedAt": "2023-09-14T05:59:33.966Z" + }, + { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + } + ] + } + }, + "recommended_location": { + "id": 1, + "lat": -35.0004723472134, + "lng": 138.66067886352542 + }, + "recommended_location_point": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + }, + "plot_selection_survey": null + }, + "plot_type": { + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.672Z", + "updatedAt": "2023-09-14T05:59:53.672Z" + }, + "permanently_marked": null, + "plot_dimensions": { + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2023-09-14T05:59:53.412Z", + "updatedAt": "2023-09-14T05:59:53.412Z" + }, + "plot_points": [ + { + "id": 2, + "lat": -27.388252, + "lng": 152.880694, + "name": { + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.787Z", + "updatedAt": "2023-09-14T05:59:34.787Z" + } + }, + { + "id": 1, + "lat": -27.388336, + "lng": 152.880651, + "name": { + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.783Z", + "updatedAt": "2023-09-14T05:59:34.783Z" + } + }, + { + "id": 3, + "lat": -27.388483, + "lng": 152.880518, + "name": { + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.779Z", + "updatedAt": "2023-09-14T05:59:34.779Z" + } + }, + { + "id": 4, + "lat": -27.388611, + "lng": 152.880389, + "name": { + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.775Z", + "updatedAt": "2023-09-14T05:59:34.775Z" + } + }, + { + "id": 5, + "lat": -27.388749, + "lng": 152.88028, + "name": { + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.771Z", + "updatedAt": "2023-09-14T05:59:34.771Z" + } + }, + { + "id": 6, + "lat": -27.388903, + "lng": 152.880154, + "name": { + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.815Z", + "updatedAt": "2023-09-14T05:59:34.815Z" + } + }, + { + "id": 7, + "lat": -27.389463, + "lng": 152.880835, + "name": { + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.823Z", + "updatedAt": "2023-09-14T05:59:34.823Z" + } + }, + { + "id": 8, + "lat": -27.389366, + "lng": 152.880644, + "name": { + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.767Z", + "updatedAt": "2023-09-14T05:59:34.767Z" + } + }, + { + "id": 9, + "lat": -27.389248, + "lng": 152.880525, + "name": { + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.763Z", + "updatedAt": "2023-09-14T05:59:34.763Z" + } + }, + { + "id": 10, + "lat": -27.389158, + "lng": 152.88035, + "name": { + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.760Z", + "updatedAt": "2023-09-14T05:59:34.760Z" + } + }, + { + "id": 11, + "lat": -27.389021, + "lng": 152.880195, + "name": { + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.756Z", + "updatedAt": "2023-09-14T05:59:34.756Z" + } + }, + { + "id": 12, + "lat": -27.389373, + "lng": 152.880195, + "name": { + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.751Z", + "updatedAt": "2023-09-14T05:59:34.751Z" + } + }, + { + "id": 13, + "lat": -27.388316, + "lng": 152.880797, + "name": { + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.827Z", + "updatedAt": "2023-09-14T05:59:34.827Z" + } + }, + { + "id": 14, + "lat": -27.388909, + "lng": 152.881448, + "name": { + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.831Z", + "updatedAt": "2023-09-14T05:59:34.831Z" + } + }, + { + "id": 15, + "lat": -27.388821, + "lng": 152.881503, + "name": { + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.811Z", + "updatedAt": "2023-09-14T05:59:34.811Z" + } + }, + { + "id": 16, + "lat": -27.388766, + "lng": 152.881422, + "name": { + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.808Z", + "updatedAt": "2023-09-14T05:59:34.808Z" + } + }, + { + "id": 17, + "lat": -27.388644, + "lng": 152.881263, + "name": { + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.803Z", + "updatedAt": "2023-09-14T05:59:34.803Z" + } + }, + { + "id": 18, + "lat": -27.388549, + "lng": 152.881107, + "name": { + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.799Z", + "updatedAt": "2023-09-14T05:59:34.799Z" + } + }, + { + "id": 19, + "lat": -27.388445, + "lng": 152.880939, + "name": { + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.795Z", + "updatedAt": "2023-09-14T05:59:34.795Z" + } + }, + { + "id": 20, + "lat": -27.389035, + "lng": 152.881314, + "name": { + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.744Z", + "updatedAt": "2023-09-14T05:59:34.744Z" + } + }, + { + "id": 21, + "lat": -27.389208, + "lng": 152.88122, + "name": { + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.740Z", + "updatedAt": "2023-09-14T05:59:34.740Z" + } + }, + { + "id": 22, + "lat": -27.388974, + "lng": 152.880106, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + } + }, + { + "id": 23, + "lat": -27.389343, + "lng": 152.881089, + "name": { + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.736Z", + "updatedAt": "2023-09-14T05:59:34.736Z" + } + }, + { + "id": 24, + "lat": -27.389472, + "lng": 152.880973, + "name": { + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.731Z", + "updatedAt": "2023-09-14T05:59:34.731Z" + } + }, + { + "id": 25, + "lat": -27.389553, + "lng": 152.880916, + "name": { + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.726Z", + "updatedAt": "2023-09-14T05:59:34.726Z" + } + } + ], + "fauna_plot_point": [] + } + } + }, + "floristics-veg-voucher-lite": [ + { + "id": 8, + "field_name": "Anthocerotidae [Subclassis] (scientific: Anthocerotidae Rosenv.)", + "voucher_barcode": "1233445", + "host_species": null, + "unique_id": null, + "date_time": null, + "createdAt": "2024-04-09T01:24:21.943Z", + "updatedAt": "2024-04-09T01:24:21.943Z", + "growth_form_1": { + "id": 1, + "symbol": "V", + "label": "Sedge", + "description": "Herbaceous, usually perennial erect plant generally with a tufted habit and of the families Cyperaceae (true sedges) or Restionaceae (node sedges).", + "uri": "https://linked.data.gov.au/def/nrm/a7b8b3c8-a329-5a62-8741-e1d6efe9528b", + "createdAt": "2024-03-26T02:39:35.671Z", + "updatedAt": "2024-03-26T02:39:35.671Z" + }, + "growth_form_2": { + "id": 2, + "symbol": "X", + "label": "Grass-tree", + "description": "The 'Growth-form' represents Australian grass-trees. E.g., members of the Xanthorrhoeaceae.", + "uri": "https://linked.data.gov.au/def/nrm/1be47880-4ee6-5df9-8eda-551c58078771", + "createdAt": "2024-03-26T02:39:35.687Z", + "updatedAt": "2024-03-26T02:39:35.687Z" + }, + "habit": [ + { + "id": 5, + "lut": { + "id": 11, + "symbol": "SUC", + "label": "Succulent", + "description": "Fleshy, juicy, soft in texture and usually thickened", + "uri": "", + "createdAt": "2024-03-26T02:39:07.633Z", + "updatedAt": "2024-03-26T02:39:07.633Z" + } + } + ], + "phenology": [ + { + "id": 5, + "lut": { + "id": 9, + "symbol": "SEE", + "label": "Seedling", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:07.771Z", + "updatedAt": "2024-03-26T02:39:07.771Z" + } + } + ], + "floristics_veg_survey_lite": { + "survey_metadata": { + "id": 8, + "org_minted_uuid": "9beeba20-374a-470a-b4f4-caed34e07572", + "createdAt": "2024-03-28T03:17:25.302Z", + "updatedAt": "2024-03-28T03:17:25.302Z", + "survey_details": { + "id": 59, + "survey_model": "floristics-veg-survey-lite", + "time": "2024-03-28T03:17:01.727Z", + "uuid": "263f710d-9af5-456a-9c68-b2b675763220", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 59, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": "0.1.0-a2ba7a48", + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": "Monitor FDCP API-production", + "system_org": null + } + }, + "start_date_time": "2022-09-21T01:55:44.186Z", + "end_date_time": "2022-09-21T01:55:44.186Z", + "createdAt": "2023-09-14T06:00:17.099Z", + "updatedAt": "2023-09-14T06:00:17.099Z", + "plot_visit": { + "start_date": "2021-08-26T11:26:54.317Z", + "end_date": "2021-08-26T13:26:54.317Z", + "visit_field_name": "QLD winter survey", + "createdAt": "2023-09-14T06:00:12.120Z", + "updatedAt": "2023-09-14T06:00:17.882Z", + "plot_layout": { + "id": 1, + "replicate": 1, + "is_the_plot_permanently_marked": false, + "orientation": 1, + "createdAt": "2023-09-14T06:00:11.473Z", + "updatedAt": "2023-09-14T06:00:11.473Z", + "plot_selection": { + "plot_label": "QDASEQ0001", + "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f1", + "comments": "some comment", + "createdAt": "2023-09-14T05:59:34.851Z", + "updatedAt": "2023-09-14T05:59:34.851Z", + "plot_name": { + "id": 1, + "unique_digits": "0001", + "state": { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + }, + "program": { + "symbol": "NLP", + "label": "National Landcare Program", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.006Z", + "updatedAt": "2023-09-14T05:59:34.006Z" + }, + "bioregion": { + "symbol": "SEQ", + "label": "South Eastern Queensland", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.695Z", + "updatedAt": "2023-09-14T05:59:39.355Z", + "states": [ + { + "symbol": "NS", + "label": "New South Wales", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/14b8c3fe-3a60-5f3a-bb7a-b9cf5df49b4e", + "createdAt": "2023-09-14T05:59:33.966Z", + "updatedAt": "2023-09-14T05:59:33.966Z" + }, + { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + } + ] + } + }, + "recommended_location": { + "id": 1, + "lat": -35.0004723472134, + "lng": 138.66067886352542 + }, + "recommended_location_point": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + }, + "plot_selection_survey": null + }, + "plot_type": { + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.672Z", + "updatedAt": "2023-09-14T05:59:53.672Z" + }, + "permanently_marked": null, + "plot_dimensions": { + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2023-09-14T05:59:53.412Z", + "updatedAt": "2023-09-14T05:59:53.412Z" + }, + "plot_points": [ + { + "id": 2, + "lat": -27.388252, + "lng": 152.880694, + "name": { + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.787Z", + "updatedAt": "2023-09-14T05:59:34.787Z" + } + }, + { + "id": 1, + "lat": -27.388336, + "lng": 152.880651, + "name": { + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.783Z", + "updatedAt": "2023-09-14T05:59:34.783Z" + } + }, + { + "id": 3, + "lat": -27.388483, + "lng": 152.880518, + "name": { + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.779Z", + "updatedAt": "2023-09-14T05:59:34.779Z" + } + }, + { + "id": 4, + "lat": -27.388611, + "lng": 152.880389, + "name": { + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.775Z", + "updatedAt": "2023-09-14T05:59:34.775Z" + } + }, + { + "id": 5, + "lat": -27.388749, + "lng": 152.88028, + "name": { + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.771Z", + "updatedAt": "2023-09-14T05:59:34.771Z" + } + }, + { + "id": 6, + "lat": -27.388903, + "lng": 152.880154, + "name": { + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.815Z", + "updatedAt": "2023-09-14T05:59:34.815Z" + } + }, + { + "id": 7, + "lat": -27.389463, + "lng": 152.880835, + "name": { + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.823Z", + "updatedAt": "2023-09-14T05:59:34.823Z" + } + }, + { + "id": 8, + "lat": -27.389366, + "lng": 152.880644, + "name": { + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.767Z", + "updatedAt": "2023-09-14T05:59:34.767Z" + } + }, + { + "id": 9, + "lat": -27.389248, + "lng": 152.880525, + "name": { + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.763Z", + "updatedAt": "2023-09-14T05:59:34.763Z" + } + }, + { + "id": 10, + "lat": -27.389158, + "lng": 152.88035, + "name": { + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.760Z", + "updatedAt": "2023-09-14T05:59:34.760Z" + } + }, + { + "id": 11, + "lat": -27.389021, + "lng": 152.880195, + "name": { + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.756Z", + "updatedAt": "2023-09-14T05:59:34.756Z" + } + }, + { + "id": 12, + "lat": -27.389373, + "lng": 152.880195, + "name": { + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.751Z", + "updatedAt": "2023-09-14T05:59:34.751Z" + } + }, + { + "id": 13, + "lat": -27.388316, + "lng": 152.880797, + "name": { + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.827Z", + "updatedAt": "2023-09-14T05:59:34.827Z" + } + }, + { + "id": 14, + "lat": -27.388909, + "lng": 152.881448, + "name": { + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.831Z", + "updatedAt": "2023-09-14T05:59:34.831Z" + } + }, + { + "id": 15, + "lat": -27.388821, + "lng": 152.881503, + "name": { + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.811Z", + "updatedAt": "2023-09-14T05:59:34.811Z" + } + }, + { + "id": 16, + "lat": -27.388766, + "lng": 152.881422, + "name": { + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.808Z", + "updatedAt": "2023-09-14T05:59:34.808Z" + } + }, + { + "id": 17, + "lat": -27.388644, + "lng": 152.881263, + "name": { + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.803Z", + "updatedAt": "2023-09-14T05:59:34.803Z" + } + }, + { + "id": 18, + "lat": -27.388549, + "lng": 152.881107, + "name": { + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.799Z", + "updatedAt": "2023-09-14T05:59:34.799Z" + } + }, + { + "id": 19, + "lat": -27.388445, + "lng": 152.880939, + "name": { + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.795Z", + "updatedAt": "2023-09-14T05:59:34.795Z" + } + }, + { + "id": 20, + "lat": -27.389035, + "lng": 152.881314, + "name": { + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.744Z", + "updatedAt": "2023-09-14T05:59:34.744Z" + } + }, + { + "id": 21, + "lat": -27.389208, + "lng": 152.88122, + "name": { + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.740Z", + "updatedAt": "2023-09-14T05:59:34.740Z" + } + }, + { + "id": 22, + "lat": -27.388974, + "lng": 152.880106, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + } + }, + { + "id": 23, + "lat": -27.389343, + "lng": 152.881089, + "name": { + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.736Z", + "updatedAt": "2023-09-14T05:59:34.736Z" + } + }, + { + "id": 24, + "lat": -27.389472, + "lng": 152.880973, + "name": { + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.731Z", + "updatedAt": "2023-09-14T05:59:34.731Z" + } + }, + { + "id": 25, + "lat": -27.389553, + "lng": 152.880916, + "name": { + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.726Z", + "updatedAt": "2023-09-14T05:59:34.726Z" + } + } + ], + "fauna_plot_point": [] + } + } + }, + "photo": [ + { + "id": 14, + "comment": null, + "single_photo": { + "id": 14, + "name": "debbccdd-1234-4a09-a1e5-3cb442788931.png", + "alternativeText": null, + "caption": null, + "width": 284, + "height": 58, + "formats": { + "thumbnail": { + "ext": ".png", + "url": "https://beta-monitor-binary.s3.ap-southeast-2.amazonaws.com/thumbnail_debbccdd_1234_4a09_a1e5_3cb442788931_8979970800.png", + "hash": "thumbnail_debbccdd_1234_4a09_a1e5_3cb442788931_8979970800", + "mime": "image/png", + "name": "thumbnail_debbccdd-1234-4a09-a1e5-3cb442788931.png", + "path": null, + "size": 7.38, + "width": 245, + "height": 50, + "sizeInBytes": 7375 + } + }, + "hash": "debbccdd_1234_4a09_a1e5_3cb442788931_8979970800", + "ext": ".png", + "mime": "image/png", + "size": 1.33, + "url": "https://beta-monitor-binary.s3.ap-southeast-2.amazonaws.com/debbccdd_1234_4a09_a1e5_3cb442788931_8979970800.png", + "previewUrl": null, + "provider": "aws-s3", + "provider_metadata": null, + "folderPath": "/1", + "createdAt": "2024-04-09T01:24:19.489Z", + "updatedAt": "2024-04-09T01:24:19.489Z" + } + } + ] + }, + { + "id": 8, + "field_name": "Anthocerotidae [Subclassis] (scientific: Anthocerotidae Rosenv.)", + "voucher_barcode": "1233445", + "host_species": null, + "unique_id": null, + "date_time": "2024-04-10T01:23:28.089Z", + "createdAt": "2024-04-09T01:24:21.943Z", + "updatedAt": "2024-04-09T01:24:21.943Z", + "growth_form_1": { + "id": 1, + "symbol": "V", + "label": "Sedge", + "description": "Herbaceous, usually perennial erect plant generally with a tufted habit and of the families Cyperaceae (true sedges) or Restionaceae (node sedges).", + "uri": "https://linked.data.gov.au/def/nrm/a7b8b3c8-a329-5a62-8741-e1d6efe9528b", + "createdAt": "2024-03-26T02:39:35.671Z", + "updatedAt": "2024-03-26T02:39:35.671Z" + }, + "growth_form_2": { + "id": 2, + "symbol": "X", + "label": "Grass-tree", + "description": "The 'Growth-form' represents Australian grass-trees. E.g., members of the Xanthorrhoeaceae.", + "uri": "https://linked.data.gov.au/def/nrm/1be47880-4ee6-5df9-8eda-551c58078771", + "createdAt": "2024-03-26T02:39:35.687Z", + "updatedAt": "2024-03-26T02:39:35.687Z" + }, + "habit": [ + { + "id": 5, + "lut": { + "id": 11, + "symbol": "SUC", + "label": "Succulent", + "description": "Fleshy, juicy, soft in texture and usually thickened", + "uri": "", + "createdAt": "2024-03-26T02:39:07.633Z", + "updatedAt": "2024-03-26T02:39:07.633Z" + } + } + ], + "phenology": [ + { + "id": 5, + "lut": { + "id": 9, + "symbol": "SEE", + "label": "Seedling", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:07.771Z", + "updatedAt": "2024-03-26T02:39:07.771Z" + } + } + ], + "floristics_veg_survey_lite": { + "survey_metadata": { + "id": 8, + "org_minted_uuid": "9beeba20-374a-470a-b4f4-caed34e07572", + "createdAt": "2024-03-28T03:17:25.302Z", + "updatedAt": "2024-03-28T03:17:25.302Z", + "survey_details": { + "id": 59, + "survey_model": "floristics-veg-survey-lite", + "time": "2024-03-28T03:17:01.727Z", + "uuid": "263f710d-9af5-456a-9c68-b2b675763220", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 59, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": "0.1.0-a2ba7a48", + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": "Monitor FDCP API-production", + "system_org": null + } + }, + "start_date_time": "2022-09-21T01:55:44.186Z", + "end_date_time": "2022-09-21T01:55:44.186Z", + "createdAt": "2023-09-14T06:00:17.099Z", + "updatedAt": "2023-09-14T06:00:17.099Z", + "plot_visit": { + "start_date": "2021-08-26T11:26:54.317Z", + "end_date": "2021-08-26T13:26:54.317Z", + "visit_field_name": "QLD winter survey", + "createdAt": "2023-09-14T06:00:12.120Z", + "updatedAt": "2023-09-14T06:00:17.882Z", + "plot_layout": { + "id": 1, + "replicate": 1, + "is_the_plot_permanently_marked": false, + "orientation": 1, + "createdAt": "2023-09-14T06:00:11.473Z", + "updatedAt": "2023-09-14T06:00:11.473Z", + "plot_selection": { + "plot_label": "QDASEQ0001", + "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f1", + "comments": "some comment", + "createdAt": "2023-09-14T05:59:34.851Z", + "updatedAt": "2023-09-14T05:59:34.851Z", + "plot_name": { + "id": 1, + "unique_digits": "0001", + "state": { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + }, + "program": { + "symbol": "NLP", + "label": "National Landcare Program", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.006Z", + "updatedAt": "2023-09-14T05:59:34.006Z" + }, + "bioregion": { + "symbol": "SEQ", + "label": "South Eastern Queensland", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.695Z", + "updatedAt": "2023-09-14T05:59:39.355Z", + "states": [ + { + "symbol": "NS", + "label": "New South Wales", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/14b8c3fe-3a60-5f3a-bb7a-b9cf5df49b4e", + "createdAt": "2023-09-14T05:59:33.966Z", + "updatedAt": "2023-09-14T05:59:33.966Z" + }, + { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + } + ] + } + }, + "recommended_location": { + "id": 1, + "lat": -35.0004723472134, + "lng": 138.66067886352542 + }, + "recommended_location_point": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + }, + "plot_selection_survey": null + }, + "plot_type": { + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.672Z", + "updatedAt": "2023-09-14T05:59:53.672Z" + }, + "permanently_marked": null, + "plot_dimensions": { + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2023-09-14T05:59:53.412Z", + "updatedAt": "2023-09-14T05:59:53.412Z" + }, + "plot_points": [ + { + "id": 2, + "lat": -27.388252, + "lng": 152.880694, + "name": { + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.787Z", + "updatedAt": "2023-09-14T05:59:34.787Z" + } + }, + { + "id": 1, + "lat": -27.388336, + "lng": 152.880651, + "name": { + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.783Z", + "updatedAt": "2023-09-14T05:59:34.783Z" + } + }, + { + "id": 3, + "lat": -27.388483, + "lng": 152.880518, + "name": { + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.779Z", + "updatedAt": "2023-09-14T05:59:34.779Z" + } + }, + { + "id": 4, + "lat": -27.388611, + "lng": 152.880389, + "name": { + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.775Z", + "updatedAt": "2023-09-14T05:59:34.775Z" + } + }, + { + "id": 5, + "lat": -27.388749, + "lng": 152.88028, + "name": { + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.771Z", + "updatedAt": "2023-09-14T05:59:34.771Z" + } + }, + { + "id": 6, + "lat": -27.388903, + "lng": 152.880154, + "name": { + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.815Z", + "updatedAt": "2023-09-14T05:59:34.815Z" + } + }, + { + "id": 7, + "lat": -27.389463, + "lng": 152.880835, + "name": { + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.823Z", + "updatedAt": "2023-09-14T05:59:34.823Z" + } + }, + { + "id": 8, + "lat": -27.389366, + "lng": 152.880644, + "name": { + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.767Z", + "updatedAt": "2023-09-14T05:59:34.767Z" + } + }, + { + "id": 9, + "lat": -27.389248, + "lng": 152.880525, + "name": { + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.763Z", + "updatedAt": "2023-09-14T05:59:34.763Z" + } + }, + { + "id": 10, + "lat": -27.389158, + "lng": 152.88035, + "name": { + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.760Z", + "updatedAt": "2023-09-14T05:59:34.760Z" + } + }, + { + "id": 11, + "lat": -27.389021, + "lng": 152.880195, + "name": { + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.756Z", + "updatedAt": "2023-09-14T05:59:34.756Z" + } + }, + { + "id": 12, + "lat": -27.389373, + "lng": 152.880195, + "name": { + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.751Z", + "updatedAt": "2023-09-14T05:59:34.751Z" + } + }, + { + "id": 13, + "lat": -27.388316, + "lng": 152.880797, + "name": { + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.827Z", + "updatedAt": "2023-09-14T05:59:34.827Z" + } + }, + { + "id": 14, + "lat": -27.388909, + "lng": 152.881448, + "name": { + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.831Z", + "updatedAt": "2023-09-14T05:59:34.831Z" + } + }, + { + "id": 15, + "lat": -27.388821, + "lng": 152.881503, + "name": { + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.811Z", + "updatedAt": "2023-09-14T05:59:34.811Z" + } + }, + { + "id": 16, + "lat": -27.388766, + "lng": 152.881422, + "name": { + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.808Z", + "updatedAt": "2023-09-14T05:59:34.808Z" + } + }, + { + "id": 17, + "lat": -27.388644, + "lng": 152.881263, + "name": { + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.803Z", + "updatedAt": "2023-09-14T05:59:34.803Z" + } + }, + { + "id": 18, + "lat": -27.388549, + "lng": 152.881107, + "name": { + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.799Z", + "updatedAt": "2023-09-14T05:59:34.799Z" + } + }, + { + "id": 19, + "lat": -27.388445, + "lng": 152.880939, + "name": { + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.795Z", + "updatedAt": "2023-09-14T05:59:34.795Z" + } + }, + { + "id": 20, + "lat": -27.389035, + "lng": 152.881314, + "name": { + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.744Z", + "updatedAt": "2023-09-14T05:59:34.744Z" + } + }, + { + "id": 21, + "lat": -27.389208, + "lng": 152.88122, + "name": { + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.740Z", + "updatedAt": "2023-09-14T05:59:34.740Z" + } + }, + { + "id": 22, + "lat": -27.388974, + "lng": 152.880106, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + } + }, + { + "id": 23, + "lat": -27.389343, + "lng": 152.881089, + "name": { + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.736Z", + "updatedAt": "2023-09-14T05:59:34.736Z" + } + }, + { + "id": 24, + "lat": -27.389472, + "lng": 152.880973, + "name": { + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.731Z", + "updatedAt": "2023-09-14T05:59:34.731Z" + } + }, + { + "id": 25, + "lat": -27.389553, + "lng": 152.880916, + "name": { + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.726Z", + "updatedAt": "2023-09-14T05:59:34.726Z" + } + } + ], + "fauna_plot_point": [] + } + } + }, + "photo": [ + { + "id": 14, + "comment": null, + "single_photo": { + "id": 14, + "name": "debbccdd-1234-4a09-a1e5-3cb442788931.png", + "alternativeText": null, + "caption": null, + "width": 284, + "height": 58, + "formats": { + "thumbnail": { + "ext": ".png", + "url": "https://beta-monitor-binary.s3.ap-southeast-2.amazonaws.com/thumbnail_debbccdd_1234_4a09_a1e5_3cb442788931_8979970800.png", + "hash": "thumbnail_debbccdd_1234_4a09_a1e5_3cb442788931_8979970800", + "mime": "image/png", + "name": "thumbnail_debbccdd-1234-4a09-a1e5-3cb442788931.png", + "path": null, + "size": 7.38, + "width": 245, + "height": 50, + "sizeInBytes": 7375 + } + }, + "hash": "debbccdd_1234_4a09_a1e5_3cb442788931_8979970800", + "ext": ".png", + "mime": "image/png", + "size": 1.33, + "url": "https://beta-monitor-binary.s3.ap-southeast-2.amazonaws.com/debbccdd_1234_4a09_a1e5_3cb442788931_8979970800.png", + "previewUrl": null, + "provider": "aws-s3", + "provider_metadata": null, + "folderPath": "/1", + "createdAt": "2024-04-09T01:24:19.489Z", + "updatedAt": "2024-04-09T01:24:19.489Z" + } + } + ] + },{ + "id": 8, + "field_name": "Anthocerotidae [Subclassis] (scientific: Anthocerotidae Rosenv.)", + "voucher_barcode": "1233445", + "host_species": null, + "unique_id": null, + "date_time": "2024-04-08T01:23:28.089Z", + "createdAt": "2024-04-09T01:24:21.943Z", + "updatedAt": "2024-04-09T01:24:21.943Z", + "growth_form_1": { + "id": 1, + "symbol": "V", + "label": "Sedge", + "description": "Herbaceous, usually perennial erect plant generally with a tufted habit and of the families Cyperaceae (true sedges) or Restionaceae (node sedges).", + "uri": "https://linked.data.gov.au/def/nrm/a7b8b3c8-a329-5a62-8741-e1d6efe9528b", + "createdAt": "2024-03-26T02:39:35.671Z", + "updatedAt": "2024-03-26T02:39:35.671Z" + }, + "growth_form_2": { + "id": 2, + "symbol": "X", + "label": "Grass-tree", + "description": "The 'Growth-form' represents Australian grass-trees. E.g., members of the Xanthorrhoeaceae.", + "uri": "https://linked.data.gov.au/def/nrm/1be47880-4ee6-5df9-8eda-551c58078771", + "createdAt": "2024-03-26T02:39:35.687Z", + "updatedAt": "2024-03-26T02:39:35.687Z" + }, + "habit": [ + { + "id": 5, + "lut": { + "id": 11, + "symbol": "SUC", + "label": "Succulent", + "description": "Fleshy, juicy, soft in texture and usually thickened", + "uri": "", + "createdAt": "2024-03-26T02:39:07.633Z", + "updatedAt": "2024-03-26T02:39:07.633Z" + } + } + ], + "phenology": [ + { + "id": 5, + "lut": { + "id": 9, + "symbol": "SEE", + "label": "Seedling", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:07.771Z", + "updatedAt": "2024-03-26T02:39:07.771Z" + } + } + ], + "floristics_veg_survey_lite": { + "survey_metadata": { + "id": 8, + "org_minted_uuid": "9beeba20-374a-470a-b4f4-caed34e07572", + "createdAt": "2024-03-28T03:17:25.302Z", + "updatedAt": "2024-03-28T03:17:25.302Z", + "survey_details": { + "id": 59, + "survey_model": "floristics-veg-survey-lite", + "time": "2024-03-28T03:17:01.727Z", + "uuid": "263f710d-9af5-456a-9c68-b2b675763220", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 59, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": "0.1.0-a2ba7a48", + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": "Monitor FDCP API-production", + "system_org": null + } + }, + "start_date_time": "2022-09-21T01:55:44.186Z", + "end_date_time": "2022-09-21T01:55:44.186Z", + "createdAt": "2023-09-14T06:00:17.099Z", + "updatedAt": "2023-09-14T06:00:17.099Z", + "plot_visit": { + "start_date": "2021-08-26T11:26:54.317Z", + "end_date": "2021-08-26T13:26:54.317Z", + "visit_field_name": "QLD winter survey", + "createdAt": "2023-09-14T06:00:12.120Z", + "updatedAt": "2023-09-14T06:00:17.882Z", + "plot_layout": { + "id": 1, + "replicate": 1, + "is_the_plot_permanently_marked": false, + "orientation": 1, + "createdAt": "2023-09-14T06:00:11.473Z", + "updatedAt": "2023-09-14T06:00:11.473Z", + "plot_selection": { + "plot_label": "QDASEQ0001", + "uuid": "a6f84a0e-2239-4bfa-aac6-6d2c99e252f1", + "comments": "some comment", + "createdAt": "2023-09-14T05:59:34.851Z", + "updatedAt": "2023-09-14T05:59:34.851Z", + "plot_name": { + "id": 1, + "unique_digits": "0001", + "state": { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + }, + "program": { + "symbol": "NLP", + "label": "National Landcare Program", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.006Z", + "updatedAt": "2023-09-14T05:59:34.006Z" + }, + "bioregion": { + "symbol": "SEQ", + "label": "South Eastern Queensland", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:34.695Z", + "updatedAt": "2023-09-14T05:59:39.355Z", + "states": [ + { + "symbol": "NS", + "label": "New South Wales", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/14b8c3fe-3a60-5f3a-bb7a-b9cf5df49b4e", + "createdAt": "2023-09-14T05:59:33.966Z", + "updatedAt": "2023-09-14T05:59:33.966Z" + }, + { + "symbol": "QD", + "label": "Queensland", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/d9d48a48-f74e-55b7-a360-82975c6be412", + "createdAt": "2023-09-14T05:59:33.978Z", + "updatedAt": "2023-09-14T05:59:33.978Z" + } + ] + } + }, + "recommended_location": { + "id": 1, + "lat": -35.0004723472134, + "lng": 138.66067886352542 + }, + "recommended_location_point": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + }, + "plot_selection_survey": null + }, + "plot_type": { + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2023-09-14T05:59:53.672Z", + "updatedAt": "2023-09-14T05:59:53.672Z" + }, + "permanently_marked": null, + "plot_dimensions": { + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2023-09-14T05:59:53.412Z", + "updatedAt": "2023-09-14T05:59:53.412Z" + }, + "plot_points": [ + { + "id": 2, + "lat": -27.388252, + "lng": 152.880694, + "name": { + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.787Z", + "updatedAt": "2023-09-14T05:59:34.787Z" + } + }, + { + "id": 1, + "lat": -27.388336, + "lng": 152.880651, + "name": { + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.783Z", + "updatedAt": "2023-09-14T05:59:34.783Z" + } + }, + { + "id": 3, + "lat": -27.388483, + "lng": 152.880518, + "name": { + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.779Z", + "updatedAt": "2023-09-14T05:59:34.779Z" + } + }, + { + "id": 4, + "lat": -27.388611, + "lng": 152.880389, + "name": { + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.775Z", + "updatedAt": "2023-09-14T05:59:34.775Z" + } + }, + { + "id": 5, + "lat": -27.388749, + "lng": 152.88028, + "name": { + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.771Z", + "updatedAt": "2023-09-14T05:59:34.771Z" + } + }, + { + "id": 6, + "lat": -27.388903, + "lng": 152.880154, + "name": { + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.815Z", + "updatedAt": "2023-09-14T05:59:34.815Z" + } + }, + { + "id": 7, + "lat": -27.389463, + "lng": 152.880835, + "name": { + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.823Z", + "updatedAt": "2023-09-14T05:59:34.823Z" + } + }, + { + "id": 8, + "lat": -27.389366, + "lng": 152.880644, + "name": { + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.767Z", + "updatedAt": "2023-09-14T05:59:34.767Z" + } + }, + { + "id": 9, + "lat": -27.389248, + "lng": 152.880525, + "name": { + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.763Z", + "updatedAt": "2023-09-14T05:59:34.763Z" + } + }, + { + "id": 10, + "lat": -27.389158, + "lng": 152.88035, + "name": { + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.760Z", + "updatedAt": "2023-09-14T05:59:34.760Z" + } + }, + { + "id": 11, + "lat": -27.389021, + "lng": 152.880195, + "name": { + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.756Z", + "updatedAt": "2023-09-14T05:59:34.756Z" + } + }, + { + "id": 12, + "lat": -27.389373, + "lng": 152.880195, + "name": { + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.751Z", + "updatedAt": "2023-09-14T05:59:34.751Z" + } + }, + { + "id": 13, + "lat": -27.388316, + "lng": 152.880797, + "name": { + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2023-09-14T05:59:34.827Z", + "updatedAt": "2023-09-14T05:59:34.827Z" + } + }, + { + "id": 14, + "lat": -27.388909, + "lng": 152.881448, + "name": { + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2023-09-14T05:59:34.831Z", + "updatedAt": "2023-09-14T05:59:34.831Z" + } + }, + { + "id": 15, + "lat": -27.388821, + "lng": 152.881503, + "name": { + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.811Z", + "updatedAt": "2023-09-14T05:59:34.811Z" + } + }, + { + "id": 16, + "lat": -27.388766, + "lng": 152.881422, + "name": { + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.808Z", + "updatedAt": "2023-09-14T05:59:34.808Z" + } + }, + { + "id": 17, + "lat": -27.388644, + "lng": 152.881263, + "name": { + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.803Z", + "updatedAt": "2023-09-14T05:59:34.803Z" + } + }, + { + "id": 18, + "lat": -27.388549, + "lng": 152.881107, + "name": { + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.799Z", + "updatedAt": "2023-09-14T05:59:34.799Z" + } + }, + { + "id": 19, + "lat": -27.388445, + "lng": 152.880939, + "name": { + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.795Z", + "updatedAt": "2023-09-14T05:59:34.795Z" + } + }, + { + "id": 20, + "lat": -27.389035, + "lng": 152.881314, + "name": { + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2023-09-14T05:59:34.744Z", + "updatedAt": "2023-09-14T05:59:34.744Z" + } + }, + { + "id": 21, + "lat": -27.389208, + "lng": 152.88122, + "name": { + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2023-09-14T05:59:34.740Z", + "updatedAt": "2023-09-14T05:59:34.740Z" + } + }, + { + "id": 22, + "lat": -27.388974, + "lng": 152.880106, + "name": { + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2023-09-14T05:59:34.718Z", + "updatedAt": "2023-09-14T05:59:34.718Z" + } + }, + { + "id": 23, + "lat": -27.389343, + "lng": 152.881089, + "name": { + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2023-09-14T05:59:34.736Z", + "updatedAt": "2023-09-14T05:59:34.736Z" + } + }, + { + "id": 24, + "lat": -27.389472, + "lng": 152.880973, + "name": { + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2023-09-14T05:59:34.731Z", + "updatedAt": "2023-09-14T05:59:34.731Z" + } + }, + { + "id": 25, + "lat": -27.389553, + "lng": 152.880916, + "name": { + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2023-09-14T05:59:34.726Z", + "updatedAt": "2023-09-14T05:59:34.726Z" + } + } + ], + "fauna_plot_point": [] + } + } + }, + "photo": [ + { + "id": 14, + "comment": null, + "single_photo": { + "id": 14, + "name": "debbccdd-1234-4a09-a1e5-3cb442788931.png", + "alternativeText": null, + "caption": null, + "width": 284, + "height": 58, + "formats": { + "thumbnail": { + "ext": ".png", + "url": "https://beta-monitor-binary.s3.ap-southeast-2.amazonaws.com/thumbnail_debbccdd_1234_4a09_a1e5_3cb442788931_8979970800.png", + "hash": "thumbnail_debbccdd_1234_4a09_a1e5_3cb442788931_8979970800", + "mime": "image/png", + "name": "thumbnail_debbccdd-1234-4a09-a1e5-3cb442788931.png", + "path": null, + "size": 7.38, + "width": 245, + "height": 50, + "sizeInBytes": 7375 + } + }, + "hash": "debbccdd_1234_4a09_a1e5_3cb442788931_8979970800", + "ext": ".png", + "mime": "image/png", + "size": 1.33, + "url": "https://beta-monitor-binary.s3.ap-southeast-2.amazonaws.com/debbccdd_1234_4a09_a1e5_3cb442788931_8979970800.png", + "previewUrl": null, + "provider": "aws-s3", + "provider_metadata": null, + "folderPath": "/1", + "createdAt": "2024-04-09T01:24:19.489Z", + "updatedAt": "2024-04-09T01:24:19.489Z" + } + } + ] + } + ], + "floristics-veg-virtual-voucher": [] + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/mintCollectionIdBasalAreaPayload.json b/src/test/resources/paratoo/mintCollectionIdBasalAreaPayload.json new file mode 100644 index 000000000..41d3ad45e --- /dev/null +++ b/src/test/resources/paratoo/mintCollectionIdBasalAreaPayload.json @@ -0,0 +1,24 @@ +{ + "eventTime": "2023-09-22T01:03:16.556Z", + "userId": "org-1", + "survey_metadata": { + "orgMintedUUID": "d1", + "survey_details": { + "survey_model": "basal-area-dbh-measure-survey", + "time": "2023-09-22T01:03:15.556Z", + "uuid": "43389075", + "project_id": "p1", + "protocol_id": "guid-2", + "protocol_version": "1" + }, + "provenance":{ + "version_app": "0.0.1-xxxxx", + "version_core": "0.1.0-1fb53f81", + "version_core_documentation": "0.0.1-xxxxx", + "version_org": "4.4-SNAPSHOT", + "system_app": "monitor", + "system_core": "Monitor-dummy-data-production", + "system_org": "MERIT" + } + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/mintCollectionIdPayload.json b/src/test/resources/paratoo/mintCollectionIdPayload.json new file mode 100644 index 000000000..f7d30648b --- /dev/null +++ b/src/test/resources/paratoo/mintCollectionIdPayload.json @@ -0,0 +1,21 @@ +{ + "survey_metadata": { + "survey_details": { + "survey_model": "coarse-woody-debris-survey", + "time": "2024-03-18T20:00:25.365Z", + "uuid": "c670ab74-ffbe-4a49-9798-a096c4de918d", + "project_id": "p1", + "protocol_id": "guid-1", + "protocol_version": "1" + }, + "provenance":{ + "version_app": "0.0.1-xxxxx", + "version_core": "0.1.0-1fb53f81", + "version_core_documentation": "0.0.1-xxxxx", + "version_org": "4.4-SNAPSHOT", + "system_app": "monitor", + "system_core": "Monitor-dummy-data-production", + "system_org": "MERIT" + } + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/opportunisticSurveyObservationsReverseLookup.json b/src/test/resources/paratoo/opportunisticSurveyObservationsReverseLookup.json new file mode 100644 index 000000000..c22fe31d6 --- /dev/null +++ b/src/test/resources/paratoo/opportunisticSurveyObservationsReverseLookup.json @@ -0,0 +1,174 @@ +{ + "survey_metadata": { + "id": 11, + "org_minted_uuid": "97ad50c5-984c-4ffc-aeb8-ee0543fa256f", + "createdAt": "2024-04-03T03:39:59.770Z", + "updatedAt": "2024-04-03T03:39:59.770Z", + "survey_details": { + "id": 66, + "survey_model": "opportunistic-survey", + "time": "2024-04-03T03:38:00.548Z", + "uuid": "8c2491e5-06e0-4833-8411-628bbf7e07c0", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "068d17e8-e042-ae42-1e42-cff4006e64b0", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 66, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": null, + "system_org": null + } + }, + "collections": { + "opportunistic-survey": { + "id": 4, + "start_date_time": "2024-04-03T03:37:54.211Z", + "end_date_time": "2024-04-03T03:39:40.156Z", + "createdAt": "2024-04-03T03:39:58.425Z", + "updatedAt": "2024-04-03T03:39:58.425Z", + "survey_metadata": { + "id": 55, + "orgMintedUUID": "97ad50c5-984c-4ffc-aeb8-ee0543fa256f", + "survey_details": { + "id": 65, + "survey_model": "opportunistic-survey", + "time": "2024-04-03T03:38:00.548Z", + "uuid": "8c2491e5-06e0-4833-8411-628bbf7e07c0", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "068d17e8-e042-ae42-1e42-cff4006e64b0", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 65, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": null, + "system_org": null + } + } + }, + "opportunistic-observation": [ + { + "id": 1, + "observation_id": "OPP001", + "date_time": "2024-04-03T03:38:00.616Z", + "species": "Eucalyptus hispida [Species] (scientific: Eucalyptus hispida (Sm.) Brooker)", + "confident": true, + "number_of_individuals": 10, + "other_observation_method_tier_2": null, + "comments": null, + "add_additional_fauna_data": false, + "createdAt": "2024-04-03T03:39:58.547Z", + "updatedAt": "2024-04-03T03:39:58.547Z", + "media": [], + "location": { + "id": 40, + "lat": -35.0005, + "lng": 138.63 + }, + "taxa_type": { + "id": 1, + "symbol": "VP", + "label": "Vascular plant", + "description": "Refers to the target taxa studied in a fauna survey. Non-vascular plants are plants that do not possess a true vascular tissue (such as xylem-water conducting, phloem-sugar transport). Instead, they may possess simpler tissues that have specialized functions for the internal transport of food and water. They are members of bryophytes for example.", + "uri": "https://linked.data.gov.au/def/nrm/599d9085-baad-5f32-86dc-3d112ecdae9c", + "createdAt": "2024-03-26T02:39:34.521Z", + "updatedAt": "2024-03-26T02:39:34.521Z" + }, + "exact_or_estimate": { + "id": 2, + "symbol": "Estimate", + "label": "Estimate", + "description": "Whether the quantity is 'exact' or an 'estimate'.", + "uri": "https://linked.data.gov.au/def/nrm/68c919a7-5759-5169-92dd-a99b6b7be407", + "createdAt": "2024-03-26T02:39:05.258Z", + "updatedAt": "2024-03-26T02:39:05.258Z" + }, + "observation_method_tier_1": null, + "observation_method_tier_2": null, + "observation_method_tier_3": null, + "observers": [ + { + "id": 3, + "observer": "x" + } + ], + "habitat_description": { + "id": 5, + "symbol": "MVG5", + "label": "Eucalypt Woodlands", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:16.462Z", + "updatedAt": "2024-03-26T02:39:16.462Z" + }, + "growth_form_and_life_stage": [ + { + "id": 1, + "growth_form": { + "id": 1, + "symbol": "Tree", + "label": "Tree", + "description": "Woody plants, more than 2m tall with a single stem or branches well above the base.", + "uri": "", + "createdAt": "2024-03-26T02:39:16.056Z", + "updatedAt": "2024-03-26T02:39:16.056Z" + }, + "life_stage": { + "id": 8, + "symbol": "DD", + "label": "Dead/dormant", + "description": "Indicates above-ground material only is dead and includes plant species that may still have dormant below-ground organs (eg orchids, lilies etc.)", + "uri": "", + "createdAt": "2024-03-26T02:39:03.169Z", + "updatedAt": "2024-03-26T02:39:03.169Z" + } + } + ], + "selections_option": [], + "vouchered_specimens": [], + "opportunistic_survey": { + "id": 4, + "start_date_time": "2024-04-03T03:37:54.211Z", + "end_date_time": "2024-04-03T03:39:40.156Z", + "createdAt": "2024-04-03T03:39:58.425Z", + "updatedAt": "2024-04-03T03:39:58.425Z", + "survey_metadata": { + "id": 55, + "orgMintedUUID": "97ad50c5-984c-4ffc-aeb8-ee0543fa256f", + "survey_details": { + "id": 65, + "survey_model": "opportunistic-survey", + "time": "2024-04-03T03:38:00.548Z", + "uuid": "8c2491e5-06e0-4833-8411-628bbf7e07c0", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "068d17e8-e042-ae42-1e42-cff4006e64b0", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 65, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": null, + "system_org": null + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/photoPointSurveyReverseLookup.json b/src/test/resources/paratoo/photoPointSurveyReverseLookup.json new file mode 100644 index 000000000..77fa093c0 --- /dev/null +++ b/src/test/resources/paratoo/photoPointSurveyReverseLookup.json @@ -0,0 +1,574 @@ +{ + "survey_metadata": { + "id": 15, + "org_minted_uuid": "de40f23c-66f7-41ce-863a-01621de38f33", + "createdAt": "2024-04-04T07:27:20.429Z", + "updatedAt": "2024-04-04T07:27:20.429Z", + "survey_details": { + "id": 79, + "survey_model": "photopoints-survey", + "time": "2024-04-04T07:25:54.795Z", + "uuid": "446c5440-ce7e-41db-afa6-0d17ab4d99d2", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "5fd206b5-25cb-4371-bd90-7b2e8801ea25", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 79, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-2405:b000:600:b0::764b-Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "system_core": null, + "system_org": null + } + }, + "collections": { + "photopoints-survey": { + "id": 1, + "date_time": "2024-04-04T07:23:15.352Z", + "createdAt": "2024-04-04T07:27:19.478Z", + "updatedAt": "2024-04-04T07:27:19.478Z", + "photopoints_protocol_variant": { + "id": 2, + "symbol": "full", + "label": "Full", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:17.189Z", + "updatedAt": "2024-03-26T02:39:17.189Z" + }, + "plot_visit": { + "id": 14, + "start_date": "2024-04-04T07:22:36.714Z", + "end_date": null, + "visit_field_name": "n", + "createdAt": "2024-04-04T07:27:08.634Z", + "updatedAt": "2024-04-04T07:27:08.634Z", + "plot_layout": { + "id": 11, + "orientation": null, + "replicate": 1, + "is_the_plot_permanently_marked": false, + "createdAt": "2024-03-26T02:48:14.152Z", + "updatedAt": "2024-03-26T02:48:14.152Z", + "plot_selection": { + "id": 14, + "plot_label": "SATRAFLB0000", + "uuid": "5c58769b-16e4-4b49-8a64-8c0a443ac103", + "comments": null, + "date_time": "2024-03-26T02:45:54.348Z", + "createdAt": "2024-03-26T02:46:21.411Z", + "updatedAt": "2024-03-26T02:46:21.411Z", + "plot_name": { + "id": 14, + "unique_digits": "0000", + "state": { + "id": 5, + "symbol": "SA", + "label": "South Australia", + "description": "State/Jurisdiction where the study is/was conducted.", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2024-03-26T02:39:32.222Z", + "updatedAt": "2024-03-26T02:39:32.222Z" + }, + "program": { + "id": 8, + "symbol": "TRA", + "label": "Training", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:18.491Z", + "updatedAt": "2024-03-26T02:39:18.491Z" + }, + "bioregion": { + "id": 27, + "symbol": "FLB", + "label": "Flinders Lofty Block", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:40.966Z", + "updatedAt": "2024-03-26T02:39:40.966Z", + "states": [ + { + "id": 5, + "symbol": "SA", + "label": "South Australia", + "description": "State/Jurisdiction where the study is/was conducted.", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2024-03-26T02:39:32.222Z", + "updatedAt": "2024-03-26T02:39:32.222Z" + } + ] + } + }, + "recommended_location": { + "id": 30, + "lat": -34.97937723550951, + "lng": 138.63613573039598 + }, + "recommended_location_point": { + "id": 1, + "symbol": "SW", + "label": "South West", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:17.274Z", + "updatedAt": "2024-03-26T02:39:17.274Z" + }, + "plot_selection_survey": { + "id": 5, + "startdate": null, + "createdAt": "2024-03-26T02:46:19.790Z", + "updatedAt": "2024-03-26T02:46:19.790Z", + "survey_metadata": { + "id": 40, + "orgMintedUUID": null, + "survey_details": { + "id": 40, + "survey_model": "plot-selection-survey", + "time": "2024-03-26T02:46:16.206Z", + "uuid": "50e7ff9f-9f61-41f4-a8f4-e467852aacc4", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "a9cb9e38-690f-41c9-8151-06108caf539d", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 40, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-129.127.180.198-Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "system_core": null, + "system_org": null + } + } + } + }, + "plot_type": { + "id": 1, + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:18.129Z", + "updatedAt": "2024-03-26T02:39:18.129Z" + }, + "permanently_marked": null, + "plot_dimensions": { + "id": 1, + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2024-03-26T02:39:17.254Z", + "updatedAt": "2024-03-26T02:39:17.254Z" + }, + "plot_points": [ + { + "id": 211, + "lat": -34.9765632, + "lng": 138.6315776, + "name": { + "id": 22, + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2024-03-26T02:39:17.807Z", + "updatedAt": "2024-03-26T02:39:17.807Z" + } + }, + { + "id": 212, + "lat": -34.97647336847159, + "lng": 138.6315776, + "name": { + "id": 12, + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.675Z", + "updatedAt": "2024-03-26T02:39:17.675Z" + } + }, + { + "id": 213, + "lat": -34.97629370541477, + "lng": 138.6315776, + "name": { + "id": 13, + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.686Z", + "updatedAt": "2024-03-26T02:39:17.686Z" + } + }, + { + "id": 214, + "lat": -34.97611404235794, + "lng": 138.6315776, + "name": { + "id": 14, + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.700Z", + "updatedAt": "2024-03-26T02:39:17.700Z" + } + }, + { + "id": 215, + "lat": -34.975934379301115, + "lng": 138.6315776, + "name": { + "id": 15, + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.713Z", + "updatedAt": "2024-03-26T02:39:17.713Z" + } + }, + { + "id": 225, + "lat": -34.975754716244296, + "lng": 138.6315776, + "name": { + "id": 16, + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.732Z", + "updatedAt": "2024-03-26T02:39:17.732Z" + } + }, + { + "id": 217, + "lat": -34.97566488471588, + "lng": 138.6315776, + "name": { + "id": 24, + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2024-03-26T02:39:17.833Z", + "updatedAt": "2024-03-26T02:39:17.833Z" + } + }, + { + "id": 216, + "lat": -34.97566488471588, + "lng": 138.63168723265542, + "name": { + "id": 17, + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.743Z", + "updatedAt": "2024-03-26T02:39:17.743Z" + } + }, + { + "id": 219, + "lat": -34.97566488471588, + "lng": 138.63190649796624, + "name": { + "id": 18, + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.752Z", + "updatedAt": "2024-03-26T02:39:17.752Z" + } + }, + { + "id": 221, + "lat": -34.97566488471588, + "lng": 138.63212576327703, + "name": { + "id": 19, + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.764Z", + "updatedAt": "2024-03-26T02:39:17.764Z" + } + }, + { + "id": 226, + "lat": -34.97566488471588, + "lng": 138.63234502858785, + "name": { + "id": 20, + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.783Z", + "updatedAt": "2024-03-26T02:39:17.783Z" + } + }, + { + "id": 218, + "lat": -34.97566488471588, + "lng": 138.63256429389867, + "name": { + "id": 21, + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.794Z", + "updatedAt": "2024-03-26T02:39:17.794Z" + } + }, + { + "id": 223, + "lat": -34.97566488471588, + "lng": 138.63267392655408, + "name": { + "id": 25, + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2024-03-26T02:39:17.844Z", + "updatedAt": "2024-03-26T02:39:17.844Z" + } + }, + { + "id": 220, + "lat": -34.975754716244296, + "lng": 138.63267392655408, + "name": { + "id": 6, + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.608Z", + "updatedAt": "2024-03-26T02:39:17.608Z" + } + }, + { + "id": 222, + "lat": -34.975934379301115, + "lng": 138.63267392655408, + "name": { + "id": 5, + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.598Z", + "updatedAt": "2024-03-26T02:39:17.598Z" + } + }, + { + "id": 224, + "lat": -34.97611404235794, + "lng": 138.63267392655408, + "name": { + "id": 4, + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.585Z", + "updatedAt": "2024-03-26T02:39:17.585Z" + } + }, + { + "id": 227, + "lat": -34.97629370541477, + "lng": 138.63267392655408, + "name": { + "id": 3, + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.575Z", + "updatedAt": "2024-03-26T02:39:17.575Z" + } + }, + { + "id": 228, + "lat": -34.97647336847159, + "lng": 138.63267392655408, + "name": { + "id": 2, + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.564Z", + "updatedAt": "2024-03-26T02:39:17.564Z" + } + }, + { + "id": 229, + "lat": -34.9765632, + "lng": 138.63267392655408, + "name": { + "id": 23, + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2024-03-26T02:39:17.821Z", + "updatedAt": "2024-03-26T02:39:17.821Z" + } + }, + { + "id": 230, + "lat": -34.9765632, + "lng": 138.63256429389867, + "name": { + "id": 11, + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.664Z", + "updatedAt": "2024-03-26T02:39:17.664Z" + } + }, + { + "id": 231, + "lat": -34.9765632, + "lng": 138.63234502858785, + "name": { + "id": 10, + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.654Z", + "updatedAt": "2024-03-26T02:39:17.654Z" + } + }, + { + "id": 232, + "lat": -34.9765632, + "lng": 138.63212576327703, + "name": { + "id": 9, + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.643Z", + "updatedAt": "2024-03-26T02:39:17.643Z" + } + }, + { + "id": 233, + "lat": -34.9765632, + "lng": 138.63190649796624, + "name": { + "id": 8, + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.629Z", + "updatedAt": "2024-03-26T02:39:17.629Z" + } + }, + { + "id": 234, + "lat": -34.9765632, + "lng": 138.63168723265542, + "name": { + "id": 7, + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.619Z", + "updatedAt": "2024-03-26T02:39:17.619Z" + } + }, + { + "id": 235, + "lat": -34.97611404235794, + "lng": 138.63212576327706, + "name": { + "id": 1, + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2024-03-26T02:39:17.548Z", + "updatedAt": "2024-03-26T02:39:17.548Z" + } + } + ], + "fauna_plot_point": [] + } + }, + "plot_centre_post_location": { + "id": 54, + "lat": -34.97615096216043, + "lng": 138.63217757491987 + }, + "photopoint_1_panorama": null, + "photopoint_2_panorama": null, + "photopoint_3_panorama": null, + "steps": [ + { + "id": 1, + "barcode_content": "{\"plot_name\":\"SATRAFLB0000\",\"photopoint_position\":1,\"lat\":-34.97639711283459,\"lng\":138.63245648736378}" + }, + { + "id": 2, + "barcode_content": "{\"plot_name\":\"SATRAFLB0000\",\"photopoint_position\":2,\"lat\":-34.97642128830733,\"lng\":138.63193076507343}" + }, + { + "id": 3, + "barcode_content": "{\"plot_name\":\"SATRAFLB0000\",\"photopoint_position\":3,\"lat\":-34.976016897642765,\"lng\":138.6322502310389}" + } + ], + "survey_metadata": { + "id": 64, + "orgMintedUUID": "de40f23c-66f7-41ce-863a-01621de38f33", + "survey_details": { + "id": 78, + "survey_model": "photopoints-survey", + "time": "2024-04-04T07:25:54.795Z", + "uuid": "446c5440-ce7e-41db-afa6-0d17ab4d99d2", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "5fd206b5-25cb-4371-bd90-7b2e8801ea25", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 78, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-2405:b000:600:b0::764b-Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "system_core": null, + "system_org": null + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/plotDefinitionSurveyReverseLookup.json b/src/test/resources/paratoo/plotDefinitionSurveyReverseLookup.json new file mode 100644 index 000000000..77e9fa776 --- /dev/null +++ b/src/test/resources/paratoo/plotDefinitionSurveyReverseLookup.json @@ -0,0 +1,542 @@ +{ + "survey_metadata": { + "id": 14, + "org_minted_uuid": "50796536-23c7-4f9b-8261-62519287b84d", + "createdAt": "2024-04-04T07:27:10.146Z", + "updatedAt": "2024-04-04T07:27:10.146Z", + "survey_details": { + "id": 77, + "survey_model": "plot-definition-survey", + "time": "2024-04-04T07:21:17.119Z", + "uuid": "46bd9d57-a937-4ef0-bde6-41fd167ad17a", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "d7179862-1be3-49fc-8ec9-2e219c6f3854", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 77, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-2405:b000:600:b0::764b-Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "system_core": null, + "system_org": null + } + }, + "collections": { + "plot-definition-survey": { + "id": 11, + "createdAt": "2024-04-04T07:27:08.781Z", + "updatedAt": "2024-04-04T07:27:08.781Z", + "survey_metadata": { + "id": 63, + "orgMintedUUID": "50796536-23c7-4f9b-8261-62519287b84d", + "survey_details": { + "id": 76, + "survey_model": "plot-definition-survey", + "time": "2024-04-04T07:21:17.119Z", + "uuid": "46bd9d57-a937-4ef0-bde6-41fd167ad17a", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "d7179862-1be3-49fc-8ec9-2e219c6f3854", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 76, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-2405:b000:600:b0::764b-Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "system_core": null, + "system_org": null + } + }, + "plot_visit": { + "id": 14, + "start_date": "2024-04-04T07:22:36.714Z", + "end_date": null, + "visit_field_name": "n", + "createdAt": "2024-04-04T07:27:08.634Z", + "updatedAt": "2024-04-04T07:27:08.634Z", + "plot_layout": { + "id": 11, + "orientation": null, + "replicate": 1, + "is_the_plot_permanently_marked": false, + "createdAt": "2024-03-26T02:48:14.152Z", + "updatedAt": "2024-03-26T02:48:14.152Z", + "plot_selection": { + "id": 14, + "plot_label": "SATRAFLB0000", + "uuid": "5c58769b-16e4-4b49-8a64-8c0a443ac103", + "comments": null, + "date_time": "2024-03-26T02:45:54.348Z", + "createdAt": "2024-03-26T02:46:21.411Z", + "updatedAt": "2024-03-26T02:46:21.411Z", + "plot_name": { + "id": 14, + "unique_digits": "0000", + "state": { + "id": 5, + "symbol": "SA", + "label": "South Australia", + "description": "State/Jurisdiction where the study is/was conducted.", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2024-03-26T02:39:32.222Z", + "updatedAt": "2024-03-26T02:39:32.222Z" + }, + "program": { + "id": 8, + "symbol": "TRA", + "label": "Training", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:18.491Z", + "updatedAt": "2024-03-26T02:39:18.491Z" + }, + "bioregion": { + "id": 27, + "symbol": "FLB", + "label": "Flinders Lofty Block", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:40.966Z", + "updatedAt": "2024-03-26T02:39:40.966Z", + "states": [ + { + "id": 5, + "symbol": "SA", + "label": "South Australia", + "description": "State/Jurisdiction where the study is/was conducted.", + "uri": "https://linked.data.gov.au/def/nrm/5648c02d-6120-5cb1-858a-d92e970c1da0", + "createdAt": "2024-03-26T02:39:32.222Z", + "updatedAt": "2024-03-26T02:39:32.222Z" + } + ] + } + }, + "recommended_location": { + "id": 30, + "lat": -34.97937723550951, + "lng": 138.63613573039598 + }, + "recommended_location_point": { + "id": 1, + "symbol": "SW", + "label": "South West", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:17.274Z", + "updatedAt": "2024-03-26T02:39:17.274Z" + }, + "plot_selection_survey": { + "id": 5, + "startdate": null, + "createdAt": "2024-03-26T02:46:19.790Z", + "updatedAt": "2024-03-26T02:46:19.790Z", + "survey_metadata": { + "id": 40, + "orgMintedUUID": null, + "survey_details": { + "id": 40, + "survey_model": "plot-selection-survey", + "time": "2024-03-26T02:46:16.206Z", + "uuid": "50e7ff9f-9f61-41f4-a8f4-e467852aacc4", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "a9cb9e38-690f-41c9-8151-06108caf539d", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 40, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": null, + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-129.127.180.198-Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "system_core": null, + "system_org": null + } + } + } + }, + "plot_type": { + "id": 1, + "symbol": "C", + "label": "Control", + "description": "", + "uri": "", + "createdAt": "2024-03-26T02:39:18.129Z", + "updatedAt": "2024-03-26T02:39:18.129Z" + }, + "permanently_marked": null, + "plot_dimensions": { + "id": 1, + "symbol": "100m", + "label": "100 x 100", + "description": "100 m by 100 m (1 hectare)", + "uri": "", + "createdAt": "2024-03-26T02:39:17.254Z", + "updatedAt": "2024-03-26T02:39:17.254Z" + }, + "plot_points": [ + { + "id": 211, + "lat": -34.9765632, + "lng": 138.6315776, + "name": { + "id": 22, + "symbol": "SW", + "description": "South West", + "label": "South West", + "uri": "", + "createdAt": "2024-03-26T02:39:17.807Z", + "updatedAt": "2024-03-26T02:39:17.807Z" + } + }, + { + "id": 212, + "lat": -34.97647336847159, + "lng": 138.6315776, + "name": { + "id": 12, + "symbol": "W1", + "description": "West 1", + "label": "West 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.675Z", + "updatedAt": "2024-03-26T02:39:17.675Z" + } + }, + { + "id": 213, + "lat": -34.97629370541477, + "lng": 138.6315776, + "name": { + "id": 13, + "symbol": "W2", + "description": "West 2", + "label": "West 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.686Z", + "updatedAt": "2024-03-26T02:39:17.686Z" + } + }, + { + "id": 214, + "lat": -34.97611404235794, + "lng": 138.6315776, + "name": { + "id": 14, + "symbol": "W3", + "description": "West 3", + "label": "West 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.700Z", + "updatedAt": "2024-03-26T02:39:17.700Z" + } + }, + { + "id": 215, + "lat": -34.975934379301115, + "lng": 138.6315776, + "name": { + "id": 15, + "symbol": "W4", + "description": "West 4", + "label": "West 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.713Z", + "updatedAt": "2024-03-26T02:39:17.713Z" + } + }, + { + "id": 225, + "lat": -34.975754716244296, + "lng": 138.6315776, + "name": { + "id": 16, + "symbol": "W5", + "description": "West 5", + "label": "West 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.732Z", + "updatedAt": "2024-03-26T02:39:17.732Z" + } + }, + { + "id": 217, + "lat": -34.97566488471588, + "lng": 138.6315776, + "name": { + "id": 24, + "symbol": "NW", + "description": "North West", + "label": "North West", + "uri": "", + "createdAt": "2024-03-26T02:39:17.833Z", + "updatedAt": "2024-03-26T02:39:17.833Z" + } + }, + { + "id": 216, + "lat": -34.97566488471588, + "lng": 138.63168723265542, + "name": { + "id": 17, + "symbol": "N1", + "description": "North 1", + "label": "North 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.743Z", + "updatedAt": "2024-03-26T02:39:17.743Z" + } + }, + { + "id": 219, + "lat": -34.97566488471588, + "lng": 138.63190649796624, + "name": { + "id": 18, + "symbol": "N2", + "description": "North 2", + "label": "North 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.752Z", + "updatedAt": "2024-03-26T02:39:17.752Z" + } + }, + { + "id": 221, + "lat": -34.97566488471588, + "lng": 138.63212576327703, + "name": { + "id": 19, + "symbol": "N3", + "description": "North 3", + "label": "North 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.764Z", + "updatedAt": "2024-03-26T02:39:17.764Z" + } + }, + { + "id": 226, + "lat": -34.97566488471588, + "lng": 138.63234502858785, + "name": { + "id": 20, + "symbol": "N4", + "description": "North 4", + "label": "North 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.783Z", + "updatedAt": "2024-03-26T02:39:17.783Z" + } + }, + { + "id": 218, + "lat": -34.97566488471588, + "lng": 138.63256429389867, + "name": { + "id": 21, + "symbol": "N5", + "description": "North 5", + "label": "North 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.794Z", + "updatedAt": "2024-03-26T02:39:17.794Z" + } + }, + { + "id": 223, + "lat": -34.97566488471588, + "lng": 138.63267392655408, + "name": { + "id": 25, + "symbol": "NE", + "description": "North East", + "label": "North East", + "uri": "", + "createdAt": "2024-03-26T02:39:17.844Z", + "updatedAt": "2024-03-26T02:39:17.844Z" + } + }, + { + "id": 220, + "lat": -34.975754716244296, + "lng": 138.63267392655408, + "name": { + "id": 6, + "symbol": "E5", + "description": "East 5", + "label": "East 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.608Z", + "updatedAt": "2024-03-26T02:39:17.608Z" + } + }, + { + "id": 222, + "lat": -34.975934379301115, + "lng": 138.63267392655408, + "name": { + "id": 5, + "symbol": "E4", + "description": "East 4", + "label": "East 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.598Z", + "updatedAt": "2024-03-26T02:39:17.598Z" + } + }, + { + "id": 224, + "lat": -34.97611404235794, + "lng": 138.63267392655408, + "name": { + "id": 4, + "symbol": "E3", + "description": "East 3", + "label": "East 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.585Z", + "updatedAt": "2024-03-26T02:39:17.585Z" + } + }, + { + "id": 227, + "lat": -34.97629370541477, + "lng": 138.63267392655408, + "name": { + "id": 3, + "symbol": "E2", + "description": "East 2", + "label": "East 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.575Z", + "updatedAt": "2024-03-26T02:39:17.575Z" + } + }, + { + "id": 228, + "lat": -34.97647336847159, + "lng": 138.63267392655408, + "name": { + "id": 2, + "symbol": "E1", + "description": "East 1", + "label": "East 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.564Z", + "updatedAt": "2024-03-26T02:39:17.564Z" + } + }, + { + "id": 229, + "lat": -34.9765632, + "lng": 138.63267392655408, + "name": { + "id": 23, + "symbol": "SE", + "description": "South East", + "label": "South East", + "uri": "", + "createdAt": "2024-03-26T02:39:17.821Z", + "updatedAt": "2024-03-26T02:39:17.821Z" + } + }, + { + "id": 230, + "lat": -34.9765632, + "lng": 138.63256429389867, + "name": { + "id": 11, + "symbol": "S5", + "description": "South 5", + "label": "South 5", + "uri": "", + "createdAt": "2024-03-26T02:39:17.664Z", + "updatedAt": "2024-03-26T02:39:17.664Z" + } + }, + { + "id": 231, + "lat": -34.9765632, + "lng": 138.63234502858785, + "name": { + "id": 10, + "symbol": "S4", + "description": "South 4", + "label": "South 4", + "uri": "", + "createdAt": "2024-03-26T02:39:17.654Z", + "updatedAt": "2024-03-26T02:39:17.654Z" + } + }, + { + "id": 232, + "lat": -34.9765632, + "lng": 138.63212576327703, + "name": { + "id": 9, + "symbol": "S3", + "description": "South 3", + "label": "South 3", + "uri": "", + "createdAt": "2024-03-26T02:39:17.643Z", + "updatedAt": "2024-03-26T02:39:17.643Z" + } + }, + { + "id": 233, + "lat": -34.9765632, + "lng": 138.63190649796624, + "name": { + "id": 8, + "symbol": "S2", + "description": "South 2", + "label": "South 2", + "uri": "", + "createdAt": "2024-03-26T02:39:17.629Z", + "updatedAt": "2024-03-26T02:39:17.629Z" + } + }, + { + "id": 234, + "lat": -34.9765632, + "lng": 138.63168723265542, + "name": { + "id": 7, + "symbol": "S1", + "description": "South 1", + "label": "South 1", + "uri": "", + "createdAt": "2024-03-26T02:39:17.619Z", + "updatedAt": "2024-03-26T02:39:17.619Z" + } + }, + { + "id": 235, + "lat": -34.97611404235794, + "lng": 138.63212576327706, + "name": { + "id": 1, + "symbol": "C", + "description": "Centre", + "label": "Centre", + "uri": "", + "createdAt": "2024-03-26T02:39:17.548Z", + "updatedAt": "2024-03-26T02:39:17.548Z" + } + } + ], + "fauna_plot_point": [] + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/vegetationMappingObservationReverseLookup.json b/src/test/resources/paratoo/vegetationMappingObservationReverseLookup.json new file mode 100644 index 000000000..22250ba1b --- /dev/null +++ b/src/test/resources/paratoo/vegetationMappingObservationReverseLookup.json @@ -0,0 +1,148 @@ +{ + "survey_metadata": { + "id": 8, + "org_minted_uuid": "9beeba20-374a-470a-b4f4-caed34e07572", + "createdAt": "2024-03-28T03:17:25.302Z", + "updatedAt": "2024-03-28T03:17:25.302Z", + "survey_details": { + "id": 59, + "survey_model": "vegetation-mapping-survey", + "time": "2024-03-28T03:17:01.727Z", + "uuid": "263f710d-9af5-456a-9c68-b2b675763220", + "project_id": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "protocol_id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "protocol_version": "1", + "submodule_protocol_id": "" + }, + "provenance": { + "id": 59, + "version_app": "1.0.0-alpha.7-a2ba7a48", + "version_core": "0.1.0-a2ba7a48", + "version_core_documentation": "0.1.0-a2ba7a48", + "version_org": null, + "system_app": "Atlas of Living Australia-beta-testing-0.0.0.0-", + "system_core": "Monitor FDCP API-production", + "system_org": null + } + }, + "collections": { + "vegetation-mapping-survey": { + "start_date_time": "2023-09-08T23:39:00.520Z", + "surveyId": { + "time": "2023-09-09T00:51:34.001Z", + "uuid": "39297148", + "protocol": { + "id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", + "version": 1 + }, + "projectId": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", + "surveyType": "vegetation-mapping-survey" + }, + "createdAt": "2023-09-09T00:51:43.687Z", + "updatedAt": "2023-09-09T00:51:43.687Z" + }, + "vegetation-mapping-observation": [{ + "date_time": "2023-09-08T23:39:02.792Z", + "field_id": "This is a test 2", + "homogeneity_measure": 10, + "comment": "Test", + "NVIS_level_5": null, + "createdAt": "2023-09-09T00:51:43.722Z", + "updatedAt": "2023-09-09T00:51:43.722Z", + "position": { + "id": 44, + "lat": -35.2592398, + "lng": 149.0651536 + }, + "photo": { + "id": 11, + "description": "Test", + "photo": { + "name": "48273978.png", + "alternativeText": null, + "caption": null, + "width": 576, + "height": 133, + "formats": { + "small": { + "ext": ".png", + "url": "https://paratoo-uploads-dev.s3.ap-southeast-2.amazonaws.com/small_48273978_f2b4e1fa84.png", + "hash": "small_48273978_f2b4e1fa84", + "mime": "image/png", + "name": "small_48273978.png", + "path": null, + "size": 22.27, + "width": 500, + "height": 115 + }, + "thumbnail": { + "ext": ".png", + "url": "https://paratoo-uploads-dev.s3.ap-southeast-2.amazonaws.com/thumbnail_48273978_f2b4e1fa84.png", + "hash": "thumbnail_48273978_f2b4e1fa84", + "mime": "image/png", + "name": "thumbnail_48273978.png", + "path": null, + "size": 7.5, + "width": 245, + "height": 57 + } + }, + "hash": "48273978_f2b4e1fa84", + "ext": ".png", + "mime": "image/png", + "size": 7.32, + "url": "https://paratoo-uploads-dev.s3.ap-southeast-2.amazonaws.com/48273978_f2b4e1fa84.png", + "previewUrl": null, + "provider": "aws-s3", + "provider_metadata": null, + "createdAt": "2023-09-09T00:51:42.889Z", + "updatedAt": "2023-09-09T00:51:42.889Z" + }, + "direction": { + "symbol": "S", + "label": "South", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/c02d24ff-488e-5039-b2f0-c8c304c273e6", + "createdAt": "2023-09-08T07:02:37.783Z", + "updatedAt": "2023-09-08T07:02:37.783Z" + } + }, + "vegetation_growth_stage": { + "symbol": "ER", + "label": "Early Regeneration", + "description": "", + "uri": "", + "createdAt": "2023-09-08T07:03:09.387Z", + "updatedAt": "2023-09-08T07:03:09.387Z" + }, + "disturbance": { + "symbol": "NC", + "label": "Not Collected", + "description": "", + "uri": "https://linked.data.gov.au/def/nrm/94ee6b46-e2f1-5101-8666-3cbcd8697f0f", + "createdAt": "2023-09-08T07:02:37.931Z", + "updatedAt": "2023-09-08T07:02:37.931Z" + }, + "fire_history": { + "symbol": "U", + "label": "Unknown", + "description": "", + "uri": "", + "createdAt": "2023-09-08T07:02:40.350Z", + "updatedAt": "2023-09-08T07:02:40.350Z" + }, + "NVIS_level_5_vegetation_association_information": [], + "substrate_cover": { + "id": 9, + "bare_cover_percent": 2, + "cryptogam_cover_percent": 3, + "outcrop_cover_percent": 4, + "litter_cover_percent": 5, + "rock_cover_percent": 6, + "coarse_woody_debris_cover_percent": 7, + "gravel_cover_percent": 8, + "unknown_cover_percent": 9 + } + }] + } +} \ No newline at end of file diff --git a/src/test/resources/paratoo/vegetationMappingSurvey.json b/src/test/resources/paratoo/vegetationMappingSurvey.json deleted file mode 100644 index 27dd2fe1e..000000000 --- a/src/test/resources/paratoo/vegetationMappingSurvey.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "id": 11, - "attributes": { - "date_time": "2023-09-08T23:39:02.792Z", - "field_id": "This is a test 2", - "homogeneity_measure": 10, - "comment": "Test", - "NVIS_level_5": null, - "createdAt": "2023-09-09T00:51:43.722Z", - "updatedAt": "2023-09-09T00:51:43.722Z", - "position": { - "id": 44, - "lat": -35.2592398, - "lng": 149.0651536 - }, - "photo": { - "id": 11, - "description": "Test", - "photo": { - "data": { - "id": 25, - "attributes": { - "name": "48273978.png", - "alternativeText": null, - "caption": null, - "width": 576, - "height": 133, - "formats": { - "small": { - "ext": ".png", - "url": "https://paratoo-uploads-dev.s3.ap-southeast-2.amazonaws.com/small_48273978_f2b4e1fa84.png", - "hash": "small_48273978_f2b4e1fa84", - "mime": "image/png", - "name": "small_48273978.png", - "path": null, - "size": 22.27, - "width": 500, - "height": 115 - }, - "thumbnail": { - "ext": ".png", - "url": "https://paratoo-uploads-dev.s3.ap-southeast-2.amazonaws.com/thumbnail_48273978_f2b4e1fa84.png", - "hash": "thumbnail_48273978_f2b4e1fa84", - "mime": "image/png", - "name": "thumbnail_48273978.png", - "path": null, - "size": 7.5, - "width": 245, - "height": 57 - } - }, - "hash": "48273978_f2b4e1fa84", - "ext": ".png", - "mime": "image/png", - "size": 7.32, - "url": "https://paratoo-uploads-dev.s3.ap-southeast-2.amazonaws.com/48273978_f2b4e1fa84.png", - "previewUrl": null, - "provider": "aws-s3", - "provider_metadata": null, - "createdAt": "2023-09-09T00:51:42.889Z", - "updatedAt": "2023-09-09T00:51:42.889Z" - } - } - }, - "direction": { - "data": { - "id": 1, - "attributes": { - "symbol": "S", - "label": "South", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/c02d24ff-488e-5039-b2f0-c8c304c273e6", - "createdAt": "2023-09-08T07:02:37.783Z", - "updatedAt": "2023-09-08T07:02:37.783Z" - } - } - } - }, - "vegetation_growth_stage": { - "data": { - "id": 1, - "attributes": { - "symbol": "ER", - "label": "Early Regeneration", - "description": "", - "uri": "", - "createdAt": "2023-09-08T07:03:09.387Z", - "updatedAt": "2023-09-08T07:03:09.387Z" - } - } - }, - "disturbance": { - "data": { - "id": 12, - "attributes": { - "symbol": "NC", - "label": "Not Collected", - "description": "", - "uri": "https://linked.data.gov.au/def/nrm/94ee6b46-e2f1-5101-8666-3cbcd8697f0f", - "createdAt": "2023-09-08T07:02:37.931Z", - "updatedAt": "2023-09-08T07:02:37.931Z" - } - } - }, - "fire_history": { - "data": { - "id": 2, - "attributes": { - "symbol": "U", - "label": "Unknown", - "description": "", - "uri": "", - "createdAt": "2023-09-08T07:02:40.350Z", - "updatedAt": "2023-09-08T07:02:40.350Z" - } - } - }, - "vegetation_mapping_survey": { - "data": { - "id": 8, - "attributes": { - "start_date_time": "2023-09-08T23:39:00.520Z", - "surveyId": { - "time": "2023-09-09T00:51:34.001Z", - "uuid": "39297148", - "protocol": { - "id": "cd2cbbc7-2f17-4b0f-91b4-06f46e9c90f2", - "version": 1 - }, - "projectId": "0d02b422-5bf7-495f-b9f2-fa0a3046937f", - "surveyType": "vegetation-mapping-survey" - }, - "createdAt": "2023-09-09T00:51:43.687Z", - "updatedAt": "2023-09-09T00:51:43.687Z" - } - } - }, - "NVIS_level_5_vegetation_association_information": [], - "substrate_cover": { - "id": 9, - "bare_cover_percent": 2, - "cryptogam_cover_percent": 3, - "outcrop_cover_percent": 4, - "litter_cover_percent": 5, - "rock_cover_percent": 6, - "coarse_woody_debris_cover_percent": 7, - "gravel_cover_percent": 8, - "unknown_cover_percent": 9 - } - } -} \ No newline at end of file