All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jenetics.jpx.format.LocationFormatter Maven / Gradle / Ivy

There is a newer version: 3.2.1
Show newest version
/*
 * Java GPX Library (jpx-1.7.0).
 * Copyright (c) 2016-2020 Franz Wilhelmstötter
 *
 * Licensed 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.
 *
 * Author:
 *    Franz Wilhelmstötter ([email protected])
 */
package io.jenetics.jpx.format;

import static java.util.Objects.requireNonNull;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import io.jenetics.jpx.Latitude;
import io.jenetics.jpx.Length;
import io.jenetics.jpx.Longitude;
import io.jenetics.jpx.format.Location.Field;

/**
 * Formatter for printing and parsing geographic location objects.
 * 

Patterns for Formatting and Parsing

* Patterns are based on a simple sequence of letters and symbols. A pattern is * used to create a Formatter using the {@code #ofPattern(String)} and * {@code #ofPattern(String, Locale)} methods. * For example, {@code DD°MM'SS.SSS"X} will format to, for example, * {@code 60°15'59.613"N}. A formatter created from a pattern can be used as * many times as necessary, it is immutable and is thread-safe. * * * * * * * * * * * * * * * * * * * * * * * *
Pattern Letters and Symbols
Symbol Meaning Examples
L the latitude value -34.4334; 23.2332
D (absolute) degree part of latitude 34; 23.2332
M minute part of latitude 45; 45.6
S second part of latitude 7; 07
X hemisphere (N or S) N; S
l the longitude value 34; -23.2332
d (absolute) degree part of longitude 34; 23.2332
m minute part of longitude 45; 45.6
s second part of longitude 7; 07
x hemisphere (E or W) E; W
E elevation in meters 234; 1023; -12
H (absolute) elevation in meters 234; 1023; 12
' escape for text
'' single quote '
[ optional section start
] optional section end
* * @author Franz Wilhelmstötter * @version 1.4 * @since 1.4 */ public final class LocationFormatter { static final Set PROTECTED_CHARS = new HashSet<>(Arrays.asList( 'L', 'D', 'M', 'S', 'l', 'd', 'm', 's', 'E', 'H', 'X', 'x', '+', '[', ']' )); /** * Latitude formatter with the pattern {@code DD°MM''SS.SSS"X}. * Example: {@code 16°27'59.180"N}. */ public static final LocationFormatter ISO_HUMAN_LAT_LONG = builder() .append(Field.DEGREE_OF_LATITUDE, "00") .appendLiteral("°") .append(Field.MINUTE_OF_LATITUDE, "00") .appendLiteral("'") .append(Field.SECOND_OF_LATITUDE, "00.000") .appendLiteral("\"") .appendNorthSouthHemisphere() .build(); /** * Longitude formatter with the pattern {@code dd°mm''ss.sss"x}. * Example: {@code 16°27'59.180"E}. */ public static final LocationFormatter ISO_HUMAN_LON_LONG = builder() .append(Field.DEGREE_OF_LONGITUDE, "00") .appendLiteral("°") .append(Field.MINUTE_OF_LONGITUDE, "00") .appendLiteral("'") .append(Field.SECOND_OF_LONGITUDE, "00.000") .appendLiteral("\"") .appendEastWestHemisphere() .build(); /** * Elevation formatter with the pattern {@code E.EE'm'}. Example: * {@code 2045m}. */ public static final LocationFormatter ISO_HUMAN_ELE_LONG = builder() .append(Field.ELEVATION, "0.00") .appendLiteral("m") .build(); /** * Elevation formatter with the pattern * {@code DD°MM''SS.SSS"X dd°mm''ss.sss"x[ E.EE'm']}. * Example: {@code 50°03′46.461″S 125°48′26.533″E 978.90m}. */ public static final LocationFormatter ISO_HUMAN_LONG = builder() .append(ISO_HUMAN_LAT_LONG) .appendLiteral(" ") .append(ISO_HUMAN_LON_LONG) .append( builder() .appendLiteral(" ") .append(ISO_HUMAN_ELE_LONG) .build(), true) .build(); /** * ISO 6709 conform latitude format, short: {@code +DD.DD}. */ public static final LocationFormatter ISO_LAT_SHORT = builder() .appendLatitudeSign() .append(Field.DEGREE_OF_LATITUDE, "00.00") .build(); /** * ISO 6709 conform latitude format, medium: {@code +DDMM.MMM}. */ public static final LocationFormatter ISO_LAT_MEDIUM = builder() .appendLatitudeSign() .append(Field.DEGREE_OF_LATITUDE, "00") .append(Field.MINUTE_OF_LATITUDE, "00.000") .build(); /** * ISO 6709 conform latitude format, long: {@code +DDMMSS.SS}. */ public static final LocationFormatter ISO_LAT_LONG = builder() .appendLatitudeSign() .append(Field.DEGREE_OF_LATITUDE, "00") .append(Field.MINUTE_OF_LATITUDE, "00") .append(Field.SECOND_OF_LATITUDE, "00.00") .build(); /** * ISO 6709 conform longitude format, short: {@code +ddd.dd}. */ public static final LocationFormatter ISO_LON_SHORT = builder() .appendLongitudeSign() .append(Field.DEGREE_OF_LONGITUDE, "000.00") .build(); /** * ISO 6709 conform longitude format, medium: {@code +dddmm.mmm}. */ public static final LocationFormatter ISO_LON_MEDIUM = builder() .appendLongitudeSign() .append(Field.DEGREE_OF_LONGITUDE, "000") .append(Field.MINUTE_OF_LONGITUDE, "00.000") .build(); /** * ISO 6709 conform longitude format, long: {@code +dddmmss.ss}. */ public static final LocationFormatter ISO_LON_LONG = builder() .appendLongitudeSign() .append(Field.DEGREE_OF_LONGITUDE, "000") .append(Field.MINUTE_OF_LONGITUDE, "00") .append(Field.SECOND_OF_LONGITUDE, "00.00") .build(); /** * ISO 6709 conform elevation format, short: {@code +E'CRS'}. */ public static final LocationFormatter ISO_ELE_SHORT = builder() .appendElevationSign() .append(Field.METER_OF_ELEVATION, "0") .appendLiteral("CRS") .build(); /** * ISO 6709 conform elevation format, medium: {@code +E.E'CRS'}. */ public static final LocationFormatter ISO_ELE_MEDIUM = builder() .appendElevationSign() .append(Field.METER_OF_ELEVATION, "0.0") .appendLiteral("CRS") .build(); /** * ISO 6709 conform elevation format, long: {@code +E.EE'CRS'}. */ public static final LocationFormatter ISO_ELE_LONG = builder() .appendElevationSign() .append(Field.METER_OF_ELEVATION, "0.00") .appendLiteral("CRS") .build(); /** * ISO 6709 conform location format, short: * {@code +DD.DD+ddd.dd[+E'CRS']}. */ public static final LocationFormatter ISO_SHORT = builder() .append(ISO_LAT_SHORT) .append(ISO_LON_SHORT) .append(ISO_ELE_SHORT, true) .build(); /** * ISO 6709 conform location format, medium: * {@code +DDMM.MMM+ddmm.mmm[+E.E'CRS']}. */ public static final LocationFormatter ISO_MEDIUM = builder() .append(ISO_LAT_MEDIUM) .append(ISO_LON_MEDIUM) .append(ISO_ELE_MEDIUM, true) .build(); /** * ISO 6709 conform location format, medium: * {@code +DDMMSS.SS+ddmmss.ss[+E.EE'CRS']}. */ public static final LocationFormatter ISO_LONG = builder() .append(ISO_LAT_LONG) .append(ISO_LON_LONG) .append(ISO_ELE_LONG, true) .build(); private final List> _formats; private LocationFormatter(final List> formats) { _formats = requireNonNull(formats); } /** * Return a new formatter builder instance. * * @return a new formatter builder instance */ public static Builder builder() { return new Builder(); } /** * Formats the given {@code location} using {@code this} formatter. * * @param location the location to format * @return the format string * @throws NullPointerException if the given {@code location} is {@code null} * @throws FormatterException if the formatter tries to format a non-existing, * non-optional location fields. */ public String format(final Location location) { requireNonNull(location); return _formats.stream() .map(format -> format.format(location) .orElseThrow(() -> toError(location))) .collect(Collectors.joining()); } private FormatterException toError(final Location location) { return new FormatterException(String.format( "Invalid format '%s' for location %s.", toPattern(), location )); } /** * Return the pattern string represented by this formatter. * * @see #ofPattern(String) * * @return the pattern string of {@code this} formatter */ public String toPattern() { return _formats.stream() .map(Objects::toString) .collect(Collectors.joining()); } /** * Formats the given location elements using {@code this} formatter. * * @see #format(Location) * * @param lat the latitude part of the location * @param lon the longitude part of the location * @param ele the elevation part of the location * @return the format string * @throws FormatterException if the formatter tries to format a non-existing, * non-optional location fields. */ public String format(final Latitude lat, final Longitude lon, final Length ele) { return format(Location.of(lat, lon, ele)); } /** * Formats the given location elements using {@code this} formatter. * * @see #format(Location) * * @param lat the latitude part of the location * @param lon the longitude part of the location * @return the format string * @throws FormatterException if the formatter tries to format a non-existing, * non-optional location fields. */ public String format(final Latitude lat, final Longitude lon) { return format(lat, lon, null); } /** * Formats the given location elements using {@code this} formatter. * * @see #format(Location) * * @param lat the latitude part of the location * @return the format string * @throws FormatterException if the formatter tries to format a non-existing, * non-optional location fields. */ public String format(final Latitude lat) { return format(lat, null, null); } /** * Formats the given location elements using {@code this} formatter. * * @see #format(Location) * * @param lon the longitude part of the location * @return the format string * @throws FormatterException if the formatter tries to format a non-existing, * non-optional location fields. */ public String format(final Longitude lon) { return format(null, lon, null); } /** * Formats the given location elements using {@code this} formatter. * * @see #format(Location) * * @param ele the elevation part of the location * @return the format string * @throws FormatterException if the formatter tries to format a non-existing, * non-optional location field. */ public String format(final Length ele) { return format(null, null, ele); } // // public Location parse(final CharSequence text, final ParsePosition pos) { // return null; // } // // public Location parse(final CharSequence text) { // return null; // } @Override public String toString() { return String.format("LocationFormat[%s]", toPattern()); } /* ************************************************************************* * Static factory methods. * ************************************************************************/ /** * Creates a formatter using the specified pattern. * * @see #toPattern() * * @param pattern the formatter pattern * @return the location-formatter of the given {@code pattern} * @throws NullPointerException if the given {@code pattern} is {@code null} * @throws IllegalArgumentException if the given {@code pattern} is invalid */ public static LocationFormatter ofPattern(final String pattern) { return builder() .appendPattern(pattern) .build(); } /* ************************************************************************* * Inner classes. * ************************************************************************/ /** * Builder to create location formatters. This allows a * {@code LocationFormatter} to be created. All location formatters are * created ultimately using this builder. The following example will create * a formatter for the latitude field: *
{@code
	 * final LocationFormatter formatter = LocationFormatter.builder()
	 *     .append(Location.Field.DEGREE_OF_LATITUDE, "00")
	 *     .appendLiteral("°")
	 *     .append(Location.Field.MINUTE_OF_LATITUDE, "00")
	 *     .appendLiteral("'")
	 *     .append(Location.Field.SECOND_OF_LATITUDE, "00.000")
	 *     .appendLiteral("\"")
	 *     .appendNorthSouthHemisphere()
	 *     .build();
	 * }
* * @implNote * This class is a mutable builder intended for use from a single thread. * * @author Franz Wilhelmstötter * @version 1.4 * @since 1.4 */ public static final class Builder { private final List> _formats = new ArrayList<>(); private Builder() { } /** * Appends all the elements of a formatter to the builder. This method * has the same effect as appending each of the constituent parts of the * formatter directly to this builder. * * @param formatter the formatter to add, not {@code null} * @param optional optional flag. If {@code true}, the created formatter * will allow missing location fields. * @return {@code this}, for chaining, not {@code null} * @throws NullPointerException if the given {@code formatter} is * {@code null} */ public Builder append( final LocationFormatter formatter, final boolean optional ) { _formats.add(optional ? OptionalFormat.of(formatter._formats) : CompositeFormat.of(formatter._formats) ); return this; } /** * Appends all the elements of a formatter to the builder. This method * has the same effect as appending each of the constituent parts of the * formatter directly to this builder. * * @param formatter the formatter to add, not {@code null} * @return {@code this}, for chaining, not {@code null} * @throws NullPointerException if the given {@code formatter} is * {@code null} */ public Builder append(final LocationFormatter formatter) { return append(formatter, false); } /** * Append a formatter for the given location field, which will be * formatted using the given number format objects. * * @param field the location field to format * @param format the number formatter used for formatting the defined * location field * @return {@code this}, for chaining, not {@code null} * @throws NullPointerException if one of the arguments is {@code null} */ public Builder append( final Location.Field field, final Supplier format ) { _formats.add(LocationFieldFormat.of(field, format)); return this; } /** * Append a formatter for the given location field, which will be * formatted using the given decimal format pattern, as used in the * {@link DecimalFormat} object * * @see DecimalFormat * * @param field the location field to format * @param pattern the decimal format pattern * @return {@code this}, for chaining, not {@code null} * @throws NullPointerException if one of the arguments is {@code null} */ public Builder append(final Location.Field field, final String pattern) { return append(field, () -> { DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US); return new DecimalFormat(pattern, symbols); }); } /** * Append a formatter for the sign of the latitude value ('+' or '-'). * * @return {@code this}, for chaining, not {@code null} */ public Builder appendLatitudeSign() { _formats.add(LatitudeSignFormat.INSTANCE); return this; } /** * Append a formatter for the sign of the longitude value ('+' or '-'). * * @return {@code this}, for chaining, not {@code null} */ public Builder appendLongitudeSign() { _formats.add(LongitudeSignFormat.INSTANCE); return this; } /** * Append a formatter for the sign of the elevation value ('+' or '-'). * * @return {@code this}, for chaining, not {@code null} */ public Builder appendElevationSign() { _formats.add(ElevationSignFormat.INSTANCE); return this; } /** * Append a formatter for the north-south hemisphere ('N' or 'S'). The * appended formatter will access the latitude value of the * location. * * @return {@code this}, for chaining, not {@code null} */ public Builder appendNorthSouthHemisphere() { _formats.add(NorthSouthFormat.INSTANCE); return this; } /** * Append a formatter for the east-west hemisphere ('E' or 'W'). The * appended formatter will access the longitude value of the * location. * * @return {@code this}, for chaining, not {@code null} */ public Builder appendEastWestHemisphere() { _formats.add(EastWestFormat.INSTANCE); return this; } /** * Appends a string literal to the formatter. This string will be output * during a format. If the literal is empty, nothing is added to the * formatter. * * @param literal the {@code literal} to append, not null * @return {@code this}, for chaining, not {@code null} * @throws NullPointerException if one of the {@code literal} is * {@code null} */ public Builder appendLiteral(final String literal) { if (!literal.isEmpty()) { _formats.add(ConstFormat.of(literal)); } return this; } /** * Appends the elements defined by the specified pattern to the builder. * * @param pattern the pattern to add * @return {@code this}, for chaining, not {@code null} * @throws NullPointerException if the given {@code pattern} is * {@code null} * @throws IllegalArgumentException if the given {@code pattern} is * invalid */ public Builder appendPattern(final String pattern) { parsePattern(pattern); return this; } /** * Completes this builder by creating the {@code LocationFormatter}. * * @return a new location-formatter object */ public LocationFormatter build() { return new LocationFormatter(new ArrayList<>(_formats)); } private void parsePattern(final String pattern) { requireNonNull(pattern); final List> formats = new ArrayList<>(); boolean optional = false; int signs = 0; boolean quote = false; List> fmt; for (Tokens tokens = new Tokens(tokenize(pattern)); tokens.hasNext();) { final String token = tokens.next(); switch (token) { case "X": fmt = optional ? formats : _formats; for (int i = 0; i < signs; ++i) { fmt.add(LatitudeSignFormat.INSTANCE); } signs = 0; fmt.add(NorthSouthFormat.INSTANCE); break; case "x": fmt = optional ? formats : _formats; for (int i = 0; i < signs; ++i) { fmt.add(LongitudeSignFormat.INSTANCE); } signs = 0; fmt.add(EastWestFormat.INSTANCE); break; case "+": ++signs; break; case "[": if (optional) { throw new IllegalArgumentException( "No nesting '[' (optional) allowed." ); } if (signs > 0) { throw new IllegalArgumentException( "No '[' after '+' allowed." ); } optional = true; break; case "]": if (!optional) { throw new IllegalArgumentException( "Missing open '[' bracket." ); } optional = false; _formats.add(OptionalFormat.of(formats)); formats.clear(); break; case "'": fmt = optional ? formats : _formats; if (tokens.after().filter("'"::equals).isPresent()) { fmt.add(ConstFormat.of("'")); tokens.next(); break; } if (quote) { if (tokens.before().isPresent()) { fmt.add(ConstFormat.of( tokens.before() .orElseThrow(AssertionError::new) )); } quote = false; } else { quote = true; } break; default: fmt = optional ? formats : _formats; if (!quote) { final Optional field = Field.ofPattern(token); if (field.isPresent()) { if (signs > 0) { if (field.get().isLatitude()) { for (int i = 0; i < signs; ++i) { fmt.add(LatitudeSignFormat.INSTANCE); } } else if (field.get().isLongitude()) { for (int i = 0; i < signs; ++i) { fmt.add(LongitudeSignFormat.INSTANCE); } } else if (field.get().isElevation()) { for (int i = 0; i < signs; ++i) { fmt.add(ElevationSignFormat.INSTANCE); } } } fmt.add(LocationFieldFormat.of( field.get(), () -> { DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US); return new DecimalFormat( field.get().toDecimalPattern(token), symbols); } )); } else { fmt.add(ConstFormat.of(token)); } } signs = 0; break; } } if (optional) { throw new IllegalArgumentException("No closing ']' found."); } if (quote) { throw new IllegalArgumentException( "Missing closing ' character." ); } _formats.addAll(formats); } static List tokenize(final String pattern) { final List tokens = new ArrayList<>(); final StringBuilder token = new StringBuilder(); boolean quote = false; char pc = '\0'; for (int i = 0; i < pattern.length(); ++i) { final char c = pattern.charAt(i); switch (c) { case '\'': quote = !quote; if (token.length() > 0) { tokens.add(token.toString()); token.setLength(0); } tokens.add(Character.toString(c)); break; case 'x': case 'X': case '+': case '[': case ']': if (quote) { token.append(c); } else { if (token.length() > 0) { tokens.add(token.toString()); token.setLength(0); } tokens.add(Character.toString(c)); } break; case 'L': case 'D': case 'M': case 'S': case 'l': case 'd': case 'm': case 's': case 'E': case 'H': if (c != pc && pc != '\0' && pc != '.' && pc != ',' && !quote) { if (token.length() > 0) { tokens.add(token.toString()); token.setLength(0); } } token.append(c); break; case ',': case '.': token.append(c); break; default: if (PROTECTED_CHARS.contains(pc) || pc == '\'') { if (token.length() > 0) { tokens.add(token.toString()); token.setLength(0); } } token.append(c); break; } pc = c; } if (token.length() > 0) { tokens.add(token.toString()); } return tokens; } } private static final class Tokens implements Iterator { private final List _tokens; private int _index = 0; private Tokens(final List tokens) { _tokens = requireNonNull(tokens); } @Override public boolean hasNext() { return _index < _tokens.size(); } @Override public String next() { return _tokens.get(_index++); } Optional before() { return _index - 1 > 0 ? Optional.of(_tokens.get(_index - 2)) : Optional.empty(); } Optional after() { return hasNext() ? Optional.of(_tokens.get(_index)) : Optional.empty(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy