sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Refactor the way we store WKT trees in a dictionary. Instead of having `Element` working in two modes (mutable or immutable), keep `Element` always mutable and create immutable snapshots with a separated class, `StoredTree`.
Date Tue, 17 Nov 2020 23:39:38 GMT
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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new b890c8c  Refactor the way we store WKT trees in a dictionary. Instead of having `Element` working in two modes (mutable or immutable), keep `Element` always mutable and create immutable snapshots with a separated class, `StoredTree`.
b890c8c is described below

commit b890c8c982630de77f77f9a886713f950141fef0
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Nov 18 00:36:47 2020 +0100

    Refactor the way we store WKT trees in a dictionary.
    Instead of having `Element` working in two modes (mutable or immutable),
    keep `Element` always mutable and create immutable snapshots with a separated class, `StoredTree`.
---
 .../java/org/apache/sis/io/wkt/AbstractParser.java | 205 +++++----
 .../main/java/org/apache/sis/io/wkt/Element.java   | 287 ++++++------
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |  12 +-
 .../org/apache/sis/io/wkt/MathTransformParser.java |   4 +-
 .../java/org/apache/sis/io/wkt/StoredTree.java     | 483 +++++++++++++++++++++
 .../java/org/apache/sis/io/wkt/WKTDictionary.java  | 116 +++--
 .../main/java/org/apache/sis/io/wkt/WKTFormat.java |  65 +--
 .../java/org/apache/sis/io/wkt/doc-files/ESRI.txt  |   6 +-
 .../referencing/factory/GeodeticObjectFactory.java |   1 +
 .../transform/DefaultMathTransformFactory.java     |   1 +
 .../java/org/apache/sis/io/wkt/ElementTest.java    |  31 +-
 .../sis/io/wkt/GeodeticObjectParserTest.java       |   4 +-
 .../apache/sis/io/wkt/MathTransformParserTest.java |   4 +-
 .../org/apache/sis/io/wkt/WKTDictionaryTest.java   |  78 +++-
 .../resources/org/apache/sis/io/wkt/ExtraCRS.txt   |  44 +-
 15 files changed, 922 insertions(+), 419 deletions(-)

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 b255587..09d9e77 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
@@ -37,7 +37,6 @@ import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.measure.Units;
 import org.apache.sis.measure.UnitFormat;
-import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
@@ -53,11 +52,11 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * <p>In current version, parsers are not intended to be subclassed outside this package.</p>
  *
  * <p>Parsers are not synchronized. It is recommended to create separate parser instances for each thread.
