/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.common.model; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.vividsolutions.jts.geom.Coordinate; import lombok.Data; /** * Class describing a location provided by clients of routing. Used to describe end points (origin, destination) of a routing request as well as any * intermediate points that should be passed through. * * Handles parsing of geospatial information from strings so that it need not be littered through the routing code. * * @author avi */ @Data public class GenericLocation implements Cloneable { /** * The name of the place, if provided. */ private final String name; /** * The identifier of the place, if provided. May be a lat,lng string or a vertex ID. */ private final String place; /** * The ID of the edge this location is on if any. */ private Integer edgeId; /** * Coordinates of the place, if provided. */ private Double lat; private Double lng; /** * Observed heading if any. * * Direction of travel in decimal degrees from -180° to +180° relative to * true north. * * 0 = heading true north. * +/-180 = heading south. */ private Double heading; // Pattern for matching lat,lng strings, i.e. an optional '-' character followed by // one or more digits, and an optional (decimal point followed by one or more digits). private static final String _doublePattern = "-{0,1}\\d+(\\.\\d+){0,1}"; // We want to ignore any number of non-digit characters at the beginning of the string, except // that signs are also non-digits. So ignore any number of non-(digit or sign or decimal point). private static final Pattern _latLonPattern = Pattern.compile("[^[\\d&&[-|+|.]]]*(" + _doublePattern + ")(\\s*,\\s*|\\s+)(" + _doublePattern + ")\\D*"); private static final Pattern _headingPattern = Pattern.compile("\\D*heading=(" + _doublePattern + ")\\D*"); private static final Pattern _edgeIdPattern = Pattern.compile("\\D*edgeId=(\\d+)\\D*"); /** * Constructs an empty GenericLocation. */ public GenericLocation() { this.name = ""; this.place = ""; } /** * Constructs a GenericLocation with coordinates only. */ public GenericLocation(double lat, double lng) { this.name = ""; this.place = ""; this.lat = lat; this.lng = lng; } /** * Constructs a GenericLocation with coordinates only. */ public GenericLocation(Coordinate coord) { this(coord.y, coord.x); } /** * Constructs a GenericLocation with coordinates and heading. */ public GenericLocation(double lat, double lng, double heading) { this.name = ""; this.place = ""; this.lat = lat; this.lng = lng; this.heading = heading; } /** * Construct from a name, place pair. * Parses latitude, longitude data, heading and numeric edge ID out of the place string. * Note that if the place string does not appear to contain a lat/lon pair, heading, or edge ID * the GenericLocation will be missing that information but will still retain the place string, * which will be interpreted during routing context construction as a vertex label within the * graph for the appropriate routerId (by StreetVertexIndexServiceImpl.getVertexForLocation()). * TODO: Perhaps the interpretation as a vertex label should be done here for clarity. */ public GenericLocation(String name, String place) { this.name = name; this.place = place; if (place == null) { return; } Matcher matcher = _latLonPattern.matcher(place); if (matcher.find()) { this.lat = Double.parseDouble(matcher.group(1)); this.lng = Double.parseDouble(matcher.group(4)); } matcher = _headingPattern.matcher(place); if (matcher.find()) { this.heading = Double.parseDouble(matcher.group(1)); } matcher = _edgeIdPattern.matcher(place); if (matcher.find()) { this.edgeId = Integer.parseInt(matcher.group(1)); } } /** * Same as above, but draws name and place string from a NamedPlace object. * * @param np */ public GenericLocation(NamedPlace np) { this(np.name, np.place); } /** * Creates the GenericLocation by parsing a "name::place" string, where "place" is a latitude,longitude string or a vertex ID. * * @param input * @return */ public static GenericLocation fromOldStyleString(String input) { String name = ""; String place = input; if (input.contains("::")) { String[] parts = input.split("::"); name = parts[0]; place = parts[1]; } return new GenericLocation(name, place); } /** * Returns true if this.heading is not null. * @return */ public boolean hasHeading() { return heading != null; } /** Returns true if this.name is set. */ public boolean hasName() { return name != null && !name.isEmpty(); } /** Returns true if this.place is set. */ public boolean hasPlace() { return place != null && !place.isEmpty(); } /** * Returns true if getCoordinate() will not return null. * @return */ public boolean hasCoordinate() { return this.lat != null && this.lng != null; } /** * Returns true if getEdgeId would not return null. * @return */ public boolean hasEdgeId() { return this.edgeId != null; } public NamedPlace getNamedPlace() { return new NamedPlace(this.name, this.place); } /** * Returns this as a Coordinate object. * @return */ public Coordinate getCoordinate() { if (this.lat == null || this.lng == null) { return null; } return new Coordinate(this.lng, this.lat); } /** * Represents the location as an old-style string for clients that relied on that behavior. * * TODO(flamholz): clients should stop relying on these being strings and then we can return a string here that fully represents the contents of * the object. */ @Override public String toString() { if (this.place != null && !this.place.isEmpty()) { if (this.name == null || this.name.isEmpty()) { return this.place; } else { return String.format("%s::%s", this.name, this.place); } } return String.format("%s,%s", this.lat, this.lng); } /** * Returns a descriptive string that has the information that I wish toString() returned. */ public String toDescriptiveString() { StringBuilder sb = new StringBuilder(); sb.append("<GenericLocation lat,lng=").append(this.lat).append(",").append(this.lng); if (this.hasHeading()) { sb.append(" heading=").append(this.heading); } if (this.hasEdgeId()) { sb.append(" edgeId=").append(this.edgeId); } sb.append(">"); return sb.toString(); } @Override public GenericLocation clone() { try { return (GenericLocation) super.clone(); } catch (CloneNotSupportedException e) { /* this will never happen since our super is the cloneable object */ throw new RuntimeException(e); } } }