You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2023/02/13 16:01:18 UTC

[sis] 01/01: feat(Geometry): add parent interface for upcoming geometry API

This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch feat/geometry
in repository https://gitbox.apache.org/repos/asf/sis.git

commit f48d2d32b178be6f2ec71040c7331e97f101a7fe
Author: jsorel <jo...@geomatys.com>
AuthorDate: Mon Feb 13 17:00:27 2023 +0100

    feat(Geometry): add parent interface for upcoming geometry API
---
 .../sis/internal/geometry/AttributeType.java       |  92 +++++
 .../sis/internal/geometry/AttributesType.java      |  58 +++
 .../org/apache/sis/internal/geometry/Geometry.java | 158 ++++++++
 .../org/apache/sis/internal/math/DataType.java     | 430 +++++++++++++++++++++
 .../org/apache/sis/internal/math/SampleSystem.java | 152 ++++++++
 5 files changed, 890 insertions(+)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/AttributeType.java b/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/AttributeType.java
new file mode 100644
index 0000000000..4a5c880af1
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/AttributeType.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.geometry;
+
+import org.apache.sis.internal.math.DataType;
+import org.apache.sis.internal.math.SampleSystem;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public interface AttributeType {
+
+    /**
+     * This attribute correspond to SFA coordinates.
+     *
+     * GLTF position attribute.
+     * POSITION,VEC3
+     * Unitless XYZ vertex positions
+     */
+    String ATT_POSITION = "POSITION";
+    /**
+     * GLTF normal attribute.
+     * NORMAL,VEC3
+     * Normalized XYZ vertex normals
+     */
+    String ATT_NORMAL = "NORMAL";
+    /**
+     * GLTF tangent attribute.
+     * TANGENT,VEC4
+     * XYZW vertex tangents where the XYZ portion is normalized,
+     * and the W component is a sign value (-1 or +1) indicating handedness of the tangent basis
+     */
+    String ATT_TANGENT = "TANGENT";
+    /**
+     * GLTF indexed texture coordinate attribute.
+     * TEXCOORD_n,VEC2
+     * ST texture coordinates
+     */
+    String ATT_TEXCOORD = "TEXCOORD";
+    /**
+     * GLTF indexed color attribute.
+     * COLOR_n,VEC3/VEC4
+     * RGB or RGBA vertex color linear multiplier
+     */
+    String ATT_COLOR = "COLOR";
+    /**
+     * GLTF indexed joints attribute.
+     * JOINTS_n,VEC4
+     * Skinned Mesh Attribute
+     */
+    String ATT_JOINTS = "JOINTS";
+    /**
+     * GLTF indexed weights attribute.
+     * JOINTS_n,VEC4
+     * Skinned Mesh Attribute
+     */
+    String ATT_WEIGHTS = "WEIGHTS";
+
+    /**
+     * @return attribute name, not null.
+     */
+    String getName();
+
+    /**
+     * Returns attribute system for given name.
+     *
+     * @return system or null.
+     */
+    SampleSystem getSampleSystem();
+
+    /**
+     * Returns attribute type for given name.
+     *
+     * @return type or null.
+     */
+    DataType getDataType();
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/AttributesType.java b/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/AttributesType.java
new file mode 100644
index 0000000000..6968e30d56
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/AttributesType.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.geometry;
+
+import java.util.Collection;
+import org.opengis.feature.PropertyNotFoundException;
+
+/**
+ * An attributesType is a description of geometry attributes.
+ * <p>
+ * Based on specification :
+ * <ul>
+ *  <li>OGC Simple Feature Access - https://www.ogc.org/standards/sfa</li>
+ *  <li>Khronos GLTF-2 - https://github.com/KhronosGroup/glTF/tree/main/specification/2.0</li>
+ * </ul>
+ *
+ * <p>
+ * Differences from OGC Simple Feature Access :<br>
+ * In SFA a single attribute is possible, and exist if method isMeasured returns true.<br>
+ * Transposed to the GPU model we obtain two possible attributes : POSITION(2D or 3D) and MEASURE(1D).
+ *
+ * <p>
+ * The GPU model as defined by GLTF is more rich and allows any number of attributes.<br>
+ * Each attribute may itself be composed of 1 to 16 values.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public interface AttributesType {
+
+    /**
+     * @param name searched attribute name
+     * @return requested attribute type, never null
+     * @throws PropertyNotFoundException if not found
+     */
+    AttributeType getAttribute(String name) throws PropertyNotFoundException;
+
+    /**
+     * Returns collection of all attributes.
+     *
+     * @return never null, can be empty
+     */
+    Collection<AttributeType> getAttributes();
+
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/Geometry.java b/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/Geometry.java
new file mode 100644
index 0000000000..4b699caaff
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geometry/Geometry.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.geometry;
+
+import java.util.Map;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ * Parent interface of any geometry.
+ * <p>
+ * Based on specification :
+ * <ul>
+ *  <li>OGC Simple Feature Access - https://www.ogc.org/standards/sfa</li>
+ *  <li>Khronos GLTF-2 - https://github.com/KhronosGroup/glTF/tree/main/specification/2.0</li>
+ * </ul>
+ *
+ * <p>
+ * Differences from OGC Simple Feature Access :
+ * <ul>
+ *  <li>SRID : replaced by getCoordinateReferenceSystem</li>
+ *  <li>is3D : look at geometry CoordinateReferenceSystem instead</li>
+ *  <li>isMeasured() :
+ *      SFA defines only a single measure attribute attached to the geometry.
+ *      Khronos/GPU geometries may defined multiple and complex attributes.
+ *      Therefor a dedicated interface AttributesType is defined and accessed with {@linkplain #getAttributesType() }.</li>
+ *  <li>spatial and relation methods : found on GeometryOperations.</li>
+ * </ul>
+ *
+ * <p>
+ * To be reviewed with upcoming OGC Geometry / ISO-19107.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public interface Geometry {
+
+    /**
+     * Get geometry coordinate system.
+     *
+     * @return never null
+     */
+    CoordinateReferenceSystem getCoordinateReferenceSystem();
+
+    /**
+     * Set coordinate system in which the coordinates are declared.
+     * This method does not transform the coordinates.
+     *
+     * @param crs , not null
+     * @Throws IllegalArgumentException if coordinate system is not compatible with geometrie.
+     */
+    void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) throws IllegalArgumentException;
+
+    /**
+     * Get geometry attributes type.
+     *
+     * @return attributes type, never null
+     */
+    AttributesType getAttributesType();
+
+    /**
+     * Get the geometry number of dimensions.<br>
+     * This is the same as coordinate system dimension.
+     *
+     * @return number of dimension
+     */
+    default int getDimension() {
+        return getCoordinateReferenceSystem().getCoordinateSystem().getDimension();
+    }
+
+    /**
+     * Returns the name of the instantiable subtype of Geometry of which this geometric object is an instantiable member.<br>
+     * The name of the subtype of Geometry is returned as a string.
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return geometry subtype name.
+     */
+    String getGeometryType();
+
+    /**
+     * The minimum bounding box for this Geometry, returned as a Geometry.<br>
+     * The polygon is defined by the corner points of the bounding box [(MINX, MINY), (MAXX, MINY), (MAXX, MAXY), (MINX, MAXY), (MINX, MINY)].<br>
+     * Minimums for Z and M may be added.<br>
+     * The simplest representation of an Envelope is as two direct positions, one containing all the minimums, and another all the maximums.<br>
+     * In some cases, this coordinate will be outside the range of validity for the Spatial Reference System.
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return Envelope in geometry coordinate reference system.
+     */
+    Envelope getEnvelope();
+
+    /**
+     * Exports this geometric object to a specific Well-known Text Representation of Geometry.
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return this geometry in Well-known Text
+     */
+    String asText();
+
+    /**
+     * Exports this geometric object to a specific Well-known Binary Representation of Geometry.
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return this geometry in Well-known Binary
+     */
+    byte[] asBinary();
+
+    /**
+     * Returns TRUE if this geometric object is the empty Geometry.
+     * If true, then this geometric object represents the empty point set ∅ for the coordinate space.
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return true if empty.
+     */
+    boolean isEmpty();
+
+    /**
+     * Returns TRUE if this geometric object has no anomalous geometric points, such as self intersection or self tangency.
+     * The description of each instantiable geometric class will include the specific conditions that cause an instance
+     * of that class to be classified as not simple.
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return true if geometry is simple
+     */
+    boolean isSimple();
+
+    /**
+     * Returns the closure of the combinatorial boundary of this geometric object (Reference [1], section 3.12.2).
+     * Because the result of this function is a closure, and hence topologically closed, the resulting boundary can be
+     * represented using representational Geometry primitives (Reference [1], section 3.12.2).
+     *
+     * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
+     * @return boundary of the geometry
+     */
+    Geometry boundary();
+
+    /**
+     * Map of properties for user needs.
+     * Those informations may be lost in geometry processes.
+     *
+     * @return Map, can be null if the geometry can not store additional informations.
+     */
+    Map<String,Object> userProperties();
+
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/math/DataType.java b/core/sis-feature/src/main/java/org/apache/sis/internal/math/DataType.java
new file mode 100644
index 0000000000..ad1b13f3fb
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/math/DataType.java
@@ -0,0 +1,430 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.math;
+
+import java.awt.image.RasterFormatException;
+import org.apache.sis.internal.feature.Resources;
+import static org.apache.sis.internal.util.Numerics.MAX_INTEGER_CONVERTIBLE_TO_FLOAT;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Numbers;
+
+
+/**
+ * This class is a clone of Apache SIS org.apache.sis.image.DataType.
+ * But without image type restrictions.
+ *
+ * Normalized values definitions can be found at :
+ * https://www.khronos.org/opengl/wiki/Normalized_Integer
+ * https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ */
+public enum DataType {
+
+    /**
+     * Signed 8-bits data.
+     */
+    BYTE(0),
+
+    /**
+     * Unsigned 8-bits data.
+     */
+    UBYTE(1),
+
+    /**
+     * Signed 16-bits data.
+     */
+    SHORT(2),
+
+    /**
+     * Unsigned 16-bits data.
+     */
+    USHORT(3),
+
+    /**
+     * Signed 32-bits data.
+     */
+    INT(4),
+
+    /**
+     * Unsigned 32-bits data.
+     */
+    UINT(5),
+
+    /**
+     * Signed 64-bits data.
+     */
+    LONG(6),
+
+    /**
+     * Single precision (32-bits) floating point data.
+     */
+    FLOAT(7),
+
+    /**
+     * Double precision (64-bits) floating point data.
+     */
+    DOUBLE(8),
+
+    /**
+     * Signed 8-bits data interpreted as a decimal in range [-1..1]
+     */
+    NORMALIZED_BYTE(9),
+
+    /**
+     * Unsigned 8-bits data interpreted as a decimal in range [0..1]
+     */
+    NORMALIZED_UBYTE(10),
+
+    /**
+     * Signed 16-bits data interpreted as a decimal in range [-1..1]
+     */
+    NORMALIZED_SHORT(11),
+
+    /**
+     * Unsigned 16-bits data interpreted as a decimal in range [0..1]
+     */
+    NORMALIZED_USHORT(12);
+
+    private final int order;
+
+    /**
+     * Creates a new enumeration.
+     */
+    private DataType(int order) {
+        this.order = order;
+    }
+
+    /**
+     * Returns the smallest data type capable to store the given range of values.
+     * If the given range uses a floating point type, there there is a choice:
+     *
+     * <ul>
+     *   <li>If {@code asInteger} is {@code false}, then this method returns
+     *       {@link #FLOAT} or {@link #DOUBLE} depending on the range type.</li>
+     *   <li>Otherwise this method treats the floating point values as if they
+     *       were integers, with minimum value rounded toward negative infinity
+     *       and maximum value rounded toward positive infinity.</li>
+     * </ul>
+     *
+     * @param  range      the range of values.
+     * @param  asInteger  whether to handle floating point values as integers.
+     * @return smallest data type for the given range of values.
+     */
+    public static DataType forRange(final NumberRange<?> range, final boolean asInteger) {
+        ArgumentChecks.ensureNonNull("range", range);
+        final byte nt = Numbers.getEnumConstant(range.getElementType());
+        if (!asInteger) {
+            if (nt >= Numbers.DOUBLE)   return DOUBLE;
+            if (nt >= Numbers.FRACTION) return FLOAT;
+        }
+        final double min = range.getMinDouble();
+        final double max = range.getMaxDouble();
+        if (nt < Numbers.BYTE || nt > Numbers.FLOAT || nt == Numbers.LONG) {
+            /*
+             * Value type is long, double, BigInteger, BigDecimal or unknown type.
+             * If conversions to 32 bits integers would lost integer digits, or if
+             * a bound is NaN, stick to the most conservative data buffer type.
+             */
+            if (!(min >= -MAX_INTEGER_CONVERTIBLE_TO_FLOAT - 0.5 &&
+                  max <   MAX_INTEGER_CONVERTIBLE_TO_FLOAT + 0.5))
+            {
+                return DOUBLE;
+            }
+        }
+        /*
+         * Check most common types first. If the range could be both signed and unsigned short,
+         * give precedence to unsigned values.
+         * If a bounds is NaN, fallback on TYPE_FLOAT.
+         */
+        final DataType type;
+        if (min >= -0.5 && max < 0xFF + 0.5) {
+            type = UBYTE;
+        } else if (min >= Byte.MIN_VALUE - 0.5 && max < 0xFF + 0.5) {
+            type = BYTE;
+        } else if (min >= -0.5 && max < 0xFFFF + 0.5) {
+            type = USHORT;
+        } else if (min >= Short.MIN_VALUE - 0.5 && max < Short.MAX_VALUE + 0.5) {
+            type = SHORT;
+        } else if (min >= - 0.5 && max < 4294967295L + 0.5) {
+            type = UINT;
+        } else if (min >= Integer.MIN_VALUE - 0.5 && max < Integer.MAX_VALUE + 0.5) {
+            type = INT;
+        } else if (min >= Long.MIN_VALUE - 0.5 && max < Long.MAX_VALUE + 0.5) {
+            type = LONG;
+        } else {
+            type = FLOAT;
+        }
+        return type;
+    }
+
+    /**
+     * Returns the data type for the given primitive type. The given {@code type} should be a primitive
+     * type such as {@link Short#TYPE}, but wrappers class such as {@code Short.class} are also accepted.
+     *
+     * @param  type      the primitive type or its wrapper class.
+     * @param  unsigned  whether the type should be considered unsigned.
+     * @return the data type (never {@code null}) for the given primitive type.
+     * @throws RasterFormatException if the given type is not a recognized.
+     */
+    public static DataType forPrimitiveType(final Class<?> type, final boolean unsigned) {
+        switch (Numbers.getEnumConstant(type)) {
+            case Numbers.BYTE:    return unsigned ? UBYTE : BYTE;
+            case Numbers.SHORT:   return unsigned ? USHORT : SHORT;
+            case Numbers.INTEGER: return unsigned ? UINT : INT;
+            case Numbers.LONG:    return LONG;
+            case Numbers.FLOAT:   return FLOAT;
+            case Numbers.DOUBLE:  return DOUBLE;
+        }
+        throw new RasterFormatException(Resources.format(Resources.Keys.UnknownDataType_1, type));
+    }
+
+    /**
+     * Returns the size in bits of this data type.
+     *
+     * @return size in bits of this data type.
+     */
+    public int size() {
+        switch (this) {
+            case BYTE :
+            case UBYTE :
+                return 8;
+            case SHORT :
+            case USHORT :
+                return 16;
+            case INT :
+            case UINT :
+            case FLOAT :
+                return 32;
+            case LONG :
+            case DOUBLE :
+                return 64;
+            default :
+                throw new IllegalStateException("Unexpected type " + this.name());
+        }
+    }
+
+    /**
+     * Returns whether this type is an unsigned integer type.
+     * Unsigned types are {@link #UBYTE}, {@link #USHORT} and {@link #UINT}.
+     *
+     * @return {@code true} if this type is an unsigned integer type.
+     */
+    public boolean isUnsigned() {
+        switch (this) {
+            case UBYTE :
+            case USHORT :
+            case UINT :
+                return true;
+            default :
+                return false;
+        }
+    }
+
+    /**
+     * Returns whether this type is an integer type, signed or not.
+     *
+     * @return {@code true} if this type is an integer type.
+     */
+    public boolean isInteger() {
+        switch (this) {
+            case BYTE :
+            case UBYTE :
+            case SHORT :
+            case USHORT :
+            case INT :
+            case UINT :
+            case LONG :
+                return true;
+            default :
+                return false;
+        }
+    }
+
+    /**
+     * Returns the smallest floating point type capable to store all values of this type
+     * without precision lost. This method returns:
+     *
+     * <ul>
+     *   <li>{@link #DOUBLE} if this data type is {@link #DOUBLE}, {@link #INT}, {@link #UINT} or {@link #LONG}.</li>
+     *   <li>{@link #FLOAT} for all other types.</li>
+     * </ul>
+     *
+     * The promotion of integer values to floating point values is sometime necessary
+     * when the image may contain {@link Float#NaN} values.
+     *
+     * @return the smallest of {@link #FLOAT} or {@link #DOUBLE} types
+     *         which can store all values of this type without any lost.
+     */
+    public DataType toFloat() {
+        switch (this) {
+            case INT :
+            case UINT :
+            case LONG :
+            case DOUBLE :
+                return DOUBLE;
+            default :
+                return FLOAT;
+        }
+    }
+
+    /**
+     * Get the widest datatype which may contain both types.
+     */
+    public static DataType largest(DataType type1, DataType type2) {
+        if (type1.equals(type2)) {
+            return type1;
+        }
+        if (type1.order > type2.order) {
+            DataType t = type1;
+            type1 = type2;
+            type2 = t;
+        }
+
+        switch (type1) {
+            case BYTE :
+                    switch (type2) {
+                    case UBYTE : return SHORT;
+                    case SHORT : return SHORT;
+                    case USHORT : return INT;
+                    case INT : return INT;
+                    case UINT : return LONG;
+                    case LONG : return LONG;
+                    case FLOAT : return FLOAT;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case UBYTE :
+                    switch (type2) {
+                    case SHORT : return SHORT;
+                    case USHORT : return USHORT;
+                    case INT : return INT;
+                    case UINT : return UINT;
+                    case LONG : return LONG;
+                    case FLOAT : return FLOAT;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case SHORT :
+                    switch (type2) {
+                    case USHORT : return INT;
+                    case INT : return INT;
+                    case UINT : return LONG;
+                    case LONG : return LONG;
+                    case FLOAT : return FLOAT;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case USHORT :
+                    switch (type2) {
+                    case INT : return INT;
+                    case UINT : return UINT;
+                    case LONG : return LONG;
+                    case FLOAT : return FLOAT;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case INT :
+                    switch (type2) {
+                    case UINT : return LONG;
+                    case LONG : return LONG;
+                    case FLOAT : return FLOAT;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case UINT :
+                    switch (type2) {
+                    case LONG : return LONG;
+                    case FLOAT : return FLOAT;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case LONG :
+                switch (type2) {
+                    case FLOAT : return DOUBLE;
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return DOUBLE;
+                    case NORMALIZED_UBYTE : return DOUBLE;
+                    case NORMALIZED_SHORT : return DOUBLE;
+                    case NORMALIZED_USHORT : return DOUBLE;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case FLOAT :
+                    switch (type2) {
+                    case DOUBLE : return DOUBLE;
+                    case NORMALIZED_BYTE : return FLOAT;
+                    case NORMALIZED_UBYTE : return FLOAT;
+                    case NORMALIZED_SHORT : return FLOAT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case DOUBLE :
+                    switch (type2) {
+                    case NORMALIZED_BYTE : return DOUBLE;
+                    case NORMALIZED_UBYTE : return DOUBLE;
+                    case NORMALIZED_SHORT : return DOUBLE;
+                    case NORMALIZED_USHORT : return DOUBLE;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case NORMALIZED_BYTE :
+                    switch (type2) {
+                    case NORMALIZED_UBYTE : return NORMALIZED_SHORT;
+                    case NORMALIZED_SHORT : return NORMALIZED_SHORT;
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case NORMALIZED_UBYTE :
+                    switch (type2) {
+                    case NORMALIZED_SHORT : return NORMALIZED_SHORT;
+                    case NORMALIZED_USHORT : return NORMALIZED_USHORT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            case NORMALIZED_SHORT :
+                    switch (type2) {
+                    case NORMALIZED_USHORT : return FLOAT;
+                    default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+                    }
+            default : throw new IllegalArgumentException("Unexpected types " + type1 + " " + type2);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/math/SampleSystem.java b/core/sis-feature/src/main/java/org/apache/sis/internal/math/SampleSystem.java
new file mode 100644
index 0000000000..13e45ae582
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/math/SampleSystem.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.math;
+
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.collection.Cache;
+import org.apache.sis.util.iso.Names;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.GenericName;
+
+/**
+ * Experimental class to store multisamples dimensions.
+ *
+ * This serves an identical purpose as SampleDimension but usable with geometry attributes.
+ *
+ * Waiting for a proper implementation in SIS when reviewing ISO 19123 / 2153.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class SampleSystem {
+
+    private static final GenericName UNNAMED = Names.createLocalName(null, null, "unnamed");
+    private static final SampleSystem UNDEFINED_1S = new SampleSystem(1);
+    private static final SampleSystem UNDEFINED_2S = new SampleSystem(2);
+    private static final SampleSystem UNDEFINED_3S = new SampleSystem(3);
+    private static final SampleSystem UNDEFINED_4S = new SampleSystem(4);
+    private static SampleSystem[] UNDEFINED = new SampleSystem[0];
+    private static Cache<CoordinateReferenceSystem, SampleSystem> CACHE = new Cache<>();
+
+    private final GenericName name;
+    private final CoordinateReferenceSystem crs;
+    private final int size;
+
+    private SampleSystem(int size) {
+        ArgumentChecks.ensureStrictlyPositive("size", size);
+        this.name = UNNAMED;
+        this.crs = null;
+        this.size = size;
+    }
+
+    private SampleSystem(CoordinateReferenceSystem crs) {
+        this(UNNAMED, crs);
+    }
+
+    private SampleSystem(GenericName name, CoordinateReferenceSystem crs) {
+        ArgumentChecks.ensureNonNull("name", name);
+        ArgumentChecks.ensureNonNull("crs", crs);
+        this.name = name;
+        this.crs = crs;
+        this.size = crs.getCoordinateSystem().getDimension();
+    }
+
+    public static SampleSystem ofSize(int nbDim) {
+        ArgumentChecks.ensureStrictlyPositive("nbDim", nbDim);
+        switch (nbDim) {
+            case 1 : return UNDEFINED_1S;
+            case 2 : return UNDEFINED_2S;
+            case 3 : return UNDEFINED_3S;
+            case 4 : return UNDEFINED_4S;
+            default: {
+                final int idx = nbDim - 4;
+                synchronized (UNNAMED) {
+                    if (idx >= UNDEFINED.length) {
+                        UNDEFINED = Arrays.copyOf(UNDEFINED, idx+1);
+                    }
+                    if (UNDEFINED[idx] == null) {
+                        UNDEFINED[idx] = new SampleSystem(nbDim);
+                    }
+                    return UNDEFINED[idx];
+                }
+            }
+        }
+    }
+
+    public static SampleSystem of(CoordinateReferenceSystem crs) {
+        ArgumentChecks.ensureNonNull("crs", crs);
+        try {
+            return CACHE.getOrCreate(crs, () -> new SampleSystem(crs));
+        } catch (Exception ex) {
+            throw new BackingStoreException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Returns an identification for this dimension. This is typically used as a way to perform a band select
+     * by using human comprehensible descriptions instead of just numbers.
+     *
+     * @return an identification of this system, nerver null.
+     */
+    public GenericName getName() {
+        return name;
+    }
+
+    /**
+     * Returns the coordinate reference system for this record dimension if it is a coordinate system.
+     *
+     * @return can be null.
+     */
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return crs;
+    }
+
+    /**
+     * Returns the size in number of samples in this dimension.
+     *
+     * @return dimension size
+     */
+    public int getSize() {
+        return size;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, crs, size);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SampleSystem other = (SampleSystem) obj;
+        if (!Objects.equals(this.name, other.name)) {
+            return false;
+        }
+        return Objects.equals(this.crs, other.crs);
+    }
+
+}