Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,25 @@ public interface CrsTransformerFactory extends Volatile2 {
Optional<CrsTransformer> getTransformer(EpsgCrs sourceCrs, EpsgCrs targetCrs);

Optional<CrsTransformer> getTransformer(EpsgCrs sourceCrs, EpsgCrs targetCrs, boolean force2d);

/**
* Returns a transformer that is selected for an area of interest. When several coordinate
* operations between the two coordinate reference systems are available, the most accurate one
* whose area of use contains the area of interest is chosen, e.g. a regional grid instead of a
* national one. Without an area of interest, or with an area that covers the whole world, the
* selection is the same as for {@link #getTransformer(EpsgCrs, EpsgCrs, boolean)}.
*
* @param areaOfInterest the area of interest, a box in any supported coordinate reference system
*/
Optional<CrsTransformer> getTransformer(
EpsgCrs sourceCrs, EpsgCrs targetCrs, boolean force2d, Optional<BoundingBox> areaOfInterest);

/**
* Returns a view of this factory that selects all transformers for the given area of interest,
* see {@link #getTransformer(EpsgCrs, EpsgCrs, boolean, Optional)}. All other methods delegate to
* this factory. If this factory also implements {@link CrsInfo}, so does the view.
*/
default CrsTransformerFactory withAreaOfInterest(BoundingBox areaOfInterest) {
return CrsTransformerFactoryForArea.of(this, areaOfInterest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2026 interactive instruments GmbH
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package de.ii.xtraplatform.crs.domain;

import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry.ChangeHandler;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import javax.measure.Unit;
import org.opengis.referencing.cs.AxisDirection;

/**
* A view of a {@link CrsTransformerFactory} that selects every transformer for a fixed area of
* interest. The view is not registered as a volatile itself, the delegate is.
*/
class CrsTransformerFactoryForArea implements CrsTransformerFactory {

private final CrsTransformerFactory delegate;
private final Optional<BoundingBox> areaOfInterest;

CrsTransformerFactoryForArea(CrsTransformerFactory delegate, BoundingBox areaOfInterest) {
this.delegate = Objects.requireNonNull(delegate);
this.areaOfInterest = Optional.of(areaOfInterest);
}

static CrsTransformerFactory of(CrsTransformerFactory delegate, BoundingBox areaOfInterest) {
return delegate instanceof CrsInfo
? new WithCrsInfo(delegate, areaOfInterest)
: new CrsTransformerFactoryForArea(delegate, areaOfInterest);
}

@Override
public boolean isSupported(EpsgCrs crs) {
return delegate.isSupported(crs);
}

@Override
public Optional<CrsTransformer> getTransformer(EpsgCrs sourceCrs, EpsgCrs targetCrs) {
return delegate.getTransformer(sourceCrs, targetCrs, false, areaOfInterest);
}

@Override
public Optional<CrsTransformer> getTransformer(
EpsgCrs sourceCrs, EpsgCrs targetCrs, boolean force2d) {
return delegate.getTransformer(sourceCrs, targetCrs, force2d, areaOfInterest);
}

/** An explicit area of interest takes precedence over the area of this view. */
@Override
public Optional<CrsTransformer> getTransformer(
EpsgCrs sourceCrs, EpsgCrs targetCrs, boolean force2d, Optional<BoundingBox> areaOfInterest) {
return delegate.getTransformer(
sourceCrs, targetCrs, force2d, areaOfInterest.or(() -> this.areaOfInterest));
}

@Override
public CrsTransformerFactory withAreaOfInterest(BoundingBox areaOfInterest) {
return of(delegate, areaOfInterest);
}

@Override
public String getUniqueKey() {
return delegate.getUniqueKey();
}

@Override
public Optional<String> getInstanceId() {
return delegate.getInstanceId();
}

@Override
public State getState() {
return delegate.getState();
}

@Override
public Optional<String> getMessage() {
return delegate.getMessage();
}

@Override
public Runnable onStateChange(ChangeHandler handler, boolean initialCall) {
return delegate.onStateChange(handler, initialCall);
}

/** The view for a delegate that also provides {@link CrsInfo}. */
static final class WithCrsInfo extends CrsTransformerFactoryForArea implements CrsInfo {

private final CrsInfo crsInfo;

WithCrsInfo(CrsTransformerFactory delegate, BoundingBox areaOfInterest) {
super(delegate, areaOfInterest);
this.crsInfo = (CrsInfo) delegate;
}

@Override
public boolean is3d(EpsgCrs crs) {
return crsInfo.is3d(crs);
}

@Override
public Unit<?> getUnit(EpsgCrs crs) {
return crsInfo.getUnit(crs);
}

@Override
public List<String> getAxisAbbreviations(EpsgCrs crs) {
return crsInfo.getAxisAbbreviations(crs);
}

@Override
public List<Unit<?>> getAxisUnits(EpsgCrs crs) {
return crsInfo.getAxisUnits(crs);
}

@Override
public List<Optional<Double>> getAxisMinimums(EpsgCrs crs) {
return crsInfo.getAxisMinimums(crs);
}

@Override
public List<Optional<Double>> getAxisMaximums(EpsgCrs crs) {
return crsInfo.getAxisMaximums(crs);
}

@Override
public OptionalInt getAxisWithWraparound(EpsgCrs crs) {
return crsInfo.getAxisWithWraparound(crs);
}

@Override
public List<AxisDirection> getAxisDirections(EpsgCrs crs) {
return crsInfo.getAxisDirections(crs);
}

@Override
public Optional<BoundingBox> getDomainOfValidity(EpsgCrs crs) {
return crsInfo.getDomainOfValidity(crs);
}

@Override
public List<Integer> getPrecisionList(EpsgCrs crs, Map<String, Integer> coordinatePrecision) {
return crsInfo.getPrecisionList(crs, coordinatePrecision);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import de.ii.xtraplatform.blobs.domain.ResourceStore;
import de.ii.xtraplatform.crs.domain.BoundingBox;
import de.ii.xtraplatform.crs.domain.CrsInfo;
import de.ii.xtraplatform.crs.domain.CrsTransformationException;
import de.ii.xtraplatform.crs.domain.CrsTransformer;
import de.ii.xtraplatform.crs.domain.CrsTransformerFactory;
import de.ii.xtraplatform.crs.domain.EpsgCrs;
Expand Down Expand Up @@ -86,8 +87,7 @@ public class CrsTransformerFactoryProj extends AbstractVolatile
private final ProjLoader projLoader;
private final ResourceStore projStore;
private final Map<EpsgCrs, CoordinateReferenceSystem> crsCache;
private final Map<EpsgCrs, Map<EpsgCrs, CrsTransformer>> transformerCache;
private final Map<EpsgCrs, Map<EpsgCrs, CrsTransformer>> transformerCacheForce2d;
private final Map<TransformerKey, CrsTransformer> transformerCache;
private final Lock projLock;
private final boolean useCaches;
private final boolean asyncStartup;
Expand All @@ -103,7 +103,6 @@ public CrsTransformerFactoryProj(
this.projStore = resourceStore.with("proj");
this.crsCache = new ConcurrentHashMap<>();
this.transformerCache = new ConcurrentHashMap<>();
this.transformerCacheForce2d = new ConcurrentHashMap<>();
this.projLock = new ReentrantLock();
this.useCaches = true;
this.asyncStartup = appContext.getConfiguration().getModules().isStartupAsync();
Expand All @@ -117,7 +116,6 @@ public CrsTransformerFactoryProj(
this.projStore = resourceStore.with("proj");
this.crsCache = new ConcurrentHashMap<>();
this.transformerCache = new ConcurrentHashMap<>();
this.transformerCacheForce2d = new ConcurrentHashMap<>();
this.projLock = new ReentrantLock();
this.useCaches = true;
this.asyncStartup = false;
Expand Down Expand Up @@ -355,6 +353,12 @@ public Optional<CrsTransformer> getTransformer(EpsgCrs sourceCrs, EpsgCrs target
@Override
public Optional<CrsTransformer> getTransformer(
EpsgCrs sourceCrs, EpsgCrs targetCrs, boolean force2d) {
return getTransformer(sourceCrs, targetCrs, force2d, Optional.empty());
}

@Override
public Optional<CrsTransformer> getTransformer(
EpsgCrs sourceCrs, EpsgCrs targetCrs, boolean force2d, Optional<BoundingBox> areaOfInterest) {
if (Objects.equals(sourceCrs, targetCrs)) {
return Optional.empty();
}
Expand All @@ -366,21 +370,58 @@ public Optional<CrsTransformer> getTransformer(
targetProjCrs = getHorizontalCrs(targetProjCrs);
}

Optional<BoundingBox> area = toGeographicArea(areaOfInterest);
CoordinateReferenceSystem finalSourceProjCrs = sourceProjCrs;
CoordinateReferenceSystem finalTargetProjCrs = targetProjCrs;
CrsTransformer transformer =
useCaches
? getCacheForSource(sourceCrs, force2d)
.computeIfAbsent(
targetCrs,
ignore ->
createCrsTransformer(
sourceCrs, targetCrs, finalSourceProjCrs, finalTargetProjCrs))
: createCrsTransformer(sourceCrs, targetCrs, sourceProjCrs, targetProjCrs);
? transformerCache.computeIfAbsent(
new TransformerKey(sourceCrs, targetCrs, force2d, area),
ignore ->
createCrsTransformer(
sourceCrs, targetCrs, finalSourceProjCrs, finalTargetProjCrs, area))
: createCrsTransformer(sourceCrs, targetCrs, sourceProjCrs, targetProjCrs, area);

return Optional.of(transformer);
}

/**
* Converts an area of interest to a 2D box in CRS84. An area that covers the whole world does not
* restrict the selection of operations and is dropped, so it shares the cache entry and the
* operation of a request without an area.
*/
private Optional<BoundingBox> toGeographicArea(Optional<BoundingBox> areaOfInterest) {
if (areaOfInterest.isEmpty()) {
return Optional.empty();
}

BoundingBox area = areaOfInterest.get();
if (!OgcCrs.CRS84.equals(area.getEpsgCrs()) && !OgcCrs.CRS84h.equals(area.getEpsgCrs())) {
try {
Optional<CrsTransformer> toCrs84 =
getTransformer(area.getEpsgCrs(), OgcCrs.CRS84, true, Optional.empty());
if (toCrs84.isPresent()) {
area = toCrs84.get().transformBoundingBox(area);
}
} catch (CrsTransformationException e) {
throw new IllegalArgumentException(
String.format("The area of interest cannot be converted to CRS84: %s", e.getMessage()),
e);
}
}

if (area.getXmin() <= -180
&& area.getYmin() <= -90
&& area.getXmax() >= 180
&& area.getYmax() >= 90) {
return Optional.empty();
}

return Optional.of(
BoundingBox.of(
area.getXmin(), area.getYmin(), area.getXmax(), area.getYmax(), OgcCrs.CRS84));
}

@Override
public List<Integer> getPrecisionList(EpsgCrs crs, Map<String, Integer> coordinatePrecision) {
ImmutableList.Builder<Integer> precisionListBuilder = new ImmutableList.Builder<>();
Expand Down Expand Up @@ -543,18 +584,13 @@ private CoordinateReferenceSystem applyAxisOrder(
return crs;
}

private Map<EpsgCrs, CrsTransformer> getCacheForSource(EpsgCrs crs, boolean force2d) {
return force2d
? transformerCacheForce2d.computeIfAbsent(crs, ignore -> new ConcurrentHashMap<>())
: transformerCache.computeIfAbsent(crs, ignore -> new ConcurrentHashMap<>());
}

@SuppressWarnings("PMD.CyclomaticComplexity")
private CrsTransformer createCrsTransformer(
EpsgCrs sourceCrs,
EpsgCrs targetCrs,
CoordinateReferenceSystem sourceProjCrs,
CoordinateReferenceSystem targetProjCrs) {
CoordinateReferenceSystem targetProjCrs,
Optional<BoundingBox> areaOfInterest) {

projLock.lock();
try {
Expand All @@ -568,12 +604,28 @@ private CrsTransformer createCrsTransformer(
coordinateOperationContext.setSpatialCriterion(SpatialCriterion.PARTIAL_INTERSECTION);
coordinateOperationContext.setGridAvailabilityUse(
GridAvailabilityUse.DISCARD_OPERATION_IF_MISSING_GRID);
areaOfInterest.ifPresent(
area ->
coordinateOperationContext.setAreaOfInterest(
area.getXmin(), area.getXmax(), area.getYmin(), area.getYmax()));
CoordinateOperation coordinateOperation =
Proj.createCoordinateOperation(sourceProjCrs, targetProjCrs, coordinateOperationContext);

checkForMissingGridFiles(coordinateOperation);

if (LOGGER.isDebugEnabled()) {
if (areaOfInterest.isPresent() && LOGGER.isInfoEnabled()) {
BoundingBox area = areaOfInterest.get();
LOGGER.info(
"Chosen operation for {} -> {} in area [{}, {}, {}, {}]: {}{}",
sourceCrs.toHumanReadableString(),
targetCrs.toHumanReadableString(),
area.getXmin(),
area.getYmin(),
area.getXmax(),
area.getYmax(),
coordinateOperation.getName().getCode(),
getGridFile(coordinateOperation).orElse(""));
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"Chosen operation for {} -> {}: {}{}",
sourceCrs.toHumanReadableString(),
Expand Down Expand Up @@ -648,4 +700,43 @@ private Optional<String> getGridFile(CoordinateOperation coordinateOperation) {
}
return Optional.empty();
}

/** Cache key of a transformer; the area of interest is a 2D box in CRS84 or empty. */
private static final class TransformerKey {
private final EpsgCrs sourceCrs;
private final EpsgCrs targetCrs;
private final boolean force2d;
private final Optional<BoundingBox> areaOfInterest;

TransformerKey(
EpsgCrs sourceCrs,
EpsgCrs targetCrs,
boolean force2d,
Optional<BoundingBox> areaOfInterest) {
this.sourceCrs = sourceCrs;
this.targetCrs = targetCrs;
this.force2d = force2d;
this.areaOfInterest = areaOfInterest;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TransformerKey)) {
return false;
}
TransformerKey that = (TransformerKey) o;
return force2d == that.force2d
&& sourceCrs.equals(that.sourceCrs)
&& targetCrs.equals(that.targetCrs)
&& areaOfInterest.equals(that.areaOfInterest);
}

@Override
public int hashCode() {
return Objects.hash(sourceCrs, targetCrs, force2d, areaOfInterest);
}
}
}
Loading
Loading