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

org.opentripplanner.graph_builder.module.shapefile.ShapefileStreetModule Maven / Gradle / Ivy

package org.opentripplanner.graph_builder.module.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.DefaultStreetEdgeFactory;
import org.opentripplanner.graph_builder.services.GraphBuilderModule;
import org.opentripplanner.graph_builder.services.StreetEdgeFactory;
import org.opentripplanner.graph_builder.services.shapefile.FeatureSourceFactory;
import org.opentripplanner.graph_builder.services.shapefile.SimpleFeatureConverter;
import org.opentripplanner.routing.alertpatch.Alert;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.StreetTraversalPermission;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.services.notes.StreetNotesService;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.opentripplanner.util.NonLocalizedString;

/**
 * Loads a shapefile into an edge-based graph.
 *
 */
public class ShapefileStreetModule implements GraphBuilderModule {
    private static Logger log = LoggerFactory.getLogger(ShapefileStreetModule.class);

    private FeatureSourceFactory featureSourceFactory;

    private ShapefileStreetSchema schema;

    public StreetEdgeFactory edgeFactory = new DefaultStreetEdgeFactory();

    public List provides() {
        return Arrays.asList("streets");
    }

    public List getPrerequisites() {
        return Collections.emptyList();
    }
    
    public void setFeatureSourceFactory(FeatureSourceFactory factory) {
        featureSourceFactory = factory;
    }

    public void setSchema(ShapefileStreetSchema schema) {
        this.schema = schema;
    }

    @Override
    public void buildGraph(Graph graph, HashMap, Object> extra) {

        try {

            FeatureSource 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 features = featureSource
                    .getFeatures(query);

            features = featureSource.getFeatures(query);

            HashMap> intersectionNameToId = new HashMap>();

            SimpleFeatureConverter streetIdConverter = schema.getIdConverter();
            SimpleFeatureConverter streetNameConverter = schema.getNameConverter();
            SimpleFeatureConverter> permissionConverter = schema
                    .getPermissionConverter();
            SimpleFeatureConverter noteConverter = schema.getNoteConverter();

            HashMap intersectionsByLocation = 
                    new HashMap();

            SimpleFeatureConverter> safetyConverter = schema.getBicycleSafetyConverter();

            SimpleFeatureConverter slopeOverrideCoverter = schema.getSlopeOverrideConverter();

            SimpleFeatureConverter featureSelector = schema.getFeatureSelector();
            
            // Keep track of features that are duplicated so we don't have duplicate streets
            Set seen = new HashSet();

            List featureList = new ArrayList();
            FeatureIterator it2 = features.features();
            while (it2.hasNext()) {
                SimpleFeature feature = it2.next();
                if (featureSelector != null && ! featureSelector.convert(feature)) {
                    continue;
                }
                featureList.add(feature);
            }
            it2.close();
            it2 = null;

            HashMap> 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, new NonLocalizedString(startIntersectionName));
                    intersectionsByLocation.put(startCoordinate, startIntersection);
                }

                IntersectionVertex endIntersection = intersectionsByLocation.get(endCoordinate);
                if (endIntersection == null) {
                    endIntersection = new IntersectionVertex(graph, endIntersectionName, endCoordinate.x,
                            endCoordinate.y, new NonLocalizedString(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 permissions = permissionConverter.convert(feature);

                // TODO Set appropriate car speed from shapefile source.
                StreetEdge street = edgeFactory.createEdge(startIntersection, endIntersection,
                        geom, new NonLocalizedString(name), length, permissions.first, false);
                LineString reversed = (LineString) geom.reverse();
                StreetEdge backStreet = edgeFactory.createEdge(endIntersection, startIntersection,
                        reversed, new NonLocalizedString(name), length, permissions.second, true);
                backStreet.shareData(street);

                if (noteConverter != null) {
                	String note = noteConverter.convert(feature);
                	if (note != null && note.length() > 0) {
				Alert noteAlert = Alert.createSimpleAlerts(note);
				graph.streetNotesService.addStaticNote(street, noteAlert, StreetNotesService.ALWAYS_MATCHER);
				graph.streetNotesService.addStaticNote(backStreet, noteAlert, StreetNotesService.ALWAYS_MATCHER);
                	}
                }

                boolean slopeOverride = slopeOverrideCoverter.convert(feature);
                street.setSlopeOverride(slopeOverride);
                backStreet.setSlopeOverride(slopeOverride);

                if (safetyConverter != null) {
                    P2 safetyFactors = safetyConverter.convert(feature);
                    if (safetyFactors != null) {
                        street.setBicycleSafetyFactor(safetyFactors.first.floatValue());
                        backStreet.setBicycleSafetyFactor(safetyFactors.second.floatValue());
                    }
                }
            }
        } catch (Exception ex) {
            throw new IllegalStateException("error loading shapefile street data", ex);
        } finally {
            featureSourceFactory.cleanup();
        }       
    }

    private HashMap> getCoordinatesToStreetNames(
            List features) {
        HashMap> coordinateToStreets = new HashMap>();
        SimpleFeatureConverter streetNameConverter = schema.getNameConverter();

        SimpleFeatureConverter featureSelector = schema.getFeatureSelector();
        Iterator 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 streets = coordinateToStreets.get(rounded);
                if (streets == null) {
                    streets = new TreeSet();
                    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> coordinateToStreets,
            HashMap> intersectionNameToId,
            Coordinate coordinate) {

        TreeSet streets = coordinateToStreets.get(coordinate);
        if (streets == null) {
            return "null";
        }

        //TODO: localize this and return localized string
        String intersection = streets.first() + " at " + streets.last();

        HashMap possibleIntersections = intersectionNameToId.get(intersection);
        if (possibleIntersections == null) {
            possibleIntersections = new HashMap();
            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();
    }
}