HereTrafficReader.java

/*  This file is part of Openrouteservice.
 *
 *  Openrouteservice 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 2.1
 *  of the License, or (at your option) any later version.

 *  This library 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 Lesser General Public License for more details.

 *  You should have received a copy of the GNU Lesser General Public License along with this library;
 *  if not, see <https://www.gnu.org/licenses/>.
 */
package org.heigit.ors.routing.graphhopper.extensions.reader.traffic;

import com.graphhopper.util.DistanceCalcEarth;
import org.apache.log4j.Logger;
import org.geotools.data.FileDataStore;
import org.geotools.data.FileDataStoreFinder;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.feature.DefaultFeatureCollection;
import org.heigit.ors.util.CSVUtility;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;

import java.io.File;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.util.*;

public class HereTrafficReader {
    private static final Logger LOGGER = Logger.getLogger(HereTrafficReader.class);

    private boolean isInitialized;
    private final String streetGeometriesFile;
    private final String patternsReferenceFile;
    private final String patternsFile;

    private final TrafficData hereTrafficData = new TrafficData();

    private static HereTrafficReader currentInstance;

    DistanceCalcEarth distCalc;

    /**
     * Empty constructor which does not read any data - the user must explicitly pass information
     */
    public HereTrafficReader() {
        this.streetGeometriesFile = "";
        this.patternsFile = "";
        this.patternsReferenceFile = "";
        this.distCalc = new DistanceCalcEarth();
        currentInstance = this;
        isInitialized = false;
    }

    /**
     * Constructor - the user must explicitly pass information
     */
    public HereTrafficReader(String streetGeometriesFile, String patterns15MinutesFile, String refPatternIdsFile) {
        this.streetGeometriesFile = streetGeometriesFile;
        this.patternsFile = patterns15MinutesFile;
        this.patternsReferenceFile = refPatternIdsFile;
        this.distCalc = new DistanceCalcEarth();
        currentInstance = this;
        isInitialized = false;
    }

    public void readData() throws IOException {
        if (streetGeometriesFile.equals("") || patternsFile.equals("") || patternsReferenceFile.equals(""))
            return;
        try {
            SimpleFeatureCollection rawGeometries = readHereGeometries();
            createHereGeometries(rawGeometries);
            LOGGER.info("Here link geometries pre-processed");

            HashMap<Integer, EnumMap<TrafficEnums.TravelDirection, Integer[]>> referencePatterns = readRefPatterns();
            LOGGER.info("Here reference patterns pre-processed");

            Map<Integer, TrafficPattern> patterns = readPatterns();
            LOGGER.info("Here patterns pre-processed");


            generatePatterns(referencePatterns, patterns);
            LOGGER.info("Here input data processed successfully");

            isInitialized = true;
        } catch (IOException ioe) {
            // Problem with reading the data
            LOGGER.error("Could not access file(s) required for Here traffic data");
            throw ioe;
        }
        currentInstance = this;
    }

    public boolean isInitialized() {
        return this.isInitialized;
    }

    private void generatePatterns(HashMap<Integer, EnumMap<TrafficEnums.TravelDirection, Integer[]>> referencePatterns, Map<Integer, TrafficPattern> patterns) {
        for (Map.Entry<Integer, EnumMap<TrafficEnums.TravelDirection, Integer[]>> linkIdEntry : referencePatterns.entrySet()) {
            Integer linkId = linkIdEntry.getKey();
            if (hereTrafficData.hasLink(linkId)) {
                TrafficLink link = hereTrafficData.getLink(linkId);
                EnumMap<TrafficEnums.TravelDirection, Integer[]> travelDirectionPatterns = referencePatterns.get(linkId);
                if (travelDirectionPatterns.containsKey(TrafficEnums.TravelDirection.TO)) {
                    Integer[] travelPatternReferences = travelDirectionPatterns.get(TrafficEnums.TravelDirection.TO);
                    for (int i = 0; i < travelPatternReferences.length; i++) {
                        Integer patternReference = travelPatternReferences[i];
                        if (patterns.containsKey(patternReference)) {
                            hereTrafficData.setPattern(patterns.get(patternReference));
                            link.setTrafficPatternId(TrafficEnums.TravelDirection.TO, TrafficEnums.WeekDay.values()[i], patternReference);
                        }
                    }
                } else if (travelDirectionPatterns.containsKey(TrafficEnums.TravelDirection.FROM)) {
                    Integer[] travelPatternReferences = travelDirectionPatterns.get(TrafficEnums.TravelDirection.FROM);
                    for (int i = 0; i < travelPatternReferences.length; i++) {
                        Integer patternReference = travelPatternReferences[i];
                        if (patterns.containsKey(patternReference)) {
                            hereTrafficData.setPattern(patterns.get(patternReference));
                            link.setTrafficPatternId(TrafficEnums.TravelDirection.FROM, TrafficEnums.WeekDay.values()[i], patternReference);
                        }
                    }
                }

                hereTrafficData.setLink(link);
            }
        }
    }

