Package org.locationtech.geogig.osm.cli.commands

Source Code of org.locationtech.geogig.osm.cli.commands.OSMHistoryImport$BBoxFiler

/* Copyright (c) 2013-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Victor Olaya (Boundless) - initial implementation
*/
package org.locationtech.geogig.osm.cli.commands;

import static org.locationtech.geogig.osm.internal.OSMUtils.NODE_TYPE_NAME;
import static org.locationtech.geogig.osm.internal.OSMUtils.WAY_TYPE_NAME;
import static org.locationtech.geogig.osm.internal.OSMUtils.nodeType;
import static org.locationtech.geogig.osm.internal.OSMUtils.wayType;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;
import javax.management.relation.Relation;

import jline.console.ConsoleReader;

import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.locationtech.geogig.api.DefaultProgressListener;
import org.locationtech.geogig.api.FeatureBuilder;
import org.locationtech.geogig.api.GeoGIG;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.ProgressListener;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevFeature;
import org.locationtech.geogig.api.RevFeatureType;
import org.locationtech.geogig.api.RevFeatureTypeImpl;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.SymRef;
import org.locationtech.geogig.api.plumbing.DiffCount;
import org.locationtech.geogig.api.plumbing.FindTreeChild;
import org.locationtech.geogig.api.plumbing.RefParse;
import org.locationtech.geogig.api.plumbing.ResolveGeogigDir;
import org.locationtech.geogig.api.plumbing.ResolveTreeish;
import org.locationtech.geogig.api.plumbing.RevObjectParse;
import org.locationtech.geogig.api.plumbing.diff.DiffObjectCount;
import org.locationtech.geogig.api.porcelain.AddOp;
import org.locationtech.geogig.api.porcelain.CommitOp;
import org.locationtech.geogig.cli.AbstractCommand;
import org.locationtech.geogig.cli.CLICommand;
import org.locationtech.geogig.cli.CommandFailedException;
import org.locationtech.geogig.cli.GeogigCLI;
import org.locationtech.geogig.cli.InvalidParameterException;
import org.locationtech.geogig.osm.internal.history.Change;
import org.locationtech.geogig.osm.internal.history.Changeset;
import org.locationtech.geogig.osm.internal.history.HistoryDownloader;
import org.locationtech.geogig.osm.internal.history.Node;
import org.locationtech.geogig.osm.internal.history.Primitive;
import org.locationtech.geogig.osm.internal.history.Way;
import org.locationtech.geogig.repository.Repository;
import org.locationtech.geogig.repository.StagingArea;
import org.locationtech.geogig.repository.WorkingTree;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;

import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;
import com.beust.jcommander.internal.Lists;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;

/**
*
*/
@Parameters(commandNames = "import-history", commandDescription = "Import OpenStreetmap history")
public class OSMHistoryImport extends AbstractCommand implements CLICommand {

    private static final GeometryFactory GEOMF = new GeometryFactory();

    @ParametersDelegate
    public HistoryImportArgs args = new HistoryImportArgs();

    @Override
    protected void runInternal(GeogigCLI cli) throws IOException {
        checkParameter(args.numThreads > 0 && args.numThreads < 7,
                "numthreads must be between 1 and 6");

        ConsoleReader console = cli.getConsole();

        final String osmAPIUrl = resolveAPIURL();

        final long startIndex;
        final long endIndex = args.endIndex;
        if (args.resume) {
            GeoGIG geogig = cli.getGeogig();
            long lastChangeset = getCurrentBranchChangeset(geogig);
            startIndex = 1 + lastChangeset;
        } else {
            startIndex = args.startIndex;
        }
        console.println(String.format("Obtaining OSM changesets %,d to %,d from %s", startIndex,
                args.endIndex, osmAPIUrl));

        final ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true)
                .setNameFormat("osm-history-fetch-thread-%d").build();
        final ExecutorService executor = Executors.newFixedThreadPool(args.numThreads,
                threadFactory);
        final File targetDir = resolveTargetDir();
        console.println("Downloading to " + targetDir.getAbsolutePath());
        console.flush();

