hoot.services.models.osm.Way.java Source code

Java tutorial

Introduction

Here is the source code for hoot.services.models.osm.Way.java

Source

/*
 * This file is part of Hootenanny.
 *
 * Hootenanny is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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/>.
 *
 * --------------------------------------------------------------------
 *
 * The following copyright notices are generated automatically. If you
 * have a new notice to add, please use the format:
 * " * @copyright Copyright ..."
 * This will properly maintain the copyright information. DigitalGlobe
 * copyrights will be updated automatically.
 *
 * @copyright Copyright (C) 2015, 2016 DigitalGlobe (http://www.digitalglobe.com/)
 */
package hoot.services.models.osm;

import static hoot.services.HootProperties.MAXIMUM_WAY_NODES;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.transform.TransformerException;

import org.apache.commons.lang3.StringUtils;
import org.apache.xpath.XPathAPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.BooleanPath;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.SimplePath;
import com.querydsl.sql.RelationalPathBase;
import com.querydsl.sql.SQLQuery;

import hoot.services.geo.BoundingBox;
import hoot.services.geo.Coordinates;
import hoot.services.models.db.CurrentNodes;
import hoot.services.models.db.CurrentWayNodes;
import hoot.services.models.db.CurrentWays;
import hoot.services.models.db.QCurrentWayNodes;
import hoot.services.utils.DbUtils;
import hoot.services.utils.DbUtils.EntityChangeType;

/**
 * Represents the model for an OSM way
 */
public class Way extends Element {
    private static final Logger logger = LoggerFactory.getLogger(Way.class);
    private static final QCurrentWayNodes currentWayNodes = QCurrentWayNodes.currentWayNodes;

    private final List<Long> wayNodeIdsCache = new ArrayList<>();

    // temp collection of way node coordinates used to calculate the way'sbounds
    private Map<Long, Coordinates> nodeCoordsCollection;

    public Way(long mapId, Connection dbConn) {
        super(dbConn);
        super.elementType = ElementType.Way;
        super.record = new CurrentWays();

        setMapId(mapId);
    }

    public Way(long mapId, Connection dbConn, CurrentWays record) {
        super(dbConn);
        super.elementType = ElementType.Way;

        CurrentWays wayRecord = new CurrentWays();
        wayRecord.setChangesetId(record.getChangesetId());
        wayRecord.setId(record.getId());
        wayRecord.setTimestamp(record.getTimestamp());
        wayRecord.setVersion(record.getVersion());
        wayRecord.setVisible(record.getVisible());
        wayRecord.setTags(record.getTags());
        super.record = wayRecord;
        setMapId(mapId);
    }

    List<Long> getWayNodeIdsCache() {
        return wayNodeIdsCache;
    }

    /**
     * Returns all node records for the specified ways from the services
     * database
     *
     * @param mapId
     *            ID of the map owning the ways
     * @param wayIds
     *            a collection of way IDs for which to retrieve node records
     * @param dbConn
     *            JDBC Connection
     * @return a list of node records
     */
    static List<CurrentNodes> getNodesForWays(long mapId, Set<Long> wayIds, Connection dbConn) {
        if (!wayIds.isEmpty()) {
            return new SQLQuery<>(dbConn, DbUtils.getConfiguration(mapId)).select(currentNodes)
                    .from(currentWayNodes, currentNodes).join(currentNodes)
                    .on(currentWayNodes.nodeId.eq(currentNodes.id)).where(currentWayNodes.wayId.in(wayIds)).fetch();
        }
        return new ArrayList<>();
    }

    /*
     * Returns the nodes associated with this way
     */
    private List<CurrentNodes> getNodes() {
        return new SQLQuery<>(conn, DbUtils.getConfiguration(getMapId())).select(currentNodes)
                .from(currentWayNodes, currentNodes).join(currentNodes)
                .on(currentWayNodes.nodeId.eq(currentNodes.id)).where(currentWayNodes.wayId.eq(getId()))
                .orderBy(currentWayNodes.sequenceId.asc()).fetch();
    }

    /*
     * Returns the IDs of the nodes associated with this way
     *
     * This is a List, rather than a Set, since the same node ID can be used for
     * the first and last node ID in the way nodes sequence for closed polygons.
     */
    private List<Long> getNodeIds() {
        return new SQLQuery<>(conn, DbUtils.getConfiguration(getMapId())).select(currentWayNodes.nodeId)
                .from(currentWayNodes).where(currentWayNodes.wayId.eq(getId()))
                .orderBy(currentWayNodes.sequenceId.asc()).fetch();
    }

