/*
* Copyright (C) 2008 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* 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.
*
*
* Author: Robert Vollmert
* Create date: 02-Dec-2008
*/
package uk.me.parabola.imgfmt.app.net;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
/**
* This is a component of the RoadNetwork.
*
* Keeps track of outside neighbours and allows subdivision
* to satisfy NOD1 constraints.
*
* The approach to subdivision is to tile the map into RouteCenters.
* One could imagine that overlapping RouteCenters would be an option,
* say by splitting largely independent networks (motorways, footways).
*
* Could be rolled into RouteCenter.
*/
public class NOD1Part {
private static final Logger log = Logger.getLogger(NOD1Part.class);
/*
* Constraints:
*
* 1. Nodes section smaller than about 0x4000, which gives
* a bound on the number of nodes.
* 2. At most 0x100 entries in Table A. This gives a bound
* on the number of (forward) arcs meeting this
* RouteCenter.
* 3. At most 0x40 entries in Table B. This gives a bound
* on the number of neighboring nodes.
* 4. Absolute values of coordinate offsets at most 0x8000,
* which translates to about 0.7 degrees, so bounding
* box should be at most 1.4 x 1.4 degrees assuming
* the reference is in the middle. (With small offsets,
* this would be 0.08 x 0.08 degrees.)
* 5. Absolute values of relative NOD1 offsets at most
* 0x2000, which limits the nodes section to 0x2000
* unless we take care to order the nodes nicely.
* 6. Distance between nodes and start of tables must
* fit in a char for writing Table C. So nodes
* section smaller than 0x10000.
*/
// maximal width and height of the bounding box, since
// NOD 1 coordinate offsets are at most 16 bit wide.
private static final int MAX_SIZE_UNSAFE = 1 << 16;
// private static final int MAX_SIZE = MAX_SIZE_UNSAFE / 2;
private static final int MAX_SIZE = MAX_SIZE_UNSAFE - 0x800;
// Table A has at most 0x100 entries
private static final int MAX_TABA_UNSAFE = 0x100;
// private static final int MAX_TABA = MAX_TABA_UNSAFE / 2;
private static final int MAX_TABA = MAX_TABA_UNSAFE - 0x8;
// Table B has at most 0x100 entries
private static final int MAX_TABB_UNSAFE = 0x100;
// private static final int MAX_TABB = MAX_TABB_UNSAFE / 2;
private static final int MAX_TABB = MAX_TABB_UNSAFE - 0x2;
// Nodes size is max 0x2000 to cope with signed 14 bit node offsets
private static final int MAX_NODES_SIZE = 0x2000;
private int nodesSize;
public class BBox {
int maxLat, minLat, maxLon, minLon;
boolean empty;
BBox() {
empty = true;
}
BBox(Coord co) {
empty = false;
int lat = co.getLatitude();
int lon = co.getLongitude();
minLat = lat;
maxLat = lat+1;
minLon = lon;
maxLon = lon+1;
}
BBox(int minLat, int maxLat, int minLon, int maxLon) {
empty = false;
this.minLat = minLat;
this.maxLat = maxLat;
this.minLon = minLon;
this.maxLon = maxLon;
}
Area toArea() {
return new Area(minLat, minLon, maxLat, maxLon);
}
boolean contains(BBox bbox) {
return minLat <= bbox.minLat && bbox.maxLat <= maxLat
&& minLon <= bbox.minLon && bbox.maxLon <= maxLon;
}
boolean contains(Coord co) {
return contains(new BBox(co));
}
void extend(BBox bbox) {
if (bbox.empty)
return;
if (empty) {
empty = false;
minLat = bbox.minLat;
maxLat = bbox.maxLat;
minLon = bbox.minLon;
maxLon = bbox.maxLon;
} else {
minLat = Math.min(minLat, bbox.minLat);
maxLat = Math.max(maxLat, bbox.maxLat);
minLon = Math.min(minLon, bbox.minLon);
maxLon = Math.max(maxLon, bbox.maxLon);
}
}
void extend(Coord co) {
extend(new BBox(co));
}
BBox[] splitLat() {
BBox[] ret = new BBox[2];
int midLat = (minLat + maxLat) / 2;
ret[0] = new BBox(minLat, midLat, minLon, maxLon);
ret[1] = new BBox(midLat, maxLat, minLon, maxLon);
return ret;
}
BBox[] splitLon() {
BBox[] ret = new BBox[2];
int midLon = (minLon + maxLon) / 2;
ret[0] = new BBox(minLat, maxLat, minLon, midLon);
ret[1] = new BBox(minLat, maxLat, midLon, maxLon);
return ret;
}
int getWidth() {
return maxLon - minLon;
}
int getHeight() {
return maxLat - minLat;
}
int getMaxDimension() {
return Math.max(getWidth(), getHeight());
}
public String toString() {
return "BBox[" + new Coord(minLat,minLon).toDegreeString()
+ ", " + new Coord(maxLat,maxLon).toDegreeString() + "]";
}
}
// The area we are supposed to cover.
private final BBox bbox;
// The area that actually has nodes.
private final BBox bboxActual = new BBox();
private List<RouteNode> nodes = new ArrayList<RouteNode>();
private TableA tabA = new TableA();
private Map<RouteNode,RouteNode> destNodes = new LinkedHashMap<RouteNode, RouteNode>();
/**
* Create an unbounded NOD1Part.
*
* All nodes will be accepted by addNode and
* all arcs will be considered internal.
*/
public NOD1Part() {
log.info("creating new unbounded NOD1Part");
this.bbox = null;
}
/**
* Create a bounded NOD1Part.
*
* The bounding box is used to decide which arcs
* are internal.
*/
private NOD1Part(BBox bbox) {
log.info("creating new NOD1Part:", bbox);
this.bbox = bbox;
}
/**
* Add a node to this part.
*
* The node is used to populate the tables. If an
* arc points outside the bbox, we know it's not
* an internal arc. It might still turn into an
* external arc at a deeper level of recursion.
*/
public void addNode(RouteNode node) {
assert bbox == null || bbox.contains(node.getCoord())
: "trying to add out-of-bounds node: " + node;
bboxActual.extend(node.getCoord());
nodes.add(node);
for (RouteArc arc : node.arcsIteration()) {
tabA.addArc(arc);
RouteNode dest = arc.getDest();
if (arc.isInternal() == false){
destNodes.put(dest, dest);
}
else if (bbox != null && !bbox.contains(dest.getCoord()) || dest.getGroup() != node.getGroup()) {
arc.setInternal(false);
destNodes.put(dest, dest);
}
}
for (RouteRestriction rr: node.getRestrictions()){
List<RouteArc> arcs = rr.getArcs();
if (arcs.size() >= 3){
for (int i = 0; i < arcs.size(); i++){
RouteArc arc = arcs.get(i);
if (arc.getSource() != node){
tabA.addArc(arc);
RouteNode dest = arc.getDest();
if (arc.isInternal() == false)
destNodes.put(dest, dest);
else if (bbox != null && !bbox.contains(dest.getCoord()) || dest.getGroup() != node.getGroup()) {
arc.setInternal(false);
destNodes.put(dest, dest);
}
}
}
}
}
nodesSize += node.boundSize();
}
/**
* Subdivide this part recursively until it satisfies the constraints.
*/
public List<RouteCenter> subdivide() {
return subdivideHelper(0);
}
/**
* Subdivide this part recursively until it satisfies the constraints.
*/
protected List<RouteCenter> subdivideHelper(int depth) {
List<RouteCenter> centers = new LinkedList<RouteCenter>();
if (satisfiesConstraints()) {
centers.add(this.toRouteCenter());
return centers;
}
if(depth > 48) {
log.error("Region contains too many nodes/arcs (discarding " + nodes.size() + " nodes to be able to continue)");
log.error(" Expect the routing to be broken near " + bbox);
for (RouteNode node : nodes)
node.discard();
return centers;
}
log.info("subdividing", bbox, bboxActual);
BBox[] split ;
if (bboxActual.getWidth() > bboxActual.getHeight())
split = bboxActual.splitLon();
else
split = bboxActual.splitLat();
NOD1Part[] parts = new NOD1Part[2];
for (int i = 0; i < split.length; i++)
parts[i] = new NOD1Part(split[i]);
for (RouteNode node : nodes) {
int i = 0;
while (!split[i].contains(node.getCoord()))
i++;
parts[i].addNode(node);
}
this.tabA = null;
this.destNodes = null;
this.nodes = null;
for (NOD1Part part : parts)
if(!part.bboxActual.empty)
centers.addAll(part.subdivideHelper(depth + 1));
return centers;
}
private boolean satisfiesConstraints() {
log.debug("constraints:", bboxActual, tabA.size(), destNodes.size(), nodesSize);
return bboxActual.getMaxDimension() < MAX_SIZE
&& tabA.size() < MAX_TABA
&& destNodes.size() < MAX_TABB
&& nodesSize < MAX_NODES_SIZE;
}
/**
* Convert to a RouteCenter.
*
* satisfiesConstraints() should be true for this to
* be a legal RouteCenter.
*/
private RouteCenter toRouteCenter() {
Collections.sort(nodes, new Comparator<RouteNode>() {
public int compare(RouteNode n1, RouteNode n2) {
return n1.getCoord().compareTo(n2.getCoord());
}
});
TableB tabB = new TableB();
for (RouteNode rn : destNodes.keySet())
tabB.addNode(rn);
return new RouteCenter(bboxActual.toArea(), nodes, tabA, tabB);
}
}