Skip to content

Commit 3593d15

Browse files
authored
Merge pull request #944 from AtlasOfLivingAustralia/feature/issue941
fauna plot #941
2 parents 0b9aeed + e815077 commit 3593d15

File tree

5 files changed

+171
-78
lines changed

5 files changed

+171
-78
lines changed

grails-app/domain/au/org/ala/ecodata/Site.groovy

+10
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,14 @@ class Site {
181181
status != Status.DELETED
182182
}.find()
183183
}
184+
185+
static List<Site> findAllByExternalId(ExternalId.IdType idType, String externalId, Map params) {
186+
where {
187+
externalIds {
188+
idType == idType
189+
externalId == externalId
190+
}
191+
status != Status.DELETED
192+
}.list(params)
193+
}
184194
}

grails-app/services/au/org/ala/ecodata/ParatooService.groovy

+47-33
Original file line numberDiff line numberDiff line change
@@ -535,35 +535,39 @@ class ParatooService {
535535
// used by protocols like bird survey where a point represents a sight a bird has been observed in a
536536
// bird survey plot
537537
def location = output[model.name]
538-
if (location instanceof Map) {
539-
output[model.name] = [
540-
type : 'Feature',
541-
geometry : [
542-
type : 'Point',
543-
coordinates: [location.lng, location.lat]
544-
],
545-
properties: [
546-
name : "Point ${formName}-${featureId}",
547-
externalId: location.id,
548-
id: "${formName}-${featureId}"
549-
]
550-
]
551-
}
552-
else if (location instanceof List) {
553-
String name
554-
switch (config?.geometryType) {
555-
case "LineString":
556-
name = "LineString ${formName}-${featureId}"
557-
output[model.name] = ParatooProtocolConfig.createLineStringFeatureFromGeoJSON (location, name, null, name)
558-
break
559-
default:
560-
name = "Polygon ${formName}-${featureId}"
561-
output[model.name] = ParatooProtocolConfig.createFeatureFromGeoJSON (location, name, null, name)
562-
break
538+
if (location) {
539+
if (location instanceof Map) {
540+
output[model.name] = [
541+
type : 'Feature',
542+
geometry : [
543+
type : 'Point',
544+
coordinates: [location.lng, location.lat]
545+
],
546+
properties: [
547+
name : "Point ${formName}-${featureId}",
548+
externalId: location.id,
549+
id : "${formName}-${featureId}"
550+
]
551+
]
552+
} else if (location instanceof List) {
553+
String name
554+
switch (config?.geometryType) {
555+
case "LineString":
556+
name = "LineString ${formName}-${featureId}"
557+
output[model.name] = ParatooProtocolConfig.createLineStringFeatureFromGeoJSON(location, name, null, name)
558+
break
559+
default:
560+
name = "Polygon ${formName}-${featureId}"
561+
output[model.name] = ParatooProtocolConfig.createFeatureFromGeoJSON(location, name, null, name)
562+
break
563+
}
563564
}
564-
}
565565

566-
featureId ++
566+
featureId ++
567+
}
568+
else {
569+
output[model.name] = null
570+
}
567571
break
568572
case "image":
569573
case "document":
@@ -588,7 +592,10 @@ class ParatooService {
588592
List features = geoJson?.features ?: []
589593
geoJson.remove('features')
590594
siteProps.features = features
591-
siteProps.type = Site.TYPE_SURVEY_AREA
595+
if (features)
596+
siteProps.type = Site.TYPE_COMPOUND
597+
else
598+
siteProps.type = Site.TYPE_SURVEY_AREA
592599
siteProps.publicationStatus = PublicationStatus.PUBLISHED
593600
siteProps.projects = [project.projectId]
594601
String externalId = geoJson.properties?.externalId
@@ -598,18 +605,19 @@ class ParatooService {
598605
Site site
599606
// create new site for every non-plot submission
600607
if (config.usesPlotLayout) {
601-
site = Site.findByExternalId(ExternalId.IdType.MONITOR_PLOT_GUID, externalId)
602-
if (site?.features) {
603-
siteProps.features?.addAll(site.features)
604-
}
608+
List sites = Site.findAllByExternalId(ExternalId.IdType.MONITOR_PLOT_GUID, externalId, [sort: "lastUpdated", order: "desc"])
609+
if (sites)
610+
site = sites.first()
605611
}
606612

607613
Map result
608-
if (!site) {
614+
// If the plot layout has been updated, create a new site
615+
if (!site || isUpdatedPlotLayout(site, observation, config)) {
609616
result = siteService.create(siteProps)
610617
} else {
611618
result = [siteId: site.siteId]
612619
}
620+
613621
if (result.error) {
614622
// Don't treat this as a fatal error for the purposes of responding to the paratoo request
615623
log.error("Error creating a site for survey " + collection.orgMintedUUID + ", project " + project.projectId + ": " + result.error)
@@ -619,6 +627,12 @@ class ParatooService {
619627
[siteId:siteId, name:siteProps?.name]
620628
}
621629

630+
private static boolean isUpdatedPlotLayout (Site site, Map observation, ParatooProtocolConfig config) {
631+
Date localSiteUpdated = site.lastUpdated
632+
Date plotLayoutUpdated = config.getPlotLayoutUpdatedAt(observation)
633+
plotLayoutUpdated?.after(localSiteUpdated) ?: false
634+
}
635+
622636
private Map syncParatooProtocols(List<Map> protocols) {
623637
Map result = [errors: [], messages: []]
624638
List guids = []

src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy

+46-26
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import org.locationtech.jts.geom.Geometry
1212
@Slf4j
1313
@JsonIgnoreProperties(['metaClass', 'errors', 'expandoMetaClass'])
1414
class ParatooProtocolConfig {
15-
15+
static final String FAUNA_PLOT = 'Fauna plot'
16+
static final String CORE_PLOT = 'Core monitoring plot'
1617
String name
1718
String apiEndpoint
1819
boolean usesPlotLayout = true
@@ -26,7 +27,9 @@ class ParatooProtocolConfig {
2627
String plotVisitPath = 'plot_visit'
2728
String plotLayoutPath = "${plotVisitPath}.plot_layout"
2829
String plotLayoutIdPath = "${plotLayoutPath}.id"
30+
String plotLayoutUpdatedAtPath = "${plotLayoutPath}.updatedAt"
2931
String plotLayoutPointsPath = "${plotLayoutPath}.plot_points"
32+
String faunaPlotPointPath = "${plotLayoutPath}.fauna_plot_point"
3033
String plotSelectionPath = "${plotLayoutPath}.plot_selection"
3134
String plotLayoutDimensionLabelPath = "${plotLayoutPath}.plot_dimensions.label"
3235
String plotLayoutTypeLabelPath = "${plotLayoutPath}.plot_type.label"
@@ -79,6 +82,16 @@ class ParatooProtocolConfig {
7982
return removeMilliseconds(date)
8083
}
8184

85+
Date getPlotLayoutUpdatedAt(Map surveyData) {
86+
def date = getProperty(surveyData, plotLayoutUpdatedAtPath)
87+
if (!date) {
88+
date = getPropertyFromSurvey(surveyData, plotLayoutUpdatedAtPath)
89+
}
90+
91+
date = getFirst(date)
92+
date ? DateUtil.parseWithMilliseconds(date) : null
93+
}
94+
8295
Map getSurveyId(Map surveyData) {
8396
if(surveyIdPath == null || surveyData == null) {
8497
return null
@@ -135,7 +148,8 @@ class ParatooProtocolConfig {
135148
List features = []
136149
paths.each { String name, node ->
137150
if (node instanceof Boolean) {
138-
features.add(output[name])
151+
if (output[name])
152+
features.add(output[name])
139153
// todo later: add featureIds and modelId for compliance with feature behaviour of reports
140154
}
141155

@@ -201,31 +215,17 @@ class ParatooProtocolConfig {
201215
Map geoJson = null
202216
if (usesPlotLayout) {
203217
geoJson = extractSiteDataFromPlotVisit(output)
204-
// get list of all features associated with observation
205-
if (geoJson && form && output) {
206-
geoJson.features = extractFeatures(output, form)
207-
}
208218
}
209219
else if (geometryPath) {
210220
geoJson = extractSiteDataFromPath(output)
211221
}
212222
else if (form && output) {
213223
List features = extractFeatures(output, form)
214224
if (features) {
215-
List featureGeometries = features.collect { it.geometry }
216-
Geometry geometry = GeometryUtils.getFeatureCollectionConvexHull(featureGeometries)
217225
String startDateInString = getStartDate(output)
218226
startDateInString = DateUtil.convertUTCDateToStringInTimeZone(startDateInString, clientTimeZone?:TimeZone.default)
219227
String name = "${form.name} site - ${startDateInString}"
220-
geoJson = [
221-
type: 'Feature',
222-
geometry: GeometryUtils.geometryToGeoJsonMap(geometry),
223-
properties: [
224-
name: name,
225-
description: "${name} (convex hull of all features)",
226-
],
227-
features: features
228-
]
228+
geoJson = createConvexHullGeoJSON(features, name)
229229
}
230230
}
231231

@@ -307,12 +307,13 @@ class ParatooProtocolConfig {
307307

308308
private Map extractSiteDataFromPlotVisit(Map survey) {
309309
Map surveyData = getSurveyData(survey)
310-
def plotLayoutId = getProperty(surveyData, plotLayoutIdPath) // Currently an int, may become uuid?
310+
String plotLayoutId = getProperty(surveyData, plotLayoutIdPath) // Currently an int, may become uuid?
311311

312312
if (!plotLayoutId) {
313313
log.warn("No plot_layout found in survey at path ${plotLayoutIdPath}")
314314
return null
315315
}
316+
316317
List plotLayoutPoints = getProperty(surveyData, plotLayoutPointsPath)
317318
Map plotSelection = getProperty(surveyData, plotSelectionPath)
318319
Map plotSelectionGeoJson = plotSelectionToGeoJson(plotSelection)
@@ -322,22 +323,41 @@ class ParatooProtocolConfig {
322323

323324
String name = plotSelectionGeoJson.properties.name + ' - ' + plotLayoutTypeLabel + ' (' + plotLayoutDimensionLabel + ')'
324325

325-
Map plotGeoJson = createFeatureFromGeoJSON(plotLayoutPoints, name, plotLayoutId, plotSelectionGeoJson?.properties?.notes)
326-
327-
//Map faunaPlotGeoJson = toGeometry(plotLayout.fauna_plot_point)
326+
Map plotGeoJson = createFeatureFromGeoJSON(plotLayoutPoints, name, plotLayoutId, "${CORE_PLOT} ${plotSelectionGeoJson?.properties?.notes?:""}")
327+
List faunaPlotPoints = getProperty(surveyData, faunaPlotPointPath)
328328

329-
// TODO maybe turn this into a feature with properties to distinguish the fauna plot?
330-
// Or a multi-polygon?
329+
if (faunaPlotPoints) {
330+
Map faunaPlotGeoJson = createFeatureFromGeoJSON(faunaPlotPoints, name, plotLayoutId, "${FAUNA_PLOT} ${plotSelectionGeoJson?.properties?.notes?:""}")
331+
List features = [plotGeoJson, faunaPlotGeoJson]
332+
plotGeoJson = createConvexHullGeoJSON(features, name, plotLayoutId, plotGeoJson.properties.notes)
333+
}
331334

332335
plotGeoJson
333336
}
334337

335-
static Map createFeatureFromGeoJSON(List plotLayoutPoints, String name, def plotLayoutId, String notes = "") {
338+
static Map createConvexHullGeoJSON (List features, String name, String externalId = "", String notes = "") {
339+
features = features.findAll { it.geometry != null }
340+
List featureGeometries = features.collect { it.geometry }
341+
Geometry geometry = GeometryUtils.getFeatureCollectionConvexHull(featureGeometries)
342+
[
343+
type: 'Feature',
344+
geometry: GeometryUtils.geometryToGeoJsonMap(geometry),
345+
properties: [
346+
name: name,
347+
externalId: externalId,
348+
notes: notes,
349+
description: "${name} (convex hull of all features)",
350+
],
351+
features: features
352+
]
353+
}
354+
355+
static Map createFeatureFromGeoJSON(List plotLayoutPoints, String name, String plotLayoutId, String notes = "") {
336356
Map plotGeometry = toGeometry(plotLayoutPoints)
337357
createFeatureObject(plotGeometry, name, plotLayoutId, notes)
338358
}
339359

340-
static Map createFeatureObject(Map plotGeometry, String name, plotLayoutId, String notes = "") {
360+
static Map createFeatureObject(Map plotGeometry, String name, String plotLayoutId, String notes = "") {
341361
[
342362
type : 'Feature',
343363
geometry : plotGeometry,
@@ -362,7 +382,7 @@ class ParatooProtocolConfig {
362382
plotGeometry
363383
}
364384

365-
static Map createLineStringFeatureFromGeoJSON (List plotLayoutPoints, String name, def plotLayoutId, String notes = "") {
385+
static Map createLineStringFeatureFromGeoJSON (List plotLayoutPoints, String name, String plotLayoutId, String notes = "") {
366386
Map plotGeometry = toLineStringGeometry(plotLayoutPoints)
367387
createFeatureObject(plotGeometry, name, plotLayoutId, notes)
368388
}

src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy

+3-3
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,9 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest<ParatooSer
339339

340340
and:
341341
site.name == "SATFLB0001 - Control (100 x 100)"
342-
site.description == "SATFLB0001 - Control (100 x 100)"
343-
site.notes == "some comment"
344-
site.type == "surveyArea"
342+
site.description == "SATFLB0001 - Control (100 x 100) (convex hull of all features)"
343+
site.notes == "Core monitoring plot some comment"
344+
site.type == "compound"
345345
site.publicationStatus == "published"
346346
site.externalIds[0].externalId == "2"
347347
site.externalIds[0].idType == ExternalId.IdType.MONITOR_PLOT_GUID

0 commit comments

Comments
 (0)