// This software is released into the Public Domain. See copying.txt for details. package org.openstreetmap.osmosis.pgsimple.v0_6.impl; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.postgis.PGgeometry; import org.postgis.Point; import org.postgis.Polygon; import org.openstreetmap.osmosis.core.OsmosisConstants; import org.openstreetmap.osmosis.core.OsmosisRuntimeException; import org.openstreetmap.osmosis.core.container.v0_6.BoundContainer; import org.openstreetmap.osmosis.core.container.v0_6.BoundContainerIterator; import org.openstreetmap.osmosis.core.container.v0_6.DatasetContext; import org.openstreetmap.osmosis.core.container.v0_6.EntityContainer; import org.openstreetmap.osmosis.core.container.v0_6.EntityManager; import org.openstreetmap.osmosis.core.container.v0_6.NodeContainer; import org.openstreetmap.osmosis.core.container.v0_6.NodeContainerIterator; import org.openstreetmap.osmosis.core.container.v0_6.RelationContainer; import org.openstreetmap.osmosis.core.container.v0_6.RelationContainerIterator; import org.openstreetmap.osmosis.core.container.v0_6.WayContainer; import org.openstreetmap.osmosis.core.container.v0_6.WayContainerIterator; import org.openstreetmap.osmosis.core.database.DatabaseLoginCredentials; import org.openstreetmap.osmosis.core.database.DatabasePreferences; import org.openstreetmap.osmosis.core.domain.v0_6.Bound; import org.openstreetmap.osmosis.core.domain.v0_6.EntityType; import org.openstreetmap.osmosis.core.domain.v0_6.Node; import org.openstreetmap.osmosis.core.domain.v0_6.Relation; import org.openstreetmap.osmosis.core.domain.v0_6.Way; import org.openstreetmap.osmosis.core.lifecycle.ReleasableContainer; import org.openstreetmap.osmosis.core.lifecycle.ReleasableIterator; import org.openstreetmap.osmosis.pgsimple.common.DatabaseContext; import org.openstreetmap.osmosis.pgsimple.common.PolygonBuilder; import org.openstreetmap.osmosis.pgsimple.common.SchemaVersionValidator; import org.openstreetmap.osmosis.pgsimple.v0_6.PostgreSqlVersionConstants; import org.openstreetmap.osmosis.core.store.MultipleSourceIterator; import org.openstreetmap.osmosis.core.store.ReleasableAdaptorForIterator; import org.openstreetmap.osmosis.core.store.UpcastIterator; /** * Provides read-only access to a PostgreSQL dataset store. Each thread * accessing the store must create its own reader. It is important that all * iterators obtained from this reader are released before releasing the reader * itself. * * @author Brett Henderson */ public class PostgreSqlDatasetContext implements DatasetContext { private static final Logger LOG = Logger.getLogger(PostgreSqlDatasetContext.class.getName()); private DatabaseLoginCredentials loginCredentials; private DatabasePreferences preferences; private DatabaseCapabilityChecker capabilityChecker; private boolean initialized; private DatabaseContext dbCtx; private UserDao userDao; private NodeDao nodeDao; private WayDao wayDao; private RelationDao relationDao; private PostgreSqlEntityManager<Node> nodeManager; private PostgreSqlEntityManager<Way> wayManager; private PostgreSqlEntityManager<Relation> relationManager; private PolygonBuilder polygonBuilder; private ReleasableContainer releasableContainer; /** * Creates a new instance. * * @param loginCredentials * Contains all information required to connect to the database. * @param preferences * Contains preferences configuring database behaviour. */ public PostgreSqlDatasetContext(DatabaseLoginCredentials loginCredentials, DatabasePreferences preferences) { this.loginCredentials = loginCredentials; this.preferences = preferences; polygonBuilder = new PolygonBuilder(); releasableContainer = new ReleasableContainer(); initialized = false; } /** * Initialises the database connection and associated data access objects. */ private void initialize() { if (dbCtx == null) { ActionDao actionDao; dbCtx = new DatabaseContext(loginCredentials); new SchemaVersionValidator(dbCtx, preferences).validateVersion( PostgreSqlVersionConstants.SCHEMA_VERSION); capabilityChecker = new DatabaseCapabilityChecker(dbCtx); actionDao = releasableContainer.add(new ActionDao(dbCtx)); userDao = releasableContainer.add(new UserDao(dbCtx, actionDao)); nodeDao = releasableContainer.add(new NodeDao(dbCtx, actionDao)); wayDao = releasableContainer.add(new WayDao(dbCtx, actionDao)); relationDao = releasableContainer.add(new RelationDao(dbCtx, actionDao)); nodeManager = new PostgreSqlEntityManager<Node>(nodeDao, userDao); wayManager = new PostgreSqlEntityManager<Way>(wayDao, userDao); relationManager = new PostgreSqlEntityManager<Relation>(relationDao, userDao); } initialized = true; } /** * {@inheritDoc} */ @Override @Deprecated public Node getNode(long id) { return getNodeManager().getEntity(id); } /** * {@inheritDoc} */ @Override @Deprecated public Way getWay(long id) { return getWayManager().getEntity(id); } /** * {@inheritDoc} */ @Override @Deprecated public Relation getRelation(long id) { return getRelationManager().getEntity(id); } /** * {@inheritDoc} */ @Override public EntityManager<Node> getNodeManager() { if (!initialized) { initialize(); } return nodeManager; } /** * {@inheritDoc} */ @Override public EntityManager<Way> getWayManager() { if (!initialized) { initialize(); } return wayManager; } /** * {@inheritDoc} */ @Override public EntityManager<Relation> getRelationManager() { if (!initialized) { initialize(); } return relationManager; } /** * {@inheritDoc} */ @Override public ReleasableIterator<EntityContainer> iterate() { List<Bound> bounds; List<ReleasableIterator<EntityContainer>> sources; if (!initialized) { initialize(); } // Build the bounds list. bounds = new ArrayList<Bound>(); bounds.add(new Bound("Osmosis " + OsmosisConstants.VERSION)); sources = new ArrayList<ReleasableIterator<EntityContainer>>(); sources.add(new UpcastIterator<EntityContainer, BoundContainer>( new BoundContainerIterator(new ReleasableAdaptorForIterator<Bound>(bounds.iterator())))); sources.add(new UpcastIterator<EntityContainer, NodeContainer>( new NodeContainerIterator(nodeDao.iterate()))); sources.add(new UpcastIterator<EntityContainer, WayContainer>( new WayContainerIterator(wayDao.iterate()))); sources.add(new UpcastIterator<EntityContainer, RelationContainer>( new RelationContainerIterator(relationDao.iterate()))); return new MultipleSourceIterator<EntityContainer>(sources); } /** * {@inheritDoc} */ @Override public ReleasableIterator<EntityContainer> iterateBoundingBox( double left, double right, double top, double bottom, boolean completeWays) { List<Bound> bounds; PreparedStatement preparedStatement = null; int prmIndex; Point[] bboxPoints; Polygon bboxPolygon; MemberTypeValueMapper memberTypeValueMapper; int rowCount; List<ReleasableIterator<EntityContainer>> resultSets; if (!initialized) { initialize(); } // Build the bounds list. bounds = new ArrayList<Bound>(); bounds.add(new Bound(right, left, top, bottom, "Osmosis " + OsmosisConstants.VERSION)); try { // PostgreSQL sometimes incorrectly chooses to perform full table scans, these options // prevent this. Note that this is not recommended practice according to documentation // but fixing this would require modifying the table statistics gathering // configuration to produce better plans. dbCtx.executeStatement("SET enable_seqscan = false"); dbCtx.executeStatement("SET enable_mergejoin = false"); dbCtx.executeStatement("SET enable_hashjoin = false"); // Create a temporary table capable of holding node ids. LOG.finer("Creating node id temp table."); dbCtx.executeStatement("CREATE TEMPORARY TABLE box_node_list (id bigint PRIMARY KEY) ON COMMIT DROP"); // Create a temporary table capable of holding way ids. LOG.finer("Creating way id temp table."); dbCtx.executeStatement("CREATE TEMPORARY TABLE box_way_list (id bigint PRIMARY KEY) ON COMMIT DROP"); // Create a temporary table capable of holding relation ids. LOG.finer("Creating relation id temp table."); dbCtx.executeStatement("CREATE TEMPORARY TABLE box_relation_list (id bigint PRIMARY KEY) ON COMMIT DROP"); // Build a polygon representing the bounding box. // Sample box for query testing may be: // GeomFromText('POLYGON((144.93912192855174 -37.82981987499741, // 144.93912192855174 -37.79310006709244, 144.98188026000003 // -37.79310006709244, 144.98188026000003 -37.82981987499741, // 144.93912192855174 -37.82981987499741))', -1) bboxPoints = new Point[5]; bboxPoints[0] = new Point(left, bottom); bboxPoints[1] = new Point(left, top); bboxPoints[2] = new Point(right, top); bboxPoints[3] = new Point(right, bottom); bboxPoints[4] = new Point(left, bottom); bboxPolygon = polygonBuilder.createPolygon(bboxPoints); // Instantiate the mapper for converting between entity types and // member type values. memberTypeValueMapper = new MemberTypeValueMapper(); // Select all nodes inside the box into the node temp table. LOG.finer("Selecting all node ids inside bounding box."); preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_node_list SELECT id FROM nodes WHERE (geom && ?)"); prmIndex = 1; preparedStatement.setObject(prmIndex++, new PGgeometry(bboxPolygon)); rowCount = preparedStatement.executeUpdate(); preparedStatement.close(); preparedStatement = null; LOG.finer(rowCount + " rows affected."); // Select all ways inside the bounding box into the way temp table. if (capabilityChecker.isWayLinestringSupported()) { LOG.finer("Selecting all way ids inside bounding box using way linestring geometry."); // We have full way geometry available so select ways // overlapping the requested bounding box. preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_way_list " + "SELECT id FROM ways w where w.linestring && ?" ); prmIndex = 1; preparedStatement.setObject(prmIndex++, new PGgeometry(bboxPolygon)); } else if (capabilityChecker.isWayBboxSupported()) { LOG.finer("Selecting all way ids inside bounding box using dynamically built" + " way linestring with way bbox indexing."); // The inner query selects the way id and node coordinates for // all ways constrained by the way bounding box which is // indexed. // The middle query converts the way node coordinates into // linestrings. // The outer query constrains the query to the linestrings // inside the bounding box. These aren't indexed but the inner // query way bbox constraint will minimise the unnecessary data. preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_way_list " + "SELECT way_id FROM (" + "SELECT c.way_id AS way_id, ST_MakeLine(c.geom) AS way_line FROM (" + "SELECT w.id AS way_id, n.geom AS geom FROM nodes n" + " INNER JOIN way_nodes wn ON n.id = wn.node_id" + " INNER JOIN ways w ON wn.way_id = w.id" + " WHERE (w.bbox && ?) ORDER BY wn.way_id, wn.sequence_id" + ") c " + "GROUP BY c.way_id" + ") w " + "WHERE (w.way_line && ?)" ); prmIndex = 1; preparedStatement.setObject(prmIndex++, new PGgeometry(bboxPolygon)); preparedStatement.setObject(prmIndex++, new PGgeometry(bboxPolygon)); } else { LOG.finer("Selecting all way ids inside bounding box using already selected nodes."); // No way bbox support is available so select ways containing // the selected nodes. preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_way_list " + "SELECT wn.way_id FROM way_nodes wn INNER JOIN box_node_list n ON wn.node_id = n.id" + " GROUP BY wn.way_id" ); } rowCount = preparedStatement.executeUpdate(); preparedStatement.close(); preparedStatement = null; LOG.finer(rowCount + " rows affected."); // Select all relations containing the nodes or ways into the relation table. LOG.finer("Selecting all relation ids containing selected nodes or ways."); preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_relation_list (" + "SELECT rm.relation_id AS relation_id FROM relation_members rm" + " INNER JOIN box_node_list n ON rm.member_id = n.id WHERE rm.member_type = ? " + "UNION " + "SELECT rm.relation_id AS relation_id FROM relation_members rm" + " INNER JOIN box_way_list w ON rm.member_id = w.id WHERE rm.member_type = ?" + ")" ); prmIndex = 1; preparedStatement.setString(prmIndex++, memberTypeValueMapper.getMemberType(EntityType.Node)); preparedStatement.setString(prmIndex++, memberTypeValueMapper.getMemberType(EntityType.Way)); rowCount = preparedStatement.executeUpdate(); preparedStatement.close(); preparedStatement = null; LOG.finer(rowCount + " rows affected."); // Include all relations containing the current relations into the // relation table and repeat until no more inclusions occur. do { LOG.finer("Selecting parent relations of selected relations."); preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_relation_list " + "SELECT rm.relation_id AS relation_id FROM relation_members rm" + " INNER JOIN box_relation_list r ON rm.member_id = r.id WHERE rm.member_type = ? " + "EXCEPT " + "SELECT id AS relation_id FROM box_relation_list" ); prmIndex = 1; preparedStatement.setString(prmIndex++, memberTypeValueMapper.getMemberType(EntityType.Relation)); rowCount = preparedStatement.executeUpdate(); preparedStatement.close(); preparedStatement = null; LOG.finer(rowCount + " rows affected."); } while (rowCount > 0); // If complete ways is set, select all nodes contained by the ways into the node temp table. if (completeWays) { LOG.finer("Selecting all node ids for selected ways."); preparedStatement = dbCtx.prepareStatement( "INSERT INTO box_node_list " + "SELECT wn.node_id AS id FROM way_nodes wn INNER JOIN box_way_list bw ON wn.way_id = bw.id " + "EXCEPT " + "SELECT id AS node_id FROM box_node_list" ); prmIndex = 1; rowCount = preparedStatement.executeUpdate(); preparedStatement.close(); preparedStatement = null; LOG.finer(rowCount + " rows affected."); } // Analyse the temporary tables to give the query planner the best chance of producing good queries. dbCtx.executeStatement("ANALYZE box_node_list"); dbCtx.executeStatement("ANALYZE box_way_list"); dbCtx.executeStatement("ANALYZE box_relation_list"); // Create iterators for the selected records for each of the entity types. LOG.finer("Iterating over results."); resultSets = new ArrayList<ReleasableIterator<EntityContainer>>(); resultSets.add( new UpcastIterator<EntityContainer, BoundContainer>( new BoundContainerIterator(new ReleasableAdaptorForIterator<Bound>(bounds.iterator())))); resultSets.add( new UpcastIterator<EntityContainer, NodeContainer>( new NodeContainerIterator(new NodeReader(dbCtx, "box_node_list")))); resultSets.add( new UpcastIterator<EntityContainer, WayContainer>( new WayContainerIterator(new WayReader(dbCtx, "box_way_list")))); resultSets.add( new UpcastIterator<EntityContainer, RelationContainer>( new RelationContainerIterator(new RelationReader(dbCtx, "box_relation_list")))); // Merge all readers into a single result iterator and return. return new MultipleSourceIterator<EntityContainer>(resultSets); } catch (SQLException e) { throw new OsmosisRuntimeException("Unable to perform bounding box queries.", e); } finally { if (preparedStatement != null) { try { preparedStatement.close(); } catch (SQLException e) { // We are already in an error condition so log and continue. LOG.log(Level.WARNING, "Unable to close prepared statement.", e); } } } } /** * {@inheritDoc} */ @Override public void complete() { if (dbCtx != null) { dbCtx.commit(); } } /** * {@inheritDoc} */ @Override public void close() { releasableContainer.close(); releasableContainer.clear(); if (dbCtx != null) { dbCtx.close(); dbCtx = null; } } }