it.unibo.alchemist.model.implementations.environments.OSMEnvironment.java Source code

Java tutorial

Introduction

Here is the source code for it.unibo.alchemist.model.implementations.environments.OSMEnvironment.java

Source

/*
 * Copyright (C) 2010-2015, Danilo Pianini and contributors
 * listed in the project's pom.xml file.
 *
 * This file is part of Alchemist, and is distributed under the terms of
 * the GNU General Public License, with a linking exception, as described
 * in the file LICENSE in the Alchemist distribution's top directory.
 */
package it.unibo.alchemist.model.implementations.environments;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.RandomAccessFile;
import java.net.URI;
import java.net.URL;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.apache.commons.codec.binary.Hex;
import org.danilopianini.util.Hashes;
import org.danilopianini.util.concurrent.FastReadWriteLock;
import org.jooq.lambda.Unchecked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopper;
import com.graphhopper.GraphHopperAPI;
import com.graphhopper.reader.osm.GraphHopperOSM;
import com.graphhopper.routing.util.EdgeFilter;
import com.graphhopper.routing.util.EncodingManager;
import com.graphhopper.storage.index.QueryResult;
import com.graphhopper.util.shapes.GHPoint;

import it.unibo.alchemist.model.implementations.GraphHopperRoute;
import it.unibo.alchemist.model.implementations.positions.LatLongPosition;
import it.unibo.alchemist.model.interfaces.GeoPosition;
import it.unibo.alchemist.model.interfaces.MapEnvironment;
import it.unibo.alchemist.model.interfaces.Node;
import it.unibo.alchemist.model.interfaces.Position;
import it.unibo.alchemist.model.interfaces.Route;
import it.unibo.alchemist.model.interfaces.Vehicle;

/**
 * This class serves as template for more specific implementations of
 * environments using a map. It encloses the navigation logic, but leaves the
 * subclasses to decide how to provide map data (e.g. loading from disk or rely
 * on online services). The data is then stored in-memory for performance
 * reasons.
 *
 * @param <T>
 */
public class OSMEnvironment<T> extends Continuous2DEnvironment<T> implements MapEnvironment<T> {

    /**
     * Default maximum communication range (in meters).
     */
    public static final double DEFAULT_MAX_RANGE = 100;

    /**
     * The default routing algorithm.
     */
    public static final String DEFAULT_ALGORITHM = "dijkstrabi";

    /**
     * The default routing strategy.
     */
    public static final String ROUTING_STRATEGY = "fastest";

    /**
     * The default value for approximating the positions comparison. 
     */
    public static final int DEFAULT_APPROXIMATION = 0;

    /**
     * The default value for the force nodes on streets option.
     */
    public static final boolean DEFAULT_ON_STREETS = true;

    /**
     * The default value for the discard of nodes too far from streets option.
     */
    public static final boolean DEFAULT_FORCE_STREETS = false;
    private static final Logger L = LoggerFactory.getLogger(OSMEnvironment.class);
    private static final long serialVersionUID = 1L;
    /**
     * System file separator.
     */
    private static final String SLASH = System.getProperty("file.separator");
    /**
     * Alchemist's temp dir.
     */
    private static final String PERSISTENTPATH = System.getProperty("user.home") + SLASH + ".alchemist";
    private static final String MAPNAME = "map";

    private final String mapResource;
    private final boolean forceStreets, onlyStreet;
    private transient FastReadWriteLock mapLock;

    private transient Map<Vehicle, GraphHopperAPI> navigators;

    private transient LoadingCache<CacheEntry, Route<GeoPosition>> routecache;
    private boolean benchmarking;
    private final int approximation;

    /**
     * Builds a new {@link OSMEnvironment}, with nodes forced on streets.
     *
     * @param file
     *            the file path where the map data is stored
     * @throws IOException
     *             if the map file is not found, or it's not readable, or
     *             accessible, or a file system error occurred, or you kicked your
     *             hard drive while Alchemist was reading the map
     */
    public OSMEnvironment(final String file) throws IOException {
        this(file, DEFAULT_ON_STREETS);
    }

    /**
     * @param file
     *            the file path where the map data is stored
     * @param onStreets
     *            if true, the nodes will be placed on the street nearest to the
     *            desired {@link Position}. 
     * @throws IOException
     *             if the map file is not found, or it's not readable, or
     *             accessible, or a file system error occurred, or you kicked your
     *             hard drive while Alchemist was reading the map
     */
    public OSMEnvironment(final String file, final boolean onStreets) throws IOException {
        this(file, onStreets, DEFAULT_FORCE_STREETS);

    }

