Skip to content

Commit

Permalink
Add diagram end point
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkleiven committed Sep 28, 2023
1 parent d233b38 commit adbd586
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 5 deletions.
51 changes: 51 additions & 0 deletions src/main/ApiUtil.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.github.statnett.loadflowservice

import com.powsybl.iidm.network.Network
import com.powsybl.loadflow.LoadFlowParameters
import com.powsybl.loadflow.json.JsonLoadFlowParameters
import com.powsybl.nad.NetworkAreaDiagram
import com.powsybl.sld.SingleLineDiagram
import io.ktor.http.content.*
import java.io.ByteArrayInputStream
import java.io.StringWriter

fun busesFromRequest(
type: String,
Expand Down Expand Up @@ -61,3 +65,50 @@ suspend fun multiPartDataHandler(
fun undoPrettyPrintJson(jsonString: String): String {
return jsonString.replace("\n", "").replace(" ", "")
}

enum class DiagramType { Generic, Substation, VoltageLevel }

// Case insensitive enum value matching
fun getDiagramType(value: String): DiagramType {
return try {
DiagramType.entries.first { item -> item.toString().lowercase() == value.lowercase() }
} catch (e: NoSuchElementException) {
DiagramType.Generic
}

}

fun singleLineDiagram(type: DiagramType, name: String, network: Network): String {
val svgWriter = StringWriter()

// Declare a writer for metadata
val metaDataWriter = StringWriter()
when (type) {
DiagramType.VoltageLevel -> {
SingleLineDiagram.drawVoltageLevel(network, name, svgWriter, metaDataWriter)
}

DiagramType.Substation -> {
SingleLineDiagram.drawSubstation(network, name, svgWriter, metaDataWriter)
}

DiagramType.Generic -> {
SingleLineDiagram.draw(network, name, svgWriter, metaDataWriter)
}
}
return svgWriter.toString()
}

fun networkDiagram(network: Network): String {
val svgWriter = StringWriter()
NetworkAreaDiagram(network).draw(svgWriter)
return svgWriter.toString()
}

fun substationNames(network: Network): List<String> {
return network.substations.map { substation -> substation.nameOrId }.toList()
}

fun voltageLevelNames(network: Network): List<String> {
return network.voltageLevels.map { voltageLevel -> voltageLevel.nameOrId }.toList()
}
56 changes: 53 additions & 3 deletions src/main/App.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.github.statnett.loadflowservice

import com.powsybl.commons.PowsyblException
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.plugins.swagger.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

Expand All @@ -21,7 +22,7 @@ fun Application.module() {
call.respondText("Hello, world!")
}

post("/get-buses") {
post("/buses") {
val files = multiPartDataHandler(call.receiveMultipart())

if (files.isEmpty()) {
Expand All @@ -36,6 +37,26 @@ fun Application.module() {
call.respondText(defaultLoadFlowParameters(), ContentType.Application.Json, HttpStatusCode.OK)
}

post("/substation-names") {
val files = multiPartDataHandler(call.receiveMultipart())
if (files.isEmpty()) {
call.response.status(HttpStatusCode.UnprocessableEntity)
} else {
val network = networkFromFileContent(files[0])
call.respond(substationNames(network))
}
}

post("/voltage-levels") {
val files = multiPartDataHandler(call.receiveMultipart())
if (files.isEmpty()) {
call.response.status(HttpStatusCode.UnprocessableEntity)
} else {
val network = networkFromFileContent(files[0])
call.respond(voltageLevelNames(network))
}
}

post("/run-load-flow") {
val paramContainer = LoadParameterContainer()
val files = multiPartDataHandler(call.receiveMultipart(), paramContainer::formItemHandler)
Expand All @@ -48,6 +69,35 @@ fun Application.module() {
call.respond(result)
}
}
swaggerUI(path="openapi", swaggerFile = "openapi/documentation.yaml")

post("/diagram/{type}/{name}") {
val diagramType = getDiagramType(call.parameters["type"] ?: DiagramType.Generic.toString())
val name = call.parameters["name"] ?: ""
val files = multiPartDataHandler((call.receiveMultipart()))
if (files.isEmpty()) {
call.response.status(HttpStatusCode.UnprocessableEntity)
} else {
try {
val network = networkFromFileContent(files[0])
val diagram = singleLineDiagram(diagramType, name, network)
call.respondText(diagram, ContentType.Image.SVG, HttpStatusCode.OK)
} catch (e: PowsyblException) {
call.respondText(e.toString(), ContentType.Text.Plain, HttpStatusCode.BadRequest)
}

}
}

post("/diagram") {
val files = multiPartDataHandler((call.receiveMultipart()))
if (files.isEmpty()) {
call.response.status(HttpStatusCode.UnprocessableEntity)
} else {
val network = networkFromFileContent(files[0])
val diagram = networkDiagram(network)
call.respondText(diagram, ContentType.Image.SVG, HttpStatusCode.OK)
}
}
swaggerUI(path = "openapi", swaggerFile = "openapi/documentation.yaml")
}
}
81 changes: 79 additions & 2 deletions src/test/AppTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ApplicationTest {
testApplication {
val response =
client.submitFormWithBinaryData(
url = "/get-buses",
url = "/buses",
formData =
formData {
append("network", "not file content")
Expand All @@ -43,7 +43,7 @@ class ApplicationTest {
testApplication {
val response =
client.submitFormWithBinaryData(
url = "/get-buses",
url = "/buses",
formData = formDataFromFile(ieeeCdfNetwork14File()),
)
assertEquals(HttpStatusCode.OK, response.status)
Expand Down Expand Up @@ -95,6 +95,77 @@ class ApplicationTest {
)
}

@Test
fun `test response ok network`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/diagram",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
val body = response.bodyAsText()
assertEquals(ContentType.Image.SVG.toString(), response.headers["Content-Type"])
assertTrue(isPlausibleSvg(body))
assertEquals(response.status, HttpStatusCode.OK)
}

@Test
fun `test bad request when substation does not exist`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/diagram/substation/non-existent-station",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.BadRequest)
}

@Test
fun `test response OK and svg produced substation`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/diagram/substation/S1",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.OK)
val body = response.bodyAsText()
assertTrue(isPlausibleSvg(body))
}

@Test
fun `test 11 substation names extracted`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/substation-names",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.OK)
val substationNames = response.bodyAsText().split(",")
assertEquals(substationNames.size, 11)
}

@Test
fun `test 2 voltage levels extracted`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/voltage-levels",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.OK)
val voltageLevels = response.bodyAsText().split(",")
assertEquals(14, voltageLevels.size)
}

@Test
fun `test svg produced for voltage level`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/diagram/voltageLevel/VL1",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.OK)
val body = response.bodyAsText()
assertTrue(isPlausibleSvg(body))
assertEquals(response.headers["Content-Type"].toString(), "image/svg+xml")
}
}

fun formDataFromFile(file: File): List<PartData> {
Expand All @@ -116,4 +187,10 @@ fun ieeeCdfNetwork14File(): File {

IeeeCdfNetworkFactory.create14().write("XIIDM", Properties(), Paths.get(file.path))
return file
}

// Function for checking some properties of a body to verify that the returned body
// is a valid svg image
fun isPlausibleSvg(body: String): Boolean {
return body.contains("<svg") && body.contains("<?xml version")
}

0 comments on commit adbd586

Please sign in to comment.