Skip to content

Commit

Permalink
#56, implementation and constructing of OsmBoundary on networkData do…
Browse files Browse the repository at this point in the history
…ne (instead of using settings). However, this is whole support is also need for zoning reader. See comment on issue on how to avoid duplicate code and make this a more general and contained piece of code. NOT DONE. after refactoring, replace use of setting boundingboundary with the one on the data objects passes around.
  • Loading branch information
MarkRaadsen committed Apr 27, 2024
1 parent a4054c5 commit a16c762
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 90 deletions.
16 changes: 14 additions & 2 deletions src/main/java/org/goplanit/osm/converter/OsmBoundary.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,20 @@ public static OsmBoundary of(String boundaryName, String boundaryType) {
}

/**
* Create a polygon based on name of administrative type for a given admin level (if provided)
* to restrict parsing to.
* Create full OsM boundary with all members populated
*
* @param boundaryName to restrict to
* @param boundaryType type of boundary (optional)
* @param adminLevel admin level of the boundary (optional)
* @param boundingPolygon to restrict to
* @return OsmBoundary to use in setting
*/
public static OsmBoundary of(String boundaryName, String boundaryType, String adminLevel, Polygon boundingPolygon) {
return new OsmBoundary(boundaryName, boundaryType, adminLevel, boundingPolygon);
}

/**
* Create OSM boundary of administrative type
*
* @param boundaryName to restrict to
* @param adminLevel admin level of the boundary (optional)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,8 @@ protected OsmNetworkBaseHandler(final PlanitOsmNetwork networkToPopulate, final
this.networkToPopulate = networkToPopulate;
this.settings = settings;
this.networkData = networkData;
initialiseUserDefinedBoundary();
}

/**
* If the user defined a bounding area in some way shape or form, we must construct a bounding polygon from
* this information to apply during parsing. This is what happens here
*/
protected void initialiseUserDefinedBoundary() {
if(getSettings().hasBoundingBoundary()){
if(getSettings().getBoundingArea().hasBoundaryName()){

}
}
}


