From bdc170af781f1b50f730b83134d8033567b5d47b Mon Sep 17 00:00:00 2001 From: C1200 Date: Sat, 20 Jan 2024 16:29:54 +0000 Subject: [PATCH 01/11] Add BlueMap support and schedule info to forge version --- build.gradle.kts | 6 +- .../littlechasiu/ctm/BlueMapIntegration.kt | 366 ++++++++++++++++++ .../kotlin/littlechasiu/ctm/Extensions.kt | 87 +++++ src/main/kotlin/littlechasiu/ctm/TrackMap.kt | 1 + .../kotlin/littlechasiu/ctm/TrackWatcher.kt | 185 ++++----- .../kotlin/littlechasiu/ctm/model/Network.kt | 10 + .../kotlin/littlechasiu/ctm/model/Schedule.kt | 36 ++ .../ctm/static/assets/js/create-track-map.js | 51 ++- 8 files changed, 626 insertions(+), 116 deletions(-) create mode 100644 src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt create mode 100644 src/main/kotlin/littlechasiu/ctm/model/Schedule.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8f67426..594f636 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ val archives_version = "$mod_version+mc$minecraft_version-fabric" repositories { mavenCentral() - maven("https://jitpack.io") // MixinExtras, Fabric ASM + maven("https://jitpack.io") // MixinExtras, Fabric ASM, BlueMap API maven("https://maven.jamieswhiteshirt.com/libs-release") // Reach Entity Attributes maven("https://mvn.devos.one/snapshots/") // Create Fabric maven("https://api.modrinth.com/maven") // LazyDFU @@ -56,8 +56,10 @@ dependencies { shadowDep("io.ktor:ktor-server-core-jvm:$ktor_version") shadowDep("io.ktor:ktor-server-cio-jvm:$ktor_version") shadowDep("io.ktor:ktor-server-cors-jvm:$ktor_version") - shadowDep("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_json_version") shadowDep("org.jetbrains.kotlin-wrappers:kotlin-css:$kotlin_css_version") + + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_json_version") + compileOnly("com.github.BlueMap-Minecraft:BlueMapAPI:v2.5.1") } val targetJavaVersion = 17 diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt new file mode 100644 index 0000000..57eb50a --- /dev/null +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -0,0 +1,366 @@ +package littlechasiu.ctm + +import com.flowpowered.math.vector.Vector3d +import com.simibubi.create.content.trains.signal.SignalBlockEntity +import de.bluecolored.bluemap.api.BlueMapAPI +import de.bluecolored.bluemap.api.BlueMapMap +import de.bluecolored.bluemap.api.markers.LineMarker +import de.bluecolored.bluemap.api.markers.MarkerSet +import de.bluecolored.bluemap.api.markers.POIMarker +import de.bluecolored.bluemap.api.markers.ShapeMarker +import de.bluecolored.bluemap.api.math.Color +import de.bluecolored.bluemap.api.math.Line +import de.bluecolored.bluemap.api.math.Shape +import littlechasiu.ctm.model.* + +object BlueMapIntegration { + var mapStyle = MapStyle() + + private const val BLUEMAP_TRACK_LABEL = "Create Track" + private const val BLUEMAP_TRACK_ID = "create-track" + private const val BLUEMAP_STATION_LABEL = "Create Station" + private const val BLUEMAP_STATION_ID = "create-station" + private const val BLUEMAP_TRAIN_LABEL = "Create Train" + private const val BLUEMAP_TRAIN_ID = "create-train" + private const val BLUEMAP_SIGNAL_LABEL = "Create Signal" + private const val BLUEMAP_SIGNAL_ID = "create-signal" + + private val CSS_NAMED_COLORS = mapOf( + "black" to "#000000", + "silver" to "#c0c0c0", + "gray" to "#808080", + "white" to "#ffffff", + "maroon" to "#800000", + "red" to "#ff0000", + "purple" to "#800080", + "fuchsia" to "#ff00ff", + "green" to "#008000", + "lime" to "#00ff00", + "olive" to "#808000", + "yellow" to "#ffff00", + "navy" to "#000080", + "blue" to "#0000ff", + "teal" to "#008080", + "aqua" to "#00ffff", + "aliceblue" to "#f0f8ff", + "antiquewhite" to "#faebd7", + "aquamarine" to "#7fffd4", + "azure" to "#f0ffff", + "beige" to "#f5f5dc", + "bisque" to "#ffe4c4", + "blanchedalmond" to "#ffebcd", + "blueviolet" to "#8a2be2", + "brown" to "#a52a2a", + "burlywood" to "#deb887", + "cadetblue" to "#5f9ea0", + "chartreuse" to "#7fff00", + "chocolate" to "#d2691e", + "coral" to "#ff7f50", + "cornflowerblue" to "#6495ed", + "cornsilk" to "#fff8dc", + "crimson" to "#dc143c", + "cyan" to "#00ffff", + "darkblue" to "#00008b", + "darkcyan" to "#008b8b", + "darkgoldenrod" to "#b8860b", + "darkgray" to "#a9a9a9", + "darkgreen" to "#006400", + "darkgrey" to "#a9a9a9", + "darkkhaki" to "#bdb76b", + "darkmagenta" to "#8b008b", + "darkolivegreen" to "#556b2f", + "darkorange" to "#ff8c00", + "darkorchid" to "#9932cc", + "darkred" to "#8b0000", + "darksalmon" to "#e9967a", + "darkseagreen" to "#8fbc8f", + "darkslateblue" to "#483d8b", + "darkslategray" to "#2f4f4f", + "darkslategrey" to "#2f4f4f", + "darkturquoise" to "#00ced1", + "darkviolet" to "#9400d3", + "deeppink" to "#ff1493", + "deepskyblue" to "#00bfff", + "dimgray" to "#696969", + "dimgrey" to "#696969", + "dodgerblue" to "#1e90ff", + "firebrick" to "#b22222", + "floralwhite" to "#fffaf0", + "forestgreen" to "#228b22", + "gainsboro" to "#dcdcdc", + "ghostwhite" to "#f8f8ff", + "gold" to "#ffd700", + "goldenrod" to "#daa520", + "greenyellow" to "#adff2f", + "grey" to "#808080", + "honeydew" to "#f0fff0", + "hotpink" to "#ff69b4", + "indianred" to "#cd5c5c", + "indigo" to "#4b0082", + "ivory" to "#fffff0", + "khaki" to "#f0e68c", + "lavender" to "#e6e6fa", + "lavenderblush" to "#fff0f5", + "lawngreen" to "#7cfc00", + "lemonchiffon" to "#fffacd", + "lightblue" to "#add8e6", + "lightcoral" to "#f08080", + "lightcyan" to "#e0ffff", + "lightgoldenrodyellow" to "#fafad2", + "lightgray" to "#d3d3d3", + "lightgreen" to "#90ee90", + "lightgrey" to "#d3d3d3", + "lightpink" to "#ffb6c1", + "lightsalmon" to "#ffa07a", + "lightseagreen" to "#20b2aa", + "lightskyblue" to "#87cefa", + "lightslategray" to "#778899", + "lightslategrey" to "#778899", + "lightsteelblue" to "#b0c4de", + "lightyellow" to "#ffffe0", + "limegreen" to "#32cd32", + "linen" to "#faf0e6", + "magenta" to "#ff00ff", + "mediumaquamarine" to "#66cdaa", + "mediumblue" to "#0000cd", + "mediumorchid" to "#ba55d3", + "mediumpurple" to "#9370db", + "mediumseagreen" to "#3cb371", + "mediumslateblue" to "#7b68ee", + "mediumspringgreen" to "#00fa9a", + "mediumturquoise" to "#48d1cc", + "mediumvioletred" to "#c71585", + "midnightblue" to "#191970", + "mintcream" to "#f5fffa", + "mistyrose" to "#ffe4e1", + "moccasin" to "#ffe4b5", + "navajowhite" to "#ffdead", + "oldlace" to "#fdf5e6", + "olivedrab" to "#6b8e23", + "orange" to "#ffa500", + "orangered" to "#ff4500", + "orchid" to "#da70d6", + "palegoldenrod" to "#eee8aa", + "palegreen" to "#98fb98", + "paleturquoise" to "#afeeee", + "palevioletred" to "#db7093", + "papayawhip" to "#ffefd5", + "peachpuff" to "#ffdab9", + "peru" to "#cd853f", + "pink" to "#ffc0cb", + "plum" to "#dda0dd", + "powderblue" to "#b0e0e6", + "rebeccapurple" to "#663399", + "rosybrown" to "#bc8f8f", + "royalblue" to "#4169e1", + "saddlebrown" to "#8b4513", + "salmon" to "#fa8072", + "sandybrown" to "#f4a460", + "seagreen" to "#2e8b57", + "seashell" to "#fff5ee", + "sienna" to "#a0522d", + "skyblue" to "#87ceeb", + "slateblue" to "#6a5acd", + "slategray" to "#708090", + "slategrey" to "#708090", + "snow" to "#fffafa", + "springgreen" to "#00ff7f", + "steelblue" to "#4682b4", + "tan" to "#d2b48c", + "thistle" to "#d8bfd8", + "tomato" to "#ff6347", + "transparent" to "#0000", + "turquoise" to "#40e0d0", + "violet" to "#ee82ee", + "wheat" to "#f5deb3", + "whitesmoke" to "#f5f5f5", + "yellowgreen" to "#9acd32", + ) + + private fun getMarkerSet(map: BlueMapMap, id: String, label: String): MarkerSet { + val mapMarkerSet = map.markerSets[id] + val markerSet: MarkerSet + + if (mapMarkerSet != null) { + markerSet = mapMarkerSet + } else { + markerSet = MarkerSet + .builder() + .label(label) + .build() + map.markerSets[id] = markerSet + } + + return markerSet + } + + private fun htmlEscape(str: String): String { + return str + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + } + + private fun getDefault(value: T?, default: T): T { + if (value == null) return default + return value + } + + private fun getCssColor(cssColor: String): String { + if (cssColor[0] == '#') { + return cssColor + } + + return CSS_NAMED_COLORS.getOrDefault(cssColor, "#000") + } + + private fun updateTrack(blueMap: BlueMapAPI, track: Edge) { + val markerBuilder = LineMarker + .builder() + .label(BLUEMAP_TRACK_LABEL) + .lineWidth(2) + + if (track.path.size == 4) { + // TODO: make curves work somehow + val pt0 = Vector3d(track.path[0].x, track.path[0].y + 1, track.path[0].z) + val pt3 = Vector3d(track.path[3].x, track.path[3].y + 1, track.path[3].z) + markerBuilder.line(Line(pt0, pt3)) + } else { + val pt0 = Vector3d(track.path[0].x, track.path[0].y + 1, track.path[0].z) + val pt1 = Vector3d(track.path[1].x, track.path[1].y + 1, track.path[1].z) + markerBuilder.line(Line(pt0, pt1)) + } + + blueMap.getWorld(track.dimension).ifPresent { world -> + for (map in world.maps) { + val markerSet = getMarkerSet(map, BLUEMAP_TRACK_ID, BLUEMAP_TRACK_LABEL) + markerSet.markers[BLUEMAP_TRACK_ID + "-" + markerSet.markers.size] = markerBuilder.build() + } + } + } + + private fun updateStation(blueMap: BlueMapAPI, station: Station) { + val marker = POIMarker + .builder() + .label(station.name) + .detail( + "
" + + "
Name: " + htmlEscape(station.name) + "
" + + "
Facing: " + + "" + + "
" + + "
" + ) + .position(station.location.x, station.location.y, station.location.z) + .build() + + blueMap.getWorld(station.dimension).ifPresent { world -> + for (map in world.maps) { + val markerSet = getMarkerSet(map, BLUEMAP_STATION_ID, BLUEMAP_STATION_LABEL) + markerSet.markers[BLUEMAP_STATION_ID + "-" + markerSet.markers.size] = marker + } + } + } + + private fun updateTrain(blueMap: BlueMapAPI, train: CreateTrain) { + for (car in train.cars) { + val pt0 = Vector3d( + getDefault(car.leading?.location?.x, 0.0), + getDefault(car.leading?.location?.y, 0.0), + getDefault(car.leading?.location?.z, 0.0) + ) + val pt1 = Vector3d( + getDefault(car.trailing?.location?.x, 0.0), + getDefault(car.trailing?.location?.y, 0.0), + getDefault(car.trailing?.location?.z, 0.0) + ) + val marker = LineMarker + .builder() + .label(train.name) + .lineColor(Color(getCssColor(mapStyle.colors.train))) + .lineWidth(12) + .line(Line(pt0, pt1)) + .build() + + blueMap.getWorld(getDefault(car.leading?.dimension, "minecraft:overworld")).ifPresent { world -> + for (map in world.maps) { + val markerSet = getMarkerSet(map, BLUEMAP_TRAIN_ID, BLUEMAP_TRAIN_LABEL) + markerSet.markers[BLUEMAP_TRAIN_ID + "-" + markerSet.markers.size] = marker + } + } + } + } + + private fun updateSignal(blueMap: BlueMapAPI, signal: Signal) { + val shape = Shape.createCircle(signal.location.x, signal.location.z, 1.0, 50) + val markerBuilder = ShapeMarker + .builder() + .label(BLUEMAP_SIGNAL_LABEL) + .shape(shape, signal.location.y.toFloat() + 1) + .lineColor(Color(getCssColor(mapStyle.colors.signal.outline))) + + if (signal.forward != null) { + when (signal.forward.state) { + SignalBlockEntity.SignalState.RED -> + markerBuilder.fillColor(Color(getCssColor(mapStyle.colors.signal.red))) + + SignalBlockEntity.SignalState.YELLOW -> + markerBuilder.fillColor(Color(getCssColor(mapStyle.colors.signal.yellow))) + + SignalBlockEntity.SignalState.GREEN -> + markerBuilder.fillColor(Color(getCssColor(mapStyle.colors.signal.green))) + + else -> {} + } + } else if (signal.reverse != null) { + when (signal.reverse.state) { + SignalBlockEntity.SignalState.RED -> + markerBuilder.fillColor(Color(getCssColor(mapStyle.colors.signal.red))) + + SignalBlockEntity.SignalState.YELLOW -> + markerBuilder.fillColor(Color(getCssColor(mapStyle.colors.signal.yellow))) + + SignalBlockEntity.SignalState.GREEN -> + markerBuilder.fillColor(Color(getCssColor(mapStyle.colors.signal.green))) + + else -> {} + } + } + + blueMap.getWorld(signal.dimension).ifPresent { world -> + for (map in world.maps) { + val markerSet = getMarkerSet(map, BLUEMAP_SIGNAL_ID, BLUEMAP_SIGNAL_LABEL) + markerSet.markers[BLUEMAP_SIGNAL_ID + "-" + markerSet.markers.size] = markerBuilder.build() + } + } + } + + fun update() { + BlueMapAPI.getInstance().ifPresent { blueMap -> + blueMap.maps.forEach { map -> + map.markerSets.remove(BLUEMAP_TRACK_ID) + map.markerSets.remove(BLUEMAP_STATION_ID) + map.markerSets.remove(BLUEMAP_TRAIN_ID) + map.markerSets.remove(BLUEMAP_SIGNAL_ID) + } + + for (track in TrackMap.network.tracks) { + updateTrack(blueMap, track) + } + + for (station in TrackMap.network.stations) { + updateStation(blueMap, station) + } + + for (train in TrackMap.trains.trains) { + updateTrain(blueMap, train) + } + + for (signal in TrackMap.signals.signals) { + updateSignal(blueMap, signal) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/littlechasiu/ctm/Extensions.kt b/src/main/kotlin/littlechasiu/ctm/Extensions.kt index 571889d..ebd650e 100644 --- a/src/main/kotlin/littlechasiu/ctm/Extensions.kt +++ b/src/main/kotlin/littlechasiu/ctm/Extensions.kt @@ -1,17 +1,35 @@ package littlechasiu.ctm +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData import com.simibubi.create.content.trains.entity.Carriage import com.simibubi.create.content.trains.entity.Train import com.simibubi.create.content.trains.entity.TravellingPoint import com.simibubi.create.content.trains.graph.TrackEdge import com.simibubi.create.content.trains.graph.TrackNode import com.simibubi.create.content.trains.graph.TrackNodeLocation +import com.simibubi.create.content.trains.schedule.ScheduleEntry +import com.simibubi.create.content.trains.schedule.ScheduleRuntime +import com.simibubi.create.content.trains.schedule.condition.FluidThresholdCondition +import com.simibubi.create.content.trains.schedule.condition.IdleCargoCondition +import com.simibubi.create.content.trains.schedule.condition.ItemThresholdCondition +import com.simibubi.create.content.trains.schedule.condition.PlayerPassengerCondition +import com.simibubi.create.content.trains.schedule.condition.RedstoneLinkCondition +import com.simibubi.create.content.trains.schedule.condition.ScheduleWaitCondition +import com.simibubi.create.content.trains.schedule.condition.ScheduledDelay +import com.simibubi.create.content.trains.schedule.condition.StationPoweredCondition +import com.simibubi.create.content.trains.schedule.condition.StationUnloadedCondition +import com.simibubi.create.content.trains.schedule.condition.TimeOfDayCondition +import com.simibubi.create.content.trains.schedule.destination.ChangeThrottleInstruction +import com.simibubi.create.content.trains.schedule.destination.ChangeTitleInstruction +import com.simibubi.create.content.trains.schedule.destination.DestinationInstruction +import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction import com.simibubi.create.foundation.utility.Couple import littlechasiu.ctm.model.* import net.minecraft.resources.ResourceKey import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 + fun MutableSet.replaceWith(other: Collection) { this.retainAll { other.contains(it) } this.addAll(other) @@ -84,4 +102,73 @@ val Train.sendable cars = carriages.map { it.sendable }.toList(), backwards = speed < 0, stopped = speed == 0.0, + schedule = runtime.sendable + ) + +val ScheduleInstruction.sendable + get() = + CreateScheduleInstruction( + destination = if (this is DestinationInstruction) filter else null, + newTitle = if (this is ChangeTitleInstruction) scheduleTitle else null, + newThrottle = if (this is ChangeThrottleInstruction) (throttle * 100).toString() + "%" else null, + ) + +val ScheduleWaitCondition.sendable + get() = + CreateScheduleCondition( + scheduledDelay = + if (this is ScheduledDelay) (value.toString() + unit.suffix) + else null, + timeOfDay = + if (this is TimeOfDayCondition) summary.second.string + " every " + (rotation / 1000) + " hour(s)" + else null, + fluidCargoCondition = + if (this is FluidThresholdCondition) getItem(0).displayName.string + " " + operator.ordinal + " " + threshold + " buckets" + else null, + itemCargoCondition = + if (this is ItemThresholdCondition) getItem(0).displayName.string + " " + operator.ordinal + " " + threshold + else null, + redstoneLink = + if (this is RedstoneLinkCondition) + "Frequency: " + freq.get(true).stack.displayName.string + "; " + freq.get(false).stack.displayName.string + + (if (lowActivation()) " is not powered" else " is powered") + else null, + playersSeated = + if (this is PlayerPassengerCondition) target.toString() + (if (canOvershoot()) " or above" else " exactly") + else null, + cargoInactivity = + if (this is IdleCargoCondition) (value.toString() + unit.suffix) + else null, + chunkUnloaded = + if (this is StationUnloadedCondition) "No info" + else null, + stationPowered = + if (this is StationPoweredCondition) "No info" + else null, + ) + +val ScheduleEntry.sendable + get() = + CreateScheduleEntry( + instruction = instruction.sendable, + conditions = conditions.map { a -> a.map { b -> b.sendable } }, + ) + +val ScheduleRuntime.sendable + get() = + if (schedule == null) + null + else + CreateSchedule( + currentEntry = currentEntry, + loops = schedule.cyclic, + entries = schedule.entries.map { it.sendable }) + +val GlobalTrainDisplayData.TrainDeparturePrediction.sendable + get() = + StationSummaryEntry( + scheduleTitle = scheduleTitle.string, + destination = destination, + trainName = train.name.string, + ticks = ticks, ) diff --git a/src/main/kotlin/littlechasiu/ctm/TrackMap.kt b/src/main/kotlin/littlechasiu/ctm/TrackMap.kt index e032808..6b45e4a 100644 --- a/src/main/kotlin/littlechasiu/ctm/TrackMap.kt +++ b/src/main/kotlin/littlechasiu/ctm/TrackMap.kt @@ -82,6 +82,7 @@ object TrackMap { server.mapView = config.mapView server.dimensions = config.dimensions server.layers = config.layers + BlueMapIntegration.mapStyle = config.mapStyle } private fun reload() { diff --git a/src/main/kotlin/littlechasiu/ctm/TrackWatcher.kt b/src/main/kotlin/littlechasiu/ctm/TrackWatcher.kt index 2dbb0ca..f3b4b85 100644 --- a/src/main/kotlin/littlechasiu/ctm/TrackWatcher.kt +++ b/src/main/kotlin/littlechasiu/ctm/TrackWatcher.kt @@ -1,6 +1,7 @@ package littlechasiu.ctm import com.simibubi.create.Create +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData import com.simibubi.create.content.trains.entity.Train import com.simibubi.create.content.trains.graph.TrackEdge import com.simibubi.create.content.trains.graph.TrackGraph @@ -60,8 +61,7 @@ class TrackWatcher() { try { update() } catch (e: Exception) { - TrackMap.LOGGER.warn("Exception during update loop") - e.printStackTrace() + TrackMap.LOGGER.warn("Exception during update loop", e) continue } delay(watchInterval) @@ -84,22 +84,20 @@ class TrackWatcher() { private val angle get() = internal.angleOn(edge) private val assembling get() = internal.assembling - override fun equals(other: Any?) = - other != null && javaClass == other.javaClass && - internal == (other as CreateStation).internal + override fun equals(other: Any?) = other != null && javaClass == other.javaClass && internal == (other as CreateStation).internal override fun hashCode() = internal.hashCode() val sendable - get() = - Station( - id = id, - name = name, - dimension = dimension, - location = location.sendable, - angle = angle, - assembling = assembling, - ) + get() = Station( + id = id, + name = name, + dimension = dimension, + location = location.sendable, + angle = angle, + assembling = assembling, + summary = GlobalTrainDisplayData.prepare(name, 6).map { it.sendable }, + ) } data class CreateSignal( @@ -128,39 +126,36 @@ class TrackWatcher() { this.reverseSegment = rev } - override fun equals(other: Any?) = - other != null && javaClass == other.javaClass && - internal == (other as CreateSignal).internal + override fun equals(other: Any?) = other != null && javaClass == other.javaClass && internal == (other as CreateSignal).internal override fun hashCode() = internal.hashCode() val sendable - get() = - Signal( - id = id, - dimension = dimension, - location = location.sendable, - forward = when (forwardState) { - null -> null - SignalState.INVALID -> null - else -> SignalSide( - type = forwardType!!, - state = forwardState!!, - angle = forwardAngle, - block = forwardGroup?.id, - ) - }, - reverse = when (reverseState) { - null -> null - SignalState.INVALID -> null - else -> SignalSide( - type = reverseType!!, - state = reverseState!!, - angle = reverseAngle, - block = reverseGroup?.id, - ) - }, - ) + get() = Signal( + id = id, + dimension = dimension, + location = location.sendable, + forward = when (forwardState) { + null -> null + SignalState.INVALID -> null + else -> SignalSide( + type = forwardType!!, + state = forwardState!!, + angle = forwardAngle, + block = forwardGroup?.id, + ) + }, + reverse = when (reverseState) { + null -> null + SignalState.INVALID -> null + else -> SignalSide( + type = reverseType!!, + state = reverseState!!, + angle = reverseAngle, + block = reverseGroup?.id, + ) + }, + ) } data class CreateSignalBlock( @@ -173,21 +168,17 @@ class TrackWatcher() { val segments = mutableListOf() val portals = mutableSetOf() - override fun equals(other: Any?) = - other != null && javaClass == other.javaClass && - internal == (other as CreateSignalBlock).internal && - segments == other.segments + override fun equals(other: Any?) = other != null && javaClass == other.javaClass && internal == (other as CreateSignalBlock).internal && segments == other.segments override fun hashCode() = internal.hashCode() val sendable - get() = - Block( - id = id, - occupied = occupied, - reserved = reserved, - segments = segments.map { it.sendable }, - ) + get() = Block( + id = id, + occupied = occupied, + reserved = reserved, + segments = segments.map { it.sendable }, + ) } private var nodes = mutableSetOf() @@ -197,34 +188,27 @@ class TrackWatcher() { private var trains = mutableSetOf() private var blocks = mutableMapOf() - fun portalsInBlock(block: UUID): Collection = - blocks[block]?.portals ?: listOf() + fun portalsInBlock(block: UUID): Collection = blocks[block]?.portals ?: listOf() val network - get() = - Network( - tracks = edges.map { it.sendable }.filterIsInstance().toList(), - portals = edges.map { it.sendable }.filterIsInstance().toList(), - stations = stations.map { it.sendable }.toList(), - ) + get() = Network( + tracks = edges.map { it.sendable }.filterIsInstance().toList(), + portals = edges.map { it.sendable }.filterIsInstance().toList(), + stations = stations.map { it.sendable }.toList(), + ) val signalStatus - get() = - SignalStatus( - signals = signals.map { it.sendable }.toList(), - ) + get() = SignalStatus( + signals = signals.map { it.sendable }.toList(), + ) val blockStatus - get() = - BlockStatus( - blocks = blocks.values.map { it.sendable }.toList() - ) + get() = BlockStatus(blocks = blocks.values.map { it.sendable }.toList()) val trainStatus - get() = - TrainStatus( - trains = trains.map { it.sendable }.toList(), - ) + get() = TrainStatus( + trains = trains.map { it.sendable }.toList(), + ) private suspend fun update() { val networkEdges = mutableMapOf>() @@ -236,8 +220,7 @@ class TrackWatcher() { RR.trackNetworks.forEach { (_, net) -> // Track topology val netNodes = net.nodes.map { net.locateNode(it) } - val netEdges = netNodes.map { net.getConnectionsFrom(it) } - .flatMap { it.values } + val netEdges = netNodes.map { net.getConnectionsFrom(it) }.flatMap { it.values } thisNodes.addAll(netNodes) thisEdges.addAll(netEdges) @@ -273,12 +256,10 @@ class TrackWatcher() { } } else { if (edge.edgeData.hasSignalBoundaries()) { - val signals = - edge.edgeData.points.filterIsInstance() - .sortedBy { - if (it.isPrimary(edge.node2)) it.position - else edge.length - it.position - } + val signals = edge.edgeData.points.filterIsInstance().sortedBy { + if (it.isPrimary(edge.node2)) it.position + else edge.length - it.position + } if (signals.isEmpty()) { return } @@ -286,39 +267,21 @@ class TrackWatcher() { val path = edge.path val segments = mutableListOf() - segments.add( - path.divideAt( - ( - if (signals[0].isPrimary(edge.node2)) signals[0].position - else edge.length - signals[0].position - ) / edge.length - ).first - ) + segments.add(path.divideAt((if (signals[0].isPrimary(edge.node2)) signals[0].position + else edge.length - signals[0].position) / edge.length).first) signals.windowed(2).forEach { sigs -> val leftSig = sigs[0] val rightSig = sigs[1] - val (rest, _) = path.divideAt( - (if (rightSig.isPrimary(edge.node2)) rightSig.position - else edge.length - rightSig.position) / edge.length - ) - val (_, seg) = rest.divideAt( - (if (leftSig.isPrimary(edge.node2)) leftSig.position - else edge.length - leftSig.position) / edge.length - ) + val (rest, _) = path.divideAt((if (rightSig.isPrimary(edge.node2)) rightSig.position + else edge.length - rightSig.position) / edge.length) + val (_, seg) = rest.divideAt((if (leftSig.isPrimary(edge.node2)) leftSig.position + else edge.length - leftSig.position) / edge.length) segments.add(seg) } - segments.add( - path.divideAt( - ( - if (signals.last() - .isPrimary(edge.node2) - ) signals.last().position - else edge.length - signals.last().position - ) / edge.length - ).second - ) + segments.add(path.divideAt((if (signals.last().isPrimary(edge.node2)) signals.last().position + else edge.length - signals.last().position) / edge.length).second) segments.windowed(2).zip(signals).forEach { (segs, sig) -> val leftSeg = segs[0] @@ -328,9 +291,7 @@ class TrackWatcher() { thisBlocks[sig.getGroup(edge.node2)]?.segments?.add(rightSeg) } } else { - thisBlocks[edge.edgeData.getEffectiveEdgeGroupId(net)]?.segments?.add( - edge.path - ) + thisBlocks[edge.edgeData.getEffectiveEdgeGroupId(net)]?.segments?.add(edge.path) } } } @@ -345,5 +306,7 @@ class TrackWatcher() { signalChannel.send(signalStatus) blockChannel.send(blockStatus) trainChannel.send(trainStatus) + + BlueMapIntegration.update() } } diff --git a/src/main/kotlin/littlechasiu/ctm/model/Network.kt b/src/main/kotlin/littlechasiu/ctm/model/Network.kt index 7735c51..eadaa4c 100644 --- a/src/main/kotlin/littlechasiu/ctm/model/Network.kt +++ b/src/main/kotlin/littlechasiu/ctm/model/Network.kt @@ -56,6 +56,14 @@ data class Portal( val to: DimensionLocation, ) +@Serializable +data class StationSummaryEntry( + val scheduleTitle: String, + val destination: String, + val trainName: String, + val ticks: Int, +) + @Serializable data class Station( @Serializable(with = UUIDSerializer::class) @@ -65,6 +73,7 @@ data class Station( val location: Point, val angle: Double, val assembling: Boolean, + val summary: List, ) @Serializable @@ -129,6 +138,7 @@ data class CreateTrain( val cars: List, val backwards: Boolean, val stopped: Boolean, + val schedule: CreateSchedule?, ) @Serializable diff --git a/src/main/kotlin/littlechasiu/ctm/model/Schedule.kt b/src/main/kotlin/littlechasiu/ctm/model/Schedule.kt new file mode 100644 index 0000000..77f5fae --- /dev/null +++ b/src/main/kotlin/littlechasiu/ctm/model/Schedule.kt @@ -0,0 +1,36 @@ +package littlechasiu.ctm.model + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateScheduleInstruction( + val destination: String?, + val newTitle: String?, + val newThrottle: String?, +) + +@Serializable +data class CreateScheduleCondition( + val scheduledDelay: String?, + val timeOfDay: String?, + val fluidCargoCondition: String?, + val itemCargoCondition: String?, + val redstoneLink: String?, + val playersSeated: String?, + val cargoInactivity: String?, + val chunkUnloaded: String?, + val stationPowered: String?, +) + +@Serializable +data class CreateScheduleEntry( + val instruction: CreateScheduleInstruction, + val conditions: List>, +) + +@Serializable +data class CreateSchedule( + val currentEntry: Int, + val loops: Boolean, + val entries: List, +) diff --git a/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js b/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js index 8566466..3233575 100644 --- a/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js +++ b/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js @@ -60,6 +60,15 @@ fetch("api/config.json") startMapUpdates() }) +function htmlEscape(str) { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'") +} + function startMapUpdates() { const dmgr = new DataManager() @@ -87,12 +96,39 @@ function startMapUpdates() { }) stations.forEach((stn) => { + const scheduleHtml = + stn.summary.map((entry) => { + let time = ""; + + if (entry.ticks === -1 || entry.ticks >= 12000 - 15 * 20) { + time = "later" + } else if (entry.ticks < 200) { + time = "now" + } else { + time = "in " + + let min = Math.floor(entry.ticks / 1200) + let sec = Math.floor(entry.ticks / 20) % 60 + sec = Math.ceil(sec / 15) * 15 + + if (sec === 60) { + min++ + sec = 0 + } + + time += min > 0 ? min : sec; + time += min > 0 ? "mins" : "secs"; + } + + return `
Due ${time} | ${entry.trainName} | ${entry.scheduleTitle}
`; + }).join(""); + L.marker(xz(stn.location), { icon: stationIcon, rotationAngle: stn.angle, pane: "stations", }) - .bindTooltip(stn.name, { + .bindTooltip(stn.name + scheduleHtml, { className: "station-name", direction: "top", offset: L.point(0, -12), @@ -197,6 +233,15 @@ function startMapUpdates() { ] : [[car.leading.dimension, [xz(car.leading.location), xz(car.trailing.location)]]] + const scheduleHtml = + train.schedule ? + train.schedule.entries.map((entry, i) => { + let entryHtml = train.schedule.currentEntry === i ? "> " : ""; + if (entry.instruction.destination) entryHtml += htmlEscape(entry.instruction.destination); + return `
${entryHtml}
`; + }).join("\n") : + ""; + parts.map(([dim, part]) => L.polyline(part, { weight: 12, @@ -205,9 +250,9 @@ function startMapUpdates() { pane: "trains", }) .bindTooltip( - train.cars.length === 1 + (train.cars.length === 1 ? train.name - : `${train.name} ${i + 1}`, + : `${train.name} ${i + 1}`) + scheduleHtml, { className: "train-name", direction: "right", From 629b6a414e4eecee7d5436dd350a7df4cf2d4f42 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 23 Jan 2024 09:28:09 +0800 Subject: [PATCH 02/11] Add curved track support --- .../littlechasiu/ctm/BlueMapIntegration.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 57eb50a..8da4209 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -16,6 +16,8 @@ import littlechasiu.ctm.model.* object BlueMapIntegration { var mapStyle = MapStyle() + private const val BLUEMAP_TRACK_CURVE_POINTS = 10 + private const val BLUEMAP_TRACK_LABEL = "Create Track" private const val BLUEMAP_TRACK_ID = "create-track" private const val BLUEMAP_STATION_LABEL = "Create Station" @@ -216,6 +218,24 @@ object BlueMapIntegration { return CSS_NAMED_COLORS.getOrDefault(cssColor, "#000") } + private fun bezierPoints(points: List, yOffset: Int, numPoints: Int): List { + // https://denisrizov.com/2016/06/02/bezier-curves-unity-package-included/ + val vecPoints = points.map { Vector3d(it.x, it.y + yOffset, it.z) } + return (0 until numPoints) + .map { it.toFloat() / numPoints } + .map { t -> + val u = 1.0 - t + val t2 = t * t + val u2 = u * u + val t3 = t2 * t + val u3 = u2 * u + (vecPoints[0].mul(u3)) + .add(vecPoints[1].mul(3.0 * u2 * t)) + .add(vecPoints[2].mul(3.0 * u * t2)) + .add(vecPoints[3].mul(t3)) + } + } + private fun updateTrack(blueMap: BlueMapAPI, track: Edge) { val markerBuilder = LineMarker .builder() @@ -223,10 +243,8 @@ object BlueMapIntegration { .lineWidth(2) if (track.path.size == 4) { - // TODO: make curves work somehow - val pt0 = Vector3d(track.path[0].x, track.path[0].y + 1, track.path[0].z) - val pt3 = Vector3d(track.path[3].x, track.path[3].y + 1, track.path[3].z) - markerBuilder.line(Line(pt0, pt3)) + // TODO: use HtmlMarker + svg for smooth curves? + markerBuilder.line(Line(bezierPoints(track.path, 1, BLUEMAP_TRACK_CURVE_POINTS))) } else { val pt0 = Vector3d(track.path[0].x, track.path[0].y + 1, track.path[0].z) val pt1 = Vector3d(track.path[1].x, track.path[1].y + 1, track.path[1].z) From 2303b9692a4a897becdeb81b4a7c18219db8f12e Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:02:30 +0800 Subject: [PATCH 03/11] Fix curved points not rendering completely --- src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 8da4209..1ec78c9 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -221,7 +221,7 @@ object BlueMapIntegration { private fun bezierPoints(points: List, yOffset: Int, numPoints: Int): List { // https://denisrizov.com/2016/06/02/bezier-curves-unity-package-included/ val vecPoints = points.map { Vector3d(it.x, it.y + yOffset, it.z) } - return (0 until numPoints) + return (0..numPoints) .map { it.toFloat() / numPoints } .map { t -> val u = 1.0 - t From edeb1a9e4299d324658ff1e5918d22da15b2b8dc Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:25:14 +0800 Subject: [PATCH 04/11] reduce curve decimal precision --- src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 1ec78c9..2f8c089 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -233,6 +233,7 @@ object BlueMapIntegration { .add(vecPoints[1].mul(3.0 * u2 * t)) .add(vecPoints[2].mul(3.0 * u * t2)) .add(vecPoints[3].mul(t3)) + .mul(1000.0).round().div(1000.0) // truncate to 3 decimal places } } From 71718add1b9f24ad935becb1b696a64e4c912c5a Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Wed, 24 Jan 2024 22:42:24 +0800 Subject: [PATCH 05/11] Optimize track markers Large number of markers hurt BlueMap performance; and CTM often provides small track segments that could be combined into larger segments. This commit combines about 50% of tracks, depending on the track layout itself. Works best with straight, non-branching tracks. --- .../littlechasiu/ctm/BlueMapIntegration.kt | 85 ++++++++++++++----- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 2f8c089..73f2529 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -1,6 +1,7 @@ package littlechasiu.ctm import com.flowpowered.math.vector.Vector3d +import com.flowpowered.math.vector.Vector3l import com.simibubi.create.content.trains.signal.SignalBlockEntity import de.bluecolored.bluemap.api.BlueMapAPI import de.bluecolored.bluemap.api.BlueMapMap @@ -179,6 +180,14 @@ object BlueMapIntegration { "yellowgreen" to "#9acd32", ) + data class IntEdge( + val dimension: String, + val path: List + ) { + constructor(edge: Edge): + this(edge.dimension, edge.path.map { Vector3l(it.x * 10000, it.y * 10000, it.z * 10000 )}) + } + private fun getMarkerSet(map: BlueMapMap, id: String, label: String): MarkerSet { val mapMarkerSet = map.markerSets[id] val markerSet: MarkerSet @@ -237,25 +246,63 @@ object BlueMapIntegration { } } - private fun updateTrack(blueMap: BlueMapAPI, track: Edge) { - val markerBuilder = LineMarker - .builder() - .label(BLUEMAP_TRACK_LABEL) - .lineWidth(2) - - if (track.path.size == 4) { - // TODO: use HtmlMarker + svg for smooth curves? - markerBuilder.line(Line(bezierPoints(track.path, 1, BLUEMAP_TRACK_CURVE_POINTS))) - } else { - val pt0 = Vector3d(track.path[0].x, track.path[0].y + 1, track.path[0].z) - val pt1 = Vector3d(track.path[1].x, track.path[1].y + 1, track.path[1].z) - markerBuilder.line(Line(pt0, pt1)) + private fun updateTracks(blueMap: BlueMapAPI, tracks: List) { + // the edges provided by TrackMap contains various duplicates in different directions + // to optimize this we normalize them into one direction and remove overlaps as much as possible + // there was a problem finding duplicates due to double's precision so each edge was truncated to 4 decimal places + // TODO: fix z-fighting due to overlapping sections + val intEdges = emptySet().toMutableSet() + val sortedEdges = emptyList().toMutableList() + for (edge in tracks) { + val first = edge.path.first() + val last = edge.path.last() + val normalizedEdge = if (first.x > last.x || first.y > last.y || first.z > last.z) Edge(edge.dimension, edge.path.reversed()) else edge + if (intEdges.add(IntEdge(normalizedEdge))) + sortedEdges.add(normalizedEdge) } + sortedEdges.sortWith(compareBy({ it.dimension }, { it.path[0].x }, { it.path[0].y }, { it.path[0].z })) + + // optimize the paths by merging adjacent paths as much as possible + var lineBuilder = Line.builder() + var lineCount = 0 + sortedEdges.forEachIndexed { index, edge -> + val nextEdge: Edge? = sortedEdges.elementAtOrNull(index + 1) + val points = if (edge.path.size == 4) bezierPoints(edge.path, 1, BLUEMAP_TRACK_CURVE_POINTS) + else edge.path.map { Vector3d(it.x, it.y + 1, it.z) } + + // add the path (only the starting point for straight paths) + if (edge.path.size == 4) { + lineBuilder.addPoints(*points.toTypedArray()) + lineCount += points.size + } else if (lineCount == 0) { + lineBuilder.addPoint(points.first()) + lineCount++ + } - blueMap.getWorld(track.dimension).ifPresent { world -> - for (map in world.maps) { - val markerSet = getMarkerSet(map, BLUEMAP_TRACK_ID, BLUEMAP_TRACK_LABEL) - markerSet.markers[BLUEMAP_TRACK_ID + "-" + markerSet.markers.size] = markerBuilder.build() + // end the path if not adjacent or this is the last edge + if (nextEdge == null || edge.dimension != nextEdge.dimension || edge.path.last() != nextEdge.path.first()) { + if (edge.path.size != 4) { + lineBuilder.addPoint(points.last()) + lineCount++ + } + + if (lineCount > 1) { + val markerBuilder = LineMarker + .builder() + .label(BLUEMAP_TRACK_LABEL) + .lineWidth(2) + .line(lineBuilder.build()) + + blueMap.getWorld(edge.dimension).ifPresent { world -> + for (map in world.maps) { + val markerSet = getMarkerSet(map, BLUEMAP_TRACK_ID, BLUEMAP_TRACK_LABEL) + markerSet.markers[BLUEMAP_TRACK_ID + "-" + markerSet.markers.size] = markerBuilder.build() + } + } + } + + lineBuilder = Line.builder() + lineCount = 0 } } } @@ -365,9 +412,7 @@ object BlueMapIntegration { map.markerSets.remove(BLUEMAP_SIGNAL_ID) } - for (track in TrackMap.network.tracks) { - updateTrack(blueMap, track) - } + updateTracks(blueMap, TrackMap.network.tracks) for (station in TrackMap.network.stations) { updateStation(blueMap, station) From f8245cd0a3f8b9a6edf2a52fb8f511c57b63e696 Mon Sep 17 00:00:00 2001 From: C1200 Date: Wed, 24 Jan 2024 11:36:31 +0000 Subject: [PATCH 06/11] Made schedule info display look nicer --- .../static/assets/css/create-track-map.css | 23 +++++++ .../ctm/static/assets/js/create-track-map.js | 67 ++++++++++--------- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/main/resources/assets/littlechasiu/ctm/static/assets/css/create-track-map.css b/src/main/resources/assets/littlechasiu/ctm/static/assets/css/create-track-map.css index 21434b3..e3e759b 100644 --- a/src/main/resources/assets/littlechasiu/ctm/static/assets/css/create-track-map.css +++ b/src/main/resources/assets/littlechasiu/ctm/static/assets/css/create-track-map.css @@ -79,6 +79,29 @@ body { fill: var(--lead-car-color); } +.train-schedule { + margin: 0; + padding-left: 1rem; + list-style-type: none; +} +.train-schedule .train-schedule-current { + list-style-type: disc; +} + +.station-schedule, +.station-schedule th, +.station-schedule td { + border: 1px solid currentColor; + border-collapse: collapse; +} +.station-schedule th, +.station-schedule td { + padding: .2rem .5rem; +} +.station-schedule td { + font-weight: normal; +} + .coords-control { font-family: var(--ui-font); font-size: 16px; diff --git a/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js b/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js index 3233575..2aeded0 100644 --- a/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js +++ b/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js @@ -97,31 +97,33 @@ function startMapUpdates() { stations.forEach((stn) => { const scheduleHtml = - stn.summary.map((entry) => { - let time = ""; - - if (entry.ticks === -1 || entry.ticks >= 12000 - 15 * 20) { - time = "later" - } else if (entry.ticks < 200) { - time = "now" - } else { - time = "in " - - let min = Math.floor(entry.ticks / 1200) - let sec = Math.floor(entry.ticks / 20) % 60 - sec = Math.ceil(sec / 15) * 15 - - if (sec === 60) { - min++ - sec = 0 - } - - time += min > 0 ? min : sec; - time += min > 0 ? "mins" : "secs"; - } - - return `
Due ${time} | ${entry.trainName} | ${entry.scheduleTitle}
`; - }).join(""); + "" + + stn.summary.map((entry) => { + let time = ""; + + if (entry.ticks === -1 || entry.ticks >= 12000 - 15 * 20) { + time = "later" + } else if (entry.ticks < 200) { + time = "now" + } else { + time = "in " + + let min = Math.floor(entry.ticks / 1200) + let sec = Math.floor(entry.ticks / 20) % 60 + sec = Math.ceil(sec / 15) * 15 + + if (sec === 60) { + min++ + sec = 0 + } + + time += min > 0 ? min : sec; + time += min > 0 ? "mins" : "secs"; + } + + return ``; + }).join("") + + "
DueTrainDestination
${time}${htmlEscape(entry.trainName)}${htmlEscape(entry.scheduleTitle)}
"; L.marker(xz(stn.location), { icon: stationIcon, @@ -235,11 +237,16 @@ function startMapUpdates() { const scheduleHtml = train.schedule ? - train.schedule.entries.map((entry, i) => { - let entryHtml = train.schedule.currentEntry === i ? "> " : ""; - if (entry.instruction.destination) entryHtml += htmlEscape(entry.instruction.destination); - return `
${entryHtml}
`; - }).join("\n") : + "
    " + + train.schedule.entries.map((entry, i) => { + let entryHtml = ""; + const className = train.schedule.currentEntry === i ? "train-schedule-current" : "" + + if (entry.instruction.destination) entryHtml += htmlEscape(entry.instruction.destination); + + return `
  • ${entryHtml}`; + }).join("\n") + + "
" : ""; parts.map(([dim, part]) => From aefe821e625985d4c78828a3ceb50313742def13 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:23:44 +0800 Subject: [PATCH 07/11] Optimize markers more Simplify the line merging algorithm and take advantage of multiple sort passes to merge more lines. --- .../littlechasiu/ctm/BlueMapIntegration.kt | 126 ++++++++++-------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 73f2529..155ac6b 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -180,14 +180,6 @@ object BlueMapIntegration { "yellowgreen" to "#9acd32", ) - data class IntEdge( - val dimension: String, - val path: List - ) { - constructor(edge: Edge): - this(edge.dimension, edge.path.map { Vector3l(it.x * 10000, it.y * 10000, it.z * 10000 )}) - } - private fun getMarkerSet(map: BlueMapMap, id: String, label: String): MarkerSet { val mapMarkerSet = map.markerSets[id] val markerSet: MarkerSet @@ -227,11 +219,12 @@ object BlueMapIntegration { return CSS_NAMED_COLORS.getOrDefault(cssColor, "#000") } - private fun bezierPoints(points: List, yOffset: Int, numPoints: Int): List { + private fun bezier(points: List): MutableList { // https://denisrizov.com/2016/06/02/bezier-curves-unity-package-included/ - val vecPoints = points.map { Vector3d(it.x, it.y + yOffset, it.z) } - return (0..numPoints) - .map { it.toFloat() / numPoints } + val vecPoints = points.map { Vector3d(it.x, it.y + 1, it.z) } + return (0..BLUEMAP_TRACK_CURVE_POINTS) + .asSequence() + .map { it.toFloat() / BLUEMAP_TRACK_CURVE_POINTS } .map { t -> val u = 1.0 - t val t2 = t * t @@ -244,65 +237,80 @@ object BlueMapIntegration { .add(vecPoints[3].mul(t3)) .mul(1000.0).round().div(1000.0) // truncate to 3 decimal places } + .toMutableList() } - private fun updateTracks(blueMap: BlueMapAPI, tracks: List) { - // the edges provided by TrackMap contains various duplicates in different directions - // to optimize this we normalize them into one direction and remove overlaps as much as possible - // there was a problem finding duplicates due to double's precision so each edge was truncated to 4 decimal places - // TODO: fix z-fighting due to overlapping sections - val intEdges = emptySet().toMutableSet() - val sortedEdges = emptyList().toMutableList() - for (edge in tracks) { - val first = edge.path.first() - val last = edge.path.last() - val normalizedEdge = if (first.x > last.x || first.y > last.y || first.z > last.z) Edge(edge.dimension, edge.path.reversed()) else edge - if (intEdges.add(IntEdge(normalizedEdge))) - sortedEdges.add(normalizedEdge) - } - sortedEdges.sortWith(compareBy({ it.dimension }, { it.path[0].x }, { it.path[0].y }, { it.path[0].z })) - - // optimize the paths by merging adjacent paths as much as possible - var lineBuilder = Line.builder() - var lineCount = 0 - sortedEdges.forEachIndexed { index, edge -> - val nextEdge: Edge? = sortedEdges.elementAtOrNull(index + 1) - val points = if (edge.path.size == 4) bezierPoints(edge.path, 1, BLUEMAP_TRACK_CURVE_POINTS) - else edge.path.map { Vector3d(it.x, it.y + 1, it.z) } - - // add the path (only the starting point for straight paths) - if (edge.path.size == 4) { - lineBuilder.addPoints(*points.toTypedArray()) - lineCount += points.size - } else if (lineCount == 0) { - lineBuilder.addPoint(points.first()) - lineCount++ - } + private fun getUnitVector(a: Vector3d, b: Vector3d): Vector3d { + return a.sub(b).normalize() + } - // end the path if not adjacent or this is the last edge - if (nextEdge == null || edge.dimension != nextEdge.dimension || edge.path.last() != nextEdge.path.first()) { - if (edge.path.size != 4) { - lineBuilder.addPoint(points.last()) - lineCount++ + private fun mergeLines(input: MutableList>) { + val lineIterator = input.listIterator() + var prevLine: MutableList? = null + while (lineIterator.hasNext()) { + val currentLine = lineIterator.next() + // lines are adjacent, try to merge + if (prevLine != null && prevLine.last() == currentLine.first()) { + // if both lines are heading to the same direction, we can omit the midpoint when joining + if (getUnitVector(prevLine[prevLine.size - 2], prevLine[prevLine.size - 1]) == getUnitVector(currentLine[0], currentLine[1])) { + prevLine.removeLast() } + prevLine.addAll(currentLine) + lineIterator.remove() + } else { + prevLine = currentLine + } + } + } - if (lineCount > 1) { + private fun updateTracks(blueMap: BlueMapAPI, tracks: List) { + val dimensions = tracks.groupBy { it.dimension } + dimensions.forEach { (dimension, edges) -> + blueMap.getWorld(dimension).ifPresent { world -> + // CTM tracks has tiny, overlapping segments, sometimes in both directions, which slows down BlueMap + // We normalize all the lines in one direction, filter out points due to precision issues, render + // Bézier curves into straight lines, sort them based on starting / ending points and merge them. + // FIXME: severe Z-fighting due to large amount of overlapping segments + val lines = edges + .asSequence() + .map { edge -> + // normalize paths to point in +ve + val first = edge.path.first() + val last = edge.path.last() + if (first.x > last.x || first.y > last.y || first.z > last.z) { + edge.path.asReversed() + } else { + edge.path + } + } + .distinctBy { it.map { p -> Vector3l(p.x * 10000, p.y * 10000, p.z * 10000) } } + .map {path -> + when (path.size) { + 4 -> bezier(path) + 2 -> path.asSequence().map { Vector3d(it.x, it.y + 1, it.z) }.toMutableList() + else -> throw IllegalStateException("unsupported edge type") + } + } + .toMutableList() + + // your mileage may vary + lines.sortWith(compareBy({ it.last().x }, { it.last().y }, { it.last().z })) + mergeLines(lines) + lines.sortWith(compareBy({ it[0].x }, { it[0].y }, { it[0].z })) + mergeLines(lines) + + for (line in lines) { val markerBuilder = LineMarker .builder() .label(BLUEMAP_TRACK_LABEL) .lineWidth(2) - .line(lineBuilder.build()) + .line(Line(line)) - blueMap.getWorld(edge.dimension).ifPresent { world -> - for (map in world.maps) { - val markerSet = getMarkerSet(map, BLUEMAP_TRACK_ID, BLUEMAP_TRACK_LABEL) - markerSet.markers[BLUEMAP_TRACK_ID + "-" + markerSet.markers.size] = markerBuilder.build() - } + for (map in world.maps) { + val markerSet = getMarkerSet(map, BLUEMAP_TRACK_ID, BLUEMAP_TRACK_LABEL) + markerSet.markers[BLUEMAP_TRACK_ID + "-" + markerSet.markers.size] = markerBuilder.build() } } - - lineBuilder = Line.builder() - lineCount = 0 } } } From ce0872f5b303a0cc005d84e3bb4517c29c545c92 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 28 Jan 2024 00:41:47 +0800 Subject: [PATCH 08/11] Optimize markers even more By performing a tiny lookahead search when merging, we are able to optimize parallel tracks up to 30% of original number of tracks provided by the CTM API. --- .../littlechasiu/ctm/BlueMapIntegration.kt | 143 +++++++++++------- 1 file changed, 88 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 155ac6b..51b4874 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -13,6 +13,7 @@ import de.bluecolored.bluemap.api.math.Color import de.bluecolored.bluemap.api.math.Line import de.bluecolored.bluemap.api.math.Shape import littlechasiu.ctm.model.* +import kotlin.math.min object BlueMapIntegration { var mapStyle = MapStyle() @@ -180,6 +181,75 @@ object BlueMapIntegration { "yellowgreen" to "#9acd32", ) + class BlueMapLine(points: List, yOffset: Double = 1.0) { + private val mutablePath: MutableList = mutableListOf() + private lateinit var longPath: List + + val path: List = mutablePath + var tomb = false + + init { + mutablePath.addAll(when (points.size) { + 4 -> bezier(points, yOffset) + 2 -> points.asSequence().map { Vector3d(it.x, it.y + yOffset, it.z) } + else -> throw IllegalArgumentException("unsupported path type") + }) + updateLongPath() + } + + fun merge(line: BlueMapLine): Boolean { + if (tomb || line.tomb || path.last() != line.path.first()) return false + // if both lines are heading to the same direction, we can omit the midpoint when joining + if (unitVector(path[path.size - 2], path[path.size - 1]) == unitVector(line.path[0], line.path[1])) { + mutablePath.removeLast() + } + mutablePath.addAll(line.path) + updateLongPath() + return true + } + + private fun updateLongPath() { + longPath = path.map { it.mul(10000.0).toLong() } + } + + private fun unitVector(a: Vector3d, b: Vector3d): Vector3d { + return a.sub(b).normalize() + } + + private fun bezier(points: List, yOffset: Double): Sequence { + // https://denisrizov.com/2016/06/02/bezier-curves-unity-package-included/ + val vecPoints = points.map { Vector3d(it.x, it.y + yOffset, it.z) } + return (0..BLUEMAP_TRACK_CURVE_POINTS) + .asSequence() + .map { it.toFloat() / BLUEMAP_TRACK_CURVE_POINTS } + .map { t -> + val u = 1.0 - t + val t2 = t * t + val u2 = u * u + val t3 = t2 * t + val u3 = u2 * u + (vecPoints[0].mul(u3)) + .add(vecPoints[1].mul(3.0 * u2 * t)) + .add(vecPoints[2].mul(3.0 * u * t2)) + .add(vecPoints[3].mul(t3)) + .mul(1000.0).round().div(1000.0) // truncate to 3 decimal places + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BlueMapLine + + return longPath == other.longPath + } + + override fun hashCode(): Int { + return longPath.hashCode() + } + } + private fun getMarkerSet(map: BlueMapMap, id: String, label: String): MarkerSet { val mapMarkerSet = map.markerSets[id] val markerSet: MarkerSet @@ -219,48 +289,18 @@ object BlueMapIntegration { return CSS_NAMED_COLORS.getOrDefault(cssColor, "#000") } - private fun bezier(points: List): MutableList { - // https://denisrizov.com/2016/06/02/bezier-curves-unity-package-included/ - val vecPoints = points.map { Vector3d(it.x, it.y + 1, it.z) } - return (0..BLUEMAP_TRACK_CURVE_POINTS) - .asSequence() - .map { it.toFloat() / BLUEMAP_TRACK_CURVE_POINTS } - .map { t -> - val u = 1.0 - t - val t2 = t * t - val u2 = u * u - val t3 = t2 * t - val u3 = u2 * u - (vecPoints[0].mul(u3)) - .add(vecPoints[1].mul(3.0 * u2 * t)) - .add(vecPoints[2].mul(3.0 * u * t2)) - .add(vecPoints[3].mul(t3)) - .mul(1000.0).round().div(1000.0) // truncate to 3 decimal places - } - .toMutableList() - } - - private fun getUnitVector(a: Vector3d, b: Vector3d): Vector3d { - return a.sub(b).normalize() - } - - private fun mergeLines(input: MutableList>) { - val lineIterator = input.listIterator() - var prevLine: MutableList? = null - while (lineIterator.hasNext()) { - val currentLine = lineIterator.next() - // lines are adjacent, try to merge - if (prevLine != null && prevLine.last() == currentLine.first()) { - // if both lines are heading to the same direction, we can omit the midpoint when joining - if (getUnitVector(prevLine[prevLine.size - 2], prevLine[prevLine.size - 1]) == getUnitVector(currentLine[0], currentLine[1])) { - prevLine.removeLast() - } - prevLine.addAll(currentLine) - lineIterator.remove() - } else { - prevLine = currentLine + private fun mergeLines(lines: MutableList, lookahead: Int) { + for (i in lines.indices) { + val line = lines[i] + if (line.tomb) continue + for (j in i + 1..i + lookahead) { + val nextLine = lines.getOrNull(j) ?: break + if (nextLine.tomb) continue + if (line.merge(nextLine)) + nextLine.tomb = true } } + lines.removeIf { it.tomb } } private fun updateTracks(blueMap: BlueMapAPI, tracks: List) { @@ -278,33 +318,26 @@ object BlueMapIntegration { val first = edge.path.first() val last = edge.path.last() if (first.x > last.x || first.y > last.y || first.z > last.z) { - edge.path.asReversed() + BlueMapLine(edge.path.asReversed()) } else { - edge.path - } - } - .distinctBy { it.map { p -> Vector3l(p.x * 10000, p.y * 10000, p.z * 10000) } } - .map {path -> - when (path.size) { - 4 -> bezier(path) - 2 -> path.asSequence().map { Vector3d(it.x, it.y + 1, it.z) }.toMutableList() - else -> throw IllegalStateException("unsupported edge type") + BlueMapLine(edge.path) } } + .distinct() .toMutableList() // your mileage may vary - lines.sortWith(compareBy({ it.last().x }, { it.last().y }, { it.last().z })) - mergeLines(lines) - lines.sortWith(compareBy({ it[0].x }, { it[0].y }, { it[0].z })) - mergeLines(lines) + lines.sortWith(compareBy({ it.path[0].x }, { it.path[0].y }, { it.path[0].z })) + // The merging algorithm performs a lookahead search (defaults to 8 paths) + // which significantly improves merging for maps with parallel tracks + mergeLines(lines, min(lines.size / 2, 8)) for (line in lines) { val markerBuilder = LineMarker .builder() .label(BLUEMAP_TRACK_LABEL) .lineWidth(2) - .line(Line(line)) + .line(Line(line.path)) for (map in world.maps) { val markerSet = getMarkerSet(map, BLUEMAP_TRACK_ID, BLUEMAP_TRACK_LABEL) From a0ab6627cec4f93af6ec4c1d85a2e27274c9ede1 Mon Sep 17 00:00:00 2001 From: C1200 Date: Sat, 27 Jan 2024 22:59:28 +0000 Subject: [PATCH 09/11] Add schedule info to BlueMap --- .../littlechasiu/ctm/BlueMapIntegration.kt | 59 ++++++++++++++++++- .../ctm/static/assets/js/create-track-map.js | 6 +- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 51b4874..5060195 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -14,6 +14,7 @@ import de.bluecolored.bluemap.api.math.Line import de.bluecolored.bluemap.api.math.Shape import littlechasiu.ctm.model.* import kotlin.math.min +import kotlin.math.ceil object BlueMapIntegration { var mapStyle = MapStyle() @@ -349,15 +350,44 @@ object BlueMapIntegration { } private fun updateStation(blueMap: BlueMapAPI, station: Station) { + val scheduleHtml = "
    " + + station.summary.joinToString(separator = "") { + var time: String + + if (it.ticks == -1 || it.ticks >= 12000 - 15 * 20) { + time = "later" + } else if (it.ticks < 200) { + time = "now" + } else { + time = "in " + + var min = it.ticks.floorDiv(1200) + var sec = it.ticks.floorDiv(20) % 60 + sec = ceil(sec / 15f).toInt() * 15 + + if (sec == 60) { + min += 1 + sec = 0 + } + + time += if (min > 0) min else sec + time += if (min > 0) "mins" else "secs" + } + + "
  1. Due: $time, Train: ${htmlEscape(it.trainName)}, Destination: ${htmlEscape(it.scheduleTitle)}
  2. " + } + + "
" + val marker = POIMarker .builder() .label(station.name) .detail( "
" + - "
Name: " + htmlEscape(station.name) + "
" + + "
" + htmlEscape(station.name) + "
" + "
Facing: " + "" + "
" + + scheduleHtml + "
" ) .position(station.location.x, station.location.y, station.location.z) @@ -372,7 +402,7 @@ object BlueMapIntegration { } private fun updateTrain(blueMap: BlueMapAPI, train: CreateTrain) { - for (car in train.cars) { + for ((i, car) in train.cars.withIndex()) { val pt0 = Vector3d( getDefault(car.leading?.location?.x, 0.0), getDefault(car.leading?.location?.y, 0.0), @@ -383,9 +413,32 @@ object BlueMapIntegration { getDefault(car.trailing?.location?.y, 0.0), getDefault(car.trailing?.location?.z, 0.0) ) + + val scheduleHtml = + if (train.schedule != null) + "
    " + + train.schedule.entries.mapIndexed { entryIndex, entry -> + var entryHtml = "" + val style = if (train.schedule.currentEntry == entryIndex) "" else "list-style-type: none" + + if (entry.instruction.destination != null) entryHtml += htmlEscape(entry.instruction.destination) + + if (entryHtml == "") return@mapIndexed "" + "
  • $entryHtml
  • " + }.joinToString(separator = "") + + "
" + else + "" + val marker = LineMarker .builder() - .label(train.name) + .label( + if (train.cars.size == 1) + train.name + else + "${train.name} (${i + 1})" + ) + .detail("
${htmlEscape(train.name)} (${i + 1})
$scheduleHtml") .lineColor(Color(getCssColor(mapStyle.colors.train))) .lineWidth(12) .line(Line(pt0, pt1)) diff --git a/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js b/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js index 2aeded0..0fae896 100644 --- a/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js +++ b/src/main/resources/assets/littlechasiu/ctm/static/assets/js/create-track-map.js @@ -99,7 +99,7 @@ function startMapUpdates() { const scheduleHtml = "" + stn.summary.map((entry) => { - let time = ""; + let time if (entry.ticks === -1 || entry.ticks >= 12000 - 15 * 20) { time = "later" @@ -123,7 +123,7 @@ function startMapUpdates() { return ``; }).join("") + - "
DueTrainDestination
${time}${htmlEscape(entry.trainName)}${htmlEscape(entry.scheduleTitle)}
"; + "" L.marker(xz(stn.location), { icon: stationIcon, @@ -244,7 +244,7 @@ function startMapUpdates() { if (entry.instruction.destination) entryHtml += htmlEscape(entry.instruction.destination); - return `
  • ${entryHtml}`; + return `
  • ${entryHtml}
  • `; }).join("\n") + "" : ""; From bb5cfd62e49beba7620a066033283c74fd9443c2 Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sun, 28 Jan 2024 20:58:32 +0800 Subject: [PATCH 10/11] Optimize markers way more The new strategy is to group every line by starting point, and then iterate and merge through the list. It works surprisingly well, in fact the result is only 11% of original. --- .../littlechasiu/ctm/BlueMapIntegration.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index 5060195..e115f22 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -13,7 +13,6 @@ import de.bluecolored.bluemap.api.math.Color import de.bluecolored.bluemap.api.math.Line import de.bluecolored.bluemap.api.math.Shape import littlechasiu.ctm.model.* -import kotlin.math.min import kotlin.math.ceil object BlueMapIntegration { @@ -199,12 +198,13 @@ object BlueMapIntegration { } fun merge(line: BlueMapLine): Boolean { - if (tomb || line.tomb || path.last() != line.path.first()) return false + if (tomb || line.tomb || path == line.path || path.last() != line.path.first()) return false // if both lines are heading to the same direction, we can omit the midpoint when joining if (unitVector(path[path.size - 2], path[path.size - 1]) == unitVector(line.path[0], line.path[1])) { mutablePath.removeLast() } mutablePath.addAll(line.path) + line.tomb = true updateLongPath() return true } @@ -290,15 +290,19 @@ object BlueMapIntegration { return CSS_NAMED_COLORS.getOrDefault(cssColor, "#000") } - private fun mergeLines(lines: MutableList, lookahead: Int) { - for (i in lines.indices) { - val line = lines[i] - if (line.tomb) continue - for (j in i + 1..i + lookahead) { - val nextLine = lines.getOrNull(j) ?: break - if (nextLine.tomb) continue - if (line.merge(nextLine)) - nextLine.tomb = true + private fun mergeLines(lines: MutableList) { + val points = lines.groupByTo(mutableMapOf()) { it.path[0] } + for (line in lines) { + while (true) { + val key = line.path.last() + val adjacentLines = points[key] ?: break + val mergedLine = adjacentLines.indexOfFirst { line.merge(it) } + if (mergedLine != -1) { + adjacentLines.removeAt(mergedLine) + if (adjacentLines.isEmpty()) points.remove(key) + } else { + break + } } } lines.removeIf { it.tomb } @@ -329,9 +333,7 @@ object BlueMapIntegration { // your mileage may vary lines.sortWith(compareBy({ it.path[0].x }, { it.path[0].y }, { it.path[0].z })) - // The merging algorithm performs a lookahead search (defaults to 8 paths) - // which significantly improves merging for maps with parallel tracks - mergeLines(lines, min(lines.size / 2, 8)) + mergeLines(lines) for (line in lines) { val markerBuilder = LineMarker From a849b1395d1fe498339ae0ea21cbf6ecd23d23fa Mon Sep 17 00:00:00 2001 From: takase1121 <20792268+takase1121@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:38:05 +0800 Subject: [PATCH 11/11] Prevent large networks from deadlocking the server The merging algorithm is incredibly slow, so the number of paths merged is now determined by the original size of the network. --- .../littlechasiu/ctm/BlueMapIntegration.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt index e115f22..d6e5c49 100644 --- a/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt +++ b/src/main/kotlin/littlechasiu/ctm/BlueMapIntegration.kt @@ -14,6 +14,8 @@ import de.bluecolored.bluemap.api.math.Line import de.bluecolored.bluemap.api.math.Shape import littlechasiu.ctm.model.* import kotlin.math.ceil +import kotlin.math.pow +import kotlin.math.roundToInt object BlueMapIntegration { var mapStyle = MapStyle() @@ -290,13 +292,15 @@ object BlueMapIntegration { return CSS_NAMED_COLORS.getOrDefault(cssColor, "#000") } - private fun mergeLines(lines: MutableList) { + private fun mergeLines(lines: MutableList, threshold: Double) { val points = lines.groupByTo(mutableMapOf()) { it.path[0] } + var merged = false for (line in lines) { - while (true) { + val adj = points[line.path.last()] ?: continue + for (i in 0 .. (adj.size * threshold).roundToInt()) { val key = line.path.last() val adjacentLines = points[key] ?: break - val mergedLine = adjacentLines.indexOfFirst { line.merge(it) } + val mergedLine = adjacentLines.indexOfFirst { if (line.merge(it)) { merged = true; true } else false } if (mergedLine != -1) { adjacentLines.removeAt(mergedLine) if (adjacentLines.isEmpty()) points.remove(key) @@ -305,7 +309,9 @@ object BlueMapIntegration { } } } - lines.removeIf { it.tomb } + if (merged) { + lines.removeIf { it.tomb } + } } private fun updateTracks(blueMap: BlueMapAPI, tracks: List) { @@ -332,8 +338,15 @@ object BlueMapIntegration { .toMutableList() // your mileage may vary - lines.sortWith(compareBy({ it.path[0].x }, { it.path[0].y }, { it.path[0].z })) - mergeLines(lines) + // We try to merge less lines as the input size increases, in order to prevent completely deadlocking the server + val threshold = 0.999.pow((lines.size - 1).toDouble()) + if (threshold > 0.2) { + // if the threshold is too low, it's unlikely for the algorithm is merge anything at all, so don't bother + // for context, a threshold of 0.2 is going to attempt to merge 1 out of 3 adjacent nodes + lines.sortWith(compareBy({ it.path[0].x }, { it.path[0].y }, { it.path[0].z })) + mergeLines(lines, threshold) + } + for (line in lines) { val markerBuilder = LineMarker