Skip to content

Commit

Permalink
Merge pull request #59 from igloo-4002/docs
Browse files Browse the repository at this point in the history
Add docs
  • Loading branch information
vishaljak authored Oct 24, 2023
2 parents adfa6cc + 420539f commit 37da4da
Show file tree
Hide file tree
Showing 58 changed files with 487 additions and 48 deletions.
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 igloo-4002

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

/**
* Configuration class for Spring MVC.
*/
@Configuration
@EnableWebMvc
class WebConfig: WebMvcConfigurer {
/**
* URL of frontend for CORS, as specified in `application.yml`. Defaults fo `http://localhost:5173` if not present,
* and is not used if [allowAllCorsOrigins] is set to `true`.
*/
@Value("\${urbanflo.frontend-url}")
private lateinit var frontendUrl: String

/**
* If set to `true`, all endpoints will accept all CORS origins.
*/
@Value("\${urbanflo.allow-all-cors-origins}")
private var allowAllCorsOrigins: Boolean = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBr
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer

/**
* Configuration class for Spring WebSocket.
*/
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class SimulationController(
private var instances: MutableMap<String, SimulationInstance> = mutableMapOf()
private var disposables: MutableMap<String, Disposable> = mutableMapOf()

/**
* Endpoint for simulation websocket.
*/
@MessageMapping("/simulation/{id}")
fun simulationSocket(
@DestinationVariable id: SimulationId,
Expand Down Expand Up @@ -278,32 +281,55 @@ class SimulationController(
@ResponseBody
fun getSimulationAnalytics(@PathVariable id: SimulationId) = storageService.getSimulationAnalytics(id.trim())

/**
* Exception handler for when the simulation cannot be found. Returns a 404 response.
*/
@ExceptionHandler(StorageSimulationNotFoundException::class)
fun handleStorageNotFound(e: StorageSimulationNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity(ErrorResponse(e.message ?: "No such simulation"), HttpStatus.NOT_FOUND)
}

/**
* Exception handler for any malformed or invalid request. Returns a 400 response.
*
* @see [handleJsonError]
*/
@ExceptionHandler(StorageBadRequestException::class)
fun handleStorageBadRequest(e: StorageBadRequestException): ResponseEntity<ErrorResponse> {
return ResponseEntity(ErrorResponse(e.message ?: "Invalid request"), HttpStatus.BAD_REQUEST)
}

/**
* Exception handler for any malformed or invalid JSON body. Returns a 400 response.
*
* @see [handleStorageBadRequest]
*/
@ExceptionHandler(JsonProcessingException::class)
fun handleJsonError(e: JsonProcessingException): ResponseEntity<ErrorResponse> {
return ResponseEntity(ErrorResponse("Invalid JSON body"), HttpStatus.BAD_REQUEST)
}

/**
* Exception handler for any storage exceptions not covered by the other handlers. Returns a 500 response.
*/
@ExceptionHandler(StorageException::class)
fun handleStorageException(e: StorageException): ResponseEntity<ErrorResponse> {
return ResponseEntity(ErrorResponse("An internal error occurred"), HttpStatus.INTERNAL_SERVER_ERROR)
}

/**
* Exception handler for any validation failures. Returns a 400 response with each validation error specified in
* [ErrorResponse.errorFields].
*/
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationErrors(e: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
val fields = e.allErrors.associate { err -> (err as FieldError).field to err.defaultMessage }.toMap()
return ResponseEntity(ErrorResponse("Invalid JSON body", fields), HttpStatus.BAD_REQUEST)
}

/**
* Force stop all simulations before shutting down the server.
*/
@PreDestroy
fun stopAllSimulations() {
logger.info { "Server is shutting down. Stopping all simulations" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import java.time.OffsetDateTime
import java.time.ZoneOffset
import kotlin.math.roundToLong

// https://stackoverflow.com/questions/20635698/how-do-i-deserialize-timestamps-that-are-in-seconds-with-jackson
/**
* Jackson deserialization class to parse Unix timestamp encoded as [Double] to [OffsetDateTime].
*
* [Source/adapted from](https://stackoverflow.com/a/20638114)
*/
class UnixDoubleTimestampDeserializer : StdDeserializer<OffsetDateTime>(OffsetDateTime::class.java) {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): OffsetDateTime {
val number = p?.valueAsString?.toDouble() ?: run { throw JsonParseException("Value is null") }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
package app.urbanflo.urbanflosumoserver.model

data class ErrorResponse(val error: String, val errorFields: Map<String, String?>? = null)
/**
* Response body for all errors.
*/
data class ErrorResponse(
/**
* Error message
*/
val error: String,
/**
* Mapping of fields in request body which failed validation or contains errors.
*/
val errorFields: Map<String, String?>? = null
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package app.urbanflo.urbanflosumoserver.model

/**
* Exception class for all errors during simulation.
*/
class SimulationException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@ package app.urbanflo.urbanflosumoserver.model
import java.nio.file.Path
import java.time.OffsetDateTime

/**
* Data class for simulation information.
*/
data class SimulationInfo(
/**
* Simulation ID
*/
val id: String,
/**
* Document name as sent by frontend.
*/
val documentName: String,
/**
* Creation date, in ISO8601 format.
*/
val createdAt: OffsetDateTime,
/**
* Last modified date, in ISO8601 format.
*/
val lastModifiedAt: OffsetDateTime
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package app.urbanflo.urbanflosumoserver.model


enum class SimulationMessageType {
START, STOP
}

/**
* WebSocket simulation message to signal the simulation instance to start or stop.
*/
data class SimulationMessageRequest(var status: SimulationMessageType)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package app.urbanflo.urbanflosumoserver.model

/**
* Type alias for all SUMO network entity IDs.
*/
typealias SumoEntityId = String
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package app.urbanflo.urbanflosumoserver.model

/**
* Vehicle data produced by TraCI from each simulation step.
*/
data class VehicleData(
val vehicleId: String,
val position: Pair<Double, Double>, // [x, y]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package app.urbanflo.urbanflosumoserver.model.network

import app.urbanflo.urbanflosumoserver.model.SumoEntityId
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty

/**
* @see [SumoConnectionsXml]
*/
data class SumoConnection(
@field:JacksonXmlProperty(isAttribute = true)
val from: SumoEntityId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
import java.nio.file.Path

/**
* Data class representing [connection description XML.](https://sumo.dlr.de/docs/Networks/PlainXML.html#connection_descriptions)
*/
@JacksonXmlRootElement(localName = "connections")
data class SumoConnectionsXml(
@field:JacksonXmlProperty(localName = "connection")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package app.urbanflo.urbanflosumoserver.model.network

import app.urbanflo.urbanflosumoserver.model.SumoEntityId
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.PositiveOrZero

/**
* @see [SumoEdgesXml]
*/
data class SumoEdge(
@field:JacksonXmlProperty(isAttribute = true)
val id: SumoEntityId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
import java.nio.file.Path

typealias SumoEntityId = String

/**
* Data class representing SUMO's [edge description XML.](https://sumo.dlr.de/docs/Networks/PlainXML.html#edge_descriptions)
*/
@JacksonXmlRootElement(localName = "edges")
data class SumoEdgesXml(
@field:JacksonXmlProperty(localName = "edge")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package app.urbanflo.urbanflosumoserver.model.network

import app.urbanflo.urbanflosumoserver.model.SumoEntityId
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty

/**
* SUMO [flow.](https://sumo.dlr.de/docs/Definition_of_Vehicles%2C_Vehicle_Types%2C_and_Routes.html#repeated_vehicles_flows)
*/
data class SumoFlow(
@field:JacksonXmlProperty(isAttribute = true)
val id: SumoEntityId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,43 @@ import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotEmpty

/**
* SUMO network data, in the format used by the frontend.
*/
data class SumoNetwork(
/**
* Document name
*/
@field:NotBlank(message = "Document name cannot be blank")
val documentName: String,
/**
* List of nodes
*/
@field:NotEmpty(message = "Nodes must not be empty")
val nodes: List<SumoNode>,
/**
* List of edges
*/
@field:NotEmpty(message = "Edges must not be empty")
@field:Valid
val edges: List<SumoEdge>,
/**
* List of connections
*/
@field:NotEmpty(message = "Connections must not be empty")
val connections: List<SumoConnection>,
/**
* List of vehicle types (as `vType` in the JSON)
*/
@field:JsonProperty("vType")
val vehicleType: List<SumoVehicleType>,
/**
* List of routes
*/
val route: List<SumoRoute>,
/**
* List of flows
*/
val flow: List<SumoFlow>
) {
constructor(
Expand All @@ -37,9 +61,21 @@ data class SumoNetwork(
routesXml.flows
)

/**
* Returns a node description compatible with SUMO's [node description XML.](https://sumo.dlr.de/docs/Networks/PlainXML.html#node_descriptions)
*/
fun nodesXml() = SumoNodesXml(this.nodes)
/**
* Returns an edge description compatible with SUMO's [edge description XML.](https://sumo.dlr.de/docs/Networks/PlainXML.html#edge_descriptions)
*/
fun edgesXml() = SumoEdgesXml(this.edges)
/**
* Returns a connection description compatible with SUMO's [connection description XML.](https://sumo.dlr.de/docs/Networks/PlainXML.html#connection_descriptions)
*/
fun connectionsXml() = SumoConnectionsXml(this.connections)
/**
* Returns a route description compatible with SUMO's [route description XML.](https://sumo.dlr.de/docs/Definition_of_Vehicles%2C_Vehicle_Types%2C_and_Routes.html#vehicles_and_routes)
*/
fun routesXml() = SumoRoutesXml(
this.vehicleType,
this.route,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package app.urbanflo.urbanflosumoserver.model.network

import app.urbanflo.urbanflosumoserver.model.SumoEntityId
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty

/**
* @see [SumoNodesXml]
*/
data class SumoNode(
@field:JacksonXmlProperty(isAttribute = true)
val id: SumoEntityId,
Expand All @@ -10,29 +14,5 @@ data class SumoNode(
@field:JacksonXmlProperty(isAttribute = true)
val y: Double,
@field:JacksonXmlProperty(isAttribute = true)
val type: String
) {
init {
type.let {
require(
it.lowercase() in listOf(
"priority",
"traffic_light",
"right_before_left",
"left_before_right",
"unregulated",
"priority_stop",
"traffic_light_unregulated",
"allway_stop",
"rail_signal",
"zipper",
"traffic_light_right_on_red",
"rail_crossing",
"dead_end"
)
) {
"Invalid value for node type: $it."
}
}
}
}
val type: SumoNodeType
)
Loading

0 comments on commit 37da4da

Please sign in to comment.