/** verify if tags represent an highway or railway that is specifically aimed at road based or rail based infrastructure, e.g.,
* asphalt or tracks and NOT an area, platform, stops, etc. and is also activated for parsing based on the settings
*
Expand All @@ -87,7 +73,7 @@ protected boolean isActivatedRoadRailOrWaterwayBasedInfrastructure(Map<String, S
* @param osmWay to parse
* @param osmWayConsumer to apply to eligible OSM way
*/
protected void wrapHandleOsmWay(OsmWay osmWay, BiConsumer<OsmWay, Map<String, String>> osmWayConsumer) {
protected void wrapHandleInfrastructureOsmWay(OsmWay osmWay, BiConsumer<OsmWay, Map<String, String>> osmWayConsumer) {

if(!settings.isOsmWayExcluded(osmWay.getId())) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import org.goplanit.osm.physical.network.macroscopic.PlanitOsmNetwork;
import org.goplanit.osm.tags.*;
import org.goplanit.osm.util.*;
import org.goplanit.utils.exceptions.PlanItException;
import org.goplanit.utils.exceptions.PlanItRunTimeException;
import org.goplanit.utils.misc.Pair;
import org.goplanit.utils.network.layer.NetworkLayer;
Expand Down Expand Up @@ -53,20 +52,20 @@ private boolean hasNetworkLayersWithActiveOsmNode(long osmNodeId){
* @return true when eligible, false otherwise
*/
private boolean isNodeSpatiallyEligible(final OsmNode osmNode) {
var settings = getSettings();
return getNetworkData().getOsmNodeData().containsPreregisteredOsmNode(osmNode.getId())
var networkData = getNetworkData();
return networkData.getOsmNodeData().containsPreregisteredOsmNode(osmNode.getId())
&&
( !settings.hasBoundingBoundary() ||
!settings.getBoundingArea().hasBoundingPolygon() ||
settings.isKeepOsmNodeOutsideBoundingPolygon(osmNode.getId())||
OsmNodeUtils.createPoint(osmNode).within(settings.getBoundingArea().getBoundingPolygon())
( !networkData.hasBoundingBoundary() ||
!networkData.getBoundingArea().hasBoundingPolygon() ||
getSettings().isKeepOsmNodeOutsideBoundingPolygon(osmNode.getId())||
OsmNodeUtils.createPoint(osmNode).within(networkData.getBoundingArea().getBoundingPolygon())
);
}


/**
* now parse the remaining circular osmWays, which by default are converted into multiple links/linksegments for each part of
* the circular way in between connecting in and outgoing links/linksegments that were parsed during the regular parsing phase
* now parse the remaining circular osmWays, which by default are converted into multiple links/link segments for each part of
* the circular way in between connecting in and outgoing links/link segments that were parsed during the regular parsing phase
*
* @param circularOsmWay the circular osm way to parse
*/
Expand Down Expand Up @@ -453,7 +452,7 @@ public void handle(OsmNode osmNode) {
@Override
public void handle(OsmWay osmWay) throws IOException {

wrapHandleOsmWay(osmWay, this::handleOsmWay);
wrapHandleInfrastructureOsmWay(osmWay, this::handleOsmWay);

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
package org.goplanit.osm.converter.network;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.atomic.LongAdder;
import java.util.logging.Logger;

import de.topobyte.osm4j.core.model.iface.EntityType;
import de.topobyte.osm4j.core.model.iface.OsmNode;
import de.topobyte.osm4j.core.model.iface.OsmRelation;
import de.topobyte.osm4j.core.model.iface.OsmWay;
import de.topobyte.osm4j.core.model.util.OsmModelUtil;
import org.goplanit.osm.converter.OsmBoundary;
import org.goplanit.osm.physical.network.macroscopic.PlanitOsmNetwork;
import org.goplanit.osm.tags.OsmBoundaryTags;
import org.goplanit.osm.tags.OsmMultiPolygonTags;
import org.goplanit.osm.tags.OsmTags;
import org.goplanit.osm.util.OsmRelationUtils;
import org.goplanit.osm.util.OsmTagUtils;
import org.goplanit.osm.util.OsmWayUtils;
import org.goplanit.utils.geo.PlanitJtsUtils;
import org.locationtech.jts.geom.Coordinate;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.LongAdder;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
* Preprocessing Handler that has two stages:
Expand Down Expand Up @@ -53,6 +64,95 @@ public enum Stage {

private final LongAdder nodeCounter;

/**
* Verify if relation defines a boundary area that was user configured to be the bounding area. If so
* register its members reflecting the outer boundary for processing in the regular pre-processing stage
*
* @param osmRelation to check
*/
private void identifyAndRegisterBoundingAreaRelationMembers(OsmRelation osmRelation) {
/* only keep going when boundary is active and based on name */
if(!getSettings().hasBoundingBoundary() || !getSettings().getBoundingArea().hasBoundaryName()){
return;
}
var boundarySettings = getSettings().getBoundingArea();

/* check for boundary tags on relation */
var tags = OsmModelUtil.getTagsAsMap(osmRelation);
if(!tags.containsKey(OsmBoundaryTags.getBoundaryKeyTag())){
return;
}

if(!OsmTagUtils.matchesAnyValueTag(
tags.get(OsmBoundaryTags.getBoundaryKeyTag()), OsmBoundaryTags.getBoundaryValues())){
return;
}

/* boundary compatible relation - now check against settings */
if(OsmTagUtils.keyMatchesAnyValueTag(tags, OsmTags.NAME, boundarySettings.getBoundaryName())){

// found, no see if more specific checks are required based on type and/or admin_level. Below flags switch to
// true if the item is not used or it is used AND it is matched
boolean boundaryTypeMatch =
boundarySettings.hasBoundaryType() && OsmBoundaryTags.hasBoundaryValueTag(tags, boundarySettings.getBoundaryType());
boolean adminLevelMatch = boundarySettings.hasBoundaryAdminLevel() &&
OsmTagUtils.keyMatchesAnyValueTag(tags, OsmBoundaryTags.ADMIN_LEVEL, boundarySettings.getBoundaryAdminLevel());
boolean boundaryAdministrativeMatch = !boundarySettings.getBoundaryType().equals(OsmBoundaryTags.ADMINISTRATIVE) ||
boundarySettings.hasBoundaryAdminLevel() &&
OsmTagUtils.keyMatchesAnyValueTag(tags, OsmBoundaryTags.ADMIN_LEVEL, boundarySettings.getBoundaryAdminLevel());

if(boundaryTypeMatch && adminLevelMatch && boundaryAdministrativeMatch){

// full match found -> register all members with correct roles to extract bounding area polygon from in next stage
for(int memberIndex = 0; memberIndex < osmRelation.getNumberOfMembers(); ++memberIndex){
var currMember = osmRelation.getMember(memberIndex);
if(OsmRelationUtils.isMemberOfTypeAndRole(currMember, EntityType.Way, OsmMultiPolygonTags.OUTER_ROLE)){
getNetworkData().registerBoundaryOsmWayOuterRoleSection(currMember.getId());
}
}
}
}
}

/**
* Construct bounding boundary based on registered OSM ways. It is assumed these OSM ways are contiguous based on the
* numbering assigned to them. It is assumed this forms a complete polygon (will be checked) and it is assumed this
* adheres to the bounding boundary configuration (without a polygon) configured by the user in the network settings.
* The result is a new OsmBoundingBoundary that will be used in the actual processing
*/
private void completeConstructionBoundingBoundaryFromOsmWays() {
List<OsmWay> boundaryOsmWays = getNetworkData().getRegisteredBoundaryOsmWaysInOrder();
List<Coordinate> contiguousBoundaryCoords = new ArrayList<>();
for(var osmWay : boundaryOsmWays){
var coordArray = OsmWayUtils.createCoordinateArrayNoThrow(osmWay,getNetworkData().getOsmNodeData().getRegisteredOsmNodes());
if(!contiguousBoundaryCoords.isEmpty() &&
!contiguousBoundaryCoords.get(contiguousBoundaryCoords.size()-1).equals2D(coordArray[0])){
LOGGER.severe("Bounding boundary outer role OSM ways supposed to be contiguous, but this was not found to be the case, this shouldn't happen, ignore");
return;
}
contiguousBoundaryCoords.addAll(Arrays.stream(coordArray).collect(Collectors.toList()));
}

// now convert to polygon
var boundingBoundaryPolygon = PlanitJtsUtils.createPolygon(
contiguousBoundaryCoords.toArray(new Coordinate[0]));
if(boundingBoundaryPolygon == null){
LOGGER.severe("Unable to construct bounding boundary polygon from OSM way coordinates listed under bounding boundary name");
return;
}

// construct final OsmBoundary copying original + adding in polygon
var originalBoundary = getSettings().getBoundingArea();
getNetworkData().setBoundingAreaWithPolygon(
OsmBoundary.of(
originalBoundary.getBoundaryName(),
originalBoundary.getBoundaryType(),
originalBoundary.getBoundaryAdminLevel(),
boundingBoundaryPolygon));

//todo implement situation no name/registered ways exist and we should just copy the one from the user settings instead!
}

/** Mark all nodes of eligible OSM ways (e.g., road, rail, etc.) to be parsed during the main processing phase
*
* @param osmWay to handle
Expand All @@ -78,7 +178,7 @@ protected void handleEligibleOsmWay(OsmWay osmWay, Map<String,String> tags) {
}
}

/* mark all nodes as potentially eligible for keeping, since they reside on an OSM way that is deemed eligible (road, rail) */
/* mark all nodes as potentially eligible for keeping, since they reside on an OSM way that is deemed eligible (road, rail, or boundary) */
for(int index=0;index<osmWay.getNumberOfNodes();++index) {
getNetworkData().getOsmNodeData().preRegisterEligibleOsmNode(osmWay.getNodeId(index));
}
Expand All @@ -105,6 +205,7 @@ public OsmNetworkPreProcessingHandler(
*/
@Override
public void handle(OsmNode node) {
// no action if stage is Stage.IDENTIFY_BOUNDARY_BY_NAME but costlier to check than just count nodes
nodeCounter.increment();
}

Expand All @@ -116,7 +217,19 @@ public void handle(OsmNode node) {
*/
@Override
public void handle(OsmWay osmWay) {
wrapHandleOsmWay(osmWay, this::handleEligibleOsmWay);
// no action if stage is Stage.IDENTIFY_BOUNDARY_BY_NAME
if(stage.equals(Stage.IDENTIFY_BOUNDARY_BY_NAME)) {
return;
}

// update registered OSM way ids with actual OSM way containing geometry (if needed)
if(getNetworkData().isRegisteredBoundaryOsmWay(osmWay.getId())){
getNetworkData().updateBoundaryRegistrationWithOsmWay(osmWay);
handleEligibleOsmWay(osmWay, OsmModelUtil.getTagsAsMap(osmWay));
}

// regular OSM way parsing representing infrastructure for networks
wrapHandleInfrastructureOsmWay(osmWay, this::handleEligibleOsmWay);
}

/**
Expand All @@ -126,64 +239,33 @@ public void handle(OsmWay osmWay) {
*/
@Override
public void handle(OsmRelation osmRelation) {

/* only keep going when boundary is active and based on name */
if(!getSettings().hasBoundingBoundary() || !getSettings().getBoundingArea().hasBoundaryName()){
// no action if stage is Stage.REGULAR_PREPROCESSING
if(stage.equals(Stage.REGULAR_PREPROCESSING)){
return;
}
var boundarySettings = getSettings().getBoundingArea();

/* check for boundary tags on relation */
var tags = OsmModelUtil.getTagsAsMap(osmRelation);
if(!tags.containsKey(OsmBoundaryTags.getBoundaryKeyTag())){
return;
}

if(!OsmTagUtils.matchesAnyValueTag(
tags.get(OsmBoundaryTags.getBoundaryKeyTag()), OsmBoundaryTags.getBoundaryValues())){
return;
}

/* boundary compatible relation - now check against settings */
if(OsmTagUtils.keyMatchesAnyValueTag(tags, OsmTags.NAME, boundarySettings.getBoundaryName())){

// found, no see if more specific checks are required based on type and/or admin_level. Below flags switch to
// true if the item is not used or it is used AND it is matched
boolean boundaryTypeMatch =
boundarySettings.hasBoundaryType() && OsmBoundaryTags.hasBoundaryValueTag(tags, boundarySettings.getBoundaryType());
boolean adminLevelMatch = boundarySettings.hasBoundaryAdminLevel() &&
OsmTagUtils.keyMatchesAnyValueTag(tags, OsmBoundaryTags.ADMIN_LEVEL, boundarySettings.getBoundaryAdminLevel());
boolean boundaryAdministrativeMatch = !boundarySettings.getBoundaryType().equals(OsmBoundaryTags.ADMINISTRATIVE) ||
boundarySettings.hasBoundaryAdminLevel() &&
OsmTagUtils.keyMatchesAnyValueTag(tags, OsmBoundaryTags.ADMIN_LEVEL, boundarySettings.getBoundaryAdminLevel());

if(boundaryTypeMatch && adminLevelMatch && boundaryAdministrativeMatch){

// full match found -> register as the bounding area polygon to use

//TODO below requires first implementing the two phases (enum created but not yet enforced)
// stage 1: do nothing by register the name and not create the polygon. In stage 2 extract the ways with outer roles
// and in complete() create the overall polygon from the line strings --> then put it on the network data there
// and it can be used in main processing (also to be implemented because it is not yet used
// then remove the initialise bit from the basehandler I think that is still there but is not needed.
// then feed this through all the way
//getNetworkData().setBoundingAreaWithPolygon();
}
}
/* identify relations that might carry bounding area information */
identifyAndRegisterBoundingAreaRelationMembers(osmRelation);
}

/** Log total number of parsed nodes and percentage retained
*/
@Override
public void complete() throws IOException {
super.complete();

// finalise bounding boundary construction
if(getNetworkData().hasRegisteredBoundaryOsmWay()){
completeConstructionBoundingBoundaryFromOsmWays();
}

// regular pre-registration for networks
int totalOsmNodes = (int) nodeCounter.sum();
int preRegisteredOsmNodes = getNetworkData().getOsmNodeData().getRegisteredOsmNodes().size();
LOGGER.info(String.format("Total OSM nodes in source: %d",totalOsmNodes));
LOGGER.info(String.format("Total OSM nodes identified as part of network: %d (%.2f%%)",preRegisteredOsmNodes, preRegisteredOsmNodes/(double)totalOsmNodes));
}


/**
* reset the contents, mainly to free up unused resources
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public class OsmNetworkReader implements NetworkReader {

/**
* Call this BEFORE we parse the OSM network to initialise the handler(s) properly
*
*/
public void initialiseBeforeParsing() {
PlanItRunTimeException.throwIf(getOsmNetworkToPopulate().getTransportLayers() != null && getOsmNetworkToPopulate().getTransportLayers().size()>0,
Expand Down Expand Up @@ -78,8 +77,9 @@ public void initialiseBeforeParsing() {
settings.excludeOsmWayTypesWithoutActivatedModes();
settings.logUnsupportedOsmWayTypes();

/* initialise layer specific parsers */
/* initialise layer specific parsers and bounding area*/
networkData.initialiseLayerParsers(getOsmNetworkToPopulate(), settings, geoUtils);
networkData.initialiseBoundingArea(settings);
}

/** Read based on reader and handler where the reader performs a callback to the handler provided
Expand Down Expand Up @@ -116,9 +116,10 @@ private void doPreprocessing(){

OsmNetworkPreProcessingHandler osmHandler;

/* identify OSM relation by name if bounding area is specified by name rather than an explicit bounding box */
/* STAGE 1 - BOUNDARY IDENTIFICATION
* identify OSM relation by name if bounding area is specified by name rather than an explicit bounding box */
if(settings.hasBoundingBoundary() && !settings.getBoundingArea().hasBoundingPolygon()) {
LOGGER.info("Pre-processing: Identifying relations representing public transport platforms");
LOGGER.info(String.format("Pre-processing: Identifying bounding boundary for %s", settings.getBoundingArea().getBoundaryName()));

/* reader to parse the actual file or source location */
OsmReader osmReader = Osm4JUtils.createOsm4jReader(settings.getInputSource());
Expand All @@ -128,9 +129,10 @@ private void doPreprocessing(){
}
osmHandler = new OsmNetworkPreProcessingHandler(
OsmNetworkPreProcessingHandler.Stage.IDENTIFY_BOUNDARY_BY_NAME, getOsmNetworkToPopulate(), networkData, settings);
read(osmReader, osmHandler);
}

/* regular preprocessing */
/* STAGE 2 - REGULAR PREPROCESSING */
LOGGER.info("Preprocessing: reducing memory footprint, identifying required OSM nodes");

/* reader to parse the actual file or source location */
Expand Down
Loading

0 comments on commit a16c762

Please sign in to comment.