/** * ***************************************************************************** * Copyright 2013 Johannes Mitlmeier * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * **************************************************************************** */ package de.fub.agg2graph.agg.tiling; import de.fub.agg2graph.agg.AggConnection; import de.fub.agg2graph.agg.AggContainer; import de.fub.agg2graph.agg.AggNode; import de.fub.agg2graph.input.FileHandler; import de.fub.agg2graph.structs.GPSCalc; import de.fub.agg2graph.structs.GPSPoint; import de.fub.agg2graph.structs.IEdge; import de.fub.agg2graph.structs.ILocation; import de.fub.agg2graph.structs.projection.OsmProjection; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; /** * Managing tiles: adding and removing nodes, proximity searches. * * @author Johannes Mitlmeier * * @param */ public class TileManager { private Tile<AggNode> root; private int splitFactor = 3; private boolean doMerge = false; private double mergeFactor = 0.7; private int maxElementsPerTile = 5000; private Rectangle2D.Double minimumSplitSize = new Rectangle2D.Double(0, 0, 10e-3, 10e-3); private int nodeCounter = 0; private int connCounter = 0; private AggContainer agg; // lon = "width", first. lat = "height", last. public static Rectangle2D.Double WORLD = new Rectangle2D.Double(-180, -90, 360, 180); private DefaultCachingStrategy dcs; public TileManager(DefaultCachingStrategy dcs) { this.dcs = dcs; clear(); } public TileManager(DefaultCachingStrategy dcs, AggContainer agg, int splitFactor) { this(dcs); this.agg = agg; this.splitFactor = splitFactor; } public int getSplitFactor() { return splitFactor; } public void setSplitFactor(int splitFactor) { this.splitFactor = splitFactor; } public double getMergeFactor() { return mergeFactor; } public void setMergeFactor(double mergeFactor) { this.mergeFactor = mergeFactor; } public int getMaxElementsPerTile() { return maxElementsPerTile; } public void setMaxElementsPerTile(int maxElementsPerTile) { this.maxElementsPerTile = maxElementsPerTile; } public Rectangle2D.Double getMinimumSplitSize() { return minimumSplitSize; } public void setMinimumSplitSize(Rectangle2D.Double minimumSplitSize) { this.minimumSplitSize = minimumSplitSize; } public Tile<AggNode> getTile(ILocation loc) { Tile<AggNode> currentTile = getRoot(); while (true) { if (currentTile.isLeaf) { boolean inMemory = ((DefaultCachingStrategy) agg.getCachingStrategy()).getTc().isInMemory(); boolean fileExist = (new File(MessageFormat.format("{0}{1}{2}.xml", agg.getDataSource(), File.separator, currentTile.getID()))).exists(); if (!inMemory && !fileExist && !isEmpty()) { currentTile.split(); } else { Point2D.Double point = new Point2D.Double(loc.getLon(), loc.getLat()); return currentTile.size.contains(point) ? currentTile : getRoot(); } } // where to go to for further checking? Tile<AggNode> subTile = currentTile.getSubTile(loc); if (subTile == null) { return currentTile; } currentTile = subTile; } } public boolean isEmpty() { return agg == null || agg.getDataSource() == null || agg.getDataSource().list(FileHandler.gpxFilter) == null || agg.getDataSource().list(FileHandler.gpxFilter).length == 0; } public Tile<AggNode> getRoot() { return root; } /** * Get a node when having only its ID. This method first determines in which * tile the node would have to be and then searches that tile. * * @param fullID * @return * @throws IOException * @throws SAXException * @throws ParserConfigurationException */ public AggNode getNodeByFullID(String fullID) throws ParserConfigurationException, SAXException, IOException { String[] parts = fullID.split(":"); GPSPoint searchPoint = new GPSPoint(Double.parseDouble(parts[0]), Double.parseDouble(parts[1])); Tile<AggNode> tile = getTile(searchPoint); dcs.tc.loadTile(tile); for (AggNode elem : tile.getElements()) { if (parts[2].equals(elem.getID())) { return elem; } } return null; } public Tile<AggNode> addElement(AggNode elem) { Tile<AggNode> targetTile = getTile(elem); if (targetTile == null) { return null; } try { dcs.getTc().loadTile(targetTile); } catch (ParserConfigurationException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } catch (SAXException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } targetTile.getElements().add(elem); nodeCounter++; // if (AggNode.class.isInstance(elem) && elem.getOut() != null) { // connCounter += elem.getOut().size(); // } // do we need to split? if (targetTile.getElemCount() > maxElementsPerTile && targetTile.size.getWidth() >= minimumSplitSize.getWidth() && targetTile.size.getHeight() >= minimumSplitSize.getHeight()) { targetTile.split(); } return targetTile; } private static final Logger LOG = Logger.getLogger(TileManager.class.getName()); public void removeElement(AggNode elem) { Tile<AggNode> targetTile = getTile(elem); targetTile.getElements().remove(elem); nodeCounter--; // do we need to merge? if (doMerge && targetTile.parent != null && targetTile.parent.getElemCount() < maxElementsPerTile * mergeFactor) { mergeTile(targetTile.parent); } } public void addConnection(AggConnection conn) { connCounter++; } public void removeConnection(AggConnection conn) { connCounter--; } public List<AggNode> clipRegionProjected(Rectangle2D.Double projectedRect) { GPSPoint topLeftProjNode = OsmProjection.cartesianToGps( projectedRect.getMinX(), projectedRect.getMinY()); GPSPoint bottomRightProjNode = OsmProjection.cartesianToGps( projectedRect.getMaxX(), projectedRect.getMaxY()); return clipRegion(new Rectangle2D.Double(topLeftProjNode.getLat(), topLeftProjNode.getLon(), bottomRightProjNode.getLat() - topLeftProjNode.getLat(), bottomRightProjNode.getLon() - topLeftProjNode.getLon())); } /** * Get all nodes in a specified region. * * @param gpsRect * @return */ public List<AggNode> clipRegion(Rectangle2D.Double gpsRect) { Queue<Tile<AggNode>> tileQueue = new LinkedList<Tile<AggNode>>(); tileQueue.add(getRoot()); Tile<AggNode> currentTile; List<AggNode> result = new ArrayList<AggNode>(maxElementsPerTile); while (tileQueue.size() > 0) { currentTile = tileQueue.poll(); if (!currentTile.isLeaf) { Rectangle2D.Double overlap = new Rectangle2D.Double(); for (Tile<AggNode> subTile : currentTile.getChildren()) { Rectangle2D.Double.intersect(gpsRect, currentTile.size, overlap); // full overlap? if (overlap.equals(currentTile.size)) { try { dcs.getTc().loadTile(subTile); } catch (ParserConfigurationException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } catch (SAXException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } result.addAll(subTile.getInnerNodes()); } else // partial overlap? if (overlap.getWidth() > 0) { try { dcs.getTc().loadTile(subTile); } catch (ParserConfigurationException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } catch (SAXException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage(), e); } tileQueue.add(subTile); } else // no overlap { // nothing ;) } } } else { Rectangle2D.Double overlap = new Rectangle2D.Double(); Rectangle2D.Double .intersect(gpsRect, currentTile.size, overlap); // full overlap? if (overlap.equals(currentTile.size)) { result.addAll(currentTile.getElements()); } else // partial overlap? if (overlap.getWidth() > 0) { // check all points for (AggNode elem : currentTile.getElements()) { if (gpsRect.contains(new Point2D.Double(elem.getLat(), elem.getLon()))) { result.add(elem); } } } } } return result; } public void mergeTile(Tile<AggNode> tile) { tile.getElements().clear(); tile.getElements().addAll(tile.getInnerNodes()); tile.isLeaf = true; tile.getChildren().clear(); } /** * Get all nodes which are close to one node. Attention: This method may be * a little inaccurate for performance reasons. * * @param loc * @param maxDist * @return */ public Set<AggNode> getCloseElements(ILocation loc, double maxDist) { Set<AggNode> result = new HashSet<AggNode>(10); Set<AggNode> candidates = new HashSet<AggNode>(10); // drill down... Tile<AggNode> currentTile = getTile(loc); // point searched was outside the world or otherwise inaccessible if (currentTile == null) { return result; } // and search up! while (true) { // TODO this can be optimized by taking directions into account, but // does it help that much? Rectangle2D.Double size = currentTile.getSize(); double minBorderDist = Double.MAX_VALUE; double maxBorderDist = 0; ILocation top = new GPSPoint(size.getMinY(), loc.getLon()); ILocation bottom = new GPSPoint(size.getMaxY(), loc.getLon()); ILocation left = new GPSPoint(loc.getLat(), size.getMinX()); ILocation right = new GPSPoint(loc.getLat(), size.getMaxX()); double[] borderDistances = new double[]{ GPSCalc.getDistance(loc, top), GPSCalc.getDistance(loc, bottom), GPSCalc.getDistance(loc, right), GPSCalc.getDistance(loc, left)}; for (double dist : borderDistances) { minBorderDist = Math.min(minBorderDist, dist); maxBorderDist = Math.max(maxBorderDist, dist); } double minCornerDist = Double.MAX_VALUE; double maxCornerDist = 0; ILocation topLeft = new GPSPoint(size.getMinY(), size.getMinX()); ILocation bottomLeft = new GPSPoint(size.getMinY(), size.getMaxX()); ILocation topRight = new GPSPoint(size.getMaxY(), size.getMinX()); ILocation bottomRight = new GPSPoint(size.getMaxY(), size.getMaxX()); double[] cornerDistances = new double[]{ GPSCalc.getDistance(loc, topLeft), GPSCalc.getDistance(loc, bottomLeft), GPSCalc.getDistance(loc, topRight), GPSCalc.getDistance(loc, bottomRight)}; for (double dist : cornerDistances) { minCornerDist = Math.min(minCornerDist, dist); maxCornerDist = Math.max(maxCornerDist, dist); } // all borders are further -> no more interesting points if (minCornerDist > maxDist) { candidates.addAll(currentTile.getInnerNodes()); break; } else // all borders closer than maxDist -> all points good if (maxBorderDist < maxDist) { // result.addAll(currentTile.getInnerNodes()); } else { candidates.addAll(currentTile.getInnerNodes()); } currentTile = currentTile.parent; if (currentTile == null) { break; } } // closely check all candidates for (AggNode candidateElem : candidates) { if (!result.contains(candidateElem) && candidateElem != loc) { double calcDist = GPSCalc.getDistance(candidateElem, loc); if (calcDist < maxDist) { result.add(candidateElem); } } } return result; } public int getNodeCount() { return nodeCounter; } @Override public String toString() { return root.toString(); } public String toDebugString() { return root.toDebugString(); } public final void clear() { nodeCounter = 0; connCounter = 0; dcs.getTc().clear(); root = new Tile<AggNode>(this, dcs.getTc(), null, WORLD); root.setID("0"); if (dcs.getTc().isInMemory()) { try { dcs.getTc().loadTile(root); } catch (ParserConfigurationException e) { } catch (SAXException e) { } catch (IOException e) { } } } public int getConnectionCount() { return connCounter; } public void setAggContainer(AggContainer agg) { this.agg = agg; } public List<Tile<AggNode>> clipTilesProjected(Rectangle2D.Double clipArea) { GPSPoint topLeftProjNode = OsmProjection.cartesianToGps( clipArea.getMinX(), clipArea.getMinY()); GPSPoint bottomRightProjNode = OsmProjection.cartesianToGps( clipArea.getMaxX(), clipArea.getMaxY()); return clipTiles(new Rectangle2D.Double(topLeftProjNode.getLat(), topLeftProjNode.getLon(), bottomRightProjNode.getLat() - topLeftProjNode.getLat(), bottomRightProjNode.getLon() - topLeftProjNode.getLon())); } /** * Get all tiles overlapping an area. * * @param clipArea * @return */ public List<Tile<AggNode>> clipTiles(Rectangle2D.Double clipArea) { List<Tile<AggNode>> result = new ArrayList<Tile<AggNode>>(); Queue<Tile<AggNode>> tileQueue = new LinkedList<Tile<AggNode>>(); tileQueue.add(getRoot()); Tile<AggNode> currentTile; while (tileQueue.size() > 0) { currentTile = tileQueue.poll(); if (currentTile.isLeaf) { result.add(currentTile); } else { Rectangle2D.Double overlap = new Rectangle2D.Double(); for (Tile<AggNode> subTile : currentTile.getChildren()) { Rectangle2D.Double.intersect(clipArea, subTile.size, overlap); // full overlap? if (overlap.equals(subTile.size)) { result.add(subTile); } else if (overlap.width > 0 && overlap.height > 0) { // partial overlap tileQueue.add(subTile); } else { // no overlap // do nothing! } } } } return result; } public Set<AggConnection> getCloseConnections( IEdge<? extends ILocation> edge, double maxDist) { Set<AggNode> setFrom = this.getCloseElements(edge.getFrom(), maxDist); Set<AggNode> setTo = this.getCloseElements(edge.getTo(), maxDist); Set<AggConnection> setClose = new HashSet<AggConnection>(); Iterator<AggNode> itFrom = setFrom.iterator(); while (itFrom.hasNext()) { AggNode from = itFrom.next(); Iterator<AggConnection> itOut = from.getOut().iterator(); while (itOut.hasNext()) { AggConnection conn = itOut.next(); if (setTo.contains(conn.getTo())) { setClose.add(conn); } } } return setClose; } public void addConnectionCounter(int i) { connCounter += i; } }