    /*
     * First calculates the bounds for all nodes belonging to this way that were
     * referenced explicitly in the changeset upload request. Then calculates
     * the bounds for all the way's nodes not mentioned in the request after
     * retrieving them from the services database. The bounds returned is a sum
     * of the two calculated bounds.
     */
    private BoundingBox getBoundsFromRequestDataAndRemainderFromDatabase() {
        double minLon = BoundingBox.LON_LIMIT + 1;
        double minLat = BoundingBox.LAT_LIMIT + 1;
        double maxLon = (-1 * BoundingBox.LON_LIMIT) - 1;
        double maxLat = (-1 * BoundingBox.LAT_LIMIT) - 1;

        // nodes that were parsed in the same request referencing this way;
        // either as a create or modify
        Set<Long> modifiedRecordIds = new HashSet<>(relatedRecordIds);
        for (long wayNodeId : relatedRecordIds) {
            double lat = nodeCoordsCollection.get(wayNodeId).getLat();
            double lon = nodeCoordsCollection.get(wayNodeId).getLon();
            if (lat < minLat) {
                minLat = lat;
            }
            if (lat > maxLat) {
                maxLat = lat;
            }
            if (lon < minLon) {
                minLon = lon;
            }
            if (lon > maxLon) {
                maxLon = lon;
            }

            modifiedRecordIds.remove(wayNodeId);
        }

        // any way nodes not mentioned in the created/modified in the changeset
        // XML represented by the remainder of the IDs in relatedRecordIds, request must now be
        // retrieved from the database
        List<?> nodeRecords = Element.getElementRecords(getMapId(), ElementType.Node, modifiedRecordIds, conn);
        for (Object record : nodeRecords) {
            CurrentNodes nodeRecord = (CurrentNodes) record;
            double lat = nodeRecord.getLatitude();
            double lon = nodeRecord.getLongitude();
            if (lat < minLat) {
                minLat = lat;
            }
            if (lat > maxLat) {
                maxLat = lat;
            }
            if (lon < minLon) {
                minLon = lon;
            }
            if (lon > maxLon) {
                maxLon = lon;
            }

            modifiedRecordIds.remove(nodeRecord.getId());
        }

        return new BoundingBox(minLon, minLat, maxLon, maxLat);
    }

    /**
     * Returns the bounds of this element
     * <p>
     * Any change to a way, including deletion, adds all of the way's nodes to
     * the bbox.
     *
     * @return a bounding box
     */
    @Override
    public BoundingBox getBounds() {
        // this is a little kludgy, but...first see if the related record data
        // (waynode data) is left over from the XML parsing (clearTempData clears it out). If it is
        // still here, use it because the way nodes will not have been written to the database yet,
        // so use the cached way node IDs and node coordinate info to construct the bounds
        if ((relatedRecordIds != null) && (!relatedRecordIds.isEmpty())) {
            return getBoundsFromRequestDataAndRemainderFromDatabase();
        }

        // If no temp related record data is present (hasn't been cleared out),
        // the way nodes data for this way must be in the services database and up to date,
        // so get it from there.
        return new BoundingBox(new ArrayList<>(getNodes()));
    }

    /**
     * Populates this element model object based on osm diff data
     *
     * @param xml
     *            xml data to construct the element from
     */
    @Override
    public void fromXml(org.w3c.dom.Node xml) {
        logger.debug("Parsing way...");

        NamedNodeMap xmlAttributes = xml.getAttributes();

        CurrentWays wayRecord = (CurrentWays) super.record;
        wayRecord.setChangesetId(parseChangesetId(xmlAttributes));
        wayRecord.setVersion(parseVersion());
        wayRecord.setTimestamp(parseTimestamp(xmlAttributes));
        wayRecord.setVisible(true);

        if (entityChangeType != EntityChangeType.DELETE) {
            wayRecord.setTags(parseTags(xml));
        }

        super.setRecord(wayRecord);

        // if we're deleting the way, all the way nodes will get deleted
        // automatically...and no new ones need to be parsed
        if (entityChangeType != EntityChangeType.DELETE) {
            parseWayNodesXml(xml);
        }
    }

