This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 5e13b7f7bad086a32e228cf9f7ce66d52d9adec9 Author: Martin Desruisseaux <[email protected]> AuthorDate: Tue Nov 10 00:11:53 2020 +0100 Initial version of `WKTDictionary` for allowing users to define CRS for custom codes. https://issues.apache.org/jira/browse/SIS-502 --- .../apache/sis/internal/referencing/Resources.java | 22 +- .../sis/internal/referencing/Resources.properties | 6 +- .../internal/referencing/Resources_fr.properties | 6 +- .../java/org/apache/sis/io/wkt/AbstractParser.java | 75 +- .../main/java/org/apache/sis/io/wkt/Element.java | 105 ++- .../java/org/apache/sis/io/wkt/WKTDictionary.java | 884 +++++++++++++++++++++ .../main/java/org/apache/sis/io/wkt/WKTFormat.java | 90 ++- .../java/org/apache/sis/io/wkt/doc-files/ESRI.txt | 69 ++ .../java/org/apache/sis/io/wkt/package-info.java | 4 +- .../org/apache/sis/io/wkt/WKTDictionaryTest.java | 104 +++ .../sis/test/suite/ReferencingTestSuite.java | 1 + .../resources/org/apache/sis/io/wkt/ExtraCRS.txt | 65 ++ .../java/org/apache/sis/internal/jdk9/JDK9.java | 14 +- .../java/org/apache/sis/internal/util/Strings.java | 25 + 14 files changed, 1405 insertions(+), 65 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java index a71a1ea..5571276 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java @@ -127,6 +127,11 @@ public final class Resources extends IndexedResourceBundle { public static final short CanNotParseCombinedReference_2 = 78; /** + * Can not read Well-Known Text at line {0}. Caused by: {1} + */ + public static final short CanNotParseWKT_2 = 96; + + /** * Can not separate the “{0}” coordinate reference system into sub-components. */ public static final short CanNotSeparateCRS_1 = 84; @@ -158,7 +163,7 @@ public final class Resources extends IndexedResourceBundle { public static final short CanNotTransformGeometry = 86; /** - * Can not use the {0} geodetic parameters: {1} + * Can not use the {0} geodetic parameters. Caused by: {1} */ public static final short CanNotUseGeodeticParameters_2 = 9; @@ -346,6 +351,11 @@ public final class Resources extends IndexedResourceBundle { public static final short MisnamedParameter_1 = 38; /** + * Missing or empty “ID[…]” element for “{0}”. + */ + public static final short MissingAuthorityCode_1 = 99; + + /** * No authority was specified for code “{0}”. The expected syntax is “AUTHORITY:CODE”. */ public static final short MissingAuthority_1 = 39; @@ -507,6 +517,11 @@ public final class Resources extends IndexedResourceBundle { public static final short StartOrEndPointNotSet_1 = 88; /** + * Syntax error for Well-Known Text alias at line {0}. + */ + public static final short SyntaxErrorForAlias_1 = 97; + + /** * Combined URI contains unexpected components. */ public static final short UnexpectedComponentInURI = 80; @@ -517,6 +532,11 @@ public final class Resources extends IndexedResourceBundle { public static final short UnexpectedDimensionForCS_1 = 64; /** + * Unexpected text “{1}” at line {0}. WKT for new object should start with a non-indented line. + */ + public static final short UnexpectedTextAtLine_2 = 98; + + /** * Parameter “{0}” does not expect unit. */ public static final short UnitlessParameter_1 = 65; diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties index 338d505..7432bcc 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties @@ -59,10 +59,11 @@ CanNotParseCombinedReference_2 = Can not parse component {1} in the combined CanNotSeparateCRS_1 = Can not separate the \u201c{0}\u201d coordinate reference system into sub-components. CanNotSeparateTransform_3 = Can not separate the transform because result would have {2} {0,choice,0#source|1#target} dimension{2,choice,1#|2#s} instead of {1}. CanNotSeparateTargetDimension_1 = Target dimension {0} depends on excluded source dimensions. +CanNotParseWKT_2 = Can not read Well-Known Text at line {0}. Caused by: {1} CanNotTransformCoordinates_2 = Can not transform the ({0,number}, {1,number}) coordinates. CanNotTransformEnvelopeToGeodetic = Can not transform envelope to a geodetic reference system. CanNotTransformGeometry = Can not transform the given geometry. -CanNotUseGeodeticParameters_2 = Can not use the {0} geodetic parameters: {1} +CanNotUseGeodeticParameters_2 = Can not use the {0} geodetic parameters. Caused by: {1} ColinearAxisDirections_2 = Axis directions {0} and {1} are colinear. CoordinateOperationNotFound_2 = Coordinate conversion of transformation from system \u201c{0}\u201d to \u201c{1}\u201d has not been found. DatumChangesDirectory_1 = Datum shift files are searched in the \u201c{0}\u201d directory. @@ -86,6 +87,7 @@ MismatchedParameterDescriptor_1 = Mismatched descriptor for \u201c{0}\u201d pa MismatchedPrimeMeridian_2 = Expected the \u201c{0}\u201d prime meridian but found \u201c{1}\u201d. MismatchedTransformDimension_3 = The transform has {2} {0,choice,0#source|1#target} dimension{2,choice,1#|2#s}, while {1} was expected. MissingAuthority_1 = No authority was specified for code \u201c{0}\u201d. The expected syntax is \u201cAUTHORITY:CODE\u201d. +MissingAuthorityCode_1 = Missing or empty \u201cID[\u2026]\u201d element for \u201c{0}\u201d. MissingInterpolationOrdinates = Not enough dimension in \u2018MathTransform\u2019 input or output coordinates for the interpolation points. MissingHorizontalDimension_1 = No horizontal dimension found in \u201c{0}\u201d. MissingVerticalDimension_1 = No vertical dimension found in \u201c{0}\u201d @@ -113,8 +115,10 @@ ParameterNotFound_2 = No parameter named \u201c{1}\u201d has been RecursiveCreateCallForCode_2 = Recursive call while creating an object of type \u2018{0}\u2019 for code \u201c{1}\u201d. SingularMatrix = Matrix is singular. StartOrEndPointNotSet_1 = The {0,choice,0#start|1#end} point has not been specified. +SyntaxErrorForAlias_1 = Syntax error for Well-Known Text alias at line {0}. UnexpectedComponentInURI = Combined URI contains unexpected components. UnexpectedDimensionForCS_1 = Unexpected dimension for a coordinate system of type \u2018{0}\u2019. +UnexpectedTextAtLine_2 = Unexpected text \u201c{1}\u201d at line {0}. WKT for new object should start with a non-indented line. UnitlessParameter_1 = Parameter \u201c{0}\u201d does not expect unit. UnknownAuthority_1 = Authority \u201c{0}\u201d is unknown. UnknownAxisDirection_1 = Axis direction \u201c{0}\u201d is unknown. diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties index 67abae0..3672c41 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties @@ -64,10 +64,11 @@ CanNotSeparateCRS_1 = Ne peut pas s\u00e9parer le syst\u00e8me de CanNotSeparateTransform_3 = Ne peut pas s\u00e9parer la transformation parce-que le r\u00e9sultat aurait {2} dimension{2,choice,1#|2#s} en {0,choice,0#entr\u00e9|1#sortie} au lieu de {1}. CanNotSeparateTargetDimension_1 = La dimension de destination {0} d\u00e9pend de dimensions sources qui ont \u00e9t\u00e9 exclues. CanNotParseCombinedReference_2 = Ne peut pas d\u00e9coder la composante {1} dans l\u2019{0,choice,0#URN|1#URL} combin\u00e9. +CanNotParseWKT_2 = Ne peut pas lire le \u00ab\u202fWell-Known Text\u202f\u00bb \u00e0 la ligne {0}. La cause est\u202f: {1} CanNotTransformCoordinates_2 = Ne peut pas transformer les coordonn\u00e9es ({0,number}; {1,number}). CanNotTransformEnvelopeToGeodetic = Ne peut pas transformer l\u2019enveloppe vers un r\u00e9f\u00e9rentiel g\u00e9od\u00e9sique. CanNotTransformGeometry = Ne peut pas transformer la g\u00e9om\u00e9trie donn\u00e9e. -CanNotUseGeodeticParameters_2 = Ne peut pas utiliser les param\u00e8tres g\u00e9od\u00e9siques {0}\u202f: {1} +CanNotUseGeodeticParameters_2 = Ne peut pas utiliser les param\u00e8tres g\u00e9od\u00e9siques {0}. La cause est\u202f: {1} ColinearAxisDirections_2 = Les directions d\u2019axes {0} et {1} sont colin\u00e9aires. CoordinateOperationNotFound_2 = La conversion ou transformation des coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e. DatumChangesDirectory_1 = Les fichiers de changements de r\u00e9f\u00e9rentiel sont cherch\u00e9s dans le dossier \u00ab\u202f{0}\u202f\u00bb. @@ -91,6 +92,7 @@ MismatchedParameterDescriptor_1 = Le descripteur du param\u00e8tre \u00ab\u202 MismatchedPrimeMeridian_2 = Le m\u00e9ridien d\u2019origine \u00ab\u202f{0}\u202f\u00bb \u00e9tait attendu, mais \u00ab\u202f{1}\u202f\u00bb a \u00e9t\u00e9 trouv\u00e9. MismatchedTransformDimension_3 = La {0,choice,0#source|1#destination} de la transformation a {2} dimension{2,choice,1#|2#s}, alors qu\u2019on en attendait {1}. MissingAuthority_1 = Aucune autorit\u00e9 n\u2019a \u00e9t\u00e9 sp\u00e9cifi\u00e9e pour le code \u00ab\u202f{0}\u202f\u00bb. Le format attendu est \u00ab\u202fAUTORIT\u00c9:CODE\u202f\u00bb. +MissingAuthorityCode_1 = L\u2019\u00e9l\u00e9ment \u00ab\u202fID[\u2026]\u202f\u00bb est manquant ou vide pour \u00ab\u202f{0}\u202f\u00bb. MissingInterpolationOrdinates = La dimension des coordonn\u00e9es en entr\u00e9 ou en sortie du \u2018MathTransform\u2019 n\u2019est pas suffisante pour contenir les points d\u2019interpolation. MissingHorizontalDimension_1 = Aucune dimension horizontale n\u2019a \u00e9t\u00e9 trouv\u00e9e dans \u00ab\u202f{0}\u202f\u00bb. MissingVerticalDimension_1 = Aucune dimension verticale n\u2019a \u00e9t\u00e9 trouv\u00e9e dans \u00ab\u202f{0}\u202f\u00bb. @@ -118,8 +120,10 @@ ParameterNotFound_2 = Aucun param\u00e8tre nomm\u00e9 \u00ab\u202f RecursiveCreateCallForCode_2 = Appels r\u00e9cursifs lors de la cr\u00e9ation d\u2019un objet de type \u2018{0}\u2019 pour le code \u00ab\u202f{1}\u202f\u00bb. SingularMatrix = La matrice est singuli\u00e8re. StartOrEndPointNotSet_1 = Le point {0,choice,0#de d\u00e9part|1#d\u2019arriv\u00e9} n\u2019a pas \u00e9t\u00e9 d\u00e9fini. +SyntaxErrorForAlias_1 = Erreur de syntaxe pour l\u2019alias de \u00ab\u202fWell-Known Text\u202f\u00bb \u00e0 la ligne {0}. UnexpectedComponentInURI = L\u2019URI combin\u00e9 contient des composantes qui n\u2019\u00e9taient pas attendues. UnexpectedDimensionForCS_1 = Dimension inattendue pour un syst\u00e8me de coordonn\u00e9es de type \u2018{0}\u2019. +UnexpectedTextAtLine_2 = Le texte \u00ab\u202f{1}\u202f\u00bb \u00e0 la ligne {0} est inattendu. Le WKT d\u2019un nouvel objet doit commencer par une ligne non-indent\u00e9e. UnitlessParameter_1 = Le param\u00e8tre \u00ab\u202f{0}\u202f\u00bb n\u2019attend pas d\u2019unit\u00e9. UnknownAuthority_1 = L\u2019autorit\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019est pas reconnue. UnknownAxisDirection_1 = La direction d\u2019axe \u00ab\u202f{0}\u202f\u00bb n\u2019est pas reconnue. diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java index 85596cd..b255587 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java @@ -254,40 +254,87 @@ abstract class AbstractParser implements Parser { } /** - * Parses a <cite>Well Know Text</cite> (WKT). + * Parses a <cite>Well Know Text</cite> (WKT) as a tree of {@link Element}s. + * This tree can be given to {@link #buildFromTree(Element)} for producing a geodetic object. * - * @param text the text to be parsed. - * @param position the position to start parsing from. - * @return the parsed object. + * @param text the text to be parsed. + * @param position the position to start parsing from. + * @param sharedValues non-null if parsing a WKT tree to be kept for a long time. + * In such case, contains values found during parsing of other elements. + * @return the parsed object as a tree of {@link Element}s. * @throws ParseException if the string can not be parsed. + * + * @see WKTFormat#textToTree(String, ParsePosition) */ - public Object parseObject(final String text, final ParsePosition position) throws ParseException { - warnings = null; - ignoredElements.clear(); - ArgumentChecks.ensureNonEmpty("text", text); + final Element textToTree(final String text, final ParsePosition position, final Map<Object,Object> sharedValues) + throws ParseException + { + /* + * Aliases for fragments (e.g. "$Foo" in ProjectedCRS["something", $Foo]) are expanded by + * the `Element` constructor, except if the alias appears at the begining of the text. + * In such case the alias is the whole text and we need a different constructor. + */ Element fragment; int lower = CharSequences.skipLeadingWhitespaces(text, position.getIndex(), text.length()); if (lower < text.length() && text.charAt(lower) == Symbols.FRAGMENT_VALUE) { final int upper = endOfFragmentName(text, ++lower); final String id = text.substring(lower, upper); - fragment = fragments.get(id); + fragment = fragments.get(id); // Should be immutable. if (fragment == null) { position.setErrorIndex(lower); throw new UnparsableObjectException(errorLocale, Errors.Keys.NoSuchValue_1, new Object[] {id}, lower); } position.setIndex(upper); - fragment = new Element(fragment); + if (sharedValues == null) { // `true` if invoked for immediate parsing. + fragment = fragment.modifiable(); // Parsing requires a modifiable copy. + } } else { - fragment = new Element(this, text, position, null); + fragment = new Element(this, text, position, sharedValues); } - final Element element = new Element("<root>", fragment); - final Object object = parseObject(element); - element.close(ignoredElements); + return fragment; + } + + /** + * Parses a tree of {@link Element}s to produce a geodetic object. + * The {@code root} argument should be a value returned by + * {@link #textToTree(String, ParsePosition, Map)}. + * + * @param root the tree of WKT elements. + * @return the parsed object. + * @throws ParseException if the tree can not be parsed. + */ + final Object buildFromTree(Element root) throws ParseException { + warnings = null; + ignoredElements.clear(); + root = new Element("<root>", root); + final Object object = parseObject(root); + root.close(ignoredElements); + return object; + } + + /** + * Parses a <cite>Well Know Text</cite> (WKT) as a geodetic object. + * + * @param text the text to be parsed. + * @param position the position to start parsing from. + * @return the parsed object. + * @throws ParseException if the string can not be parsed. + */ + public Object parseObject(final String text, final ParsePosition position) throws ParseException { + warnings = null; + ignoredElements.clear(); + ArgumentChecks.ensureNonEmpty("text", text); + Element root = textToTree(text, position, null); + root = new Element("<root>", root); + final Object object = parseObject(root); + root.close(ignoredElements); return object; } /** * Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree. + * Subclasses will typically get the name of the first element and delegate to a specialized method + * such as {@code parseAxis(…)}, {@code parseEllipsoid(…)}, {@code parseTimeDatum(…)}, <i>etc</i>. * * @param element the element to be parsed. * @return the parsed object. diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java index 137203e..938dcce 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java @@ -62,7 +62,7 @@ final class Element implements Serializable { /** * Indirectly for {@link WKTFormat} serialization compatibility. */ - private static final long serialVersionUID = 4048095121452884024L; + private static final long serialVersionUID = -7345192763818308443L; /** * Kind of value expected in the element. Value 0 means "not yet determined". @@ -97,11 +97,16 @@ final class Element implements Serializable { /** * {@code true} if the keyword was not followed by a pair of brackets (e.g. "north"). - * If {@code true}, then {@link #children} shall be an empty list. + * If {@code true}, then {@link #children} shall be an empty list and {@link #isImmutable} should be {@code true}. */ private final boolean isEnumeration; /** + * Whether this element is immutable. + */ + private final boolean isImmutable; + + /** * An ordered sequence of {@link String}s, {@link Number}s and other {@link Element}s. * Access to this collection should be done using the iterator, not by random access. */ @@ -120,29 +125,34 @@ final class Element implements Serializable { * @param singleton the only child for this root. */ Element(final String name, final Element singleton) { - keyword = name; - offset = singleton.offset; - errorLocale = singleton.errorLocale; - children = new LinkedList<>(); // Needs to be a modifiable collection. - children.add(singleton); + keyword = name; + offset = singleton.offset; + errorLocale = singleton.errorLocale; isEnumeration = false; + isImmutable = false; + children = new LinkedList<>(); // Needs to be a modifiable collection. + children.add(singleton.modifiable()); } /** * Creates a modifiable copy of the given element. + * Modifiable instances are needed by the WKT parser. + * + * @see #modifiable() */ - Element(final Element toCopy) { + private Element(final Element toCopy) { offset = toCopy.offset; keyword = toCopy.keyword; - isEnumeration = toCopy.isEnumeration; errorLocale = toCopy.errorLocale; + isEnumeration = toCopy.isEnumeration; // Should always be `false`. + isImmutable = isEnumeration; children = new LinkedList<>(toCopy.children); // Needs to be a modifiable collection. final ListIterator<Object> it = children.listIterator(); while (it.hasNext()) { final Object value = it.next(); if (value instanceof Element) { final Element fragment = (Element) value; - if (!fragment.isEnumeration) { + if (fragment.isImmutable) { it.set(new Element(fragment)); } } @@ -150,6 +160,15 @@ final class Element implements Serializable { } /** + * Returns a mutable instance of this {@code Element}. + * If this element is already modifiable, then it is returned as-is. + * If this element is unmodifiable, then a modifiable copy is created. + */ + final Element modifiable() { + return isImmutable ? new Element(this) : this; + } + + /** * Constructs a new {@code Element}. * The {@code sharedValues} argument have two meanings: * @@ -166,7 +185,8 @@ final class Element implements Serializable { * @param text the text to parse. * @param position on input, the position where to start parsing from. * On output, the first character after the separator. - * @param sharedValues if parsing a fragment, a map with the values found in other elements. Otherwise {@code null}. + * @param sharedValues non-null if parsing a WKT tree to be kept for a long time. + * In such case, contains values found during parsing of other elements. */ Element(final AbstractParser parser, final String text, final ParsePosition position, final Map<Object,Object> sharedValues) throws ParseException @@ -207,8 +227,9 @@ final class Element implements Serializable { openingBracket = text.codePointAt(lower))) < 0) { position.setIndex(lower); - children = Collections.emptyList(); + this.children = Collections.emptyList(); isEnumeration = true; + isImmutable = true; return; } lower = skipLeadingWhitespaces(text, lower + Character.charCount(openingBracket), length); @@ -240,8 +261,8 @@ final class Element implements Serializable { position.setErrorIndex(lower); throw new UnparsableObjectException(errorLocale, Errors.Keys.NoSuchValue_1, new Object[] {id}, lower); } - if (!fragment.isEnumeration) { - fragment = new Element(fragment); + if (sharedValues == null) { // `true` if created for immediate parsing. + fragment = fragment.modifiable(); // WKT parser needs modifiable elements. } children.add(fragment); lower = upper; @@ -319,7 +340,7 @@ final class Element implements Serializable { lower = position.getIndex(); } /* - * Store the value, using shared instances if this Element may be stored for a long time. + * Store the value, using shared instances if this `Element` may be stored for a long time. */ if (sharedValues != null) { final Object e = sharedValues.putIfAbsent(value, value); @@ -341,12 +362,9 @@ final class Element implements Serializable { final int c = text.codePointAt(lower); if (c == closingBracket) { position.setIndex(lower + Character.charCount(c)); - if (sharedValues != null) { - this.children = UnmodifiableArrayList.wrap(children.toArray()); - } else { - this.children = children; - } isEnumeration = false; + isImmutable = (sharedValues != null); + this.children = isImmutable ? UnmodifiableArrayList.wrap(children.toArray()) : children; return; } position.setErrorIndex(lower); @@ -509,6 +527,34 @@ final class Element implements Serializable { ////////////////////////////////////////////////////////////////////////////////////// /** + * Returns the last element of the given names without removing it. + * This method searches only in children of this element. + * It does not search recursively in children of children. + * + * @param keys the element names (e.g. {@code "ID"}). + * @return the last {@link Element} of the given names found in the children, or {@code null} if none. + * + * @see #pullElement(int, String...) + */ + public Element peekLastElement(final String... keys) { + final ListIterator<Object> iterator = children.listIterator(children.size()); + while (iterator.hasPrevious()) { + final Object object = iterator.previous(); + if (object instanceof Element) { + final Element element = (Element) object; + if (!element.isEnumeration) { + for (int i=0; i<keys.length; i++) { + if (element.keyword.equalsIgnoreCase(keys[i])) { + return element; + } + } + } + } + } + return null; + } + + /** * Returns the next value (not a child element) without removing it. * * @return the next value, or {@code null} if none. @@ -525,6 +571,25 @@ final class Element implements Serializable { } /** + * Returns the next values (not child elements) without removing them. + * The maximum number of values fetched is the length of the given array. + * If there is less WKT elements, remaining array elements are unchanged. + * + * @param addTo non-empty array where to store the values. + */ + public void peekValues(final Object[] addTo) { + int count = 0; + final Iterator<Object> iterator = children.iterator(); + while (iterator.hasNext()) { + final Object object = iterator.next(); + if (!(object instanceof Element)) { + addTo[count] = object; + if (++count >= addTo.length) break; + } + } + } + + /** * Removes the next {@link Date} from the children and returns it. * * @param key the parameter name. Used for formatting an error message if no date is found. diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java new file mode 100644 index 0000000..0f69a1f --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java @@ -0,0 +1,884 @@ +/* + * 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.io.wkt; + +import java.util.Locale; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.stream.Stream; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.io.LineNumberReader; +import java.io.BufferedReader; +import java.io.IOException; +import java.text.ParseException; +import java.text.ParsePosition; +import org.opengis.util.FactoryException; +import org.opengis.metadata.Identifier; +import org.opengis.metadata.citation.Citation; +import org.opengis.referencing.IdentifiedObject; +import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.apache.sis.referencing.factory.GeodeticAuthorityFactory; +import org.apache.sis.referencing.factory.FactoryDataException; +import org.apache.sis.internal.referencing.Resources; +import org.apache.sis.internal.referencing.WKTKeywords; +import org.apache.sis.internal.util.CollectionsExt; +import org.apache.sis.internal.util.Constants; +import org.apache.sis.internal.util.Strings; +import org.apache.sis.internal.jdk9.JDK9; +import org.apache.sis.metadata.iso.citation.Citations; +import org.apache.sis.util.CharSequences; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.Exceptions; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.collection.Containers; +import org.apache.sis.util.collection.FrequencySortedSet; + + +/** + * A factory providing CRS objects parsed from WKT definitions associated to authority codes. + * Each WKT definition is associated to a key according the <var>authority:version:code</var> + * pattern where <var>code</var> is mandatory and <var>authority:version</var> are optional. + * Coordinate Reference Systems or other kinds of objects are created from WKT definitions + * when a {@code create(…)} method is invoked for the first time for a given key. + * + * <p>Newly constructed {@code WKTDictionary} are initially empty. For populating the factory, + * the {@link #load(BufferedReader)} or {@link #addDefinitions(Stream)} methods must be invoked.</p> + * + * <h2>Errors management</h2> + * Well-Known Text parsing is performed in two steps, each of them executed at a different time: + * + * <h3>Early validation</h3> + * WKT strings added by {@code load(…)} or {@code addDefinitions(…)} methods are verified + * for matching quotes, balanced parenthesis or brackets, and valid number or date formats. + * If a syntax error is detected, the loading process is interrupted at the point the error occurred; + * CRS definitions after the error location are not loaded. + * However WKT keywords and geodetic parameters (e.g. map projections) are not validated at this stage. + * + * <h3>Late validation</h3> + * WKT keywords and geodetic parameters inside WKT elements are validated only when {@link #createObject(String)} + * is invoked. If an error occurs at this stage, only the CRS (or other geodetic object) for the code given to + * the {@code createFoo(…)} method become invalid. Objects associated to other codes are not impacted. + * + * <h2>Multi-threading</h2> + * This class is thread-safe but not necessarily concurrent. + * This class is designed for a relatively small amount of WKT; + * it is not a replacement for database-backed factory such as + * {@link org.apache.sis.referencing.factory.sql.EPSGFactory}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.1 + * @since 1.1 + * @module + */ +public class WKTDictionary extends GeodeticAuthorityFactory { + /** + * The organization or specification that defines the codes recognized by this factory. + * May be {@code null} if not yet determined. + * + * @see #updateAuthority() + * @see #getAuthority() + */ + private Citation authority; + + /** + * Authorities declared in all {@code "ID[CITATION[…]]"} elements found in WKT definitions. + * This set is {@code null} if an {@link #authority} value has been explicitly specified at + * construction time. If non-null, this is used for creating a default {@link #authority}. + */ + private final Set<String> authorities; + + /** + * Code spaces of authority codes recognized by this factory. + * This set is computed from the {@code "ID[…]"} elements found in WKT definitions. + * + * @see #getCodeSpaces() + */ + private final Set<String> codespaces; + + /** + * The parser to use for creating geodetic objects from WKT definitions. + * All uses of this parser shall be synchronized by the <code>{@linkplain #lock}.writeLock()</code>. + */ + private final WKTFormat parser; + + /** + * The write lock for {@link #parser} and the read/write locks for {@link #definitions} accesses. + * + * <div class="note"><b>Implementation note:</b> + * we manage the locks ourselves instead than using a {@link java.util.concurrent.ConcurrentHashMap} + * because if a {@link #definitions} value needs to be computed, then we need to block all other + * threads anyway since {@link #parser} is not thread-safe. Consequently the high concurrency + * capability provided by {@code ConcurrentHashMap} does not help us in this case.</div> + */ + private final ReadWriteLock lock; + + /** + * CRS definitions associated to <var>authority:version:code</var> keys. + * Keys are authority codes, ignoring code space (authority) and version. + * For example in "EPSG:9.1:4326" the key would be only "4326". + * Values can be one of the following 4 types: + * + * <ol> + * <li>{@link Element}: this is the initial state when there is no duplicated codes. + * This is the root of a tree of WKT keywords with their values as children. + * A tree can be parsed later as an {@link IdentifiedObject} when first requested.</li> + * <li>{@link IdentifiedObject}: the result of parsing the root {@link Element} + * when {@link #createObject(String)} is invoked for a given authority code. + * The parsing result replaces the previous {@link Element} value.</li> + * <li>{@link Disambiguation}: if the same code is used by two or more authorities or versions, + * then above-cited {@link Element} or {@link IdentifiedObject} alternatives are wrapped + * in a {@link Disambiguation} object.</li> + * <li>{@link String} if parsing failed, in which case the string is the error message.</li> + * </ol> + * + * <h4>Synchronization</h4> + * All read operations in this map shall be synchronized by the <code>{@linkplain #lock}.readLock()</code> + * and write operations synchronized by the <code>{@linkplain #lock}.writeLock()</code>. + * + * @see #addDefinition(Element) + * @see #createObject(String) + */ + private final Map<String,Object> definitions; + + /** + * A special kind of value used in the {@link #definitions} map when the same code is used by more + * than one authority and version. In the common case where a {@link WKTDictionary} instance + * contains definitions for only one namespace and version, this class will never be instantiated. + */ + private static final class Disambiguation { + /** + * The previous {@code Disambiguation} in a linked list, or {@code null} if we reached the end of list. + * The use of a linked list should be efficient enough if the amount of {@code Disambiguation}s for a + * given code is small. + */ + private final Disambiguation previous; + + /** + * The authority (or other kind of code space) providing CRS definitions. + */ + private final String codespace; + + /** + * Version of the CRS definition, or {@code null} if unspecified. + */ + private final String version; + + /** + * The value as an {@link Element} before parsing or an {@link IdentifiedObject} after parsing. + * They are the kind of types documented in {@link WKTDictionary#definitions}, excluding + * other {@code Disambiguation} instances. + */ + Object value; + + /** + * Creates a new {@code Disambiguation} instance as a wrapper around the given identifier object. + * This constructor may be invoked if {@link WKTDictionary} has been used for creating some + * objects before new definitions are added. It should rarely happen. + * + * @param object the CRS (or other geodetic object) to wrap. + */ + Disambiguation(final IdentifiedObject object) { + /* + * Identifier should never be null because `WKTDictionary` accepts only definitions having + * an `ID[…]` or `AUTHORITY[…]` element. A WKT can contain at most one of those elements. + */ + final Identifier id = CollectionsExt.first(object.getIdentifiers()); + codespace = id.getCodeSpace(); + version = id.getVersion(); + value = object; + previous = null; + } + + /** + * Creates a new {@code Disambiguation} instance as a wrapper around the given identifier object. + * + * @param object definition in WKT of the CRS (or other geodetic object) to wrap. + * @param fullId an array of length 3 to be used for getting the {@code codespace:version:code} tuple. + */ + Disambiguation(final Element object, final Object[] fullId) { + Arrays.fill(fullId, null); + object.peekLastElement(MathTransformParser.ID_KEYWORDS).peekValues(fullId); + codespace = trimOrNull(fullId[0]); + version = trimOrNull(fullId[2]); + value = object; + previous = null; + } + + /** + * Creates a new {@code Disambiguation} instance identified by {@code codespace:version:code}. + * + * @param previous previous disambiguation, or {@code null} if none. + * @param codespace the authority (or other kind of code space) providing CRS definitions. + * @param version version of the CRS definition, or {@code null} if unspecified. + * @param code code allocated by the authority for the CRS definition. + * @param value the CRS (or other geodetic object) definition. + * @throws IllegalArgumentException if <var>authority:version:code</var> identifier is already used. + * + * @see WKTDictionary#addDefinition(Element) + */ + Disambiguation(Disambiguation previous, final String codespace, final String version, + final String code, final Element value) + { + this.previous = previous; + this.codespace = codespace; + this.version = version; + this.value = value; + while (previous != null) { + if (Strings.equalsIgnoreCase(codespace, previous.codespace) && + Strings.equalsIgnoreCase(version, previous.version)) + { + throw new IllegalArgumentException(Errors.format( + Errors.Keys.DuplicatedIdentifier_1, identifier(code))); + } + previous = previous.previous; + } + } + + /** + * Finds the {@code Disambiguation} for the given authority and version. + * + * @param choices end of a linked list of {@code Disambiguation}s, or {@code null} if none. + * @param codespace the authority providing CRS definitions, or {@code null} if unspecified. + * @param version version of the CRS definition, or {@code null} if unspecified. + * @param code code allocated by the authority for the CRS definition. + * @return container for the given authority and version, or {@code null} if none. + * @throws NoSuchAuthorityCodeException if the given authority and version are ambiguous. + */ + static Disambiguation find(Disambiguation choices, final String codespace, final String version, final String code) + throws NoSuchAuthorityCodeException + { + Disambiguation found = null; + for (boolean isExact = false; choices != null; choices = choices.previous) { + if (codespace == null || codespace.equalsIgnoreCase(choices.codespace)) { + if (Strings.equalsIgnoreCase(version, choices.version)) { + if (!isExact) { + isExact = true; + found = choices; // Silently discard previous value since we have a better match. + continue; + } + } else if (isExact) { + continue; // Ignore this value since previous one was a better match. + } + if (isExact && found != null) { + final String identifier = identifier(codespace, version, code); + throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.AmbiguousName_3, + choices.identifier(code), found.identifier(code), identifier), + codespace, code, identifier); + } + found = choices; + } + } + return found; + } + + /** + * Adds all authority codes to the given set. + * + * @param choices end of a linked list of {@code Disambiguation}s. + * @param code authority code (code space and version may vary). + * @param addTo where to add the {@code codespace:version:code} tuples. + * + * @see WKTDictionary#getAuthorityCodes(Class) + */ + static void list(Disambiguation choices, final String code, final Set<String> addTo) { + do { + addTo.add(choices.identifier(code)); + choices = choices.previous; + } while (choices != null); + } + + /** + * Creates an <var>authority:version:code</var> identifier with the given code. + * This is used for formatting error messages. + */ + private String identifier(final String code) { + return identifier(codespace, version, code); + } + + /** + * Creates an <var>authority:version:code</var> identifier with the given code. + * This is used for formatting error messages. + */ + private static String identifier(final String codespace, final String version, final String code) { + return Strings.orEmpty(codespace) + Constants.DEFAULT_SEPARATOR + + Strings.orEmpty(version) + Constants.DEFAULT_SEPARATOR + + Strings.orEmpty(code); + } + } + + /** + * Creates an initially empty factory. The authority can specified explicitly or inferred from the WKTs. + * In the later case (when the given authority is {@code null}), an authority will be inferred from all + * {@code ID[…]} or {@code AUTHORITY[…]} elements found in WKT strings as below, in preference order: + * + * <ol> + * <li>Most frequent {@code CITATION[…]} value.</li> + * <li>If there is no citation, then most frequent code space + * in {@code ID[…]} or {@code AUTHORITY[…]} elements.</li> + * </ol> + * + * The WKT strings are specified by calls to {@link #load(BufferedReader)} or {@link #addDefinitions(Stream)} + * after construction. + * + * @param authority organization that defines the codes recognized by this factory, or {@code null}. + */ + public WKTDictionary(final Citation authority) { + /* + * Note: we do not allow users to specify their own `WKTFormat` instance because current + * `WKTDictionary` implementation invokes package-private methods. If user supplies + * a `WKTFormat` with overridden public methods, (s)he may be surprised to see that those + * methods are not invoked. + */ + definitions = new HashMap<>(); + codespaces = new FrequencySortedSet<>(true); + parser = new WKTFormat(null, null); + lock = new ReentrantReadWriteLock(); + authorities = (authority != null) ? null : new FrequencySortedSet<>(true); + this.authority = authority; + } + + /** + * If {@link #authority} is not yet defined, computes a value from {@code ID[…]} found + * in all WKT strings. This method should be invoked after new WKTs have been added. + */ + private void updateAuthority() { + if (authorities != null) { + String name = CollectionsExt.first(authorities); // Most frequently declared authority. + if (name == null) { + name = CollectionsExt.first(codespaces); // Most frequently declared codespace. + } + authority = Citations.fromName(name); // May still be null. + } + } + + /** + * Keyword recognized by {@link #load(BufferedReader)}. + */ + private static final String SET = "SET"; + + /** + * Adds to this factory all definitions read from the given source. + * Each Coordinate Reference System (or other geodetic object) is defined by a string in WKT format. + * The key associated to each object is given by the {@code ID[…]} or {@code AUTHORITY[…]} element, + * which is typically the last element of a WKT string and is mandatory for definitions in this file. + * + * <p>WKT strings can span many lines. All lines after the first line shall be indented with at least + * one white space. Non-indented lines start new definitions.</p> + * + * <p>Blank lines and lines starting with the {@code #} character (ignoring white spaces) are ignored.</p> + * + * <h4>Aliases for WKT fragments</h4> + * Files with more than one WKT definition tend to repeat the same WKT fragments many times. + * For example the same {@code BaseGeogCRS[…]} element may be repeated in every {@code ProjectedCRS} definitions. + * Redundant fragments can be replaced by aliases for making the file more compact, + * easier to read, faster to parse and with smaller memory footprint. + * + * <p>Each line starting with "<code>SET <<var>identifier</var>>=<<var>WKT</var>></code>" + * defines an alias for a fragment of WKT string. The WKT can span many lines as described above. + * Aliases are local to the file where they are defined. + * Aliases can be expanded in other WKT strings by "<code>$<<var>identifier</var>></code>".</p> + * + * <h4>Validation</h4> + * This method verifies that definitions have matching quotes, balanced parenthesis or brackets, + * and valid number or date formats. It does not verify WKT keywords or geodetic parameters. + * See class javadoc for more details. + * + * <h4>Example</h4> + * An example is <a href="./doc-files/ESRI.txt">available here</a>. + * + * @param source the source of WKT definitions. + * @throws FactoryException if the definition file can not be read. + */ + public void load(final BufferedReader source) throws FactoryException { + ArgumentChecks.ensureNonNull("source", source); + lock.writeLock().lock(); + try { + final Loader loader = new Loader(source); + try { + loader.read(); + } catch (IOException e) { + throw new FactoryException(loader.canNotRead(null, e), e); + } catch (ParseException | IllegalArgumentException e) { + throw new FactoryDataException(loader.canNotRead(null, e), e); + } finally { + loader.restore(); + updateAuthority(); + } + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Implementation of {@link WKTDictionary#load(BufferedReader)} method. + * Caller must own the write lock before to instantiate and use this class. + */ + private final class Loader { + /** The source of WKT definitions. */ + private final BufferedReader source; + + /** Temporary buffer where to put the WKT to parse. */ + private final StringBuilder buffer; + + /** If the WKT being parsed is an alias, the alias key. Otherwise {@code null}. */ + private String aliasKey; + + /** Argument for {@link #addAliasOrDefinition()}. */ + private final ParsePosition pos; + + /** Zero-based number of current line. Equivalent to {@link LineNumberReader#getLineNumber()}. */ + private int lineNumber; + + /** Aliases that existed before in {@link #parser} before loading started. */ + private final Set<String> aliases; + + /** Creates a new loader. */ + Loader(final BufferedReader source) { + this.source = source; + buffer = new StringBuilder(500); + pos = new ParsePosition(0); + aliases = new HashSet<>(parser.getFragmentNames()); + } + + /** + * Restores {@link #parser} to its initial state. This method should be invoked + * in a finally block regardless if the parsing succeeded or failed. + */ + final void restore() { + parser.getFragmentNames().retainAll(aliases); + parser.clear(); + } + + /** + * Returns an error message saying "Can not read WKT at line X". The message is followed + * by a "Caused by" phrase specified either as a string or an exception. At least one of + * {@code cause} and {@code e} shall be non-null. + */ + final String canNotRead(String cause, final Exception e) { + final Locale locale = parser.getErrorLocale(); + if (cause == null) { + cause = Exceptions.getLocalizedMessage(e, locale); + } + return Resources.forLocale(locale).getString(Resources.Keys.CanNotParseWKT_2, getLineNumber(), cause); + } + + /** + * Returns the one-based line number of the last line read. + * Actually this method returns the zero-based line number of current position, + * but since current position is after the last line read, this is equivalent + * to line number of last line read + 1. + * + * @return one-based line number of current position. + */ + private int getLineNumber() { + if (source instanceof LineNumberReader) { + // In case an unusual implementation counts lines in a different way than we do. + lineNumber = ((LineNumberReader) source).getLineNumber(); + } + return lineNumber; + } + + /** + * Adds to the enclosing factory all definitions read from the given source. + * See {@link WKTDictionary#load(BufferedReader)} for a format description. + * + * @throws IOException if an error occurred while reading lines. + * @throws ParseException if an error occurred while parsing a WKT. + * @throws FactoryDataException if the file has a syntax error. + * @throws IllegalArgumentException if a {@code codespace:version:code} tuple or an alias is assigned twice. + */ + final void read() throws IOException, ParseException, FactoryDataException { + final String lineSeparator = System.lineSeparator(); + int indentation = 0; + String line; + while ((line = source.readLine()) != null) { + lineNumber++; + final int length = line.length(); + int defStart = CharSequences.skipLeadingWhitespaces(line, 0, length); + if (defStart < length && line.charAt(defStart) == '#') continue; // Skip comment lines. + /* + * If the line is indented compared to the first line, we presume that it is the continuation + * of previous line and skip the check for "SET" keyword. If the line is not indented, + * previous buffer content need to be parsed before we start a new WKT definition. + */ + if (defStart > indentation) { + defStart = indentation; + } else { + addAliasOrDefinition(); + indentation = defStart; + if (line.regionMatches(true, defStart, SET, 0, SET.length())) { + final int keyStart = CharSequences.skipLeadingWhitespaces(line, defStart + SET.length(), length); + if (keyStart > defStart) { // `true` if "SET" is followed by at least one white space. + defStart = line.indexOf('=', keyStart); + if (defStart <= keyStart) { + throw new FactoryDataException(resources().getString( + Resources.Keys.SyntaxErrorForAlias_1, getLineNumber())); + } + final int keyEnd = CharSequences.skipTrailingWhitespaces(line, keyStart, defStart); + defStart = CharSequences.skipLeadingWhitespaces(line, defStart + 1, length); + final String key = line.substring(keyStart, keyEnd); + if (!CharSequences.isUnicodeIdentifier(key)) { + String c = parser.errors().getString(Errors.Keys.NotAUnicodeIdentifier_1, key); + throw new FactoryDataException(canNotRead(c, null)); + } + aliasKey = key; + } + } + } + /* + * Copy non-empty lines in the buffer, omitting indentation and trailing spaces. + * The leading spaces after indentation are kept in order to have a more readable + * WKT string in error message if parsing fail. + */ + final int end = CharSequences.skipTrailingWhitespaces(line, defStart, length); + if (defStart < end) { + if (buffer.length() != 0) buffer.append(lineSeparator); + buffer.append(line, defStart, end); + } + } + addAliasOrDefinition(); + } + + /** + * Parses the current {@link #buffer} content as a WKT elements (possibly with children elements). + * This method does not build the full {@link IdentifiedObject}; this later part will be done only + * when first needed. + * + * <p>If {@link #aliasKey} is non-null, the first WKT is taken as a {@linkplain WKTFormat#addFragment + * fragment} associated to the given alias. All other WKT (if any) are taken as definitions of CRS or + * other objects.</p> + * + * @throws ParseException if an error occurred while parsing the WKT string. + * @throws FactoryDataException if there is unparsed text after the WKT. + * @throws IllegalArgumentException if a {@code codespace:version:code} tuple or an alias is assigned twice. + */ + private void addAliasOrDefinition() throws ParseException, FactoryDataException { + if (buffer.length() != 0) { + pos.setIndex(0); + final String wkt = buffer.toString(); + final Element root = parser.textToTree(wkt, pos); + final int end = pos.getIndex(); + if (end < wkt.length()) { // Trailing white spaces already removed by `read(…)`. + throw new FactoryDataException(resources().getString(Resources.Keys.UnexpectedTextAtLine_2, + getLineNumber(), CharSequences.token(wkt, end))); + } + if (aliasKey != null) { + parser.addFragment(aliasKey, root); + aliasKey = null; + } else { + addDefinition(root); + } + buffer.setLength(0); + } + } + } + + /** + * Adds the definition of a CRS (or other geodetic objects) from a tree of WKT elements. + * The authority code is inferred from the {@code ID[…]} or {@code AUTHORITY[…]} element. + * Caller must own the write lock before to invoke this method. + * {@link #updateAuthority()} should be invoked after this method. + * + * @param root root of a tree of WKT elements. + * @throws IllegalArgumentException if a {@code codespace:version:code} tuple is assigned twice. + * @throws FactoryDataException if the WKT does not have an {@code ID[…]} or {@code AUTHORITY[…]} element. + * + * @see #definitions + */ + private void addDefinition(final Element root) throws FactoryDataException { + final Element ide = root.peekLastElement(MathTransformParser.ID_KEYWORDS); + if (ide != null) { + final Object[] fullId = new Object[3]; // Codespace, code, version. + ide.peekValues(fullId); + final String codespace = trimOrNull(fullId[0]); + final String code = trimOrNull(fullId[1]); + if (code != null) { // Needs at least the code. + definitions.merge(code, root, (oldValue, newValue) -> { + final String version = trimOrNull(fullId[2]); + final Disambiguation previous; + if (oldValue instanceof Disambiguation) { + previous = (Disambiguation) oldValue; + } else if (oldValue instanceof Element) { + previous = new Disambiguation((Element) oldValue, fullId); + } else if (oldValue instanceof IdentifiedObject) { + previous = new Disambiguation((IdentifiedObject) oldValue); + } else { + previous = null; // Discard previous parsing failure. + } + return new Disambiguation(previous, codespace, version, code, (Element) newValue); + }); + codespaces.add(codespace); + if (authorities != null) { + final Element citation = ide.peekLastElement(WKTKeywords.Citation); + if (citation != null) { + final String title = trimOrNull(citation.peekValue()); + if (title != null) { + authorities.add(title); + } + } + } + return; + } + } + throw new FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1, root.peekValue())); + } + + /** + * Adds definitions of CRS (or other geodetic objects) from Well-Known Texts. Blank strings are ignored. + * Each non-blank {@link String} shall contain the complete definition of at least one geodetic object. + * More than one geodetic object can appear in the same {@link String} if they are separated by spaces + * or line separators. However the same geodetic object can not have its definition splitted in two or + * more {@link String}s. + * + * <p>The key associated to each object is given by the {@code ID[…]} or {@code AUTHORITY[…]} element, + * which is typically the last element of a WKT string and is mandatory. WKT strings can contain line + * separators for human readability.</p> + * + * @param objects CRS (or other geodetic objects) definitions as WKT strings. + * @throws FactoryException if a WKT can not be parsed, or does not contain an {@code ID[…]} or + * {@code AUTHORITY[…]} element, or if the same {@code codespace:version:code} tuple is + * used for two objects. + */ + public void addDefinitions(final Stream<String> objects) throws FactoryException { + ArgumentChecks.ensureNonNull("objects", objects); + /* + * We work with iterator because we do not support parallelism yet. + * However a future version may support that, which is why argument + * type is a `Stream`. + */ + final Iterator<String> it = objects.iterator(); + final ParsePosition pos = new ParsePosition(0); + lock.writeLock().lock(); + try { + int lineNumber = 1; + try { + while (it.hasNext()) { + final String wkt = it.next(); + final int length = CharSequences.skipTrailingWhitespaces(wkt, 0, wkt.length()); + while (pos.getIndex() < length) { + addDefinition(parser.textToTree(wkt, pos)); + } + pos.setIndex(0); + lineNumber++; + } + } catch (ParseException | IllegalArgumentException e) { + throw new FactoryDataException(resources().getString( + Resources.Keys.CanNotParseWKT_2, lineNumber, e.getLocalizedMessage())); + } finally { + updateAuthority(); + } + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Convenience methods for resources in the language used for error messages. + */ + private Resources resources() { + return Resources.forLocale(parser.getErrorLocale()); + } + + /** + * Trims the leading and trailing spaces of the string representation of given object. + * If null, empty or contains only spaces, then this method returns {@code null}. + */ + private static String trimOrNull(final Object value) { + return (value != null) ? Strings.trimOrNull(value.toString()) : null; + } + + /** + * Returns the authority or specification that defines the codes recognized by this factory. + * This is the first of the following values, in preference order: + * + * <ol> + * <li>The authority explicitly specified at construction time.</li> + * <li>A citation built from the most frequent value found in {@code CITATION} elements.</li> + * <li>A citation built from the most frequent value found in {@code ID} or {@code AUTHORITY} elements.</li> + * </ol> + * + * @return the organization responsible for CRS definitions, or {@code null} if unknown. + */ + @Override + public Citation getAuthority() { + return authority; + } + + /** + * Returns all namespaces recognized by this factory. Those namespaces can appear before codes in + * calls to {@code createFoo(String)} methods, for example {@code "ESRI"} in {@code "ESRI:102018"}. + * Namespaces are case-insensitive. + * + * @return the namespaces recognized by this factory. + */ + @Override + public Set<String> getCodeSpaces() { + lock.readLock().lock(); + try { + return JDK9.copyOf(codespaces); + } finally { + lock.readLock().unlock(); + } + } + + /** + * Returns an arbitrary object from a code. + * + * @param code value allocated by authority. + * @return the object for the given code. + * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. + * @throws FactoryException if the object creation failed for some other reason. + */ + @Override + public IdentifiedObject createObject(final String code) throws FactoryException { + /* + * Separate the authority from the rest of the code. The CharSequences.skipWhitespaces(…) + * methods are robust to negative index and will work even if code.indexOf(…) returned -1. + */ + String codespace = null; + String version = null; + String localCode = code; + int afterAuthority = code.indexOf(Constants.DEFAULT_SEPARATOR); + int end = CharSequences.skipTrailingWhitespaces(code, 0, afterAuthority); + int start = CharSequences.skipLeadingWhitespaces(code, 0, end); + if (start < end) { + codespace = code.substring(start, end); + /* + * Separate the version from the rest of the code. The version is optional. The code may have no room + * for version (e.g. "EPSG:4326"), or specify an empty version (e.g. "EPSG::4326"). If the version is + * equals to an empty string, it will be considered as no version. + */ + int afterVersion = code.indexOf(Constants.DEFAULT_SEPARATOR, ++afterAuthority); + start = CharSequences.skipLeadingWhitespaces(code, afterAuthority, afterVersion); + end = CharSequences.skipTrailingWhitespaces(code, start, afterVersion++); + if (start < end) { + version = code.substring(start, end); + } + start = Math.max(afterAuthority, afterVersion); + end = code.length(); + localCode = CharSequences.trimWhitespaces(code, start, end).toString(); + } + /* + * At this point we separated codespace, code and version. First, verify that codespace is valid. + * Then get CRS definition as an `IdentifiedObject` or an `Element` (the `Disambiguation` case is + * resolved as an `IdentifiedObject` or `Element`). + */ + Disambiguation choices = null; + Object value = null; + lock.readLock().lock(); + try { + boolean valid = (codespace == null || codespace.isEmpty() || codespaces.contains(codespace)); + if (!valid) { + for (final String cs : codespaces) { // More costly check if no exact match. + valid = cs.equalsIgnoreCase(codespace); + if (valid) break; + } + } + if (valid) { + value = definitions.get(localCode); + if (value instanceof Disambiguation) { + choices = Disambiguation.find((Disambiguation) value, codespace, version, localCode); + value = (choices != null) ? choices.value : null; + } + } + } finally { + lock.readLock().unlock(); + } + if (value == null) { + throw new NoSuchAuthorityCodeException(parser.errors().getString( + Errors.Keys.NoSuchValue_1, code), codespace, localCode, code); + } + /* + * At this point we got a value which may be one of the following classes: + * + * - `Element` — if this method is invoked for the first time for the given code. + * - `IdentifiedObject` — if we already built the geodetic object in a previous invocation of this method. + * - `String` — if a previous invocation for given code failed to build the geodetic object. + * In this case, the string is the exception message. + * + * If `Element`, try to replace that value by an `IdentifiedObject` (on success) or `String` (on failure). + * Must be done under write lock because `parser` is not thread-safe. + */ + if (value instanceof Element) { + lock.writeLock().lock(); + try { + if (choices != null) { + value = choices.value; // Check again in case value has been computed concurrently. + } else { + value = definitions.get(localCode); + } + if (value instanceof Element) { + ParseException cause = null; + try { + value = parser.parse((Element) value); + } catch (ParseException e) { + cause = e; + value = e.getLocalizedMessage(); + if (value == null) { + value = e.getClass().getSimpleName(); + } + } + if (choices != null) { + choices.value = value; // Save result for future uses. + } else { + definitions.put(localCode, value); + } + if (cause != null) { + throw new FactoryException(resources().getString( + Resources.Keys.CanNotInstantiateGeodeticObject_1, code), cause); + } + } + } finally { + lock.writeLock().unlock(); + } + } + if (value instanceof IdentifiedObject) { + return (IdentifiedObject) value; + } else { + // Exception message saved in a previous invocation of this method. + throw new FactoryException(String.valueOf(value)); + } + } + + /** + * Returns the set of authority codes for objects of the given type. + * The {@code type} argument specifies the base type of identified objects. + * + * @param type the spatial reference objects type. + * @return the set of authority codes for spatial reference objects of the given type. + * @throws FactoryException if an error occurred while fetching the codes. + */ + @Override + public Set<String> getAuthorityCodes(Class<? extends IdentifiedObject> type) throws FactoryException { + final Set<String> codes = new HashSet<>(Containers.hashMapCapacity(definitions.size())); + for (final Map.Entry<String,Object> entry : definitions.entrySet()) { + final String code = entry.getKey(); + final Object value = entry.getValue(); + if (value instanceof Disambiguation) { + Disambiguation.list((Disambiguation) value, code, codes); + } else { + codes.add(code); + } + } + return codes; + } +} diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java index f9ab3e9..6b03397 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java @@ -340,7 +340,7 @@ public class WKTFormat extends CompoundFormat<Object> { * * @see #errors() */ - private Locale getErrorLocale() { + final Locale getErrorLocale() { final Locale locale = getLocale(Locale.Category.DISPLAY); return (locale != null && locale != Locale.ROOT) ? locale : Locale.getDefault(Locale.Category.DISPLAY); } @@ -757,22 +757,32 @@ public class WKTFormat extends CompoundFormat<Object> { public void addFragment(final String name, final String wkt) throws IllegalArgumentException, ParseException { ArgumentChecks.ensureNonEmpty("wkt", wkt); ArgumentChecks.ensureNonEmpty("name", name); - short error = Errors.Keys.NotAUnicodeIdentifier_1; - if (CharSequences.isUnicodeIdentifier(name)) { - final ParsePosition pos = new ParsePosition(0); - final Element element = parseFragment(wkt, pos); - final int index = CharSequences.skipLeadingWhitespaces(wkt, pos.getIndex(), wkt.length()); - if (index < wkt.length()) { - throw new UnparsableObjectException(getErrorLocale(), Errors.Keys.UnexpectedCharactersAfter_2, - new Object[] {name + " = " + element.keyword + "[…]", CharSequences.token(wkt, index)}, index); - } - // `fragments` map has been created by `parser(true)`. - if (fragments.putIfAbsent(name, element) == null) { - return; - } - error = Errors.Keys.ElementAlreadyPresent_1; + if (!CharSequences.isUnicodeIdentifier(name)) { + throw new IllegalArgumentException(errors().getString(Errors.Keys.NotAUnicodeIdentifier_1, name)); + } + final ParsePosition pos = new ParsePosition(0); + final Element element = textToTree(wkt, pos); + final int length = wkt.length(); + final int index = CharSequences.skipLeadingWhitespaces(wkt, pos.getIndex(), length); + if (index < length) { + throw new UnparsableObjectException(getErrorLocale(), Errors.Keys.UnexpectedCharactersAfter_2, + new Object[] {name + " = " + element.keyword + "[…]", CharSequences.token(wkt, index)}, index); + } + addFragment(name, element); + } + + /** + * Adds a fragment of Well Know Text (WKT). + * Caller must have verified that {@code name} is a valid Unicode identifier. + * + * @param name the Unicode identifier to assign to the WKT fragment. + * @param element root of the WKT fragment to add. + * @throws IllegalArgumentException if a fragment is already associated to the given name. + */ + final void addFragment(final String name, final Element element) { + if (fragments(true).putIfAbsent(name, element) != null) { + throw new IllegalArgumentException(errors().getString(Errors.Keys.ElementAlreadyPresent_1, name)); } - throw new IllegalArgumentException(errors().getString(error, name)); } /** @@ -782,11 +792,19 @@ public class WKTFormat extends CompoundFormat<Object> { * @param pos index of the first character to parse (on input) or after last parsed character (on output). * @return root of the tree of elements. */ - final Element parseFragment(final String wkt, final ParsePosition pos) throws ParseException { + final Element textToTree(final String wkt, final ParsePosition pos) throws ParseException { if (sharedValues == null) { sharedValues = new HashMap<>(); } - return new Element(parser(true), wkt, pos, sharedValues); + return parser(true).textToTree(wkt, pos, sharedValues); + } + + /** + * Clears warnings and cache of shared values. + */ + final void clear() { + warnings = null; + sharedValues = null; } /** @@ -802,17 +820,36 @@ public class WKTFormat extends CompoundFormat<Object> { */ @Override public Object parse(final CharSequence wkt, final ParsePosition pos) throws ParseException { - warnings = null; - sharedValues = null; + clear(); ArgumentChecks.ensureNonEmpty("wkt", wkt); ArgumentChecks.ensureNonNull ("pos", pos); final AbstractParser parser = parser(false); Object object = null; try { - return object = parser.parseObject(wkt.toString(), pos); + object = parser.parseObject(wkt.toString(), pos); } finally { warnings = parser.getAndClearWarnings(object); } + return object; + } + + /** + * Creates an object from the given tree of WKT elements. + * + * @param root the tree of WKT elements. + * @return the parsed object (never {@code null}). + * @throws ParseException if an error occurred while parsing the WKT. + */ + final Object parse(final Element root) throws ParseException { + clear(); + final AbstractParser parser = parser(false); + Object object = null; + try { + object = parser.buildFromTree(root); + } finally { + warnings = parser.getAndClearWarnings(object); + } + return object; } /** @@ -822,6 +859,10 @@ public class WKTFormat extends CompoundFormat<Object> { */ private AbstractParser parser(final boolean modifiable) { AbstractParser parser = this.parser; + /* + * `parser` is always null on a fresh clone. However the `fragments` + * map may need to be cloned if the caller intents to modify it. + */ if (parser == null || (isCloned & modifiable)) { this.parser = parser = new Parser(symbols, fragments(modifiable), (NumberFormat) getFormat(Number.class), @@ -871,7 +912,7 @@ public class WKTFormat extends CompoundFormat<Object> { */ @Override public void format(final Object object, final Appendable toAppendTo) throws IOException { - warnings = null; + clear(); ArgumentChecks.ensureNonNull("object", object); ArgumentChecks.ensureNonNull("toAppendTo", toAppendTo); /* @@ -970,7 +1011,7 @@ public class WKTFormat extends CompoundFormat<Object> { /** * Convenience methods for resources for error message in the locale given by {@link #getLocale()}. */ - private Errors errors() { + final Errors errors() { return Errors.getResources(getErrorLocale()); } @@ -983,11 +1024,10 @@ public class WKTFormat extends CompoundFormat<Object> { @Override public WKTFormat clone() { final WKTFormat clone = (WKTFormat) super.clone(); - clone.sharedValues = null; + clone.clear(); clone.factories = null; // Not thread-safe; clone needs its own. clone.formatter = null; // Do not share the formatter. clone.parser = null; - clone.warnings = null; clone.isCloned = isCloned = true; // Symbols and Colors do not need to be cloned because they are flagged as immutable. return clone; diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt new file mode 100644 index 0000000..c1b56f3 --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt @@ -0,0 +1,69 @@ +# +# Example file for org.apache.sis.io.wkt.ReferencingFactory. +# This file provides Coordinate Reference System definitions +# in Well-Known Text (WKT) format. One or many files can be +# used for extending the set of CRS with custom definitions. +# +# Each Coordinate Reference System (or other geodetic object) +# is defined by a string in WKT format. The key associated to +# each object is given by the ID[…] or AUTHORITY[…] element, +# which is typically the last element of a WKT string and is +# mandatory for definitions in this file. +# +# WKT strings can span many lines. All lines after the first +# line shall be indented with at least one white space. +# Non-indented lines start new definitions. +# +# All lines starting with the # character are comment lines. +# +# ---- Aliases for WKT fragments --------------------------- +# Files with more than one WKT definition tend to repeat the +# same WKT fragments many times, e.g. the same BaseGeogCRS[…] +# element may be repeated in every ProjectedCRS definitions. +# Redundant fragments can be replaced by aliases for making +# the file more compact, easier to read, faster to parse and +# with smaller memory footprint. +# +# Each line starting with "SET <identifier>=<WKT>" defines +# an alias for a fragment of WKT string. The WKT can span +# many lines as described above. Aliases are local to the +# file where they are defined. Aliases can be expanded in +# other WKT strings by "$<identifier>". +# + +# +# Alias for WGS84 geographic CRS. +# Can be inserted in projected CRS with "$WGS84". +# +SET WGS84 = + BaseGeodCRS["GCS_WGS_1984", + Datum["D_WGS_1984", + Ellipsoid["WGS_1984", 6378137, 298.257223563]], + AngleUnit["Degree", 0.0174532925199433]] + +# +# Derived from https://github.com/Esri/projection-engine-db-doc +# with base CRS replaced by alias and parameter values omitted +# when they have the default value. +# +ProjectedCRS["North_Pole_Stereographic", + $WGS84, + Conversion["Stereographic North Pole", + Method["Polar Stereographic (variant A)"], + Parameter["Latitude of natural origin", 90]], + CS[Cartesian, 2], + Axis["Easting (E)", east], + Axis["Northing (N)", north], + Unit["metre", 1], + Id["ESRI", 102018]] + +ProjectedCRS["South_Pole_Stereographic", + $WGS84, + Conversion["Stereographic South Pole", + Method["Polar Stereographic (variant A)"], + Parameter["Latitude of natural origin", -90]], + CS[Cartesian, 2], + Axis["Easting (E)", east], + Axis["Northing (N)", north], + Unit["metre", 1], + Id["ESRI", 102021]] diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java index 2f21e4c..169fa18 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java @@ -64,7 +64,7 @@ * The {@link org.apache.sis.geometry.GeneralEnvelope} and {@link org.apache.sis.geometry.GeneralDirectPosition} classes * provide their own, limited, WKT parsing and formatting services for the {@code BOX} and {@code POINT} elements. * A description for this WKT format can be found on - * <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_coordinate_reference_systems">Wikipedia</a>. + * <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">Wikipedia</a>. * * <h2>Where to find WKT examples</h2> * An excellent source of well-formed WKT is the online <cite>EPSG Geodetic Parameter Registry</cite>. @@ -83,7 +83,7 @@ * @author Martin Desruisseaux (IRD, Geomatys) * @author Rémi Eve (IRD) * @author Rueben Schulz (UBC) - * @version 1.0 + * @version 1.1 * * @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html">WKT 2 specification</a> * @see <a href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html">Legacy WKT 1</a> diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java new file mode 100644 index 0000000..1e32651 --- /dev/null +++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java @@ -0,0 +1,104 @@ +/* + * 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.io.wkt; + +import java.util.Arrays; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import org.opengis.util.FactoryException; +import org.opengis.referencing.crs.SingleCRS; +import org.opengis.referencing.crs.ProjectedCRS; +import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.cs.AxisDirection; +import org.apache.sis.metadata.iso.citation.Citations; +import org.apache.sis.test.DependsOn; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.apache.sis.test.Assert.*; + + +/** + * Tests {@link WKTDictionary}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.1 + * @since 1.1 + * @module + */ +@DependsOn(WKTFormatTest.class) +public final strictfp class WKTDictionaryTest extends TestCase { + /** + * Tests {@link WKTDictionary#load(BufferedReader)}. + * + * @throws IOException if an error occurred while reading the test file. + * @throws FactoryException if an error occurred while parsing a WKT. + */ + @Test + public void testLoad() throws IOException, FactoryException { + final WKTDictionary factory = new WKTDictionary(null); + try (BufferedReader source = new BufferedReader(new InputStreamReader( + WKTFormatTest.class.getResourceAsStream("ExtraCRS.txt"), "UTF-8"))) + { + factory.load(source); + } + /* + * ESRI code space should be fist because it is the most frequently used + * in the test file. The authority should be "ESRI" for the same reason. + * Codes can be in any order. + */ + assertArrayEquals("getCodeSpaces()", new String[] {"ESRI", "MyCodeSpace"}, factory.getCodeSpaces().toArray()); + assertSame("getAuthority()", Citations.ESRI, factory.getAuthority()); + assertSetEquals(Arrays.asList("102018", "ESRI::102021", "MyCodeSpace::102021", "MyCodeSpace:v2:102021"), + factory.getAuthorityCodes(SingleCRS.class)); + /* + * Tests CRS creation. + */ + verifyCRS(factory.createProjectedCRS( "102018"), "North_Pole_Stereographic", +90); + verifyCRS(factory.createProjectedCRS("ESRI:102021"), "South_Pole_Stereographic", -90); + verifyCRS(factory.createGeographicCRS("MyCodeSpace::102021"), "Anguilla 1957"); + verifyCRS(factory.createGeographicCRS("MyCodeSpace:v2:102021"), "Anguilla 1957 (bis)"); + } + + /** + * Verifies a projected CRS. + * + * @param crs the CRS to verify. + * @param name expected CRS name. + * @param φ0 expected latitude of origin. + */ + private static void verifyCRS(final ProjectedCRS crs, final String name, final double φ0) { + assertEquals("name", name, crs.getName().getCode()); + assertAxisDirectionsEqual(name, crs.getCoordinateSystem(), + AxisDirection.EAST, AxisDirection.NORTH); + assertEquals("φ0", φ0, crs.getConversionFromBase().getParameterValues() + .parameter("Latitude of natural origin").doubleValue(), STRICT); + } + + /** + * Verifies a geographic CRS. + * + * @param crs the CRS to verify. + * @param name expected CRS name. + */ + private static void verifyCRS(final GeographicCRS crs, final String name) { + assertEquals("name", name, crs.getName().getCode()); + assertAxisDirectionsEqual(name, crs.getCoordinateSystem(), + AxisDirection.NORTH, AxisDirection.EAST); + } +} diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java index ac64c98..074d59f 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java @@ -222,6 +222,7 @@ import org.junit.BeforeClass; org.apache.sis.io.wkt.GeodeticObjectParserTest.class, org.apache.sis.io.wkt.WKTFormatTest.class, org.apache.sis.io.wkt.WKTParserTest.class, + org.apache.sis.io.wkt.WKTDictionaryTest.class, org.apache.sis.io.wkt.ComparisonWithEPSG.class, // Geodetic object creations from authority codes. diff --git a/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt b/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt new file mode 100644 index 0000000..06e7eb9 --- /dev/null +++ b/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt @@ -0,0 +1,65 @@ +# +# Test file for org.apache.sis.io.wkt.ReferencingFactory. +# See "doc-files/ESRI.txt" for file syntax. +# + +# +# Alias for WGS84 geographic CRS. +# +SET DEGREE = Unit["Degree", 0.0174532925199433] +SET WGS84 = + BaseGeodCRS["GCS_WGS_1984", + Datum["D_WGS_1984", + Ellipsoid["WGS_1984", 6378137, 298.257223563]], + $DEGREE] + + +# +# Derived from https://github.com/Esri/projection-engine-db-doc +# with base CRS replaced by alias and parameter values omitted +# when they have the default value. +# +ProjectedCRS["North_Pole_Stereographic", + $WGS84, + Conversion["Stereographic North Pole", + Method["Polar Stereographic (variant A)"], + Parameter["Latitude of natural origin", 90]], + CS[Cartesian, 2], + Axis["Easting (E)", east], + Axis["Northing (N)", north], + Unit["metre", 1], + Id["ESRI", 102018]] + +ProjectedCRS["South_Pole_Stereographic", + $WGS84, + Conversion["Stereographic South Pole", + Method["Polar Stereographic (variant A)"], + Parameter["Latitude of natural origin", -90]], + CS[Cartesian, 2], + Axis["Easting (E)", east], + Axis["Northing (N)", north], + Unit["metre", 1], + Id["ESRI", 102021]] + +# +# A dummy CRS using the same code than ESRI::102021 but +# different code space and versions. Used for testing +# resolution of code collisions. +# +GeodCRS["Anguilla 1957", + Datum["Anguilla 1957", + Ellipsoid["Clarke 1880", 6378249.145, 293.465]], + CS[ellipsoidal, 2], + Axis["Latitude", north], + Axis["Longitude", east], + $DEGREE, + Id["MyCodeSpace", 102021]] + +GeodCRS["Anguilla 1957 (bis)", + Datum["Anguilla 1957", + Ellipsoid["Clarke 1880", 6378249.145, 293.465]], + CS[ellipsoidal, 2], + Axis["Latitude", north], + Axis["Longitude", east], + $DEGREE, + Id["MyCodeSpace", 102021, "v2"]] diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java index 054f4f0..fa9c45a 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Set; import java.util.List; import java.util.Collections; +import java.util.HashSet; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -100,7 +101,18 @@ public final class JDK9 { } /** - * Placeholder for {@code Map.of(...)} (actually a JDK10 method). + * Placeholder for {@code Set.copyOf(...)} (actually a JDK10 method). + */ + public static <V> Set<V> copyOf(final Set<V> set) { + switch (set.size()) { + case 0: return Collections.emptySet(); + case 1: return Collections.singleton(set.iterator().next()); + default: return new HashSet<>(set); + } + } + + /** + * Placeholder for {@code Map.copyOf(...)} (actually a JDK10 method). */ public static <K,V> Map<K,V> copyOf(final Map<K,V> map) { return map.size() < 2 ? CollectionsExt.compact(map) : new HashMap<>(map); diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java index 2acad54..94d8c5d 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java @@ -54,6 +54,31 @@ public final class Strings extends Static { } /** + * Returns whether the given strings are equal, ignoring case. + * This method accepts null arguments. + * + * @param a first string. + * @param b another string to be compared with {@code a}. + * @return whether the given strings are equal, ignoring case. + * + * @see java.util.Objects#equals(Object, Object) + * @see String#equalsIgnoreCase(String) + */ + public static boolean equalsIgnoreCase(final String a, final String b) { + return (a == b) || (a != null && a.equalsIgnoreCase(b)); + } + + /** + * Returns the given text is non-null, or the empty string otherwise. + * + * @param text text or null. + * @return given text or empty string (never null). + */ + public static String orEmpty(final String text) { + return (text != null) ? text : ""; + } + + /** * Trims the leading and trailing spaces of the given string. * If the string is null, empty or contains only spaces, then this method returns {@code null}. *
