diff --git a/cimr-reader-ui/pom.xml b/cimr-reader-ui/pom.xml new file mode 100644 index 000000000..41a3d07f4 --- /dev/null +++ b/cimr-reader-ui/pom.xml @@ -0,0 +1,80 @@ + + 4.0.0 + + eu.esa.microwavetbx + microwave-toolbox + 14.0.0-SNAPSHOT + + + eu.esa.snap.cimr.ui + cimr-reader-ui + CIMR Reader UI + + nbm + + + + org.esa.snap + snap-core + + + org.esa.snap + ceres-glayer + + + org.esa.snap + snap-ui + + + org.esa.snap + ceres-binding + + + org.esa.snap + snap-rcp + + + eu.esa.snap.cimr + cimr-reader + ${project.version} + + + eu.esa.snap.netbeans + snap-gui-lib + 1.2.0 + + + org.mockito + mockito-core + test + + + + + + + org.apache.netbeans.utilities + nbm-maven-plugin + + + eu.esa.snap.cimr.ui.* + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + diff --git a/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrFootprintOverlay.java b/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrFootprintOverlay.java new file mode 100644 index 000000000..b1b5aeb21 --- /dev/null +++ b/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrFootprintOverlay.java @@ -0,0 +1,106 @@ +package eu.esa.snap.cimr.ui; + +import com.bc.ceres.glayer.swing.LayerCanvas; +import com.bc.ceres.grender.Rendering; +import com.bc.ceres.grender.Viewport; +import eu.esa.snap.cimr.cimr.CimrFootprints; +import eu.esa.snap.cimr.cimr.CimrFootprintShape; +import org.esa.snap.core.datamodel.ColorPaletteDef; +import org.esa.snap.core.datamodel.ImageInfo; +import org.esa.snap.core.datamodel.RasterDataNode; +import org.esa.snap.core.image.ImageManager; + +import java.awt.*; +import java.awt.geom.*; +import java.util.List; + + +public class CimrFootprintOverlay implements LayerCanvas.Overlay { + + public static final CimrFootprintOverlay INSTANCE = new CimrFootprintOverlay(); + + private CimrFootprints footprints; + private RasterDataNode raster; + + private CimrFootprintOverlay() {} + + public void setFootprints(CimrFootprints footprints) { + this.footprints = footprints; + } + + public void setRaster(RasterDataNode raster) { + this.raster = raster; + } + + @Override + public void paintOverlay(LayerCanvas canvas, Rendering rendering) { + if (footprints == null || footprints.getShapes().isEmpty()) { + return; + } + + Graphics2D g = rendering.getGraphics(); + + Color oldColor = g.getColor(); + java.awt.Stroke oldStroke = g.getStroke(); + + Viewport vp = canvas.getViewport(); + AffineTransform m2vBase = vp.getModelToViewTransform(); + + ImageInfo imageInfo = raster.getImageInfo(); + Color baseColor = Color.WHITE; + Color[] fullPalette = null; + ColorPaletteDef cpd = null; + + if (imageInfo != null) { + cpd = imageInfo.getColorPaletteDef(); + fullPalette = ImageManager.createColorPalette(imageInfo); + } + + List shapes = footprints.getShapes(); + List values = footprints.getValues(); + + for (int ii = 0; ii < shapes.size(); ii++) { + CimrFootprintShape shape = shapes.get(ii); + double cx = shape.getGeoPos().getLon(); + double cy = shape.getGeoPos().getLat(); + double rx = shape.getMajorAxisDegree(); + double ry = shape.getMinorAxisDegree(); + + Ellipse2D modelEllipse = new Ellipse2D.Double(cx - rx, cy - ry, 2 * rx, 2 * ry); + double angleRad = Math.toRadians(shape.getAngle()); + + AffineTransform rotModel = AffineTransform.getRotateInstance(angleRad, cx, cy); + Shape rotatedModelShape = rotModel.createTransformedShape(modelEllipse); + Shape viewEllipse = m2vBase.createTransformedShape(rotatedModelShape); + + if (imageInfo != null && cpd != null) { + baseColor = getColorForValue(cpd, fullPalette, values.get(ii)); + } + + g.setColor(baseColor); + g.fill(viewEllipse); + } + + g.setStroke(oldStroke); + g.setColor(oldColor); + } + + private Color getColorForValue(ColorPaletteDef cpd, Color[] fullPalette, double value) { + int numColors = cpd.getNumColors(); + double min = cpd.getMinDisplaySample(); + double max = cpd.getMaxDisplaySample(); + + if (Double.compare(min, max) == 0) { + Color c = cpd.getLastPoint().getColor(); + return new Color(c.getRed(), c.getGreen(), c.getBlue()); + } + + double v = Math.max(min, Math.min(max, value)); + double f = (v - min) / (max - min); + + int idx = (int) Math.round(f * (numColors - 1)); + idx = Math.max(0, Math.min(idx, numColors - 1)); + + return fullPalette[idx]; + } +} diff --git a/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrSceneViewSelectionService.java b/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrSceneViewSelectionService.java new file mode 100644 index 000000000..c2acf1078 --- /dev/null +++ b/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrSceneViewSelectionService.java @@ -0,0 +1,57 @@ +package eu.esa.snap.cimr.ui; + +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.rcp.windows.ToolTopComponent; +import org.esa.snap.ui.product.ProductSceneView; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + + +public class CimrSceneViewSelectionService extends ToolTopComponent { + + private final List selectionListeners = new ArrayList<>(); + private ProductSceneView selectedSceneView; + + + @Override + protected void productSceneViewSelected(@NonNull ProductSceneView view) { + setSelectedSceneView(view); + } + + @Override + protected void productSceneViewDeselected(@NonNull ProductSceneView view) { + setSelectedSceneView(null); + } + + private void setSelectedSceneView(ProductSceneView newView) { + ProductSceneView oldView = selectedSceneView; + if (oldView == newView) { + return; + } + if (newView != null) { + Product p = newView.getProduct(); + if (p == null) { + return; + } + } + selectedSceneView = newView; + fireSelectionChange(oldView, newView); + } + + public synchronized void addSceneViewSelectionListener(SelectionListener l) { + selectionListeners.add(l); + } + + private void fireSelectionChange(ProductSceneView oldView, ProductSceneView newView) { + for (SelectionListener listener : new ArrayList<>(selectionListeners)) { + listener.handleSceneViewSelectionChanged(oldView, newView); + } + } + + + public interface SelectionListener { + void handleSceneViewSelectionChanged(ProductSceneView oldView, ProductSceneView newView); + } +} diff --git a/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrUIManager.java b/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrUIManager.java new file mode 100644 index 000000000..8bbd4d45c --- /dev/null +++ b/cimr-reader-ui/src/main/java/eu/esa/snap/cimr/ui/CimrUIManager.java @@ -0,0 +1,105 @@ +package eu.esa.snap.cimr.ui; + +import com.bc.ceres.binding.PropertySet; +import com.bc.ceres.glayer.Layer; +import com.bc.ceres.glayer.LayerType; +import com.bc.ceres.glayer.LayerTypeRegistry; +import com.bc.ceres.glayer.support.ImageLayer; +import com.bc.ceres.glayer.support.LayerUtils; +import eu.esa.snap.cimr.CimrL1BProductReader; +import eu.esa.snap.cimr.cimr.CimrFootprints; +import org.esa.snap.core.dataio.ProductReader; +import org.esa.snap.core.datamodel.RasterDataNode; +import org.esa.snap.core.layer.WorldMapLayerType; +import org.esa.snap.rcp.SnapApp; +import org.esa.snap.ui.product.ProductSceneView; +import org.openide.modules.OnStart; +import org.openide.windows.OnShowing; + +import java.util.logging.Logger; + + +public class CimrUIManager { + + private static final String WORLDMAP_TYPE_PROPERTY_NAME = "worldmap.type"; + private static final String BLUE_MARBLE_LAYER_TYPE = "BlueMarbleLayerType"; + + private static final Logger LOG = Logger.getLogger(CimrUIManager.class.getName()); + private static volatile CimrSceneViewSelectionService sceneViewSelectionService; + + @OnStart + public static class StartOp implements Runnable { + @Override + public void run() { + LOG.info("Starting CIMR UI"); + sceneViewSelectionService = new CimrSceneViewSelectionService(); + } + } + + @OnShowing + public static class ShowingOp implements Runnable { + @Override + public void run() { + LOG.info("CIMR UI showing – installing footprint overlay listener"); + sceneViewSelectionService.addSceneViewSelectionListener(CimrUIManager::handleSceneViewChange); + } + } + + private static void handleSceneViewChange(ProductSceneView oldView, ProductSceneView newView) { +// if (oldView != null) { +// oldView.getLayerCanvas().removeOverlay(CimrFootprintOverlay.INSTANCE); +// } + if (newView != null) { + // add worldmap layer + Layer worldMap = findWorldMapLayer(newView); + if (worldMap == null) { + worldMap = createWorldMapLayer(); + final Layer rootLayer = newView.getRootLayer(); + rootLayer.getChildren().add(worldMap); + } + worldMap.setVisible(true); + +// // add footprints +// CimrL1BProductReader cimrReader = getCimrReader(newView); +// if (cimrReader != null) { +// RasterDataNode raster = newView.getRaster(); +// String band = raster.getName(); +// CimrFootprints fps = cimrReader.getFootprints(band); +// if (!fps.getShapes().isEmpty()) { +// CimrFootprintOverlay.INSTANCE.setFootprints(fps); +// CimrFootprintOverlay.INSTANCE.setRaster(raster); +// newView.getLayerCanvas().addOverlay(CimrFootprintOverlay.INSTANCE); +// } +// } + } + } + +// private static CimrL1BProductReader getCimrReader(ProductSceneView view) { +// RasterDataNode raster = view.getRaster(); +// if (raster == null) { +// return null; +// } +// ProductReader reader = raster.getProductReader(); +// if (reader instanceof CimrL1BProductReader) { +// return (CimrL1BProductReader) reader; +// } +// return null; +// } + + private static Layer findWorldMapLayer(ProductSceneView view) { + return LayerUtils.getChildLayer(view.getRootLayer(), LayerUtils.SearchMode.DEEP, + layer -> layer.getLayerType() instanceof WorldMapLayerType); + } + + private static Layer createWorldMapLayer() { + final LayerType layerType = getWorldMapLayerType(); + final PropertySet template = layerType.createLayerConfig(null); + template.setValue(ImageLayer.PROPERTY_NAME_PIXEL_BORDER_SHOWN, false); + return layerType.createLayer(null, template); + } + + private static LayerType getWorldMapLayerType() { + String layerTypeClassName = SnapApp.getDefault().getPreferences().get(WORLDMAP_TYPE_PROPERTY_NAME, BLUE_MARBLE_LAYER_TYPE); + return LayerTypeRegistry.getLayerType(layerTypeClassName); + } +} diff --git a/cimr-reader-ui/src/main/nbm/manifest.mf b/cimr-reader-ui/src/main/nbm/manifest.mf new file mode 100644 index 000000000..ac61e99d3 --- /dev/null +++ b/cimr-reader-ui/src/main/nbm/manifest.mf @@ -0,0 +1,7 @@ +Manifest-Version: 1.0 +OpenIDE-Module-Specification-Version: ${microwavetbx.nbmSpecVersion} +OpenIDE-Module-Implementation-Version: ${microwavetbx.nbmImplVersion} +AutoUpdate-Show-In-Client: false +AutoUpdate-Essential-Module: false +OpenIDE-Module-Java-Dependencies: Java > 11 +OpenIDE-Module-Display-Category: SNAP Toolboxes diff --git a/cimr-reader-ui/src/test/java/eu/esa/snap/cimr/ui/CimrFootprintOverlayTest.java b/cimr-reader-ui/src/test/java/eu/esa/snap/cimr/ui/CimrFootprintOverlayTest.java new file mode 100644 index 000000000..ba8b65b06 --- /dev/null +++ b/cimr-reader-ui/src/test/java/eu/esa/snap/cimr/ui/CimrFootprintOverlayTest.java @@ -0,0 +1,52 @@ +package eu.esa.snap.cimr.ui; + +import com.bc.ceres.glayer.swing.LayerCanvas; +import com.bc.ceres.grender.Rendering; +import com.bc.ceres.grender.Viewport; +import eu.esa.snap.cimr.cimr.CimrFootprintShape; +import eu.esa.snap.cimr.cimr.CimrFootprints; +import org.esa.snap.core.datamodel.GeoPos; +import org.esa.snap.core.datamodel.RasterDataNode; +import org.junit.Test; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.*; + + +public class CimrFootprintOverlayTest { + + + @Test + public void testPaintOverlay_doesNotThrow() { + CimrFootprintOverlay overlay = CimrFootprintOverlay.INSTANCE; + + CimrFootprintShape shape = new CimrFootprintShape(new GeoPos(10f, 20f), 30.0, 1000.0, 2000.0); + ArrayList values = new ArrayList<>(); + values.add(1.0); + + overlay.setFootprints(new CimrFootprints(List.of(shape), values)); + + BufferedImage img = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = img.createGraphics(); + + LayerCanvas canvas = mock(LayerCanvas.class); + Rendering rendering = mock(Rendering.class); + Viewport vp = mock(Viewport.class); + RasterDataNode raster = mock(RasterDataNode.class); + CimrFootprintOverlay.INSTANCE.setRaster(raster); + + when(canvas.getViewport()).thenReturn(vp); + when(vp.getModelToViewTransform()).thenReturn(new AffineTransform()); + when(rendering.getGraphics()).thenReturn(g2d); + when(raster.getImageInfo()).thenReturn(null); + + // should not throw exception + overlay.paintOverlay(canvas, rendering); + } + +} \ No newline at end of file diff --git a/cimr-reader/pom.xml b/cimr-reader/pom.xml new file mode 100644 index 000000000..1111b5be5 --- /dev/null +++ b/cimr-reader/pom.xml @@ -0,0 +1,75 @@ + + 4.0.0 + + eu.esa.microwavetbx + microwave-toolbox + 14.0.0-SNAPSHOT + + + eu.esa.snap.cimr + cimr-reader + CIMR Reader + + nbm + + + 5.3.3 + + + + + org.esa.snap + snap-core + + + org.esa.snap + ceres-core + + + org.esa.snap + ceres-jai + + + org.esa.snap + snap-netcdf + + + edu.ucar + netcdfAll + ${netcdf.version} + + + org.mockito + mockito-core + test + + + + + + + org.apache.netbeans.utilities + nbm-maven-plugin + + + eu.esa.snap.cimr.* + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrL1BProductReader.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrL1BProductReader.java new file mode 100644 index 000000000..7e5df7b2d --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrL1BProductReader.java @@ -0,0 +1,118 @@ +package eu.esa.snap.cimr; + +import com.bc.ceres.core.ProgressMonitor; +import eu.esa.snap.cimr.cimr.*; +import eu.esa.snap.cimr.config.CimrConfigLoader; +import eu.esa.snap.cimr.grid.CimrBoundingBox; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.CimrGridFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrGeometryFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrBandFactory; +import org.esa.snap.core.dataio.AbstractProductReader; +import org.esa.snap.core.dataio.ProductReaderPlugIn; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.datamodel.ProductData; +import org.esa.snap.dataio.netcdf.util.NetcdfFileOpener; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.NetcdfFile; + +import java.awt.*; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.IOException; +import java.util.List; + + +public class CimrL1BProductReader extends AbstractProductReader { + + private NetcdfFile ncFile; + private CimrReaderContext readerContext; + + + public CimrL1BProductReader(ProductReaderPlugIn readerPlugIn) { + super(readerPlugIn); + } + + // TODO BL write tests + @Override + protected Product readProductNodesImpl() throws IOException { + final String path = getInputPath(); + + try { + this.ncFile = NetcdfFileOpener.open(path); + assert this.ncFile != null; + + this.readerContext = initContext(this.ncFile); + CimrGridProduct cimrGridProduct = CimrGridProduct.buildLazy(this.readerContext, true); + + // TODO: name and type from Metadata + Product snapProduct = CimrSnapProductBuilder.buildProduct("CIMR_L1B", "CIMR_L1B", cimrGridProduct, path); + + return snapProduct; + + } catch (Exception e) { + throw new IOException("Failed to read CIMR product from " + path, e); + } + } + + @Override + protected void readBandRasterDataImpl(int sourceOffsetX, int sourceOffsetY, int sourceWidth, int sourceHeight, int sourceStepX, int sourceStepY, Band destBand, int destOffsetX, int destOffsetY, int destWidth, int destHeight, ProductData destBuffer, ProgressMonitor pm) throws IOException { + // TODO BL handle destination offsets + final RenderedImage image = destBand.getSourceImage(); + final Raster data = image.getData(new Rectangle(destOffsetX, destOffsetY, destWidth, destHeight)); + data.getDataElements(destOffsetX, destOffsetY, destWidth, destHeight, destBuffer.getElems()); + } + + @Override + public void close() throws IOException { + if (this.ncFile != null) { + this.ncFile.close(); + this.ncFile = null; + } + if (this.readerContext != null) { + this.readerContext.clearCache(); + this.readerContext = null; + } + super.close(); + } + + public CimrFootprints getFootprints(String name) { + CimrBandDescriptor desc = this.readerContext.getDescriptorSet().getMeasurementByName(name); + if (desc == null) { + desc = this.readerContext.getDescriptorSet().getTpVariableByName(name); + } + if (desc == null) { + return new CimrFootprints( List.of(), List.of()); + } + return this.readerContext.getOrCreateFootprints(desc); + } + + + private String getInputPath() { + Object input = getInput(); + if (!(input instanceof String || input instanceof File)) { + throw new IllegalArgumentException("Unsupported input: " + input); + } + + if (input instanceof File) { + return ((File) input).getPath(); + } + return (String) input; + } + + private CimrReaderContext initContext(NetcdfFile ncFile) throws IOException, InvalidRangeException { + CimrDescriptorSet descriptorSet = CimrConfigLoader.load("cimr-l1b-config.json"); + CimrDimensions dimensions = CimrDimensions.from(ncFile); + + CimrBandDescriptor bbDescriptor = descriptorSet.getMeasurements().getFirst(); + NetcdfCimrGeometryFactory geometryFactory = new NetcdfCimrGeometryFactory(ncFile, descriptorSet.getGeometries(), dimensions); + CimrBoundingBox bBox = geometryFactory.getBoundingBox(bbDescriptor, CimrGridFactory.DEFAULT_CELL_SIZE_DEG); + + CimrGrid cimrGrid = CimrGridFactory.createPlateCarreeFromBoundingBox(bBox); + NetcdfCimrBandFactory bandFactory = new NetcdfCimrBandFactory(ncFile, dimensions); + + return new CimrReaderContext(ncFile, descriptorSet, cimrGrid, geometryFactory, bandFactory); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrL1BProductReaderPlugin.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrL1BProductReaderPlugin.java new file mode 100644 index 000000000..1c96715f8 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrL1BProductReaderPlugin.java @@ -0,0 +1,71 @@ +package eu.esa.snap.cimr; + +import org.esa.snap.core.dataio.DecodeQualification; +import org.esa.snap.core.dataio.ProductReader; +import org.esa.snap.core.dataio.ProductReaderPlugIn; +import org.esa.snap.core.util.io.FileUtils; +import org.esa.snap.core.util.io.SnapFileFilter; + +import java.io.File; +import java.util.Locale; + + +public class CimrL1BProductReaderPlugin implements ProductReaderPlugIn { + + private static final String EXTENSION = ".nc"; + private static final String NAME_PATTERN = "^W_[A-Za-z]{2}-[A-Za-z]{2,3}+-[A-Za-z]{1,11}+-SAT-CIMR-1B_C_(?:DME|ESA)_\\d{8}T\\d{6}_[A-Z]{1,2}+_\\d{8}T\\d{6}_\\d{8}T\\d{6}_[A-Z0-9_]{0,3}\\.nc$"; + + + @Override + public DecodeQualification getDecodeQualification(Object input) { + final File file = input instanceof File ? (File) input : new File(input.toString()); + final String fileName = file.getName(); + + final String extension = FileUtils.getExtension(fileName); + if (!EXTENSION.equals(extension)) { + return DecodeQualification.UNABLE; + } + + if (isValidCimrL1BProduct(fileName)) { + return DecodeQualification.INTENDED; + } + + return DecodeQualification.UNABLE; + } + + @Override + public Class[] getInputTypes() { + return new Class[]{File.class, String.class}; + } + + @Override + public ProductReader createReaderInstance() { + return new CimrL1BProductReader(this); + } + + + @Override + public String[] getFormatNames() { + return new String[]{"CIMR-L1B"}; + } + + @Override + public String[] getDefaultFileExtensions() { + return new String[]{EXTENSION}; + } + + @Override + public String getDescription(Locale locale) { + return "CIMR Level 1B Data Products in NetCDF Format"; + } + + @Override + public SnapFileFilter getProductFileFilter() { + return new SnapFileFilter(getFormatNames()[0], getDefaultFileExtensions(), getDescription(null)); + } + + + private boolean isValidCimrL1BProduct(String fileName) { + return fileName.matches(NAME_PATTERN); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrReaderContext.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrReaderContext.java new file mode 100644 index 000000000..795e12e84 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/CimrReaderContext.java @@ -0,0 +1,115 @@ +package eu.esa.snap.cimr; + +import eu.esa.snap.cimr.cimr.*; +import eu.esa.snap.cimr.grid.*; +import eu.esa.snap.cimr.netcdf.NetcdfCimrFootprintFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrGeometryFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrBandFactory; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.NetcdfFile; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +public class CimrReaderContext { + + private final NetcdfFile ncFile; + private final CimrDescriptorSet descriptorSet; + private final CimrGrid cimrGrid; + private final GeometryBandToGridMapper mapper; + private final NetcdfCimrGeometryFactory geometryFactory; + private final NetcdfCimrBandFactory bandFactory; + private final NetcdfCimrFootprintFactory footprintFactory; + + private final Map geometryBandCache = new ConcurrentHashMap<>(); + private final Map> footprintCache = new ConcurrentHashMap<>(); + + + public CimrReaderContext(NetcdfFile ncFile, + CimrDescriptorSet descriptorSet, + CimrGrid cimrGrid, + NetcdfCimrGeometryFactory geomFactory, + NetcdfCimrBandFactory bandFactory) { + this.ncFile = ncFile; + this.descriptorSet = descriptorSet; + this.cimrGrid = cimrGrid; + this.mapper = new GeometryBandToGridMapper(); + this.geometryFactory = geomFactory; + this.bandFactory = bandFactory; + this.footprintFactory = new NetcdfCimrFootprintFactory(); + } + + + public CimrGrid getGlobalGrid() { + return this.cimrGrid; + } + + public CimrDescriptorSet getDescriptorSet() { + return this.descriptorSet; + } + + public GridBandDataSource getOrCreateGridForVariable(CimrBandDescriptor varDesc, boolean useAverage) { + CimrGeometryBand geometryBand = getOrCreateGeometryBand(varDesc); + CimrGridBuilder gridBuilder = new CimrGridBuilder(this.mapper); + return gridBuilder.build(geometryBand, this.cimrGrid, useAverage); + } + + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor varDesc) { + try { + return this.geometryFactory.getOrCreateGeometry(varDesc); + } catch (IOException | InvalidRangeException e) { + throw new RuntimeException("Failed to build geometry for variable " + varDesc.getName(), e); + } + } + + private CimrGeometryBand getOrCreateGeometryBand(CimrBandDescriptor varDesc) { + String key = getGeometryBandKey(varDesc); + return this.geometryBandCache.computeIfAbsent(key, k -> { + try { + CimrGeometry geom = getOrCreateGeometry(varDesc); + return this.bandFactory.createGeometryBand(varDesc, geom); + } catch (IOException | InvalidRangeException e) { + throw new RuntimeException("Failed to build geometry band for variable " + varDesc.getName(), e); + } + }); + } + + private String getGeometryBandKey(CimrBandDescriptor varDesc) { + return varDesc.getBand().name() + ":" + varDesc.getValueVarName() + ":" + varDesc.getFeedIndex(); + } + + public CimrFootprints getOrCreateFootprints(CimrBandDescriptor varDesc) { + String key = getFootprintKey(varDesc); + + List shapes = this.footprintCache.computeIfAbsent(key, d -> { + CimrBandDescriptor minorAxisDesc = this.descriptorSet.getTpVariableByName(varDesc.getFootprintVars()[0]); + CimrBandDescriptor majorAxisDesc = this.descriptorSet.getTpVariableByName(varDesc.getFootprintVars()[1]); + CimrBandDescriptor angleDesc = this.descriptorSet.getTpVariableByName(varDesc.getFootprintVars()[2]); + + CimrGeometryBand geometryBand = getOrCreateGeometryBand(varDesc); + CimrGeometryBand minorAxisBand = getOrCreateGeometryBand(minorAxisDesc); + CimrGeometryBand majorAxisBand = getOrCreateGeometryBand(majorAxisDesc); + CimrGeometryBand angleBand = getOrCreateGeometryBand(angleDesc); + + return footprintFactory.createFootprintShapes(geometryBand, minorAxisBand, majorAxisBand, angleBand); + }); + + CimrGeometryBand geometryBand = getOrCreateGeometryBand(varDesc); + List values = this.footprintFactory.getFootprintValues(geometryBand); + + return new CimrFootprints(shapes, values); + } + + private String getFootprintKey(CimrBandDescriptor varDesc) { + return varDesc.getBand().name() + ":" + varDesc.getFeedIndex(); + } + + public void clearCache() { + this.geometryBandCache.clear(); + this.footprintCache.clear(); + this.geometryFactory.clearCache(); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrBandDescriptor.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrBandDescriptor.java new file mode 100644 index 000000000..0a26404a2 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrBandDescriptor.java @@ -0,0 +1,82 @@ +package eu.esa.snap.cimr.cimr; + + +public class CimrBandDescriptor { + + private final String name; + private final String valueVarName; + private final CimrFrequencyBand band; + private final String[] geometryNames; + private final String[] footprintVars; + private final String groupPath; + private final int feedIndex; + private final CimrDescriptorKind kind; + private final String[] dimensions; + private final String dataType; + private final String unit; + private final String description; + + + public CimrBandDescriptor(String name, String valueVarName, CimrFrequencyBand band, String[] geometryNames, String[] footprintVars, String groupPath, int feedIndex, CimrDescriptorKind kind, String[] dimensions, String dataType, String unit, String description) { + this.name = name; + this.valueVarName = valueVarName; + this.band = band; + this.geometryNames = geometryNames; + this.footprintVars = footprintVars; + this.groupPath = groupPath; + this.feedIndex = feedIndex; + this.kind = kind; + this.dimensions = dimensions; + this.dataType = dataType; + this.unit = unit; + this.description = description; + } + + public String getName() { + return name; + } + + public String getValueVarName() { + return valueVarName; + } + + public CimrFrequencyBand getBand() { + return band; + } + + public String[] getGeometryNames() { + return geometryNames; + } + + public String[] getFootprintVars() { + return footprintVars; + } + + public String getGroupPath() { + return groupPath; + } + + public int getFeedIndex() { + return feedIndex; + } + + public CimrDescriptorKind getKind() { + return kind; + } + + public String[] getDimensions() { + return dimensions; + } + + public String getDataType() { + return dataType; + } + + public String getUnit() { + return unit; + } + + public String getDescription() { + return description; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDescriptorKind.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDescriptorKind.java new file mode 100644 index 000000000..7029e7342 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDescriptorKind.java @@ -0,0 +1,7 @@ +package eu.esa.snap.cimr.cimr; + +public enum CimrDescriptorKind { + VARIABLE, + TIEPOINT_VARIABLE, + GEOMETRY +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDescriptorSet.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDescriptorSet.java new file mode 100644 index 000000000..564a7fa62 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDescriptorSet.java @@ -0,0 +1,59 @@ +package eu.esa.snap.cimr.cimr; + +import java.util.List; + + +public class CimrDescriptorSet { + + private final List measurements; + private final List geometries; + private final List tiepointVariables; + + + public CimrDescriptorSet(List measurements, + List geometries, + List tiepointVariables) { + this.measurements = measurements; + this.geometries = geometries; + this.tiepointVariables = tiepointVariables; + } + + public CimrBandDescriptor getGeometryByName(String name) { + for (CimrBandDescriptor descriptor : this.geometries) { + if (descriptor.getName().equals(name)) { + return descriptor; + } + } + return null; + } + + public CimrBandDescriptor getTpVariableByName(String name) { + for (CimrBandDescriptor descriptor : this.tiepointVariables) { + if (descriptor.getName().equals(name)) { + return descriptor; + } + } + return null; + } + + public CimrBandDescriptor getMeasurementByName(String name) { + for (CimrBandDescriptor descriptor : this.measurements) { + if (descriptor.getName().equals(name)) { + return descriptor; + } + } + return null; + } + + public List getMeasurements() { + return this.measurements; + } + + public List getGeometries() { + return this.geometries; + } + + public List getTiepointVariables() { + return this.tiepointVariables; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDimensions.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDimensions.java new file mode 100644 index 000000000..110d3c280 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrDimensions.java @@ -0,0 +1,37 @@ +package eu.esa.snap.cimr.cimr; + +import ucar.nc2.Dimension; +import ucar.nc2.NetcdfFile; + +import java.util.HashMap; +import java.util.Map; + + +public class CimrDimensions { + + private final Map values = new HashMap<>(); + + public CimrDimensions() { + } + + public CimrDimensions(Map valMap) { + this.values.putAll(valMap); + } + + + public static CimrDimensions from(NetcdfFile ncFile) { + CimrDimensions dims = new CimrDimensions(); + for (Dimension dim : ncFile.getDimensions()) { + dims.values.put(dim.getShortName(), dim.getLength()); + } + return dims; + } + + public int get(String name) { + Integer v = values.get(name); + if (v == null) { + throw new IllegalArgumentException("Unknown dimension: " + name); + } + return v; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFootprintShape.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFootprintShape.java new file mode 100644 index 000000000..d5e4e2757 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFootprintShape.java @@ -0,0 +1,42 @@ +package eu.esa.snap.cimr.cimr; + +import org.esa.snap.core.datamodel.GeoPos; + +public class CimrFootprintShape { + + GeoPos geoPos; + double angle; // degree + double minor_axis; + double major_axis; + + public CimrFootprintShape(GeoPos geoPos, double angle, double minor_axis, double major_axis) { + this.geoPos = geoPos; + this.angle = angle; + this.minor_axis = minor_axis; + this.major_axis = major_axis; + } + + public GeoPos getGeoPos() { + return geoPos; + } + + public double getAngle() { + return angle; + } + + public double getMinorAxisDegree() { + return metersToLatDeg(minor_axis); + } + + public double getMajorAxisDegree() { + return metersToLonDeg(major_axis, geoPos.getLat()); + } + + private double metersToLatDeg(double meters) { + return meters / 111320.0; + } + + private double metersToLonDeg(double meters, double latDeg) { + return meters / (111320.0 * Math.cos(Math.toRadians(latDeg))); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFootprints.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFootprints.java new file mode 100644 index 000000000..0fca2444b --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFootprints.java @@ -0,0 +1,22 @@ +package eu.esa.snap.cimr.cimr; + +import java.util.List; + +public class CimrFootprints { + + private final List shapes; + private final List values; + + public CimrFootprints(List shapes, List values) { + this.shapes = shapes; + this.values = values; + } + + public List getShapes() { + return shapes; + } + + public List getValues() { + return values; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFrequencyBand.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFrequencyBand.java new file mode 100644 index 000000000..039a84736 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrFrequencyBand.java @@ -0,0 +1,21 @@ +package eu.esa.snap.cimr.cimr; + +public enum CimrFrequencyBand { + + L_BAND(2.12e8f), + C_BAND(4.33e7f), + X_BAND(2.82e7f), + KU_BAND(1.6e7f), + KA_BAND(8.22e6f); + + + private final float spectralWaveLength; + + CimrFrequencyBand(float spectralWaveLength) { + this.spectralWaveLength = spectralWaveLength; + } + + public float getSpectralWaveLength() { + return spectralWaveLength; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridBuilder.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridBuilder.java new file mode 100644 index 000000000..bcdbc2534 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridBuilder.java @@ -0,0 +1,27 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.grid.CimrBand; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.CimrGridBandDataSource; +import eu.esa.snap.cimr.grid.GeometryBandToGridMapper; + + +public class CimrGridBuilder { + + private final GeometryBandToGridMapper mapper; + + + public CimrGridBuilder(GeometryBandToGridMapper mapper) { + this.mapper = mapper; + } + + public CimrGridBandDataSource build(CimrBand band, CimrGrid grid, boolean useAverage) { + CimrGridBandDataSource target = CimrGridBandDataSource.createEmpty(grid.getWidth(), grid.getHeight()); + if (useAverage) { + mapper.mapAverage(band, grid, target); + } else { + mapper.mapNearest(band, grid, target); + } + return target; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridMultiLevelSource.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridMultiLevelSource.java new file mode 100644 index 000000000..66fe34c1f --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridMultiLevelSource.java @@ -0,0 +1,45 @@ +package eu.esa.snap.cimr.cimr; + +import com.bc.ceres.multilevel.MultiLevelModel; +import com.bc.ceres.multilevel.MultiLevelSource; +import com.bc.ceres.multilevel.support.AbstractMultiLevelSource; +import com.bc.ceres.multilevel.support.DefaultMultiLevelImage; +import com.bc.ceres.multilevel.support.DefaultMultiLevelModel; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.GridBandDataSource; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.image.ResolutionLevel; + +import java.awt.geom.AffineTransform; +import java.awt.image.RenderedImage; + + +public class CimrGridMultiLevelSource extends AbstractMultiLevelSource { + + private static final int MLM_LEVEL_COUNT = 7; + + private final Band targetBand; + private final GridBandDataSource gridDataSource; + + + public CimrGridMultiLevelSource(MultiLevelModel multiLevelModel, Band targetBand, + GridBandDataSource gridDataSource) { + super(multiLevelModel); + this.targetBand = targetBand; + this.gridDataSource = gridDataSource; + } + + @Override + protected RenderedImage createImage(int level) { + ResolutionLevel resLevel = ResolutionLevel.create(getModel(), level); + return new CimrGridOpImage(targetBand, resLevel, gridDataSource); + } + + public static void attachToBand(Band band, GridBandDataSource gridDataSource, CimrGrid grid) { + AffineTransform imageToModel = grid.getProjection().getAffineTransform(grid); + MultiLevelModel model = new DefaultMultiLevelModel(MLM_LEVEL_COUNT, imageToModel, grid.getWidth(), grid.getHeight()); + + MultiLevelSource source = new CimrGridMultiLevelSource(model, band, gridDataSource); + band.setSourceImage(new DefaultMultiLevelImage(source)); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridOpImage.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridOpImage.java new file mode 100644 index 000000000..1a7986cb1 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridOpImage.java @@ -0,0 +1,69 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.grid.GridBandDataSource; +import org.esa.snap.core.datamodel.ProductData; +import org.esa.snap.core.datamodel.RasterDataNode; +import org.esa.snap.core.image.RasterDataNodeOpImage; +import org.esa.snap.core.image.ResolutionLevel; + +import java.awt.*; + + +public class CimrGridOpImage extends RasterDataNodeOpImage { + + private final GridBandDataSource gridDataSource; + + + public CimrGridOpImage(RasterDataNode rasterDataNode, ResolutionLevel level, GridBandDataSource gridBandDataSource) { + super(rasterDataNode, level); + this.gridDataSource = gridBandDataSource; + } + + + @Override + protected void computeProductData(ProductData productData, Rectangle region) { + int w = region.width; + int h = region.height; + + int levelIndex = getLevel(); + double scale = Math.pow(2.0, levelIndex); + int blockSize = (int) Math.round(scale); + + int baseWidth = getRasterDataNode().getRasterWidth(); + int baseHeight = getRasterDataNode().getRasterHeight(); + + int idx = 0; + for (int dy = 0; dy < h; dy++) { + int yLevel = region.y + dy; + int y0 = (int) Math.floor(yLevel * scale); + int y1 = Math.min(y0 + blockSize, baseHeight); + + for (int dx = 0; dx < w; dx++) { + int xLevel = region.x + dx; + int x0 = (int) Math.floor(xLevel * scale); + int x1 = Math.min(x0 + blockSize, baseWidth); + + double sum = 0.0; + int count = 0; + + for (int yy = y0; yy < y1; yy++) { + for (int xx = x0; xx < x1; xx++) { + double v; + try { + v = gridDataSource.getSample(xx, yy); + } catch (IllegalArgumentException e) { + v = Double.NaN; + } + if (!Double.isNaN(v)) { + sum += v; + count++; + } + } + } + + double out = (count > 0) ? (sum / count) : Double.NaN; + productData.setElemDoubleAt(idx++, out); + } + } + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridProduct.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridProduct.java new file mode 100644 index 000000000..d990505f4 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrGridProduct.java @@ -0,0 +1,63 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.CimrReaderContext; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.GridBandDataSource; +import eu.esa.snap.cimr.grid.LazyGridBandDataSource; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + + +public class CimrGridProduct { + + private final CimrGrid cimrGrid; + private final Map bands = new LinkedHashMap<>(); + + + public CimrGridProduct(CimrGrid cimrGrid) { + this.cimrGrid = cimrGrid; + } + + + public CimrGrid getGlobalGrid() { + return cimrGrid; + } + + public void addBand(CimrBandDescriptor descriptor, GridBandDataSource dataSource) { + bands.put(descriptor, dataSource); + } + + public GridBandDataSource getBandData(CimrBandDescriptor descriptor) { + return bands.get(descriptor); + } + + public Map getBands() { + return Collections.unmodifiableMap(bands); + } + + public int getBandCount() { + return bands.size(); + } + + + public static CimrGridProduct buildLazy(CimrReaderContext context, boolean useAverage) { + CimrGrid cimrGrid = context.getGlobalGrid(); + CimrDescriptorSet descriptorSet = context.getDescriptorSet(); + + CimrGridProduct product = new CimrGridProduct(cimrGrid); + + for (CimrBandDescriptor desc : descriptorSet.getTiepointVariables()) { + GridBandDataSource dataSource = new LazyGridBandDataSource(context, desc, useAverage); + product.addBand(desc, dataSource); + } + + for (CimrBandDescriptor desc : descriptorSet.getMeasurements()) { + GridBandDataSource dataSource = new LazyGridBandDataSource(context, desc, useAverage); + product.addBand(desc, dataSource); + } + + return product; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrSnapProductBuilder.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrSnapProductBuilder.java new file mode 100644 index 000000000..e3cf682b1 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/cimr/CimrSnapProductBuilder.java @@ -0,0 +1,56 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.GridBandDataSource; +import eu.esa.snap.cimr.grid.LazyCrsGeoCoding; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.datamodel.ProductData; +import org.esa.snap.core.datamodel.GeoCoding; + +import java.io.File; +import java.util.Map; + + +public class CimrSnapProductBuilder { + + private static final String AUTO_GROUPING = "L_BAND:C_BAND:X_BAND:KU_BAND:KA_BAND"; + + + public static Product buildProduct(String productName, String productType, CimrGridProduct cimrProduct, String path) throws Exception { + CimrGrid grid = cimrProduct.getGlobalGrid(); + Product product = new Product(productName, productType, grid.getWidth(), grid.getHeight()); + + addGeoCoding(grid, product); + addBands(cimrProduct, product); + + product.setFileLocation(new File(path)); + product.setAutoGrouping(AUTO_GROUPING); + + return product; + } + + private static void addGeoCoding(CimrGrid grid, Product product) { + GeoCoding geoCoding = new LazyCrsGeoCoding(grid); + product.setSceneGeoCoding(geoCoding); + } + + + private static void addBands(CimrGridProduct cimrProduct, Product product) { + CimrGrid grid = cimrProduct.getGlobalGrid(); + + for (Map.Entry e : cimrProduct.getBands().entrySet()) { + CimrBandDescriptor desc = e.getKey(); + GridBandDataSource dataSource = e.getValue(); + + Band band = product.addBand(desc.getName(), ProductData.TYPE_FLOAT64); + band.setDescription(desc.getDescription()); + band.setUnit(desc.getUnit()); + band.setNoDataValue(Double.NaN); + band.setNoDataValueUsed(true); + band.setSpectralWavelength(desc.getBand().getSpectralWaveLength()); + + CimrGridMultiLevelSource.attachToBand(band, dataSource, grid); + } + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrBandEntry.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrBandEntry.java new file mode 100644 index 000000000..483b922fc --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrBandEntry.java @@ -0,0 +1,16 @@ +package eu.esa.snap.cimr.config; + +public class CimrBandEntry { + + public String name; + public String valueVarName; + public String band; + public String[] geometryNames; + public String[] footprintVars; + public String groupPath; + public int feedIndex; + public String[] dimensions; + public String dataType; + public String unit = ""; + public String description = ""; +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrConfig.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrConfig.java new file mode 100644 index 000000000..6f47a8a93 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrConfig.java @@ -0,0 +1,36 @@ +package eu.esa.snap.cimr.config; + +import java.util.List; + + +public class CimrConfig { + + private List variables; + private List tiepointVariables; + private List geometries; + + + public List getVariables() { + return variables; + } + + public List getTiepointVariables() { + return tiepointVariables; + } + + public List getGeometries() { + return geometries; + } + + public void setVariables(List variables) { + this.variables = variables; + } + + public void setTiepointVariables(List tiepointVariables) { + this.tiepointVariables = tiepointVariables; + } + + public void setGeometries(List geometries) { + this.geometries = geometries; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrConfigLoader.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrConfigLoader.java new file mode 100644 index 000000000..aeed76970 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/config/CimrConfigLoader.java @@ -0,0 +1,59 @@ +package eu.esa.snap.cimr.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDescriptorKind; +import eu.esa.snap.cimr.cimr.CimrDescriptorSet; +import eu.esa.snap.cimr.cimr.CimrFrequencyBand; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + + +public class CimrConfigLoader { + + + public static CimrDescriptorSet load(String jsonPath) throws IOException { + try (InputStream in = CimrConfigLoader.class.getResourceAsStream(jsonPath)) { + if (in == null) { + throw new IOException("Config resource 'cimr-config.json' not found on classpath"); + } + + ObjectMapper mapper = new ObjectMapper(); + CimrConfig cfg = mapper.readValue(in, CimrConfig.class); + + List meas = cfg.getVariables().stream() + .map(e -> toDescriptor(e, CimrDescriptorKind.VARIABLE)) + .toList(); + + List tpVal = cfg.getTiepointVariables().stream() + .map(e -> toDescriptor(e, CimrDescriptorKind.TIEPOINT_VARIABLE)) + .toList(); + + List tpGeo = cfg.getGeometries().stream() + .map(e -> toDescriptor(e, CimrDescriptorKind.GEOMETRY)) + .toList(); + + return new CimrDescriptorSet(meas, tpGeo, tpVal); + } + } + + private static CimrBandDescriptor toDescriptor(CimrBandEntry e, CimrDescriptorKind kind) { + CimrFrequencyBand band = CimrFrequencyBand.valueOf(e.band); + return new CimrBandDescriptor( + e.name, + e.valueVarName, + band, + e.geometryNames, + e.footprintVars, + e.groupPath, + e.feedIndex, + kind, + e.dimensions, + e.dataType, + e.unit, + e.description + ); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrBand.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrBand.java new file mode 100644 index 000000000..1587abd66 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrBand.java @@ -0,0 +1,12 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; + + +public interface CimrBand { + + int getScanCount(); + int getSampleCount(); + double getValue(int scanIndex, int sampleIndex); + GeoPos getGeoPos(int scanIndex, int sampleIndex); +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrBoundingBox.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrBoundingBox.java new file mode 100644 index 000000000..0eb2b37af --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrBoundingBox.java @@ -0,0 +1,78 @@ +package eu.esa.snap.cimr.grid; + + +public class CimrBoundingBox { + + private static final double DEFAULT_BOUNDING_BOX_OFFSET_DEG = .5; + + double lonMin; + double lonMax; + double latMin; + double latMax; + + + public CimrBoundingBox(double lonMin, double lonMax, double latMin, double latMax) { + this.lonMin = lonMin; + this.lonMax = lonMax; + this.latMin = latMin; + this.latMax = latMax; + } + + public double getLonMin() { + return lonMin; + } + + public double getLonMax() { + return lonMax; + } + + public double getLatMin() { + return latMin; + } + + public double getLatMax() { + return latMax; + } + + public double getWidth() { + return lonMax - lonMin; + } + + public double getHeight() { + return latMax - latMin; + } + + public static CimrBoundingBox create(CimrGeometry geometry, double cellSizeDeg) { + double lonMin = Double.POSITIVE_INFINITY; + double lonMax = Double.NEGATIVE_INFINITY; + double latMin = Double.POSITIVE_INFINITY; + double latMax = Double.NEGATIVE_INFINITY; + + for (int ii = 0; ii < geometry.getScanCount(); ii++) { + for(int jj = 0; jj < geometry.getSampleCount(); jj++) { + double lon = geometry.getGeoPos(ii, jj, 0).getLon(); + double lat = geometry.getGeoPos(ii, jj, 0).getLat(); + + if (lon < lonMin) lonMin = lon; + if (lon > lonMax) lonMax = lon; + if (lat < latMin) latMin = lat; + if (lat > latMax) latMax = lat; + } + } + + lonMin = snapToGrid(lonMin - DEFAULT_BOUNDING_BOX_OFFSET_DEG, true, cellSizeDeg); + lonMax = snapToGrid(lonMax + DEFAULT_BOUNDING_BOX_OFFSET_DEG, false, cellSizeDeg); + latMin = snapToGrid(latMin - DEFAULT_BOUNDING_BOX_OFFSET_DEG, true, cellSizeDeg); + latMax = snapToGrid(latMax + DEFAULT_BOUNDING_BOX_OFFSET_DEG, false, cellSizeDeg); + + return new CimrBoundingBox(lonMin, lonMax, latMin, latMax); + } + + private static double snapToGrid(double val, boolean isMin, double cellSizeDeg) { + if (isMin) { + return Math.floor(val / cellSizeDeg) * cellSizeDeg; + } else { + return Math.ceil(val / cellSizeDeg) * cellSizeDeg; + } + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGeometry.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGeometry.java new file mode 100644 index 000000000..dad0366c9 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGeometry.java @@ -0,0 +1,12 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; + + +public interface CimrGeometry { + + int getScanCount(); + int getSampleCount(); + int getTiePointCount(); + GeoPos getGeoPos(int scanIndex, int sampleIndex, int feedIndex); +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGeometryBand.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGeometryBand.java new file mode 100644 index 000000000..bcbc670be --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGeometryBand.java @@ -0,0 +1,63 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; + + +public class CimrGeometryBand implements CimrBand { + + private final double[][] values; + private final CimrGeometry geometry; + private final int feedIndex; + + + public CimrGeometryBand(double[][] values, + CimrGeometry geometry, + int feedIndex) { + if (values == null || geometry == null) { + throw new IllegalArgumentException("values and geometry must not be null"); + } + if (values.length == 0) { + throw new IllegalArgumentException("values must not be empty"); + } + int sampleCount = values[0].length; + for (double[] row : values) { + if (row.length != sampleCount) { + throw new IllegalArgumentException("all scan rows must have same sample count"); + } + } + if (geometry.getScanCount() != values.length) { + throw new IllegalArgumentException("geometry scanCount does not match values"); + } + if (geometry.getSampleCount() != sampleCount) { + throw new IllegalArgumentException("geometry sampleCount does not match values"); + } + + this.values = values; + this.geometry = geometry; + this.feedIndex = feedIndex; + } + + @Override + public int getScanCount() { + return values.length; + } + + @Override + public int getSampleCount() { + return values[0].length; + } + + @Override + public GeoPos getGeoPos(int scanIndex, int sampleIndex) { + return geometry.getGeoPos(scanIndex, sampleIndex, 0); + } + + @Override + public double getValue(int scanIndex, int sampleIndex) { + return values[scanIndex][sampleIndex]; + } + + public int getFeedIndex() { + return feedIndex; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGrid.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGrid.java new file mode 100644 index 000000000..6c307ca53 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGrid.java @@ -0,0 +1,40 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; + +import java.awt.*; + + +public class CimrGrid { + + private final int width; + private final int height; + private final GridProjection projection; + + + public CimrGrid(GridProjection projection, int width, int height) { + this.projection = projection; + this.height = height; + this.width = width; + } + + public GeoPos gridToGeoPos(int x, int y) { + return projection.gridToGeoPos(x, y); + } + + public boolean geoPosToGrid(GeoPos pos, Point out) { + return projection.geoPosToGrid(pos, out); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public GridProjection getProjection() { + return projection; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGridBandDataSource.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGridBandDataSource.java new file mode 100644 index 000000000..8bc79adfe --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGridBandDataSource.java @@ -0,0 +1,56 @@ +package eu.esa.snap.cimr.grid; + +import java.util.Arrays; + + +public class CimrGridBandDataSource implements GridBandDataSource { + + private final int width; + private final int height; + private final double[] data; + + + public CimrGridBandDataSource(int width, int height, double[] data) { + if (data.length != width * height) { + throw new IllegalArgumentException("data length must be width * height"); + } + this.width = width; + this.height = height; + this.data = data; + } + + + public static CimrGridBandDataSource createEmpty(int width, int height) { + double[] data = new double[width * height]; + Arrays.fill(data, Double.NaN); + return new CimrGridBandDataSource(width, height, data); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + @Override + public double getSample(int x, int y) { + checkBounds(x, y); + int index = y * width + x; + return data[index]; + } + + @Override + public void setSample(int x, int y, double value) { + checkBounds(x, y); + int index = y * width + x; + data[index] = value; + } + + private void checkBounds(int x, int y) { + if (x < 0 || x >= width || y < 0 || y >= height) { + throw new IllegalArgumentException("Grid index out of range: x=" + x + ", y=" + y); + } + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGridFactory.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGridFactory.java new file mode 100644 index 000000000..2219a5afa --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrGridFactory.java @@ -0,0 +1,38 @@ +package eu.esa.snap.cimr.grid; + + +public class CimrGridFactory { + + public static final double DEFAULT_CELL_SIZE_DEG = .02; + + + public static CimrGrid createGlobalPlateCarree(double cellSizeDeg) { + int width = (int) Math.round(360.0 / cellSizeDeg); + int height = (int) Math.round(180.0 / cellSizeDeg); + + double lonMin = -180.0; + double latMax = 90.0; + + PlateCarreeProjection proj = new PlateCarreeProjection( + width, height, lonMin, latMax, cellSizeDeg, cellSizeDeg + ); + return new CimrGrid(proj, width, height); + } + + public static CimrGrid createPlateCarreeFromBoundingBox(CimrBoundingBox bBox) { + return createPlateCarreeFromBoundingBox(bBox, DEFAULT_CELL_SIZE_DEG); + } + + public static CimrGrid createPlateCarreeFromBoundingBox(CimrBoundingBox bBox, double cellSizeDeg) { + int width = (int) Math.round(bBox.getWidth() / cellSizeDeg); + int height = (int) Math.round(bBox.getHeight() / cellSizeDeg); + + double lonMin = bBox.lonMin; + double latMax = bBox.latMax; + + PlateCarreeProjection proj = new PlateCarreeProjection( + width, height, lonMin, latMax, cellSizeDeg, cellSizeDeg + ); + return new CimrGrid(proj, width, height); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrTiepointGeometry.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrTiepointGeometry.java new file mode 100644 index 000000000..c177d1e31 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/CimrTiepointGeometry.java @@ -0,0 +1,87 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; + + +public class CimrTiepointGeometry implements CimrGeometry { + + private final int scanCount; + private final int sampleCount; + private final int tiePointCount; + private final GeoPos[][][] tiePoints; // [scan][tiePoint][feed] + + + public CimrTiepointGeometry(GeoPos[][][] tiePoints, int sampleCount) { + if (tiePoints.length == 0) { + throw new IllegalArgumentException("scan dimension must be > 0"); + } + int scans = tiePoints.length; + int tpCount = tiePoints[0].length; + if (tpCount == 0) { + throw new IllegalArgumentException("tiePoint dimension must be > 0"); + } + int feeds = tiePoints[0][0].length; + for (int s = 0; s < scans; s++) { + if (tiePoints[s].length != tpCount) { + throw new IllegalArgumentException("tiePoint dimension mismatch at scan " + s); + } + for (int tp = 0; tp < tpCount; tp++) { + if (tiePoints[s][tp].length != feeds) { + throw new IllegalArgumentException("feed dimension mismatch at scan " + s + ", tp " + tp); + } + } + } + if (sampleCount < 2) { + throw new IllegalArgumentException("sampleCount must be >= 2"); + } + + this.scanCount = scans; + this.tiePointCount = tpCount; + this.sampleCount = sampleCount; + this.tiePoints = tiePoints; + } + + @Override + public int getScanCount() { + return scanCount; + } + + @Override + public int getSampleCount() { + return sampleCount; + } + + @Override + public int getTiePointCount() { + return tiePointCount; + } + + @Override + public GeoPos getGeoPos(int scanIndex, int sampleIndex, int feedIndex) { + if (scanIndex < 0 || scanIndex >= scanCount) { + throw new IllegalArgumentException("scanIndex out of range: " + scanIndex); + } + if (sampleIndex < 0 || sampleIndex >= sampleCount) { + throw new IllegalArgumentException("sampleIndex out of range: " + sampleIndex); + } + if (feedIndex < 0 || feedIndex >= tiePoints[0][0].length) { + throw new IllegalArgumentException("feedIndex out of range: " + feedIndex); + } + + + // TODO: BL refactor interpolation method to be single class with methods for handling all cases + double t = (double) sampleIndex * (tiePointCount - 1) / (double) (sampleCount - 1); + + int tp0 = (int) Math.floor(t); + int tp1 = Math.min(tp0 + 1, tiePointCount - 1); + double f = t - tp0; + + GeoPos p0 = tiePoints[scanIndex][tp0][feedIndex]; + GeoPos p1 = tiePoints[scanIndex][tp1][feedIndex]; + + double lat = p0.getLat() + f * (p1.getLat() - p0.getLat()); + double lon = p0.getLon() + f * (p1.getLon() - p0.getLon()); + + return new GeoPos((float) lat, (float) lon); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GeometryBandToGridMapper.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GeometryBandToGridMapper.java new file mode 100644 index 000000000..b35d4033f --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GeometryBandToGridMapper.java @@ -0,0 +1,65 @@ +package eu.esa.snap.cimr.grid; + + +import org.esa.snap.core.datamodel.GeoPos; + +import java.awt.*; + + +public class GeometryBandToGridMapper { + + + public void mapNearest(CimrBand band, CimrGrid grid, GridBandDataSource target) { + Point gridPoint = new Point(); + + for (int ss = 0; ss < band.getScanCount(); ss++) { + for (int cc = 0; cc < band.getSampleCount(); cc++) { + + GeoPos geoPos = band.getGeoPos(ss, cc); + boolean inside = grid.geoPosToGrid(geoPos, gridPoint); + + if (!inside) { + continue; + } + double value = band.getValue(ss, cc); + + target.setSample(gridPoint.x, gridPoint.y, value); + } + } + } + + public void mapAverage(CimrBand band, CimrGrid grid, GridBandDataSource target) { + int width = grid.getWidth(); + int height = grid.getHeight(); + + double[] sum = new double[width * height]; + int[] count = new int[width * height]; + + Point p = new Point(); + + for (int ss = 0; ss < band.getScanCount(); ss++) { + for (int cc = 0; cc < band.getSampleCount(); cc++) { + GeoPos geo = band.getGeoPos(ss, cc); + if (!grid.geoPosToGrid(geo, p)) { + continue; + } + double value = band.getValue(ss, cc); + if (Double.isNaN(value)) { + continue; + } + int idx = p.y * width + p.x; + sum[idx] += value; + count[idx]++; + } + } + + for (int yy = 0; yy < height; yy++) { + for (int xx = 0; xx < width; xx++) { + int idx = yy * width + xx; + if (count[idx] > 0) { + target.setSample(xx, yy, sum[idx] / count[idx]); + } + } + } + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GridBandDataSource.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GridBandDataSource.java new file mode 100644 index 000000000..d02d9accc --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GridBandDataSource.java @@ -0,0 +1,8 @@ +package eu.esa.snap.cimr.grid; + + +public interface GridBandDataSource { + + double getSample(int x, int y); + void setSample(int x, int y, double value); +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GridProjection.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GridProjection.java new file mode 100644 index 000000000..056e53540 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/GridProjection.java @@ -0,0 +1,23 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import java.awt.*; +import java.awt.geom.AffineTransform; + + +public interface GridProjection { + + GeoPos gridToGeoPos(int x, int y); + boolean geoPosToGrid(GeoPos lat, Point out); + + CoordinateReferenceSystem getCrs() throws FactoryException; + AffineTransform getAffineTransform(CimrGrid grid); + + double getLonMin(); + double getLatMax(); + double getDeltaLon(); + double getDeltaLat(); +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/LazyCrsGeoCoding.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/LazyCrsGeoCoding.java new file mode 100644 index 000000000..a61472601 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/LazyCrsGeoCoding.java @@ -0,0 +1,109 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.*; +import org.esa.snap.core.dataop.maptransf.Datum; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.MathTransform; + +import java.awt.*; +import java.awt.geom.AffineTransform; + + +public class LazyCrsGeoCoding implements GeoCoding { + + private final CimrGrid grid; + private GeoCoding delegate; + + public LazyCrsGeoCoding(CimrGrid grid) { + this.grid = grid; + } + + private GeoCoding getDelegate() { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + try { + final int width = grid.getWidth(); + final int height = grid.getHeight(); + CoordinateReferenceSystem crs = grid.getProjection().getCrs(); + AffineTransform imageToModel = grid.getProjection().getAffineTransform(grid); + delegate = new CrsGeoCoding(crs, new Rectangle(width, height), imageToModel); + } catch (Exception e) { + throw new RuntimeException("Failed to create CrsGeoCoding", e); + } + } + } + } + return delegate; + } + + @Override + public boolean isCrossingMeridianAt180() { + return getDelegate().isCrossingMeridianAt180(); + } + + @Override + public boolean canGetPixelPos() { + return true; + } + + @Override + public boolean canGetGeoPos() { + return true; + } + + @Override + public PixelPos getPixelPos(GeoPos geoPos, PixelPos pixelPos) { + return getDelegate().getPixelPos(geoPos, pixelPos); + } + + @Override + public GeoPos getGeoPos(PixelPos pixelPos, GeoPos geoPos) { + return getDelegate().getGeoPos(pixelPos, geoPos); + } + + @Override + public Datum getDatum() { + return getDelegate().getDatum(); + } + + @Override + public void dispose() { + getDelegate().dispose(); + } + + @Override + public CoordinateReferenceSystem getImageCRS() { + return getDelegate().getImageCRS(); + } + + @Override + public CoordinateReferenceSystem getMapCRS() { + return getDelegate().getMapCRS(); + } + + @Override + public CoordinateReferenceSystem getGeoCRS() { + return getDelegate().getGeoCRS(); + } + + @Override + public MathTransform getImageToMapTransform() { + return getDelegate().getImageToMapTransform(); + } + + @Override + public GeoCoding clone() { + return getDelegate().clone(); + } + + @Override + public boolean canClone() { + return getDelegate().canClone(); + } + + @Override + public boolean isGlobal() { + return true; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/LazyGridBandDataSource.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/LazyGridBandDataSource.java new file mode 100644 index 000000000..e717466a5 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/LazyGridBandDataSource.java @@ -0,0 +1,47 @@ +package eu.esa.snap.cimr.grid; + +import eu.esa.snap.cimr.CimrReaderContext; +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; + + +public class LazyGridBandDataSource implements GridBandDataSource { + + private final CimrReaderContext context; + private final CimrBandDescriptor descriptor; + private final boolean useAverage; + + private volatile GridBandDataSource delegate; + + + public LazyGridBandDataSource(CimrReaderContext context, + CimrBandDescriptor descriptor, + boolean useAverage) { + this.context = context; + this.descriptor = descriptor; + this.useAverage = useAverage; + } + + private GridBandDataSource getDelegate() { + GridBandDataSource local = delegate; + if (local == null) { + synchronized (this) { + local = delegate; + if (local == null) { + local = context.getOrCreateGridForVariable(descriptor, useAverage); + delegate = local; + } + } + } + return local; + } + + @Override + public double getSample(int x, int y) { + return getDelegate().getSample(x, y); + } + + @Override + public void setSample(int x, int y, double value) { + getDelegate().setSample(x, y, value); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/PlateCarreeProjection.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/PlateCarreeProjection.java new file mode 100644 index 000000000..49937cb5a --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/grid/PlateCarreeProjection.java @@ -0,0 +1,101 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.geotools.referencing.CRS; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import java.awt.*; +import java.awt.geom.AffineTransform; + + +public class PlateCarreeProjection implements GridProjection { + + private final int width; + private final int height; + private final double lonMin; + private final double latMax; + private final double deltaLon; + private final double deltaLat; + + + public PlateCarreeProjection(int width, int height, double lonMin, double latMax, double deltaLon, double deltaLat) { + this.width = width; + this.height = height; + this.lonMin = lonMin; + this.latMax = latMax; + this.deltaLon = deltaLon; + this.deltaLat = deltaLat; + } + + @Override + public GeoPos gridToGeoPos(int x, int y) { + if (x < 0 || x >= width || y < 0 || y >= height) { + throw new IllegalArgumentException("Grid index out of range: x=" + x + ", y=" + y); + } + + double lon = lonMin + (x + 0.5) * deltaLon; + double lat = latMax - (y + 0.5) * deltaLat; + + return new GeoPos((float) lat, (float) lon); + } + + @Override + public boolean geoPosToGrid(GeoPos geoPos, Point out) { + double lat = geoPos.getLat(); + double lon = geoPos.getLon(); + + double lonMax = lonMin + width * deltaLon; + double latMin = latMax - height * deltaLat; + + if (lon < lonMin || lon >= lonMax || lat <= latMin || lat > latMax) { + return false; + } + + int x = (int) ((lon - lonMin) / deltaLon); + int y = (int) ((latMax - lat) / deltaLat); + + out.x = x; + out.y = y; + return true; + } + + @Override + public double getLonMin() { + return lonMin; + } + + @Override + public double getLatMax() { + return latMax; + } + + @Override + public double getDeltaLon() { + return deltaLon; + } + + @Override + public double getDeltaLat() { + return deltaLat; + } + + @Override + public CoordinateReferenceSystem getCrs() throws FactoryException { + return CRS.decode("EPSG:4326", true); + } + + @Override + public AffineTransform getAffineTransform(CimrGrid grid) { + double lonMin = grid.getProjection().getLonMin(); + double latMax = grid.getProjection().getLatMax(); + double deltaLon = grid.getProjection().getDeltaLon(); + double deltaLat = grid.getProjection().getDeltaLat(); + + return new AffineTransform( + deltaLon, 0.0, + 0.0, -deltaLat, + lonMin, latMax + ); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NcUtil.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NcUtil.java new file mode 100644 index 000000000..da9ae57c8 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NcUtil.java @@ -0,0 +1,27 @@ +package eu.esa.snap.cimr.netcdf; + +import ucar.nc2.Group; +import ucar.nc2.NetcdfFile; +import ucar.nc2.Variable; + + +public class NcUtil { + + public static Group findGroupOrThrow(NetcdfFile ncFile, String path) { + Group g = ncFile.findGroup(path); + if (g == null) { + throw new IllegalArgumentException("Group not found: " + path); + } + return g; + } + + public static Variable findVarOrThrow(Group group, String name) { + Variable v = group.findVariable(name); + if (v == null) { + throw new IllegalArgumentException( + "Variable '" + name + "' not found in group " + group.getFullName() + ); + } + return v; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrBandFactory.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrBandFactory.java new file mode 100644 index 000000000..30af11907 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrBandFactory.java @@ -0,0 +1,92 @@ +package eu.esa.snap.cimr.netcdf; + +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDescriptorKind; +import eu.esa.snap.cimr.cimr.CimrDimensions; +import eu.esa.snap.cimr.grid.CimrGeometry; +import eu.esa.snap.cimr.grid.CimrGeometryBand; +import ucar.ma2.Array; +import ucar.ma2.Index3D; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.Group; +import ucar.nc2.NetcdfFile; +import ucar.nc2.Variable; + +import java.io.IOException; + + +public class NetcdfCimrBandFactory { + + private final NetcdfFile ncFile; + private final CimrDimensions dimensions; + + + public NetcdfCimrBandFactory(NetcdfFile ncFile, CimrDimensions dimensions) { + this.ncFile = ncFile; + this.dimensions = dimensions; + } + + + public CimrGeometryBand createGeometryBand(CimrBandDescriptor desc, CimrGeometry geometry) throws IOException, InvalidRangeException { + + Group group = NcUtil.findGroupOrThrow(this.ncFile, desc.getGroupPath()); + Variable var = NcUtil.findVarOrThrow(group, desc.getValueVarName()); + + if (var.getRank() != 3) { + throw new IllegalArgumentException("Expected 3D variable for '" + + desc.getValueVarName() + "', but rank=" + var.getRank()); + } + + int nScans = dimensions.get(desc.getDimensions()[0]); + int nSamples = dimensions.get(desc.getDimensions()[1]); + int feedIdx = desc.getFeedIndex(); + + int[] origin = new int[] {0, 0, feedIdx}; + int[] shape = new int[] {nScans, nSamples, 1}; + + final Array data; + synchronized (this.ncFile) { + data = var.read(origin, shape); + } + + double[][] values; + Index3D idx = new Index3D(data.getShape()); + + if (desc.getKind() == CimrDescriptorKind.TIEPOINT_VARIABLE) { + int sampleCount = getSampleCount(desc); + values = new double[nScans][sampleCount]; + + // TODO extract Tiepoint interpolation + for (int s = 0; s < nScans; s++) { + for (int smp = 0; smp < sampleCount; smp++) { + double t = (double) smp * (nSamples - 1) / (double) (sampleCount - 1); + int tp0 = (int) Math.floor(t); + int tp1 = Math.min(tp0 + 1, nSamples - 1); + double f = t - tp0; + + idx.set(s, tp0, 0); + double v0 = data.getDouble(idx); + idx.set(s, tp1, 0); + double v1 = data.getDouble(idx); + + values[s][smp] = v0 + f * (v1 - v0); + } + } + } else { + values = new double[nScans][nSamples]; + + for (int s = 0; s < nScans; s++) { + for (int smp = 0; smp < nSamples; smp++) { + idx.set(s, smp, 0); + values[s][smp] = data.getDouble(idx); + } + } + } + + return new CimrGeometryBand(values, geometry, feedIdx); + } + + private int getSampleCount(CimrBandDescriptor d) { + return dimensions.get("n_samples_" + d.getBand()); + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrFootprintFactory.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrFootprintFactory.java new file mode 100644 index 000000000..89ffdc2e7 --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrFootprintFactory.java @@ -0,0 +1,45 @@ +package eu.esa.snap.cimr.netcdf; + +import eu.esa.snap.cimr.cimr.CimrFootprintShape; +import eu.esa.snap.cimr.grid.CimrGeometryBand; +import org.esa.snap.core.datamodel.GeoPos; + +import java.util.ArrayList; +import java.util.List; + + +public class NetcdfCimrFootprintFactory { + + + public List createFootprintShapes(CimrGeometryBand geometryBand, CimrGeometryBand minorAxisBand, CimrGeometryBand majorAxisBand, CimrGeometryBand angleBand) { + List footprints = new ArrayList<>(); + int scans = geometryBand.getScanCount(); + int samples = geometryBand.getSampleCount(); + + for (int scanIndex = 0; scanIndex < scans; scanIndex++) { + for (int sampleIndex = 0; sampleIndex < samples; sampleIndex++) { + final GeoPos pos = geometryBand.getGeoPos(scanIndex, sampleIndex); + final double angle = angleBand.getValue(scanIndex, sampleIndex); + final double minorAxis = minorAxisBand.getValue(scanIndex, sampleIndex); + final double majorAxis = majorAxisBand.getValue(scanIndex, sampleIndex); + footprints.add( new CimrFootprintShape(pos, angle, minorAxis, majorAxis)); + } + } + + return footprints; + } + + public List getFootprintValues(CimrGeometryBand geometryBand) { + List values = new ArrayList<>(); + int scans = geometryBand.getScanCount(); + int samples = geometryBand.getSampleCount(); + + for (int scanIndex = 0; scanIndex < scans; scanIndex++) { + for (int sampleIndex = 0; sampleIndex < samples; sampleIndex++) { + values.add(geometryBand.getValue(scanIndex, sampleIndex)); + } + } + + return values; + } +} diff --git a/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrGeometryFactory.java b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrGeometryFactory.java new file mode 100644 index 000000000..df0f67d5c --- /dev/null +++ b/cimr-reader/src/main/java/eu/esa/snap/cimr/netcdf/NetcdfCimrGeometryFactory.java @@ -0,0 +1,122 @@ +package eu.esa.snap.cimr.netcdf; + +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDimensions; +import eu.esa.snap.cimr.grid.CimrBoundingBox; +import eu.esa.snap.cimr.grid.CimrTiepointGeometry; +import eu.esa.snap.cimr.grid.CimrGeometry; +import org.esa.snap.core.datamodel.GeoPos; +import ucar.ma2.Array; +import ucar.ma2.Index3D; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.Group; +import ucar.nc2.NetcdfFile; +import ucar.nc2.Variable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class NetcdfCimrGeometryFactory { + + + private final NetcdfFile ncFile; + private final CimrDimensions dimensions; + private final Map geometryByName = new HashMap<>(); + private final Map cache = new HashMap<>(); + + + public NetcdfCimrGeometryFactory(NetcdfFile ncFile, List geometryDescriptors, CimrDimensions dimensions) { + this.ncFile = ncFile; + this.dimensions = dimensions; + for (CimrBandDescriptor d : geometryDescriptors) { + this.geometryByName.put(d.getName(), d); + } + } + + + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor variableDesc) throws IOException, InvalidRangeException { + CimrGeometry geometry = this.cache.get(key(variableDesc)); + if (geometry != null) { + return geometry; + } + + String[] geomNames = variableDesc.getGeometryNames(); + if (geomNames == null || geomNames.length != 2) { + throw new IllegalStateException( + "Descriptor '" + variableDesc.getName() + "' must define exactly two geometryNames (lat, lon)"); + } + + CimrBandDescriptor latDesc = geometryByName.get(geomNames[0]); + CimrBandDescriptor lonDesc = geometryByName.get(geomNames[1]); + + if (latDesc == null || lonDesc == null) { + throw new IllegalStateException("Geometry descriptors not found for variable '" + variableDesc.getName() + "': expected '" + geomNames[0] + "' and '" + geomNames[1] + "'"); + } + + Group latGroup = NcUtil.findGroupOrThrow(this.ncFile, latDesc.getGroupPath()); + Group lonGroup = NcUtil.findGroupOrThrow(this.ncFile, lonDesc.getGroupPath()); + Variable latVar = NcUtil.findVarOrThrow(latGroup, latDesc.getValueVarName()); + Variable lonVar = NcUtil.findVarOrThrow(lonGroup, lonDesc.getValueVarName()); + + int nScans = dimensions.get(latDesc.getDimensions()[0]); + int nTiePoints = dimensions.get(latDesc.getDimensions()[1]); + + int feedIndex = variableDesc.getFeedIndex(); + + int[] origin = {0, 0, feedIndex}; + int[] shape = {nScans, nTiePoints, 1}; + + Array latData; + Array lonData; + synchronized (this.ncFile) { + lonData = lonVar.read(origin, shape); + latData = latVar.read(origin, shape); + } + + + GeoPos[][][] tiePoints = new GeoPos[nScans][nTiePoints][1]; + Index3D idx = new Index3D(new int[]{nScans, nTiePoints, 1}); + + for (int s = 0; s < nScans; s++) { + for (int tp = 0; tp < nTiePoints; tp++) { + idx.set(s, tp, 0); + double lat = latData.getDouble(idx); + double lon = lonData.getDouble(idx); + tiePoints[s][tp][0] = new GeoPos(lat, ensureLongitude(lon)); + } + } + + int sampleCount = getSampleCount(variableDesc); + geometry = new CimrTiepointGeometry(tiePoints, sampleCount); + this.cache.put(key(variableDesc), geometry); + + return geometry; + } + + + private String key(CimrBandDescriptor d) { + return d.getBand().name() + "#" + d.getFeedIndex(); + } + + private int getSampleCount(CimrBandDescriptor d) { + return dimensions.get("n_samples_" + d.getBand()); + } + + public void clearCache() { + this.cache.clear(); + } + + private double ensureLongitude(double lon) { + while (lon > 180.0) lon -= 360.0; + while (lon <= -180.0) lon += 360.0; + return lon; + } + + public CimrBoundingBox getBoundingBox(CimrBandDescriptor variableDesc, double cellSizeDeg) throws InvalidRangeException, IOException { + CimrGeometry bbGeometry = getOrCreateGeometry(variableDesc); + return CimrBoundingBox.create(bbGeometry, cellSizeDeg); + } +} diff --git a/cimr-reader/src/main/nbm/manifest.mf b/cimr-reader/src/main/nbm/manifest.mf new file mode 100644 index 000000000..ac61e99d3 --- /dev/null +++ b/cimr-reader/src/main/nbm/manifest.mf @@ -0,0 +1,7 @@ +Manifest-Version: 1.0 +OpenIDE-Module-Specification-Version: ${microwavetbx.nbmSpecVersion} +OpenIDE-Module-Implementation-Version: ${microwavetbx.nbmImplVersion} +AutoUpdate-Show-In-Client: false +AutoUpdate-Essential-Module: false +OpenIDE-Module-Java-Dependencies: Java > 11 +OpenIDE-Module-Display-Category: SNAP Toolboxes diff --git a/cimr-reader/src/main/resources/META-INF/services/org.esa.snap.core.dataio.ProductReaderPlugIn b/cimr-reader/src/main/resources/META-INF/services/org.esa.snap.core.dataio.ProductReaderPlugIn new file mode 100644 index 000000000..93bfeb723 --- /dev/null +++ b/cimr-reader/src/main/resources/META-INF/services/org.esa.snap.core.dataio.ProductReaderPlugIn @@ -0,0 +1 @@ +eu.esa.snap.cimr.CimrL1BProductReaderPlugin \ No newline at end of file diff --git a/cimr-reader/src/main/resources/eu/esa/snap/cimr/config/cimr-l1b-config.json b/cimr-reader/src/main/resources/eu/esa/snap/cimr/config/cimr-l1b-config.json new file mode 100644 index 000000000..fcff04e0a --- /dev/null +++ b/cimr-reader/src/main/resources/eu/esa/snap/cimr/config/cimr-l1b-config.json @@ -0,0 +1,195 @@ +{ + "variables": [ + { + "name": "C_BAND_raw_bt_h_feed1", + "valueVarName": "raw_bt_h", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)" + }, + { + "name": "C_BAND_raw_bt_v_feed1", + "valueVarName": "raw_bt_v", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in V polarization, from raw counts (no RFI mitigation)" + }, + { + "name": "C_BAND_raw_bt_h_feed2", + "valueVarName": "raw_bt_h", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)" + }, + { + "name": "C_BAND_raw_bt_v_feed2", + "valueVarName": "raw_bt_v", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in V polarization, from raw counts (no RFI mitigation)" + } + ], + "tiepointVariables": [ + { + "name": "C_BAND_altitude_feed1", + "valueVarName": "altitude", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Altitude for intersection of the LOS with the earth surface for the C band Earth views" + }, + { + "name": "C_BAND_footprint_major_axis_feed1", + "valueVarName": "footprint_major_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint major semi-axis value" + }, + { + "name": "C_BAND_footprint_minor_axis_feed1", + "valueVarName": "footprint_minor_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint minor semi-axis value" + }, + { + "name": "C_BAND_geometric_rot_angle_feed1", + "valueVarName": "geometric_rot_angle", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Geometric rotation angle value corresponding to the measured BT value" + }, + { + "name": "C_BAND_footprint_major_axis_feed2", + "valueVarName": "footprint_major_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint major semi-axis value" + }, + { + "name": "C_BAND_footprint_minor_axis_feed2", + "valueVarName": "footprint_minor_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint minor semi-axis value" + }, + { + "name": "C_BAND_geometric_rot_angle_feed2", + "valueVarName": "geometric_rot_angle", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Geometric rotation angle value corresponding to the measured BT value" + } + ], + "geometries": [ + { + "name": "C_BAND_latitude_feed1", + "valueVarName": "latitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Latitude of Earth surface point in the boresight direction for the C band acquisitions" + }, + { + "name": "C_BAND_longitude_feed1", + "valueVarName": "longitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Longitude of Earth surface point in the boresight direction for the C band acquisitions" + }, + { + "name": "C_BAND_latitude_feed2", + "valueVarName": "latitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Latitude of Earth surface point in the boresight direction for the C band acquisitions" + }, + { + "name": "C_BAND_longitude_feed2", + "valueVarName": "longitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Longitude of Earth surface point in the boresight direction for the C band acquisitions" + } + ] +} \ No newline at end of file diff --git a/cimr-reader/src/main/resources/eu/esa/snap/cimr/config/test-config.json b/cimr-reader/src/main/resources/eu/esa/snap/cimr/config/test-config.json new file mode 100644 index 000000000..fcff04e0a --- /dev/null +++ b/cimr-reader/src/main/resources/eu/esa/snap/cimr/config/test-config.json @@ -0,0 +1,195 @@ +{ + "variables": [ + { + "name": "C_BAND_raw_bt_h_feed1", + "valueVarName": "raw_bt_h", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)" + }, + { + "name": "C_BAND_raw_bt_v_feed1", + "valueVarName": "raw_bt_v", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in V polarization, from raw counts (no RFI mitigation)" + }, + { + "name": "C_BAND_raw_bt_h_feed2", + "valueVarName": "raw_bt_h", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)" + }, + { + "name": "C_BAND_raw_bt_v_feed2", + "valueVarName": "raw_bt_v", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "K", + "description": "Brightness temperature of the Earth, in V polarization, from raw counts (no RFI mitigation)" + } + ], + "tiepointVariables": [ + { + "name": "C_BAND_altitude_feed1", + "valueVarName": "altitude", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Altitude for intersection of the LOS with the earth surface for the C band Earth views" + }, + { + "name": "C_BAND_footprint_major_axis_feed1", + "valueVarName": "footprint_major_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint major semi-axis value" + }, + { + "name": "C_BAND_footprint_minor_axis_feed1", + "valueVarName": "footprint_minor_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint minor semi-axis value" + }, + { + "name": "C_BAND_geometric_rot_angle_feed1", + "valueVarName": "geometric_rot_angle", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed1", "C_BAND_longitude_feed1"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Geometric rotation angle value corresponding to the measured BT value" + }, + { + "name": "C_BAND_footprint_major_axis_feed2", + "valueVarName": "footprint_major_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint major semi-axis value" + }, + { + "name": "C_BAND_footprint_minor_axis_feed2", + "valueVarName": "footprint_minor_axis", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "m", + "description": "Elliptical footprint minor semi-axis value" + }, + { + "name": "C_BAND_geometric_rot_angle_feed2", + "valueVarName": "geometric_rot_angle", + "band": "C_BAND", + "geometryNames": ["C_BAND_latitude_feed2", "C_BAND_longitude_feed2"], + "footprintVars" : ["C_BAND_footprint_minor_axis_feed2", "C_BAND_footprint_major_axis_feed2", "C_BAND_geometric_rot_angle_feed2"], + "groupPath": "/Data/Measurement_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Geometric rotation angle value corresponding to the measured BT value" + } + ], + "geometries": [ + { + "name": "C_BAND_latitude_feed1", + "valueVarName": "latitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Latitude of Earth surface point in the boresight direction for the C band acquisitions" + }, + { + "name": "C_BAND_longitude_feed1", + "valueVarName": "longitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 0, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Longitude of Earth surface point in the boresight direction for the C band acquisitions" + }, + { + "name": "C_BAND_latitude_feed2", + "valueVarName": "latitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Latitude of Earth surface point in the boresight direction for the C band acquisitions" + }, + { + "name": "C_BAND_longitude_feed2", + "valueVarName": "longitude", + "band": "C_BAND", + "groupPath": "/Data/Navigation_Data/C_BAND/", + "feedIndex": 1, + "dimensions": ["n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"], + "dataType": "double", + "unit": "deg", + "description": "Longitude of Earth surface point in the boresight direction for the C band acquisitions" + } + ] +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrL1BProductReaderPluginTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrL1BProductReaderPluginTest.java new file mode 100644 index 000000000..56142555a --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrL1BProductReaderPluginTest.java @@ -0,0 +1,95 @@ +package eu.esa.snap.cimr; + +import org.esa.snap.core.dataio.DecodeQualification; +import org.esa.snap.core.dataio.ProductReader; +import org.esa.snap.core.util.io.SnapFileFilter; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.*; + + +public class CimrL1BProductReaderPluginTest { + + private CimrL1BProductReaderPlugin plugIn; + + + @Before + public void setUp() { + plugIn = new CimrL1BProductReaderPlugin(); + } + + + @Test + public void getDecodeQualification_wrongExtension() { + final File file = new File("ignore_the_name.txt"); + + assertEquals(DecodeQualification.UNABLE, plugIn.getDecodeQualification(file)); + } + + @Test + public void getDecodeQualification_correctExtension_wrongFilePattern() { + final File file = new File("ignore_the_name.nc"); + + assertEquals(DecodeQualification.UNABLE, plugIn.getDecodeQualification(file)); + } + + @Test + public void getDecodeQualification_correctExtension_correctFilePattern() { + final File file = new File("W_PT-DME-Lisbon-SAT-CIMR-1B_C_DME_20230420T103323_LD_20280110T114800_20280110T115700_TN.nc"); + final File file2 = new File("W_xx-esa-Lisbon-SAT-CIMR-1B_C_DME_20251029T000420_G_20280105T121500_20280105T121600_002.nc"); + + assertEquals(DecodeQualification.INTENDED, plugIn.getDecodeQualification(file)); + assertEquals(DecodeQualification.INTENDED, plugIn.getDecodeQualification(file2)); + } + + + @Test + public void getInputTypes() { + final Class[] inputTypes = plugIn.getInputTypes(); + assertEquals(2, inputTypes.length); + assertEquals(File.class, inputTypes[0]); + assertEquals(String.class, inputTypes[1]); + } + + @Test + public void createReaderInstance() { + final ProductReader readerInstance = plugIn.createReaderInstance(); + + assertNotNull(readerInstance); + assertTrue(readerInstance instanceof CimrL1BProductReader); + } + + @Test + public void getFormatNames() { + final String[] formatNames = plugIn.getFormatNames(); + + assertEquals(1, formatNames.length); + assertEquals("CIMR-L1B", formatNames[0]); + } + + @Test + public void getDefaultFileExtensions() { + final String[] extensions = plugIn.getDefaultFileExtensions(); + + assertEquals(1, extensions.length); + assertEquals(".nc", extensions[0]); + } + + @Test + public void getDescription() { + final String description = plugIn.getDescription(null); + assertEquals("CIMR Level 1B Data Products in NetCDF Format", description); + } + + @Test + public void getProductFileFilter() { + final SnapFileFilter productFileFilter = plugIn.getProductFileFilter(); + assertArrayEquals(plugIn.getDefaultFileExtensions(), productFileFilter.getExtensions()); + assertEquals(plugIn.getFormatNames()[0], productFileFilter.getFormatName()); + + assertEquals("CIMR Level 1B Data Products in NetCDF Format (*.nc)", productFileFilter.getDescription()); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrL1BProductReaderTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrL1BProductReaderTest.java new file mode 100644 index 000000000..3390dc772 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrL1BProductReaderTest.java @@ -0,0 +1,128 @@ +package eu.esa.snap.cimr; + +import com.bc.ceres.core.ProgressMonitor; +import com.bc.ceres.multilevel.MultiLevelImage; +import com.bc.ceres.multilevel.MultiLevelSource; +import com.bc.ceres.multilevel.support.DefaultMultiLevelImage; +import com.bc.ceres.multilevel.support.DefaultMultiLevelSource; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.ProductData; +import org.junit.Test; +import ucar.nc2.NetcdfFile; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.reflect.Field; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + + +public class CimrL1BProductReaderTest { + + + @Test + public void testReadBandRasterDataImplCopiesFromSourceImage() throws Exception { + int width = 4; + int height = 3; + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + int value = 1; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + image.setRGB(x, y, value++); + } + } + + MultiLevelSource source = new DefaultMultiLevelSource(image, 1); + MultiLevelImage multiLevelImage = new DefaultMultiLevelImage(source); + + Band band = new TestBand(multiLevelImage); + CimrL1BProductReader reader = new CimrL1BProductReader( null); + + int destOffsetX = 1; + int destOffsetY = 1; + int destWidth = 2; + int destHeight = 2; + + ProductData destBuffer = ProductData.createInstance(ProductData.TYPE_INT32, + destWidth * destHeight); + + reader.readBandRasterDataImpl( + destOffsetX, destOffsetY, + destWidth, destHeight, + 1, 1, + band, + destOffsetX, destOffsetY, + destWidth, destHeight, + destBuffer, + ProgressMonitor.NULL + ); + + int[] expected = new int[destWidth * destHeight]; + int idx = 0; + for (int y = destOffsetY; y < destOffsetY + destHeight; y++) { + for (int x = destOffsetX; x < destOffsetX + destWidth; x++) { + expected[idx++] = image.getRGB(x, y); + } + } + + assertArrayEquals(expected, (int[]) destBuffer.getElems()); + } + + @Test + public void close_closesNcFileAndClearsContextAndNullsFields() throws IOException, NoSuchFieldException, IllegalAccessException { + CimrL1BProductReader reader = new CimrL1BProductReader(null); + + NetcdfFile ncFile = mock(NetcdfFile.class); + CimrReaderContext ctx = mock(CimrReaderContext.class); + + setField(reader, "ncFile", ncFile); + setField(reader, "readerContext", ctx); + + assertNotNull(getField(reader, "ncFile")); + assertNotNull(getField(reader, "readerContext")); + + reader.close(); + + verify(ncFile).close(); + verify(ctx).clearCache(); + assertNull(getField(reader, "ncFile")); + assertNull(getField(reader, "readerContext")); + } + + @Test + public void close_NullFields() throws IOException { + CimrL1BProductReader reader = new CimrL1BProductReader(null); + + reader.close(); + reader.close(); + } + + + + private static class TestBand extends Band { + private final MultiLevelImage sourceImage; + + TestBand(MultiLevelImage sourceImage) { + super("test_band", ProductData.TYPE_INT32, sourceImage.getWidth(), sourceImage.getHeight()); + this.sourceImage = sourceImage; + } + + @Override + public MultiLevelImage getSourceImage() { + return sourceImage; + } + } + + private static void setField(Object target, String name, Object value) throws NoSuchFieldException, IllegalAccessException { + Field f = target.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(target, value); + } + + private static Object getField(Object target, String name) throws NoSuchFieldException, IllegalAccessException { + Field f = target.getClass().getDeclaredField(name); + f.setAccessible(true); + return f.get(target); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrReaderContextFootprintTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrReaderContextFootprintTest.java new file mode 100644 index 000000000..ec39ea4cc --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrReaderContextFootprintTest.java @@ -0,0 +1,191 @@ +package eu.esa.snap.cimr; + +import eu.esa.snap.cimr.cimr.*; +import eu.esa.snap.cimr.grid.CimrGeometry; +import eu.esa.snap.cimr.grid.CimrGeometryBand; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.netcdf.NetcdfCimrBandFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrFootprintFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrGeometryFactory; +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.NetcdfFile; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + + +@RunWith(MockitoJUnitRunner.class) +public class CimrReaderContextFootprintTest { + + @Mock + private NetcdfFile ncFile; + + @Mock + private CimrDescriptorSet descriptorSet; + + @Mock + private CimrGrid cimrGrid; + + @Mock + private NetcdfCimrGeometryFactory geometryFactory; + + @Mock + private NetcdfCimrBandFactory bandFactory; + + @Mock + private NetcdfCimrFootprintFactory footprintFactory; + + @Mock + private CimrBandDescriptor mainDesc; + + @Mock + private CimrBandDescriptor minorDesc; + + @Mock + private CimrBandDescriptor majorDesc; + + @Mock + private CimrBandDescriptor angleDesc; + + @Mock + private CimrGeometry mainGeom; + + @Mock + private CimrGeometry minorGeom; + + @Mock + private CimrGeometry majorGeom; + + @Mock + private CimrGeometry angleGeom; + + @Mock + private CimrGeometryBand mainGeomBand; + + @Mock + private CimrGeometryBand minorGeomBand; + + @Mock + private CimrGeometryBand majorGeomBand; + + @Mock + private CimrGeometryBand angleGeomBand; + + private CimrReaderContext context; + + @Before + public void setUp() throws Exception { + context = new CimrReaderContext(ncFile, descriptorSet, cimrGrid, geometryFactory, bandFactory); + + Field ff = CimrReaderContext.class.getDeclaredField("footprintFactory"); + ff.setAccessible(true); + ff.set(context, footprintFactory); + + when(mainDesc.getFootprintVars()).thenReturn(new String[]{ + "FOOT_MINOR", + "FOOT_MAJOR", + "FOOT_ANGLE" + }); + when(mainDesc.getBand()).thenReturn(CimrFrequencyBand.C_BAND); + when(mainDesc.getFeedIndex()).thenReturn(0); + + when(descriptorSet.getTpVariableByName("FOOT_MINOR")).thenReturn(minorDesc); + when(descriptorSet.getTpVariableByName("FOOT_MAJOR")).thenReturn(majorDesc); + when(descriptorSet.getTpVariableByName("FOOT_ANGLE")).thenReturn(angleDesc); + + when(minorDesc.getBand()).thenReturn(CimrFrequencyBand.C_BAND); + when(minorDesc.getFeedIndex()).thenReturn(1); + when(minorDesc.getValueVarName()).thenReturn("FOOT_MINOR"); + + when(majorDesc.getBand()).thenReturn(CimrFrequencyBand.C_BAND); + when(majorDesc.getFeedIndex()).thenReturn(1); + when(majorDesc.getValueVarName()).thenReturn("FOOT_MAJOR"); + + when(angleDesc.getBand()).thenReturn(CimrFrequencyBand.C_BAND); + when(angleDesc.getFeedIndex()).thenReturn(1); + when(angleDesc.getValueVarName()).thenReturn("FOOT_ANGLE"); + + when(geometryFactory.getOrCreateGeometry(mainDesc)).thenReturn(mainGeom); + when(geometryFactory.getOrCreateGeometry(minorDesc)).thenReturn(minorGeom); + when(geometryFactory.getOrCreateGeometry(majorDesc)).thenReturn(majorGeom); + when(geometryFactory.getOrCreateGeometry(angleDesc)).thenReturn(angleGeom); + + when(bandFactory.createGeometryBand(eq(mainDesc), eq(mainGeom))).thenReturn(mainGeomBand); + when(bandFactory.createGeometryBand(eq(minorDesc), eq(minorGeom))).thenReturn(minorGeomBand); + when(bandFactory.createGeometryBand(eq(majorDesc), eq(majorGeom))).thenReturn(majorGeomBand); + when(bandFactory.createGeometryBand(eq(angleDesc), eq(angleGeom))).thenReturn(angleGeomBand); + } + + @Test + public void testGetOrCreateFootprints_createsFromDependenciesAndCaches() throws InvalidRangeException, IOException { + CimrFootprintShape dummyFp = new CimrFootprintShape(new GeoPos(10.f, 20.f), 45.0, 1000.0, 2000.0); + List expectedList = Collections.singletonList(dummyFp); + + when(footprintFactory.createFootprintShapes(mainGeomBand, minorGeomBand, majorGeomBand, angleGeomBand)) + .thenReturn(expectedList); + + CimrFootprints first = context.getOrCreateFootprints(mainDesc); + + assertSame(expectedList, first.getShapes()); + + verify(descriptorSet).getTpVariableByName("FOOT_MINOR"); + verify(descriptorSet).getTpVariableByName("FOOT_MAJOR"); + verify(descriptorSet).getTpVariableByName("FOOT_ANGLE"); + + verify(geometryFactory).getOrCreateGeometry(mainDesc); + verify(geometryFactory).getOrCreateGeometry(minorDesc); + verify(geometryFactory).getOrCreateGeometry(majorDesc); + verify(geometryFactory).getOrCreateGeometry(angleDesc); + + verify(bandFactory).createGeometryBand(mainDesc, mainGeom); + verify(bandFactory).createGeometryBand(minorDesc, minorGeom); + verify(bandFactory).createGeometryBand(majorDesc, majorGeom); + verify(bandFactory).createGeometryBand(angleDesc, angleGeom); + + verify(footprintFactory, times(1)) + .createFootprintShapes(mainGeomBand, minorGeomBand, majorGeomBand, angleGeomBand); + + CimrFootprints second = context.getOrCreateFootprints(mainDesc); + + assertSame(first.getShapes(), second.getShapes()); + assertNotSame(first, second); + verify(footprintFactory, times(1)).createFootprintShapes(mainGeomBand, minorGeomBand, majorGeomBand, angleGeomBand); + verify(footprintFactory, times(2)).getFootprintValues(mainGeomBand); + + assertNotSame(first.getValues(), second.getValues()); + } + + @Test + public void testGetOrCreateFootprints_usesFootprintKeyBasedOnBandAndFeed() { + CimrBandDescriptor otherDesc = mock(CimrBandDescriptor.class); + + when(otherDesc.getBand()).thenReturn(CimrFrequencyBand.C_BAND); + when(otherDesc.getFeedIndex()).thenReturn(0); + + CimrFootprintShape fp = new CimrFootprintShape(new GeoPos(0.f, 0.f), 0.0, 500.0, 1000.0); + List expected = Collections.singletonList(fp); + + when(footprintFactory.createFootprintShapes( + mainGeomBand, minorGeomBand, majorGeomBand, angleGeomBand)) + .thenReturn(expected); + + List list1 = context.getOrCreateFootprints(mainDesc).getShapes(); + List list2 = context.getOrCreateFootprints(otherDesc).getShapes(); + + assertSame(list1, list2); + + verify(footprintFactory, times(1)).createFootprintShapes(mainGeomBand, minorGeomBand, majorGeomBand, angleGeomBand); + } +} diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrReaderContextTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrReaderContextTest.java new file mode 100644 index 000000000..babec2ecb --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/CimrReaderContextTest.java @@ -0,0 +1,330 @@ +package eu.esa.snap.cimr; + +import eu.esa.snap.cimr.cimr.*; +import eu.esa.snap.cimr.grid.*; +import eu.esa.snap.cimr.netcdf.NetcdfCimrBandFactory; +import eu.esa.snap.cimr.netcdf.NetcdfCimrGeometryFactory; +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; +import ucar.nc2.NetcdfFile; + +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; + + +public class CimrReaderContextTest { + + private static final double doubleErr = 1e-6; + + + @Test + public void testConstructorAndGetters() { + CimrGrid grid = createTestGrid(); + CimrDescriptorSet descriptorSet = createEmptyDescriptorSet(); + + NetcdfCimrGeometryFactory geomFactory = new NetcdfCimrGeometryFactory(null, Collections.emptyList(), null); + NetcdfCimrBandFactory bandFactory = new NetcdfCimrBandFactory(null, null); + + CimrReaderContext ctx = new CimrReaderContext( + null, + descriptorSet, + grid, + geomFactory, + bandFactory + ); + + assertSame(grid, ctx.getGlobalGrid()); + assertSame(descriptorSet, ctx.getDescriptorSet()); + } + + @Test + public void testGetOrCreateGeometry_DelegatesAndWrapsCheckedException() { + CimrGrid grid = createTestGrid(); + CimrDescriptorSet descriptorSet = createEmptyDescriptorSet(); + CimrBandDescriptor desc = createTestDescriptor(); + + NetcdfCimrGeometryFactory geomFactory = new NetcdfCimrGeometryFactory(null, Collections.emptyList(), null) { + @Override + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor variableDesc) + throws IOException { + throw new IOException("boom-geo"); + } + }; + NetcdfCimrBandFactory bandFactory = new NetcdfCimrBandFactory(null, null); + CimrReaderContext ctx = new CimrReaderContext( + null, + descriptorSet, + grid, + geomFactory, + bandFactory + ); + + try { + ctx.getOrCreateGeometry(desc); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("Failed to build geometry for variable testVar")); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof IOException); + assertEquals("boom-geo", e.getCause().getMessage()); + } + } + + @Test + public void testGetOrCreateGridForVariable() { + CimrGrid grid = createTestGrid(); + CimrDescriptorSet descriptorSet = createEmptyDescriptorSet(); + CimrBandDescriptor desc = createTestDescriptor(); + + CimrGeometry stubGeom = createStubGeometry(); + + AtomicInteger geomCalls = new AtomicInteger(); + AtomicInteger bandCalls = new AtomicInteger(); + + NetcdfCimrGeometryFactory geomFactory = new NetcdfCimrGeometryFactory(null, Collections.emptyList(), null) { + @Override + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor variableDesc){ + geomCalls.incrementAndGet(); + return stubGeom; + } + }; + + NetcdfCimrBandFactory bandFactory = new NetcdfCimrBandFactory(null, null) { + @Override + public CimrGeometryBand createGeometryBand(CimrBandDescriptor desc, CimrGeometry geometry) { + bandCalls.incrementAndGet(); + double[][] values = {{42.0, 42.0}}; + return new CimrGeometryBand(values, geometry, desc.getFeedIndex()); + } + }; + + CimrReaderContext ctx = new CimrReaderContext( + null, + descriptorSet, + grid, + geomFactory, + bandFactory + ); + + GridBandDataSource grid1 = ctx.getOrCreateGridForVariable(desc, true); + GridBandDataSource grid2 = ctx.getOrCreateGridForVariable(desc, false); + + assertNotSame(grid1, grid2); + + assertEquals(1, geomCalls.get()); + assertEquals(1, bandCalls.get()); + + assertEquals(42.0, grid1.getSample(0, 0), doubleErr); + } + + @Test + public void testGetOrCreateGridForVariable_WrapsBandFactoryCheckedException() { + CimrGrid grid = createTestGrid(); + CimrDescriptorSet descriptorSet = createEmptyDescriptorSet(); + CimrBandDescriptor desc = createTestDescriptor(); + + CimrGeometry stubGeom = createStubGeometry(); + + NetcdfCimrGeometryFactory geomFactory = new NetcdfCimrGeometryFactory(null, Collections.emptyList(), null) { + @Override + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor variableDesc) { + return stubGeom; + } + }; + + NetcdfCimrBandFactory bandFactory = new NetcdfCimrBandFactory(null, null) { + @Override + public CimrGeometryBand createGeometryBand(CimrBandDescriptor desc, CimrGeometry geometry) + throws IOException { + throw new IOException("boom-band"); + } + }; + CimrReaderContext ctx = new CimrReaderContext( + null, + descriptorSet, + grid, + geomFactory, + bandFactory + ); + + try { + ctx.getOrCreateGridForVariable(desc, true); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("Failed to build geometry band for variable testVar")); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof IOException); + assertEquals("boom-band", e.getCause().getMessage()); + } + } + + @Test + public void testGetOrCreateGridForVariable_PropagatesGeometryRuntimeException() { + CimrGrid grid = createTestGrid(); + CimrDescriptorSet descriptorSet = createEmptyDescriptorSet(); + CimrBandDescriptor desc = createTestDescriptor(); + + NetcdfCimrGeometryFactory geomFactory = new NetcdfCimrGeometryFactory(null, Collections.emptyList(), null) { + @Override + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor variableDesc) + throws IOException { + throw new IOException("boom-geo"); + } + }; + NetcdfCimrBandFactory bandFactory = new NetcdfCimrBandFactory(null, null); + CimrReaderContext ctx = new CimrReaderContext( + null, + descriptorSet, + grid, + geomFactory, + bandFactory + ); + + RuntimeException geoEx; + try { + ctx.getOrCreateGeometry(desc); + fail("Expected RuntimeException from getOrCreateGeometry"); + return; + } catch (RuntimeException e) { + geoEx = e; + assertTrue(e.getMessage().contains("Failed to build geometry for variable testVar")); + assertTrue(e.getCause() instanceof IOException); + } + + try { + ctx.getOrCreateGridForVariable(desc, true); + fail("Expected RuntimeException from getOrCreateGridForVariable"); + } catch (RuntimeException e) { + assertEquals(geoEx.getMessage(), e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof IOException); + } + } + + @Test + public void testClearCache_clearsBandCacheAndGeometryCache() { + PlateCarreeProjection proj = new PlateCarreeProjection( + 2, 1, + 0.0, 0.0, + 1.0, 1.0 + ); + CimrGrid grid = new CimrGrid(proj, 2, 1); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "testVar", "v", CimrFrequencyBand.C_BAND, + new String[]{"lat", "lon"}, new String[] {""}, + "/Data", 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrDescriptorSet descriptorSet = new CimrDescriptorSet( + Collections.singletonList(varDesc), + Collections.emptyList(), + Collections.emptyList() + ); + + NetcdfFile ncFile = NetcdfFile.builder().setLocation("dummy").build(); + CimrDimensions dims = new CimrDimensions(Collections.emptyMap()); + + class CountingGeometryFactory extends NetcdfCimrGeometryFactory { + int getCalls = 0; + int clearCalls = 0; + + CountingGeometryFactory() { + super(ncFile, Collections.emptyList(), dims); + } + + @Override + public CimrGeometry getOrCreateGeometry(CimrBandDescriptor d) { + getCalls++; + return createStubGeometry(); + } + + @Override + public void clearCache() { + clearCalls++; + super.clearCache(); + } + } + + class CountingBandFactory extends NetcdfCimrBandFactory { + int calls = 0; + + CountingBandFactory() { + super(ncFile, dims); + } + + @Override + public CimrGeometryBand createGeometryBand(CimrBandDescriptor desc, CimrGeometry geometry) { + calls++; + double[][] values = {{1.0, 2.0}}; + return new CimrGeometryBand(values, geometry, desc.getFeedIndex()); + } + } + + CountingGeometryFactory geomFactory = new CountingGeometryFactory(); + CountingBandFactory bandFactory = new CountingBandFactory(); + + CimrReaderContext ctx = new CimrReaderContext(ncFile, descriptorSet, grid, geomFactory, bandFactory); + + GridBandDataSource ds1 = ctx.getOrCreateGridForVariable(varDesc, true); + GridBandDataSource ds2 = ctx.getOrCreateGridForVariable(varDesc, true); + + assertNotSame(ds1, ds2); + assertEquals(1, geomFactory.getCalls); + assertEquals(1, bandFactory.calls); + + ctx.clearCache(); + assertEquals(1, geomFactory.clearCalls); + + ctx.getOrCreateGridForVariable(varDesc, true); + assertEquals(2, geomFactory.getCalls); + assertEquals(2, bandFactory.calls); + } + + + + private CimrGrid createTestGrid() { + PlateCarreeProjection proj = new PlateCarreeProjection( + 1, 1, + -0.5, 0.5, + 1.0, 1.0 + ); + return new CimrGrid(proj, 1, 1); + } + + private CimrBandDescriptor createTestDescriptor() { + return new CimrBandDescriptor( + "testVar", + "testVar", + CimrFrequencyBand.C_BAND, + new String[]{"lat", "lon"}, + new String[] {""}, + "/dummy", + 0, + CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", + "", + "" + ); + } + + private CimrDescriptorSet createEmptyDescriptorSet() { + return new CimrDescriptorSet( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + } + + private CimrGeometry createStubGeometry() { + GeoPos[][][] tp = new GeoPos[1][2][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + tp[0][1][0] = new GeoPos(0f, 1f); + return new CimrTiepointGeometry(tp, 2); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrDescriptorSetTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrDescriptorSetTest.java new file mode 100644 index 000000000..e58094e39 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrDescriptorSetTest.java @@ -0,0 +1,167 @@ +package eu.esa.snap.cimr.cimr; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + + +public class CimrDescriptorSetTest { + + + @Test + public void getGeometryByName_returnsDescriptorWhenPresent() { + CimrBandDescriptor geom1 = descriptor("LAT", CimrDescriptorKind.GEOMETRY); + CimrBandDescriptor geom2 = descriptor("LON", CimrDescriptorKind.GEOMETRY); + + List measurements = Collections.emptyList(); + List geometries = Arrays.asList(geom1, geom2); + List tiepoints = Collections.emptyList(); + + CimrDescriptorSet set = new CimrDescriptorSet(measurements, geometries, tiepoints); + + CimrBandDescriptor result = set.getGeometryByName("LON"); + + assertSame("Expected to get the matching geometry descriptor", geom2, result); + } + + @Test + public void getGeometryByName_returnsNullWhenNameNotFound() { + CimrBandDescriptor geom1 = descriptor("LAT", CimrDescriptorKind.GEOMETRY); + + CimrDescriptorSet set = new CimrDescriptorSet( + Collections.emptyList(), + Collections.singletonList(geom1), + Collections.emptyList() + ); + + CimrBandDescriptor result = set.getGeometryByName("LON"); + + assertNull("Expected null when geometry name is not found", result); + } + + @Test + public void getGeometryByName_returnsNullWhenNoGeometries() { + CimrDescriptorSet set = new CimrDescriptorSet( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + + CimrBandDescriptor result = set.getGeometryByName("ANY"); + + assertNull("Expected null when geometries list is empty", result); + } + + @Test + public void getters_returnListsPassedToConstructor() { + List measurements = Collections.singletonList(descriptor("MEAS", CimrDescriptorKind.VARIABLE)); + List geometries = Collections.singletonList(descriptor("GEOM",CimrDescriptorKind.GEOMETRY)); + List tiepoints = Collections.singletonList(descriptor("TP",CimrDescriptorKind.TIEPOINT_VARIABLE)); + + CimrDescriptorSet set = new CimrDescriptorSet(measurements, geometries, tiepoints); + + assertSame(measurements, set.getMeasurements()); + assertSame(geometries, set.getGeometries()); + assertSame(tiepoints, set.getTiepointVariables()); + } + + @Test + public void getGeometryByName_returnsFirstMatchWhenMultipleWithSameName() { + CimrBandDescriptor geom1 = descriptor("LAT", CimrDescriptorKind.GEOMETRY); + CimrBandDescriptor geom2 = descriptor("LAT", CimrDescriptorKind.GEOMETRY); + + CimrDescriptorSet set = new CimrDescriptorSet( + Collections.emptyList(), + Arrays.asList(geom1, geom2), + Collections.emptyList() + ); + + CimrBandDescriptor result = set.getGeometryByName("LAT"); + + assertSame("Expected first matching descriptor to be returned", geom1, result); + } + + @Test + public void getMeasurementByName_returnsDescriptorWhenPresent() { + CimrBandDescriptor var1 = descriptor("LAT", CimrDescriptorKind.VARIABLE); + CimrBandDescriptor var2 = descriptor("LON", CimrDescriptorKind.VARIABLE); + + List geometries = Collections.emptyList(); + List measurements = Arrays.asList(var1, var2); + List tiepoints = Collections.emptyList(); + + CimrDescriptorSet set = new CimrDescriptorSet(measurements, geometries, tiepoints); + + CimrBandDescriptor result = set.getMeasurementByName("LON"); + + assertSame("Expected to get the matching geometry descriptor", var2, result); + } + + @Test + public void getMeasurementByName_returnsNull() { + CimrBandDescriptor var1 = descriptor("LAT", CimrDescriptorKind.VARIABLE); + + List geometries = Collections.emptyList(); + List measurements = Arrays.asList(var1); + List tiepoints = Collections.emptyList(); + + CimrDescriptorSet set = new CimrDescriptorSet(measurements, geometries, tiepoints); + + CimrBandDescriptor result = set.getMeasurementByName("LON"); + + assertNull(result); + } + + @Test + public void getTPByName_returnsDescriptorWhenPresent() { + CimrBandDescriptor tp1 = descriptor("LAT", CimrDescriptorKind.TIEPOINT_VARIABLE); + CimrBandDescriptor tp2 = descriptor("LON", CimrDescriptorKind.TIEPOINT_VARIABLE); + + List geometries = Collections.emptyList(); + List tiepoints = Arrays.asList(tp1, tp2); + List measurements = Collections.emptyList(); + + CimrDescriptorSet set = new CimrDescriptorSet(measurements, geometries, tiepoints); + + CimrBandDescriptor result = set.getTpVariableByName("LON"); + + assertSame("Expected to get the matching geometry descriptor", tp2, result); + } + + @Test + public void getTPByName_returnsNull() { + CimrBandDescriptor tp1 = descriptor("LAT", CimrDescriptorKind.TIEPOINT_VARIABLE); + + List geometries = Collections.emptyList(); + List tiepoints = Arrays.asList(tp1); + List measurements = Collections.emptyList(); + + CimrDescriptorSet set = new CimrDescriptorSet(measurements, geometries, tiepoints); + + CimrBandDescriptor result = set.getTpVariableByName("LON"); + + assertNull(result); + } + + + private static CimrBandDescriptor descriptor(String name, CimrDescriptorKind kind) { + return new CimrBandDescriptor( + name, + "C_BAND_bt", + CimrFrequencyBand.C_BAND, + new String[] {"C_BAND_latitude", "C_BAND_longitude"}, + new String[] {""}, + "/dummy/group", + 0, + kind, + new String[]{"n_scans", "n_samples_C_BAND"}, + "double", + "", + "" + ); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrDimensionsTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrDimensionsTest.java new file mode 100644 index 000000000..46e3cf8bb --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrDimensionsTest.java @@ -0,0 +1,64 @@ +package eu.esa.snap.cimr.cimr; + +import org.junit.Test; +import ucar.nc2.Dimension; +import ucar.nc2.NetcdfFile; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + + +public class CimrDimensionsTest { + + + @Test + public void testFromCollectsAllDimensions() { + NetcdfFile ncFile = new NetcdfFile() { + @Override + public List getDimensions() { + List dims = new ArrayList<>(); + dims.add(new Dimension("n_scans", 100)); + dims.add(new Dimension("n_samples_C_BAND", 200)); + dims.add(new Dimension("n_feeds_C_BAND", 3)); + return dims; + } + }; + + CimrDimensions dims = CimrDimensions.from(ncFile); + assertNotNull(dims); + + assertEquals(100, dims.get("n_scans")); + assertEquals(200, dims.get("n_samples_C_BAND")); + assertEquals(3, dims.get("n_feeds_C_BAND")); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetThrowsOnUnknownDimension() { + NetcdfFile ncFile = new NetcdfFile() { + @Override + public List getDimensions() { + List dims = new ArrayList<>(); + dims.add(new Dimension("n_scans", 100)); + return dims; + } + }; + + CimrDimensions dims = CimrDimensions.from(ncFile); + dims.get("does_not_exist"); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetThrowsWhenNoDimensionsPresent() { + NetcdfFile ncFile = new NetcdfFile() { + @Override + public List getDimensions() { + return new ArrayList<>(); + } + }; + + CimrDimensions dims = CimrDimensions.from(ncFile); + dims.get("n_scans"); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrFootprintShapeTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrFootprintShapeTest.java new file mode 100644 index 000000000..e89096e23 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrFootprintShapeTest.java @@ -0,0 +1,60 @@ +package eu.esa.snap.cimr.cimr; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrFootprintShapeTest { + + private static final double doubleErr = 1e-9; + + @Test + public void testConstructorAndGetters() { + GeoPos geoPos = new GeoPos(24.5f, 280.1f); + double angle = 123.4; + double minor = 2500.0; + double major = 5000.0; + + CimrFootprintShape fp = new CimrFootprintShape(geoPos, angle, minor, major); + + assertSame(geoPos, fp.getGeoPos()); + assertEquals(angle, fp.getAngle(), doubleErr); + } + + @Test + public void testMinorAxisToDegree() { + GeoPos geoPos = new GeoPos(0.0f, 0.0f); + CimrFootprintShape fp = new CimrFootprintShape(geoPos, 0.0, 111_320.0, 0.0); + + assertEquals(1.0, fp.getMinorAxisDegree(), doubleErr); + + CimrFootprintShape fpHalf = new CimrFootprintShape(geoPos, 0.0, 55_660.0, 0.0); + assertEquals(0.5, fpHalf.getMinorAxisDegree(), doubleErr); + } + + @Test + public void testMajorAxisToDegreeAtEquator() { + GeoPos geoPos = new GeoPos(0.0f, 10.0f); + CimrFootprintShape fp = new CimrFootprintShape(geoPos, 0.0, 0.0, 111_320.0); + + assertEquals(1.0, fp.getMajorAxisDegree(), doubleErr); + } + + @Test + public void testMajorAxisToDegreeAtMidLatitude() { + GeoPos geoPos = new GeoPos(60.0f, 10.0f); + CimrFootprintShape fp = new CimrFootprintShape(geoPos, 0.0, 0.0, 55_660.0); + + assertEquals(1.0, fp.getMajorAxisDegree(), doubleErr); + } + + @Test + public void testMajorAxisToDegreeAtNegativeLatitude() { + GeoPos geoPos = new GeoPos(-60.0f, 10.0f); + CimrFootprintShape fp = new CimrFootprintShape(geoPos, 0.0, 0.0, 55_660.0); + + assertEquals(1.0, fp.getMajorAxisDegree(), doubleErr); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrFrequencyBandTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrFrequencyBandTest.java new file mode 100644 index 000000000..5015cfbe5 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrFrequencyBandTest.java @@ -0,0 +1,25 @@ +package eu.esa.snap.cimr.cimr; + +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrFrequencyBandTest { + + + @Test + public void testSpectralWaveLengths() { + float sw_L_BAND = CimrFrequencyBand.L_BAND.getSpectralWaveLength(); + float sw_C_BAND = CimrFrequencyBand.C_BAND.getSpectralWaveLength(); + float sw_X_BAND = CimrFrequencyBand.X_BAND.getSpectralWaveLength(); + float sw_KU_BAND = CimrFrequencyBand.KU_BAND.getSpectralWaveLength(); + float sw_KA_BAND = CimrFrequencyBand.KA_BAND.getSpectralWaveLength(); + + assertEquals(212000000f, sw_L_BAND, 0.1); + assertEquals(43300000f, sw_C_BAND, 0.1); + assertEquals(28200000f, sw_X_BAND, 0.1); + assertEquals(16000000f, sw_KU_BAND, 0.1); + assertEquals(8220000f, sw_KA_BAND, 0.1); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridBuilderTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridBuilderTest.java new file mode 100644 index 000000000..c4eddec5a --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridBuilderTest.java @@ -0,0 +1,65 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.grid.*; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrGridBuilderTest { + + + @Test + public void build_whenUseAverageTrue_usesMapAverage() { + RecordingMapper mapper = new RecordingMapper(); + CimrGridBuilder builder = new CimrGridBuilder(mapper); + CimrGrid grid = CimrGridFactory.createGlobalPlateCarree(10.0); + + CimrGridBandDataSource result = builder.build(null, grid, true); + + assertTrue(mapper.mapAverageCalled); + assertFalse(mapper.mapNearestCalled); + assertSame(grid, mapper.lastGrid); + assertSame(result, mapper.lastTarget); + assertNull(mapper.lastBand); + } + + @Test + public void build_whenUseAverageFalse_usesMapNearest() { + RecordingMapper mapper = new RecordingMapper(); + CimrGridBuilder builder = new CimrGridBuilder(mapper); + CimrGrid grid = CimrGridFactory.createGlobalPlateCarree(10.0); + + CimrGridBandDataSource result = builder.build(null, grid, false); + + assertFalse(mapper.mapAverageCalled); + assertTrue(mapper.mapNearestCalled); + assertSame(grid, mapper.lastGrid); + assertSame(result, mapper.lastTarget); + assertNull(mapper.lastBand); + } + + private static class RecordingMapper extends GeometryBandToGridMapper { + boolean mapAverageCalled; + boolean mapNearestCalled; + CimrBand lastBand; + CimrGrid lastGrid; + GridBandDataSource lastTarget; + + @Override + public void mapAverage(CimrBand band, CimrGrid grid, GridBandDataSource target) { + mapAverageCalled = true; + lastBand = band; + lastGrid = grid; + lastTarget = target; + } + + @Override + public void mapNearest(CimrBand band, CimrGrid grid, GridBandDataSource target) { + mapNearestCalled = true; + lastBand = band; + lastGrid = grid; + lastTarget = target; + } + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridMultiLevelSourceTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridMultiLevelSourceTest.java new file mode 100644 index 000000000..4faa5518a --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridMultiLevelSourceTest.java @@ -0,0 +1,106 @@ +package eu.esa.snap.cimr.cimr; + + +import com.bc.ceres.multilevel.MultiLevelModel; +import com.bc.ceres.multilevel.support.DefaultMultiLevelImage; +import com.bc.ceres.multilevel.support.DefaultMultiLevelModel; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.GridBandDataSource; +import eu.esa.snap.cimr.grid.PlateCarreeProjection; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.ProductData; +import org.junit.Test; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; + +import static org.junit.Assert.*; + + +public class CimrGridMultiLevelSourceTest { + + private static final double doubleErr = 1e-6; + + + @Test + public void testLevel0ImageMatchesGridValues() { + int width = 2; + int height = 2; + Band band = new Band("test", ProductData.TYPE_FLOAT64, width, height); + + GridBandDataSource grid = new GridBandDataSource() { + @Override + public double getSample(int x, int y) { + return x + 10 * y; + } + + @Override + public void setSample(int x, int y, double value) { + } + }; + + MultiLevelModel model = new DefaultMultiLevelModel(1, new AffineTransform(), width, height); + CimrGridMultiLevelSource source = new CimrGridMultiLevelSource(model, band, grid); + DefaultMultiLevelImage mli = new DefaultMultiLevelImage(source); + + RenderedImage level0 = mli.getImage(0); + assertEquals(width, level0.getWidth()); + assertEquals(height, level0.getHeight()); + + Raster raster = level0.getData(new Rectangle(0, 0, width, height)); + assertEquals(0.0, raster.getSampleDouble(0, 0, 0), doubleErr); + assertEquals(1.0, raster.getSampleDouble(1, 0, 0), doubleErr); + assertEquals(10.0, raster.getSampleDouble(0, 1, 0), doubleErr); + assertEquals(11.0, raster.getSampleDouble(1, 1, 0), doubleErr); + } + + @Test + public void testAttachToBand_setsSourceImageAndUsesGridValues() { + int width = 2; + int height = 2; + Band band = new Band("test", ProductData.TYPE_FLOAT64, width, height); + + GridBandDataSource dataSource = new GridBandDataSource() { + @Override + public double getSample(int x, int y) { + return x + 10 * y; + } + + @Override + public void setSample(int x, int y, double value) { + // not needed for this test + } + }; + + PlateCarreeProjection projection = new PlateCarreeProjection( + width, height, + -180.0, 90.0, + 360.0 / width, 180.0 / height + ); + CimrGrid cimrGrid = new CimrGrid(projection, width, height); + + CimrGridMultiLevelSource.attachToBand(band, dataSource, cimrGrid); + + assertNotNull(band.getSourceImage()); + assertTrue(band.getSourceImage() instanceof DefaultMultiLevelImage); + + DefaultMultiLevelImage mli = (DefaultMultiLevelImage) band.getSourceImage(); + RenderedImage level0 = mli.getImage(0); + assertEquals(width, level0.getWidth()); + assertEquals(height, level0.getHeight()); + + Raster raster = level0.getData(new Rectangle(0, 0, width, height)); + assertEquals(0.0, raster.getSampleDouble(0, 0, 0), doubleErr); + assertEquals(1.0, raster.getSampleDouble(1, 0, 0), doubleErr); + assertEquals(10.0, raster.getSampleDouble(0, 1, 0), doubleErr); + assertEquals(11.0, raster.getSampleDouble(1, 1, 0), doubleErr); + + RenderedImage level1 = mli.getImage(1); + assertEquals(1, level1.getWidth()); + assertEquals(1, level1.getHeight()); + raster = level1.getData(new Rectangle(0, 0, 1, 1)); + assertEquals(5.5, raster.getSampleDouble(0, 0, 0), doubleErr); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridOpImageTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridOpImageTest.java new file mode 100644 index 000000000..d238f044c --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridOpImageTest.java @@ -0,0 +1,148 @@ +package eu.esa.snap.cimr.cimr; + + +import com.bc.ceres.multilevel.MultiLevelModel; +import com.bc.ceres.multilevel.support.DefaultMultiLevelImage; +import com.bc.ceres.multilevel.support.DefaultMultiLevelModel; +import eu.esa.snap.cimr.grid.GridBandDataSource; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.datamodel.ProductData; +import org.esa.snap.core.image.ResolutionLevel; +import org.junit.Test; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; + +import static org.junit.Assert.*; + + +public class CimrGridOpImageTest { + + private static final double doubleErr = 1e-8; + + + @Test + public void testLevel0UsesBaseGridValues() throws Exception { + int width = 4; + int height = 4; + Product product = new Product("P", "T", width, height); + Band band = product.addBand("b", ProductData.TYPE_FLOAT64); + + SimpleGrid grid = new SimpleGrid(width, height); + CimrGridOpImage opImage = createOpImageLevel(band, 0, width, height, grid); + + ProductData data = ProductData.createInstance(ProductData.TYPE_FLOAT64, width * height); + Rectangle rect = new Rectangle(0, 0, width, height); + + opImage.computeProductData(data, rect); + + assertEquals(0.0, data.getElemDoubleAt(0), doubleErr); + assertEquals(1.0, data.getElemDoubleAt(1), doubleErr); + assertEquals(10.0, data.getElemDoubleAt(4), doubleErr); + assertEquals(33.0, data.getElemDoubleAt(3 + 3 * width), doubleErr); + } + + @Test + public void testLevel1AveragesBlocks() throws Exception { + int baseWidth = 4; + int baseHeight = 4; + Product product = new Product("P", "T", baseWidth, baseHeight); + Band band = product.addBand("b", ProductData.TYPE_FLOAT64); + + SimpleGrid grid = new SimpleGrid(baseWidth, baseHeight); + + int levelIndex = 1; + CimrGridOpImage opImage = createOpImageLevel(band, levelIndex, baseWidth, baseHeight, grid); + + int w = baseWidth / 2; + int h = baseHeight / 2; + ProductData data = ProductData.createInstance(ProductData.TYPE_FLOAT64, w * h); + Rectangle rect = new Rectangle(0, 0, w, h); + + opImage.computeProductData(data, rect); + + + assertEquals(5.5, data.getElemDoubleAt(0), doubleErr); + assertEquals(7.5, data.getElemDoubleAt(1), doubleErr); + assertEquals(25.5, data.getElemDoubleAt(2), doubleErr); + assertEquals(27.5, data.getElemDoubleAt(3), doubleErr); + } + + + @Test + public void testIllegalArgumentFromGridDataSourceProducesNaN() { + int width = 2; + int height = 2; + + Product product = new Product("T", "T", width, height); + Band band = product.addBand("test", ProductData.TYPE_FLOAT64); + + GridBandDataSource grid = new GridBandDataSource() { + @Override + public double getSample(int x, int y) { + if (x == 1 && y == 0) { + throw new IllegalArgumentException("Test exception"); + } + return 42.0; + } + + @Override + public void setSample(int x, int y, double value) { + // not needed for this test + } + }; + + MultiLevelModel model = new DefaultMultiLevelModel(1, new AffineTransform(), width, height); + CimrGridMultiLevelSource source = new CimrGridMultiLevelSource(model, band, grid); + DefaultMultiLevelImage mli = new DefaultMultiLevelImage(source); + + RenderedImage level0 = mli.getImage(0); + Raster raster = level0.getData(new Rectangle(0, 0, width, height)); + + + assertEquals(42.0, raster.getSampleDouble(0, 0, 0), doubleErr); + assertTrue(Double.isNaN(raster.getSampleDouble(1, 0, 0))); + assertEquals(42.0, raster.getSampleDouble(0, 1, 0), doubleErr); + assertEquals(42.0, raster.getSampleDouble(1, 1, 0), doubleErr); + } + + + private static class SimpleGrid implements GridBandDataSource { + + private final int width; + private final int height; + private final double[] data; + + SimpleGrid(int width, int height) { + this.width = width; + this.height = height; + this.data = new double[width * height]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + data[y * width + x] = x + 10.0 * y; + } + } + } + + @Override + public double getSample(int x, int y) { + return data[y * width + x]; + } + + @Override + public void setSample(int x, int y, double value) { + data[y * width + x] = value; + } + } + + private CimrGridOpImage createOpImageLevel(Band band, int levelIndex, int baseWidth, int baseHeight, GridBandDataSource grid) { + AffineTransform at = new AffineTransform(); + MultiLevelModel model = new DefaultMultiLevelModel(2, at, baseWidth, baseHeight); + ResolutionLevel level = ResolutionLevel.create(model, levelIndex); + return new CimrGridOpImage(band, level, grid); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridProductTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridProductTest.java new file mode 100644 index 000000000..89d729255 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrGridProductTest.java @@ -0,0 +1,123 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.CimrReaderContext; +import eu.esa.snap.cimr.grid.*; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + + +public class CimrGridProductTest { + + + @Test + public void testAddAndGetBands() { + PlateCarreeProjection proj = new PlateCarreeProjection( + 2, 1, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid grid = new CimrGrid(proj, 2,1); + + CimrGridProduct product = new CimrGridProduct(grid); + + CimrBandDescriptor band1 = new CimrBandDescriptor( + "C_raw_bt_h_feed1", "raw_bt_h", CimrFrequencyBand.C_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", + 1, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor band2 = new CimrBandDescriptor( + "X_raw_bt_v_feed1", "raw_bt_h", CimrFrequencyBand.X_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", + 2, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + double[] data1 = {1.0, 2.0}; + double[] data2 = {10.0, 20.0}; + GridBandDataSource ds1 = new CimrGridBandDataSource(2, 1, data1); + GridBandDataSource ds2 = new CimrGridBandDataSource(2, 1, data2); + + product.addBand(band1, ds1); + product.addBand(band2, ds2); + + assertEquals(2, product.getBandCount()); + assertSame(grid, product.getGlobalGrid()); + assertEquals(ds1, product.getBandData(band1)); + assertEquals(2, product.getBands().size()); + + assertEquals(1.0, product.getBandData(band1).getSample(0, 0), 1e-12); + assertEquals(20.0, product.getBandData(band2).getSample(1, 0), 1e-12); + } + + @Test + public void testBuildLazyCreatesBandsFromDescriptorSet() { + PlateCarreeProjection proj = new PlateCarreeProjection(2, 1, 0.0, 1.0, 1.0, 1.0); + CimrGrid grid = new CimrGrid(proj, 2, 1); + + CimrBandDescriptor tieDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + new String[] {"lat", "lon"}, new String[] {""}, + "/Geolocation/", 0, + CimrDescriptorKind.TIEPOINT_VARIABLE, + new String[] {"n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor measDesc = new CimrBandDescriptor( + "C_raw_bt_h_feed1", "raw_bt_h", CimrFrequencyBand.C_BAND, + new String[] {"lat", "lon"}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", 0, + CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + CimrDescriptorSet descriptorSet = new CimrDescriptorSet( + List.of(measDesc), + java.util.Collections.emptyList(), + List.of(tieDesc) + ); + + CimrReaderContext context = new CimrReaderContext( + null, descriptorSet, grid, null, null + ); + + CimrGridProduct product = CimrGridProduct.buildLazy(context, true); + + assertSame(grid, product.getGlobalGrid()); + assertEquals(2, product.getBandCount()); + assertTrue(product.getBands().containsKey(tieDesc)); + assertTrue(product.getBands().containsKey(measDesc)); + assertTrue(product.getBandData(tieDesc) instanceof LazyGridBandDataSource); + assertTrue(product.getBandData(measDesc) instanceof LazyGridBandDataSource); + } + + + @Test(expected = UnsupportedOperationException.class) + public void testGetBandsIsUnmodifiable() { + PlateCarreeProjection proj = new PlateCarreeProjection(2, 1, 0.0, 1.0, 1.0, 1.0); + CimrGrid grid = new CimrGrid(proj, 2, 1); + CimrGridProduct product = new CimrGridProduct(grid); + + CimrBandDescriptor band = new CimrBandDescriptor( + "C_raw_bt_h_feed1", "raw_bt_h", CimrFrequencyBand.C_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", + 1, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + GridBandDataSource ds = new CimrGridBandDataSource(2, 1, new double[]{1.0, 2.0}); + product.addBand(band, ds); + + product.getBands().put(band, ds); + } + +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrSnapProductBuilderTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrSnapProductBuilderTest.java new file mode 100644 index 000000000..de1b6905f --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/cimr/CimrSnapProductBuilderTest.java @@ -0,0 +1,167 @@ +package eu.esa.snap.cimr.cimr; + +import eu.esa.snap.cimr.grid.CimrGridBandDataSource; +import eu.esa.snap.cimr.grid.CimrGrid; +import eu.esa.snap.cimr.grid.GridBandDataSource; +import eu.esa.snap.cimr.grid.PlateCarreeProjection; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.Product; +import org.junit.Test; + +import java.awt.image.Raster; + +import static org.junit.Assert.*; + + +public class CimrSnapProductBuilderTest { + + private static final double doubleErr = 1e-6; + + @Test + public void testBuildSnapProduct_createsBandsAndValues() throws Exception { + PlateCarreeProjection proj = new PlateCarreeProjection( + 2, 1, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid cimrGrid = new CimrGrid(proj, 2, 1); + + CimrGridProduct gridProduct = new CimrGridProduct(cimrGrid); + + CimrBandDescriptor bandDesc = new CimrBandDescriptor( + "C_raw_bt_h_feed1", "raw_bt_h", CimrFrequencyBand.C_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", + 1, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + double[] data = {1.0, 2.0}; + GridBandDataSource ds = new CimrGridBandDataSource(2, 1, data); + gridProduct.addBand(bandDesc, ds); + + Product product = CimrSnapProductBuilder.buildProduct("TEST", "CIMR_GRID", gridProduct, "path"); + + assertEquals(2, product.getSceneRasterWidth()); + assertEquals(1, product.getSceneRasterHeight()); + assertNotNull(product.getSceneGeoCoding()); + + Band band = product.getBand("C_raw_bt_h_feed1"); + assertNotNull(band); + + Raster raster = band.getSourceImage().getImage(0).getData(); + assertEquals(1.0, raster.getSampleDouble(0,0,0), doubleErr); + assertEquals(2.0, raster.getSampleDouble(1,0,0), doubleErr); + } + + @Test + public void testBuildSnapProduct_setsMetadataAndAutoGrouping() throws Exception { + PlateCarreeProjection proj = new PlateCarreeProjection( + 2, 1, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid cimrGrid = new CimrGrid(proj, 2, 1); + + CimrGridProduct gridProduct = new CimrGridProduct(cimrGrid); + + CimrBandDescriptor bandDesc = new CimrBandDescriptor( + "C_raw_bt_h_feed1", "raw_bt_h", CimrFrequencyBand.C_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", + 1, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "K", + "Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)" + ); + + double[] data = {1.0, 2.0}; + GridBandDataSource ds = new CimrGridBandDataSource(2, 1, data); + gridProduct.addBand(bandDesc, ds); + + String path = "some\\path\\file.nc"; + Product product = CimrSnapProductBuilder.buildProduct("TEST", "CIMR_GRID", gridProduct, path); + + assertEquals("TEST", product.getName()); + assertEquals("CIMR_GRID", product.getProductType()); + assertNotNull(product.getFileLocation()); + assertTrue(product.getFileLocation().getPath().endsWith(path)); + assertEquals("L_BAND:C_BAND:X_BAND:KU_BAND:KA_BAND", product.getAutoGrouping().toString()); + + Band band = product.getBand("C_raw_bt_h_feed1"); + assertEquals("K", band.getUnit()); + assertEquals("Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)", band.getDescription()); + assertEquals(Double.NaN, band.getNoDataValue(), doubleErr); + assertTrue(band.isNoDataValueSet()); + assertEquals(43300000f, band.getSpectralWavelength(), doubleErr); + } + + @Test + public void testBuildSnapProduct_withMultipleBands_allPresentAndCorrect() throws Exception { + PlateCarreeProjection proj = new PlateCarreeProjection( + 2, 1, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid cimrGrid = new CimrGrid(proj, 2, 1); + + CimrGridProduct gridProduct = new CimrGridProduct(cimrGrid); + + CimrBandDescriptor band1 = new CimrBandDescriptor( + "band1", "raw1", CimrFrequencyBand.C_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/C_BAND/", + 0, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor band2 = new CimrBandDescriptor( + "band2", "raw2", CimrFrequencyBand.X_BAND, + new String[] {""}, new String[] {""}, + "/Data/Measurement_Data/X_BAND/", + 0, CimrDescriptorKind.VARIABLE, + new String[] {"n_scans", "n_samples_X_BAND", "n_feeds_X_BAND"}, + "double", "", "" + ); + + GridBandDataSource ds1 = new CimrGridBandDataSource(2, 1, new double[]{1.0, 2.0}); + GridBandDataSource ds2 = new CimrGridBandDataSource(2, 1, new double[]{10.0, 20.0}); + + gridProduct.addBand(band1, ds1); + gridProduct.addBand(band2, ds2); + + Product product = CimrSnapProductBuilder.buildProduct("TEST", "CIMR_GRID", gridProduct, "path"); + + assertEquals(2, product.getNumBands()); + + Band b1 = product.getBand("band1"); + Band b2 = product.getBand("band2"); + assertNotNull(b1); + assertNotNull(b2); + + assertEquals(1.0, b1.getSourceImage().getImage(0).getData().getSampleDouble(0, 0, 0), 1e-6); + assertEquals(2.0, b1.getSourceImage().getImage(0).getData().getSampleDouble(1, 0, 0), 1e-6); + assertEquals(10.0, b2.getSourceImage().getImage(0).getData().getSampleDouble(0, 0, 0), 1e-6); + assertEquals(20.0, b2.getSourceImage().getImage(0).getData().getSampleDouble(1, 0, 0), 1e-6); + } + + @Test + public void testBuildSnapProduct_withNoBands_createsEmptyProductWithGeoCoding() throws Exception { + PlateCarreeProjection proj = new PlateCarreeProjection( + 4, 2, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid cimrGrid = new CimrGrid(proj, 4, 2); + + CimrGridProduct gridProduct = new CimrGridProduct(cimrGrid); + + Product product = CimrSnapProductBuilder.buildProduct("EMPTY", "CIMR_GRID", gridProduct, "path"); + + assertEquals(4, product.getSceneRasterWidth()); + assertEquals(2, product.getSceneRasterHeight()); + assertNotNull(product.getSceneGeoCoding()); + assertEquals(0, product.getNumBands()); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/config/CimrConfigLoaderTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/config/CimrConfigLoaderTest.java new file mode 100644 index 000000000..354719686 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/config/CimrConfigLoaderTest.java @@ -0,0 +1,80 @@ +package eu.esa.snap.cimr.config; + +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDescriptorKind; +import eu.esa.snap.cimr.cimr.CimrDescriptorSet; +import eu.esa.snap.cimr.cimr.CimrFrequencyBand; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.*; + + +public class CimrConfigLoaderTest { + + + @Test + public void testLoadTestConfigJson() throws Exception { + CimrDescriptorSet set = CimrConfigLoader.load("test-config.json"); + assertNotNull(set); + + List meas = set.getMeasurements(); + List tpVars = set.getTiepointVariables(); + List tpGeoms = set.getGeometries(); + + assertEquals(4, meas.size()); + assertEquals(7, tpVars.size()); + assertEquals(4, tpGeoms.size()); + + + CimrBandDescriptor m0 = meas.get(0); + assertEquals("C_BAND_raw_bt_h_feed1", m0.getName()); + assertEquals("raw_bt_h", m0.getValueVarName()); + assertEquals(CimrFrequencyBand.C_BAND, m0.getBand()); + assertEquals("/Data/Measurement_Data/C_BAND/", m0.getGroupPath()); + assertEquals(0, m0.getFeedIndex()); + assertEquals(CimrDescriptorKind.VARIABLE, m0.getKind()); + assertArrayEquals(new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, m0.getDimensions()); + assertEquals("double", m0.getDataType()); + assertArrayEquals(new String[]{"C_BAND_latitude_feed1", "C_BAND_longitude_feed1"}, m0.getGeometryNames()); + assertArrayEquals(new String[]{"C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"}, m0.getFootprintVars()); + assertEquals("K", m0.getUnit()); + assertEquals("Brightness temperature of the Earth, in H polarization, from raw counts (no RFI mitigation)", m0.getDescription()); + + + CimrBandDescriptor tpVal0 = tpVars.get(0); + assertEquals("C_BAND_altitude_feed1", tpVal0.getName()); + assertEquals("altitude", tpVal0.getValueVarName()); + assertEquals(CimrFrequencyBand.C_BAND, tpVal0.getBand()); + assertEquals("/Data/Navigation_Data/C_BAND/", tpVal0.getGroupPath()); + assertEquals(0, tpVal0.getFeedIndex()); + assertEquals(CimrDescriptorKind.TIEPOINT_VARIABLE, tpVal0.getKind()); + assertArrayEquals(new String[]{"n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"}, tpVal0.getDimensions()); + assertEquals("double", tpVal0.getDataType()); + assertArrayEquals(new String[]{"C_BAND_latitude_feed1", "C_BAND_longitude_feed1"}, tpVal0.getGeometryNames()); + assertArrayEquals(new String[]{"C_BAND_footprint_minor_axis_feed1", "C_BAND_footprint_major_axis_feed1", "C_BAND_geometric_rot_angle_feed1"}, tpVal0.getFootprintVars()); + assertEquals("m", tpVal0.getUnit()); + assertEquals("Altitude for intersection of the LOS with the earth surface for the C band Earth views", tpVal0.getDescription()); + + + CimrBandDescriptor tpGeom0 = tpGeoms.get(0); + assertEquals("C_BAND_latitude_feed1", tpGeom0.getName()); + assertEquals("latitude", tpGeom0.getValueVarName()); + assertEquals(CimrFrequencyBand.C_BAND, tpGeom0.getBand()); + assertEquals("/Data/Navigation_Data/C_BAND/", tpGeom0.getGroupPath()); + assertEquals(0, tpGeom0.getFeedIndex()); + assertEquals(CimrDescriptorKind.GEOMETRY, tpGeom0.getKind()); + assertArrayEquals(new String[]{"n_scans", "n_tie_points_C_BAND", "n_feeds_C_BAND"}, tpGeom0.getDimensions()); + assertEquals("double", tpGeom0.getDataType()); + assertEquals("deg", tpGeom0.getUnit()); + assertEquals("Latitude of Earth surface point in the boresight direction for the C band acquisitions", tpGeom0.getDescription()); + } + + + @Test(expected = IOException.class) + public void testLoadTestConfigJson_throws() throws IOException { + CimrConfigLoader.load("not-existent.json"); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrBoundingBoxTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrBoundingBoxTest.java new file mode 100644 index 000000000..f383c750c --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrBoundingBoxTest.java @@ -0,0 +1,36 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrBoundingBoxTest { + + private static final double doubleErr = 0.00001; + + @Test + public void testCreateBoundingBox() { + int scanCount = 5; + int tpCount = 4; + int sampleCount = 8; + GeoPos[][][] tiePoints = new GeoPos[scanCount][tpCount][1]; + + for (int s = 0; s < scanCount; s++) { + for (int tp = 0; tp < tpCount; tp++) { + double lat = 50.0234 + s; + double lon = 10.542 + tp * 2; + tiePoints[s][tp][0] = new GeoPos(lat, lon); + } + } + + CimrGeometry geometry = new CimrTiepointGeometry(tiePoints, sampleCount); + CimrBoundingBox bBox = CimrBoundingBox.create(geometry, 0.02); + + assertEquals(49.52, bBox.getLatMin(), doubleErr); + assertEquals(10.04, bBox.getLonMin(), doubleErr); + assertEquals(54.54, bBox.getLatMax(), doubleErr); + assertEquals(17.06, bBox.getLonMax(), doubleErr); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGeometryBandTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGeometryBandTest.java new file mode 100644 index 000000000..2c72c5f49 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGeometryBandTest.java @@ -0,0 +1,113 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrGeometryBandTest { + + private static final double doubleErr = 1e-6; + + + @Test + public void testValuesAndGeometryAreWiredCorrectly() { + double[][] values = { + {1.0, 2.0, 3.0, 4.0} + }; + + GeoPos[][][] tp = new GeoPos[1][2][1]; + tp[0][0][0] = new GeoPos(50f, 0f); + tp[0][1][0] = new GeoPos(50f, 10f); + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 4); + + CimrGeometryBand band = new CimrGeometryBand(values, geom, 5); + + assertEquals(1, band.getScanCount()); + assertEquals(4, band.getSampleCount()); + assertEquals(5, band.getFeedIndex()); + + assertEquals(1.0, band.getValue(0, 0), doubleErr); + assertEquals(4.0, band.getValue(0, 3), doubleErr); + + GeoPos g0 = band.getGeoPos(0, 0); + GeoPos g2 = band.getGeoPos(0, 2); + GeoPos g3 = band.getGeoPos(0, 3); + + assertEquals(50.0, g0.getLat(), doubleErr); + assertEquals(0.0, g0.getLon(), doubleErr); + assertEquals(50.0, g2.getLat(), doubleErr); + assertEquals(6.666666, g2.getLon(), doubleErr); + assertEquals(50.0, g3.getLat(), doubleErr); + assertEquals(10.0, g3.getLon(), doubleErr); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_FailsWhenValuesNull() { + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 2); + + new CimrGeometryBand(new double[0][0], geom, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_FailsWhenGeometryNull() { + double[][] values = { + {1.0, 2.0} + }; + new CimrGeometryBand(values, null, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_FailsWhenNoScans() { + double[][] values = new double[0][]; + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 1); + new CimrGeometryBand(values, geom, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_FailsWhenSampleDimMismatchInValues() { + double[][] values = new double[2][]; + values[0] = new double[]{1.0, 2.0}; + values[1] = new double[]{3.0}; + + GeoPos[][][] tp = new GeoPos[2][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + tp[1][0][0] = new GeoPos(1f, 1f); + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 2); + new CimrGeometryBand(values, geom, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_FailsWhenGeometryScanCountMismatch() { + double[][] values = { + {1.0, 2.0} + }; + + GeoPos[][][] tp = new GeoPos[2][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + tp[1][0][0] = new GeoPos(1f, 1f); + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 2); + new CimrGeometryBand(values, geom, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_FailsWhenGeometrySampleCountMismatch() { + double[][] values = { + {1.0, 2.0} + }; + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 3); + new CimrGeometryBand(values, geom, 0); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridBandDataSourceTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridBandDataSourceTest.java new file mode 100644 index 000000000..96e3f1774 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridBandDataSourceTest.java @@ -0,0 +1,63 @@ +package eu.esa.snap.cimr.grid; + +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrGridBandDataSourceTest { + + @Test + public void testGetSample_basicLayout() { + int width = 2; + int height = 2; + double[] data = { + 1.0, 2.0, + 3.0, 4.0 + }; + CimrGridBandDataSource ds = new CimrGridBandDataSource(width, height, data); + + assertEquals(1.0, ds.getSample(0, 0), 1e-12); + assertEquals(2.0, ds.getSample(1, 0), 1e-12); + assertEquals(3.0, ds.getSample(0, 1), 1e-12); + assertEquals(4.0, ds.getSample(1, 1), 1e-12); + } + + @Test + public void testCreateEmpty_initialNaN() { + CimrGridBandDataSource ds = CimrGridBandDataSource.createEmpty(2, 2); + + assertTrue(Double.isNaN(ds.getSample(0, 0))); + assertTrue(Double.isNaN(ds.getSample(1, 0))); + assertTrue(Double.isNaN(ds.getSample(0, 1))); + assertTrue(Double.isNaN(ds.getSample(1, 1))); + } + + @Test + public void testSetSample() { + CimrGridBandDataSource ds = CimrGridBandDataSource.createEmpty(2, 1); + + ds.setSample(0, 0, 42.0); + ds.setSample(1, 0, 7.0); + + assertEquals(42.0, ds.getSample(0, 0), 1e-12); + assertEquals(7.0, ds.getSample(1, 0), 1e-12); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_invalidLength_throws() { + new CimrGridBandDataSource(2, 2, new double[]{1.0, 2.0, 3.0}); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetSample_outOfBounds_throws() { + CimrGridBandDataSource ds = CimrGridBandDataSource.createEmpty(2, 2); + ds.getSample(2, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetSample_outOfBounds_throws() { + CimrGridBandDataSource ds = CimrGridBandDataSource.createEmpty(2, 2); + ds.setSample(-1, 0, 5.0); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridBuilderTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridBuilderTest.java new file mode 100644 index 000000000..d49558311 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridBuilderTest.java @@ -0,0 +1,82 @@ +package eu.esa.snap.cimr.grid; + +import eu.esa.snap.cimr.cimr.CimrGridBuilder; +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrGridBuilderTest { + + private static final double doubleErr = 1e-6; + + + @Test + public void testBuild_usesAverageWhenTrue() { + GeometryBandToGridMapper mapper = new GeometryBandToGridMapper(); + CimrGridBuilder builder = new CimrGridBuilder(mapper); + + CimrBand swath = createDummySwath(); + PlateCarreeProjection proj = new PlateCarreeProjection( + 1, 1, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid grid = new CimrGrid(proj, 1, 1); + + CimrGridBandDataSource target = builder.build(swath, grid, true); + + assertEquals(41.0, target.getSample(0, 0), doubleErr); + assertEquals(1, target.getWidth()); + assertEquals(1, target.getHeight()); + } + + @Test + public void testBuild_usesNearestWhenFalse() { + GeometryBandToGridMapper mapper = new GeometryBandToGridMapper(); + CimrGridBuilder builder = new CimrGridBuilder(mapper); + + CimrBand swath = createDummySwath(); + PlateCarreeProjection proj = new PlateCarreeProjection( + 1, 1, + 0.0, 1.0, + 1.0, 1.0 + ); + CimrGrid grid = new CimrGrid(proj, 1, 1); + + CimrGridBandDataSource target = builder.build(swath, grid, false); + + assertEquals(40.0, target.getSample(0, 0), doubleErr); + assertEquals(1, target.getWidth()); + assertEquals(1, target.getHeight()); + } + + private CimrBand createDummySwath() { + return new CimrBand() { + @Override + public int getScanCount() { + return 1; + } + + @Override + public int getSampleCount() { + return 2; + } + + @Override + public double getValue(int scanIndex, int sampleIndex) { + if (sampleIndex == 0) { + return 42.0; + } else { + return 40.0; + } + } + + @Override + public GeoPos getGeoPos(int scanIndex, int sampleIndex) { + return new GeoPos(0.5f, 0.5f); + } + }; + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridFactoryTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridFactoryTest.java new file mode 100644 index 000000000..b6fbb3c59 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridFactoryTest.java @@ -0,0 +1,41 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrGridFactoryTest { + + private static final double doubleErr = 0.00001; + + @Test + public void testCreatePlateCarreeFromBoundingBox() { + int scanCount = 5; + int tpCount = 4; + int sampleCount = 8; + GeoPos[][][] tiePoints = new GeoPos[scanCount][tpCount][1]; + + for (int s = 0; s < scanCount; s++) { + for (int tp = 0; tp < tpCount; tp++) { + double lat = 50.0234 + s; + double lon = 10.542 + tp * 2; + tiePoints[s][tp][0] = new GeoPos(lat, lon); + } + } + + CimrGeometry geometry = new CimrTiepointGeometry(tiePoints, sampleCount); + CimrBoundingBox bBox = CimrBoundingBox.create(geometry, CimrGridFactory.DEFAULT_CELL_SIZE_DEG); + + CimrGrid grid = CimrGridFactory.createPlateCarreeFromBoundingBox(bBox); + + assertEquals(351.0, grid.getWidth(), doubleErr); + assertEquals(251.0, grid.getHeight(), doubleErr); + assertEquals(CimrGridFactory.DEFAULT_CELL_SIZE_DEG, grid.getProjection().getDeltaLat(), doubleErr); + assertEquals(CimrGridFactory.DEFAULT_CELL_SIZE_DEG, grid.getProjection().getDeltaLon(), doubleErr); + assertEquals(10.04, grid.getProjection().getLonMin(), doubleErr); + assertEquals(54.54, grid.getProjection().getLatMax(), doubleErr); + } + +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridTest.java new file mode 100644 index 000000000..3733fc834 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrGridTest.java @@ -0,0 +1,45 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Before; +import org.junit.Test; + +import java.awt.*; + +import static org.junit.Assert.*; + + +public class CimrGridTest { + + CimrGrid grid; + + @Before + public void setUp() { + PlateCarreeProjection proj = new PlateCarreeProjection( + 360, 180, + -180.0, 90.0, + 1.0, 1.0 + ); + grid = new CimrGrid(proj, 180, 360); + } + + @Test + public void gridToGeoPos() { + GeoPos pos = grid.gridToGeoPos(50, 100); + assertEquals(-10.5, pos.getLat(), 1e-6); + assertEquals(-129.5, pos.getLon(), 1e-6); + } + + @Test + public void geoPosToGrid() { + Point p = new Point(); + int x = 50; + int y = 100; + GeoPos pos = grid.gridToGeoPos(x, y); + boolean inside = grid.geoPosToGrid(pos, p); + + assertTrue("Point should be inside grid", inside); + assertEquals("x mismatch", x, p.x); + assertEquals("y mismatch", y, p.y); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrTiepointGeometryTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrTiepointGeometryTest.java new file mode 100644 index 000000000..9919d106d --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/CimrTiepointGeometryTest.java @@ -0,0 +1,139 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class CimrTiepointGeometryTest { + + private static final double doubleErr = 1e-6; + + @Test + public void testInterpolation_simple4Samples2TiePoints() { + int scans = 1; + int tiePoints = 2; + int feeds = 1; + int samples = 4; + + GeoPos[][][] tp = new GeoPos[scans][tiePoints][feeds]; + tp[0][0][0] = new GeoPos(50f, 0f); + tp[0][1][0] = new GeoPos(50f, 10f); + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, samples); + + GeoPos g0 = geom.getGeoPos(0, 0, 0); + GeoPos g1 = geom.getGeoPos(0, 1, 0); + GeoPos g2 = geom.getGeoPos(0, 2, 0); + GeoPos g3 = geom.getGeoPos(0, 3, 0); + + assertEquals(2, geom.getTiePointCount()); + + assertEquals(50.0, g0.getLat(), doubleErr); + assertEquals(0.0, g0.getLon(), doubleErr); + + assertEquals(50.0, g1.getLat(), doubleErr); + assertEquals(3.333333, g1.getLon(), doubleErr); + + assertEquals(50.0, g2.getLat(), doubleErr); + assertEquals(6.666666, g2.getLon(), doubleErr); + + assertEquals(50.0, g3.getLat(), doubleErr); + assertEquals(10.0, g3.getLon(), doubleErr); + } + + @Test + public void testInterpolation_realisticCounts() { + int scans = 1; + int tiePoints = 274; + int feeds = 1; + int samples = 549; + + GeoPos[][][] tp = new GeoPos[scans][tiePoints][feeds]; + for (int i = 0; i < tiePoints; i++) { + float lon = (float) (10.0 * i / (tiePoints - 1)); + tp[0][i][0] = new GeoPos(60f, lon); + } + + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, samples); + + GeoPos gStart = geom.getGeoPos(0, 0, 0); + GeoPos gMiddle = geom.getGeoPos(0, 200, 0); + GeoPos gEnd = geom.getGeoPos(0, samples - 1, 0); + + assertEquals(60.0, gStart.getLat(), doubleErr); + assertEquals(0.0, gStart.getLon(), doubleErr); + + assertEquals(60.0, gMiddle.getLat(), doubleErr); + assertEquals(3.649635, gMiddle.getLon(), doubleErr); + + assertEquals(60.0, gEnd.getLat(), doubleErr); + assertEquals(10.0, gEnd.getLon(), doubleErr); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_failsWhenNoScans() { + GeoPos[][][] tp = new GeoPos[0][0][0]; + new CimrTiepointGeometry(tp, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_failsWhenNoTiePoints() { + GeoPos[][][] tp = new GeoPos[1][0][0]; + new CimrTiepointGeometry(tp, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_failsWhenTiePointDimMismatchBetweenScans() { + GeoPos[][][] tp = new GeoPos[2][][]; + tp[0] = new GeoPos[2][1]; + tp[1] = new GeoPos[3][1]; + + new CimrTiepointGeometry(tp, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_failsWhenFeedDimMismatch() { + GeoPos[][][] tp = new GeoPos[1][][]; + tp[0] = new GeoPos[2][]; + tp[0][0] = new GeoPos[1]; + tp[0][1] = new GeoPos[2]; + + new CimrTiepointGeometry(tp, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_failsWhenSampleCountTooSmall() { + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + new CimrTiepointGeometry(tp, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetGeoPos_failsWhenScanIndexOutOfRange() { + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 2); + + geom.getGeoPos(1, 0, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetGeoPos_failsWhenSampleIndexOutOfRange() { + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 2); + + geom.getGeoPos(0, 2, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetGeoPos_failsWhenFeedIndexOutOfRange() { + GeoPos[][][] tp = new GeoPos[1][1][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, 2); + + geom.getGeoPos(0, 0, 1); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/GeometryBandToGridMapperTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/GeometryBandToGridMapperTest.java new file mode 100644 index 000000000..3d85c3aa7 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/GeometryBandToGridMapperTest.java @@ -0,0 +1,76 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class GeometryBandToGridMapperTest { + + private static final double doubleErr = 1e-6; + + @Test + public void testMap_simpleCase_mapNearest() { + PlateCarreeProjection projection = new PlateCarreeProjection( + 4, 2, + -10.0, 81.0, + 1.0, 1.0 + ); + CimrGrid grid = new CimrGrid(projection, 4, 2); + CimrBand swath = new DummyCimrBand(); + + CimrGridBandDataSource target = CimrGridBandDataSource.createEmpty(4, 2); + + GeometryBandToGridMapper mapper = new GeometryBandToGridMapper(); + mapper.mapNearest(swath, grid, target); + + assertEquals(0.0, target.getSample(0, 0), doubleErr); + assertEquals(1.0, target.getSample(1, 0), doubleErr); + assertEquals(2.0, target.getSample(2, 0), doubleErr); + assertEquals(3.0, target.getSample(3, 0), doubleErr); + + assertEquals(10.0, target.getSample(0, 1), doubleErr); + assertEquals(11.0, target.getSample(1, 1), doubleErr); + assertEquals(12.0, target.getSample(2, 1), doubleErr); + assertEquals(13.0, target.getSample(3, 1), doubleErr); + } + + + @Test + public void testMap_simpleCase_mapAverage() { + PlateCarreeProjection proj = new PlateCarreeProjection(1, 1, -180, 90, 360, 180); + CimrGrid grid = new CimrGrid(proj, 1, 1); + + CimrBand swath = new DummyCimrBand(); + CimrGridBandDataSource target = CimrGridBandDataSource.createEmpty(1, 1); + GeometryBandToGridMapper mapper = new GeometryBandToGridMapper(); + mapper.mapAverage(swath, grid, target); + + assertEquals(6.5, target.getSample(0, 0), 1e-6); + } +} + +class DummyCimrBand implements CimrBand { + @Override + public int getScanCount() { + return 2; + } + + @Override + public int getSampleCount() { + return 4; + } + + @Override + public double getValue(int scanIndex, int sampleIndex) { + return scanIndex * 10 + sampleIndex; + } + + @Override + public GeoPos getGeoPos(int scanIndex, int sampleIndex) { + float lat = 80.5f - scanIndex; + float lon = -9.5f + sampleIndex; + return new GeoPos(lat, lon); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/LazyCrsGeoCodingTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/LazyCrsGeoCodingTest.java new file mode 100644 index 000000000..9443d551f --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/LazyCrsGeoCodingTest.java @@ -0,0 +1,91 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.esa.snap.core.datamodel.PixelPos; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + + +public class LazyCrsGeoCodingTest { + + @Test + public void test_canGetFlagsAndIsGlobalDoNotInitDelegate() throws Exception { + CimrGrid grid = mock(CimrGrid.class); + + LazyCrsGeoCoding gc = new LazyCrsGeoCoding(grid); + + assertTrue(gc.canGetGeoPos()); + assertTrue(gc.canGetPixelPos()); + assertTrue(gc.isGlobal()); + + assertNull(getField(gc, "delegate")); + verifyNoInteractions(grid); + } + + @Test + public void test_delegateIsCreatedLazilyAndReusedForAllDelegatingMethods() throws Exception { + CimrGrid grid = CimrGridFactory.createGlobalPlateCarree(1.0); + LazyCrsGeoCoding gc = new LazyCrsGeoCoding(grid); + + assertNull(getField(gc, "delegate")); + + GeoPos gp1 = gc.getGeoPos(new PixelPos(0.5f, 0.5f), null); + + Object delegate1 = getField(gc, "delegate"); + assertNotNull(delegate1); + assertNotNull(gp1); + + GeoPos gp2 = gc.getGeoPos(new PixelPos(10.5f, 20.5f), null); + Object delegate2 = getField(gc, "delegate"); + assertSame(delegate1, delegate2); + assertNotNull(gp2); + + gc.isCrossingMeridianAt180(); + gc.getPixelPos(new GeoPos(0.0f, 0.0f), null); + assertNotNull(gc.getDatum()); + assertNotNull(gc.getImageCRS()); + assertNotNull(gc.getMapCRS()); + assertNotNull(gc.getGeoCRS()); + assertNotNull(gc.getImageToMapTransform()); + assertFalse(gc.canClone()); + + gc.dispose(); + } + + @Test + public void wrapsDelegateCreationFailuresInRuntimeException() { + CimrGrid badGrid = mock(CimrGrid.class); + when(badGrid.getWidth()).thenReturn(10); + when(badGrid.getHeight()).thenReturn(10); + when(badGrid.getProjection()).thenThrow(new RuntimeException("boom")); + + LazyCrsGeoCoding gc = new LazyCrsGeoCoding(badGrid); + + try { + gc.getGeoPos(new PixelPos(0.5f, 0.5f), null); + fail("Expected RuntimeException to be thrown"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("Failed to create CrsGeoCoding")); + assertNotNull(e.getCause()); + assertEquals("boom", e.getCause().getMessage()); + } + } + + @Test(expected = IllegalStateException.class) + public void cloneThrowsIllegalStateException() { + CimrGrid grid = CimrGridFactory.createGlobalPlateCarree(1.0); + LazyCrsGeoCoding gc = new LazyCrsGeoCoding(grid); + + gc.clone(); + } + + private static Object getField(Object target, String name) throws NoSuchFieldException, IllegalAccessException { + Field f = target.getClass().getDeclaredField(name); + f.setAccessible(true); + return f.get(target); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/LazyGridBandDataSourceTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/LazyGridBandDataSourceTest.java new file mode 100644 index 000000000..be7f42634 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/LazyGridBandDataSourceTest.java @@ -0,0 +1,107 @@ +package eu.esa.snap.cimr.grid; + + +import eu.esa.snap.cimr.CimrReaderContext; +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDescriptorKind; +import eu.esa.snap.cimr.cimr.CimrDescriptorSet; +import eu.esa.snap.cimr.cimr.CimrFrequencyBand; +import org.junit.Test; +import ucar.nc2.NetcdfFile; + +import static org.junit.Assert.*; + + +public class LazyGridBandDataSourceTest { + + + private static final double doubleErr = 1e-6; + + @Test + public void testLazyInitializationAndGetSampleDelegation() { + double[] data = { + 1.0, 2.0, + 3.0, 4.0 + }; + CimrGridBandDataSource delegate = new CimrGridBandDataSource(2, 2, data); + + TestReaderContext context = new TestReaderContext(delegate); + CimrBandDescriptor desc = createDummyDescriptor("test_band"); + + LazyGridBandDataSource lazy = new LazyGridBandDataSource(context, desc, true); + + double v00 = lazy.getSample(0, 0); + double v11 = lazy.getSample(1, 1); + + assertEquals(1.0, v00, doubleErr); + assertEquals(4.0, v11, doubleErr); + + assertEquals(1, context.callCount); + assertSame(desc, context.lastDescriptor); + assertTrue(context.lastUseAverage); + } + + @Test + public void testSetSampleDelegationAndUseAverageFalse() { + double[] data = { + 0.0, 0.0, + 0.0, 0.0 + }; + CimrGridBandDataSource delegate = new CimrGridBandDataSource(2, 2, data); + + TestReaderContext context = new TestReaderContext(delegate); + CimrBandDescriptor desc = createDummyDescriptor("test_band_2"); + + LazyGridBandDataSource lazy = new LazyGridBandDataSource(context, desc, false); + + assertEquals(0.0, delegate.getSample(1, 1), doubleErr); + + lazy.setSample(1, 1, 42.0); + assertEquals(42.0, delegate.getSample(1, 1), doubleErr); + + assertEquals(1, context.callCount); + assertSame(desc, context.lastDescriptor); + assertFalse(context.lastUseAverage); + } + + + private static class TestReaderContext extends CimrReaderContext { + + int callCount = 0; + CimrBandDescriptor lastDescriptor; + boolean lastUseAverage; + private final CimrGridBandDataSource delegate; + + TestReaderContext(CimrGridBandDataSource delegate) { + super((NetcdfFile) null, + new CimrDescriptorSet(null, null, null), + null, null, null); + this.delegate = delegate; + } + + @Override + public CimrGridBandDataSource getOrCreateGridForVariable(CimrBandDescriptor descriptor, boolean useAverage) { + callCount++; + lastDescriptor = descriptor; + lastUseAverage = useAverage; + return delegate; + } + } + + private static CimrBandDescriptor createDummyDescriptor(String name) { + return new CimrBandDescriptor( + name, + "valueVar", + CimrFrequencyBand.C_BAND, + new String[]{"lat", "lon"}, + new String[] {""}, + "/dummy/path", + 0, + CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", + "", + "" + ); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/PlateCarreeProjectionTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/PlateCarreeProjectionTest.java new file mode 100644 index 000000000..3c816a06a --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/grid/PlateCarreeProjectionTest.java @@ -0,0 +1,86 @@ +package eu.esa.snap.cimr.grid; + +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Before; +import org.junit.Test; + +import java.awt.*; + +import static org.junit.Assert.*; + + +public class PlateCarreeProjectionTest { + + private PlateCarreeProjection proj; + private double doubleErr = 1e-6; + + @Before + public void setUp() { + proj = new PlateCarreeProjection( + 360, 180, + -180.0, 90.0, + 1.0, 1.0 + ); + } + + + @Test + public void testGridToGeoPos_corners() { + GeoPos ul = proj.gridToGeoPos(0, 0); + assertEquals(89.5, ul.getLat(), doubleErr); + assertEquals(-179.5, ul.getLon(), doubleErr); + + GeoPos ur = proj.gridToGeoPos(359, 0); + assertEquals(89.5, ur.getLat(), doubleErr); + assertEquals(179.5, ur.getLon(), doubleErr); + + GeoPos ll = proj.gridToGeoPos(0, 179); + assertEquals(-89.5, ll.getLat(), doubleErr); + assertEquals(-179.5, ll.getLon(), doubleErr); + + GeoPos lr = proj.gridToGeoPos(359, 179); + assertEquals(-89.5, lr.getLat(), doubleErr); + assertEquals(179.5, lr.getLon(), doubleErr); + } + + @Test + public void testGeoPosToGrid_roundTrip() { + Point p = new Point(); + + int[][] samples = { + {0, 0}, + {100, 50}, + {359, 0}, + {0, 179}, + {359, 179} + }; + + for (int[] s : samples) { + int x = s[0]; + int y = s[1]; + + GeoPos geo = proj.gridToGeoPos(x, y); + boolean inside = proj.geoPosToGrid(geo, p); + + assertTrue("Point should be inside grid", inside); + assertEquals("x mismatch", x, p.x); + assertEquals("y mismatch", y, p.y); + } + } + + @Test + public void testGeoPosToGrid_outside() { + Point p = new Point(); + + assertFalse(proj.geoPosToGrid(new GeoPos(0f, -181f), p)); + assertFalse(proj.geoPosToGrid(new GeoPos(0f, 181f), p)); + + assertFalse(proj.geoPosToGrid(new GeoPos(91f, 0f), p)); + assertFalse(proj.geoPosToGrid(new GeoPos(-91f, 0f), p)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGridToGeoPos_outOfRange_throws() { + proj.gridToGeoPos(-1, 0); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NcUtilTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NcUtilTest.java new file mode 100644 index 000000000..e7c113dc1 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NcUtilTest.java @@ -0,0 +1,63 @@ +package eu.esa.snap.cimr.netcdf; + +import org.junit.Test; +import ucar.ma2.DataType; +import ucar.nc2.Group; +import ucar.nc2.NetcdfFile; +import ucar.nc2.Variable; + +import static org.junit.Assert.*; + + +public class NcUtilTest { + + @Test + public void testFindGroupOrThrow_Found() { + Group.Builder rootBuilder = Group.builder(null).setName("root"); + Group.Builder childBuilder = Group.builder(rootBuilder).setName("child"); + rootBuilder.addGroup(childBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + Group g = NcUtil.findGroupOrThrow(ncFile, "/child"); + assertNotNull(g); + assertEquals("child", g.getShortName()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFindGroupOrThrow_NotFound() { + Group.Builder rootBuilder = Group.builder(null).setName("root"); + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + NcUtil.findGroupOrThrow(ncFile, "/does/not/exist"); + } + + @Test + public void testFindVarOrThrow_Found() { + Group.Builder groupBuilder = Group.builder(null).setName("g"); + Variable.Builder varBuilder = Variable.builder() + .setName("v") + .setDataType(DataType.DOUBLE); + groupBuilder.addVariable(varBuilder); + + Group group = groupBuilder.build(null); + + Variable v = NcUtil.findVarOrThrow(group, "v"); + assertNotNull(v); + assertEquals("v", v.getShortName()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFindVarOrThrow_NotFound() { + Group.Builder groupBuilder = Group.builder(null).setName("g"); + Group group = groupBuilder.build(null); + + NcUtil.findVarOrThrow(group, "missing"); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrBandFactoryTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrBandFactoryTest.java new file mode 100644 index 000000000..39f312536 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrBandFactoryTest.java @@ -0,0 +1,227 @@ +package eu.esa.snap.cimr.netcdf; + + +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDescriptorKind; +import eu.esa.snap.cimr.cimr.CimrDimensions; +import eu.esa.snap.cimr.cimr.CimrFrequencyBand; +import eu.esa.snap.cimr.grid.CimrGeometryBand; +import eu.esa.snap.cimr.grid.CimrTiepointGeometry; +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; +import ucar.ma2.ArrayDouble; +import ucar.ma2.DataType; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.*; + +import java.io.IOException; + +import static org.junit.Assert.*; + + +public class NetcdfCimrBandFactoryTest { + + + private static final double doubleErr = 1e-6; + + + @Test + public void testCreateGeometryBand_NormalVariable() throws IOException, InvalidRangeException { + int nScans = 2; + int nSamples = 3; + int nFeeds = 1; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension sampleDim = new Dimension("n_samples_C_BAND", nSamples); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + + Group.Builder root = Group.builder(null).setName("root"); + root.addDimension(scanDim).addDimension(sampleDim).addDimension(feedDim); + + Group.Builder dataGroup = Group.builder(root).setName("Data"); + root.addGroup(dataGroup); + + ArrayDouble.D3 data = new ArrayDouble.D3(nScans, nSamples, nFeeds); + for (int s = 0; s < nScans; s++) { + for (int smp = 0; smp < nSamples; smp++) { + data.set(s, smp, 0, 100 * s + smp); + } + } + + Variable.Builder varBuilder = Variable.builder() + .setName("altitude") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_samples_C_BAND n_feeds_C_BAND") + .setCachedData(data, false); + dataGroup.addVariable(varBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(root) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + + CimrBandDescriptor desc = new CimrBandDescriptor( + "altitude", + "altitude", + CimrFrequencyBand.C_BAND, + new String[] {}, + new String[] {}, + "/Data", + 0, + CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", + "", + "" + ); + + GeoPos[][][] tp = new GeoPos[nScans][nSamples][1]; + for (int s = 0; s < nScans; s++) { + for (int smp = 0; smp < nSamples; smp++) { + tp[s][smp][0] = new GeoPos(0f, 0f); + } + } + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, nSamples); + + NetcdfCimrBandFactory factory = new NetcdfCimrBandFactory(ncFile, dims); + CimrGeometryBand band = factory.createGeometryBand(desc, geom); + + assertEquals(nScans, band.getScanCount()); + assertEquals(nSamples, band.getSampleCount()); + + assertEquals(0.0, band.getValue(0, 0), doubleErr); + assertEquals(2.0, band.getValue(0, 2), doubleErr); + assertEquals(100.0, band.getValue(1, 0), doubleErr); + assertEquals(102.0, band.getValue(1, 2), doubleErr); + } + + @Test + public void testCreateGeometryBand_TiepointVariableInterpolates() throws IOException, InvalidRangeException { + int nScans = 1; + int nTiepoints = 2; + int nFeeds = 1; + int nSamplesOut = 4; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension tpDim = new Dimension("n_tiepoints_C_BAND", nTiepoints); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + Dimension samplesDim = new Dimension("n_samples_C_BAND", nSamplesOut); + + Group.Builder root = Group.builder(null).setName("root"); + root.addDimension(scanDim) + .addDimension(tpDim) + .addDimension(feedDim) + .addDimension(samplesDim); + + Group.Builder dataGroup = Group.builder(root).setName("Data"); + root.addGroup(dataGroup); + + ArrayDouble.D3 data = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + data.set(0, 0, 0, 0.0); + data.set(0, 1, 0, 10.0); + + Variable.Builder varBuilder = Variable.builder() + .setName("tie_var") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(data, false); + dataGroup.addVariable(varBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(root) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + + CimrBandDescriptor desc = new CimrBandDescriptor( + "tie_var", + "tie_var", + CimrFrequencyBand.C_BAND, + new String[]{}, + new String[] {}, + "/Data", + 0, + CimrDescriptorKind.TIEPOINT_VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", + "", + "" + ); + + GeoPos[][][] tp = new GeoPos[nScans][nTiepoints][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + tp[0][1][0] = new GeoPos(0f, 10f); + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, nSamplesOut); + + NetcdfCimrBandFactory factory = new NetcdfCimrBandFactory(ncFile, dims); + CimrGeometryBand band = factory.createGeometryBand(desc, geom); + + assertEquals(nScans, band.getScanCount()); + assertEquals(nSamplesOut, band.getSampleCount()); + + assertEquals(0.0, band.getValue(0, 0), doubleErr); + assertEquals(10.0, band.getValue(0, 3), doubleErr); + assertEquals(10.0 / 3, band.getValue(0, 1), doubleErr); + assertEquals(20.0 / 3, band.getValue(0, 2), doubleErr); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateGeometryBand_FailsForNon3DVariable() throws IOException, InvalidRangeException { + int nScans = 1; + int nSamples = 2; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension sampleDim = new Dimension("n_samples_C_BAND", nSamples); + Dimension feedDim = new Dimension("n_feeds_C_BAND", 1); + + Group.Builder root = Group.builder(null).setName("root"); + root.addDimension(scanDim).addDimension(sampleDim).addDimension(feedDim); + + Group.Builder dataGroup = Group.builder(root).setName("Data"); + root.addGroup(dataGroup); + + ArrayDouble.D2 data = new ArrayDouble.D2(nScans, nSamples); + data.set(0, 0, 1.0); + data.set(0, 1, 2.0); + + Variable.Builder varBuilder = Variable.builder() + .setName("bad_var") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_samples_C_BAND") + .setCachedData(data, false); + dataGroup.addVariable(varBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(root) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + + CimrBandDescriptor desc = new CimrBandDescriptor( + "bad_var", + "bad_var", + CimrFrequencyBand.C_BAND, + new String[]{}, + new String[] {}, + "/Data", + 0, + CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", + "", + "" + ); + + GeoPos[][][] tp = new GeoPos[1][2][1]; + tp[0][0][0] = new GeoPos(0f, 0f); + tp[0][1][0] = new GeoPos(0f, 1f); + CimrTiepointGeometry geom = new CimrTiepointGeometry(tp, nSamples); + + NetcdfCimrBandFactory factory = new NetcdfCimrBandFactory(ncFile, dims); + factory.createGeometryBand(desc, geom); + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrFootprintFactoryTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrFootprintFactoryTest.java new file mode 100644 index 000000000..e71a81fc4 --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrFootprintFactoryTest.java @@ -0,0 +1,141 @@ +package eu.esa.snap.cimr.netcdf; + +import eu.esa.snap.cimr.cimr.CimrFootprintShape; +import eu.esa.snap.cimr.grid.CimrGeometryBand; +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class NetcdfCimrFootprintFactoryTest { + + private static final double EPS = 1e-9; + + @Test + public void testCreateFootprints_populatesAllScanSampleCombinations() { + CimrGeometryBand geometryBand = mock(CimrGeometryBand.class); + CimrGeometryBand minorAxisBand = mock(CimrGeometryBand.class); + CimrGeometryBand majorAxisBand = mock(CimrGeometryBand.class); + CimrGeometryBand angleBand = mock(CimrGeometryBand.class); + + int scans = 2; + int samples = 3; + + when(geometryBand.getScanCount()).thenReturn(scans); + when(geometryBand.getSampleCount()).thenReturn(samples); + + when(geometryBand.getGeoPos(anyInt(), anyInt())).thenAnswer(inv -> { + int s = inv.getArgument(0); + int t = inv.getArgument(1); + return new GeoPos((float) (10 + s), (float) (20 + t)); + }); + + when(geometryBand.getValue(anyInt(), anyInt())).thenAnswer(inv -> { + int s = inv.getArgument(0); + int t = inv.getArgument(1); + return 100.0 + 10.0 * s + t; + }); + + when(angleBand.getValue(anyInt(), anyInt())).thenAnswer(inv -> { + int s = inv.getArgument(0); + int t = inv.getArgument(1); + return 1.0 * s + 0.1 * t; + }); + + when(minorAxisBand.getValue(anyInt(), anyInt())).thenAnswer(inv -> { + int s = inv.getArgument(0); + int t = inv.getArgument(1); + return 1000.0 + s + t; + }); + + when(majorAxisBand.getValue(anyInt(), anyInt())).thenAnswer(inv -> { + int s = inv.getArgument(0); + int t = inv.getArgument(1); + return 2000.0 + 2.0 * s + 3.0 * t; + }); + + NetcdfCimrFootprintFactory factory = new NetcdfCimrFootprintFactory(); + + + List footprints = factory.createFootprintShapes( + geometryBand, minorAxisBand, majorAxisBand, angleBand); + + + assertEquals(scans * samples, footprints.size()); + + int idx = 0; + for (int s = 0; s < scans; s++) { + for (int t = 0; t < samples; t++) { + CimrFootprintShape fp = footprints.get(idx++); + + assertEquals(10.0 + s, fp.getGeoPos().getLat(), EPS); + assertEquals(20.0 + t, fp.getGeoPos().getLon(), EPS); + + double expectedAngle = 1.0 * s + 0.1 * t; + double expectedMinorAxis = 1000.0 + s + t; + + assertEquals(expectedAngle, fp.getAngle(), EPS); + assertEquals(expectedMinorAxis, fp.getMinorAxisDegree() * 111320.0, 1e-6 * 111320.0); + } + } + } + + + @Test + public void testCreateFootprints_emptyGeometryBandReturnsEmptyList() { + CimrGeometryBand geometryBand = mock(CimrGeometryBand.class); + CimrGeometryBand minorAxisBand = mock(CimrGeometryBand.class); + CimrGeometryBand majorAxisBand = mock(CimrGeometryBand.class); + CimrGeometryBand angleBand = mock(CimrGeometryBand.class); + + when(geometryBand.getScanCount()).thenReturn(0); + when(geometryBand.getSampleCount()).thenReturn(0); + + NetcdfCimrFootprintFactory factory = new NetcdfCimrFootprintFactory(); + + List footprints = factory.createFootprintShapes( + geometryBand, minorAxisBand, majorAxisBand, angleBand); + + assertNotNull(footprints); + assertTrue(footprints.isEmpty()); + } + + @Test + public void testgetFootprintValues() { + CimrGeometryBand geometryBand = mock(CimrGeometryBand.class); + + int scans = 2; + int samples = 3; + + when(geometryBand.getScanCount()).thenReturn(scans); + when(geometryBand.getSampleCount()).thenReturn(samples); + + when(geometryBand.getValue(anyInt(), anyInt())).thenAnswer(inv -> { + int s = inv.getArgument(0); + int t = inv.getArgument(1); + return 100.0 + 10.0 * s + t; + }); + + NetcdfCimrFootprintFactory factory = new NetcdfCimrFootprintFactory(); + + List values = factory.getFootprintValues(geometryBand); + + assertEquals(scans * samples, values.size()); + + int idx = 0; + for (int s = 0; s < scans; s++) { + for (int t = 0; t < samples; t++) { + double value = values.get(idx++); + + double expectedValue = 100.0 + 10.0 * s + t; + assertEquals(expectedValue, value, EPS); + } + } + } +} \ No newline at end of file diff --git a/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrGeometryFactoryTest.java b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrGeometryFactoryTest.java new file mode 100644 index 000000000..cb5eb765b --- /dev/null +++ b/cimr-reader/src/test/java/eu/esa/snap/cimr/netcdf/NetcdfCimrGeometryFactoryTest.java @@ -0,0 +1,462 @@ +package eu.esa.snap.cimr.netcdf; + + +import eu.esa.snap.cimr.cimr.CimrBandDescriptor; +import eu.esa.snap.cimr.cimr.CimrDimensions; +import eu.esa.snap.cimr.cimr.CimrFrequencyBand; +import eu.esa.snap.cimr.cimr.CimrDescriptorKind; +import eu.esa.snap.cimr.grid.CimrBoundingBox; +import eu.esa.snap.cimr.grid.CimrGeometry; +import org.esa.snap.core.datamodel.GeoPos; +import org.junit.Test; +import ucar.ma2.ArrayDouble; +import ucar.ma2.DataType; +import ucar.ma2.InvalidRangeException; +import ucar.nc2.Dimension; +import ucar.nc2.Group; +import ucar.nc2.NetcdfFile; +import ucar.nc2.Variable; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + + +import static org.junit.Assert.*; + + +public class NetcdfCimrGeometryFactoryTest { + + private static final double doubleErr = 1e-6; + + + @Test + public void testGetOrCreateGeometry_BuildsGeometryAndCaches() throws IOException, InvalidRangeException { + int nScans = 2; + int nTiepoints = 2; + int nFeeds = 2; + int nSamplesOut = 4; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension tpDim = new Dimension("n_tiepoints_C_BAND", nTiepoints); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + Dimension samplesDim = new Dimension("n_samples_C_BAND", nSamplesOut); + + Group.Builder rootBuilder = Group.builder(null).setName("root"); + rootBuilder.addDimension(scanDim) + .addDimension(tpDim) + .addDimension(feedDim) + .addDimension(samplesDim); + + ArrayDouble.D3 latData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + ArrayDouble.D3 lonData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + for (int s = 0; s < nScans; s++) { + for (int tp = 0; tp < nTiepoints; tp++) { + latData.set(s, tp, 0, 10.0 * s); + lonData.set(s, tp, 0, 100.0 * tp); + } + } + + Variable.Builder latBuilder = Variable.builder() + .setName("lat_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(latData, false); + Variable.Builder lonBuilder = Variable.builder() + .setName("lon_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(lonData, false); + + rootBuilder.addVariable(latBuilder); + rootBuilder.addVariable(lonBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + String rootPath = "/"; + + CimrBandDescriptor latDesc = new CimrBandDescriptor( + "lat_c", "lat_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {""}, + rootPath, + 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor lonDesc = new CimrBandDescriptor( + "lon_c", "lon_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {""}, + rootPath, + 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + new String[]{"lat_c", "lon_c"}, new String[] {""}, + rootPath, + 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + NetcdfCimrGeometryFactory factory = new NetcdfCimrGeometryFactory(ncFile, Arrays.asList(latDesc, lonDesc), dims); + + CimrGeometry geom1 = factory.getOrCreateGeometry(varDesc); + assertNotNull(geom1); + assertEquals(nScans, geom1.getScanCount()); + assertEquals(nSamplesOut, geom1.getSampleCount()); + + GeoPos g0_start = geom1.getGeoPos(0, 0, 0); + GeoPos g0_end = geom1.getGeoPos(0, nSamplesOut - 1, 0); + assertEquals(0.0, g0_start.getLat(), doubleErr); + assertEquals(0.0, g0_start.getLon(), doubleErr); + assertEquals(0.0, g0_end.getLat(), doubleErr); + assertEquals(100.0, g0_end.getLon(), doubleErr); + + GeoPos g1_start = geom1.getGeoPos(1, 0, 0); + GeoPos g1_end = geom1.getGeoPos(1, nSamplesOut - 1, 0); + assertEquals(10.0, g1_start.getLat(), doubleErr); + assertEquals(0.0, g1_start.getLon(), doubleErr); + assertEquals(10.0, g1_end.getLat(), doubleErr); + assertEquals(100.0, g1_end.getLon(), doubleErr); + + CimrGeometry geom2 = factory.getOrCreateGeometry(varDesc); + assertSame(geom1, geom2); + + assertEquals(4, geom2.getSampleCount()); + } + + @Test + public void testClearCacheCreatesNewInstance() throws IOException, InvalidRangeException { + int nScans = 2; + int nTiepoints = 2; + int nFeeds = 2; + int nSamplesOut = 4; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension tpDim = new Dimension("n_tiepoints_C_BAND", nTiepoints); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + Dimension samplesDim = new Dimension("n_samples_C_BAND", nSamplesOut); + + Group.Builder rootBuilder = Group.builder(null).setName("root"); + rootBuilder.addDimension(scanDim) + .addDimension(tpDim) + .addDimension(feedDim) + .addDimension(samplesDim); + + ArrayDouble.D3 latData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + ArrayDouble.D3 lonData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + latData.set(0, 0, 0, 42.0); + lonData.set(0, 0, 0, 7.0); + + Variable.Builder latBuilder = Variable.builder() + .setName("lat_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(latData, false); + Variable.Builder lonBuilder = Variable.builder() + .setName("lon_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(lonData, false); + + rootBuilder.addVariable(latBuilder); + rootBuilder.addVariable(lonBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + String rootPath = "/"; + + CimrBandDescriptor latDesc = new CimrBandDescriptor( + "lat_c", "lat_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor lonDesc = new CimrBandDescriptor( + "lon_c", "lon_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + new String[]{"lat_c", "lon_c"}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + NetcdfCimrGeometryFactory factory = new NetcdfCimrGeometryFactory(ncFile, Arrays.asList(latDesc, lonDesc), dims); + + CimrGeometry g1 = factory.getOrCreateGeometry(varDesc); + factory.clearCache(); + CimrGeometry g2 = factory.getOrCreateGeometry(varDesc); + + assertNotSame(g1, g2); + GeoPos p = g2.getGeoPos(0, 0, 0); + assertEquals(42.0, p.getLat(), doubleErr); + assertEquals(7.0, p.getLon(), doubleErr); + } + + @Test(expected = IllegalStateException.class) + public void testGetOrCreateGeometry_FailsForInvalidGeometryNames() throws IOException, InvalidRangeException { + Group.Builder rootBuilder = Group.builder(null).setName("root"); + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + + NetcdfCimrGeometryFactory factory = new NetcdfCimrGeometryFactory(ncFile, Collections.emptyList(), dims); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + null, new String[] {}, + "root", 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + factory.getOrCreateGeometry(varDesc); + } + + @Test(expected = IllegalStateException.class) + public void testGetOrCreateGeometry_FailsWhenGeometryDescriptorsMissing() throws IOException, InvalidRangeException { + int nScans = 2; + int nTiepoints = 2; + int nFeeds = 2; + int nSamplesOut = 4; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension tpDim = new Dimension("n_tiepoints_C_BAND", nTiepoints); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + Dimension samplesDim = new Dimension("n_samples_C_BAND", nSamplesOut); + + Group.Builder rootBuilder = Group.builder(null).setName("root"); + rootBuilder.addDimension(scanDim) + .addDimension(tpDim) + .addDimension(feedDim) + .addDimension(samplesDim); + + ArrayDouble.D3 latData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + ArrayDouble.D3 lonData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + latData.set(0, 0, 0, 0.0); + lonData.set(0, 0, 0, 0.0); + + Variable.Builder latBuilder = Variable.builder() + .setName("lat_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(latData, false); + Variable.Builder lonBuilder = Variable.builder() + .setName("lon_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(lonData, false); + + rootBuilder.addVariable(latBuilder); + rootBuilder.addVariable(lonBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + String rootPath = ncFile.getRootGroup().getShortName(); + + CimrBandDescriptor latDesc = new CimrBandDescriptor( + "lat_c", "lat_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + new String[]{"lat_c", "lon_c"}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + NetcdfCimrGeometryFactory factory = new NetcdfCimrGeometryFactory(ncFile, Collections.singletonList(latDesc), dims); + + factory.getOrCreateGeometry(varDesc); + } + + @Test + public void testGetOrCreateGeometry_UsesVariableFeedIndex() throws IOException, InvalidRangeException { + int nScans = 2; + int nTiepoints = 2; + int nFeeds = 2; + int nSamplesOut = 4; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension tpDim = new Dimension("n_tiepoints_C_BAND", nTiepoints); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + Dimension samplesDim = new Dimension("n_samples_C_BAND", nSamplesOut); + + Group.Builder rootBuilder = Group.builder(null).setName("root"); + rootBuilder.addDimension(scanDim) + .addDimension(tpDim) + .addDimension(feedDim) + .addDimension(samplesDim); + + ArrayDouble.D3 latData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + ArrayDouble.D3 lonData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + latData.set(0, 0, 0, 1.0); + lonData.set(0, 0, 0, 2.0); + latData.set(0, 0, 1, 10.0); + lonData.set(0, 0, 1, 20.0); + + Variable.Builder latBuilder = Variable.builder() + .setName("lat_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(latData, false); + Variable.Builder lonBuilder = Variable.builder() + .setName("lon_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(lonData, false); + + rootBuilder.addVariable(latBuilder); + rootBuilder.addVariable(lonBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + String rootPath = "/"; + + CimrBandDescriptor latDesc = new CimrBandDescriptor( + "lat_c", "lat_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor lonDesc = new CimrBandDescriptor( + "lon_c", "lon_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + new String[]{"lat_c", "lon_c"}, new String[] {}, + rootPath, 1, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + NetcdfCimrGeometryFactory factory = new NetcdfCimrGeometryFactory(ncFile, Arrays.asList(latDesc, lonDesc), dims); + + CimrGeometry geom = factory.getOrCreateGeometry(varDesc); + GeoPos p = geom.getGeoPos(0, 0, 0); + assertEquals(10.0, p.getLat(), doubleErr); + assertEquals(20.0, p.getLon(), doubleErr); + } + + + @Test + public void testGetBoundingBox() throws IOException, InvalidRangeException { + int nScans = 2; + int nTiepoints = 2; + int nFeeds = 2; + int nSamplesOut = 4; + + Dimension scanDim = new Dimension("n_scans", nScans); + Dimension tpDim = new Dimension("n_tiepoints_C_BAND", nTiepoints); + Dimension feedDim = new Dimension("n_feeds_C_BAND", nFeeds); + Dimension samplesDim = new Dimension("n_samples_C_BAND", nSamplesOut); + + Group.Builder rootBuilder = Group.builder(null).setName("root"); + rootBuilder.addDimension(scanDim) + .addDimension(tpDim) + .addDimension(feedDim) + .addDimension(samplesDim); + + ArrayDouble.D3 latData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + ArrayDouble.D3 lonData = new ArrayDouble.D3(nScans, nTiepoints, nFeeds); + latData.set(0, 0, 0, 1.0); + lonData.set(0, 0, 0, 2.0); + latData.set(0, 0, 1, 10.0); + lonData.set(0, 0, 1, 20.0); + + Variable.Builder latBuilder = Variable.builder() + .setName("lat_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(latData, false); + Variable.Builder lonBuilder = Variable.builder() + .setName("lon_c") + .setDataType(DataType.DOUBLE) + .setDimensionsByName("n_scans n_tiepoints_C_BAND n_feeds_C_BAND") + .setCachedData(lonData, false); + + rootBuilder.addVariable(latBuilder); + rootBuilder.addVariable(lonBuilder); + + NetcdfFile ncFile = NetcdfFile.builder() + .setLocation("test") + .setRootGroup(rootBuilder) + .build(); + + CimrDimensions dims = CimrDimensions.from(ncFile); + String rootPath = "/"; + + CimrBandDescriptor latDesc = new CimrBandDescriptor( + "lat_c", "lat_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + CimrBandDescriptor lonDesc = new CimrBandDescriptor( + "lon_c", "lon_c", CimrFrequencyBand.C_BAND, + new String[]{}, new String[] {}, + rootPath, 0, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_tiepoints_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + CimrBandDescriptor varDesc = new CimrBandDescriptor( + "altitude", "altitude", CimrFrequencyBand.C_BAND, + new String[]{"lat_c", "lon_c"}, new String[] {}, + rootPath, 1, CimrDescriptorKind.VARIABLE, + new String[]{"n_scans", "n_samples_C_BAND", "n_feeds_C_BAND"}, + "double", "", "" + ); + + NetcdfCimrGeometryFactory factory = new NetcdfCimrGeometryFactory(ncFile, Arrays.asList(latDesc, lonDesc), dims); + + CimrBoundingBox bBox = factory.getBoundingBox(varDesc, 0.02); + + assertEquals(-0.5, bBox.getLatMin(), doubleErr); + assertEquals(-0.5, bBox.getLonMin(), doubleErr); + assertEquals(10.5, bBox.getLatMax(), doubleErr); + assertEquals(20.5, bBox.getLonMax(), doubleErr); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index c4eca328a..d54f47af5 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,8 @@ + cimr-reader + cimr-reader-ui jlinda rstb sar-cloud