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