- * If multiple threads access a parser concurrently, it must be synchronized externally.</p>
+ * If many threads access the same parser instance concurrently, it must be synchronized externally.</p>
  *
  * @author  Rémi Eve (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.1
  * @since   0.6
  * @module
  */
@@ -71,19 +70,21 @@ abstract class AbstractParser implements Parser {
 
     /**
      * A mode for the {@link Element#pullElement(int, String...)} method meaning that the requested element
-     * is optional but is not necessarily first. If no element have a name matching one of the requested names,
+     * is optional but not necessarily first. If no element has a name matching one of the requested names,
      * then {@code pullElement(…)} returns {@code null}.
      */
     static final int OPTIONAL = 1;
 
     /**
      * A mode for the {@link Element#pullElement(int, String...)} method meaning that an exception shall be
-     * thrown if no element have a name matching one of the requested names.
+     * thrown if no element has a name matching one of the requested names.
      */
     static final int MANDATORY = 2;
 
     /**
-     * The locale for error messages (not for number parsing), or {@code null} for the system default.
+     * The locale for formatting error messages if parsing fails, or {@code null} for system default.
+     * This is <strong>not</strong> the locale for parsing number or date values.
+     * The locale for numbers and dates is contained in {@link #symbols}.
      */
     final Locale errorLocale;
 
@@ -115,20 +116,25 @@ abstract class AbstractParser implements Parser {
 
     /**
      * Reference to the {@link WKTFormat#fragments} map, or an empty map if none.
-     * This parser will only read this map, never write to it.
+     * Shall be used in read-only mode; never write through this reference.
+     *
+     * @see WKTFormat#addFragment(String, StoredTree)
      */
-    final Map<String,Element> fragments;
+    final Map<String,StoredTree> fragments;
 
     /**
      * Keyword of unknown elements. The ISO 19162 specification requires that we ignore unknown elements,
-     * but we will nevertheless report them as warnings.
-     * The meaning of this map is:
+     * but we will nevertheless report them as {@linkplain #warnings}. The meaning of this map is:
+     *
      * <ul>
      *   <li><b>Keys</b>: keyword of ignored elements. Note that a key may be null.</li>
      *   <li><b>Values</b>: keywords of all elements containing an element identified by the above-cited key.
      *       This list is used for helping the users to locate the ignored elements.</li>
      * </ul>
      *
+     * Content of this map is not discarded immediately {@linkplain #getAndClearWarnings(Object) after parsing}.
+     * It is kept for some time because {@link Warnings} will copy its content only when first needed.
+     *
      * @see #getAndClearWarnings(Object)
      */
     final Map<String, List<String>> ignoredElements;
@@ -136,6 +142,7 @@ abstract class AbstractParser implements Parser {
     /**
      * The warning (other than {@link #ignoredElements}) that occurred during the parsing.
      * Created when first needed and reset to {@code null} when a new parsing start.
+     * Warnings are reported when {@link #getAndClearWarnings(Object)} is invoked.
      */
     private Warnings warnings;
 
@@ -149,8 +156,8 @@ abstract class AbstractParser implements Parser {
      * @param  unitFormat    the unit format provided by {@link WKTFormat}, or {@code null} for a default format.
      * @param  errorLocale   the locale for error messages (not for parsing), or {@code null} for the system default.
      */
-    AbstractParser(final Symbols symbols, final Map<String,Element> fragments, NumberFormat numberFormat,
-            final DateFormat dateFormat, final UnitFormat unitFormat, final Locale errorLocale)
+    AbstractParser(final Symbols symbols, final Map<String,StoredTree> fragments, NumberFormat numberFormat,
+                   final DateFormat dateFormat, final UnitFormat unitFormat, final Locale errorLocale)
     {
         ensureNonNull("symbols", symbols);
         if (numberFormat == null) {
@@ -184,23 +191,44 @@ abstract class AbstractParser implements Parser {
 
     /**
      * Returns the name of the class providing the publicly-accessible {@code createFromWKT(String)} method.
-     * This information is used for logging purpose only.
+     * This information is used for logging purposes only. Values can be:
+     *
+     * <ul>
+     *   <li>{@code "org.apache.sis.io.wkt.WKTFormat"}</li>
+     *   <li>{@code "org.apache.sis.referencing.factory.GeodeticObjectFactory"}</li>
+     *   <li>{@code "org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory"}</li>
+     * </ul>
      */
     abstract String getPublicFacade();
 
     /**
      * Returns the name of the method invoked from {@link #getPublicFacade()}.
-     * This information is used for logging purpose only.
+     * This information is used for logging purposes only.
+     * Another possible value is {@codd "parse"}.
      */
     String getFacadeMethod() {
         return "createFromWKT";
     }
 
     /**
-     * Creates the object from a string and log the warnings if any.
-     * This method is for implementation of {@code createFromWKT(String)} method is SIS factories only.
+     * Logs the given record for a warning that occurred during parsing.
+     * This is used when we can not use the {@link #warning warning methods},
+     * or when the information is not worth to report as a warning.
+     */
+    final void log(final LogRecord record) {
+        Logger logger = Logging.getLogger(Loggers.WKT);
+        record.setSourceClassName (getPublicFacade());
+        record.setSourceMethodName(getFacadeMethod());
+        record.setLoggerName(logger.getName());
+        logger.log(record);
+    }
+
+    /**
+     * Creates the object from a WKT string and logs the warnings if any.
+     * This method is for implementation of {@code createFromWKT(String)} method in SIS factories only.
+     * Callers should ensure that {@code wkt} is non-null and non-empty (this method does not verify).
      *
-     * @param  text  coordinate system encoded in Well-Known Text format (version 1 or 2).
+     * @param  wkt  object encoded in Well-Known Text format (version 1 or 2).
      * @return the result of parsing the given text.
      * @throws FactoryException if the object creation failed.
      *
@@ -208,34 +236,39 @@ abstract class AbstractParser implements Parser {
      * @see org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#createFromWKT(String)
      */
     @Override
-    public final Object createFromWKT(final String text) throws FactoryException {
-        final Object value;
+    public final Object createFromWKT(final String wkt) throws FactoryException {
+        Object result = null;
+        Warnings warnings;
         try {
-            value = parseObject(text, new ParsePosition(0));
+            result = createFromWKT(wkt, new ParsePosition(0));
         } catch (ParseException exception) {
             final Throwable cause = exception.getCause();
             if (cause instanceof FactoryException) {
                 throw (FactoryException) cause;
             }
             throw new FactoryException(exception.getLocalizedMessage(), exception);
+        } finally {
+            warnings = getAndClearWarnings(result);
         }
-        final Warnings warnings = getAndClearWarnings(value);
         if (warnings != null) {
             log(new LogRecord(Level.WARNING, warnings.toString()));
         }
-        return value;
+        return result;
     }
 
     /**
-     * Logs the given record. This is used only when we can not use the {@link #warning warning methods},
-     * or when the information is not worth to report as a warning.
+     * Parses a <cite>Well-Know Text</cite> from specified position as a geodetic object.
+     * Caller should invoke {@link #getAndClearWarnings(Object)} in a {@code finally} block
+     * after this method.
+     *
+     * @return the parsed object.
+     * @throws ParseException if the string can not be parsed.
      */
-    final void log(final LogRecord record) {
-        Logger logger = Logging.getLogger(Loggers.WKT);
-        record.setSourceClassName(getPublicFacade());
-        record.setSourceMethodName(getFacadeMethod());
-        record.setLoggerName(logger.getName());
-        logger.log(record);
+    Object createFromWKT(final String text, final ParsePosition position) throws ParseException {
+        final Element root = new Element(textToTree(text, position));
+        final Object result = buildFromTree(root);
+        root.close(ignoredElements);
+        return result;
     }
 
     /**
@@ -243,92 +276,47 @@ abstract class AbstractParser implements Parser {
      * Current implementation assumes that the fragment name is a Unicode identifier,
      * except for the first character which is not required to be an identifier start.
      */
-    static int endOfFragmentName(final String text, int upper) {
+    static int endOfFragmentName(final String text, int position) {
         final int length = text.length();
-        while (upper < length) {
-            final int c = text.codePointAt(upper);
+        while (position < length) {
+            final int c = text.codePointAt(position);
             if (!Character.isUnicodeIdentifierPart(c)) break;
-            upper += Character.charCount(c);
+            position += Character.charCount(c);
         }
-        return upper;
+        return position;
     }
 
     /**
-     * Parses a <cite>Well Know Text</cite> (WKT) as a tree of {@link Element}s.
+     * Parses the <cite>Well Know Text</cite> from specified position 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.
-     * @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.
+     * @param  wkt       the Well-Known Text to be parsed.
+     * @param  position  before parsing, provides index of the first character to parse in the {@code wkt} string.
+     *                   After parsing completion, provides index after the last character parsed.
      * @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)
      */
-    final Element textToTree(final String text, final ParsePosition position, final Map<Object,Object> sharedValues)
-            throws ParseException
-    {
+    final Element textToTree(final String wkt, final ParsePosition position) throws ParseException {
+        int lower = CharSequences.skipLeadingWhitespaces(wkt, position.getIndex(), wkt.length());
+        if (lower >= wkt.length() || wkt.charAt(lower) != Symbols.FRAGMENT_VALUE) {
+            return new Element(this, wkt, position);    // This is the usual case.
+        }
         /*
-         * 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.
+         * Aliases for fragments (e.g. "FOO" in ProjectedCRS["something", $FOO]) are expanded by `Element`
+         * constructor invoked above, except if the alias appears at the begining of the WKT string.
+         * In such case the alias is the whole text and is handled in a special way below.
          */
-        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);                       // Should be immutable.
-            if (fragment == null) {
-                position.setErrorIndex(lower);
-                throw new UnparsableObjectException(errorLocale, Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
-            }
-            position.setIndex(upper);
-            if (sharedValues == null) {                         // `true` if invoked for immediate parsing.
-                fragment = fragment.modifiable();               // Parsing requires a modifiable copy.
-            }
-        } else {
-            fragment = new Element(this, text, position, sharedValues);
+        final int upper = endOfFragmentName(wkt, ++lower);
+        final String id = wkt.substring(lower, upper);
+        StoredTree fragment = fragments.get(id);
+        if (fragment == null) {
+            position.setErrorIndex(--lower);
+            throw new UnparsableObjectException(errorLocale, Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
         }
-        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;
+        position.setIndex(upper);
+        return fragment.toElement(this, ~0);
     }
 
     /**
@@ -340,7 +328,7 @@ abstract class AbstractParser implements Parser {
      * @return the parsed object.
      * @throws ParseException if the element can not be parsed.
      */
-    abstract Object parseObject(final Element element) throws ParseException;
+    abstract Object buildFromTree(Element element) throws ParseException;
 
     /**
      * Parses the number at the given position.
@@ -378,7 +366,8 @@ abstract class AbstractParser implements Parser {
     }
 
     /**
-     * Parses the given unit name or symbol.
+     * Parses the given unit name or symbol. Contrarily to other {@code parseFoo()} methods,
+     * this method has no {@link ParsePosition} and expects the given string to be the full unit symbol.
      */
     final Unit<?> parseUnit(final String text) throws ParserException {
         if (unitFormat == null) {
@@ -424,12 +413,13 @@ abstract class AbstractParser implements Parser {
      * Returns the warnings, or {@code null} if none.
      * This method clears the warnings after the call.
      *
-     * <p>The returned object is valid only before a new parsing starts. If a longer lifetime is desired,
-     * then the caller <strong>must</strong> invokes {@link Warnings#publish()}.</p>
+     * <p>The returned object is valid only until a new parsing starts. If a longer lifetime
+     * is desired, then the caller <strong>must</strong> invokes {@link Warnings#publish()}.</p>
      *
-     * @param  object  the object that resulted from the parsing operation, or {@code null}.
+     * @param  result  the object that resulted from the parsing operation, or {@code null}.
+     * @return the warnings, or {@code null} if none.
      */
-    final Warnings getAndClearWarnings(final Object object) {
+    final Warnings getAndClearWarnings(final Object result) {
         Warnings w = warnings;
         warnings = null;
         if (w == null) {
@@ -437,8 +427,9 @@ abstract class AbstractParser implements Parser {
                 return null;
             }
             w = new Warnings(errorLocale, true, ignoredElements);
+            // Do not clear `ignoredElements` now.
         }
-        w.setRoot(object);
+        w.setRoot(result);
         return w;
     }
 }
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 938dcce..711170d 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
@@ -20,11 +20,9 @@ import java.util.Date;
 import java.util.Map;
 import java.util.List;
 import java.util.LinkedList;
-import java.util.ListIterator;
 import java.util.Iterator;
 import java.util.Collections;
 import java.util.Locale;
-import java.io.Serializable;
 import java.text.ParsePosition;
 import java.text.ParseException;
 import org.opengis.referencing.cs.CoordinateSystem;
@@ -35,7 +33,6 @@ import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.referencing.WKTKeywords;
 import org.apache.sis.internal.util.CollectionsExt;
-import org.apache.sis.internal.util.UnmodifiableArrayList;
 
 import static org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
 
@@ -50,7 +47,14 @@ import static org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
  *
  * Each {@code Element} object can contain an arbitrary amount of other elements.
  * The result is a tree, which can be seen with {@link #toString()} for debugging purpose.
- * Elements can be pulled in a <cite>first in, first out</cite> order.
+ * Elements can be pulled by their name and other children (numbers, dates, strings)
+ * can be pulled in a <cite>first in, first out</cite> order.
+ *
+ * <h2>Sharing repetitive information</h2>
+ * {@link Element} instances are mutable because {@link AbstractParser} needs to remove elements from
+ * the {@link #children} list as they are processed. If that parsing does not happen immediately,
+ * then {@code Element} content needs to be copied in a different structure ({@link StoredTree})
+ * which is immutable and designed for reducing redundancies.
  *
  * @author  Rémi Ève (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
@@ -58,12 +62,7 @@ import static org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
  * @since   0.6
  * @module
  */
-final class Element implements Serializable {
-    /**
-     * Indirectly for {@link WKTFormat} serialization compatibility.
-     */
-    private static final long serialVersionUID = -7345192763818308443L;
-
+final class Element {
     /**
      * Kind of value expected in the element. Value 0 means "not yet determined".
      */
@@ -79,123 +78,126 @@ final class Element implements Serializable {
     };
 
     /**
-     * The position where this element starts in the string to be parsed.
+     * Index of the character where this element starts in the WKT string to parse.
+     *
+     * @see #offsetAfterKeyword()
      */
     final int offset;
 
     /**
      * Index of the keyword in the array given to the {@link #pullElement(int, String...)} method.
+     * This is a workaround for the lack of multiple return values in Java.
      *
      * @see #getKeywordIndex()
      */
     private byte keywordIndex;
 
     /**
-     * Keyword of this entity. For example: {@code "PrimeMeridian"}.
+     * Keyword of the WKT element, for example {@code "PrimeMeridian"}. Never {@code null}.
      */
     public final String keyword;
 
     /**
      * {@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 and {@link #isImmutable} should be {@code true}.
+     * If {@code true}, then {@link #children} shall be an empty list.
      */
     private final boolean isEnumeration;
 
     /**
-     * Whether this element is immutable.
+     * {@code true} if this element has been reconstituted from {@link WKTFormat#fragments}.
+     * In such case, all {@link #offset} values are identical.
      */
-    private final boolean isImmutable;
+    final boolean isFragment;
 
     /**
      * 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.
+     * Parsing will remove elements (in any order) from this list as they are consumed.
+     *
+     * @see #getChildren()
      */
     private final List<Object> children;
 
     /**
      * The locale to be used for formatting an error message if the parsing fails, or {@code null} for
      * the system default. This is <strong>not</strong> the locale for parting number or date values.
+     *
+     * <div class="note"><b>Design note:</b>
+     * the same reference is duplicated in every {@code Element} instances. We nevertheless copy it
+     * as a convenience for avoiding to make this argument appears in the {@code pullFoo(…)} methods.</div>
      */
     private final Locale errorLocale;
 
     /**
-     * Constructs a root element.
+     * Constructs a root element as a modifiable wrapper around the given element.
+     * This wrapper is a convenience for branching on different codes depending on
+     * the keyword value. For example:
+     *
+     * {@preformat java
+     *    Element wrapper = new Element(an_element_with_unknown_keyword);
+     *    Element e = wrapper.pullElement(…, "ProjectedCRS");
+     *    if (e != null) {
+     *        // Do something specific to projected CRS.
+     *        return;
+     *    }
+     *    e = wrapper.pullElement(…, "GeographicCRS");
+     *    if (e != null) {
+     *        // Do something specific to Geographic CRS.
+     *        return;
+     *    }
+     *    // etc.
+     * }
+     *
+     * @param  singleton  the only child for this root.
      *
-     * @param name       an arbitrary name for the root element.
-     * @param singleton  the only child for this root.
+     * @see #pullElement(int, String...)
      */
-    Element(final String name, final Element singleton) {
-        keyword       = name;
+    Element(Element singleton) {
+        keyword       = "<root>";               // Ignored (any arbitrary name is okay)
         offset        = singleton.offset;
         errorLocale   = singleton.errorLocale;
         isEnumeration = false;
-        isImmutable   = false;
-        children      = new LinkedList<>();                     // Needs to be a modifiable collection.
-        children.add(singleton.modifiable());
+        isFragment    = false;
+        children      = new LinkedList<>();     // Needs to be a modifiable collection.
+        children.add(singleton);
     }
 
     /**
-     * Creates a modifiable copy of the given element.
-     * Modifiable instances are needed by the WKT parser.
+     * Creates a new node for the given keyword and list of children. This is used by {@link StoredTree}
+     * for recreating a tree of {@link Element}s from a previously saved snapshot.
      *
-     * @see #modifiable()
-     */
-    private Element(final Element toCopy) {
-        offset        = toCopy.offset;
-        keyword       = toCopy.keyword;
-        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.isImmutable) {
-                    it.set(new Element(fragment));
-                }
-            }
-        }
-    }
-
-    /**
-     * 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;
+     * @param  keyword   keyword of the WKT element, e.g. {@code "PrimeMeridian"}. Shall not be {@code null}.
+     * @param  children  children of this element, or {@code null} if this element is an enumeration.
+     * @param  offset    index of the character where this element started in the WKT string.
+     *         If negative, actual offset is {@code ~offset} and {@link #isFragment} is set to {@code true}.
+     */
+    // Children intentionally forced to LinkedList type for consistency with next constructor.
+    Element(final String keyword, final LinkedList<Object> children, final int offset, final Locale errorLocale) {
+        this.keyword       = keyword;
+        this.isEnumeration = (children == null);
+        this.children      = isEnumeration ? Collections.emptyList() : children;
+        this.isFragment    = (offset < 0);
+        this.offset        = isFragment ? ~offset : offset;
+        this.errorLocale   = errorLocale;
     }
 
     /**
-     * Constructs a new {@code Element}.
-     * The {@code sharedValues} argument have two meanings:
+     * Constructs a new {@code Element} by parsing the given WKT string starting at the given position.
      *
-     * <ul class="verbose">
-     *   <li>If {@code null}, then the caller is parsing a WKT string. The {@code Element}
-     *     must be mutable because its content will be emptied as the parsing progress.</li>
-     *
-     *   <li>If non-null, then the caller is storing a WKT fragment. We create the elements but the caller will
-     *     not parse them immediately. The {@code Element} should be immutable because the fragment will potentially
-     *     be reused many time. Since the fragment may be stored for a long time, the {@code sharedValues} map will
-     *     be used for sharing unique instance of each value if possible.</li>
-     * </ul>
-     *
-     * @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  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
-    {
+     * @param  parser    information about symbols (such as brackets) and formats to use.
+     * @param  text      the Well-Known Text (WKT) to parse.
+     * @param  position  on input, the position where to start parsing from.
+     *                   On output, the first character after the separator.
+     * @throws ParseException if quotes, brackets or parenthesis are not balanced, or a date/number
+     *         can not be parsed, or a referenced WKT fragment (e.g. {@code "$FOO"}) can not be found.
+     */
+    Element(final AbstractParser parser, final String text, final ParsePosition position) throws ParseException {
+        isFragment  = false;
+        errorLocale = parser.errorLocale;
         /*
          * Find the first keyword in the specified string. If a keyword is found, then
          * the position is set to the index of the first character after the keyword.
          */
-        errorLocale = parser.errorLocale;
         offset = position.getIndex();
         final int length = text.length();
         int lower = skipLeadingWhitespaces(text, offset, length);
@@ -227,9 +229,8 @@ final class Element implements Serializable {
                                 openingBracket = text.codePointAt(lower))) < 0)
         {
             position.setIndex(lower);
-            this.children = Collections.emptyList();
+            children = Collections.emptyList();
             isEnumeration = true;
-            isImmutable   = true;
             return;
         }
         lower = skipLeadingWhitespaces(text, lower + Character.charCount(openingBracket), length);
@@ -242,32 +243,35 @@ final class Element implements Serializable {
          *   - Otherwise, if the first character is a unicode identifier start, then the element is parsed as a chid Element.
          *   - Otherwise, if the first character is a quote, then the value is taken as a String.
          *   - Otherwise, the element is parsed as a number or as a date, depending of 'isTemporal' boolean value.
+         *
+         * A `LinkedList` implementation is suitable: we will always use iterators (never random access)
+         * and the parser will delete elements at any point during iteration.
          */
-        final List<Object> children = new LinkedList<>();
+        children = new LinkedList<>();
+        isEnumeration = false;
         final String separator = parser.symbols.trimmedSeparator();
         while (lower < length) {
             final int firstChar = text.codePointAt(lower);
             if (firstChar == Symbols.FRAGMENT_VALUE) {
                 /*
+                 * ══════════ ALIAS ════════════════════════════════════════════════════════════════════════════════
                  * WKTFormat allows to substitute strings like "$FOO" by a WKT fragment. This is something similar
                  * to environment variables in Unix. If we find the "$" character, get the identifier behind "$"
                  * and insert the corresponding WKT fragment here.
                  */
                 final int upper = AbstractParser.endOfFragmentName(text, ++lower);
-                final String id = text.substring(lower, upper);
-                Element fragment = parser.fragments.get(id);
+                final String id = text.substring(lower--, upper);
+                StoredTree fragment = parser.fragments.get(id);
                 if (fragment == null) {
                     position.setIndex(offset);
                     position.setErrorIndex(lower);
                     throw new UnparsableObjectException(errorLocale, Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
                 }
-                if (sharedValues == null) {                         // `true` if created for immediate parsing.
-                    fragment = fragment.modifiable();               // WKT parser needs modifiable elements.
-                }
-                children.add(fragment);
+                children.add(fragment.toElement(parser, ~lower));               // Set offset to '$' in "$FOO".
                 lower = upper;
             } else if (Character.isUnicodeIdentifierStart(firstChar)) {
                 /*
+                 * ══════════ ELEMENT or BOOLEAN ═══════════════════════════════════════════════════════════════════
                  * If the character is the beginning of a Unicode identifier, add as a child element
                  * except for the boolean "true" and "false" values which are handled in a special way.
                  */
@@ -277,14 +281,16 @@ final class Element implements Serializable {
                     children.add(Boolean.FALSE);
                 } else {
                     position.setIndex(lower);
-                    children.add(new Element(parser, text, position, sharedValues));
+                    children.add(new Element(parser, text, position));
                     lower = position.getIndex();
                 }
             } else {
-                Object value;
+                // ══════════ PRIMITIVES (STRING, NUMBER, DATE, etc.) ══════════════════════════════════════════════
+                final Object value;
                 final int closingQuote = parser.symbols.matchingQuote(firstChar);
                 if (closingQuote >= 0) {
                     /*
+                     * ══════════ STRING ═══════════════════════════════════════════════════════════════════════════
                      * Try to parse the next element as a quoted string. We will take it as a string if the first non-blank
                      * character is a quote.  Note that a double quote means that the quote should be included as-is in the
                      * parsed text.
@@ -310,19 +316,20 @@ final class Element implements Serializable {
                             }
                             ((StringBuilder) content).appendCodePoint(closingQuote).append(text, lower, upper);
                         }
-                        lower = upper + n;  // After the closing quote.
+                        lower = upper + n;                          // After the closing quote.
                     } while (lower < text.length() && text.codePointAt(lower) == closingQuote);
                     /*
-                     * Leading and trailing spaces should be ignored according ISO 19162 §B.4.
+                     * Leading and trailing spaces should be ignored according ISO 19162 annex.
                      * Note that the specification suggests also to replace consecutive white
                      * spaces by a single space, but we don't do that yet.
                      */
                     value = CharSequences.trimWhitespaces(content).toString();
                 } else {
                     /*
+                     * ══════════ NUMBER or DATE ═══════════════════════════════════════════════════════════════════
                      * Try to parse the next element as a date or a number. We attempt such parsing when
                      * the first non-blank character is not the beginning of an unicode identifier.
-                     * Otherwise we assume that the next element is the keyword of a child 'Element'.
+                     * Otherwise we assume that the next element is the keyword of a child `Element`.
                      */
                     position.setIndex(lower);
                     if (valueType == 0) {
@@ -339,15 +346,6 @@ final class Element implements Serializable {
                     }
                     lower = position.getIndex();
                 }
-                /*
-                 * 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);
-                    if (e != null) {
-                        value = e;
-                    }
-                }
                 children.add(value);
             }
             /*
@@ -362,10 +360,7 @@ final class Element implements Serializable {
                 final int c = text.codePointAt(lower);
                 if (c == closingBracket) {
                     position.setIndex(lower + Character.charCount(c));
-                    isEnumeration = false;
-                    isImmutable   = (sharedValues != null);
-                    this.children = isImmutable ? UnmodifiableArrayList.wrap(children.toArray()) : children;
-                    return;
+                    return;                         // End of parsing does not need to be end of string.
                 }
                 position.setErrorIndex(lower);
                 throw unparsableString(text, position);
@@ -402,7 +397,7 @@ final class Element implements Serializable {
      * <code>"Error in &lt;{@link #keyword}&gt;"</code> will be prepend to the message.
      * The error index will be the starting index of this {@code Element}.
      *
-     * @param  cause  the cause of the failure, or {@code null} if none.
+     * @param  cause   the cause of the failure, or {@code null} if none.
      * @return the exception to be thrown.
      */
     final ParseException parseFailed(final Exception cause) {
@@ -413,8 +408,8 @@ final class Element implements Serializable {
     /**
      * Returns a {@link ParseException} with a "Unparsable string" message.
      * The error message is built from the specified string starting at the specified position.
-     * Properties {@link ParsePosition#getIndex()} and {@link ParsePosition#getErrorIndex()}
-     * must be accurate before this method is invoked.
+     * The {@link ParsePosition#getErrorIndex()} property must be accurate before this method is invoked.
+     * The {@link ParsePosition#getIndex()} property will be set by this method.
      *
      * @param  text      the unparsable string.
      * @param  position  the position in the string.
@@ -425,7 +420,7 @@ final class Element implements Serializable {
         final CharSequence[] arguments;
         final int errorIndex = Math.max(offset, position.getErrorIndex());
         final int length = text.length();
-        if (errorIndex == length) {
+        if (errorIndex >= length) {
             errorKey  = Errors.Keys.UnexpectedEndOfString_1;
             arguments = new String[] {keyword};
         } else {
@@ -439,9 +434,9 @@ final class Element implements Serializable {
     /**
      * Returns an exception saying that a character is missing.
      *
-     * @param c           the missing character.
-     * @param errorIndex  the error position.
-     * @param position    the position to update with the error index.
+     * @param  c           the missing character.
+     * @param  errorIndex  the error position.
+     * @param  position    the position to update with the error index.
      */
     private ParseException missingCharacter(final int c, final int errorIndex, final ParsePosition position) {
         position.setIndex(offset);
@@ -457,12 +452,8 @@ final class Element implements Serializable {
      * @param key  the name of the missing sub-element.
      */
     final ParseException missingComponent(final String key) {
-        int error = offset;
-        if (keyword != null) {
-            error += keyword.length();
-        }
         return new UnparsableObjectException(errorLocale, Errors.Keys.MissingComponentInElement_2,
-                new String[] {keyword, key}, error);
+                    new String[] {keyword, key}, offsetAfterKeyword());
     }
 
     /**
@@ -517,6 +508,14 @@ final class Element implements Serializable {
         return new UnparsableObjectException(errorLocale, key, new String[] {value}, offset);
     }
 
+    /**
+     * Returns index of the character after the keyword in the WKT string to parse.
+     */
+    private int offsetAfterKeyword() {
+        if (isFragment) return offset;
+        return offset + keyword.length();
+    }
+
 
 
 
@@ -527,34 +526,6 @@ 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.
@@ -571,25 +542,6 @@ 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.
@@ -795,6 +747,15 @@ final class Element implements Serializable {
     }
 
     /**
+     * Returns a copy of children list, or {@code null} if this {@code Element} is an enumeration.
+     * This method is used only for creating a snapshot of this {@code Element} in {@link StoredTree}.
+     * The returned array may contain nested {@link Element} instances.
+     */
+    final Object[] getChildren() {
+        return isEnumeration ? null : children.toArray();
+    }
+
+    /**
      * Returns {@code true} if this element does not contains any remaining child.
      *
      * @return {@code true} if there is no child remaining.
@@ -807,7 +768,7 @@ final class Element implements Serializable {
      * Returns the index of the keyword in the array given to the {@link #pullElement(int, String...)} method.
      */
     final int getKeywordIndex() {
-        return keywordIndex;
+        return Byte.toUnsignedInt(keywordIndex);
     }
 
     /**
@@ -830,7 +791,7 @@ final class Element implements Serializable {
                 CollectionsExt.addToMultiValuesMap(ignoredElements, ((Element) value).keyword, keyword);
             } else {
                 throw new UnparsableObjectException(errorLocale, Errors.Keys.UnexpectedValueInElement_2,
-                        new Object[] {keyword, value}, offset + keyword.length());
+                        new Object[] {keyword, value}, offsetAfterKeyword());
             }
         }
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 257bbcb..66ee90d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -207,7 +207,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
      * @param  errorLocale   the locale for error messages (not for parsing), or {@code null} for the system default.
      * @param  factories     on input, the factories to use. On output, the factories used. Can be null.
      */
-    GeodeticObjectParser(final Symbols symbols, final Map<String,Element> fragments,
+    GeodeticObjectParser(final Symbols symbols, final Map<String,StoredTree> fragments,
             final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat unitFormat,
             final Convention convention, final Transliterator transliterator, final Locale errorLocale,
             final ReferencingFactoryContainer factories)
@@ -228,7 +228,9 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
     }
 
     /**
-     * Parses a <cite>Well Know Text</cite> (WKT).
+     * Parses a <cite>Well-Know Text</cite> from specified position as a geodetic object.
+     * Caller should invoke {@link #getAndClearWarnings(Object)} in a {@code finally} block
+     * after this method.
      *
      * @param  text      the text to be parsed.
      * @param  position  the position to start parsing from.
@@ -236,10 +238,10 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
      * @throws ParseException if the string can not be parsed.
      */
     @Override
-    public final Object parseObject(final String text, final ParsePosition position) throws ParseException {
+    final Object createFromWKT(final String text, final ParsePosition position) throws ParseException {
         final Object object;
         try {
-            object = super.parseObject(text, position);
+            object = super.createFromWKT(text, position);
             /*
              * After parsing the object, we may have been unable to set the VerticalCRS of VerticalExtent instances.
              * First, try to set a default VerticalCRS for Mean Sea Level Height in metres. In the majority of cases
@@ -281,7 +283,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
      * @throws ParseException if the element can not be parsed.
      */
     @Override
-    final Object parseObject(final Element element) throws ParseException {
+    final Object buildFromTree(final Element element) throws ParseException {
         Object value = parseCoordinateReferenceSystem(element, false);
         if (value != null) {
             return value;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
index fd0711f..c8a51f7 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
@@ -155,7 +155,7 @@ class MathTransformParser extends AbstractParser {
      * @param  factories     the factories to use for creating math transforms and geodetic objects.
      * @param  errorLocale   the locale for error messages (not for parsing), or {@code null} for the system default.
      */
-    MathTransformParser(final Symbols symbols, final Map<String,Element> fragments,
+    MathTransformParser(final Symbols symbols, final Map<String,StoredTree> fragments,
             final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat unitFormat,
             final ReferencingFactoryContainer factories, final Locale errorLocale)
     {
@@ -181,7 +181,7 @@ class MathTransformParser extends AbstractParser {
      * @throws ParseException if the element can not be parsed.
      */
     @Override
-    Object parseObject(final Element element) throws ParseException {
+    Object buildFromTree(final Element element) throws ParseException {
         return parseMathTransform(element, true);
     }
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/StoredTree.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/StoredTree.java
new file mode 100644
index 0000000..5800f5b
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/StoredTree.java
@@ -0,0 +1,483 @@
+/*
+ * 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.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Stream;
+import java.io.Serializable;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.internal.referencing.WKTKeywords;
+
+
+/**
+ * A tree of {@link Element}s saved for later use. {@code StoredTree}s are created in following situations:
+ *
+ * <ul>
+ *   <li>{@link WKTFormat#addFragment(String, String)} for defining shortcuts
+ *       to be inserted into an arbitrary amount of other WKT strings.</li>
+ *   <li>{@link WKTDictionary#addDefinitions(Stream)} for preparing WKT definitions to be parsed
+ *       only when first needed. While WKT trees are waiting, they may share references to same
+ *       {@code Node} instances for reducing memory usage.</li>
+ * </ul>
+ *
+ * This class does not store {@link Element} instances directly because {@code Element}s are not easily shareable.
+ * Contrarily to {@code Element} design, {@code StoredTree} needs unmodifiable {@link Element#children} list and
+ * needs to store {@link Element#offset} values in separated arrays. Those changes make possible to have many
+ * {@code StoredTree} instances sharing the same {@code Node} instances in the common case where some WKT elements
+ * are repeated in many trees.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class StoredTree implements Serializable {
+    /**
+     * Indirectly for {@link WKTFormat} serialization compatibility.
+     */
+    private static final long serialVersionUID = 8436779786449395346L;
+
+    /**
+     * Unmodifiable copy of {@link Element} without contextual information such as {@link Element#offset}.
+     * The removal of contextual information increase greatly the possibility to reuse the same {@code Node}
+     * instances in many {@link StoredTree}s. For example the {@code UNIT["degrees", 0.0174532925199433]} node
+     * is repeated a lot, so we want to share only one {@code Node} instance for every places in the WKT tree
+     * where degrees unit is declared, even if they appear at different offsets in the WKT string.
+     *
+     * @see StoredTree#root
+     */
+    private static final class Node implements Serializable {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = 1463070931527783896L;
+
+        /**
+         * Copy of {@link Element#keyword} reference. Never {@code null}.
+         *
+         * @see StoredTree#keyword()
+         */
+        final String keyword;
+
+        /**
+         * Snapshot of {@link Element#children} list. Array content shall not be modified.
+         * This array is {@code null} if the keyword was not followed by a pair of brackets
+         * (e.g. "north"). A null value is not equivalent to an empty list. For example the
+         * list is null when parsing {@code "FOO"} but is empty when parsing {@code "FOO[]"}.
+         */
+        private final Object[] children;
+
+        /**
+         * Creates an immutable copy of the given element. Keywords and children references
+         * are copied in this new {@code Node} but {@link Element#offset}s are copied in a
+         * separated array for making possible to share {@code Node} instances.
+         */
+        Node(final Deflater deflater, final Element element) {
+            keyword  = element.keyword;
+            children = element.getChildren();
+            if (children != null) {
+                for (int i=0; i<children.length; i++) {
+                    final Object child = children[i];
+                    if (child instanceof Element) {
+                        children[i] = deflater.unique(new Node(deflater, (Element) child));
+                    }
+                }
+            }
+            deflater.addOffset(element);
+        }
+
+        /**
+         * Copies this node in a modifiable {@link Element}.
+         * This is the converse of the {@link #Node(Deflater, Element)} constructor.
+         *
+         * @see StoredTree#toElement(AbstractParser, int)
+         */
+        final Element toElement(final Inflater inflater) {
+            final LinkedList<Object> list;
+            if (children == null) {
+                list = null;
+            } else {
+                list = new LinkedList<>();
+                for (Object child : children) {
+                    if (child instanceof Node) {
+                        child = ((Node) child).toElement(inflater);
+                    }
+                    list.add(child);
+                }
+            }
+            // Offsets must be read in the same order as they have been written.
+            return new Element(keyword, list, inflater.nextOffset(), inflater.errorLocale);
+        }
+
+        /**
+         * Returns the last element of the given names.
+         * This method searches only in children of this node.
+         * It does not search recursively in children of children.
+         *
+         * @param  keys  the element names (e.g. {@code "ID"}).
+         * @return the last {@link Node} of the given names found in the children, or {@code null} if none.
+         */
+        final Node peekLastElement(final String... keys) {
+            if (children != null) {
+                for (int i = children.length; --i >= 0;) {
+                    final Object value = children[i];
+                    if (value instanceof Node) {
+                        final Node node = (Node) value;
+                        if (node.children != null) {
+                            for (final String key : keys) {
+                                if (node.keyword.equalsIgnoreCase(key)) {
+                                    return node;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Returns the next values (not child elements).
+         * 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.
+         * @param  index  index where to store the first element in given array.
+         */
+        final void peekValues(final Object[] addTo, int index) {
+            for (final Object object : children) {
+                if (!(object instanceof Node)) {
+                    addTo[index] = object;
+                    if (++index >= addTo.length) break;
+                }
+            }
+        }
+
+        /**
+         * Returns the string representation of the first value, which is usually the element name.
+         * For example in {@code DATUM["WGS 84", …]} this is "WGS 84". If there is no children then
+         * this method returns the keyword, which is usually an enumeration value (for example "NORTH"}).
+         *
+         * @see StoredTree#toString()
+         */
+        @Override
+        public String toString() {
+            return (children != null && children.length != 0) ? String.valueOf(children[0]) : keyword;
+        }
+
+        /**
+         * Returns a hash code value for this node. It uses hash codes of child elements, except
+         * for children that are instances of {@link Node} for which identity hash codes are used.
+         * We avoid requesting "normal" hash code of child {@link Node} because the tree structure
+         * may cause the same hash codes to be computed many times. The use of identity hash codes
+         * is sufficient if children have been replaced by unique instances before to compute the
+         * hash code of this {@link Node}. This replacement is done by {@link Deflater#unique(Node)}.
+         *
+         * @see Deflater#unique(Node)
+         */
+        @Override
+        public int hashCode() {
+            int hash = keyword.hashCode();
+            if (children != null) {
+                for (final Object value : children) {
+                    hash = 31*hash + ((value instanceof Node) ? System.identityHashCode(value) : value.hashCode());
+                }
+            }
+            return hash;
+        }
+
+        /**
+         * Returns whether the given object is equal to this {@code Node}, comparing keyword and children.
+         * Nested {@link Node}s are compared by identity comparisons; see {@link #hashCode()} for rational.
+         *
+         * @see #hashCode()
+         * @see Deflater#unique(Node)
+         */
+        @Override
+        public boolean equals(final Object other) {
+            if (other instanceof Node) {
+                final Node that = (Node) other;
+                if (keyword.equals(that.keyword)) {
+                    if (children == that.children) {
+                        return true;
+                    }
+                    if (children != null && that.children != null && children.length == that.children.length) {
+                        for (int i=0; i<children.length; i++) {
+                            final Object value = children[i];
+                            final Object otherValue = that.children[i];
+                            if (!(value instanceof Node ? (value == otherValue) : value.equals(otherValue))) {
+                                // Identity comparison of `Node` instances for consistency with `hashCode()`.
+                                return false;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Root of a tree of {@link Element} snapshots.
+     */
+    private final Node root;
+
+    /**
+     * Indices in the WKT string where elements have been found. If negative, the actual offset
+     * value is {@code ~offset} and {@link Element#isFragment} shall be set to {@code true}.
+     * This array shall not be modified because it may be shared by many {@link StoredTree}s.
+     *
+     * @see Deflater#addOffset(Element)
+     * @see Inflater#nextOffset()
+     */
+    private final short[] offsets;
+
+    /**
+     * Creates a new {@code StoredTree} with a copy of given arrays.
+     * Changes to the given array after construction will not affect this {@code StoredTree}.
+     *
+     * @param  root          root of the tree of WKT elements.
+     * @param  sharedValues  pool to use for sharing unique instances of values.
+     */
+    StoredTree(final Element root, final Map<Object,Object> sharedValues) {
+        final Deflater deflater = new Deflater(sharedValues);
+        this.root = deflater.unique(new Node(deflater, root));
+        offsets = deflater.offsets();
+    }
+
+    /**
+     * Recreates {@link Element} tree. This method is the converse of the constructor.
+     *
+     * @param  parser      the parser which will be used for parsing the tree.
+     * @param  isFragment  non-zero if and only if {@link Element#isFragment} shall be {@code true}.
+     *                     In such case, this value must be <code>~{@linkplain Element#offset}</code>.
+     * @return root of {@link Element} tree.
+     */
+    final Element toElement(final AbstractParser parser, final int isFragment) {
+        return root.toElement(new Inflater(parser, offsets, isFragment));
+    }
+
+    /**
+     * A helper class for compressing a tree of {@link Element}s as a tree of {@link Node}s.
+     * Contrarily to {@code Element} instances, {@code Node}s instances can be shared between many trees.
+     * Each instances shall be used for constructing only one {@link Node}. After node construction, this
+     * instance lives longer in the {@link #sharedValues} map for sharing {@link #offsets} arrays.
+     *
+     * @see StoredTree#StoredTree(Element, Map)
+     */
+    private static final class Deflater {
+        /**
+         * Pool to use for sharing unique instances of values.
+         * This is a copy of {@link WKTFormat#sharedValues} map.
+         * This is reset to {@code null} when not needed anymore.
+         */
+        private Map<Object,Object> sharedValues;
+
+        /**
+         * The {@link Element#offset} value of {@link StoredTree#root} together with offsets of all
+         * {@link Element#children} in iteration order. Order is defined by {@link Node} constructor.
+         * This array is expanded as needed. Shall not be modified after call to {@link #offsets()}.
+         */
+        private short[] offsets;
+
+        /**
+         * Number of valid elements in {@link #offsets}.
+         */
+        private int count;
+
+        /**
+         * Pool of previously constructed values used for replacing equal instances by unique instances.
+         * May contain {@link String}, {@link Long}, {@link Double} and {@link Node} instances among others.
+         *
+         * @param  sharedValues  pool of previously created objects.
+         */
+        Deflater(final Map<Object,Object> sharedValues) {
+            this.sharedValues = sharedValues;
+            offsets = new short[24];
+        }
+
+        /**
+         * Returns a unique instance of given node.
+         *
+         * @return a previous instance from the pool, or {@code node} if none.
+         *
+         * @see Node#hashCode()
+         * @see Node#equals(Object)
+         */
+        final Node unique(final Node node) {
+            final Object existing = sharedValues.putIfAbsent(node, node);
+            return (existing != null) ? (Node) existing : node;
+        }
+
+        /**
+         * Adds the given {@link Element#offset} value.
+         *
+         * @see Inflater#nextOffset()
+         */
+        final void addOffset(final Element element) {
+            if (count >= offsets.length) {
+                offsets = Arrays.copyOf(offsets, count * 2);
+            }
+            int offset = Math.min(Short.MAX_VALUE, element.offset);
+            if (element.isFragment) offset = ~offset;
+            offsets[count++] = (short) offset;
+        }
+
+        /**
+         * Returns all {@link Element#offset} values in iteration order.
+         * This method may return an array shared by different {@link Node} instances; do not modify.
+         */
+        @SuppressWarnings("ReturnOfCollectionOrArrayField")
+        final short[] offsets() {
+            offsets = ArraysExt.resize(offsets, count);
+            final Deflater other = (Deflater) sharedValues.putIfAbsent(this, this);
+            sharedValues = null;
+            if (other != null) {
+                this.offsets = other.offsets;
+            }
+            return offsets;
+        }
+
+        /**
+         * Compares the {@link #offsets} arrays for equality. This is used by {@link #offsets()}
+         * (indirectly, through {@link Map}) as a workaround for Java arrays not overriding
+         * {@code equals(Object)} method.
+         */
+        @Override
+        public boolean equals(final Object other) {
+            return (other instanceof Deflater) && Arrays.equals(offsets, ((Deflater) other).offsets);
+        }
+
+        /**
+         * Computes a hash code value based only on the {@link #offsets} array.
+         */
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(offsets);
+        }
+    }
+
+    /**
+     * A helper class for decompressing a tree of {@link Element}s from a tree of {@link Node}s.
+     * This is the converse of {@link Deflater}.
+     *
+     * @see StoredTree#toElement(AbstractParser, int)
+     */
+    private static final class Inflater {
+        /**
+         * If {@link Element#offset} must be fixed to a value, the bitwise NOT value of that offset.
+         * Otherwise 0. This field packs two information:
+         *
+         * <ul>
+         *   <li>{@link Element#isFragment} = ({@code isFragment} != 0)</li>
+         *   <li>If {@code isFragment} is {@code true}, then:
+         *     <ul><li>{@link Element#offset} = {@code ~isFragment}</li></ul>
+         *   </li>
+         * </ul>
+         */
+        private final int isFragment;
+
+        /**
+         * The {@link StoredTree#offsets} array. Shall not be modified because potentially shared.
+         * Ignored if {@link #isFragment} != 0.
+         */
+        private final short[] offsets;
+
+        /**
+         * Index of the next offset to return in the {@link #offsets} array.
+         * Ignored if {@link #isFragment} != 0.
+         */
+        private int index;
+
+        /**
+         * Locale to use for producing error message.
+         */
+        final Locale errorLocale;
+
+        /**
+         * Creates a new inflater.
+         *
+         * @param  parser      the parser which will be used for parsing the tree.
+         * @param  offsets     the {@link StoredTree#offsets} array. Will not be modified.
+         * @param  isFragment  non-zero if and only if {@link Element#isFragment} is {@code true}.
+         *                     In such case, this value must be <code>~{@linkplain Element#offset}</code>.
+         */
+        Inflater(final AbstractParser parser, final short[] offsets, final int isFragment) {
+            this.errorLocale = parser.errorLocale;
+            this.isFragment  = isFragment;
+            this.offsets     = offsets;
+        }
+
+        /**
+         * Returns the value to assign to {@link Element#offset} for the next element.
+         */
+        final int nextOffset() {
+            return (isFragment != 0) ? isFragment : offsets[index++];
+        }
+    }
+
+    /**
+     * Stores identifier information in the given array. This method locates the last {@code "ID"} (WKT 2)
+     * or {@code "AUTHORITY"} (WKT 1) node and optionally the {@code "CITATION"} sub-node. Values are copied
+     * in the given array, in that order!
+     *
+     * <ol>
+     *   <li>Code space</li>
+     *   <li>Code</li>
+     *   <li>Version if present</li>
+     *   <li>Authority if present (skipped if the array length is less than 4)</li>
+     * </ol>
+     *
+     * If any of above values is missing, the corresponding array element is left unchanged.
+     * Callers should set all array elements to {@code null} before to invoke this method.
+     *
+     * @param  fullId  where to store code space, code, version, authority.
+     */
+    final void peekIdentifiers(final Object[] fullId) {
+        Node id = root.peekLastElement(MathTransformParser.ID_KEYWORDS);
+        if (id != null) {
+            id.peekValues(fullId, 0);
+            if (fullId.length >= 4) {
+                id = id.peekLastElement(WKTKeywords.Citation);
+                if (id != null) {
+                    id.peekValues(fullId, 3);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the keyword of the root element.
+     */
+    final String keyword() {
+        return root.keyword;
+    }
+
+    /**
+     * Returns the string representation of the first value of the root element, which is usually the element name.
+     * For example in {@code DATUM["WGS 84", …]} this is "WGS 84". If there is no children then this method returns
+     * the keyword, which is usually an enumeration value (for example "NORTH"}).
+     */
+    @Override
+    public String toString() {
+        return root.toString();
+    }
+}
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
index 62a0840..a20ff85 100644
--- 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
@@ -146,14 +146,14 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
      * Values can be one of the following 4 types:
      *
      * <ol>
-     *   <li>{@link Element}: this is the initial state when there is no duplicated codes.
+     *   <li>{@link StoredTree}: 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}
+     *   <li>{@link IdentifiedObject}: the result of parsing the {@link StoredTree}
      *       when {@link #createObject(String)} is invoked for a given authority code.
-     *       The parsing result replaces the previous {@link Element} value.</li>
+     *       The parsing result replaces the previous {@link StoredTree} 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
+     *       then above-cited {@link StoredTree} 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>
@@ -162,7 +162,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
      * 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 #addDefinition(StoredTree)
      * @see #createObject(String)
      */
     private final Map<String,Object> definitions;
@@ -191,9 +191,9 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
         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.
+         * The value as an {@link StoredTree} 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;
 
@@ -222,9 +222,9 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
          * @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) {
+        Disambiguation(final StoredTree object, final Object[] fullId) {
             Arrays.fill(fullId, null);
-            object.peekLastElement(MathTransformParser.ID_KEYWORDS).peekValues(fullId);
+            object.peekIdentifiers(fullId);
             codespace = trimOrNull(fullId[0]);
             version   = trimOrNull(fullId[2]);
             value     = object;
@@ -241,10 +241,10 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
          * @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)
+         * @see WKTDictionary#addDefinition(StoredTree)
          */
         Disambiguation(Disambiguation previous, final String codespace, final String version,
-                       final String code, final Element value)
+                       final String code, final StoredTree value)
         {
             this.previous  = previous;
             this.codespace = codespace;
@@ -588,17 +588,17 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
             if (buffer.length() != 0) {
                 pos.setIndex(0);
                 final String wkt = buffer.toString();
-                final Element root = parser.textToTree(wkt, pos);
+                final StoredTree tree = 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);
+                    parser.addFragment(aliasKey, tree);
                     aliasKey = null;
                 } else {
-                    addDefinition(root);
+                    addDefinition(tree);
                 }
                 buffer.setLength(0);
             }
@@ -611,48 +611,41 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
      * 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.
+     * @param  tree  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;
+    private void addDefinition(final StoredTree tree) throws FactoryDataException {
+        final Object[] fullId = new Object[authorities == null ? 4 : 3];
+        tree.peekIdentifiers(fullId);                   // Codespace, code, version, (authority).
+        final String code = trimOrNull(fullId[1]);
+        if (code == null) {
+            throw new FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1, tree));
+        }
+        final String codespace = trimOrNull(fullId[0]);
+        definitions.merge(code, tree, (oldValue, newValue) -> {
+            final String version = trimOrNull(fullId[2]);
+            final Disambiguation previous;
+            if (oldValue instanceof Disambiguation) {
+                previous = (Disambiguation) oldValue;
+            } else if (oldValue instanceof StoredTree) {
+                previous = new Disambiguation((StoredTree) 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, (StoredTree) newValue);
+        });
+        codespaces.add(codespace);
+        if (fullId.length >= 4) {
+            final String title = trimOrNull(fullId[3]);
+            if (title != null) {
+                authorities.add(title);
             }
         }
-        throw new FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1, root.peekValue()));
     }
 
     /**
@@ -697,6 +690,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
                 throw new FactoryDataException(resources().getString(
                         Resources.Keys.CanNotParseWKT_2, lineNumber, e.getLocalizedMessage()));
             } finally {
+                parser.clear();
                 updateAuthority();
             }
         } finally {
@@ -792,8 +786,8 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
         }
         /*
          * 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`).
+         * Then get CRS definition as an `IdentifiedObject` or an `StoredTree` (the `Disambiguation` case
+         * is resolved as an `IdentifiedObject` or `StoredTree`).
          */
         Disambiguation choices = null;
         Object value = null;
@@ -823,15 +817,15 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
         /*
          * 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.
+         *   - `StoredTree`       — 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).
+         * If `StoredTree`, 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) {
+        if (value instanceof StoredTree) {
             lock.writeLock().lock();
             try {
                 if (choices != null) {
@@ -839,10 +833,10 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
                 } else {
                     value = definitions.get(localCode);
                 }
-                if (value instanceof Element) {
+                if (value instanceof StoredTree) {
                     ParseException cause = null;
                     try {
-                        value = parser.parse((Element) value);
+                        value = parser.buildFromTree((StoredTree) value);
                     } catch (ParseException e) {
                         cause = e;
                         value = e.getLocalizedMessage();
@@ -898,8 +892,8 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
             final String[] keywords = WKTKeywords.forType(type);
             final Class<? extends IdentifiedObject> baseType = type;                // Because lambdas require final.
             final Predicate<Object> filter = (element) -> {
-                if (element instanceof Element) {
-                    return (keywords == null) || ArraysExt.containsIgnoreCase(keywords, ((Element) element).keyword);
+                if (element instanceof StoredTree) {
+                    return (keywords == null) || ArraysExt.containsIgnoreCase(keywords, ((StoredTree) element).keyword());
                 } else {
                     return baseType.isInstance(element);
                 }
@@ -920,7 +914,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
                     }
                     /*
                      * Verify if an existing collection (assigned to another type) provides the same values.
-                     * If we find one, we will share the same instances.
+                     * If we find one, share the same instance for reducing memory usage.
                      */
                     boolean share = false;
                     for (final Set<String> other : codeCaches.values()) {
@@ -931,6 +925,8 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
                         }
                     }
                     if (!share) {
+                        // TODO: replace by Set.copyOf(Set) in JDK9 and remove the `share` flag
+                        // (not needed because Set.copyOf(Set) does the verification itself).
                         codes = CollectionsExt.unmodifiableOrCopy(codes);
                     }
                     codeCaches.put(type, 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 6b03397..95a78af 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
@@ -202,11 +202,11 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * WKT fragments that can be inserted in longer WKT strings, or {@code null} if none. Keys are short identifiers
      * and values are WKT subtrees to substitute to the identifiers when they are found in a WKT to parse.
-     * The same map instance may be shared by different {@linkplain #clone() clones}.
+     * The same map instance may be shared by different {@linkplain #clone() clones} as long as they are not modified.
      *
      * @see #fragments(boolean)
      */
-    private Map<String,Element> fragments;
+    private Map<String,StoredTree> fragments;
 
     /**
      * {@code true} if the {@link #fragments} map is shared by two or more {@code WKTFormat} instances.
@@ -284,7 +284,7 @@ public class WKTFormat extends CompoundFormat<Object> {
      * @param  modifiable  whether the caller intents to modify the map.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    private Map<String,Element> fragments(final boolean modifiable) {
+    private Map<String,StoredTree> fragments(final boolean modifiable) {
         if (fragments == null) {
             if (!modifiable) {
                 // Most common cases: invoked before to parse a WKT and no fragments specified.
@@ -761,42 +761,52 @@ public class WKTFormat extends CompoundFormat<Object> {
             throw new IllegalArgumentException(errors().getString(Errors.Keys.NotAUnicodeIdentifier_1, name));
         }
         final ParsePosition pos = new ParsePosition(0);
-        final Element element = textToTree(wkt, pos);
+        final StoredTree definition = 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);
+                    new Object[] {name + " = " + definition.keyword() + "[…]", CharSequences.token(wkt, index)}, index);
         }
-        addFragment(name, element);
+        addFragment(name, definition);
     }
 
     /**
      * 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.
+     * @param  name        the Unicode identifier to assign to the WKT fragment.
+     * @param  definition  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) {
+    final void addFragment(final String name, final StoredTree definition) {
+        if (fragments(true).putIfAbsent(name, definition) != null) {
             throw new IllegalArgumentException(errors().getString(Errors.Keys.ElementAlreadyPresent_1, name));
         }
     }
 
     /**
-     * Parses a fragment of Well Know Text (WKT).
+     * Parses a Well Know Text (WKT) for a fragment or an entire object definition.
+     * This method should be invoked only for WKT trees to be stored for a long time.
+     * It should not be invoked for immediate {@link IdentifiedObject} parsing.
      *
      * @param  wkt  the Well Know Text (WKT) fragment to parse.
      * @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 textToTree(final String wkt, final ParsePosition pos) throws ParseException {
+    final StoredTree textToTree(final String wkt, final ParsePosition pos) throws ParseException {
+        final AbstractParser parser = parser(true);
+        Element result = null;
+        warnings = null;
+        try {
+            result = parser.textToTree(wkt, pos);
+        } finally {
+            warnings = parser.getAndClearWarnings(result);
+        }
         if (sharedValues == null) {
             sharedValues = new HashMap<>();
         }
-        return parser(true).textToTree(wkt, pos, sharedValues);
+        return new StoredTree(result, sharedValues);
     }
 
     /**
@@ -824,32 +834,35 @@ public class WKTFormat extends CompoundFormat<Object> {
         ArgumentChecks.ensureNonEmpty("wkt", wkt);
         ArgumentChecks.ensureNonNull ("pos", pos);
         final AbstractParser parser = parser(false);
-        Object object = null;
+        Object result = null;
         try {
-            object = parser.parseObject(wkt.toString(), pos);
+            result = parser.createFromWKT(wkt.toString(), pos);
         } finally {
-            warnings = parser.getAndClearWarnings(object);
+            warnings = parser.getAndClearWarnings(result);
         }
-        return object;
+        return result;
     }
 
     /**
-     * Creates an object from the given tree of WKT elements.
+     * 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)}.
      *
-     * @param  root  the tree of WKT elements.
+     * @param  tree  the tree of WKT elements.
      * @return the parsed object (never {@code null}).
-     * @throws ParseException if an error occurred while parsing the WKT.
+     * @throws ParseException if the tree can not be parsed.
      */
-    final Object parse(final Element root) throws ParseException {
+    final Object buildFromTree(StoredTree tree) throws ParseException {
         clear();
         final AbstractParser parser = parser(false);
-        Object object = null;
+        final Element root = new Element(tree.toElement(parser, 0));
+        Object result = null;
         try {
-            object = parser.buildFromTree(root);
+            result = parser.buildFromTree(root);
+            root.close(parser.ignoredElements);
         } finally {
-            warnings = parser.getAndClearWarnings(object);
+            warnings = parser.getAndClearWarnings(result);
         }
-        return object;
+        return result;
     }
 
     /**
@@ -881,7 +894,7 @@ public class WKTFormat extends CompoundFormat<Object> {
      * for the source of logging messages which is the enclosing {@code WKTParser} instead than a factory.
      */
     private static final class Parser extends GeodeticObjectParser {
-        Parser(final Symbols symbols, final Map<String,Element> fragments,
+        Parser(final Symbols symbols, final Map<String,StoredTree> fragments,
                 final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat unitFormat,
                 final Convention convention, final Transliterator transliterator, final Locale errorLocale,
                 final ReferencingFactoryContainer factories)
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
index c1b56f3..dacde57 100644
--- 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
@@ -35,7 +35,7 @@
 # Alias for WGS84 geographic CRS.
 # Can be inserted in projected CRS with "$WGS84".
 #
-SET WGS84 =
+SET WGS84_BASE =
  BaseGeodCRS["GCS_WGS_1984",
   Datum["D_WGS_1984",
    Ellipsoid["WGS_1984", 6378137, 298.257223563]],
@@ -47,7 +47,7 @@ SET WGS84 =
 # when they have the default value.
 #
 ProjectedCRS["North_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
  Conversion["Stereographic North Pole",
   Method["Polar Stereographic (variant A)"],
   Parameter["Latitude of natural origin", 90]],
@@ -58,7 +58,7 @@ ProjectedCRS["North_Pole_Stereographic",
  Id["ESRI", 102018]]
 
 ProjectedCRS["South_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
  Conversion["Stereographic South Pole",
   Method["Polar Stereographic (variant A)"],
   Parameter["Latitude of natural origin", -90]],
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
index 33250aa..3245f66 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
@@ -1641,6 +1641,7 @@ public class GeodeticObjectFactory extends AbstractFactory implements CRSFactory
      */
     @Override
     public CoordinateReferenceSystem createFromWKT(final String text) throws FactoryException {
+        ArgumentChecks.ensureNonEmpty("text", text);
         Parser p = parser.getAndSet(null);
         if (p == null) try {
             Constructor<? extends Parser> c = parserConstructor;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
index 4190e33..f98830c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
@@ -1537,6 +1537,7 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math
     @Override
     public MathTransform createFromWKT(final String text) throws FactoryException {
         lastMethod.remove();
+        ArgumentChecks.ensureNonEmpty("text", text);
         Parser p = parser.getAndSet(null);
         if (p == null) try {
             Constructor<? extends Parser> c = parserConstructor;
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
index e8f9395..254deeb 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
@@ -34,7 +34,7 @@ import static org.junit.Assert.*;
  * Tests the {@link Element} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.1
  * @since   0.6
  * @module
  */
@@ -49,25 +49,19 @@ public final strictfp class ElementTest extends TestCase {
             throw new UnsupportedOperationException();
         }
 
-        @Override Object parseObject(Element element) throws ParseException {
+        @Override Object buildFromTree(Element element) throws ParseException {
             throw new UnsupportedOperationException();
         }
     };
 
     /**
-     * The map of shared values to gives to the {@link Element} constructor.
-     * This is usually null, except for the test of WKT fragments.
-     */
-    private Map<Object,Object> sharedValues;
-
-    /**
      * Parses the given text and ensures that {@link ParsePosition} index is set at to the end of string.
      */
     private Element parse(final String text) throws ParseException {
         final ParsePosition position = new ParsePosition(0);
         final Element element;
         try {
-            element = new Element(parser, text, position, sharedValues);
+            element = new Element(parser, text, position);
         } catch (ParseException e) {
             assertEquals("index should be unchanged.", 0, position.getIndex());
             assertTrue("Error index should be set.", position.getErrorIndex() > 0);
@@ -296,27 +290,14 @@ public final strictfp class ElementTest extends TestCase {
     @Test
     @DependsOnMethod({"testPullString", "testPullElement"})
     public void testFragments() throws ParseException {
-        sharedValues = new HashMap<>();
-        Element frag = parse("Frag[“A”,“B”,“A”]");
-        parser.fragments.put("MyFrag", frag);
-        try {
-            frag.pullString("A");
-            fail("Element shall be unmodifiable.");
-        } catch (UnsupportedOperationException e) {
-            // This is the expected exception.
-        }
-        /*
-         * Parse a normal value. Since this is not a fragment,
-         * we should be able to pull a copy of the components.
-         */
-        sharedValues = null;
+        parser.fragments.put("MyFrag", new StoredTree(parse("Frag[“A”,“B”,“A”]"), new HashMap<>()));
         final Element element = parse("Foo[“C”,$MyFrag,“D”]");
         assertEquals("C", element.pullString("C"));
         assertEquals("D", element.pullString("D"));
-        frag = element.pullElement(AbstractParser.MANDATORY, "Frag");
+        Element frag = element.pullElement(AbstractParser.MANDATORY, "Frag");
         final String a = frag.pullString("A");
         assertEquals("A", a);
         assertEquals("B", frag.pullString("B"));
-        assertSame(a, frag.pullString("A"));    // 'sharedValues' should have allowed to share the same instance.
+        assertEquals(a, frag.pullString("A"));
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index c5e3b04..e3368e0 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -56,7 +56,7 @@ import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_D
  * Tests {@link GeodeticObjectParser}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.6
  * @module
  */
@@ -101,7 +101,7 @@ public final strictfp class GeodeticObjectParserTest extends TestCase {
             newParser(Convention.DEFAULT);
         }
         final ParsePosition position = new ParsePosition(0);
-        final Object obj = parser.parseObject(text, position);
+        final Object obj = parser.createFromWKT(text, position);
         assertEquals("errorIndex", -1, position.getErrorIndex());
         assertEquals("index", text.length(), position.getIndex());
         assertInstanceOf("GeodeticObjectParser.parseObject", type, obj);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
index 7728b78..baef439 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
@@ -38,7 +38,7 @@ import static org.opengis.test.Assert.*;
  * Tests {@link MathTransformParser}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.6
  * @module
  */
@@ -78,7 +78,7 @@ public final strictfp class MathTransformParserTest extends TestCase {
             assertEquals(DefaultMathTransformFactory.class.getCanonicalName(), parser.getPublicFacade());
         }
         final ParsePosition position = new ParsePosition(0);
-        final MathTransform mt = (MathTransform) parser.parseObject(text, position);
+        final MathTransform mt = (MathTransform) parser.createFromWKT(text, position);
         assertEquals("errorIndex", -1, position.getErrorIndex());
         assertEquals("index", text.length(), position.getIndex());
         return mt;
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
index 5ae1678..56001f0 100644
--- 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
@@ -28,7 +28,6 @@ import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.IdentifiedObject;
-import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
@@ -61,28 +60,34 @@ public final strictfp class WKTDictionaryTest extends TestCase {
             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.
+         * TEST code space should be fist because it is the most frequently used
+         * in the test file. The authority should be "TEST" for the same reason.
+         * Codes can be in any order. Code spaces are omitted when there is no ambiguity.
          */
-        assertArrayEquals("getCodeSpaces()", new String[] {"ESRI", "MyCodeSpace"}, factory.getCodeSpaces().toArray());
-        assertSame("getAuthority()", Citations.ESRI, factory.getAuthority());
+        assertArrayEquals("getCodeSpaces()", new String[] {"TEST", "ESRI"}, factory.getCodeSpaces().toArray());
+        assertEquals("getAuthority()", "TEST", factory.getAuthority().getTitle().toString());
         Set<String> codes = factory.getAuthorityCodes(IdentifiedObject.class);
-        assertSame(codes, factory.getAuthorityCodes(IdentifiedObject.class));       // Test caching.
-        assertSame(codes, factory.getAuthorityCodes(SingleCRS.class));              // Test sharing.
-        assertSetEquals(Arrays.asList("102018", "ESRI::102021", "MyCodeSpace::102021", "MyCodeSpace:v2:102021"), codes);
+        assertSame( codes,  factory.getAuthorityCodes(IdentifiedObject.class));     // Test caching.
+        assertSame( codes,  factory.getAuthorityCodes(SingleCRS.class));            // Test sharing.
+        assertSetEquals(Arrays.asList("102018", "ESRI::102021", "TEST::102021", "TEST:v2:102021", "E1", "E2"), codes);
         assertSetEquals(Arrays.asList("102018", "ESRI::102021"), factory.getAuthorityCodes(ProjectedCRS.class));
         codes = factory.getAuthorityCodes(GeographicCRS.class);
-        assertSetEquals(Arrays.asList("MyCodeSpace::102021", "MyCodeSpace:v2:102021"), codes);
+        assertSetEquals(Arrays.asList("TEST::102021", "TEST:v2:102021", "E1", "E2"), codes);
         assertSame(codes, factory.getAuthorityCodes(GeodeticCRS.class));            // Test sharing.
         assertSame(codes, factory.getAuthorityCodes(GeographicCRS.class));          // Test caching.
         /*
          * 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)");
+        verifyCRS(factory.createProjectedCRS (        "102018"), "North_Pole_Stereographic", +90);
+        verifyCRS(factory.createProjectedCRS ("ESRI :  102021"), "South_Pole_Stereographic", -90);
+        verifyCRS(factory.createGeographicCRS("TEST:  :102021"), "Anguilla 1957");
+        verifyCRS(factory.createGeographicCRS("TEST:v2:102021"), "Anguilla 1957 (bis)");
+        /*
+         * Test creation of CRS having errors.
+         *   - Verify error index.
+         */
+        verifyErroneousCRS(factory, "E1", 69);
+        verifyErroneousCRS(factory, "E2", 42);
     }
 
     /**
@@ -111,4 +116,49 @@ public final strictfp class WKTDictionaryTest extends TestCase {
         assertAxisDirectionsEqual(name, crs.getCoordinateSystem(),
                                   AxisDirection.NORTH, AxisDirection.EAST);
     }
+
+    /**
+     * Verifies the error message and error offset when trying to parse an erroneous CRS.
+     *
+     * @param  factory      factory to use.
+     * @param  code         code of erroneous CRS.
+     * @param  errorOffset  expected error index.
+     */
+    private static void verifyErroneousCRS(final WKTDictionary factory, final String code, final int errorOffset) {
+        String details = null;
+        try {
+            factory.createGeographicCRS(code);
+            fail("Parsing should have failed.");
+        } catch (FactoryException e) {
+            /*
+             * Expect a message like: Can not create a geodetic object for "E1".
+             * The exact message is locale-dependent, so we can not test fully.
+             */
+            final String message = e.getMessage();
+            assertTrue(message, message.contains(code));
+            /*
+             * Expect a message like: Missing "semiMajorAxis" component in "Ellipsoid" element.
+             * The error offset (zero-based) should point to the character after "Ellipsoid" in
+             * the following WKT:
+             *
+             *     Datum["Erroneous", Ellipsoid["Missing axis length"]]
+             */
+            final UnparsableObjectException cause = (UnparsableObjectException) e.getCause();
+            details = cause.getMessage();
+            assertTrue(message, details.contains("Ellipsoid"));
+            assertTrue(message, details.contains("semiMajorAxis"));
+            assertEquals("errorOffset", errorOffset, cause.getErrorOffset());
+        }
+        /*
+         * Try parsing again. The exception message should have been saved,
+         * i.e. the parsing process is not repeated.
+         */
+        try {
+            factory.createGeographicCRS(code);
+            fail("Parsing should have failed.");
+        } catch (FactoryException e) {
+            assertEquals(details, e.getMessage());
+            assertNull(e.getCause());
+        }
+    }
 }
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
index 06e7eb9..816038e 100644
--- 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
@@ -7,7 +7,7 @@
 # Alias for WGS84 geographic CRS.
 #
 SET DEGREE = Unit["Degree", 0.0174532925199433]
-SET WGS84 =
+SET WGS84_BASE =
  BaseGeodCRS["GCS_WGS_1984",
   Datum["D_WGS_1984",
    Ellipsoid["WGS_1984", 6378137, 298.257223563]],
@@ -20,7 +20,7 @@ SET WGS84 =
 # when they have the default value.
 #
 ProjectedCRS["North_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
  Conversion["Stereographic North Pole",
   Method["Polar Stereographic (variant A)"],
   Parameter["Latitude of natural origin", 90]],
@@ -31,7 +31,7 @@ ProjectedCRS["North_Pole_Stereographic",
  Id["ESRI", 102018]]
 
 ProjectedCRS["South_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
  Conversion["Stereographic South Pole",
   Method["Polar Stereographic (variant A)"],
   Parameter["Latitude of natural origin", -90]],
@@ -41,25 +41,49 @@ ProjectedCRS["South_Pole_Stereographic",
   Unit["metre", 1],
  Id["ESRI", 102021]]
 
+
 #
-# A dummy CRS using the same code than ESRI::102021 but
+# 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]],
+ Datum["Anguilla 1957",
+  Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
  CS[ellipsoidal, 2],
   Axis["Latitude", north],
   Axis["Longitude", east],
   $DEGREE,
- Id["MyCodeSpace", 102021]]
+ Id["TEST", 102021]]
 
 GeodCRS["Anguilla 1957 (bis)",
-  Datum["Anguilla 1957",
-   Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
+ Datum["Anguilla 1957",
+  Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
+ CS[ellipsoidal, 2],
+  Axis["Latitude", north],
+  Axis["Longitude", east],
+  $DEGREE,
+ Id["TEST", 102021, "v2"]]
+
+
+#
+# Intentionally malformed CRS for testing error indices reported in `ParseException`.
+# The erroneous element should be on the first line for avoiding platform-dependency
+# caused by various line separators ("\n" versus "\r\n").
+#
+
+SET BAD_DATUM = Datum["Erroneous", Ellipsoid["Missing axis length"]]
+
+GeodCRS["Error index 69 (on Ellipsoid)", Datum["Erroneous", Ellipsoid["Missing axis length"]],
+ CS[ellipsoidal, 2],
+  Axis["Latitude", north],
+  Axis["Longitude", east],
+  $DEGREE,
+ Id["TEST", "E1"]]
+
+GeodCRS["Error index 42 (on $BAD_DATUM)", $BAD_DATUM,
  CS[ellipsoidal, 2],
   Axis["Latitude", north],
   Axis["Longitude", east],
   $DEGREE,
- Id["MyCodeSpace", 102021, "v2"]]
+ Id["TEST", "E2"]]


Mime
View raw message