Skip to content

Commit baac2f6

Browse files
committed
Unscale false_easting/false_northing when writing Coverage
When writing a CF netCDF file from a Coverage dataset, make sure to unscale false_easting/false_northing, if needed. Fixes #1375.
1 parent 4bc9c9c commit baac2f6

File tree

3 files changed

+176
-5
lines changed

3 files changed

+176
-5
lines changed

cdm-test/src/test/java/ucar/nc2/dt/grid/TestCFWriter2.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
/*
2-
* Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
2+
* Copyright (c) 1998-2025 University Corporation for Atmospheric Research/Unidata
33
* See LICENSE for license information.
44
*/
55

66
package ucar.nc2.dt.grid;
77

8+
import static com.google.common.truth.Truth.assertThat;
9+
import static org.junit.Assert.assertNotNull;
10+
import static org.junit.Assert.assertTrue;
11+
812
import org.junit.Ignore;
913
import org.junit.Rule;
1014
import org.junit.Test;
1115
import org.junit.experimental.categories.Category;
1216
import org.junit.rules.TemporaryFolder;
1317
import org.slf4j.Logger;
1418
import org.slf4j.LoggerFactory;
19+
import ucar.nc2.Attribute;
1520
import ucar.nc2.NetcdfFile;
1621
import ucar.nc2.NetcdfFileWriter;
1722
import ucar.nc2.NetcdfFiles;
1823
import ucar.nc2.Variable;
24+
import ucar.nc2.constants.CF;
1925
import ucar.nc2.grib.collection.Grib;
2026
import ucar.nc2.time.CalendarDate;
2127
import ucar.nc2.time.CalendarDateRange;
2228
import ucar.nc2.util.DebugFlagsImpl;
2329
import ucar.unidata.geoloc.LatLonPoint;
2430
import ucar.unidata.geoloc.LatLonRect;
2531
import ucar.unidata.geoloc.ProjectionPoint;
26-
import ucar.unidata.geoloc.ProjectionPointImpl;
2732
import ucar.unidata.geoloc.ProjectionRect;
2833
import ucar.unidata.util.test.category.NeedsCdmUnitTest;
2934
import ucar.unidata.util.test.TestDir;
3035
import java.lang.invoke.MethodHandles;
3136
import java.util.ArrayList;
3237
import java.util.Collections;
3338
import java.util.List;
34-
import static org.junit.Assert.assertTrue;
3539

3640
/**
3741
* Test CFGridWriter2
@@ -256,6 +260,51 @@ private void testFileSize(String fileIn, String gridNames, String startDate, Str
256260
}
257261
}
258262

263+
@Test
264+
@Category(NeedsCdmUnitTest.class)
265+
public void testAxisProjUnitMismatch() throws Exception {
266+
String fileOut = tempFolder.newFile().getAbsolutePath();
267+
String varName = "HNW";
268+
269+
final double falseEastingMeters = 400000.0;
270+
final double falseEastingKm = falseEastingMeters / 1000;
271+
272+
try (GridDataset gds = GridDataset.open(TestDir.cdmUnitTestDir + "ncss/test/falseEastingNorthingScaleReset.nc4")) {
273+
Variable x = gds.getNetcdfFile().findVariable("x");
274+
assertNotNull(x);
275+
Attribute xUnits = x.findAttribute(CF.UNITS);
276+
assertNotNull(xUnits);
277+
assertThat(xUnits.getStringValue()).isEqualTo("m");
278+
279+
Variable proj = gds.getNetcdfFile().findVariable("lambert_conformal_conic");
280+
assertNotNull(proj);
281+
Attribute feAttr = proj.findAttribute(CF.FALSE_EASTING);
282+
assertNotNull(feAttr);
283+
// false_easting in meters
284+
assertThat(feAttr.getNumericValue()).isEqualTo(falseEastingMeters);
285+
286+
// setup subset
287+
List<String> gridList = new ArrayList<>();
288+
gridList.add(varName);
289+
290+
NetcdfFileWriter writer = NetcdfFileWriter.createNew(NetcdfFileWriter.Version.netcdf3, fileOut);
259291

292+
// write subset
293+
CFGridWriter2.writeFile(gds, gridList, null, null, 1, null, null, 1, true, writer);
294+
}
260295

296+
try (NetcdfFile ncf = NetcdfFiles.open(fileOut)) {
297+
Variable proj = ncf.findVariable("lambert_conformal_conic");
298+
assertNotNull(proj);
299+
Attribute feAttr = proj.findAttribute(CF.FALSE_EASTING);
300+
assertNotNull(feAttr);
301+
// GeoX in km
302+
Variable x = ncf.findVariable("x");
303+
Attribute xUnits = x.findAttribute(CF.UNITS);
304+
assertNotNull(xUnits);
305+
assertThat(xUnits.getStringValue()).isEqualTo("km");
306+
// false_easting in km
307+
assertThat(feAttr.getNumericValue()).isEqualTo(falseEastingKm);
308+
}
309+
}
261310
}

cdm/core/src/main/java/ucar/nc2/ft2/coverage/writer/CFGridCoverageWriter.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/*
2-
* Copyright (c) 1998-2020 John Caron and University Corporation for Atmospheric Research/Unidata
2+
* Copyright (c) 1998-2025 John Caron and University Corporation for Atmospheric Research/Unidata
33
* See LICENSE for license information.
44
*/
5+
56
package ucar.nc2.ft2.coverage.writer;
67

