LocationFormatter.java
0001 /*
0002  * Java GPX Library (jpx-3.1.0).
0003  * Copyright (c) 2016-2023 Franz Wilhelmstötter
0004  *
0005  * Licensed under the Apache License, Version 2.0 (the "License");
0006  * you may not use this file except in compliance with the License.
0007  * You may obtain a copy of the License at
0008  *
0009  *      http://www.apache.org/licenses/LICENSE-2.0
0010  *
0011  * Unless required by applicable law or agreed to in writing, software
0012  * distributed under the License is distributed on an "AS IS" BASIS,
0013  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014  * See the License for the specific language governing permissions and
0015  * limitations under the License.
0016  *
0017  * Author:
0018  *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
0019  */
0020 package io.jenetics.jpx.format;
0021 
0022 import static java.util.Objects.requireNonNull;
0023 
0024 import java.text.ParsePosition;
0025 import java.util.ArrayList;
0026 import java.util.Iterator;
0027 import java.util.List;
0028 import java.util.Optional;
0029 import java.util.Set;
0030 import java.util.stream.Collectors;
0031 
0032 import io.jenetics.jpx.Latitude;
0033 import io.jenetics.jpx.Length;
0034 import io.jenetics.jpx.Longitude;
0035 
0036 /**
0037  * Formatter for printing and parsing geographic location objects.
0038  <p>
0039  <b>Patterns for Formatting and Parsing</b>
0040  <p>
0041  * Patterns are based on a simple sequence of letters and symbols. A pattern is
0042  * used to create a Formatter using the {@code #ofPattern(String)} and
0043  * {@code #ofPattern(String, Locale)} methods.
0044  *
0045  * For example, {@code D°MM'SS.SSS"X} will format to {@code 60°15'59.613"N}.
0046  *
0047  * A formatter created from a pattern can be used as many times as necessary, it
0048  * is immutable and is thread-safe.
0049  *
0050  <table class="striped">
0051  <caption><b>Pattern Letters and Symbols</b></caption>
0052  <thead>
0053  *  <tr><th scope="col">Symbol</th><th scope="col">Meaning</th><th scope="col">Examples</th>
0054  </thead>
0055  <tbody>
0056  *   <tr><th scope="row">L</th><td>deprecated synonym for 'D'</td></tr>
0057  *   <tr>
0058  *       <th scope="row">D</th>
0059  *       <td>Latitude in degrees. Values in {@code -90 <= d <= +90}. See examples.</td>
0060  *       <td>34; 23.2332</td>
0061  *   </tr>
0062  *   <tr>
0063  *       <th scope="row">M</th>
0064  *       <td>Latitude minutes. Values in {@code 0 <= m < 60}. See examples.</td>
0065  *       <td>45; 45.6</td>
0066  *   </tr>
0067  *   <tr>
0068  *       <th scope="row">S</th>
0069  *       <td>Latitude seconds. Values in {@code 0 <= s < 60}. See examples.</td>
0070  *       <td>7; 07</td>
0071  *   </tr>
0072  *   <tr>
0073  *       <th scope="row">X</th>
0074  *       <td>hemisphere (N or S)</td>
0075  *       <td>N; S</td>
0076  *   </tr>
0077  *   <tr><th scope="row">l</th><td>deprecated synonym for 'd'</td></tr>
0078  *   <tr>
0079  *       <th scope="row">d</th>
0080  *       <td>Longitude degrees. Values in {@code -180 <= d <= +180}. Similar to Latitude degrees.</td>
0081  *       <td>34; 23.2332</td>
0082  *   </tr>
0083  *   <tr>
0084  *       <th scope="row">m</th>
0085  *       <td>Longitude minutes. Similar to latitude minutes.</td>
0086  *       <td>45; 45.6</td>
0087  *   </tr>
0088  *   <tr>
0089  *       <th scope="row">s</th>
0090  *       <td>Longitude seconds. Similar to Latitude seconds.</td>
0091  *       <td>7; 07</td>
0092  *   </tr>
0093  *   <tr>
0094  *       <th scope="row">x</th>
0095  *       <td>hemisphere (E or W)</td>
0096  *       <td>E; W</td>
0097  *   </tr>
0098  *   <tr>
0099  *       <th scope="row">E</th>
0100  *       <td>Elevation in meters. See examples.</td>
0101  *       <td>234; 1023; -12</td>
0102  *   </tr>
0103  *   <tr><th scope="row">H</th><td>deprecated synonym for 'E'</td></tr>
0104  *   <tr>
0105  *       <th scope="row">'</th>
0106  *       <td>escape for text</td>
0107  *       <td></td>
0108  *   </tr>
0109  *   <tr>
0110  *       <th scope="row">''</th>
0111  *       <td>single quote</td>
0112  *       <td>'</td>
0113  *   </tr>
0114  *   <tr>
0115  *       <th scope="row">[</th>
0116  *       <td>optional section start</td>
0117  *       <td></td>
0118  *   </tr>
0119  *   <tr>
0120  *       <th scope="row">]</th>
0121  *       <td>optional section end</td>
0122  *       <td></td>
0123  *   </tr>
0124  </tbody>
0125  </table>
0126  *
0127  <table class="striped">
0128  <caption><b>Examples</b></caption>
0129  <thead>
0130  *  <tr>
0131  *      <th scope="col">Pattern</th>
0132  *      <th scope="col">Meaning</th>
0133  *      <th scope="col">Examples</th>
0134  </thead>
0135  <tbody>
0136  *   <tr>
0137  *       <th scope="row">+D</th>
0138  *       <td>Latitude sign indicated by prefix +/-</td>
0139  *       <td>+47; -24</td>
0140  *   </tr>
0141  *   <tr>
0142  *       <th scope="row">D X</th>
0143  *       <td>Latitude sign indicated by X</td>
0144  *       <td>47 N; 24 S</td>
0145  *   </tr>
0146  *   <tr>
0147  *       <th scope="row">D.DD</th>
0148  *       <td>Latitude degrees with decimal faction</td>
0149  *       <td>47.50</td>
0150  *   </tr>
0151  *   <tr>
0152  *       <th scope="row">D M</th>
0153  *       <td>Latitude in degrees and minutes</td>
0154  *       <td>47 30</td>
0155  *   </tr>
0156  *   <tr>
0157  *       <th scope="row">D</th>
0158  *       <td>Latitude degrees in variable width</td>
0159  *       <td>9; 47</td>
0160  *   </tr>
0161  *   <tr>
0162  *       <th scope="row">DD</th>
0163  *       <td>Latitude degrees in fixed width</td>
0164  *       <td>09; 47</td>
0165  *   </tr>
0166  *   <tr>
0167  *       <th scope="row">D M.MM</th>
0168  *       <td>Latitude minutes with decimal fraction</td>
0169  *       <td>47 2.25</td>
0170  *   </tr>
0171  *   <tr>
0172  *       <th scope="row">D M S</th>
0173  *       <td>Latitude minutes and seconds</td>
0174  *       <td>47 2 15</td>
0175  *   </tr>
0176  *   <tr>
0177  *       <th scope="row">D M</th>
0178  *       <td>Latitude minutes in variable width</td>
0179  *       <td>47 2; 46 12</td>
0180  *   </tr>
0181  *   <tr>
0182  *       <th scope="row">D MM</th>
0183  *       <td>Latitude minutes in fixed width</td>
0184  *       <td>47 02; 46 12</td>
0185  *   </tr>
0186  *   <tr>
0187  *       <th scope="row">D M S</th>
0188  *       <td>Latitude seconds in variable width</td>
0189  *       <td>47 2 3; 46 2 13</td>
0190  *   </tr>
0191  *   <tr>
0192  *       <th scope="row">D M SS</th>
0193  *       <td>Latitude seconds in fixed width</td>
0194  *       <td>47 2 03; 46 2 13</td>
0195  *   </tr>
0196  *   <tr>
0197  *       <th scope="row">D M S.SS</th>
0198  *       <td>Latitude seconds with decimal fraction</td>
0199  *       <td>46 2 13.54</td>
0200  *   </tr>
0201  *   <tr>
0202  *       <th scope="row">+d</th>
0203  *       <td>Longitude sign indicated by prefix +/-</td>
0204  *       <td>+147; -124</td>
0205  *   </tr>
0206  *   <tr>
0207  *       <th scope="row">d x</th>
0208  *       <td>Latitude sign indicated by x</td>
0209  *       <td>147 E; 124 W</td>
0210  *   </tr>
0211  *   <tr>
0212  *       <th scope="row">d</th>
0213  *       <td>Longitude degrees in variable width</td>
0214  *       <td>9; 47; 175</td>
0215  *   </tr>
0216  *   <tr>
0217  *       <th scope="row">ddd</th>
0218  *       <td>Longitude degrees in fixed width</td>
0219  *       <td>009; 047; 175</td>
0220  *   </tr>
0221  *   <tr>
0222  *       <th scope="row">d m.mm</th>
0223  *       <td>Longitude minutes with decimal fraction</td>
0224  *       <td>47 2.25</td>
0225  *   </tr>
0226  *   <tr>
0227  *       <th scope="row">d m s</th>
0228  *       <td>Longitude minutes and seconds</td>
0229  *       <td>47 2 15</td>
0230  *   </tr>
0231  *   <tr>
0232  *       <th scope="row">d m</th>
0233  *       <td>Longitude minutes in variable width</td>
0234  *       <td>47 2; 46 12</td>
0235  *   </tr>
0236  *   <tr>
0237  *       <th scope="row">d mm</th>
0238  *       <td>Longitude minutes in fixed width</td>
0239  *       <td>47 02; 46 12</td>
0240  *   </tr>
0241  *   <tr>
0242  *       <th scope="row">d m s</th>
0243  *       <td>Longitude seconds in variable width</td>
0244  *       <td>47 2 3; 46 2 13</td>
0245  *   </tr>
0246  *   <tr>
0247  *       <th scope="row">d m ss</th>
0248  *       <td>Longitude seconds in fixed width</td>
0249  *       <td>47 2 03; 46 2 13</td>
0250  *   </tr>
0251  *   <tr>
0252  *       <th scope="row">d m s.ss</th>
0253  *       <td>Longitude seconds with decimal fraction</td>
0254  *       <td>46 2 13.54</td>
0255  *   </tr>
0256  *   <tr>
0257  *       <th scope="row">E</th>
0258  *       <td>Elevation without positive sign</td>
0259  *       <td>9; -9</td>
0260  *   </tr>
0261  *   <tr>
0262  *       <th scope="row">+E</th>
0263  *       <td>Elevation with positive or negative sign</td>
0264  *       <td>+9; -9</td>
0265  *   </tr>
0266  *   <tr>
0267  *       <th scope="row">E</th>
0268  *       <td>Elevation in variable width</td>
0269  *       <td>9; 19; 190</td>
0270  *   </tr>
0271  *   <tr>
0272  *       <th scope="row">EEE</th>
0273  *       <td>Elevation in fixed width</td>
0274  *       <td>009; 019; 190</td>
0275  *   </tr>
0276  *   <tr>
0277  *       <th scope="row">E.EEE</th>
0278  *       <td>Elevation with decimal fraction</td>
0279  *       <td>9.123</td>
0280  *   </tr>
0281  </tbody>
0282  </table>
0283  *
0284  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
0285  @version 2.2
0286  @since 1.4
0287  */
0288 public final class LocationFormatter {
0289 
0290     static final Set<Character> PROTECTED_CHARS = Set.of(
0291         'L''D''M''S''l''d''m''s''E''H''X''x''+''['']'
0292     );
0293 
0294     /**
0295      * Latitude formatter with the pattern <em>{@code D°MM''SS.SSS"X}</em>.
0296      * Example: <em>{@code 16°27'59.180"N}</em>.
0297      */
0298     public static final LocationFormatter ISO_HUMAN_LAT_LONG =
0299         ofPattern("D°MM''SS.SSS\"X");
0300 
0301     /**
0302      * Longitude formatter with the pattern <em>{@code d°mm''ss.sss"x}</em>.
0303      * Example: <em>{@code 16°27'59.180"E}</em>.
0304      */
0305     public static final LocationFormatter ISO_HUMAN_LON_LONG =
0306         ofPattern("d°mm''ss.sss\"x");
0307 
0308     /**
0309      * Elevation formatter with the pattern <em>{@code E.EE'm'}</em>. Example:
0310      <em>{@code 2045m}</em>.
0311      */
0312     public static final LocationFormatter ISO_HUMAN_ELE_LONG =
0313         ofPattern("E.EE'm'");
0314 
0315     /**
0316      * Elevation formatter with the pattern
0317      <em>{@code D°MM''SS.SSS"X d°mm''ss.sss"x[ E.EE'm']}</em>.
0318      * Example: <em>{@code 50°03′46.461″S 125°48′26.533″E 978.90m}</em>.
0319      */
0320     public static final LocationFormatter ISO_HUMAN_LONG =
0321         ofPattern("D°MM''SS.SSS\"X d°mm''ss.sss\"x[ E.EE'm']");
0322 
0323     /**
0324      * ISO 6709 conform latitude format, short: <em>{@code +DD.DD}</em>.
0325      */
0326     public static final LocationFormatter ISO_LAT_SHORT = ofPattern("+DD.DD");
0327 
0328     /**
0329      * ISO 6709 conform latitude format, medium: <em>{@code +DDMM.MMM}</em>.
0330      */
0331     public static final LocationFormatter ISO_LAT_MEDIUM = ofPattern("+DDMM.MMM");
0332 
0333     /**
0334      * ISO 6709 conform latitude format, long: <em>{@code +DDMMSS.SS}</em>.
0335      */
0336     public static final LocationFormatter ISO_LAT_LONG = ofPattern("+DDMMSS.SS");
0337 
0338     /**
0339      * ISO 6709 conform longitude format, short: <em>{@code +ddd.dd}</em>.
0340      */
0341     public static final LocationFormatter ISO_LON_SHORT = ofPattern("+ddd.dd");
0342 
0343     /**
0344      * ISO 6709 conform longitude format, medium: <em>{@code +dddmm.mmm}</em>.
0345      */
0346     public static final LocationFormatter ISO_LON_MEDIUM = ofPattern("+dddmm.mmm");
0347 
0348     /**
0349      * ISO 6709 conform longitude format, long: <em>{@code +dddmmss.ss}</em>.
0350      */
0351     public static final LocationFormatter ISO_LON_LONG = ofPattern("+dddmmss.ss");
0352 
0353     /**
0354      * ISO 6709 conform elevation format, short: <em>{@code +E'CRS'}</em>.
0355      */
0356     public static final LocationFormatter ISO_ELE_SHORT = ofPattern("+E'CRS'");
0357 
0358     /**
0359      * ISO 6709 conform elevation format, medium: <em>{@code +E.E'CRS'}</em>.
0360      */
0361     public static final LocationFormatter ISO_ELE_MEDIUM = ofPattern("+E.E'CRS'");
0362 
0363     /**
0364      * ISO 6709 conform elevation format, long: <em>{@code +E.EE'CRS'}</em>.
0365      */
0366     public static final LocationFormatter ISO_ELE_LONG = ofPattern("+E.EE'CRS'");
0367 
0368     /**
0369      * ISO 6709 conform location format, short:
0370      <em>{@code +DD.DD+ddd.dd[+E'CRS']}</em>.
0371      */
0372     public static final LocationFormatter ISO_SHORT =
0373         ofPattern("+DD.DD+ddd.dd[+E'CRS']");
0374 
0375     /**
0376      * ISO 6709 conform location format, medium:
0377      <em>{@code +DDMM.MMM+dddmm.mmm[+E.E'CRS']}</em>.
0378      */
0379     public static final LocationFormatter ISO_MEDIUM =
0380         ofPattern("+DDMM.MMM+dddmm.mmm[+E.E'CRS']");
0381 
0382     /**
0383      * ISO 6709 conform location format, medium:
0384      <em>{@code +DDMMSS.SS+dddmmss.ss[+E.EE'CRS']}</em>.
0385      */
0386     public static final LocationFormatter ISO_LONG =
0387         ofPattern("+DDMMSS.SS+dddmmss.ss[+E.EE'CRS']");
0388 
0389     private final List<Format> _formats;
0390 
0391     private LocationFormatter(final List<Format> formats) {
0392         _formats = List.copyOf(formats);
0393     }
0394 
0395     /**
0396      * Formats the given {@code location} using {@code this} formatter.
0397      @param location the location to format
0398      @return the format string
0399      @throws NullPointerException if the given {@code location} is {@code null}
0400      @throws FormatterException if the formatter tries to format a non-existing,
0401      *         non-optional location fields.
0402      */
0403     public String format(final Location location) {
0404         requireNonNull(location);
0405         return _formats.stream()
0406             .map(format -> format
0407                 .format(location)
0408                 .orElseThrow(() -> toError(location)))
0409             .collect(Collectors.joining());
0410     }
0411 
0412     private FormatterException toError(final Location location) {
0413         return new FormatterException(String.format(
0414             "Invalid format '%s' for location %s.", toPattern(), location
0415         ));
0416     }
0417 
0418     /**
0419      * Return the pattern string represented by this formatter.
0420      *
0421      @see #ofPattern(String)
0422      *
0423      @return the pattern string of {@code this} formatter
0424      */
0425     public String toPattern() {
0426         return _formats.stream()
0427             .map(Format::toPattern)
0428             .collect(Collectors.joining());
0429     }
0430 
0431     /**
0432      * Formats the given location elements using {@code this} formatter.
0433      *
0434      @see #format(Location)
0435      *
0436      @param lat the latitude part of the location
0437      @param lon the longitude part of the location
0438      @param ele the elevation part of the location
0439      @return the format string
0440      @throws FormatterException if the formatter tries to format a non-existing,
0441      *         non-optional location fields.
0442      */
0443     public String format(
0444         final Latitude lat,
0445         final Longitude lon,
0446         final Length ele
0447     ) {
0448         return format(Location.of(lat, lon, ele));
0449     }
0450 
0451     /**
0452      * Formats the given location elements using {@code this} formatter.
0453      *
0454      @see #format(Location)
0455      *
0456      @param lat the latitude part of the location
0457      @param lon the longitude part of the location
0458      @return the format string
0459      @throws FormatterException if the formatter tries to format a non-existing,
0460      *         non-optional location fields.
0461      */
0462     public String format(final Latitude lat, final Longitude lon) {
0463         return format(lat, lon, null);
0464     }
0465 
0466     /**
0467      * Formats the given location elements using {@code this} formatter.
0468      *
0469      @see #format(Location)
0470      *
0471      @param lat the latitude part of the location
0472      @return the format string
0473      @throws FormatterException if the formatter tries to format a non-existing,
0474      *         non-optional location fields.
0475      */
0476     public String format(final Latitude lat) {
0477         return format(lat, null, null);
0478     }
0479 
0480     /**
0481      * Formats the given location elements using {@code this} formatter.
0482      *
0483      @see #format(Location)
0484      *
0485      @param lon the longitude part of the location
0486      @return the format string
0487      @throws FormatterException if the formatter tries to format a non-existing,
0488      *         non-optional location fields.
0489      */
0490     public String format(final Longitude lon) {
0491         return format(null, lon, null);
0492     }
0493 
0494     /**
0495      * Formats the given location elements using {@code this} formatter.
0496      *
0497      @see #format(Location)
0498      *
0499      @param ele the elevation part of the location
0500      @return the format string
0501      @throws FormatterException if the formatter tries to format a non-existing,
0502      *         non-optional location field.
0503      */
0504     public String format(final Length ele) {
0505         return format(null, null, ele);
0506     }
0507 
0508     /**
0509      * Parses the text using this formatter, providing control over the text
0510      * position. This parses the text without requiring the parse to start from
0511      * the beginning of the string or finish at the end. The text will be parsed
0512      * from the specified start ParsePosition. The entire length of the text
0513      * does not have to be parsed, the {@link ParsePosition} will be updated with
0514      * the index at the end of parsing.
0515      *
0516      @param text the text to parse, not null
0517      @param pos the position to parse from, updated with length parsed and the
0518      *            index of any error, not null
0519      @return the parsed Location, not null
0520      @throws ParseException - if unable to parse the requested result
0521      @throws IndexOutOfBoundsException - if the position is invalid
0522      */
0523     public Location parse(final CharSequence text, final ParsePosition pos) {
0524         requireNonNull(text);
0525         requireNonNull(pos);
0526 
0527         if (pos.getIndex() || text.length() <= pos.getIndex()) {
0528             throw new IndexOutOfBoundsException(pos.getIndex());
0529         }
0530 
0531         final var builder = new LocationBuilder();
0532         for (var format : _formats) {
0533             format.parse(text, pos, builder);
0534         }
0535 
0536         return builder.build();
0537     }
0538 
0539     /**
0540      * Fully parses the text producing a location. This parses the entire text
0541      * producing a location. It is typically more useful to use
0542      {@link #parse(CharSequence, ParsePosition)}. If the parse completes
0543      * without reading the entire length of the text, or a problem occurs during
0544      * parsing or merging, then an exception is thrown.
0545      *
0546      @param text the text to parse, not null
0547      @return the parsed temporal object, not null
0548      @throws ParseException if unable to parse the requested result
0549      */
0550     public Location parse(final CharSequence text) {
0551         requireNonNull(text);
0552 
0553         final var pos = new ParsePosition(0);
0554         final var location = parse(text, pos);
0555         if (pos.getIndex() != text.length()) {
0556             throw new ParseException("Not all input used", text, pos.getIndex());
0557         }
0558 
0559         return location;
0560     }
0561 
0562     @Override
0563     public String toString() {
0564         return String.format("LocationFormat[%s]", toPattern());
0565     }
0566 
0567     /* *************************************************************************
0568      * Static factory methods.
0569      * ************************************************************************/
0570 
0571     /**
0572      * Creates a formatter using the specified pattern.
0573      *
0574      @see #toPattern()
0575      *
0576      @param pattern the formatter pattern
0577      @return the location-formatter of the given {@code pattern}
0578      @throws NullPointerException if the given {@code pattern} is {@code null}
0579      @throws IllegalArgumentException if the given {@code pattern} is invalid
0580      */
0581     public static LocationFormatter ofPattern(final String pattern) {
0582         requireNonNull(pattern);
0583 
0584         return new Builder()
0585             .appendPattern(pattern)
0586             .build();
0587     }
0588 
0589     /* *************************************************************************
0590      * Inner classes.
0591      * ************************************************************************/
0592 
0593     /**
0594      * Builder to create location formatters. This allows a
0595      * {@code LocationFormatter} to be created. All location formatters are
0596      * created ultimately using this builder.
0597      *
0598      * @implNote
0599      * This class is a mutable builder intended for use from a single thread.
0600      *
0601      @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
0602      @version 1.4
0603      @since 1.4
0604      */
0605     static class Builder {
0606 
0607         /**
0608          * The formats that will go into the LocationFormatter.
0609          */
0610         private final List<Format> _formats = new ArrayList<>();
0611 
0612         private Builder() {
0613         }
0614 
0615         /**
0616          * Appends the elements defined by the specified pattern to the builder.
0617          *
0618          @param pattern the pattern to add
0619          @return {@code this}, for chaining, not {@code null}
0620          @throws NullPointerException if the given {@code pattern} is
0621          *         {@code null}
0622          @throws IllegalArgumentException if the given {@code pattern} is
0623          *         invalid
0624          */
0625         Builder appendPattern(final String pattern) {
0626             parsePattern(pattern);
0627             return this;
0628         }
0629 
0630         /**
0631          * Completes this builder by creating the {@code LocationFormatter}.
0632          *
0633          @return a new location-formatter object
0634          @throws IllegalArgumentException invalid pattern
0635          */
0636         LocationFormatter build() {
0637             validate();
0638             return new LocationFormatter(_formats);
0639         }
0640 
0641         private void validate(){
0642             LatitudeDegree D = null;
0643             LatitudeMinute M = null;
0644             LatitudeSecond S = null;
0645             LatitudeNS X = null;
0646             LongitudeDegree d = null;
0647             LongitudeMinute m = null;
0648             LongitudeSecond s = null;
0649             LongitudeEW x = null;
0650             Elevation E = null;
0651 
0652             for (var format : _formats) {
0653                 if (format instanceof LatitudeDegree ld) {
0654                     if (D == null) {
0655                         D = ld;
0656                     else {
0657                         throw iae("Only one 'D' pattern allowed.");
0658                     }
0659                 else if (format instanceof LatitudeMinute lm) {
0660                     if (M == null) {
0661                         M = lm;
0662                     else {
0663                         throw iae("Only one 'M' pattern allowed.");
0664                     }
0665                 else if (format instanceof LatitudeSecond ls) {
0666                     if (S == null) {
0667                         S = ls;
0668                     else {
0669                         throw iae("Only one 'S' pattern allowed.");
0670                     }
0671                 else if (format instanceof LatitudeNS && X==null) {
0672                     X = (LatitudeNS)format;
0673                 else if (format instanceof LongitudeDegree ld) {
0674                     if (d == null) {
0675                         d = ld;
0676                     else {
0677                         throw iae("Only one 'd' pattern allowed.");
0678                     }
0679                 else if (format instanceof LongitudeMinute lm) {
0680                     if (m == null) {
0681                         m = lm;
0682                     else {
0683                         throw iae("Only one 'm' pattern allowed.");
0684                     }
0685                 else if (format instanceof LongitudeSecond ls) {
0686                     if (s == null) {
0687                         s = ls;
0688                     else {
0689                         throw iae("Only one 's' pattern allowed.");
0690                     }
0691                 else if (format instanceof LongitudeEW lew && x == null) {
0692                     x = lew;
0693                 else if (format instanceof Elevation ele) {
0694                     if (E == null) {
0695                         E = ele;
0696                     else {
0697                         throw iae("Only one 'E' pattern allowed.");
0698                     }
0699                 }
0700             }
0701 
0702             // Validating latitude /////////////////////////////////////////////
0703 
0704             if (D == null && M != null) {
0705                 throw iae("No 'M' without 'D'.");
0706             }
0707             if (M == null && S != null) {
0708                 throw iae("No 'S' without 'M'.");
0709             }
0710 
0711             // If X, D without sign.
0712             if (X != null && D != null && D.isPrefixSign()) {
0713                 throw iae("If 'X' in pattern, 'D' must be without '+'.");
0714             }
0715 
0716             // If D has fractional, no M or S
0717             if (D != null &&
0718                 < D.getMinimumFractionDigits()
0719                 && M != null)
0720             {
0721                 throw iae("If 'D' has fraction, no 'M' or 'S' allowed.");
0722             }
0723 
0724             // If M has fractional, no S
0725             if (M != null &&
0726                 < M.getMinimumFractionDigits() &&
0727                 S != null)
0728             {
0729                 throw iae("If 'M' has fraction, no 'S' allowed.");
0730             }
0731 
0732             // Validating longitude ////////////////////////////////////////////
0733 
0734             if (d == null && m != null) {
0735                 throw iae("No 'm' without 'd'.");
0736             }
0737             if (m == null && s != null) {
0738                 throw iae("No 's' without 'm'.");
0739             }
0740 
0741             // If x, d without sign.
0742             if (x != null && d != null && d.isPrefixSign()) {
0743                 throw iae("If 'x' in pattern, 'd' must be without '+'.");
0744             }
0745 
0746             // If d has fractional, no m or s
0747             if (d != null &&
0748                 < d.getMinimumFractionDigits() &&
0749                 m != null)
0750             {
0751                 throw iae("If 'd' has fraction, no 'm' or 's' allowed.");
0752             }
0753 
0754             // If m has fractional, no s.
0755             if (m != null &&
0756                 < m.getMinimumFractionDigits() &&
0757                 s != null)
0758             {
0759                 throw iae("If 'm' has fraction, no 's' allowed.");
0760             }
0761 
0762             // This is still construction, not a validity check. ///////////////
0763 
0764             if (X != null && D != null) {
0765                 D.setAbsolute(true);
0766             }
0767             if (M != null) {
0768                 D.setTruncate(true);
0769             }
0770             if (S != null) {
0771                 M.setTruncate(true);
0772             }
0773 
0774             if (x != null && d != null) {
0775                 d.setAbsolute(true);
0776             }
0777             if (m != null) {
0778                 d.setTruncate(true);
0779             }
0780             if (s != null) {
0781                 m.setTruncate(true);
0782             }
0783         }
0784 
0785         private static IllegalArgumentException iae(final String message) {
0786             return new IllegalArgumentException(message);
0787         }
0788 
0789         private void parsePattern(final String pattern) {
0790             requireNonNull(pattern);
0791 
0792             // The formats we've collected and that are not yet added to
0793             // formats. They may be added to _formats directly or be bundled
0794             // into an Optional first.
0795             final List<Format> formats = new ArrayList<>();
0796 
0797             boolean optional = false// Inside [ ] ?
0798             int signs = 0;            // How many unprocessed '+' ?
0799             boolean quote = false;    // last was ' ?
0800 
0801             final var tokens = new Tokens(tokenize(pattern));
0802             while (tokens.hasNext()) {
0803                 var token = tokens.next();
0804                 switch (token) {
0805                     case "X" -> {
0806                         List<Format> fs = optional ? formats : _formats;
0807                         for (int i = 0; i < signs; ++ifs.add(Plus.INSTANCE);
0808                         signs = 0;
0809                         fs.add(LatitudeNS.INSTANCE);
0810                     }
0811                     case "x" -> {
0812                         List<Format> fs = optional ? formats : _formats;
0813                         for (int i = 0; i < signs; ++ifs.add(Plus.INSTANCE);
0814                         signs = 0;
0815                         fs.add(LongitudeEW.INSTANCE);
0816                     }
0817                     case "+" -> ++signs;
0818                     case "[" -> {
0819                         if (optional) {
0820                             throw iae("No nesting '[' (optional) allowed.");
0821                         }
0822                         for (int i = 0; i < signs; i++) {
0823                             _formats.add(Plus.INSTANCE);
0824                         }
0825                         signs = 0;
0826                         optional = true;
0827                     }
0828                     case "]" -> {
0829                         if (!optional) {
0830                             throw iae("Missing open '[' bracket.");
0831                         }
0832                         // Formats will be bundled into Optional and added to
0833                         // _formats.
0834                         for (int i = 0; i < signs; i++) {
0835                             formats.add(Plus.INSTANCE);
0836                         }
0837                         signs = 0;
0838                         optional = false;
0839                         _formats.add(OptionalFormat.of(formats));
0840                         formats.clear();
0841                     }
0842                     case "'" -> {
0843                         List<Format> fs = optional ? formats : _formats;
0844                         for (int i = 0; i < signs; ++i)
0845                             fs.add(Plus.INSTANCE);
0846 
0847                         if (tokens.after().filter("'"::equals).isPresent()) {
0848                             fs.add(ConstFormat.of("'"));
0849                             tokens.next();
0850                             break;
0851                         }
0852                         if (quote) {
0853                             if (tokens.before().isPresent()) {
0854                                 fs.add(ConstFormat.of(
0855                                     tokens.before()
0856                                         .orElseThrow(AssertionError::new)
0857                                 ));
0858                             }
0859                             quote = false;
0860                         else {
0861                             quote = true;
0862                         }
0863                     }
0864                     default -> {
0865                         List<Format> fs = optional ? formats : _formats;
0866                         if (!quote) {
0867                             final var field = Field.ofPattern(token);
0868                             if (field.isPresent()) {
0869                                 final var f = field.get();
0870 
0871                                 // Maybe first add some sign formats.
0872                                 if (< signs) {
0873                                     // One goes to the field.
0874                                     f.setPrefixSign(true);
0875                                     for (int i = 1; i < signs; i++) {
0876                                         // The rest will be Plus.
0877                                         fs.add(Plus.INSTANCE);
0878                                     }
0879                                 }
0880 
0881                                 fs.add(f);
0882                             else {
0883                                 fs.add(ConstFormat.of(token));
0884                             }
0885                         }
0886                         signs = 0;
0887                     }
0888                 }
0889             }
0890 
0891             // Maybe there are still signs left over.
0892             for (int i = 0; i < signs; i++) {
0893                 formats.add(Plus.INSTANCE);
0894             }
0895 
0896             if (optional) {
0897                 throw iae("No closing ']' found.");
0898             }
0899             if (quote) {
0900                 throw iae("Missing closing ' character.");
0901             }
0902 
0903             _formats.addAll(formats);
0904         }
0905 
0906         static List<String> tokenize(final String pattern) {
0907             final var tokens = new ArrayList<String>();
0908             final var token = new StringBuilder();
0909 
0910             boolean quote = false;
0911             char pc = '\0';
0912             for (int i = 0; i < pattern.length(); ++i) {
0913                 final char c = pattern.charAt(i);
0914 
0915                 switch (c) {
0916                     case '\'':
0917                         quote = !quote;
0918                         if (token.length() 0) {
0919                             tokens.add(token.toString());
0920                             token.setLength(0);
0921                         }
0922                         tokens.add(Character.toString(c));
0923                         break;
0924                     case 'x''X''+''['']':
0925                         if (quote) {
0926                             token.append(c);
0927                         else {
0928                             if (token.length() 0) {
0929                                 tokens.add(token.toString());
0930                                 token.setLength(0);
0931                             }
0932                             tokens.add(Character.toString(c));
0933                         }
0934                         break;
0935                     case 'L''D''M''S''l''d''m''s''E''H':
0936                         if (c != pc &&
0937                             pc != '\0' &&
0938                             pc != '.' &&
0939                             pc != ',' &&
0940                             !quote)
0941                         {
0942                             if (token.length() 0) {
0943                                 tokens.add(token.toString());
0944                                 token.setLength(0);
0945                             }
0946                         }
0947                         token.append(c);
0948                         break;
0949                     case ',''.':
0950                         token.append(c);
0951                         break;
0952                     default:
0953                         if (PROTECTED_CHARS.contains(pc|| pc == '\'') {
0954                             if (token.length() 0) {
0955                                 tokens.add(token.toString());
0956                                 token.setLength(0);
0957                             }
0958                         }
0959                         token.append(c);
0960                         break;
0961                 }
0962 
0963                 pc = c;
0964             }
0965 
0966             if (token.length() 0) {
0967                 tokens.add(token.toString());
0968             }
0969 
0970             return tokens;
0971         }
0972 
0973     }
0974 
0975     private static final class Tokens implements Iterator<String> {
0976         private final List<String> _tokens;
0977 
0978         private int _index = 0;
0979 
0980         private Tokens(final List<String> tokens) {
0981             _tokens = List.copyOf(tokens);
0982         }
0983 
0984         @Override
0985         public boolean hasNext() {
0986             return _index < _tokens.size();
0987         }
0988 
0989         @Override
0990         public String next() {
0991             return _tokens.get(_index++);
0992         }
0993 
0994         Optional<String> before() {
0995             return _index - 0
0996                 ? Optional.of(_tokens.get(_index - 2))
0997                 : Optional.empty();
0998         }
0999 
1000         Optional<String> after() {
1001             return hasNext()
1002                 ? Optional.of(_tokens.get(_index))
1003                 : Optional.empty();
1004         }
1005 
1006     }
1007 
1008 }