    @Override
    public void checkAndFailIfUsedByOtherObjects()
            throws OSMAPIAlreadyDeletedException, OSMAPIPreconditionException {
        if (!super.getVisible()) {
            throw new OSMAPIAlreadyDeletedException("Way with ID = " + super.getId() + " has been already deleted "
                    + "from map with ID = " + getMapId());
        }

        // From the Rails port of OSM API:
        // rels = Relation.joins(:relation_members).where(:visible => true,
        // :current_relation_members => { :member_type => "Way", :member_id => id }).
        SQLQuery<Long> owningRelationsQuery = new SQLQuery<>(conn, DbUtils.getConfiguration(getMapId()))
                .select(currentRelationMembers.relationId).distinct().from(currentRelations, currentRelationMembers)
                .join(currentRelationMembers).on(currentRelations.id.eq(currentRelationMembers.relationId))
                .where(currentRelations.visible.eq(true)
                        .and(currentRelationMembers.memberType.eq(DbUtils.nwr_enum.way))
                        .and(currentRelationMembers.memberId.eq(super.getId())))
                .orderBy(currentRelationMembers.relationId.asc());

        List<Long> owningRelationsIds = owningRelationsQuery.fetch();

        if (!owningRelationsIds.isEmpty()) {
            throw new OSMAPIPreconditionException(
                    "Node with ID = " + super.getId() + " is still used by other relation(s): "
                            + StringUtils.join(owningRelationsIds) + " from map with ID = " + getMapId());
        }
    }

    /**
     * Returns an XML representation of the element returned in a query; does
     * not add tags; assumes way nodes have already been written to the services
     * db
     *
     * @param parentXml
     *            XML node this element should be attached under
     * @param modifyingUserId
     *            ID of the user which created this element
     * @param modifyingUserDisplayName
     *            user display name of the user which created this element
     * @param multiLayerUniqueElementIds
     *            if true, IDs are prepended with <map id>_<first letter of the
     *            element type>_; this setting activated is not compatible with
     *            standard OSM clients (specific to Hootenanny iD)
     * @param addChildren
     *            if true, element children are added to the element xml
     * @return an XML element
     */
    @Override
    public org.w3c.dom.Element toXml(org.w3c.dom.Element parentXml, long modifyingUserId,
            String modifyingUserDisplayName, boolean multiLayerUniqueElementIds, boolean addChildren) {
        org.w3c.dom.Element element = super.toXml(parentXml, modifyingUserId, modifyingUserDisplayName,
                multiLayerUniqueElementIds, addChildren);
        Document doc = parentXml.getOwnerDocument();

        if (addChildren) {
            List<Long> nodeIds = getNodeIds();
            Set<Long> elementIds = new HashSet<>();

            // way nodes are output in sequence order; list should already be sorted by the query
            for (long nodeId : nodeIds) {
                org.w3c.dom.Element nodeElement = doc.createElement("nd");
                nodeElement.setAttribute("ref", String.valueOf(nodeId));
                element.appendChild(nodeElement);
                elementIds.add(nodeId);
            }

            List<Tuple> elementRecords = (List<Tuple>) Element.getElementRecordsWithUserInfo(getMapId(),
                    ElementType.Node, elementIds, conn);

            for (Tuple elementRecord : elementRecords) {
                Element nodeFullElement = ElementFactory.create(ElementType.Node, elementRecord, conn, getMapId());
                org.w3c.dom.Element nodeXml = nodeFullElement.toXml(parentXml, modifyingUserId,
                        modifyingUserDisplayName, false, false);
                parentXml.appendChild(nodeXml);
            }
        }

        org.w3c.dom.Element elementWithTags = addTagsXml(element);
        if (elementWithTags == null) {
            return element;
        }

        return elementWithTags;
    }

    private void validateWayNodesSize(NodeList wayNodesXml) {
        if (entityChangeType != EntityChangeType.DELETE) {
            CurrentWays wayRecord = (CurrentWays) record;
            long maximumWayNodes = Long.parseLong(MAXIMUM_WAY_NODES);

            long numWayNodes = wayNodesXml.getLength();
            if (numWayNodes < 2) {
                throw new IllegalArgumentException("Too few nodes specified for way with ID: " + wayRecord.getId());
            } else if (numWayNodes > maximumWayNodes) {
                throw new IllegalArgumentException(
                        "Too many nodes specified for way with ID: " + wayRecord.getId());
            }
        }
    }