78
import com.google.common.base.Preconditions;
@@ -13,13 +14,15 @@
1314
import java.util.List;
1415
import java.util.Map;
1516
import java.util.Optional;
17+
import java.util.stream.Collectors;
1618
import javax.annotation.Nullable;
1719
import ucar.ma2.Array;
1820
import ucar.ma2.DataType;
1921
import ucar.ma2.InvalidRangeException;
2022
import ucar.ma2.Section;
2123
import ucar.nc2.Attribute;
2224
import ucar.nc2.AttributeContainer;
25+
import ucar.nc2.AttributeContainerMutable;
2326
import ucar.nc2.Dimension;
2427
import ucar.nc2.Group;
2528
import ucar.nc2.Variable;
@@ -28,6 +31,7 @@
2831
import ucar.nc2.constants.CDM;
2932
import ucar.nc2.constants.CF;
3033
import ucar.nc2.constants._Coordinate;
34+
import ucar.nc2.dataset.transform.AbstractTransformBuilder;
3135
import ucar.nc2.ft2.coverage.Coverage;
3236
import ucar.nc2.ft2.coverage.CoverageCollection;
3337
import ucar.nc2.ft2.coverage.CoverageCoordAxis;
@@ -308,8 +312,53 @@ private void addCoordTransforms(CoverageCollection subsetDataset, Group.Builder
308312
// scalar coordinate transform variable - container for transform info
309313
Variable.Builder ctv = Variable.builder().setName(ct.getName()).setDataType(DataType.INT);
310314
group.addVariable(ctv);
311-
ctv.addAttributes(ct.attributes());
315+
AttributeContainer ctAttrs = ct.attributes();
316+
317+
AttributeContainerMutable newAttrs = AttributeContainerMutable.copyFrom(ctAttrs);
318+
// adjust false_easting/false_northing if needed
319+
// possibly needed if the subset dataset includes GeoX or GeoY axes, so first find those
320+
Map<AxisType, CoverageCoordAxis> mapCoordAxes = subsetDataset.getCoordAxes().stream()
321+
.filter(ca -> ca.getAxisType() == AxisType.GeoX || ca.getAxisType() == AxisType.GeoY)
322+
.collect(Collectors.toMap(CoverageCoordAxis::getAxisType, mca -> mca));
323+
// if we found GeoX and/or GeoY axes, start checking
324+
if (!mapCoordAxes.isEmpty()) {
325+
boolean eastScaled = false;
326+
if (mapCoordAxes.containsKey(AxisType.GeoX)) {
327+
eastScaled = scaleFalseEastingNorthing(CF.FALSE_EASTING, ctAttrs, newAttrs,
328+
mapCoordAxes.get(AxisType.GeoX).attributes().findAttributeString(CF.UNITS, null));
329+
}
330+
boolean northScaled = false;
331+
if (mapCoordAxes.containsKey(AxisType.GeoY)) {
332+
northScaled = scaleFalseEastingNorthing(CF.FALSE_NORTHING, ctAttrs, newAttrs,
333+
mapCoordAxes.get(AxisType.GeoY).attributes().findAttributeString(CF.UNITS, null));
334+
}
335+
// do not propagate the unit attribute on the projection variable if we ensured the
336+
// units match, as this is state matchs CF (likely this attribute was added when creating
337+
// a coverage from the NetcdfDataset.
338+
if (!ctAttrs.findAttributeString(CF.UNITS, "").isEmpty()) {
339+
if (eastScaled || northScaled) {
340+
newAttrs.removeAttribute(CF.UNITS);
341+
}
342+
}
343+
}
344+
ctv.addAttributes(newAttrs.toImmutable());
345+
}
346+
}
347+
348+
private boolean scaleFalseEastingNorthing(String falseValueType, AttributeContainer cta,
349+
AttributeContainerMutable newCta, String caUnits) {
350+
double falseValue = cta.findAttributeDouble(falseValueType, Double.MIN_VALUE);
351+
boolean scaled = false;
352+
if (falseValue != Double.MIN_VALUE) {
353+
double scalef = AbstractTransformBuilder.getFalseEastingScaleFactor(caUnits);
354+
if (scalef != 1.0) {
355+
scaled = true;
356+
newCta.removeAttribute(falseValueType);
357+
// here we divide by scalef, to undo the scalef applied when creating the Coverage
358+
newCta.addAttribute(Attribute.builder(falseValueType).setNumericValue(falseValue / scalef, false).build());
359+
}
312360
}
361+
return scaled;
313362
}
314363