    private HashMap<Integer, EnumMap<TrafficEnums.TravelDirection, Integer[]>> readRefPatterns() {
        List<List<String>> rawPatternReferenceList = CSVUtility.readFile(patternsReferenceFile);
        HashMap<Integer, EnumMap<TrafficEnums.TravelDirection, Integer[]>> processedPatternReferenceList = new HashMap<>();
        for (List<String> rawPatternReference : rawPatternReferenceList) {
            EnumMap<TrafficEnums.TravelDirection, Integer[]> patternMap = new EnumMap<>(TrafficEnums.TravelDirection.class);
            Integer linkId = Integer.parseInt(rawPatternReference.get(0));
            TrafficEnums.TravelDirection travelDirection = TrafficEnums.TravelDirection.forValue(rawPatternReference.get(1));
            if (travelDirection == null || rawPatternReference.size() != 9) {
                // Skip this entry as its not a complete week pattern.
                continue;
            }
            Integer[] patternList = new Integer[rawPatternReference.size() - 2];
            for (int i = 2; i < rawPatternReference.size(); i++) {
                patternList[i - 2] = Integer.parseInt(rawPatternReference.get(i));
            }
            patternMap.put(travelDirection, patternList);
            processedPatternReferenceList.put(linkId, patternMap);
        }
        return processedPatternReferenceList;
    }

    private Map<Integer, TrafficPattern> readPatterns() {
        List<List<String>> patterns = CSVUtility.readFile(patternsFile);
        Map<Integer, TrafficPattern> hereTrafficPatterns = new HashMap<>();
        for (List<String> pattern : patterns) {
            int patternID = Integer.parseInt(pattern.get(0));
            short[] patternValues = new short[pattern.size() - 1];
            for (int i = 1; i < pattern.size(); i++) {
                patternValues[i - 1] = Short.parseShort(pattern.get(i));
            }
            TrafficPattern hereTrafficPattern = new TrafficPattern(patternID, TrafficEnums.PatternResolution.MINUTES_15, patternValues);
            hereTrafficPatterns.put(patternID, hereTrafficPattern);
        }
        return hereTrafficPatterns;
    }


    /**
     * Method to read the geometries from a GeoJSON file that represent the boundaries of different countries. Ideally
     * it should be written using many small objects split into hierarchies.
     * <p>
     * If the file is a .tar.gz format, it will decompress it and then store the reulting data to be read into the
     * JSON object.
     *
     * @return A (Geo)JSON object representing the contents of the file
     */
    private SimpleFeatureCollection readHereGeometries() throws IOException {
        SimpleFeatureCollection collection = new DefaultFeatureCollection();
        try {
            File file = new File(streetGeometriesFile);
            FileDataStore store = FileDataStoreFinder.getDataStore(file);
            SimpleFeatureSource featureSource = store.getFeatureSource();
            collection = featureSource.getFeatures();
        } catch (IOException e) {
            LOGGER.error("Error reading here shape file with error: " + e);
            throw e;
        } catch (Exception e) {
            LOGGER.error("Unknown error while reading here shape file with error: " + e);
            throw e;
        }
        return collection;
    }

    /**
     * Construct the Here Links from the raw featureCollection
     *
     * @param featureCollection Raw featureCollection.
     */
    private void createHereGeometries(SimpleFeatureCollection featureCollection) throws InvalidObjectException {
        if (featureCollection == null || featureCollection.isEmpty()) {
            return;
        }
        int linkCounter = 0;
        SimpleFeatureIterator iterator = featureCollection.features();
        GeometryFactory gf = new GeometryFactory();
        WKTReader reader = new WKTReader(gf);
        try {
            while (iterator.hasNext()) {
                SimpleFeature feature = iterator.next();
                int linkId = Integer.parseInt(String.valueOf(feature.getAttribute("LINK_ID")));
                MultiLineString defaultGeometry = (MultiLineString) feature.getDefaultGeometry();
                Collection<Property> properties = feature.getProperties();
                if (defaultGeometry.getNumGeometries() == 1) {
                    String geometryString = defaultGeometry.getGeometryN(0).toText();
                    try {
                        hereTrafficData.setLink(new TrafficLink(linkId, reader.read(geometryString), properties, distCalc));
                    } catch (ParseException e) {
                        LOGGER.info("Couldn't parse here geometry for Link_ID: " + linkId);
                    }
                } else {
                    LOGGER.debug("Geometry malformed. Skip parsing here geometry for Link_ID: " + linkId);
                }
                // process feature
                linkCounter += 1;
            }
        } finally {
            iterator.close();
        }
        LOGGER.info(linkCounter + " Here links found");
    }

    public TrafficData getHereTrafficData() {
        return hereTrafficData;
    }

}