        HistoryDownloader downloader;
        downloader = new HistoryDownloader(osmAPIUrl, targetDir, startIndex, endIndex, executor);

        Envelope env = parseBbox();
        Predicate<Changeset> filter = parseFilter(env);
        downloader.setChangesetFilter(filter);
        try {
            importOsmHistory(cli, console, downloader, env);
        } finally {
            executor.shutdownNow();
            try {
                executor.awaitTermination(30, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                throw new CommandFailedException(e);
            }
        }
    }

    private Predicate<Changeset> parseFilter(Envelope env) {
        if (env == null) {
            return Predicates.alwaysTrue();
        }
        BBoxFiler filter = new BBoxFiler(env);
        return filter;
    }

    private Envelope parseBbox() {
        final String bbox = args.bbox;
        if (bbox != null) {
            String[] split = bbox.split(",");
            checkParameter(split.length == 4,
                    String.format("Invalid bbox format: '%s'. Expected minx,miny,maxx,maxy", bbox));
            try {
                double x1 = Double.parseDouble(split[0]);
                double y1 = Double.parseDouble(split[1]);
                double x2 = Double.parseDouble(split[2]);
                double y2 = Double.parseDouble(split[3]);
                Envelope envelope = new Envelope(x1, x2, y1, y2);
                checkParameter(!envelope.isNull(), "Provided envelope is nil");
                return envelope;
            } catch (NumberFormatException e) {
                String message = String.format(
                        "One or more bbox coordinate can't be parsed to double: '%s'", bbox);
                throw new InvalidParameterException(message, e);
            }
        }
        return null;
    }

    private static class BBoxFiler implements Predicate<Changeset> {

        private Envelope envelope;

        public BBoxFiler(Envelope envelope) {
            this.envelope = envelope;
        }

        @Override
        public boolean apply(Changeset input) {
            Optional<Envelope> wgs84Bounds = input.getWgs84Bounds();
            return wgs84Bounds.isPresent() && envelope.intersects(wgs84Bounds.get());
        }

    }

    private File resolveTargetDir() throws IOException {
        final File targetDir;
        if (args.saveFolder == null) {
            try {
                File tmp = new File(System.getProperty("java.io.tmpdir"), "changesets.osm");
                tmp.mkdirs();
                targetDir = tmp;
            } catch (Exception e) {
                throw Throwables.propagate(e);
            }
        } else {
            if (!args.saveFolder.exists() && !args.saveFolder.mkdirs()) {
                throw new IllegalArgumentException("Unable to create directory "
                        + args.saveFolder.getAbsolutePath());
            }
            targetDir = args.saveFolder;
        }
        return targetDir;
    }

    private String resolveAPIURL() {
        String osmAPIUrl;
        if (args.useTestApiEndpoint) {
            osmAPIUrl = HistoryImportArgs.DEVELOPMENT_API_ENDPOINT;
        } else if (args.apiUrl.isEmpty()) {
            osmAPIUrl = HistoryImportArgs.DEFAULT_API_ENDPOINT;
        } else {
            osmAPIUrl = args.apiUrl.get(0);
        }
        return osmAPIUrl;
    }

    private void importOsmHistory(GeogigCLI cli, ConsoleReader console,
            HistoryDownloader downloader, @Nullable Envelope featureFilter) throws IOException {

        Iterator<Changeset> changesets = downloader.fetchChangesets();

        GeoGIG geogig = cli.getGeogig();
        WorkingTree workingTree = geogig.getContext().workingTree();

        while (changesets.hasNext()) {
            Changeset changeset = changesets.next();
            if (changeset.isOpen()) {
                throw new CommandFailedException("Can't import past changeset " + changeset.getId()
                        + " as it is still open.");
            }
            String desc = String.format("obtaining osm changeset %,d...", changeset.getId());
            console.print(desc);
            console.flush();

            Optional<Iterator<Change>> opchanges = changeset.getChanges().get();
            if (!opchanges.isPresent()) {
                updateBranchChangeset(geogig, changeset.getId());
                console.println(" does not apply.");
                console.flush();
                continue;
            }
            Iterator<Change> changes = opchanges.get();
            console.print("applying...");
            console.flush();

            ObjectId workTreeId = workingTree.getTree().getId();
            long changeCount = insertChanges(cli, changes, featureFilter);
            console.print(String.format("Applied %,d changes, staging...", changeCount));
            console.flush();
            ObjectId afterTreeId = workingTree.getTree().getId();

            DiffObjectCount diffCount = geogig.command(DiffCount.class)
                    .setOldVersion(workTreeId.toString()).setNewVersion(afterTreeId.toString())
                    .call();

            geogig.command(AddOp.class).call();
            console.println(String.format("done. %,d changes actually applied.",
                    diffCount.featureCount()));
            console.flush();

            commit(cli, changeset);
        }
    }

