/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License 3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * ******************************************************************************/ package com.opendoorlogistics.core.geometry.rog.builder; import java.awt.Dimension; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D.Double; import java.util.ArrayList; import java.util.List; import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactoryInfo; import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.util.GeoUtil; import com.opendoorlogistics.core.geometry.rog.builder.ROGBuilder.PendingWrite; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.io.WKBReader; public class QuadBlockBuilder { public static final int MIN_SIZE_PIXELS = 256; public static final int MIN_SIZE_BYTES = 64 * 1024; public static final int MAX_SIZE_BYTES = 512 * 1024; public QuadBlock build(Iterable<PendingWrite> writes, TileFactoryInfo info, final int zoom){ // calculate all centroids WKBReader reader = new WKBReader(); for(PendingWrite pw:writes){ try { Geometry g = reader.read(pw.geomBytes); pw.centroid= g.getCentroid(); } catch (Exception e) { throw new RuntimeException(e); } } // get bounds Rectangle2D.Double mapBounds=null; if(zoom<0){ // WGS84 mapBounds = new Rectangle2D.Double(-180, -90, 360, 180); }else{ // Map tile Dimension dim = GeoUtil.getMapSize(zoom, info); double h = (double)dim.height * info.getTileSize(zoom); double w = (double)dim.width * info.getTileSize(zoom); Point2D centre = info.getMapCenterInPixelsAtZoom(zoom); mapBounds = new Rectangle2D.Double(centre.getX() - w/2, centre.getY() - h/2, w, h); } QuadBlock root = new QuadBlock(mapBounds); SplitRule splitRule = new SplitRule() { @Override public boolean isSplit(QuadBlock block, PendingWrite newPending) { if(zoom>=0){ // don't split beyond the minimum on-screen size as we will typically want to load this all at once if(block.bounds.getWidth() / 2 < MIN_SIZE_PIXELS || block.bounds.getHeight() < MIN_SIZE_PIXELS){ return false; } } // never split an empty leaf if(block.getNbLeaves()==0){ return false; } // don't split if block will still be under the minimum size in uncompressed bytes long newNbBytes = block.leafBytesCount + newPending.geomBytes.length; if(newNbBytes < MIN_SIZE_BYTES){ return false; } // split if new size is over max limit if(newNbBytes > MAX_SIZE_BYTES){ return true; } return false; } }; // add all for(PendingWrite write:writes){ root.addWrite(write, splitRule); } // System.out.println("Built zoom " + zoom + " quadtree: " + root.getSummary()); return root; } static interface SplitRule{ boolean isSplit(QuadBlock block, PendingWrite newPending); } static class QuadBlock{ private final Rectangle2D.Double bounds; private QuadBlock [] children; private List<PendingWrite> leaves; private long leafBytesCount; private byte[] bjson; public QuadBlock(Double bounds) { this.bounds = bounds; } public List<PendingWrite> getLeaves(){ return leaves; } public long getLeavesBytesCount(){ return leafBytesCount; } public int getNbChildren(){ return children!=null?children.length:0; } public QuadBlock getChild(int i){ return children[i]; } public byte[] getBjson() { return bjson; } public void setBjson(byte[] bjson) { this.bjson = bjson; } public void split(SplitRule splitRule){ children = new QuadBlock[4]; double hw = bounds.width * 0.5; double hh = bounds.height * 0.5; int indx=0; for(int ix = 0 ; ix<=1 ; ix++){ for(int iy = 0 ; iy<=1 ; iy++){ children[indx++] = new QuadBlock(new Rectangle2D.Double(bounds.x + ix*hw, bounds.y + iy*hh, hw, hh)); } } // add to children for(PendingWrite pw:leaves){ findChild(pw).addWrite(pw, splitRule); } leaves = null; leafBytesCount=0; } @Override public String toString(){ StringBuilder builder = new StringBuilder(); toString(builder,0); return builder.toString(); } private void toString(StringBuilder builder, int depth){ String base = getSummary(); for(int i =0 ; i < depth ; i++){ builder.append(" "); } builder.append(base); builder.append(System.lineSeparator()); for(int i =0 ; i<getNbChildren();i++){ getChild(i).toString(builder, depth+1); } } public String getSummary() { String base= "[x=" + bounds.x + ",y=" + bounds.y + ",w=" + bounds.width + ",h=" + bounds.height + ",objs=" + getNbLeaves()+ ",bytes=" + leafBytesCount+ ",total_blocks=" + getTotalNbBlocks()+ ",total_objs=" + getTotalNbLeaves()+ ",total_bytes=" + getTotalNbBytes()+ ",split=" + (children!=null?"T":"F") + "]"; return base; } public int getNbLeaves(){ return leaves!=null ? leaves.size():0; } public int getTotalNbBlocks(){ int sum=1; for(int i =0 ; i<getNbChildren();i++){ sum += getChild(i).getTotalNbBlocks(); } return sum; } public long getTotalNbBytes(){ long sum=getLeavesBytesCount(); for(int i =0 ; i<getNbChildren();i++){ sum += getChild(i).getTotalNbBytes(); } return sum; } public int getTotalNbLeaves(){ int sum=getNbLeaves(); for(int i =0 ; i<getNbChildren();i++){ sum += getChild(i).getTotalNbLeaves(); } return sum; } public void addWrite(PendingWrite write, SplitRule splitRule){ // test whether we should split if(children == null && splitRule.isSplit(this, write)){ split(splitRule); } if(children!=null){ findChild(write).addWrite(write,splitRule); }else{ // add to current level if(leaves == null){ leaves = new ArrayList<PendingWrite>(1); } leaves.add(write); leafBytesCount+=write.geomBytes.length; } } /** * @param write * @return */ private QuadBlock findChild(PendingWrite write) { QuadBlock found=null; for(QuadBlock child : children){ if(child.bounds.contains(write.centroid.getX(), write.centroid.getY())){ found = child; break; } } // Very unlikely edge case where because of rounding error or similar // the child has not been assigned to any block. Take nearest and print error. if(found==null){ double nearestDistSqd = java.lang.Double.POSITIVE_INFINITY; for(QuadBlock child : children){ Point2D.Double centre = new Point2D.Double(child.bounds.getCenterX(), child.bounds.getCenterY()); double distSqd = centre.distanceSq(write.centroid.getX(), write.centroid.getY()); if(distSqd < nearestDistSqd){ nearestDistSqd = distSqd; found = child; } } // System.out.println("Warning: geom " + write.index.rowNb + " doesn't fit inside any quadblock; assigning it to nearest"); } return found; } } }