/* 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.graph_builder.impl.shapefile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opentripplanner.common.geometry.GeometryUtils;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.graph_builder.services.GraphBuilder;
import org.opentripplanner.graph_builder.services.shapefile.FeatureSourceFactory;
import org.opentripplanner.graph_builder.services.shapefile.SimpleFeatureConverter;
import org.opentripplanner.routing.edgetype.PlainStreetEdge;
import org.opentripplanner.routing.edgetype.StreetTraversalPermission;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.patch.Alert;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
/**
* Loads a shapefile into an edge-based graph.
*
*/
public class ShapefileStreetGraphBuilderImpl implements GraphBuilder {
private static Logger log = LoggerFactory.getLogger(ShapefileStreetGraphBuilderImpl.class);
private FeatureSourceFactory _featureSourceFactory;
private ShapefileStreetSchema _schema;
public List<String> provides() {
return Arrays.asList("streets");
}
public List<String> getPrerequisites() {
return Collections.emptyList();
}
public void setFeatureSourceFactory(FeatureSourceFactory factory) {
_featureSourceFactory = factory;
}
public void setSchema(ShapefileStreetSchema schema) {
_schema = schema;
}
@Override
public void buildGraph(Graph graph, HashMap<Class<?>, Object> extra) {
try {
FeatureSource<SimpleFeatureType, SimpleFeature> featureSource = _featureSourceFactory
.getFeatureSource();
CoordinateReferenceSystem sourceCRS = featureSource.getInfo().getCRS();
Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
CRSAuthorityFactory factory = ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG",
hints);
CoordinateReferenceSystem worldCRS = factory
.createCoordinateReferenceSystem("EPSG:4326");
Query query = new Query();
query.setCoordinateSystem(sourceCRS);
query.setCoordinateSystemReproject(worldCRS);
FeatureCollection<SimpleFeatureType, SimpleFeature> features = featureSource
.getFeatures(query);
features = featureSource.getFeatures(query);
HashMap<String, HashMap<Coordinate, Integer>> intersectionNameToId = new HashMap<String, HashMap<Coordinate, Integer>>();
SimpleFeatureConverter<String> streetIdConverter = _schema.getIdConverter();
SimpleFeatureConverter<String> streetNameConverter = _schema.getNameConverter();
SimpleFeatureConverter<P2<StreetTraversalPermission>> permissionConverter = _schema
.getPermissionConverter();
SimpleFeatureConverter<String> noteConverter = _schema.getNoteConverter();
HashMap<Coordinate, IntersectionVertex> intersectionsByLocation =
new HashMap<Coordinate, IntersectionVertex>();
SimpleFeatureConverter<P2<Double>> safetyConverter = _schema.getBicycleSafetyConverter();
SimpleFeatureConverter<Boolean> slopeOverrideCoverter = _schema.getSlopeOverrideConverter();
SimpleFeatureConverter<Boolean> featureSelector = _schema.getFeatureSelector();
// Keep track of features that are duplicated so we don't have duplicate streets
Set<Object> seen = new HashSet<Object>();
List<SimpleFeature> featureList = new ArrayList<SimpleFeature>();
FeatureIterator<SimpleFeature> it2 = features.features();
while (it2.hasNext()) {
SimpleFeature feature = it2.next();
if (featureSelector != null && ! featureSelector.convert(feature)) {
continue;
}
featureList.add(feature);
}
it2.close();
HashMap<Coordinate, TreeSet<String>> coordinateToStreetNames = getCoordinatesToStreetNames(featureList);
for (SimpleFeature feature : featureList) {
if (feature.getDefaultGeometry() == null) {
log.warn("feature has no geometry: " + feature.getIdentifier());
continue;
}
LineString geom = toLineString((Geometry) feature.getDefaultGeometry());
Object o = streetIdConverter.convert(feature);
String label = "" + o;
if (o != null && seen.contains(label)) {
continue;
}
seen.add(label);
String name = streetNameConverter.convert(feature);
Coordinate[] coordinates = geom.getCoordinates();
if (coordinates.length < 2) {
//not a real linestring
log.warn("Bad geometry for street with label " + label + " name " + name);
continue;
}
// this rounding is a total hack, to work around
// http://jira.codehaus.org/browse/GEOT-2811
Coordinate startCoordinate = new Coordinate(
Math.round(coordinates[0].x * 1048576) / 1048576.0, Math
.round(coordinates[0].y * 1048576) / 1048576.0);
Coordinate endCoordinate = new Coordinate(Math
.round(coordinates[coordinates.length - 1].x * 1048576) / 1048576.0, Math
.round(coordinates[coordinates.length - 1].y * 1048576) / 1048576.0);
String startIntersectionName = getIntersectionName(coordinateToStreetNames,
intersectionNameToId, startCoordinate);
if (startIntersectionName == "null") {
log.warn("No intersection name for " + name);
}
String endIntersectionName = getIntersectionName(coordinateToStreetNames,
intersectionNameToId, endCoordinate);
IntersectionVertex startIntersection = intersectionsByLocation.get(startCoordinate);
if (startIntersection == null) {
startIntersection = new IntersectionVertex(graph, startIntersectionName, startCoordinate.x,
startCoordinate.y, startIntersectionName);
intersectionsByLocation.put(startCoordinate, startIntersection);
}
IntersectionVertex endIntersection = intersectionsByLocation.get(endCoordinate);
if (endIntersection == null) {
endIntersection = new IntersectionVertex(graph, endIntersectionName, endCoordinate.x,
endCoordinate.y, endIntersectionName);
intersectionsByLocation.put(endCoordinate, endIntersection);
}
double length = 0;
for (int i = 0; i < coordinates.length - 1; ++i) {
length += JTS.orthodromicDistance(coordinates[i],
coordinates[i + 1], worldCRS);
}
P2<StreetTraversalPermission> permissions = permissionConverter.convert(feature);
PlainStreetEdge street = new PlainStreetEdge(startIntersection, endIntersection,
geom, name, length, permissions.getFirst(), false);
LineString reversed = (LineString) geom.reverse();
PlainStreetEdge backStreet = new PlainStreetEdge(endIntersection,
startIntersection, reversed, name, length, permissions.getSecond(), true);
if (noteConverter != null) {
String note = noteConverter.convert(feature);
if (note != null && note.length() > 0) {
HashSet<Alert> notes = Alert.newSimpleAlertSet(note);
street.setNote(notes);
backStreet.setNote(notes);
}
}
boolean slopeOverride = slopeOverrideCoverter.convert(feature);
street.setSlopeOverride(slopeOverride);
backStreet.setSlopeOverride(slopeOverride);
P2<Double> effectiveLength;
if (safetyConverter != null) {
effectiveLength = safetyConverter.convert(feature);
if (effectiveLength != null) {
street.setBicycleSafetyEffectiveLength(effectiveLength.getFirst() * length);
backStreet.setBicycleSafetyEffectiveLength(effectiveLength.getSecond() * length);
}
}
}
it2.close();
} catch (Exception ex) {
throw new IllegalStateException("error loading shapefile street data", ex);
}
}
private HashMap<Coordinate, TreeSet<String>> getCoordinatesToStreetNames(
List<SimpleFeature> features) {
HashMap<Coordinate, TreeSet<String>> coordinateToStreets = new HashMap<Coordinate, TreeSet<String>>();
SimpleFeatureConverter<String> streetNameConverter = _schema.getNameConverter();
SimpleFeatureConverter<Boolean> featureSelector = _schema.getFeatureSelector();
Iterator<SimpleFeature> it = features.iterator();
while (it.hasNext()) {
SimpleFeature feature = it.next();
if (featureSelector != null && !featureSelector.convert(feature)) {
continue;
}
if (feature.getDefaultGeometry() == null) {
log.warn("feature has no geometry: " + feature.getIdentifier());
continue;
}
LineString geom = toLineString((Geometry) feature.getDefaultGeometry());
for (Coordinate coord : geom.getCoordinates()) {
// this rounding is a total hack, to work around
// http://jira.codehaus.org/browse/GEOT-2811
Coordinate rounded = new Coordinate(Math.round(coord.x * 1048576) / 1048576.0, Math
.round(coord.y * 1048576) / 1048576.0);
TreeSet<String> streets = coordinateToStreets.get(rounded);
if (streets == null) {
streets = new TreeSet<String>();
coordinateToStreets.put(rounded, streets);
}
String streetName = streetNameConverter.convert(feature);
if (streetName == null) {
throw new IllegalStateException("Unexpectedly got null for a street name for feature at " + coord);
}
streets.add(streetName);
}
}
return coordinateToStreets;
}
private String getIntersectionName(HashMap<Coordinate, TreeSet<String>> coordinateToStreets,
HashMap<String, HashMap<Coordinate, Integer>> intersectionNameToId,
Coordinate coordinate) {
TreeSet<String> streets = coordinateToStreets.get(coordinate);
if (streets == null) {
return "null";
}
String intersection = streets.first() + " at " + streets.last();
HashMap<Coordinate, Integer> possibleIntersections = intersectionNameToId.get(intersection);
if (possibleIntersections == null) {
possibleIntersections = new HashMap<Coordinate, Integer>();
possibleIntersections.put(coordinate, 1);
intersectionNameToId.put(intersection, possibleIntersections);
return intersection;
}
Integer index = possibleIntersections.get(coordinate);
if (index == null) {
int max = 0;
for (Integer value : possibleIntersections.values()) {
if (value > max)
max = value;
}
possibleIntersections.put(coordinate, max + 1);
index = max + 1;
}
if (index > 1) {
intersection += " #" + possibleIntersections.get(coordinate);
}
return intersection;
}
private LineString toLineString(Geometry g) {
if (g instanceof LineString) {
return (LineString) g;
} else if (g instanceof MultiLineString) {
MultiLineString ml = (MultiLineString) g;
Coordinate[] coords = ml.getCoordinates();
return GeometryUtils.getGeometryFactory().createLineString(coords);
} else {
throw new RuntimeException("found a geometry feature that's not a linestring: " + g);
}
}
@Override
public void checkInputs() {
_featureSourceFactory.checkInputs();
}
}