Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add diagram end point #9

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}
Loading