    /**
     * @param file
     *            the file path where the map data is stored
     * @param onStreets
     *            if true, the nodes will be placed on the street nearest to the
     *            desired {@link Position}. 
     * @param onlyOnStreets
     *            if true, the nodes which are too far from a street will be simply
     *            discarded. If false, they will be placed anyway, in the original
     *            position.
     * @throws IOException
     *             if the map file is not found, or it's not readable, or
     *             accessible, or a file system error occurred, or you kicked your
     *             hard drive while Alchemist was reading the map
     */
    public OSMEnvironment(final String file, final boolean onStreets, final boolean onlyOnStreets)
            throws IOException {
        this(file, DEFAULT_APPROXIMATION, onStreets, onlyOnStreets);

    }

    /**
     * @param file
     *            the file path where the map data is stored. Accepts OSM maps of
     *            any format (xml, osm, pbf). The map will be processed, optimized
     *            and stored for future use.
     * @param approximation
     *            the amount of ciphers of the IEEE 754 encoded
     *            position that may be discarded when comparing two positions,
     *            allowing a quicker retrieval of the route between two position,
     *            since the cache may already contain a similar route  which
     *            can be considered to be the same route, according to
     *            the level of precision determined by this value
     * @throws IOException
     *             if the map file is not found, or it's not readable, or
     *             accessible, or a file system error occurred, or you kicked
     *             your hard drive while Alchemist was reading the map
     */
    public OSMEnvironment(final String file, final int approximation) throws IOException {
        this(file, approximation, DEFAULT_ON_STREETS, DEFAULT_FORCE_STREETS);
    }

    /**
     * @param file
     *            the file path where the map data is stored. Accepts OSM maps of
     *            any format (xml, osm, pbf). The map will be processed, optimized
     *            and stored for future use.
     * @param approximation
     *            the amount of ciphers of the IEEE 754 encoded
     *            position that may be discarded when comparing two positions,
     *            allowing a quicker retrieval of the route between two position,
     *            since the cache may already contain a similar route  which
     *            can be considered to be the same route, according to
     *            the level of precision determined by this value
     * @param onStreets
     *            if true, the nodes will be placed on the street nearest to the
     *            desired {@link Position}.
     * @param onlyOnStreets
     *            if true, the nodes which are too far from a street will be simply
     *            discarded. If false, they will be placed anyway, in the original
     *            position.
     * @throws IOException
     *             if the map file is not found, or it's not readable, or
     *             accessible, or a file system error occurred, or you kicked your
     *             hard drive while Alchemist was reading the map
     */
    public OSMEnvironment(final String file, final int approximation, final boolean onStreets,
            final boolean onlyOnStreets) throws IOException {
        super();
        if (approximation < 0 || approximation > 64) {
            throw new IllegalArgumentException();
        }
        forceStreets = onStreets;
        onlyStreet = onlyOnStreets;
        mapResource = file;
        this.approximation = approximation;
        initAll(file);
    }

    private boolean canWriteOnDir(final String dir) {
        return new File(dir).canWrite();
    }

    @Override
    protected Position computeActualInsertionPosition(final Node<T> node, final Position position) {

        /*
         * If it must be located on streets, query the navigation engine for a street
         * point. Otherwise, put it where it is declared.
         */
        assert position != null;
        return forceStreets ? getNearestStreetPoint(position).orElse(position) : position;

    }

    @Override
    public void enableBenchmark() {
        this.benchmarking = true;
    }

    @Override
    public double getBenchmarkResult() {
        if (benchmarking) {
            if (routecache != null) {
                return routecache.stats().hitRate();
            }
            return 0;
        } else {
            throw new IllegalStateException("You should call doBenchmark() before.");
        }
    }

    @Override
    public Route<GeoPosition> computeRoute(final Node<T> node, final Node<T> node2) {
        return computeRoute(node, getPosition(node2));
    }

    @Override
    public Route<GeoPosition> computeRoute(final Node<T> node, final Position coord) {
        return computeRoute(node, coord, DEFAULT_VEHICLE);
    }

    @Override
    public Route<GeoPosition> computeRoute(final Node<T> node, final Position coord, final Vehicle vehicle) {
        return computeRoute(getPosition(node), coord, vehicle);
    }

    @Override
    public Route<GeoPosition> computeRoute(final Position p1, final Position p2) {
        return computeRoute(p1, p2, DEFAULT_VEHICLE);
    }

