Skip to content

Commit

Permalink
#369 monitor vector tiles poc - improve tile coordinate calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
vmarc committed Dec 14, 2023
1 parent 5393328 commit 86ca6a8
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Params } from '@angular/router';
import { MonitorRouteMapPage } from '@api/common/monitor';
import { MapPosition } from '@app/ol/domain';
import { ZoomLevel } from '@app/ol/domain';
import { TileDebug256Layer } from '@app/ol/layers';
import { NetworkMarkerLayer } from '@app/ol/layers';
import { BackgroundLayer } from '@app/ol/layers';
import { MapControls } from '@app/ol/layers';
Expand Down Expand Up @@ -168,6 +169,7 @@ export class MonitorRouteMapService extends OpenlayersMapService {
registry.register([], BackgroundLayer.build(), true);
registry.register([], OsmLayer.build(), false);
registry.register([], MonitorLayer.build(), true);
registry.register([], TileDebug256Layer.build(), false);
this.register(registry);
}

Expand Down Expand Up @@ -296,7 +298,7 @@ export class MonitorRouteMapService extends OpenlayersMapService {
if (features && features.length > 0) {
features.forEach((feature) => {
const layer = feature.get('layer');
if (layer === 'xx') {
if (layer === 'relation') {
const id = feature.get('id');
relationIds.push(id);
}
Expand Down
1 change: 0 additions & 1 deletion frontend/src/app/ol/layers/monitor-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export class MonitorLayer {

static build(): MapLayer {
const source = new VectorTile({
tileSize: 512,
minZoom: 2,
maxZoom: 14,
format: new MVT(),
Expand Down
11 changes: 11 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
<name>server</name>
<description>knooppuntnet server</description>

<repository>
<id>ECC</id>
<url>https://maven.ecc.no/releases</url>
</repository>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
Expand Down Expand Up @@ -339,6 +344,12 @@
<version>7.23.0</version>
</dependency>

<dependency>
<groupId>no.ecc.vectortile</groupId>
<artifactId>java-vector-tile</artifactId>
<version>1.3.23</version>
</dependency>

</dependencies>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import kpn.core.util.Log
import kpn.database.base.Database
import kpn.database.base.Id
import kpn.database.util.Mongo
import kpn.server.analyzer.engine.tiles.domain.Line
import kpn.server.analyzer.engine.tiles.domain.Tile
import kpn.server.analyzer.engine.tiles.vector.encoder.VectorTileEncoder
import kpn.server.analyzer.engine.tiles.domain.CoordinateTransform.toWorldCoordinates
import kpn.server.analyzer.engine.tiles.domain.NewTile
import kpn.server.monitor.repository.MonitorRelationRepositoryImpl
import kpn.server.monitor.repository.MonitorRouteRepositoryImpl
import org.apache.commons.io.FileUtils
Expand All @@ -23,6 +22,7 @@ import org.mongodb.scala.model.Filters.equal
import org.mongodb.scala.model.Projections.excludeId
import org.mongodb.scala.model.Projections.fields
import org.mongodb.scala.model.Projections.include
import no.ecc.vectortile.VectorTileEncoder

import java.io.File

Expand All @@ -36,7 +36,7 @@ case class OsmSegments(
)

case class TileRelationSegment(
lines: Seq[Line]
worldCoordinates: Seq[Coordinate]
)

case class TileRelationData(
Expand All @@ -45,7 +45,6 @@ case class TileRelationData(
)

object MonitorTileTool {

def main(args: Array[String]): Unit = {
Mongo.executeIn("kpn-monitor") { database =>
val config = new MonitorTileToolConfig(database)
Expand Down Expand Up @@ -81,7 +80,7 @@ class MonitorTileTool(config: MonitorTileToolConfig) {
tileDatas.zipWithIndex.foreach { case (tileData, index) =>
Log.context(s"${index + 1}/${tileDatas.size}") {
val Array(z, x, y) = tileData.name.split("-").map(namePart => java.lang.Integer.parseInt(namePart))
val tile = new Tile(z, x, y)
val tile = new NewTile(z, x, y)
val tileRelationDatas = tileData.relationIds.flatMap { relationId =>
allRelationDatas.get(relationId)
}
Expand All @@ -95,32 +94,23 @@ class MonitorTileTool(config: MonitorTileToolConfig) {
}
}

private def build(tile: Tile, tileRelationDatas: Seq[TileRelationData]): Array[Byte] = {
private def build(tile: NewTile, tileRelationDatas: Seq[TileRelationData]): Array[Byte] = {

val geometryFactory = new GeometryFactory

val encoder = new VectorTileEncoder()

tileRelationDatas.foreach { tileRelationData =>
tileRelationData.segments.foreach { segment =>
val coordinates = segment.lines.flatMap { line =>
Seq(
new Coordinate(tile.scaleLon(line.p1.x), tile.scaleLat(line.p1.y)),
new Coordinate(tile.scaleLon(line.p2.x), tile.scaleLat(line.p2.y))
)
}
val lineString = geometryFactory.createLineString(coordinates.toArray)
val userData = Seq(
Some("id" -> tileRelationData.relationId.toString),
// Some("name" -> tileRoute.routeName),
// Some("oneway" -> segment.oneWay.toString),
// Some("surface" -> segment.surface),
).flatten.toMap
encoder.addLineStringFeature("xx", userData, lineString)
val scaledCoordinates = tile.scale(segment.worldCoordinates)
val lineString = geometryFactory.createLineString(scaledCoordinates.toArray)
val userData = new java.util.HashMap[String, String]()
userData.put("id", tileRelationData.relationId.toString)
encoder.addFeature("relation", userData, lineString)
}
}

encoder.encode
encoder.encode()
}

private def readOsmSegments(relationId: Long): Seq[OsmSegments] = {
Expand Down Expand Up @@ -167,14 +157,7 @@ class MonitorTileTool(config: MonitorTileToolConfig) {
val segments = geoJsons.flatMap { geoJson =>
val geometry = GeoJSONReader.parseGeometry(geoJson)
geometry match {
case lineString: LineString =>
val points = (0 until lineString.getNumPoints).map { index =>
lineString.getPointN(index)
}
val lines = points.sliding(2).toSeq.map { case Seq(p1, p2) =>
Line(p1.getX, p1.getY, p2.getX, p2.getY)
}
Some(TileRelationSegment(lines))
case lineString: LineString => Some(TileRelationSegment(toWorldCoordinates(lineString)))
case _ => None
}
}
Expand All @@ -185,7 +168,7 @@ class MonitorTileTool(config: MonitorTileToolConfig) {
}
}

private def writeTile(tile: Tile, tileBytes: Array[Byte]): Unit = {
private def writeTile(tile: NewTile, tileBytes: Array[Byte]): Unit = {
val fileName = s"/kpn/tiles/monitor/${tile.z}/${tile.x}/${tile.y}.mvt"
val file = new File(fileName)
if (file.exists()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package kpn.server.analyzer.engine.tiles.domain

import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.LineString

import scala.math.Pi
import scala.math.atan
import scala.math.exp
import scala.math.log
import scala.math.sin

/*
EPSG:4326
WGS84 reference system used by OSM
See: https://en.wikipedia.org/wiki/Mercator_projection
coordinates
longitude (x)
latitude (y)
EPSG:3857
reference system WGS84 Web Mercator used in tiles
See: https://en.wikipedia.org/wiki/Web_Mercator_projection
world coordinates
worldX
The longitude for a web mercator coordinate where 0 is the international
date line on the west side, 1 is the international date line on the east
side, and 0.5 is the prime meridian.
worldY
The latitude for a web mercator coordinate where 0 is the north edge of
the map, 0.5 is the equator, and 1 is the south edge of the map.
*/

object CoordinateTransform {

private val DEGREES_PER_RADIAN = 180 / Pi
private val RADIANS_PER_DEGREE = Pi / 180
private val MAX_LAT = worldYtoLat(-0.1)
private val MIN_LAT = worldYtoLat(1.1)

def worldXtoLon(worldX: Double): Double = {
worldX * 360 - 180
}

def worldYtoLat(worldY: Double): Double = {
val n = Pi - 2 * Pi * worldY
DEGREES_PER_RADIAN * atan(0.5 * (exp(n) - exp(-n)))
}

def lonToWorldX(lon: Double): Double = {
(lon + 180) / 360
}

def latToWorldY(lat: Double): Double = {
if (lat <= MIN_LAT) return 1.1
if (lat >= MAX_LAT) return -0.1
val sinus = sin(lat * RADIANS_PER_DEGREE)
0.5 - 0.25 * log((1 + sinus) / (1 - sinus)) / Pi
}

def toWorldCoordinates(lineString: LineString): Array[Coordinate] = {
(0 until lineString.getNumPoints).toArray.map { index =>
val point = lineString.getPointN(index)
val x = lonToWorldX(point.getX)
val y = latToWorldY(point.getY)
new Coordinate(x, y)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kpn.server.analyzer.engine.tiles.domain

import org.locationtech.jts.geom.Coordinate

class NewTile(val z: Int, val x: Int, val y: Int) {

val name: String = s"$z-$x-$y"

private val zoomFactor = 1 << z // the number of tiles across the map in each direction

private val worldXMin = x.toDouble / zoomFactor
private val worldXMax = (x.toDouble + 1) / zoomFactor
private val worldYMin = y.toDouble / zoomFactor
private val worldYMax = (y.toDouble + 1) / zoomFactor
private val worldWidth = worldXMax - worldXMin
private val worldHeight = worldYMax - worldYMin


override def equals(obj: Any): Boolean = {
obj.isInstanceOf[Tile] && obj.asInstanceOf[Tile].name == name
}

override def hashCode(): Int = name.hashCode()

def scale(worldCoordinates: Seq[Coordinate]): Seq[Coordinate] = {
worldCoordinates.map(scale)
}

def scale(coordinate: Coordinate): Coordinate = {
val scaledX = ((coordinate.x - worldXMin) * 256 / worldWidth)
val scaledY = ((coordinate.y - worldYMin) * 256 / (worldHeight))
new Coordinate(scaledX, scaledY)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kpn.server.analyzer.engine.tiles.domain

import kpn.core.util.UnitTest
import kpn.server.analyzer.engine.tiles.domain.CoordinateTransform.worldYtoLat

class CoordinateTransformTest extends UnitTest {

test("coordinate transform") {
assertTransform(0, 0, 0.5, 0.5)
assertTransform(0, -180, 0, 0.5)
assertTransform(0, 180, 1, 0.5)
assertTransform(0, 180 - 1e-7, 1, 0.5)
assertTransform(45, 0, 0.5, 0.3597)

worldYtoLat(-0.1) should equal(87.35 +- 0.01)
worldYtoLat(1.1) should equal(-87.35 +- 0.01)
}

private def assertTransform(lat: Double, lon: Double, worldX: Double, worldY: Double): Unit = {
CoordinateTransform.latToWorldY(lat) should equal(worldY +- 0.0001)
CoordinateTransform.lonToWorldX(lon) should equal(worldX +- 0.0001)
CoordinateTransform.worldYtoLat(worldY) should equal(lat +- 0.01)
CoordinateTransform.worldXtoLon(worldX) should equal(lon +- 0.01)
}
}

0 comments on commit 86ca6a8

Please sign in to comment.