    /**
     * @param cli
     * @param changeset
     * @throws IOException
     */
    private void commit(GeogigCLI cli, Changeset changeset) throws IOException {
        Preconditions.checkArgument(!changeset.isOpen());
        ConsoleReader console = cli.getConsole();
        console.print("Committing changeset " + changeset.getId() + "...");
        console.flush();

        GeoGIG geogig = cli.getGeogig();
        CommitOp command = geogig.command(CommitOp.class);
        command.setAllowEmpty(true);
        String message = "";
        if (changeset.getComment().isPresent()) {
            message = changeset.getComment().get() + "\nchangeset " + changeset.getId();
        } else {
            message = "changeset " + changeset.getId();
        }
        command.setMessage(message);
        final String userName = changeset.getUserName();
        command.setAuthor(userName, null);
        command.setAuthorTimestamp(changeset.getCreated());
        command.setAuthorTimeZoneOffset(0);// osm timestamps are in GMT

        if (userName != null) {
            command.setCommitter(userName, null);
        }
        command.setCommitterTimestamp(changeset.getClosed().get());
        command.setCommitterTimeZoneOffset(0);// osm timestamps are in GMT

        ProgressListener listener = cli.getProgressListener();
        listener.setProgress(0f);
        listener.started();
        command.setProgressListener(listener);
        try {
            RevCommit commit = command.call();
            Ref head = geogig.command(RefParse.class).setName(Ref.HEAD).call().get();
            Preconditions.checkState(commit.getId().equals(head.getObjectId()));
            updateBranchChangeset(geogig, changeset.getId());
            listener.complete();
            console.println("Commit " + commit.getId().toString());
            console.flush();
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * @param geogig
     * @param id
     * @throws IOException
     */
    private void updateBranchChangeset(GeoGIG geogig, long id) throws IOException {
        final File branchTrackingChangesetFile = getBranchTrackingFile(geogig);
        Preconditions.checkState(branchTrackingChangesetFile.exists());
        Files.write(String.valueOf(id), branchTrackingChangesetFile, Charset.forName("UTF-8"));
    }

    private long getCurrentBranchChangeset(GeoGIG geogig) throws IOException {
        final File branchTrackingChangesetFile = getBranchTrackingFile(geogig);
        Preconditions.checkState(branchTrackingChangesetFile.exists());
        String line = Files.readFirstLine(branchTrackingChangesetFile, Charset.forName("UTF-8"));
        if (line == null) {
            return 0;
        }
        long changeset = Long.parseLong(line);
        return changeset;
    }

    private File getBranchTrackingFile(GeoGIG geogig) throws IOException {
        final SymRef head = getHead(geogig);
        final String branch = head.getTarget();
        final URL geogigDirUrl = geogig.command(ResolveGeogigDir.class).call().get();
        File repoDir;
        try {
            repoDir = new File(geogigDirUrl.toURI());
        } catch (URISyntaxException e) {
            throw Throwables.propagate(e);
        }
        File branchTrackingFile = new File(new File(repoDir, "osm"), branch);
        Files.createParentDirs(branchTrackingFile);
        if (!branchTrackingFile.exists()) {
            Files.touch(branchTrackingFile);
        }
        return branchTrackingFile;
    }

    private SymRef getHead(GeoGIG geogig) {
        final Ref currentHead = geogig.command(RefParse.class).setName(Ref.HEAD).call().get();
        if (!(currentHead instanceof SymRef)) {
            throw new CommandFailedException("Cannot run on a dettached HEAD");
        }
        return (SymRef) currentHead;
    }

    /**
     * @param cli
     * @param changes
     * @param featureFilter
     * @throws IOException
     */
    private long insertChanges(GeogigCLI cli, final Iterator<Change> changes,
            @Nullable Envelope featureFilter) throws IOException {

        final GeoGIG geogig = cli.getGeogig();
        final Repository repository = geogig.getRepository();
        final WorkingTree workTree = repository.workingTree();

        Map<Long, Coordinate> thisChangePointCache = new LinkedHashMap<Long, Coordinate>() {
            /** serialVersionUID */
            private static final long serialVersionUID = 1277795218777240552L;

            @Override
            protected boolean removeEldestEntry(Map.Entry<Long, Coordinate> eldest) {
                return size() == 10000;
            }
        };

        long cnt = 0;

        Set<String> deletes = Sets.newHashSet();
        Multimap<String, SimpleFeature> insertsByParent = HashMultimap.create();

        while (changes.hasNext()) {
            Change change = changes.next();
            final String featurePath = featurePath(change);
            if (featurePath == null) {
                continue;// ignores relations
            }
            final String parentPath = NodeRef.parentPath(featurePath);
            if (Change.Type.delete.equals(change.getType())) {
                cnt++;
                deletes.add(featurePath);
            } else {
                final Primitive primitive = change.getNode().isPresent() ? change.getNode().get()
                        : change.getWay().get();
                final Geometry geom = parseGeometry(geogig, primitive, thisChangePointCache);
                if (geom instanceof Point) {
                    thisChangePointCache.put(Long.valueOf(primitive.getId()),
                            ((Point) geom).getCoordinate());
                }

                SimpleFeature feature = toFeature(primitive, geom);

                if (featureFilter == null
                        || featureFilter.intersects((Envelope) feature.getBounds())) {
                    insertsByParent.put(parentPath, feature);
                    cnt++;
                }
            }
        }

        for (String parentPath : insertsByParent.keySet()) {
            Collection<SimpleFeature> features = insertsByParent.get(parentPath);
            if (features.isEmpty()) {
                continue;
            }

            Iterator<? extends Feature> iterator = features.iterator();
            ProgressListener listener = new DefaultProgressListener();
            List<org.locationtech.geogig.api.Node> insertedTarget = null;
            Integer collectionSize = Integer.valueOf(features.size());
            workTree.insert(parentPath, iterator, listener, insertedTarget, collectionSize);
        }
        if (!deletes.isEmpty()) {
            workTree.delete(deletes.iterator());
        }
        return cnt;
    }

    /**
     * @param primitive
     * @param thisChangePointCache
     * @return
     */
    private Geometry parseGeometry(GeoGIG geogig, Primitive primitive,
            Map<Long, Coordinate> thisChangePointCache) {

        if (primitive instanceof Relation) {
            return null;
        }

        if (primitive instanceof Node) {
            Optional<Point> location = ((Node) primitive).getLocation();
            return location.orNull();
        }

        final Way way = (Way) primitive;
        final ImmutableList<Long> nodes = way.getNodes();

        StagingArea index = geogig.getRepository().index();

        FeatureBuilder featureBuilder = new FeatureBuilder(NODE_REV_TYPE);
        List<Coordinate> coordinates = Lists.newArrayList(nodes.size());
        FindTreeChild findTreeChild = geogig.command(FindTreeChild.class);
        findTreeChild.setIndex(true);
        ObjectId rootTreeId = geogig.command(ResolveTreeish.class).setTreeish(Ref.HEAD).call()
                .get();
        if (!rootTreeId.isNull()) {
            RevTree headTree = geogig.command(RevObjectParse.class).setObjectId(rootTreeId)
                    .call(RevTree.class).get();
            findTreeChild.setParent(headTree);
        }
        for (Long nodeId : nodes) {
            Coordinate coord = thisChangePointCache.get(nodeId);
            if (coord == null) {
                String fid = String.valueOf(nodeId);
                String path = NodeRef.appendChild(NODE_TYPE_NAME, fid);
                Optional<org.locationtech.geogig.api.Node> ref = index.findStaged(path);
                if (!ref.isPresent()) {
                    Optional<NodeRef> nodeRef = findTreeChild.setChildPath(path).call();
                    if (nodeRef.isPresent()) {
                        ref = Optional.of(nodeRef.get().getNode());
                    } else {
                        ref = Optional.absent();
                    }
                }
                if (ref.isPresent()) {
                    org.locationtech.geogig.api.Node nodeRef = ref.get();

                    RevFeature revFeature = index.getDatabase().getFeature(nodeRef.getObjectId());
                    String id = NodeRef.nodeFromPath(nodeRef.getName());
                    Feature feature = featureBuilder.build(id, revFeature);

                    Point p = (Point) ((SimpleFeature) feature).getAttribute("location");
                    if (p != null) {
                        coord = p.getCoordinate();
                        thisChangePointCache.put(Long.valueOf(nodeId), coord);
                    }
                }
            }
            if (coord != null) {
                coordinates.add(coord);
            }
        }
        if (coordinates.size() < 2) {
            return null;
        }
        return GEOMF.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
    }

    /**
     * @param change
     * @return
     */
    private String featurePath(Change change) {
        if (change.getRelation().isPresent()) {
            return null;// ignore relations for the time being
        }
        if (change.getNode().isPresent()) {
            String fid = String.valueOf(change.getNode().get().getId());
            return NodeRef.appendChild(NODE_TYPE_NAME, fid);
        }
        String fid = String.valueOf(change.getWay().get().getId());
        return NodeRef.appendChild(WAY_TYPE_NAME, fid);
    }

    private static final RevFeatureType NODE_REV_TYPE = RevFeatureTypeImpl.build(nodeType());

    private static SimpleFeature toFeature(Primitive feature, Geometry geom) {

        SimpleFeatureType ft = feature instanceof Node ? nodeType() : wayType();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(ft);

        // "visible:Boolean,version:Int,timestamp:long,[location:Point | way:LineString];
        builder.set("visible", Boolean.valueOf(feature.isVisible()));
        builder.set("version", Integer.valueOf(feature.getVersion()));
        builder.set("timestamp", Long.valueOf(feature.getTimestamp()));
        builder.set("changeset", Long.valueOf(feature.getChangesetId()));

        String tags = buildTagsString(feature.getTags());
        builder.set("tags", tags);

        String user = feature.getUserName() + ":" + feature.getUserId();
        builder.set("user", user);

        if (feature instanceof Node) {
            builder.set("location", geom);
        } else if (feature instanceof Way) {
            builder.set("way", geom);
            String nodes = buildNodesString(((Way) feature).getNodes());
            builder.set("nodes", nodes);
        } else {
            throw new IllegalArgumentException();
        }

        String fid = String.valueOf(feature.getId());
        SimpleFeature simpleFeature = builder.buildFeature(fid);
        return simpleFeature;
    }

    /**
     * @param tags
     * @return
     */
    @Nullable
    private static String buildTagsString(Map<String, String> tags) {
        if (tags.isEmpty()) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        for (Iterator<Map.Entry<String, String>> it = tags.entrySet().iterator(); it.hasNext();) {
            Entry<String, String> e = it.next();
            String key = e.getKey();
            if (key == null || key.isEmpty()) {
                continue;
            }
            String value = e.getValue();
            sb.append(key).append(':').append(value);
            if (it.hasNext()) {
                sb.append(';');
            }
        }
        return sb.toString();
    }

    private static String buildNodesString(List<Long> nodeIds) {
        StringBuilder sb = new StringBuilder();
        for (Iterator<Long> it = nodeIds.iterator(); it.hasNext();) {
            Long node = it.next();
            sb.append(node);
            if (it.hasNext()) {
                sb.append(";");
            }
        }
        return sb.toString();

    }
}
TOP

Related Classes of org.locationtech.geogig.osm.cli.commands.OSMHistoryImport$BBoxFiler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.