    @Override
    public Route<GeoPosition> computeRoute(final Position p1, final Position p2, final Vehicle vehicle) {
        if (routecache == null) {
            CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
            if (benchmarking) {
                builder = builder.recordStats();
            }
            routecache = builder.expireAfterAccess(10, TimeUnit.SECONDS)
                    .build(new CacheLoader<CacheEntry, Route<GeoPosition>>() {
                        @Override
                        public Route<GeoPosition> load(final CacheEntry key) {
                            final Vehicle vehicle = key.v;
                            final Position p1 = key.start;
                            final Position p2 = key.end;
                            final GHRequest req = new GHRequest(p1.getCoordinate(1), p1.getCoordinate(0),
                                    p2.getCoordinate(1), p2.getCoordinate(0)).setAlgorithm(DEFAULT_ALGORITHM)
                                            .setVehicle(vehicle.toString()).setWeighting(ROUTING_STRATEGY);
                            mapLock.read();
                            final GraphHopperAPI gh = navigators.get(vehicle);
                            mapLock.release();
                            if (gh != null) {
                                final GHResponse resp = gh.route(req);
                                return new GraphHopperRoute(resp);
                            }
                            throw new IllegalStateException("Something went wrong while evaluating a route.");
                        }
                    });
        }
        try {
            return routecache.get(new CacheEntry(vehicle, p1, p2));
        } catch (ExecutionException e) {
            L.error("", e);
            throw new IllegalStateException("The navigator was unable to compute a route from " + p1 + " to " + p2
                    + " using the navigator " + vehicle + ". This is most likely a bug", e);
        }
    }

    private class CacheEntry {

        private final Vehicle v;
        private final Position start;
        private final Position end;
        private final Position apprStart;
        private final Position apprEnd;
        private int hash;

        CacheEntry(final Vehicle v, final Position p1, final Position p2) {
            this.v = Objects.requireNonNull(v);
            this.start = Objects.requireNonNull(p1);
            this.end = Objects.requireNonNull(p2);
            this.apprStart = approximate(start);
            this.apprEnd = approximate(end);
        }

        @Override
        public int hashCode() {
            if (hash == 0) {
                hash = Hashes.hash32(v, apprStart, apprEnd);
            }
            return hash;
        }

        @Override
        public boolean equals(final Object obj) {
            if (obj instanceof OSMEnvironment.CacheEntry) {
                final OSMEnvironment<?>.CacheEntry other = (OSMEnvironment<?>.CacheEntry) obj;
                return v.equals(other.v) && apprStart.equals(other.apprStart) && apprEnd.equals(other.apprEnd);
            }
            return false;
        }

        private Position approximate(final Position p) {
            if (approximation == 0) {
                return p;
            }
            return makePosition(approximate(p.getCoordinate(0)), approximate(p.getCoordinate(1)));
        }

        private double approximate(final double value) {
            return Double.longBitsToDouble(Double.doubleToLongBits(value) & (0xFFFFFFFFFFFFFFFFL << approximation));
        }
    }

    /**
     * @return the maximum latitude
     */
    protected double getMaxLatitude() {
        return super.getOffset()[1] + super.getSize()[1];
    }

    /**
     * @return the maximum longitude
     */
    protected double getMaxLongitude() {
        return super.getOffset()[0] + super.getSize()[0];
    }

    /**
     * @return the minimum latitude
     */
    protected double getMinLatitude() {
        return super.getOffset()[1];
    }

    /**
     * @return the minimum longitude
     */
    protected double getMinLongitude() {
        return super.getOffset()[0];
    }

    private Optional<Position> getNearestStreetPoint(final Position position) {
        assert position != null;
        mapLock.read();
        final GraphHopperAPI gh = navigators.get(Vehicle.BIKE);
        mapLock.release();
        final QueryResult qr = ((GraphHopper) gh).getLocationIndex().findClosest(position.getCoordinate(1),
                position.getCoordinate(0), EdgeFilter.ALL_EDGES);
        if (qr.isValid()) {
            final GHPoint pt = qr.getSnappedPoint();
            return Optional.of(new LatLongPosition(pt.lat, pt.lon));
        }
        return Optional.empty();
    }

    @Override
    public GeoPosition getPosition(final Node<T> node) {
        if (super.getPosition(node) instanceof GeoPosition) {
            return (GeoPosition) super.getPosition(node);
        }
        throw new IllegalStateException("the position isn't a GeoPosition");
    }

    @Override
    public double[] getSizeInDistanceUnits() {
        final double minlat = getMinLatitude();
        final double maxlat = getMaxLatitude();
        final double minlon = getMinLongitude();
        final double maxlon = getMaxLongitude();
        final Position minmin = new LatLongPosition(minlat, minlon);
        final Position minmax = new LatLongPosition(minlat, maxlon);
        final Position maxmin = new LatLongPosition(maxlat, minlon);
        final Position maxmax = new LatLongPosition(maxlat, maxlon);
        /*
         * Maximum x: maximum distance between the same longitudes Maximum y: maximum
         * distance between the same latitudes
         */
        final double sizex = Math.max(minmin.getDistanceTo(minmax), maxmax.getDistanceTo(maxmin));
        final double sizey = Math.max(minmin.getDistanceTo(maxmin), maxmax.getDistanceTo(minmax));
        return new double[] { sizex, sizey };
    }