    private long parseWayNode(org.w3c.dom.Node nodeXml) {
        NamedNodeMap nodeXmlAttributes = nodeXml.getAttributes();

        long parsedNodeId = Long.parseLong(nodeXmlAttributes.getNamedItem("ref").getNodeValue());
        wayNodeIdsCache.add(parsedNodeId);

        Coordinates nodeCoords = new Coordinates();

        Map<Long, Element> parsedNodes = parsedElementIdsToElementsByType.get(ElementType.Node);

        // if this is a node created within the same request that is referencing
        // this way, it won't exist in the database, but it will be in the element cache created
        // when parsing the node from the request
        if (parsedNodeId < 0) {
            if (!parsedNodes.containsKey(parsedNodeId)) {
                // TODO: add test for this
                throw new IllegalArgumentException("Created way references new node not "
                        + "found in create request with ID: " + parsedNodeId);
            }
        }

        // The node is referenced somewhere else in this request, so get its
        // info from the request, not the database b/c the database either won't have it or will have
        // outdated info. Only get info from the request if the node is being created/modified, as if it is
        // being deleted, we can just get the info from the database since its coords won't be
        // changing and might not be in the request (not required).
        long actualNodeId;
        if (parsedNodes.containsKey(parsedNodeId)
                && (parsedNodes.get(parsedNodeId).getEntityChangeType() != EntityChangeType.DELETE)) {
            Node node = (Node) parsedElementIdsToElementsByType.get(ElementType.Node).get(parsedNodeId);
            CurrentNodes nodeRecord = (CurrentNodes) node.getRecord();
            actualNodeId = nodeRecord.getId();
            nodeCoords.setLat(nodeRecord.getLatitude());
            nodeCoords.setLon(nodeRecord.getLongitude());
        } else {
            // element not referenced in this request, so should already exist in
            // the db and its info be up to date

            actualNodeId = parsedNodeId;
            CurrentNodes existingNodeRecord = dbNodeCache.get(actualNodeId);

            if (existingNodeRecord == null) {
                throw new IllegalStateException("Node with ID: " + actualNodeId + " does not exist for way.");
            }

            if (!existingNodeRecord.getVisible()) {
                throw new IllegalStateException("Node with ID: " + actualNodeId + " is not visible for way.");
            }

            nodeCoords.setLat(existingNodeRecord.getLatitude());
            nodeCoords.setLon(existingNodeRecord.getLongitude());
        }

        nodeCoordsCollection.put(actualNodeId, nodeCoords);

        return actualNodeId;
    }

    private CurrentWayNodes createWayNodeRecord(long actualNodeId, long sequenceId) {
        CurrentWayNodes wayNodeRecord = new CurrentWayNodes();
        wayNodeRecord.setNodeId(actualNodeId);
        wayNodeRecord.setSequenceId(sequenceId);
        wayNodeRecord.setWayId(getId());
        return wayNodeRecord;
    }

    /*
     * Parse the way nodes XML. Keep a cache of the parsed records and node geo
     * info.
     */
    private void parseWayNodesXml(org.w3c.dom.Node xml) {
        NodeList wayNodesXml = null;
        try {
            wayNodesXml = XPathAPI.selectNodeList(xml, "nd");
        } catch (TransformerException e) {
            throw new RuntimeException("Error selecting XML node 'nd'!", e);
        }

        validateWayNodesSize(wayNodesXml);

        relatedRecords = new ArrayList<>();
        relatedRecordIds = new HashSet<>();
        nodeCoordsCollection = new HashMap<>();

        for (int i = 0; i < wayNodesXml.getLength(); i++) {
            org.w3c.dom.Node nodeXml = wayNodesXml.item(i);
            long actualNodeId = parseWayNode(nodeXml);
            relatedRecordIds.add(actualNodeId);
            relatedRecords.add(createWayNodeRecord(actualNodeId, (i + 1)));
        }
    }

    /**
     * Returns the generated table identifier for this element
     *
     * @return a table
     */
    @Override
    public RelationalPathBase<?> getElementTable() {
        return currentWays;
    }

    /**
     * Returns the generated ID field for this element
     *
     * @return a table field
     */
    @Override
    public NumberPath<Long> getElementIdField() {
        return currentWays.id;
    }

    /**
     * Returns the generated visibility field for this element
     *
     * @return a table field
     */
    @Override
    public BooleanPath getElementVisibilityField() {
        return currentWays.visible;
    }

    /**
     * Returns the generated version field for this element
     *
     * @return a table field
     */
    @Override
    public NumberPath<Long> getElementVersionField() {
        return currentWays.version;
    }

    /**
     * Returns the generated changeset ID field for this element
     *
     * @return a table field
     */
    @Override
    public NumberPath<Long> getChangesetIdField() {
        return currentWays.changesetId;
    }

    /**
     * Returns the generated table identifier for records related to this
     * element
     *
     * @return a table
     */
    @Override
    public RelationalPathBase<?> getRelatedRecordTable() {
        return currentWayNodes;
    }

    /**
     * Returns the table field in the related record table that can be joined
     * with the parent element record table
     *
     * @return a table field
     */
    @Override
    public NumberPath<Long> getRelatedRecordJoinField() {
        return currentWayNodes.wayId;
    }

    /**
     * Returns the generated tag field for this element
     *
     * @return a table field
     */
    public SimplePath<Object> getTagField() {
        return currentWays.tags;
    }

    /**
     * OSM related element type (e.g. way nodes for ways, relation members for
     * relations)
     *
     * @return a list of element types
     */
    @Override
    public List<ElementType> getRelatedElementTypes() {
        return Collections.singletonList(ElementType.Node);
    }
}