/**
* *****************************************************************************
* 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;
import de.fub.agg2graph.agg.tiling.CachingStrategyFactory;
import de.fub.agg2graph.agg.tiling.DefaultCachingStrategy;
import de.fub.agg2graph.agg.tiling.ICachingStrategy;
import de.fub.agg2graph.agg.tiling.TileManager;
import de.fub.agg2graph.input.GPXReader;
import de.fub.agg2graph.structs.CartesianCalc;
import de.fub.agg2graph.structs.GPSSegment;
import de.fub.agg2graph.structs.ILocation;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/**
* Container class for an aggregation graph. Contains a number of
* {@link AggNode}s and {@link AggConnection}s. Can be serialized to disk with
* an {@link ICachingStrategy}. Merging is done using an
* {@link IAggregationStrategy}.
*
* @author Johannes Mitlmeier
*/
public class AggContainer {
private static final Logger LOG = Logger.getLogger(AggContainer.class.getName());
private File sourceFolder = null;
private IAggregationStrategy aggregationStrategy;
private ICachingStrategy cachingStrategy;
private AggContainer() {
}
/**
* Preferred way to construct an AggContainer specifying all its important
* features.
*
* @param sourceFolder
* @param aggStrategy
* @param cachingStrategy
* @return
*/
public static AggContainer createContainer(File sourceFolder,
IAggregationStrategy aggStrategy, ICachingStrategy cachingStrategy) {
AggContainer agg = new AggContainer();
if (aggStrategy != null) {
agg.setAggregationStrategy(aggStrategy);
}
if (cachingStrategy != null) {
agg.setCachingStrategy(cachingStrategy);
}
agg.setDataSource(sourceFolder);
return agg;
}
public static AggContainer createContainer(File sourceFolder) {
return createContainer(sourceFolder,
AggregationStrategyFactory.getObject(),
CachingStrategyFactory.getObject());
}
public void setDataSource(File sourceFolder) {
if (sourceFolder == null) {
return;
}
this.sourceFolder = sourceFolder;
}
public File getDataSource() {
return sourceFolder;
}
/**
* Call this method to merge a .gpx file to the AggContainer using the
* current {@link IAggregationStrategy}.
*
* @param gpxFile
*/
public void addFile(File gpxFile) {
List<GPSSegment> segments = GPXReader.getSegments(gpxFile);
if (segments != null && !segments.isEmpty()) {
for (int index = 0; index < segments.size(); index++) {
if (index < segments.size() - 1) {
addSegment(segments.get(index), true);
} else {
addSegment(segments.get(index), false);
}
}
}
}
/**
* This method merges a {@link GPSSegment} into the container.
*
* @param segment
* @param isAgg
*/
public void addSegment(GPSSegment segment, boolean isAgg) {
getAggregationStrategy().aggregate(segment, isAgg);
}
public void addNode(AggNode node) {
node.setAggContainer(this);
if (getCachingStrategy() != null) {
getCachingStrategy().addNode(node);
}
}
public void addConnection(AggConnection conn) {
if (conn == null
|| conn.getFrom() == null
|| (conn.getFrom().getOut() != null && conn.getFrom().getOut()
.contains(conn))) {
return;
}
conn.setAggContainer(this);
getCachingStrategy().addConnection(conn);
}
public void save() {
getCachingStrategy().save();
}
public void removeConnection(AggConnection conn) {
getCachingStrategy().removeConnection(conn);
}
/**
* Removes all incoming and outgoing {@link AggConnection}s before
* ultimately removing the specified node itself.
*
* @param node
*/
public void deleteNode(AggNode node) {
// get all connections and remove them first
ArrayList<AggConnection> connections = new ArrayList<AggConnection>();
connections.addAll(node.getOut());
for (AggConnection conn : connections) {
getCachingStrategy().removeConnection(conn);
}
connections.clear();
connections.addAll(node.getIn());
for (AggConnection conn : connections) {
getCachingStrategy().removeConnection(conn);
}
getCachingStrategy().removeNode(node);
}
public void moveNodeTo(AggNode target, ILocation newPos) {
target.setLatLon(newPos.getLatLon());
}
/**
* Removes the onde only without touching its {@link AggConnection}s.
*
* @param node
*/
public void removeNodeSilently(AggNode node) {
getCachingStrategy().removeNode(node);
}
/**
* Removes an {@link AggNode} while preserving and reconnecting the
* {@link AggConnection}s it was part of. Every ingoing connection is merged
* with every outgoing one using
* {@link IAggregationStrategy#combineConnections(AggConnection, AggConnection)}
*
* @param node
*/
public void extractNode(AggNode node) {
for (AggConnection inConn : new ArrayList<AggConnection>(node.getIn())) {
for (AggConnection outConn : new ArrayList<AggConnection>(
node.getOut())) {
if (!inConn.getFrom().equals(outConn.getTo())) {
if (inConn.getFrom().getConnectionTo(outConn.getTo()) == null) {
getCachingStrategy().addConnection(getAggregationStrategy()
.combineConnections(inConn, outConn));
}
}
}
}
// remove old things
for (AggConnection inConn : new ArrayList<AggConnection>(node.getIn())) {
getCachingStrategy().removeConnection(inConn);
}
for (AggConnection outConn : new ArrayList<AggConnection>(node.getOut())) {
getCachingStrategy().removeConnection(outConn);
}
getCachingStrategy().removeNode(node);
}
/**
* Split an {@link AggConnection} into a given number of sub-connections of
* equal length.
*
* @param conn
* @param numParts
* @return
*/
public List<AggConnection> splitConnection(AggConnection conn, int numParts) {
// System.out.println("called");
List<AggNode> nodes = new ArrayList<AggNode>(5);
List<AggConnection> result = new ArrayList<AggConnection>(Math.max(1,
numParts));
if (conn == null) {
return null;
}
AggNode from = conn.getFrom();
AggNode to = conn.getTo();
nodes.add(from);
if (numParts > 1) {
double latDiff = (to.getLat() - from.getLat()) / numParts;
double lonDiff = (to.getLon() - from.getLon()) / numParts;
AggNode lastNode = from, newNode;
for (int i = 1; i < numParts; i++) {
newNode = new AggNode(
from.getID() + "-" + i + "-" + to.getID(),
from.getLat() + i * latDiff, from.getLon() + i
* lonDiff, this);
// add node
// System.out.println(getCachingStrategy().getNodeCount());
// System.out.println(getCachingStrategy().getConnectionCount());
insertNode(newNode, lastNode.getConnectionTo(to));
// System.out.println(getCachingStrategy().getNodeCount());
// System.out.println(getCachingStrategy().getConnectionCount());
nodes.add(newNode);
lastNode = newNode;
}
}
nodes.add(to);
// make connection list
result = new ArrayList<AggConnection>(nodes.size() - 1);
for (int i = 1; i < nodes.size(); i++) {
// System.out.println("adding conn "
// + nodes.get(i - 1).getConnectionTo(nodes.get(i)));
result.add(nodes.get(i - 1).getConnectionTo(nodes.get(i)));
}
return result;
}
public IAggregationStrategy getAggregationStrategy() {
return aggregationStrategy;
}
public void setAggregationStrategy(IAggregationStrategy aggregationStrategy) {
this.aggregationStrategy = aggregationStrategy;
if (aggregationStrategy != null) {
aggregationStrategy.setAggContainer(AggContainer.this);
}
}
public ICachingStrategy getCachingStrategy() {
return cachingStrategy;
}
public void setCachingStrategy(ICachingStrategy cachingStrategy) {
this.cachingStrategy = cachingStrategy;
if (cachingStrategy != null) {
cachingStrategy.clear();
cachingStrategy.setAggContainer(AggContainer.this);
}
}
@Override
public String toString() {
return toStringHelper(false);
}
/**
* Get a string representation of this {@link AggContainer} as seen by later
* steps in the process.
*
* @return
*/
public String toStringVisible() {
return toStringHelper(true);
}
private String toStringHelper(boolean onlyVisible) {
StringBuilder sb = new StringBuilder("AggContainer: [\n");
if (getCachingStrategy().getNodeCount() > 100) {
sb.append(String.format("%d active nodes\n",
getCachingStrategy().getNodeCount()));
} else {
for (AggNode node : getCachingStrategy().clipRegion(TileManager.WORLD)) {
if (!node.isVisible()) {
continue;
}
sb.append("\t").append(node);
sb.append("\n\t\tin: ");
for (AggConnection inConn : node.getIn()) {
if (!inConn.isVisible()) {
continue;
}
AggNode inNode = inConn.getFrom();
sb.append(inNode).append(" ");
}
sb.append("\n\t\tout: ");
for (AggConnection outConn : node.getOut()) {
if (!outConn.isVisible()) {
continue;
}
AggNode outNode = outConn.getTo();
sb.append(outNode).append(
outConn.isComplete() ? " " : " (<- shallow) ");
}
sb.append("\n");
}
}
return sb.append("]").toString();
}
public String toDebugString() {
StringBuilder sb = new StringBuilder("AggContainer (Debug): [\n");
if (getCachingStrategy().getNodeCount() > 100) {
sb.append(String.format("%d active nodes\n",
getCachingStrategy().getNodeCount()));
} else {
for (AggNode node : getCachingStrategy().clipRegion(TileManager.WORLD)) {
sb.append("\t").append(node.toDebugString());
sb.append("\n\t\tin: ");
for (AggConnection inConn : node.getIn()) {
AggNode inNode = inConn.getFrom();
sb.append(inNode.toDebugString()).append(" ");
}
sb.append("\n\t\tout: ");
for (AggConnection outConn : node.getOut()) {
AggNode outNode = outConn.getTo();
sb.append(outNode.toDebugString()).append(
outConn.isComplete() ? " " : " (<- shallow) ");
}
sb.append("\n");
}
}
return sb.append("]").toString();
}
/**
* Merges two nodes by moving all connections that lead to or originated
* from the mergeSource to the mergeTarget.
*
* @param mergeSource
* @param mergeTarget
*/
public void mergeNodes(AggNode mergeSource, AggNode mergeTarget) {
List<AggConnection> connections = new ArrayList<AggConnection>();
connections.addAll(mergeSource.getIn());
for (AggConnection conn : connections) {
if (!conn.getFrom().equals(mergeTarget)) {
conn.setTo(mergeTarget);
} else {
getCachingStrategy().removeConnection(conn);
}
}
connections.clear();
connections.addAll(mergeSource.getOut());
for (AggConnection conn : connections) {
if (!conn.getTo().equals(mergeTarget)) {
conn.setFrom(mergeTarget);
} else {
getCachingStrategy().removeConnection(conn);
}
}
// remove source
getCachingStrategy().removeNode(mergeSource);
}
/**
* Add new {@link AggNode}s between two other ones. Make sure the nodes are
* added somewhat in order to prevent artifacts.
*
* @param before
* @param after
* @param newNodes
* @return
*/
public List<AggConnection> insertNodesOrdered(final AggNode before,
AggNode after, List<AggNode> newNodes) {
// TODO handle null for before or after
// sort by distance to before
if (newNodes != null) {
Collections.sort(newNodes, new Comparator<AggNode>() {
@Override
public int compare(AggNode o1, AggNode o2) {
return (int) Math.signum(CartesianCalc
.getDistancePointToPoint(o1, before)
- CartesianCalc.getDistancePointToPoint(o2, before));
}
});
}
// add the nodes to the edge
AggNode lastNode = before;
List<AggConnection> result = new ArrayList<AggConnection>();
// no new connection?
if (newNodes == null || newNodes.isEmpty()) {
result.add(before.getConnectionTo(after));
return result;
}
for (AggNode node : newNodes) {
insertNode(node, lastNode, after);
result.add(lastNode.getConnectionTo(node));
lastNode = node;
}
result.add(lastNode.getConnectionTo(after));
return result;
}
/**
* Add a new {@link AggNode} on an existing {@link AggConnection}.
*
* @param newNode
* @param conn
*/
public void insertNode(AggNode newNode, AggConnection conn) {
addNode(newNode);
insertNodeWithoutAdding(newNode, conn);
}
private void insertNodeWithoutAdding(AggNode newNode, AggConnection conn) {
AggConnection newConn1 = connect(conn.getFrom(), newNode);
AggConnection newConn2 = connect(newNode, conn.getTo());
newConn1.inheritPropertiesFrom(conn);
newConn2.inheritPropertiesFrom(conn);
removeConnection(conn);
}
/**
* Add a new {@link AggNode} between two existing nodes, taking both
* directions into account.
*
* @param newNode
* @param a
* @param b
*/
public void insertNode(AggNode newNode, AggNode a, AggNode b) {
addNode(newNode);
AggConnection conn = a.getConnectionTo(b);
if (conn != null) {
insertNodeWithoutAdding(newNode, conn);
}
conn = b.getConnectionTo(a);
if (conn != null) {
insertNodeWithoutAdding(newNode, conn);
}
}
/**
* Remove everything from the container.
*/
public void clear() {
if (getCachingStrategy() != null) {
getCachingStrategy().clear();
getAggregationStrategy().clear();
}
}
/**
* Make a new {@link AggConnection} between two {@link AggNode}s. If a
* connection already exists, it is returned.
*
* @param from
* @param to
* @return
*/
public AggConnection connect(AggNode from, AggNode to) {
if (from == null || to == null) {
return null;
}
// find already existing connection with same endpoints
AggConnection existingConn = findConn(from, to);
if (existingConn != null) {
// add nodes
if (ShallowAggNode.class
.isInstance(existingConn.getFrom())
&& !ShallowAggNode.class
.isInstance(from)) {
existingConn.fillFrom(from);
}
if (ShallowAggNode.class
.isInstance(existingConn.getTo())
&& !ShallowAggNode.class
.isInstance(to)) {
existingConn.fillTo(to);
}
return existingConn;
} else {
AggConnection conn = new AggConnection(from, to, this);
if (getCachingStrategy() != null) {
getCachingStrategy().addConnection(conn);
}
return conn;
}
}
/**
* Find an existing {@link AggConnection} between two {@link AggNode}s.
*
* @param from
* @param to
* @return
*/
private AggConnection findConn(AggNode from, AggNode to) {
if (from.isShallow()) {
AggNode fromAggNode;
try {
fromAggNode = ((DefaultCachingStrategy) getCachingStrategy())
.getTm().getNodeByFullID(from.getInternalID());
if (fromAggNode != null && fromAggNode.getOut() != null) {
for (AggConnection conn : fromAggNode.getOut()) {
if (conn.getFrom().equals(from)
&& conn.getTo().equals(to)) {
return conn;
}
}
}
} catch (ParserConfigurationException e) {
LOG.log(Level.INFO, e.getMessage(), e);
} catch (SAXException e) {
LOG.log(Level.INFO, e.getMessage(), e);
} catch (IOException e) {
LOG.log(Level.INFO, e.getMessage(), e);
}
} else if (to.isShallow()) {
try {
AggNode toAggNode = ((DefaultCachingStrategy) getCachingStrategy())
.getTm().getNodeByFullID(to.getInternalID());
if (toAggNode != null && toAggNode.getIn() != null) {
for (AggConnection conn : toAggNode.getIn()) {
if (conn.getFrom().equals(from)
&& conn.getTo().equals(to)) {
return conn;
}
}
}
} catch (ParserConfigurationException e) {
LOG.log(Level.INFO, e.getMessage(), e);
} catch (SAXException e) {
LOG.log(Level.INFO, e.getMessage(), e);
} catch (IOException e) {
LOG.log(Level.INFO, e.getMessage(), e);
}
}
return null;
}
}