315364
private void addLatLon2D(CoverageCollection subsetDataset, Group.Builder group) {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata
3+
* See LICENSE for license information.
4+
*/
5+
6+
package ucar.nc2.ft2.coverage.writer;
7+
8+
import static com.google.common.truth.Truth.assertThat;
9+
import static org.junit.Assert.assertNotNull;
10+
11+
import java.io.IOException;
12+
import org.junit.Rule;
13+
import org.junit.Test;
14+
import org.junit.experimental.categories.Category;
15+
import org.junit.rules.TemporaryFolder;
16+
import ucar.ma2.InvalidRangeException;
17+
import ucar.nc2.Attribute;
18+
import ucar.nc2.NetcdfFile;
19+
import ucar.nc2.NetcdfFiles;
20+
import ucar.nc2.Variable;
21+
import ucar.nc2.constants.CF;
22+
import ucar.nc2.constants.FeatureType;
23+
import ucar.nc2.ft2.coverage.CoverageCollection;
24+
import ucar.nc2.ft2.coverage.CoverageDatasetFactory;
25+
import ucar.nc2.ft2.coverage.FeatureDatasetCoverage;
26+
import ucar.nc2.write.NetcdfFileFormat;
27+
import ucar.nc2.write.NetcdfFormatWriter;
28+
import ucar.unidata.util.test.TestDir;
29+
import ucar.unidata.util.test.category.NeedsCdmUnitTest;
30+
31+
public class TestCFGridCoverageWriter {
32+
33+
@Rule
34+
public final TemporaryFolder tempFolder = new TemporaryFolder();
35+
36+
@Test
37+
@Category(NeedsCdmUnitTest.class)
38+
public void testCFGridCoverageWriterNonKmProjectionParams() throws IOException, InvalidRangeException {
39+
String fileOut = tempFolder.newFile().getAbsolutePath();
40+
String fileIn = TestDir.cdmUnitTestDir + "ncss/test/falseEastingNorthingScaleReset.nc4";
41+
42+
try (FeatureDatasetCoverage cc = CoverageDatasetFactory.open(fileIn)) {
43+
CoverageCollection gcs = cc.findCoverageDataset(FeatureType.GRID);
44+
assertNotNull(gcs);
45+
NetcdfFormatWriter.Builder writerb =
46+
NetcdfFormatWriter.builder().setNewFile(true).setFormat(NetcdfFileFormat.NETCDF3).setLocation(fileOut);
47+
48+
CFGridCoverageWriter.Result result = CFGridCoverageWriter.write(gcs, null, null, false, writerb, 0);
49+
if (!result.wasWritten()) {
50+
throw new InvalidRangeException("Error writing: " + result.getErrorMessage());
51+
}
52+
}
53+
54+
try (NetcdfFile ncf = NetcdfFiles.open(fileOut)) {
55+
Variable proj = ncf.findVariable("lambert_conformal_conic");
56+
assertNotNull(proj);
57+
Attribute feAttr = proj.findAttribute(CF.FALSE_EASTING);
58+
assertNotNull(feAttr);
59+
Attribute fnAttr = proj.findAttribute(CF.FALSE_NORTHING);
60+
assertNotNull(fnAttr);
61+
// GeoX axis in meters
62+
Variable x = ncf.findVariable("x");
63+
assertNotNull(x);
64+
assertThat(x.getUnitsString()).isEqualTo("m");
65+
// false_easting in m
66+
assertThat(feAttr.getNumericValue()).isEqualTo(400000.0);
67+
assertThat(fnAttr.getNumericValue()).isEqualTo(400000.0);
68+
// should not have a units attribute on the proj variable
69+
Attribute unitsAttr = proj.findAttribute(CF.UNITS);
70+
assertThat(unitsAttr).isNull();
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)