OsmEntityProcessor.java
package nl.tudelft.simulation.dsol.animation.gis.osm;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.djutils.logger.CategoryLogger;
import nl.tudelft.simulation.dsol.animation.gis.FeatureInterface;
import nl.tudelft.simulation.dsol.animation.gis.FloatXY;
import nl.tudelft.simulation.dsol.animation.gis.transform.CoordinateTransform;
/**
* OsmEntityProcessor processes one read entity from the input file.
* <p>
* Copyright (c) 2021-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
* project is distributed under a three-clause BSD-style license, which can be found at
* <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
* </p>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
*/
public class OsmEntityProcessor
{
/** the nodes in the OSM file. */
private Map<Long, MiniNode> nodes = new HashMap<Long, MiniNode>();
/** the ways in the OSM file. */
private Map<Long, MiniWay> ways = new HashMap<Long, MiniWay>();
/** the relations in the OSM file. */
private Map<Long, MiniRelation> relations = new HashMap<Long, MiniRelation>();
/** the key - value pairs to read. There can be multiple values per key, or '*' for all. */
private final List<FeatureInterface> featuresToRead;
/** an optional transformation of the lat/lon (or other) coordinates. */
private final CoordinateTransform coordinateTransform;
/**
* Construct a sink to read the features form the OSM file. For OSM this the Feature list is typically the complete set of
* features that needs to be read. It is rare that multiple OSM files are available for the data, but this could be the case
* (e.g., state or country OSM files).<br>
* TODO: add an optional initial extent in case the source's extent is much larger than the extent we want to display
* @param featuresToRead the features that the sink needs to read.
* @param coordinateTransform the transformation of (x, y) coordinates to (x', y') coordinates.
*/
public OsmEntityProcessor(final List<FeatureInterface> featuresToRead, final CoordinateTransform coordinateTransform)
{
this.featuresToRead = featuresToRead;
this.coordinateTransform = coordinateTransform;
}
/**
* Process an entity that was just read.
* @param entity the node, way or other OSM entity
*/
public void process(final OsmEntity entity)
{
if (entity instanceof Node node)
{
MiniNode miniNode = new MiniNode(node.getId(), (float) node.getLat(), (float) node.getLon());
this.nodes.put(miniNode.id, miniNode);
/*-
Iterator<Tag> tagIterator = entity.getTags().iterator();
while (tagIterator.hasNext())
{
Tag tag = tagIterator.next();
String key = tag.getKey();
String value = tag.getValue();
// TODO: look whether we want to display special nodes.
}
*/
}
else if (entity instanceof Way way)
{
boolean read = false;
FeatureInterface featureToUse = null;
for (var entry : way.getTags().entrySet())
{
String key = entry.getKey();
String value = entry.getValue();
for (FeatureInterface feature : this.featuresToRead)
{
if (feature.getKey().equals("*"))
{
featureToUse = feature;
read = true;
break;
}
if (feature.getKey().equals(key))
{
if (feature.getValue().equals("*") || feature.getValue().equals(value))
{
featureToUse = feature;
read = true;
break;
}
}
}
if (read)
{
break;
}
}
// store all the ways since they can be used in a Relation. featureToUse can therefore be null.
List<MiniNode> wayNodes = new ArrayList<>();
for (long nodeId : way.getRefs())
{
MiniNode node = this.nodes.get(nodeId);
if (node == null)
continue;
wayNodes.add(node);
}
MiniWay miniWay = new MiniWay(way.getId(), featureToUse, wayNodes);
this.ways.put(miniWay.id, miniWay);
}
else if (entity instanceof Relation relation)
{
boolean read = false;
FeatureInterface featureToUse = null;
for (var entry : relation.getTags().entrySet())
{
String key = entry.getKey();
String value = entry.getValue();
for (FeatureInterface feature : this.featuresToRead)
{
if (feature.getKey().equals("*"))
{
featureToUse = feature;
read = true;
break;
}
if (feature.getKey().equals(key))
{
if (feature.getValue().equals("*") || feature.getValue().equals(value))
{
featureToUse = feature;
read = true;
break;
}
}
}
if (read)
{
break;
}
}
if (read)
{
// For now, just multipolygon
if ("multipolygon".equals(relation.getTags().get("type")) || "boundary".equals(relation.getTags().get("type")))
{
List<Long> outerWayIds = new ArrayList<>();
List<Long> innerWayIds = new ArrayList<>();
for (Relation.Member member : relation.getMembers())
{
if (member.type() != Relation.Type.WAY)
continue;
long wayId = member.ref();
if (!this.ways.containsKey(wayId))
{
continue;
}
if ("inner".equals(member.role()))
innerWayIds.add(wayId);
else
outerWayIds.add(wayId);
}
MiniRelation miniRelation = new MiniRelation(relation.getId(), featureToUse, outerWayIds, innerWayIds,
relation.getTags().get("name"));
this.relations.put(miniRelation.id, miniRelation);
}
}
}
}
/**
* Preprocessing of the data.
*/
public void initialize()
{
// Nothing to do right now
}
/**
* Postprocessing of the data.
*/
public void complete()
{
for (MiniWay way : this.ways.values())
{
if (way.feature != null) // can be null if only used in a relation
{
addWay(way);
}
}
for (MiniRelation relation : this.relations.values())
{
addRelation(relation);
}
CategoryLogger.always().info("Nodes read: {}, ways read: {}, relations read: {}", this.nodes.size(), this.ways.size(),
this.relations.size());
}
/**
* Add a way to a feature.
* @param way the way to add to the feature shape list
*/
private void addWay(final MiniWay way)
{
Path2D.Float path = new Path2D.Float(Path2D.WIND_NON_ZERO, way.wayNodesLat.length);
boolean start = false;
for (int i = 0; i < way.wayNodesLat.length; i++)
{
FloatXY coordinate;
if (way.wayNodesId[i] != 0)
{
MiniNode node = this.nodes.get(way.wayNodesId[i]);
coordinate = this.coordinateTransform.floatTransform(node.lon, node.lat);
}
else
{
coordinate = this.coordinateTransform.floatTransform(way.wayNodesLon[i], way.wayNodesLat[i]);
}
if (!start)
{
path.moveTo(coordinate.x(), coordinate.y());
start = true;
}
path.lineTo(coordinate.x(), coordinate.y());
}
way.feature.addShape(path);
}
/**
* Add a relation to a feature.
* @param relation the relation to add to the feature shape list
*/
private void addRelation(final MiniRelation relation)
{
List<Path2D> outerRings = makeRings(relation.outerWayIds, true);
List<Path2D> innerRings = makeRings(relation.innerWayIds, false);
Path2D.Float multipolygon = new Path2D.Float(Path2D.WIND_NON_ZERO);
for (Path2D outer : outerRings)
multipolygon.append(outer, false);
for (Path2D inner : innerRings)
multipolygon.append(inner, false);
relation.feature.addShape(multipolygon);
}
/**
* Make a list of inner or outer rings for the relation.
* @param wayIds the inner or outer wayIds to process
* @param outer to determine the winding
* @return a list of Path2D elements for inner or outer ways
*/
private List<Path2D> makeRings(final List<Long> wayIds, final boolean outer)
{
var rings = new ArrayList<Path2D>();
var partials = new ArrayList<List<FloatXY>>();
for (long wayId : wayIds)
{
MiniWay way = this.ways.get(wayId);
boolean started = false;
List<FloatXY> ring = new ArrayList<>();
FloatXY first = null;
FloatXY coordinate = null;
for (int i = 0; i < way.wayNodesLat.length; i++)
{
if (way.wayNodesId[i] != 0)
{
MiniNode node = this.nodes.get(way.wayNodesId[i]);
coordinate = this.coordinateTransform.floatTransform(node.lon, node.lat);
}
else
{
coordinate = this.coordinateTransform.floatTransform(way.wayNodesLon[i], way.wayNodesLat[i]);
}
if (!started)
{
first = coordinate;
started = true;
}
ring.add(coordinate);
}
// Sometimes rings are made from partial ways that still have to be stitched together
if (!first.equals(coordinate))
{
partials.add(ring);
continue;
}
Path2D path = makePath(first, ring, outer);
rings.add(path);
}
if (!partials.isEmpty())
stitch(partials, outer, rings);
return rings;
}
/**
* Test whether the ring is clockwise or anticlockwise.
* @param ring list of points
* @return clockwise or not
*/
protected boolean isClockwise(final List<FloatXY> ring)
{
double sum = 0;
for (int i = 0; i < ring.size() - 1; i++)
{
FloatXY p1 = ring.get(i);
FloatXY p2 = ring.get(i + 1);
sum += (p2.x() - p1.x()) * (p2.y() + p1.y());
}
return sum > 0;
}
/**
* Make a path from a ring of coordinates.
* @param first the first point
* @param ring the list of points
* @param outer outer ring or inner ring
* @return the path constructed from the points
*/
protected Path2D makePath(final FloatXY first, final List<FloatXY> ring, final boolean outer)
{
// Outer rings have to be clockwise; inner rings have to be anti-clockwise
if (outer)
{
if (!isClockwise(ring))
Collections.reverse(ring);
}
else
{
if (isClockwise(ring))
Collections.reverse(ring);
}
Path2D path = new Path2D.Float();
path.moveTo(first.x(), first.y());
boolean started = false;
for (var c : ring)
{
if (!started)
{
path.moveTo(c.x(), c.y());
started = true;
}
else
{
path.lineTo(c.x(), c.y());
}
}
path.closePath();
return path;
}
/**
* Stitch partial rings together into rings if possible.
* @param partials partial rings, i.e. the ways that make up one or more rings
* @param outer outer or inner ring
* @param rings the ring list to which stitched rings need to be added
*/
protected void stitch(final List<List<FloatXY>> partials, final boolean outer, final List<Path2D> rings)
{
boolean changed;
do
{
changed = false;
outerLoop: for (int i = 0; i < partials.size(); i++)
{
List<FloatXY> p1 = partials.get(i);
for (int j = 0; j < partials.size(); j++)
{
if (i == j)
continue;
List<FloatXY> p2 = partials.get(j);
FloatXY p1Start = p1.get(0);
FloatXY p1End = p1.get(p1.size() - 1);
FloatXY p2Start = p2.get(0);
FloatXY p2End = p2.get(p2.size() - 1);
if (p1End.equals(p2Start))
{
// p1 ---> p2
p1.addAll(p2.subList(1, p2.size()));
partials.remove(j);
changed = true;
break outerLoop;
}
else if (p1End.equals(p2End))
{
// p1 ---> reversed(p2)
Collections.reverse(p2);
p1.addAll(p2.subList(1, p2.size()));
partials.remove(j);
changed = true;
break outerLoop;
}
else if (p1Start.equals(p2End))
{
// p2 --> p1
p2.addAll(p1.subList(1, p1.size()));
partials.remove(i);
changed = true;
break outerLoop;
}
else if (p1Start.equals(p2Start))
{
// reversed(p1) --> p2
Collections.reverse(p1);
p1.addAll(p2.subList(1, p2.size()));
partials.remove(j);
changed = true;
break outerLoop;
}
}
}
}
while (changed);
// Convert closed rings to Path2D
for (var p : partials)
{
if (p.size() > 2 && p.get(0).equals(p.get(p.size() - 1)))
{
Path2D path = makePath(p.get(0), p, outer);
rings.add(path);
}
}
}
/**
* Store only the id, lat and lon of a Node to reduce the memory footprint of the Node storage during parsing. <br>
* <br>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
*/
protected static class MiniNode
{
/** the node id. */
protected long id;
/** the latitude of the node. */
protected float lat;
/** the longitude of the node. */
protected float lon;
/**
* Create a MniniNode.
* @param id the node id
* @param lat the latitude of the node
* @param lon the longitude of the node
*/
public MiniNode(final long id, final float lat, final float lon)
{
this.id = id;
this.lat = lat;
this.lon = lon;
}
}
/**
* Store the minimum set of features of a Way to reduce the memory footprint of the Way storage during parsing. <br>
* <br>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
*/
protected static class MiniWay
{
/** the way id. */
protected long id;
/** the feature that characterizes this way. */
protected FeatureInterface feature;
/** the latitude of the way nodes that define e.g. a contour. */
protected float[] wayNodesLat;
/** the longitude of the way nodes that define e.g. a contour. */
protected float[] wayNodesLon;
/** the possible nodeIds. */
protected long[] wayNodesId;
/**
* Create a MniniWay.
* @param id the way id
* @param feature the feature that characterizes this way
* @param wayNodes the way nodes
*/
public MiniWay(final long id, final FeatureInterface feature, final List<MiniNode> wayNodes)
{
this.id = id;
this.feature = feature;
this.wayNodesLat = new float[wayNodes.size()];
this.wayNodesLon = new float[wayNodes.size()];
this.wayNodesId = new long[wayNodes.size()];
int i = 0;
for (MiniNode wayNode : wayNodes)
{
this.wayNodesLat[i] = (float) wayNode.lat;
this.wayNodesLon[i] = (float) wayNode.lon;
this.wayNodesId[i] = wayNode.id;
i++;
}
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "MiniWay [id=" + this.id + ", feature=" + this.feature + ", wayNodesLat=" + Arrays.toString(this.wayNodesLat)
+ ", wayNodesLon=" + Arrays.toString(this.wayNodesLon) + "]";
}
}
/**
* Store the minimum set of features of a Relation to reduce the memory footprint of the Relation storage during parsing.
* <br>
* <br>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
*/
protected static class MiniRelation
{
/** the relation id. */
protected long id;
/** the feature that characterizes this relation. */
protected FeatureInterface feature;
/** the way ids of the outer ways. */
protected List<Long> outerWayIds;
/** the way ids of the inner ways. */
protected List<Long> innerWayIds;
/** the name. */
protected String name;
/**
* Create a MniniRelation.
* @param id the relation id
* @param feature the feature that characterizes this relation
* @param outerWayIds the way ids of the outer ways
* @param innerWayIds the way ids of the inner ways
* @param name the name of the relation for debugging
*/
public MiniRelation(final long id, final FeatureInterface feature, final List<Long> outerWayIds,
final List<Long> innerWayIds, final String name)
{
this.id = id;
this.feature = feature;
this.outerWayIds = outerWayIds;
this.innerWayIds = innerWayIds;
this.name = name;
}
}
}