Skip to content

Commit

Permalink
Add road/path joining at tile boundaries to getGridFeatureCollections()
Browse files Browse the repository at this point in the history
getGridFeatureCollections is called to get the current tile grid whenever
a callout is about to be calculated. This change adds tiny roads to join
any roads/paths which cross the tile boundaries.

As part of this, the tile database schema changes. I've added code so that
if there's an exception opening the database is simply deletes it and tries
again with a fresh database. That's okay for now, though may need more
thinking about in future.
  • Loading branch information
davecraig committed Nov 14, 2024
1 parent b11efcc commit 5e92af5
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ object RealmConfiguration {
if(!SOUNDSCAPE_TILE_BACKEND)
deleteRealm(config)

tileDataRealm = Realm.open(config)
try {
tileDataRealm = Realm.open(config)
} catch(e: Exception) {
Log.e("Realm", "Exception opening database: $e")
// We're going to delete it and try again. This is likely due to a change in schema
Log.e("Realm", "Deleting and re-trying")
deleteRealm(config)
tileDataRealm = Realm.open(config)
}
}
return tileDataRealm!!
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class TilesDao(val realm: Realm) {
pois = tile?.pois ?: "-"
busStops = tile?.busStops ?: "-"
crossings = tile?.crossings ?: "-"
interpolations = tile?.interpolations ?: "-"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey

class TileData() : RealmObject {

@PrimaryKey
var quadKey : String = ""
var lastUpdated : RealmInstant? = RealmInstant.now() // this timestamps it
Expand All @@ -16,6 +17,7 @@ class TileData() : RealmObject {
var pois : String = "" // same as above
var busStops: String = ""
var crossings: String = ""
var interpolations: String = ""
//var pois : RealmList<GDASpatialDataResultEntity> = realmListOf()
//var roads : RealmList<GDASpatialDataResultEntity> = realmListOf()
//var paths : RealmList<GDASpatialDataResultEntity> = realmListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.scottishtecharmy.soundscape.network.ITileDAO
import org.scottishtecharmy.soundscape.network.ProtomapsTileClient
import org.scottishtecharmy.soundscape.network.SoundscapeBackendTileClient
import org.scottishtecharmy.soundscape.network.TileClient
import org.scottishtecharmy.soundscape.utils.InterpolatedPointsJoiner
import org.scottishtecharmy.soundscape.utils.RelativeDirections
import org.scottishtecharmy.soundscape.utils.TileGrid
import org.scottishtecharmy.soundscape.utils.TileGrid.Companion.SOUNDSCAPE_TILE_BACKEND
Expand Down Expand Up @@ -821,7 +822,13 @@ class GeoEngine {
}

private enum class Fc(val id: Int) {
ROADS(0), INTERSECTIONS(1), CROSSINGS(2), POIS(3), BUS_STOPS(4), MAX_COLLECTION_ID(5)
ROADS(0),
INTERSECTIONS(1),
CROSSINGS(2),
POIS(3),
BUS_STOPS(4),
INTERPOLATIONS(5),
MAX_COLLECTION_ID(6)
}

private fun getGridFeatureCollections(): List<FeatureCollection> {
Expand All @@ -835,12 +842,13 @@ class GeoEngine {
val processedOsmIds = Array(Fc.MAX_COLLECTION_ID.id) { mutableSetOf<Any>() }
val gridFeatureCollection = Array(Fc.MAX_COLLECTION_ID.id) { FeatureCollection() }

val joiner = InterpolatedPointsJoiner()
for (tile in tileGrid.tiles) {
//Check the db for the tile
val frozenTileResult =
tileDataRealm.query<TileData>("quadKey == $0", tile.quadkey).first().find()
if (frozenTileResult != null) {
val featureCollection = Array<FeatureCollection?>(5) { null }
val featureCollection = Array<FeatureCollection?>(Fc.MAX_COLLECTION_ID.id) { null }
featureCollection[Fc.ROADS.id] = frozenTileResult.roads.let {
moshi.adapter(FeatureCollection::class.java).fromJson(it)
}
Expand All @@ -856,13 +864,20 @@ class GeoEngine {
featureCollection[Fc.BUS_STOPS.id] = frozenTileResult.busStops.let {
moshi.adapter(FeatureCollection::class.java).fromJson(it)
}
featureCollection[Fc.INTERPOLATIONS.id] = frozenTileResult.interpolations.let {
moshi.adapter(FeatureCollection::class.java).fromJson(it)
}
for(ip in featureCollection[Fc.INTERPOLATIONS.id]!!) {
joiner.addInterpolatedPoints(ip)
}

for((index, fc) in featureCollection.withIndex())
deduplicateFeatureCollection(gridFeatureCollection[index], fc, processedOsmIds[index])
}
}
for(fc in gridFeatureCollection)
results.add(fc)
joiner.addJoiningLines(results[Fc.ROADS.id])

return results
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.scottishtecharmy.soundscape.utils
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Feature
import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection
import org.scottishtecharmy.soundscape.geojsonparser.geojson.GeoJsonObject
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Geometry
import org.scottishtecharmy.soundscape.geojsonparser.geojson.LineString
import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt
import org.scottishtecharmy.soundscape.geojsonparser.geojson.MultiPoint
Expand Down Expand Up @@ -117,6 +118,68 @@ data class IntersectionDetails(
var lineEnd : Boolean = false
)

class InterpolatedPointsJoiner {

private val interpolatedPoints: HashMap<Double, MutableList<LngLatAlt>> = hashMapOf()

fun addInterpolatedPoints(feature: Feature): Boolean {
// We add all edgePoint coordinates to our HashMap of interpolated points by OSM id
if (feature.properties?.containsKey("class")!!) {
if (feature.properties!!["class"] == "edgePoint") {
val geometry = feature.geometry as Geometry<LngLatAlt>
val osmId = feature.foreign!!["osm_id"] as Double
if (!interpolatedPoints.containsKey(osmId)) {
interpolatedPoints[osmId] = mutableListOf()
}
for (point in geometry.coordinates) {
// Add the point
interpolatedPoints[osmId]!!.add(point)
}
return false
}
}
return true
}

fun addJoiningLines(featureCollection : FeatureCollection) {
for (entries in interpolatedPoints) {
if (entries.value.size > 1) {
// We want to find points that we can join together. Go through the list of points
// for the OSM id comparing against the other members in the list to see if any are
// almost at the same point.
for ((index1, point1) in entries.value.withIndex()) {
for ((index2, point2) in entries.value.withIndex()) {
if (index1 != index2) {
if (distance(
point1.latitude,
point1.longitude,
point2.latitude,
point2.longitude
) < 0.1
) {
// If the points are within 10cm of each other, then join their
// LineStrings together.
val joining = Feature()
val foreign: HashMap<String, Any?> = hashMapOf()
val osmIds = arrayListOf<Double>()
osmIds.add(entries.key)
foreign["osm_ids"] = entries.key
joining.foreign = foreign
joining.geometry = LineString(point1, point2)
joining.properties = foreign

featureCollection.addFeature(joining)
}
}
}
}
} else {
// This is point must be on the outer edge or our grid, so we need do nothing
}
}
}
}

class IntersectionDetection {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,25 @@ fun getCrossingsFromTileFeatureCollection(tileFeatureCollection: FeatureCollecti
return crossingsFeatureCollection
}

/**
* Given a valid Tile feature collection this will parse the collection and return an interpolation
* points feature collection. Uses the "edgePoint" feature_value to extract crossings from GeoJSON.
* @param tileFeatureCollection
* A FeatureCollection object.
* @return A FeatureCollection object that contains only edgePoints
*/
fun getInterpolationPointsFromTileFeatureCollection(tileFeatureCollection: FeatureCollection): FeatureCollection{
val interpolationPointsFeatureCollection = FeatureCollection()
for (feature in tileFeatureCollection) {
feature.properties?.let { properties ->
if (properties["class"] == "edgePoint") {
interpolationPointsFeatureCollection.addFeature(feature)
}
}
}
return interpolationPointsFeatureCollection
}

/**
* Given a valid Tile feature collection this will parse the collection and return a paths
* feature collection. Uses the "footway", "path", "cycleway", "bridleway" feature_value to extract
Expand Down Expand Up @@ -487,6 +506,14 @@ fun processTileFeatureCollection(tileFeatureCollection: FeatureCollection?,
)
tileData.crossings = crossingsString

val interpolationFeatureCollection = getInterpolationPointsFromTileFeatureCollection(
tileFeatureCollection
)
val interpolationString = moshi.adapter(FeatureCollection::class.java).toJson(
interpolationFeatureCollection
)
tileData.interpolations = interpolationString

return tileData

}
Expand Down
48 changes: 31 additions & 17 deletions app/src/test/java/org/scottishtecharmy/soundscape/MvtTileTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.scottishtecharmy.soundscape.geojsonparser.geojson.MultiPolygon
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Point
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Polygon
import org.scottishtecharmy.soundscape.geojsonparser.moshi.GeoJsonObjectMoshiAdapter
import org.scottishtecharmy.soundscape.utils.InterpolatedPointsJoiner
import org.scottishtecharmy.soundscape.utils.getBoundingBoxOfLineString
import org.scottishtecharmy.soundscape.utils.getBoundingBoxOfMultiLineString
import org.scottishtecharmy.soundscape.utils.getBoundingBoxOfMultiPoint
Expand All @@ -23,13 +24,14 @@ import vector_tile.VectorTile
import java.io.FileInputStream
import java.io.FileOutputStream


class MvtTileTest {

private fun vectorTileToGeoJsonFromFile(tileX: Int,
tileY: Int,
filename: String,
cropPoints: Boolean = true): FeatureCollection {
private fun vectorTileToGeoJsonFromFile(
tileX: Int,
tileY: Int,
filename: String,
cropPoints: Boolean = true
): FeatureCollection {

val path = "src/test/res/org/scottishtecharmy/soundscape/"
val remoteTile = FileInputStream(path + filename)
Expand All @@ -39,20 +41,25 @@ class MvtTileTest {

// We want to check that all of the coordinates generated are within the buffered
// bounds of the tile. The tile edges are 4/256 further out, so we adjust for that.
val nwPoint = getLatLonTileWithOffset(tileX, tileY, 15, -4/256.0, -4/256.0)
val sePoint = getLatLonTileWithOffset(tileX+1, tileY+1, 15, 4/256.0, 4/256.0)
for(feature in featureCollection) {
val nwPoint = getLatLonTileWithOffset(tileX, tileY, 15, -4 / 256.0, -4 / 256.0)
val sePoint = getLatLonTileWithOffset(tileX + 1, tileY + 1, 15, 4 / 256.0, 4 / 256.0)
for (feature in featureCollection) {
var box = BoundingBox()
when(feature.geometry.type) {
when (feature.geometry.type) {
"Point" -> box = getBoundingBoxOfPoint(feature.geometry as Point)
"MultiPoint" -> box = getBoundingBoxOfMultiPoint(feature.geometry as MultiPoint)
"MultiPoint" -> box = getBoundingBoxOfMultiPoint(feature.geometry as MultiPoint)
"LineString" -> box = getBoundingBoxOfLineString(feature.geometry as LineString)
"MultiLineString" -> box = getBoundingBoxOfMultiLineString(feature.geometry as MultiLineString)
"MultiLineString" -> box =
getBoundingBoxOfMultiLineString(feature.geometry as MultiLineString)

"Polygon" -> box = getBoundingBoxOfPolygon(feature.geometry as Polygon)
"MultiPolygon" -> box = getBoundingBoxOfMultiPolygon(feature.geometry as MultiPolygon)
"MultiPolygon" -> box =
getBoundingBoxOfMultiPolygon(feature.geometry as MultiPolygon)

else -> assert(false)
}
// // Check that the feature bounding box is within the tileBoundingBox
// // Check that the feature bounding box is within the tileBoundingBox. This has been
// // broken by the addition of POI polygons which go beyond tile boundaries.
// assert(box.westLongitude >= nwPoint.longitude) { "${box.westLongitude} vs. ${nwPoint.longitude}" }
// assert(box.eastLongitude <= sePoint.longitude) { "${box.eastLongitude} vs. ${sePoint.longitude}" }
// assert(box.southLatitude >= sePoint.latitude) { "${box.southLatitude} vs. ${sePoint.latitude}" }
Expand Down Expand Up @@ -131,16 +138,23 @@ class MvtTileTest {
@Test
fun testVectorToGeoJsonGrid() {

val joiner = InterpolatedPointsJoiner()

// Make a large grid to aid analysis
val featureCollection = FeatureCollection()
for(x in 15990..15992) {
for (x in 15990..15992) {
for (y in 10212..10213) {
val geojson = vectorTileToGeoJsonFromFile(x, y, "${x}x${y}.mvt")
for(feature in geojson) {
featureCollection.addFeature(feature)
for (feature in geojson) {
val addFeature = joiner.addInterpolatedPoints(feature)
if (addFeature) {
featureCollection.addFeature(feature)
}
}
}
}
// Add lines to connect all of the interpolated points
joiner.addJoiningLines(featureCollection)

val adapter = GeoJsonObjectMoshiAdapter()

Expand All @@ -154,4 +168,4 @@ class MvtTileTest {
outputFile.write(adapter.toJson(featureCollection).toByteArray())
outputFile.close()
}
}
}

0 comments on commit 5e92af5

Please sign in to comment.