From 4320b8cc0faf127bc997e756bf54c732fac835eb Mon Sep 17 00:00:00 2001 From: C1200 Date: Sat, 20 Jan 2024 16:29:54 +0000 Subject: [PATCH 01/10] Add BlueMap support and schedule info to forge version --- build.gradle.kts | 3 +- .../littlechasiu/ctm/BlueMapIntegration.kt | 366 ++++++++++++++++++ .../kotlin/littlechasiu/ctm/Extensions.kt | 87 +++++ src/main/kotlin/littlechasiu/ctm/TrackMap.kt | 1 + .../kotlin/littlechasiu/ctm/TrackWatcher.kt | 187 ++++----- .../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, 625 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 490f487..8e5456e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ val archives_version = "$mod_version+mc$minecraft_version-neoforge" 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://api.modrinth.com/maven") // LazyDFU maven("https://maven.tterrag.com/") // Create Forge, Flywheel @@ -63,6 +63,7 @@ dependencies { // included in Kotlin for Forge 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 4528624..ba07004 100644 --- a/src/main/kotlin/littlechasiu/ctm/TrackMap.kt +++ b/src/main/kotlin/littlechasiu/ctm/TrackMap.kt @@ -86,6 +86,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 45eb330..f3b4b85 100644 --- a/src/main/kotlin/littlechasiu/ctm/TrackWatcher.kt +++ b/src/main/kotlin/littlechasiu/ctm/TrackWatcher.kt @@ -1,10 +1,11 @@ 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 import com.simibubi.create.content.trains.graph.TrackNode -import com.simibubi.create.content.trains.entity.Train import com.simibubi.create.content.trains.signal.SignalBlock.SignalType import com.simibubi.create.content.trains.signal.SignalBlockEntity.SignalState import com.simibubi.create.content.trains.signal.SignalBoundary @@ -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 c53caa3d7b100c80ff39d084e74cbf285f85271d 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/10] 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 ac92d9a316cda879f3e8b66fa30d5730fdf61c50 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/10] 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 1a1b7a654bfb9f61e7f040575eac46b17a543f94 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/10] 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 c17e3ae0e9077ad901e6ca3c77ec4390e523e09b Mon Sep 17 00:00:00 2001 From: C1200 Date: Wed, 24 Jan 2024 11:36:31 +0000 Subject: [PATCH 05/10] 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 21c2d0ceb80084289157fbb4a23a9ee3442f6374 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 06/10] 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 f79783b31c83bfadb0a59642174f1b2ed3f7de1d 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/10] 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 1f9222a8735f12b495eee64037e866ac7071bc5c 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/10] 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 85e65766da67b256539c6238d8be4f66f5422e89 Mon Sep 17 00:00:00 2001 From: C1200 Date: Sat, 27 Jan 2024 22:59:28 +0000 Subject: [PATCH 09/10] 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 e86cd9367ec262607db1dc5a4bcb8b5e5b14fcf6 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/10] 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