diff --git a/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACEncoder.java b/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACEncoder.java index b1bd4cd61..78893b4ca 100644 --- a/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACEncoder.java +++ b/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACEncoder.java @@ -7,6 +7,7 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Map; @@ -224,37 +225,55 @@ public void getEncodedData(IResource resource, Map urnParameters Space space = (Space) geometry.getDimensions().stream().filter(d -> d instanceof Space).findFirst().orElseThrow(); IEnvelope envelope = space.getEnvelope(); List bbox = List.of(envelope.getMinX(), envelope.getMaxX(), envelope.getMinY(), envelope.getMaxY()); + IGrid grid = space.getGrid(); + + RegionMap region = RegionMap.fromBoundsAndGrid(space.getEnvelope().getMinX(), space.getEnvelope().getMaxX(), + space.getEnvelope().getMinY(), space.getEnvelope().getMaxY(), (int) grid.getXCells(), (int) grid.getYCells()); + + ReferencedEnvelope regionEnvelope = new ReferencedEnvelope(region.toEnvelope(), + space.getProjection().getCoordinateReferenceSystem()); + RegionMap regionTransformed = RegionMap.fromEnvelopeAndGrid(regionEnvelope, (int) grid.getXCells(), + (int) grid.getYCells()); // Only for Backward Compatiability // A new COG Adapter would be added if (resource.getParameters().get("cog") != null) { - COGURL = resource.getParameters().get("cog", String.class); - scope.getMonitor().info("Getting requested extent from the COG Asset from url" + COGURL); - GridCoverage2D coverage = COGAssetExtension.getCOGWindowCoverage(bbox, COGURL); - - String rcrs = geometry.getDimension(IGeometry.Dimension.Type.SPACE).getParameters() - .get(org.integratedmodelling.klab.common.Geometry.PARAMETER_SPACE_PROJECTION, String.class); - - Projection crs = Projection.create(rcrs); - org.locationtech.jts.geom.Envelope requestedExtend = new org.locationtech.jts.geom.Envelope(bbox.get(0), bbox.get(1), - bbox.get(2), bbox.get(3)); - - HMRaster raster = HMRaster.fromGridCoverage(coverage); - HMRaster outRaster = new HMRasterWritableBuilder() - .setRegion(RegionMap.fromEnvelopeAndGrid(requestedExtend, (int) space.shape()[0], (int) space.shape()[1])) - .setCrs(crs.getCoordinateReferenceSystem()).setNoValue(raster.getNovalue()).build(); - - GridCoverage2D adjCoverage = null; - try { - outRaster.mapRaster(null, raster, null); - adjCoverage = outRaster.buildCoverage(); + try { + COGURL = resource.getParameters().get("cog", String.class); + scope.getMonitor().info("Getting requested extent from the COG Asset from url" + COGURL); + GridCoverage2D coverage = COGAssetExtension.getCOGWindowCoverage(bbox, COGURL); + + String rcrs = geometry.getDimension(IGeometry.Dimension.Type.SPACE).getParameters() + .get(org.integratedmodelling.klab.common.Geometry.PARAMETER_SPACE_PROJECTION, String.class); + + Projection crs = Projection.create(rcrs); + org.locationtech.jts.geom.Envelope requestedExtend = new org.locationtech.jts.geom.Envelope(bbox.get(0), bbox.get(1), + bbox.get(2), bbox.get(3)); + + HMRaster raster = HMRaster.fromGridCoverage(coverage); + + + CoordinateReferenceSystem targetCRS = HMCrsRegistry.INSTANCE.getCrs("4326"); + if (!HMCrsRegistry.crsEquals(raster.getCrs(),targetCRS)) { + var transformer = new HMCrsTransformer(raster.getCrs(), targetCRS); + transformer.setAcceptLenientDatumShift(true); + raster = transformer.transform(raster); + } + + + HMRaster paddedRaster = new HMRasterWritableBuilder() + .setName("padded") + .setRegion(RegionMap.fromEnvelopeAndGrid(requestedExtend, (int) space.shape()[0], (int) space.shape()[1])) + .setCrs(targetCRS) + .setNoValue(raster.getNovalue()).build(); + paddedRaster.mapRaster(null, raster, null); + coverage = paddedRaster.buildCoverage(); + encoder = new RasterEncoder(); + ((RasterEncoder) encoder).encodeFromCoverage(resource, urnParameters, coverage, geometry, builder, scope); + return; } catch (Exception e) { throw new KlabResourceAccessException("Cannot build COG Output " + e.getMessage()); } - - encoder = new RasterEncoder(); - ((RasterEncoder) encoder).encodeFromCoverage(resource, urnParameters, adjCoverage, geometry, builder, scope); - return; } String collectionUrl = resource.getParameters().get("collection", String.class); @@ -345,14 +364,6 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou } var time2 = effectiveTime; // TODO merge with similar code from below - IGrid grid = space.getGrid(); - RegionMap region = RegionMap.fromBoundsAndGrid(space.getEnvelope().getMinX(), space.getEnvelope().getMaxX(), - space.getEnvelope().getMinY(), space.getEnvelope().getMaxY(), (int) grid.getXCells(), (int) grid.getYCells()); - - ReferencedEnvelope regionEnvelope = new ReferencedEnvelope(region.toEnvelope(), - space.getProjection().getCoordinateReferenceSystem()); - RegionMap regionTransformed = RegionMap.fromEnvelopeAndGrid(regionEnvelope, (int) grid.getXCells(), - (int) grid.getYCells()); // end //TODO List items = features.stream().map(f -> { try { @@ -368,14 +379,12 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou GridCoverage2D coverage = null; try { - // TODO see if we can access to the same readRasterBandOnRegion without using a - // collection LogProgressMonitor lpm = new LogProgressMonitor(); try (HMStacManager manager = new HMStacManager(catalogUrl, lpm)) { HMStacCollection collection = null; try { manager.open(); - collection = manager.getCollectionById(collectionId); + collection = manager.getCollectionByURL(collectionUrl); } catch (Exception e1) { throw new KlabResourceAccessException("Cannot access to STAC collection " + collectionUrl); } @@ -420,7 +429,7 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou HMStacCollection collection = null; try { manager.open(); - collection = manager.getCollectionById(collectionId); + collection = manager.getCollectionByURL(collectionUrl); if (collection == null) { scope.getMonitor() @@ -448,28 +457,51 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou bbox.get(1), bbox.get(3) }); - - // Allow transform ensures the process to finish, but I would not bet on the resulting - // data - if (assetPredicate == null) { + + if (assetPredicate == null || + resource.getType() != org.integratedmodelling.klab.api.provenance.IArtifact.Type.NUMBER) { + // If it was raster, then it necessarily should have been number, object means certainly that it's a vector // NO JSONSelector and JSONValue found, NO assetID was passed as well scope.getMonitor().debug("Query STAC " + collectionUrl + "to get the features"); // Only get the features from STAC Collection, no need to interact with Rasters FeatureSource source; - try { - source = STACFeatureExtension.getFeatures(catalogData, collectionId, bbox, effectiveTime.getStart(), - effectiveTime.getEnd()); - } catch (Exception e) { + if (assetPredicate == null) { + try { + source = STACFeatureExtension.getFeatures(catalogData, collectionId, bbox, effectiveTime.getStart(), + effectiveTime.getEnd()); + } catch (Exception e) { + manager.close(); + throw new KlabResourceAccessException("Cannot extract features from STAC Collection - " + e.getMessage()); + } + encoder = new VectorEncoder(); + ((VectorEncoder) encoder).encodeFromFeatures(source, resource, urnParameters, geometry, builder, scope); + manager.close(); + } else { + List items = collection.searchItems(); + if (items.isEmpty()) { + manager.close(); + throw new KlabIllegalStateException("No STAC items found for this context, check the Spatial/ Temporal bounds of items and the Context"); + } + + var assets = items.stream() + .flatMap(item -> item.getAssets().stream()) + .filter(assetPredicate) + .toList(); + scope.getMonitor().debug("Found " + assets.size() + " Assets to get the requested geometry"); + try { + source = STACFeatureExtension.getFeatures(assets); + encoder = new VectorEncoder(); + ((VectorEncoder) encoder).encodeFromFeatures(source, resource, urnParameters, geometry, builder, scope); + } catch (Exception e) { + manager.close(); + throw new KlabResourceAccessException("Cannot extract features from STAC Collection - " + e.getMessage()); + } manager.close(); - throw new KlabResourceAccessException("Cannot extract features from STAC Collection - " + e.getMessage()); } - encoder = new VectorEncoder(); - ((VectorEncoder) encoder).encodeFromFeatures(source, resource, urnParameters, geometry, builder, scope); - manager.close(); - return; + return; } - List items = collection.searchItems(); + List items = searchItemsWithRetry(collection, scope.getMonitor()); if (items.isEmpty()) { manager.close(); throw new KlabIllegalStateException("No STAC items found for this context, check the Spatial/ Temporal bounds of items and the Context"); @@ -479,16 +511,6 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou sortByDate(items, scope.getMonitor()); } - IGrid grid = space.getGrid(); - - RegionMap region = RegionMap.fromBoundsAndGrid(space.getEnvelope().getMinX(), space.getEnvelope().getMaxX(), - space.getEnvelope().getMinY(), space.getEnvelope().getMaxY(), (int) grid.getXCells(), (int) grid.getYCells()); - - ReferencedEnvelope regionEnvelope = new ReferencedEnvelope(region.toEnvelope(), - space.getProjection().getCoordinateReferenceSystem()); - RegionMap regionTransformed = RegionMap.fromEnvelopeAndGrid(regionEnvelope, (int) grid.getXCells(), - (int) grid.getYCells()); - if (resource.getParameters().contains("s3EndpointUrl")) { String s3EndpointURL = resource.getParameters().get("s3EndpointUrl", String.class); Client s3Client = buildS3Client(s3EndpointURL); @@ -497,16 +519,48 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou var time = effectiveTime; // Filter here based on time, since in some STAC collections they don't yet support // temporal filtering :( like ECDC - items = items.stream() - .filter(item -> isWithinRange(item, time.getStart().getMilliseconds(), time.getEnd().getMilliseconds())) - .collect(Collectors.toList()); + + var pred2 = assetPredicate; + + List itemsWithinTime = items.stream() + .filter(item -> isWithinRange( + item, + time.getStart().getMilliseconds(), + time.getEnd().getMilliseconds())) + .filter(item -> item.getAssets().stream() + .anyMatch(pred2)) + .collect(Collectors.toList()); + + if (itemsWithinTime.isEmpty()) { + scope.getMonitor().debug("Couldn't find items satisfying temporal and asset based constraints" + + " within the specified time range, Applying Temporal Mediation"); + + Map> grouped = items.stream() + .filter(item -> item.getAssets().stream() + .anyMatch(pred2)) + .collect(Collectors.groupingBy( + item -> geometryKey(item.getGeometry()) + )); + + items = grouped.values().stream() + .map(group -> group.stream() + .min(Comparator.comparingLong( + groupItem -> checkDuration( + groupItem, + time.getEnd().getMilliseconds()))) + .orElseThrow()) + .collect(Collectors.toList()); + + } else { + items = itemsWithinTime; + } if (items.size() == 0) { manager.close(); throw new KlabIllegalStateException( - "No STAC items found covering the entire time duration of the context requested"); + "No STAC items were satifying constraints and Couldn't apply Temporal Mediation"); } else { - scope.getMonitor().debug("Found " + items.size() + " STAC items satisfying the temporal constraint."); + scope.getMonitor().debug("Found " + items.size() + " STAC items for generating the observation."); } // Once the support for customized predicate is added, we can apply for features as well @@ -604,6 +658,12 @@ public boolean test(HMStacAsset asset) { // Assuming for now that "eo:bands" wou throw new KlabInternalErrorException("Cannot build STAC raster output. Reason " + e.getMessage()); } } + + private static String geometryKey(Geometry geom) { + Geometry normalized = geom.copy(); + normalized.normalize(); + return normalized.toText(); + } private Predicate getAssetPredicateFromJSONSelector(IResource resource) { String jsonSelector = resource.getParameters().get("jsonSelector", String.class); @@ -647,7 +707,32 @@ private boolean isWithinRange(HMStacItem item, long startMillis, long endMillis) return false; } } - + + private long checkDuration(HMStacItem item, long endMillis) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + if (item.getEndTimestamp() == null) { // assume like it's the correct data + return 0; + } + + try { + long itemEnd = LocalDateTime.parse(item.getEndTimestamp(), formatter).atZone(ZoneOffset.UTC).toInstant() + .toEpochMilli(); + long itemStart = LocalDateTime.parse(item.getEndTimestamp(), formatter).atZone(ZoneOffset.UTC).toInstant() + .toEpochMilli(); + + if (itemStart > endMillis) { // Deprioritize future items since it doesn't make a lot of sense + return Long.MAX_VALUE; + } + + return Math.abs(itemEnd - endMillis); + } catch(Exception e) { + e.printStackTrace(); + return Long.MAX_VALUE; // return an arbitrary large number + } + } + + private List getFeaturesFromStaticCollection(String collectionUrl, JSONObject collectionData, String collectionId) { List links = collectionData.getJSONArray("links").toList().stream() @@ -662,6 +747,44 @@ private List getFeaturesFromStaticCollection(String collectionUrl } }).toList(); } + + + private List searchItemsWithRetry(HMStacCollection collection, IMonitor monitor) throws IOException { + int maxRetries = 5; + long backoffMillis = 10000; // start at 10s + + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + return collection.searchItems(); + } catch (Exception e) { + boolean isRateLimited = isRateLimitError(e); + if (!isRateLimited || attempt == maxRetries) { + throw e instanceof IOException ? (IOException) e : new IOException(e); + } + + try { + Thread.sleep(backoffMillis); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while backing off after 429", ie); + } + + backoffMillis *= 2; // exponential: 10s, 20s, 40s, 80s, 160s + monitor.debug("Search Request throttled; Retrying Search after: " + backoffMillis + " Miliseconds"); + } + } + throw new IOException("Unreachable"); + } + + private boolean isRateLimitError(Throwable e) { + while (e != null) { + if (e.getMessage() != null && e.getMessage().contains("429")) { + return true; + } + e = e.getCause(); + } + return false; + } @Override public void listDetail(IResource resource, OutputStream stream, boolean verbose, IMonitor monitor) { diff --git a/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACValidator.java b/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACValidator.java index 3e3f0d6e6..3a7c5a57d 100644 --- a/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACValidator.java +++ b/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/STACValidator.java @@ -4,11 +4,12 @@ import java.net.URL; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; - +import org.geotools.api.feature.type.AttributeDescriptor; import org.integratedmodelling.kim.api.IParameters; import org.integratedmodelling.klab.api.data.IGeometry; import org.integratedmodelling.klab.api.data.IResource; @@ -23,11 +24,17 @@ import org.integratedmodelling.klab.data.resources.ResourceBuilder; import org.integratedmodelling.klab.exceptions.KlabIllegalArgumentException; import org.integratedmodelling.klab.exceptions.KlabUnimplementedException; +import org.integratedmodelling.klab.exceptions.KlabInternalErrorException; import org.integratedmodelling.klab.rest.CodelistReference; import org.integratedmodelling.klab.rest.MappingReference; import org.integratedmodelling.klab.rest.ResourceCRUDRequest; +import org.integratedmodelling.klab.stac.extensions.STACFeatureExtension; import org.integratedmodelling.klab.utils.Pair; +import org.integratedmodelling.klab.utils.Utils; import org.integratedmodelling.klab.utils.s3.S3URLUtils; +import org.locationtech.jts.geom.Lineal; +import org.locationtech.jts.geom.Polygonal; +import org.locationtech.jts.geom.Puntal; import kong.unirest.json.JSONObject; @@ -62,35 +69,64 @@ public Builder validate(String urn, URL url, IParameters userData, IMoni // The default URL of the resource is the collection endpoint. May be overwritten. builder.withMetadata(IMetadata.DC_URL, collectionUrl); - JSONObject assetNode; - - if (userData.contains("asset")) { - if(userData.contains("cog")) { - throw new KlabIllegalArgumentException("STAC asset and cog URL both shouldn't be provided while importing"); - } - String requestedAssetId = userData.get("asset", String.class); - assetNode = STACCollectionParser.readAssetInformationFromCollection(collectionUrl, collectionData, requestedAssetId); - - } else if (userData.contains("jsonSelector")) { - if (!userData.contains("jsonValue")) { - throw new KlabIllegalArgumentException("Both jsonSelector and jsonValue must be provided"); - } - - if(userData.contains("cog")) { - throw new KlabIllegalArgumentException("jsonSelector and jsonValue shouldn't be provided along with cog URL while importing"); - } - - Predicate predicate = STACPathExpression.STACAssetPredicate - .fromKongJsonObject(userData.get("jsonSelector", String.class), userData.get("jsonValue", String.class)); - - assetNode = STACCollectionParser.readAssetInformationFromCollection(collectionUrl, collectionData, predicate); - - } else { - // Just import Features - monitor.info("import STAC Collection for Features"); - readMetadata(collectionData, builder); - return builder; - } + JSONObject assetNode = null; + + if (userData.contains("asset")) { + if(userData.contains("cog")) { + throw new KlabIllegalArgumentException("STAC asset and cog URL both shouldn't be provided while importing"); + } + String requestedAssetId = userData.get("asset", String.class); + assetNode = STACCollectionParser.readAssetInformationFromCollection(collectionUrl, collectionData, requestedAssetId); + } + + if (userData.contains("jsonSelector")) { + if (!userData.contains("jsonValue")) { + throw new KlabIllegalArgumentException("Both jsonSelector and jsonValue must be provided"); + } + + if(userData.contains("cog")) { + throw new KlabIllegalArgumentException("jsonSelector and jsonValue shouldn't be provided along with cog URL while importing"); + } + + Predicate predicate = STACPathExpression.STACAssetPredicate + .fromKongJsonObject(userData.get("jsonSelector", String.class), userData.get("jsonValue", String.class)); + + assetNode = STACCollectionParser.readAssetInformationFromCollection(collectionUrl, collectionData, predicate); + + } + + if (assetNode == null || assetNode.getJSONObject(assetNode.keys().next()).get("href").toString().endsWith("json")) { + // Importing Vector from STAC + if (assetNode == null) { // Import all the features from STAC + monitor.info("Importing Vectors from STAC Item Geometries from Item Metadata"); + readMetadata(collectionData, builder); + } else { + // The Asset is a Vector in itself, targeting the Asset like WFS + monitor.info("Targetting Vector Assets from STAC Items"); + Map> attributeTypes = new HashMap<>(); + try { + var source = STACFeatureExtension.getFeatures(assetNode.getJSONObject(assetNode.keys().next())); + + String geomName = source.getSchema().getGeometryDescriptor().getName().toString(); + for (AttributeDescriptor ad : source.getSchema().getAttributeDescriptors()) { + + if (ad.getLocalName().equals(geomName)) { + // set shape dimensionality from geometry type: 0 = point, 1 = line, 2 = polygon + continue; + } else { + // store attribute ID and type + attributeTypes.put(ad.getName().toString(), ad.getType().getBinding()); + builder.withAttribute(ad.getName().toString(), Utils.getArtifactType(ad.getType().getBinding()), false, + true); + } + } + } catch (Exception e) { + throw new KlabInternalErrorException("Unable to import Vector Resource from STAC"); + } + } + return builder; + } + String assetId = assetNode.keys().next(); JSONObject asset = assetNode.getJSONObject(assetId); @@ -150,7 +186,8 @@ private void generateCodeList(Builder builder, String assetId, JSONObject asset) builder.withType(type); } } - + + private Type readRasterDataType(JSONObject asset) { if (asset.has("type")) { diff --git a/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/extensions/STACFeatureExtension.java b/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/extensions/STACFeatureExtension.java index b422e8122..7cd800bf2 100644 --- a/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/extensions/STACFeatureExtension.java +++ b/adapters/klab.ogc/src/main/java/org/integratedmodelling/klab/stac/extensions/STACFeatureExtension.java @@ -1,6 +1,8 @@ package org.integratedmodelling.klab.stac.extensions; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Iterator; @@ -9,10 +11,18 @@ import org.geotools.api.data.FeatureSource; import org.geotools.data.geojson.GeoJSONReader; import org.geotools.data.memory.MemoryDataStore; +import org.geotools.geojson.feature.FeatureJSON; +import org.geotools.geometry.jts.JTS; +import org.geotools.referencing.CRS; import org.integratedmodelling.klab.stac.STACUtils; +import org.locationtech.jts.geom.Geometry; import org.geotools.api.feature.simple.SimpleFeature; import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.geotools.api.referencing.operation.MathTransform; import org.hortonmachine.gears.io.stac.HMStacItem; +import org.hortonmachine.gears.utils.crs.HMCrsRegistry; +import org.hortonmachine.gears.utils.crs.HMCrsTransformer; import kong.unirest.HttpResponse; import kong.unirest.JsonNode; @@ -26,15 +36,139 @@ import org.integratedmodelling.klab.stac.STACUtils; import java.time.format.DateTimeFormatter; import java.time.*; +import org.hortonmachine.gears.io.stac.HMStacAsset; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.geotools.feature.simple.SimpleFeatureTypeBuilder; +import org.geotools.feature.simple.SimpleFeatureBuilder; +import org.geotools.api.feature.type.AttributeDescriptor; +import org.geotools.api.feature.type.GeometryDescriptor; +import org.locationtech.jts.geom.util.GeometryFixer; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.precision.GeometryPrecisionReducer; +import org.locationtech.jts.geom.Geometry; + public class STACFeatureExtension { + + + public static FeatureSource getFeatures(JSONObject assetNode) throws Exception { + List featureList = new ArrayList<>(); + + String href =assetNode.get("href").toString(); + HttpResponse response = Unirest + .get(href) + .asJson(); + + JSONObject body = response.getBody().getObject(); + JSONArray features = body.getJSONArray("features"); + Iterator featureIterator = features.iterator(); + + while (featureIterator.hasNext()) { + JSONObject feature = (JSONObject) featureIterator.next(); + SimpleFeature feat = GeoJSONReader.parseFeature(feature.toString()); + featureList.add(feat); + } + + + if (featureList.isEmpty()) { + throw new Exception("No features found"); + } + + SimpleFeatureType type = featureList.get(0).getFeatureType(); + MemoryDataStore dataStore = new MemoryDataStore(type); + dataStore.addFeatures(featureList); + return dataStore.getFeatureSource(type.getTypeName()); + } + + public static FeatureSource getFeatures(List assets) throws Exception { + List featureList = new ArrayList<>(); + CoordinateReferenceSystem targetCRS = HMCrsRegistry.INSTANCE.getCrs("4326"); + SimpleFeatureType type4326 = null; + + for (HMStacAsset asset : assets) { + String href = asset.getAssetNode().get("href").asText(); + HttpResponse response = Unirest + .get(href) + .asJson(); + + JSONObject body = response.getBody().getObject(); + JSONArray features = body.getJSONArray("features"); + Iterator featureIterator = features.iterator(); + + + while (featureIterator.hasNext()) { + JSONObject feature = (JSONObject) featureIterator.next(); + SimpleFeature feat = GeoJSONReader.parseFeature(feature.toString()); + var transformer = new HMCrsTransformer(HMCrsRegistry.INSTANCE.getCrs( + String.valueOf(asset.getEpsg()), true), targetCRS); + transformer.setAcceptLenientDatumShift(true); + + if (type4326 == null) { + SimpleFeatureType type = feat.getFeatureType(); + + SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); + builder.setName(type.getTypeName()); + builder.setCRS(targetCRS); + + for (var ad : type.getAttributeDescriptors()) { + if (ad instanceof GeometryDescriptor) { + builder.add( + ad.getLocalName(), + Geometry.class, + targetCRS + ); + } else { + builder.add( + ad.getLocalName(), + ad.getType().getBinding() + ); + } + } + type4326 = builder.buildFeatureType(); + } + + if (!asset.getEpsg().equals(4326)) { + SimpleFeatureType type = feat.getFeatureType(); + Geometry vectorGeom = (Geometry) feat.getDefaultGeometry(); + Geometry geom4326 = transformer.transform(vectorGeom); + + SimpleFeatureBuilder fb = + new SimpleFeatureBuilder(type4326); + for (var ad : type.getAttributeDescriptors()) { + String name = ad.getLocalName(); + if (ad instanceof GeometryDescriptor) { + fb.set(name, geom4326); + } else { + fb.set(name, feat.getAttribute(name)); + } + } + SimpleFeature updatedFeature = fb.buildFeature(feat.getID()); + updatedFeature.setDefaultGeometry(geom4326); + featureList.add(updatedFeature); + } else { + featureList.add(feat); + } + } + } + + if (featureList.isEmpty()) { + throw new Exception("No features found"); + } + + MemoryDataStore dataStore = new MemoryDataStore(type4326); + dataStore.addFeatures(featureList); + return dataStore.getFeatureSource(type4326.getTypeName()); + } + + public static FeatureSource getFeatures(JSONObject catalogData, String collectionId, List bbox, ITimeInstant start, ITimeInstant end) throws Exception { String searchEndpoint = STACUtils.getLinkTo(catalogData, "search") .orElseThrow(() -> new Exception("Search Link not found for the Catalog")); List featureList = new ArrayList<>(); - JSONArray bboxArray = new JSONArray(); for (Double v : bbox) { bboxArray.put(v); @@ -111,7 +245,7 @@ public static FeatureSource getFeatures(JSONOb } SimpleFeatureType type = featureList.get(0).getType(); - MemoryDataStore dataStore = new org.geotools.data.memory.MemoryDataStore(type); + MemoryDataStore dataStore = new MemoryDataStore(type); dataStore.addFeatures(featureList); return dataStore.getFeatureSource(type.getTypeName()); } diff --git a/adapters/klab.ogc/src/main/resources/ogc/prototypes/cog.kdl b/adapters/klab.ogc/src/main/resources/ogc/prototypes/cog.kdl new file mode 100644 index 000000000..257637c1d --- /dev/null +++ b/adapters/klab.ogc/src/main/resources/ogc/prototypes/cog.kdl @@ -0,0 +1,23 @@ +/* + * Parameters accepted in a COG resource + */ + annotation cog + "The COG adapter needs a valid COG URL hosted in a remote server, which supports Range Requests making it fast, economic and requires less compute" + label "COG Assets adapter" + { + + final text 'url' + "[REQUIRED] The URL pointing to the COG Asset" + + optional number 'band' + "Relevant only for raster resources. + The band for a multi-band raster. Default it will pick up the first one." + default 0 + + optional enum 'bandmixer' + "Relevant only for raster resources.\n + The operation that will merge the raster bands into a single observation.\n + Restricted to the following values: max_band, min_band, max_value, min_value, avg_value and sum_value" + values max_band, min_band, max_value, min_value, avg_value, sum_value + default "" + } \ No newline at end of file diff --git a/klab.engine/src/main/java/org/integratedmodelling/klab/components/geospace/extents/Shape.java b/klab.engine/src/main/java/org/integratedmodelling/klab/components/geospace/extents/Shape.java index 520bdfd76..9f771a2e1 100644 --- a/klab.engine/src/main/java/org/integratedmodelling/klab/components/geospace/extents/Shape.java +++ b/klab.engine/src/main/java/org/integratedmodelling/klab/components/geospace/extents/Shape.java @@ -83,6 +83,8 @@ import org.locationtech.jts.io.WKTReader; import org.locationtech.jts.operation.overlay.snap.SnapIfNeededOverlayOp; import org.locationtech.jts.simplify.TopologyPreservingSimplifier; +import org.locationtech.jts.operation.overlayng.OverlayNGRobust; +import org.locationtech.jts.operation.overlayng.OverlayNG; /** * TODO setup for using as locator. @@ -335,7 +337,8 @@ public Shape intersection(IShape other) { Geometry intersection = null; try { - intersection = fixedShape.intersection(fixedOther); + //intersection = fixedShape.intersection(fixedOther); + intersection = OverlayNGRobust.overlay(fixedShape, fixedOther, OverlayNG.INTERSECTION); } catch (Exception e) { GeotoolsUtils.INSTANCE.dumpFailingOperationGeometries("intersection", shapeGeometry, ((Shape) other).shapeGeometry); e.printStackTrace();