    private void initAll(final String fileName) throws IOException {
        Objects.requireNonNull(fileName, "define the file with the map: " + fileName);
        final Optional<URL> file = Optional.of(new File(fileName)).filter(File::exists).map(File::toURI)
                .map(Unchecked.function(URI::toURL));
        final URL resource = Optional.ofNullable(OSMEnvironment.class.getResource(fileName))
                .orElseGet(Unchecked.supplier(() -> file.orElseThrow(
                        () -> new FileNotFoundException("No file or resource with name " + fileName))));
        final String dir = initDir(resource).intern();
        final File workdir = new File(dir);
        mkdirsIfNeeded(workdir);
        final File mapFile = new File(dir + SLASH + MAPNAME);

        try (RandomAccessFile fileAccess = new RandomAccessFile(workdir + SLASH + "lock", "rw")) {
            try (FileLock lock = fileAccess.getChannel().lock()) {
                if (!mapFile.exists()) {
                    Files.copy(resource.openStream(), mapFile.toPath());
                }
            }
        }
        navigators = new EnumMap<>(Vehicle.class);
        mapLock = new FastReadWriteLock();
        final Optional<Exception> error = Arrays.stream(Vehicle.values()).parallel().<Optional<Exception>>map(v -> {
            try {
                final String internalWorkdir = workdir + SLASH + v;
                final File iwdf = new File(internalWorkdir);
                if (mkdirsIfNeeded(iwdf)) {
                    final GraphHopperAPI gh = initNavigationSystem(mapFile, internalWorkdir, v);
                    mapLock.write();
                    navigators.put(v, gh);
                    mapLock.release();
                }
                return Optional.empty();
            } catch (Exception e) {
                return Optional.of(e);
            }
        }).filter(Optional::isPresent).map(Optional::get).findFirst();
        if (error.isPresent()) {
            throw new IllegalStateException("A error occurred during initialization.", error.get());
        }
    }

    private String initDir(final URL mapfile) throws IOException {
        final String code = Hex.encodeHexString(Hashes.hashResource(mapfile, e -> {
            throw new IllegalStateException(e);
        }).asBytes());
        final String append = SLASH + code;
        final String[] prefixes = new String[] { PERSISTENTPATH, System.getProperty("java.io.tmpdir"),
                System.getProperty("user.dir"), "." };
        String dir = prefixes[0] + append;
        for (int i = 1; (!mkdirsIfNeeded(dir) || !canWriteOnDir(dir)) && i < prefixes.length; i++) {
            L.warn("Can not write on " + dir + ", trying " + prefixes[i]);
            dir = prefixes[i] + append;
        }
        if (!canWriteOnDir(dir)) {
            /*
             * Give up.
             */
            throw new IOException("None of: " + Arrays.toString(prefixes)
                    + " is writeable. I can not initialize GraphHopper cache.");
        }
        return dir;
    }

    @Override
    public GeoPosition makePosition(final Number... coordinates) {
        if (coordinates.length != 2) {
            throw new IllegalArgumentException(
                    getClass().getSimpleName() + " only supports bi-dimensional coordinates (latitude, longitude)");
        }
        return new LatLongPosition(coordinates[0].doubleValue(), coordinates[1].doubleValue());
    }

    /**
     * There is a single case in which nodes are discarded: if there are no traces
     * for this node and nodes are required to lay on streets, but the navigation
     * engine can not resolve any such position.
     */
    @Override
    protected boolean nodeShouldBeAdded(final Node<T> node, final Position position) {
        assert node != null;
        return !onlyStreet || getNearestStreetPoint(position).isPresent();
    }

    private void readObject(final ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        initAll(mapResource);
    }

    private static GraphHopperAPI initNavigationSystem(final File mapFile, final String internalWorkdir,
            final Vehicle v) throws IOException {
        return new GraphHopperOSM().setOSMFile(mapFile.getAbsolutePath()).forDesktop().setElevation(false)
                .setEnableInstructions(false).setEnableCalcPoints(true).setInMemory()
                .setGraphHopperLocation(internalWorkdir)
                .setEncodingManager(new EncodingManager(v.toString().toLowerCase(Locale.US))).importOrLoad();
    }

    private static boolean mkdirsIfNeeded(final File target) {
        return target.exists() || target.mkdirs();
    }

    private static boolean mkdirsIfNeeded(final String target) {
        return mkdirsIfNeeded(new File(target));
    }
}