diff --git a/services/hanke-service/build.gradle.kts b/services/hanke-service/build.gradle.kts index b905d97df..cc63505e4 100644 --- a/services/hanke-service/build.gradle.kts +++ b/services/hanke-service/build.gradle.kts @@ -8,8 +8,13 @@ group = "fi.hel.haitaton" version = "0.0.1-SNAPSHOT" val sentryVersion = "7.19.0" +val geoToolsVersion = "32.1" -repositories { mavenCentral() } +repositories { + mavenCentral().content { excludeModule("javax.media", "jai_core") } + maven { url = uri("https://repo.osgeo.org/repository/release/") } + maven { url = uri("https://maven.geotoolkit.org") } +} sourceSets { create("integrationTest") { @@ -107,6 +112,12 @@ dependencies { implementation("com.github.librepdf:openpdf:2.0.3") implementation("org.apache.xmlgraphics:fop:2.10") + // Geotools + implementation("org.geotools:gt-wms:$geoToolsVersion") + implementation("org.geotools:gt-brewer:$geoToolsVersion") + implementation("org.geotools:gt-epsg-hsql:$geoToolsVersion") + implementation("org.locationtech.jts.io:jts-io-common:1.19.0") + // Testcontainers testImplementation(platform("org.testcontainers:testcontainers-bom:1.20.4")) testImplementation("org.springframework.boot:spring-boot-testcontainers") diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt index a1dd8b907..d10798c02 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusService.kt @@ -79,6 +79,7 @@ class HakemusService( private val paatosService: PaatosService, private val applicationEventPublisher: ApplicationEventPublisher, private val tormaystarkasteluLaskentaService: TormaystarkasteluLaskentaService, + private val haittojenhallintasuunnitelmaPdfEncoder: HaittojenhallintasuunnitelmaPdfEncoder, ) { @Transactional(readOnly = true) @@ -714,7 +715,7 @@ class HakemusService( val totalArea = geometriatDao.calculateCombinedArea(data.areas?.flatMap { it.geometries() } ?: listOf()) - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, data, totalArea) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, data, totalArea) val attachmentMetadata = AttachmentMetadata( id = null, diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoder.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoder.kt index ff426f4a5..d4a118fff 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoder.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoder.kt @@ -1,20 +1,27 @@ package fi.hel.haitaton.hanke.pdf +import com.lowagie.text.Chunk import com.lowagie.text.Document +import com.lowagie.text.Image import com.lowagie.text.ImgTemplate +import com.lowagie.text.Paragraph import fi.hel.haitaton.hanke.domain.Hanke +import fi.hel.haitaton.hanke.domain.SavedHankealue +import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusAlue import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusData import java.time.ZonedDateTime +import org.springframework.stereotype.Component -object HaittojenhallintasuunnitelmaPdfEncoder { +@Component +class HaittojenhallintasuunnitelmaPdfEncoder(private val mapGenerator: MapGenerator) { fun createPdf(hanke: Hanke, data: KaivuilmoitusData, totalArea: Float?): ByteArray = createDocument { document, writer -> val locationIcon = loadLocationIcon(writer) - formatKaivuilmoitusPdf(document, hanke, data, totalArea, locationIcon) + formatPdf(document, hanke, data, totalArea, locationIcon) } - private fun formatKaivuilmoitusPdf( + private fun formatPdf( document: Document, hanke: Hanke, data: KaivuilmoitusData, @@ -30,7 +37,12 @@ object HaittojenhallintasuunnitelmaPdfEncoder { document.subtitle(data.name) document.mapHeader("Alueiden sijainti", locationIcon) - document.placeholderImage() + document.map(data.areas!!, hanke.alueet) + + val spacer = Paragraph(Chunk.NEWLINE) + spacer.spacingBefore = 1f + spacer.spacingAfter = pxToPt(-16) + document.add(spacer) document.section("Perustiedot") { row("Työn nimi", data.name) @@ -53,4 +65,18 @@ object HaittojenhallintasuunnitelmaPdfEncoder { row("Työn loppupäivämäärä", data.endTime.format()) } } + + fun Document.map(areas: List, hankealueet: List) { + // The image is better quality, if it's rendered at a higher resolution and then scaled down + // to fit in the PDF layout. + val bytes = mapGenerator.mapWithAreas(areas, hankealueet, MAP_WIDTH * 2, MAP_HEIGHT * 2) + val image = Image.getInstance(bytes) + image.scaleToFit(pxToPt(MAP_WIDTH), pxToPt(MAP_HEIGHT)) + this.add(image) + } + + companion object { + const val MAP_WIDTH = 1110 + const val MAP_HEIGHT = 400 + } } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapBounds.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapBounds.kt new file mode 100644 index 000000000..0158b302f --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapBounds.kt @@ -0,0 +1,64 @@ +package fi.hel.haitaton.hanke.pdf + +import java.util.Locale +import kotlin.math.max +import org.geotools.api.referencing.crs.CoordinateReferenceSystem +import org.geotools.geometry.jts.ReferencedEnvelope +import org.geotools.referencing.CRS + +data class Point(val x: Double, val y: Double) { + + private fun fixedPoint(a: Double): String = String.format(Locale.UK, "%.3f", a) + + override fun toString(): String = "(x=${fixedPoint(x)}, y=${fixedPoint(y)})" + + companion object { + fun center(a: Point, b: Point) = Point(y = (a.y + b.y) / 2.0, x = (a.x + b.x) / 2.0) + } +} + +data class MapBounds(val min: Point, val max: Point) { + val xSize: Double = max.x - min.x + val ySize: Double = max.y - min.y + val center: Point = Point.center(min, max) + + fun padded(): MapBounds { + val xPadding = max(xSize * PADDING_RATIO, MIN_PADDING) + val yPadding = max(ySize * PADDING_RATIO, MIN_PADDING) + + return MapBounds( + Point(y = min.y - yPadding, x = min.x - xPadding), + Point(y = max.y + yPadding, x = max.x + xPadding), + ) + } + + fun fitToImage(imageWidth: Int, imageHeight: Int): MapBounds { + val metersPerPixel = max(xSize / imageWidth, ySize / imageHeight) + + val newXSize = imageWidth * metersPerPixel + val newYSize = imageHeight * metersPerPixel + + return MapBounds( + Point(y = center.y - newYSize / 2, x = center.x - newXSize / 2), + Point(y = center.y + newYSize / 2, x = center.x + newXSize / 2), + ) + } + + fun squaredOff(): MapBounds { + val maxSize = max(xSize, ySize) + + return MapBounds( + Point(y = center.y - maxSize / 2, x = center.x - maxSize / 2), + Point(y = center.y + maxSize / 2, x = center.x + maxSize / 2), + ) + } + + fun asReferencedEnvelope() = ReferencedEnvelope(min.y, max.y, min.x, max.x, sourceCRS) + + companion object { + const val MIN_PADDING = 30.0 + const val PADDING_RATIO = 0.1 + + val sourceCRS: CoordinateReferenceSystem = CRS.decode("EPSG:3879") + } +} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapGenerator.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapGenerator.kt new file mode 100644 index 000000000..0b1df4823 --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapGenerator.kt @@ -0,0 +1,180 @@ +package fi.hel.haitaton.hanke.pdf + +import fi.hel.haitaton.hanke.SRID +import fi.hel.haitaton.hanke.domain.SavedHankealue +import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusAlue +import fi.hel.haitaton.hanke.toJsonString +import fi.hel.haitaton.hanke.tormaystarkastelu.TormaystarkasteluTulos +import java.awt.Color +import java.awt.Rectangle +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import javax.imageio.ImageIO +import kotlin.math.max +import org.geojson.Crs +import org.geojson.Polygon as JsonPolygon +import org.geotools.api.feature.simple.SimpleFeature +import org.geotools.api.feature.simple.SimpleFeatureType +import org.geotools.api.style.Style +import org.geotools.brewer.styling.builder.PolygonSymbolizerBuilder +import org.geotools.data.DataUtilities +import org.geotools.data.simple.SimpleFeatureCollection +import org.geotools.feature.simple.SimpleFeatureBuilder +import org.geotools.map.FeatureLayer +import org.geotools.map.MapContent +import org.geotools.ows.wms.WMSUtils +import org.geotools.ows.wms.WebMapServer +import org.geotools.ows.wms.map.WMSLayer +import org.geotools.renderer.GTRenderer +import org.geotools.renderer.lite.StreamingRenderer +import org.locationtech.jts.geom.CoordinateFilter +import org.locationtech.jts.geom.Polygon +import org.locationtech.jts.io.geojson.GeoJsonReader +import org.springframework.stereotype.Component + +@Component +class MapGenerator(private val wms: WebMapServer) { + + fun mapWithAreas( + areas: List, + hankealueet: List, + imageWidth: Int, + imageHeight: Int, + ): ByteArray { + val bounds = calculateBounds(areas, imageWidth, imageHeight) + + val layer = + WMSUtils.getNamedLayers(wms.capabilities).find { + it.title == KIINTEISTOKARTTA_LAYER_TITLE + }!! + + val mapLayer = WMSLayer(wms, layer, KIINTEISTOKARTTA_STYLE, "image/png") + + val map = MapContent() + map.addLayer(mapLayer) + + val hankealueIds = areas.map { it.hankealueId }.toSet() + hankealueet + .filter { hankealueIds.contains(it.id) } + .forEach { alue -> + val polygons = + alue.geometriat + ?.featureCollection + ?.features + ?.map { it.geometry } + ?.filterIsInstance() ?: listOf() + + val style = selectColorStyle(alue.tormaystarkasteluTulos) + val featureLayer = FeatureLayer(readPolygons(polygons), style) + map.addLayer(featureLayer) + } + + areas + .flatMap { it.tyoalueet } + .forEach { + val style = selectColorStyle(it.tormaystarkasteluTulos) + val featureLayer = FeatureLayer(readPolygon(it.geometry), style) + map.addLayer(featureLayer) + } + + val renderer: GTRenderer = StreamingRenderer() + renderer.mapContent = map + + val envelope = bounds.asReferencedEnvelope() + + // The maps are distorted, unless we request a square map. So we request a square and then + // crop it to fit the requested image dimensions. + val imageSize = max(imageWidth, imageHeight) + val imageBounds = Rectangle(0, 0, imageSize, imageSize) + val image = BufferedImage(imageBounds.width, imageBounds.height, BufferedImage.TYPE_INT_RGB) + + val gr = image.createGraphics() + gr.paint = Color.WHITE + gr.fill(imageBounds) + + val cropped = + image.getSubimage( + (imageSize - imageWidth) / 2, + (imageSize - imageHeight) / 2, + imageWidth, + imageHeight, + ) + + renderer.paint(gr, imageBounds, envelope) + val baos = ByteArrayOutputStream() + ImageIO.write(cropped, "png", baos) + map.dispose() + return baos.toByteArray() + } + + companion object { + const val KIINTEISTOKARTTA_LAYER_TITLE = "Kiinteistokartan_maastotiedot" + const val KIINTEISTOKARTTA_STYLE = "default-style-avoindata:Kiinteistokartan_maastotiedot" + + private val polygonType: SimpleFeatureType = + DataUtilities.createType("Polygon", "the_geom:Polygon:srid=$SRID") + + fun calculateBounds( + areas: List, + imageWidth: Int, + imageHeight: Int, + ): MapBounds { + val allCoords = areas.flatMap { it.geometries() }.flatMap { it.coordinates.flatten() } + + val latitudes = allCoords.map { it.latitude } + val longitudes = allCoords.map { it.longitude } + val bounds = + MapBounds( + Point(y = latitudes.min(), x = longitudes.min()), + Point(y = latitudes.max(), x = longitudes.max()), + ) + + return bounds.padded().fitToImage(imageWidth, imageHeight).squaredOff() + } + + fun selectColorStyle(tormaystarkasteluTulos: TormaystarkasteluTulos?): Style = + NuisanceColor.selectColor(tormaystarkasteluTulos).style + + private fun readPolygon(polygon: JsonPolygon): SimpleFeatureCollection = + readPolygons(listOf(polygon)) + + private fun readPolygons(polygons: List): SimpleFeatureCollection { + val reader = GeoJsonReader() + + val features: MutableList = mutableListOf() + + polygons.forEach { polygon -> + val copy = JsonPolygon() + copy.exteriorRing = polygon.exteriorRing + copy.interiorRings.addAll(polygon.interiorRings) + copy.crs = Crs().apply { properties = mapOf(Pair("name", "EPSG:$SRID")) } + val geometry = reader.read(copy.toJsonString()) as Polygon + + // I'm not quite sure why, but the x and y coordinates seem to be reversed in + // different + // implementations. Currently, the maps are drawn correctly, so I don't want to mess + // with them. The coordinates are reversed here and when calculating the final map + // boundaries for the image (the referenced envelope). + geometry.apply( + CoordinateFilter { coordinate -> + val oldX = coordinate.x + coordinate.x = coordinate.y + coordinate.y = oldX + } + ) + val featureBuilder = SimpleFeatureBuilder(polygonType) + featureBuilder.add(geometry) + features.add(featureBuilder.buildFeature(null)) + } + + return DataUtilities.collection(features) + } + + fun buildAreaStyle(color: Color): Style { + val builder = PolygonSymbolizerBuilder() + builder.stroke().color(Color.BLACK).width(4.0) + builder.fill().color(color).opacity(0.6) + return builder.buildStyle() + } + } +} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/NuisanceColor.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/NuisanceColor.kt new file mode 100644 index 000000000..9176c8010 --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/NuisanceColor.kt @@ -0,0 +1,31 @@ +package fi.hel.haitaton.hanke.pdf + +import fi.hel.haitaton.hanke.tormaystarkastelu.TormaystarkasteluTulos +import java.awt.Color +import org.geotools.api.style.Style + +enum class NuisanceColor(val color: Color) { + BLUE(Color(0, 98, 185)), + GRAY(Color(176, 184, 191)), + GREEN(Color(0, 146, 70)), + YELLOW(Color(255, 218, 7)), + RED(Color(196, 18, 62)); + + val style: Style by lazy { MapGenerator.buildAreaStyle(color) } + + companion object { + fun selectColor(tormaystarkasteluTulos: TormaystarkasteluTulos?): NuisanceColor { + val maxHaitta = tormaystarkasteluTulos?.liikennehaittaindeksi?.indeksi + + return when { + maxHaitta == null -> BLUE + maxHaitta.isNaN() -> BLUE + maxHaitta < 0f -> BLUE + maxHaitta == 0f -> GRAY + maxHaitta < 3f -> GREEN + maxHaitta < 4f -> YELLOW + else -> RED + } + } + } +} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/PdfHelpers.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/PdfHelpers.kt index e099769e8..137ecfd24 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/PdfHelpers.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/PdfHelpers.kt @@ -4,7 +4,6 @@ import com.lowagie.text.Chunk import com.lowagie.text.Document import com.lowagie.text.Element import com.lowagie.text.Font -import com.lowagie.text.Image import com.lowagie.text.ImgTemplate import com.lowagie.text.PageSize import com.lowagie.text.Paragraph @@ -147,18 +146,6 @@ fun Document.section(sectionTitle: String, addRows: PdfPTable.() -> Unit) { this.add(paragraph) } -/** Placeholder image before we get proper map rendering. */ -fun Document.placeholderImage() { - val placeholderImage = Image.getInstanceFromClasspath("pdf-assets/placeholder-map.png") - placeholderImage.scaleToFit(pxToPt(1110), pxToPt(400)) - this.add(placeholderImage) - - val spacer = Paragraph(Chunk.NEWLINE) - spacer.spacingBefore = 1f - spacer.spacingAfter = pxToPt(-16) - this.add(spacer) -} - fun createDocument(addContent: (Document, PdfWriter) -> Unit): ByteArray { val outputStream = ByteArrayOutputStream() val padding = pxToPt(40) diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/WmsConfiguration.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/WmsConfiguration.kt new file mode 100644 index 000000000..6a57f2812 --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/WmsConfiguration.kt @@ -0,0 +1,21 @@ +package fi.hel.haitaton.hanke.pdf + +import java.net.URI +import java.net.URL +import org.geotools.ows.wms.WebMapServer +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class WmsConfiguration( + @Value("\${haitaton.map-service.capability-url}") private val mapServiceUrl: String +) { + + val url: URL = URI(mapServiceUrl).toURL() + + @Bean + fun wms(): WebMapServer { + return WebMapServer(url) + } +} diff --git a/services/hanke-service/src/main/resources/application.yml b/services/hanke-service/src/main/resources/application.yml index 37a08e73a..91deb8024 100644 --- a/services/hanke-service/src/main/resources/application.yml +++ b/services/hanke-service/src/main/resources/application.yml @@ -50,6 +50,8 @@ haitaton: issuer: ${HAITATON_OAUTH2_ISSUER:http://gdpr-api-tester:8888/} delete-scope: gdprdelete query-scope: gdprquery + map-service: + capability-url: https://kartta.hel.fi/ws/geoserver/avoindata/wms?REQUEST=GetCapabilities&SERVICE=WMS profiili-api: graph-ql-url: ${PROFIILI_GRAPHQL_URL:https://profile-api.test.hel.ninja/graphql/} audience: ${PROFIILI_AUDIENCE:profile-api-test} diff --git a/services/hanke-service/src/main/resources/pdf-assets/placeholder-map.png b/services/hanke-service/src/main/resources/pdf-assets/placeholder-map.png deleted file mode 100644 index 311fe8aab..000000000 Binary files a/services/hanke-service/src/main/resources/pdf-assets/placeholder-map.png and /dev/null differ diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HaittaFactory.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HaittaFactory.kt index b2e87172a..eb6750576 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HaittaFactory.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HaittaFactory.kt @@ -30,6 +30,9 @@ object HaittaFactory { 1, ) + val TORMAYSTARKASTELUTULOS_WITH_ZEROES = + tormaystarkasteluTulos(TORMAYSTARKASTELU_ZERO_AUTOLIIKENNELUOKITTELU, 0f, 0f, 0f) + const val DEFAULT_HHS_YLEINEN = "Yleisten haittojen hallintasuunnitelma" const val DEFAULT_HHS_PYORALIIKENNE = "Pyöräliikenteelle koituvien haittojen hallintasuunnitelma" diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt index 97b1c4261..fd831c9c4 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/hakemus/HakemusServiceTest.kt @@ -36,6 +36,7 @@ import fi.hel.haitaton.hanke.logging.HakemusLoggingService import fi.hel.haitaton.hanke.logging.HankeLoggingService import fi.hel.haitaton.hanke.logging.Status import fi.hel.haitaton.hanke.paatos.PaatosService +import fi.hel.haitaton.hanke.pdf.HaittojenhallintasuunnitelmaPdfEncoder import fi.hel.haitaton.hanke.pdf.withName import fi.hel.haitaton.hanke.permissions.HankeKayttajaService import fi.hel.haitaton.hanke.taydennys.TaydennysRepository @@ -86,6 +87,8 @@ class HakemusServiceTest { private val paatosService: PaatosService = mockk() private val publisher: ApplicationEventPublisher = mockk() private val tormaystarkasteluLaskentaService: TormaystarkasteluLaskentaService = mockk() + private val haittojenhallintasuunnitelmaPdfEncoder: HaittojenhallintasuunnitelmaPdfEncoder = + mockk() private val hakemusService = HakemusService( @@ -105,6 +108,7 @@ class HakemusServiceTest { paatosService, publisher, tormaystarkasteluLaskentaService, + haittojenhallintasuunnitelmaPdfEncoder, ) @BeforeEach @@ -129,6 +133,7 @@ class HakemusServiceTest { alluClient, paatosService, publisher, + haittojenhallintasuunnitelmaPdfEncoder, ) } diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoderTest.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoderTest.kt index 7de762cbf..cee3c5733 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoderTest.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HaittojenhallintasuunnitelmaPdfEncoderTest.kt @@ -4,24 +4,46 @@ import assertk.all import assertk.assertThat import assertk.assertions.contains import assertk.assertions.doesNotContain -import fi.hel.haitaton.hanke.domain.Hanke import fi.hel.haitaton.hanke.factory.ApplicationFactory import fi.hel.haitaton.hanke.factory.HakemusFactory import fi.hel.haitaton.hanke.factory.HakemusyhteystietoFactory import fi.hel.haitaton.hanke.factory.HakemusyhteystietoFactory.withYhteyshenkilo import fi.hel.haitaton.hanke.factory.HankeFactory -import fi.hel.haitaton.hanke.hakemus.ApplicationType -import fi.hel.haitaton.hanke.hakemus.Hakemus -import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusData -import java.nio.file.Files -import java.time.ZoneId +import fi.hel.haitaton.hanke.getResourceAsBytes +import io.mockk.checkUnnecessaryStub +import io.mockk.clearAllMocks +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifySequence import java.time.ZonedDateTime -import kotlin.io.path.Path +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class HaittojenhallintasuunnitelmaPdfEncoderTest { + private val mapGenerator: MapGenerator = mockk() + + private val haittojenhallintasuunnitelmaPdfEncoder = + HaittojenhallintasuunnitelmaPdfEncoder(mapGenerator) + + private val blankImage: ByteArray = + "/fi/hel/haitaton/hanke/pdf-test-data/blank.png".getResourceAsBytes() + + @BeforeEach + fun clearMocks() { + clearAllMocks() + every { mapGenerator.mapWithAreas(any(), any(), any(), any()) } returns blankImage + } + + @AfterEach + fun checkMocks() { + checkUnnecessaryStub() + confirmVerified(mapGenerator) + } + @Nested inner class CreatePdf { val hanke = HankeFactory.create() @@ -30,7 +52,7 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { fun `created PDF contains title and section headers`() { val hakemusData = HakemusFactory.createKaivuilmoitusData() - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) assertThat(getPdfAsText(pdfData)) .contains( @@ -40,13 +62,14 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { "Perustiedot", "Alueet", ) + verifySequence { mapGenerator.mapWithAreas(hakemusData.areas!!, listOf(), 2220, 800) } } @Test fun `created PDF contains headers for basic information`() { val hakemusData = HakemusFactory.createKaivuilmoitusData(cableReportDone = false) - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) assertThat(getPdfAsText(pdfData)).all { contains("Työn nimi") @@ -57,6 +80,7 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { contains("Sijoitussopimustunnukset") contains("Työhön vaadittavat pätevyydet") } + verifySequence { mapGenerator.mapWithAreas(hakemusData.areas!!, listOf(), 2220, 800) } } @Test @@ -82,7 +106,7 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { ), ) - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) val pdfText = getPdfAsText(pdfData) assertThat(pdfText).all { @@ -99,6 +123,7 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { contains("Sopimus1, Sopimus2") contains("Kyllä") } + verifySequence { mapGenerator.mapWithAreas(hakemusData.areas!!, listOf(), 2220, 800) } } @Test @@ -110,26 +135,28 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { emergencyWork = false, ) - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 1f) assertThat(getPdfAsText(pdfData)).all { doesNotContain("Uuden rakenteen tai johdon rakentamisesta") doesNotContain("Olemassaolevan rakenteen kunnossapitotyöstä") doesNotContain("välttämiseksi") } + verifySequence { mapGenerator.mapWithAreas(hakemusData.areas!!, listOf(), 2220, 800) } } @Test fun `created PDF contains headers for area information`() { val hakemusData = HakemusFactory.createKaivuilmoitusData() - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 614f) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 614f) assertThat(getPdfAsText(pdfData)).all { contains("Alueiden kokonaispinta-ala") contains("Työn alkupäivämäärä") contains("Työn loppupäivämäärä") } + verifySequence { mapGenerator.mapWithAreas(hakemusData.areas!!, listOf(), 2220, 800) } } @Test @@ -141,81 +168,14 @@ class HaittojenhallintasuunnitelmaPdfEncoderTest { areas = listOf(), ) - val pdfData = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 614f) + val pdfData = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, hakemusData, 614f) assertThat(getPdfAsText(pdfData)).all { contains("614,00 m²") contains("18.11.2022") contains("28.11.2022") } + verifySequence { mapGenerator.mapWithAreas(hakemusData.areas!!, listOf(), 2220, 800) } } } - - /** - * Writes a new Haittojenhallintasuunnitelma PDF to the local filesystem for manual inspection. - * - * This test should be removed when the HAI-375 story is completed. - */ - @Test - fun `write to file to manually inspect content`() { - val data = - KaivuilmoitusData( - applicationType = ApplicationType.EXCAVATION_NOTIFICATION, - name = "Lorem Ipsum", - workDescription = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", - constructionWork = false, - maintenanceWork = false, - emergencyWork = true, - cableReportDone = false, - rockExcavation = true, - cableReports = listOf("JS-2313241", "JS-21398"), - placementContracts = listOf("SL1029392"), - requiredCompetence = true, - startTime = - ZonedDateTime.of(2021, 1, 1, 12, 12, 12, 0, ZoneId.of("Europe/Helsinki")), - endTime = ZonedDateTime.of(2022, 1, 1, 12, 12, 12, 0, ZoneId.of("Europe/Helsinki")), - areas = listOf(), - paperDecisionReceiver = null, - customerWithContacts = null, - contractorWithContacts = null, - propertyDeveloperWithContacts = null, - representativeWithContacts = null, - invoicingCustomer = null, - additionalInfo = null, - ) - - val hakemus = - Hakemus( - id = 0, - alluid = null, - alluStatus = null, - applicationIdentifier = null, - applicationType = ApplicationType.EXCAVATION_NOTIFICATION, - applicationData = data, - hankeTunnus = "HAI-21389", - hankeId = 0, - valmistumisilmoitukset = mapOf(), - ) - - val hanke = - Hanke( - id = 0, - hankeTunnus = hakemus.hankeTunnus, - onYKTHanke = null, - nimi = "Hankkeen nimi", - kuvaus = null, - vaihe = null, - version = null, - createdBy = null, - createdAt = null, - modifiedBy = null, - modifiedAt = null, - status = null, - generated = false, - ) - - val bytes = HaittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, data, 500f) - Files.write(Path("hhs-pdf-test.pdf"), bytes) - } } diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HhsManualTest.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HhsManualTest.kt new file mode 100644 index 000000000..80291921e --- /dev/null +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/HhsManualTest.kt @@ -0,0 +1,126 @@ +package fi.hel.haitaton.hanke.pdf + +import fi.hel.haitaton.hanke.asJsonResource +import fi.hel.haitaton.hanke.domain.Hanke +import fi.hel.haitaton.hanke.domain.SavedHankealue +import fi.hel.haitaton.hanke.hakemus.ApplicationType +import fi.hel.haitaton.hanke.hakemus.Hakemus +import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusAlue +import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusData +import java.net.URI +import java.nio.file.Files +import java.time.ZoneId +import java.time.ZonedDateTime +import kotlin.io.path.Path +import org.geotools.ows.wms.WebMapServer +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +/** + * Writes some new Haittojenhallintasuunnitelma PDFs to the local filesystem for manual inspection. + * + * This test should be removed when the HAI-375 story is completed. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class HhsManualTest { + + private val capabilityUrl = + "https://kartta.hel.fi/ws/geoserver/avoindata/wms?REQUEST=GetCapabilities&SERVICE=WMS&VERSION=1.1.1" + + private val mapGenerator = MapGenerator(WebMapServer(URI(capabilityUrl).toURL())) + private val haittojenhallintasuunnitelmaPdfEncoder = + HaittojenhallintasuunnitelmaPdfEncoder(mapGenerator) + + private val hankealueet: List = + "/fi/hel/haitaton/hanke/pdf-test-data/hankealueet.json".asJsonResource() + + private val pystysuora: List = + "/fi/hel/haitaton/hanke/pdf-test-data/tall-area.json".asJsonResource() + + private val vaakasuora: List = + "/fi/hel/haitaton/hanke/pdf-test-data/wide-area.json".asJsonResource() + + private val pieniAlue: List = + "/fi/hel/haitaton/hanke/pdf-test-data/small-area.json".asJsonResource() + + private val montaAluetta: List = + "/fi/hel/haitaton/hanke/pdf-test-data/many-areas.json".asJsonResource() + + private fun alueet(): List = + listOf( + Arguments.of("pystysuora", pystysuora), + Arguments.of("vaakasuora", vaakasuora), + Arguments.of("pieniAlue", pieniAlue), + Arguments.of("montaAluetta", montaAluetta), + ) + + @ParameterizedTest(name = "{displayName} {0}") + @MethodSource("alueet") + fun `write to file to manually inspect content`(name: String, areas: List) { + val data = + KaivuilmoitusData( + applicationType = ApplicationType.EXCAVATION_NOTIFICATION, + name = "Lorem Ipsum", + workDescription = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", + constructionWork = false, + maintenanceWork = false, + emergencyWork = true, + cableReportDone = false, + rockExcavation = true, + cableReports = listOf("JS-2313241", "JS-21398"), + placementContracts = listOf("SL1029392"), + requiredCompetence = true, + startTime = + ZonedDateTime.of(2021, 1, 1, 12, 12, 12, 0, ZoneId.of("Europe/Helsinki")), + endTime = ZonedDateTime.of(2022, 1, 1, 12, 12, 12, 0, ZoneId.of("Europe/Helsinki")), + areas = areas, + paperDecisionReceiver = null, + customerWithContacts = null, + contractorWithContacts = null, + propertyDeveloperWithContacts = null, + representativeWithContacts = null, + invoicingCustomer = null, + additionalInfo = null, + ) + + val hakemus = + Hakemus( + id = 0, + alluid = null, + alluStatus = null, + applicationIdentifier = null, + applicationType = ApplicationType.EXCAVATION_NOTIFICATION, + applicationData = data, + hankeTunnus = "HAI-21389", + hankeId = 0, + valmistumisilmoitukset = mapOf(), + ) + + val hanke = + Hanke( + id = 0, + hankeTunnus = hakemus.hankeTunnus, + onYKTHanke = null, + nimi = "Hankkeen nimi", + kuvaus = null, + vaihe = null, + version = null, + createdBy = null, + createdAt = null, + modifiedBy = null, + modifiedAt = null, + status = null, + generated = false, + ) + + val hankealueet = + hankealueet.filter { hankealue -> areas.any { it.hankealueId == hankealue.id } } + hanke.alueet = hankealueet.toMutableList() + + val bytes = haittojenhallintasuunnitelmaPdfEncoder.createPdf(hanke, data, 500f) + Files.write(Path("hhs-pdf-test-$name.pdf"), bytes) + } +} diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/MapGeneratorTest.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/MapGeneratorTest.kt new file mode 100644 index 000000000..7f854be13 --- /dev/null +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/pdf/MapGeneratorTest.kt @@ -0,0 +1,414 @@ +package fi.hel.haitaton.hanke.pdf + +import assertk.all +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isSameInstanceAs +import assertk.assertions.prop +import fi.hel.haitaton.hanke.asJsonResource +import fi.hel.haitaton.hanke.factory.HaittaFactory +import fi.hel.haitaton.hanke.getResourceAsBytes +import fi.hel.haitaton.hanke.getResourceAsText +import fi.hel.haitaton.hanke.hakemus.KaivuilmoitusAlue +import fi.hel.haitaton.hanke.tormaystarkastelu.Autoliikenneluokittelu +import fi.hel.haitaton.hanke.tormaystarkastelu.TormaystarkasteluTulos +import java.io.ByteArrayInputStream +import javax.imageio.ImageIO +import net.pwall.mustache.Template +import okhttp3.HttpUrl +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import okio.Buffer +import org.geotools.ows.wms.WebMapServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class MapGeneratorTest { + + private val wideArea: List = + "/fi/hel/haitaton/hanke/pdf-test-data/wide-area.json".asJsonResource() + private val tallArea: List = + "/fi/hel/haitaton/hanke/pdf-test-data/tall-area.json".asJsonResource() + private val manyAreas: List = + "/fi/hel/haitaton/hanke/pdf-test-data/many-areas.json".asJsonResource() + + @Nested + inner class MapWithAreas { + private lateinit var mockWmsServer: MockWebServer + + private lateinit var wms: WebMapServer + private lateinit var mapGenerator: MapGenerator + + @BeforeEach + fun setup() { + mockWmsServer = MockWebServer() + mockWmsServer.start() + mockWmsServer.dispatcher = GeoserverDispatcher(mockWmsServer.url("")) + + wms = WebMapServer(mockWmsServer.url("/capabilities").toUrl()) + mapGenerator = MapGenerator(wms) + } + + @AfterEach + fun tearDown() { + mockWmsServer.shutdown() + } + + @Test + fun `calls the map server with the correct parameters`() { + mapGenerator.mapWithAreas(wideArea, listOf(), 1000, 500) + + assertThat(mockWmsServer.requestCount).isEqualTo(2) + val capabilityRequest = mockWmsServer.takeRequest() + assertThat(capabilityRequest).all { + prop(RecordedRequest::path) + .isNotNull() + .contains( + "/capabilities", + "REQUEST=GetCapabilities", + "VERSION=1.3.0", + "SERVICE=WMS", + ) + } + val mapRequest = mockWmsServer.takeRequest() + assertThat(mapRequest).all { + prop(RecordedRequest::path) + .isNotNull() + .contains( + "/image", // The URL is read from the capabilities XML. + "REQUEST=GetMap", + "FORMAT=image/png", + "CRS=EPSG:3879", + // Check CalculateBounds tests for where these bounds come from. + "BBOX=6672690.0,2.549634E7,6673410.0,2.549706E7", + "VERSION=1.3.0", + "STYLES=default-style-avoindata%3AKiinteistokartan_maastotiedot", + "SERVICE=WMS", + "WIDTH=1000", + "HEIGHT=1000", + "TRANSPARENT=TRUE", + "LAYERS=avoindata%3AKiinteistokartan_maastotiedot", + ) + } + } + + @Test + fun `returns an image of the requested size`() { + val image = mapGenerator.mapWithAreas(wideArea, listOf(), 1000, 500) + + val png = ImageIO.read(ByteArrayInputStream(image)) + assertThat(png.width).isEqualTo(1000) + assertThat(png.height).isEqualTo(500) + } + } + + @Nested + inner class CalculateBounds { + + @Test + fun `returns correct bounds when fitting a wide area to a wide image`() { + val kaivuilmoitusalueet: List = + "/fi/hel/haitaton/hanke/pdf-test-data/wide-area.json".asJsonResource() + + val result = MapGenerator.calculateBounds(kaivuilmoitusalueet, 1000, 500) + + assertThat(result).all { + prop(MapBounds::xSize).isEqualTo(720.0) + prop(MapBounds::ySize).isEqualTo(720.0) + prop(MapBounds::center).all { + prop(Point::x).isEqualTo(25496700.0) + prop(Point::y).isEqualTo(6673050.0) + } + prop(MapBounds::min).all { + // Width is padded by 10%. + // Image and area are both wide, so no adjustment here. + prop(Point::x).isEqualTo(25496400.0 - 0.1 * 600.0) + prop(Point::x).isEqualTo(25496340.0) + // Y-coordinates are expanded so the height matches the width. + prop(Point::y).isEqualTo(result.center.y - (600 + 2 * 60) / 2) + prop(Point::y).isEqualTo(6672690.0) + } + prop(MapBounds::max).all { + prop(Point::x).isEqualTo(25497000.0 + 0.1 * 600.0) + prop(Point::x).isEqualTo(25497060.0) + prop(Point::y).isEqualTo(result.center.y + (600 + 2 * 60) / 2) + prop(Point::y).isEqualTo(6673410.0) + } + } + } + + @Test + fun `returns correct bounds when fitting a wide area to a tall image`() { + val result = MapGenerator.calculateBounds(wideArea, 500, 1000) + + assertThat(result).all { + // The area is wide. With padding, it's 720 wide. + // The image is tall, so the y-coordinates are expanded so the height is double + // (1000 / 500) the width, so the height is set at 1440. + // The x-coordinates are then expanded so that the width equals the height. + prop(MapBounds::xSize).isEqualTo(1440.0) + prop(MapBounds::ySize).isEqualTo(1440.0) + prop(MapBounds::center).all { + prop(Point::x).isEqualTo(25496700.0) + prop(Point::y).isEqualTo(6673050.0) + } + prop(MapBounds::min).all { + prop(Point::x).isEqualTo(result.center.x - result.xSize / 2) + prop(Point::x).isEqualTo(25495980.0) + prop(Point::y).isEqualTo(result.center.y - result.ySize / 2) + prop(Point::y).isEqualTo(6672330.0) + } + prop(MapBounds::max).all { + prop(Point::x).isEqualTo(result.center.x + result.xSize / 2) + prop(Point::x).isEqualTo(25497420.0) + prop(Point::y).isEqualTo(result.center.y + result.ySize / 2) + prop(Point::y).isEqualTo(6673770.0) + } + } + } + + @Test + fun `returns correct bounds when fitting a tall area to a wide image`() { + val result = MapGenerator.calculateBounds(tallArea, 1000, 500) + + assertThat(result).all { + // The area is tall. With padding, it's 260 tall. + // The image is wide, so the x-coordinates are expanded so the width is double + // (1000 / 500) the height, so the width is set at 520. + // The y-coordinates are then expanded so that the height equals the width. + prop(MapBounds::xSize).isEqualTo(520.0) + prop(MapBounds::ySize).isEqualTo(520.0) + prop(MapBounds::center).all { + prop(Point::x).isEqualTo(25497275.0) + prop(Point::y).isEqualTo(6673000.0) + } + prop(MapBounds::min).all { + prop(Point::x).isEqualTo(result.center.x - result.xSize / 2) + prop(Point::x).isEqualTo(25497015.0) + prop(Point::y).isEqualTo(result.center.y - result.ySize / 2) + prop(Point::y).isEqualTo(6672740.0) + } + prop(MapBounds::max).all { + prop(Point::x).isEqualTo(result.center.x + result.xSize / 2) + prop(Point::x).isEqualTo(25497535.0) + prop(Point::y).isEqualTo(result.center.y + result.ySize / 2) + prop(Point::y).isEqualTo(6673260.0) + } + } + } + + @Test + fun `returns correct bounds when fitting a tall area to a tall image`() { + val result = MapGenerator.calculateBounds(tallArea, 500, 1000) + + assertThat(result).all { + prop(MapBounds::xSize).isEqualTo(260.0) + prop(MapBounds::ySize).isEqualTo(260.0) + prop(MapBounds::center).all { + prop(Point::x).isEqualTo(25497275.0) + prop(Point::y).isEqualTo(6673000.0) + } + prop(MapBounds::min).all { + // x-coordinates are expanded so the width matches the height. + prop(Point::x).isEqualTo(result.center.x - result.xSize / 2) + prop(Point::x).isEqualTo(25497145.0) + // The y-coordinates are padded with the minimum padding of 30. + // Image and area are both tall, so no adjustment here. + prop(Point::y).isEqualTo(6672900.0 - 30) + prop(Point::y).isEqualTo(6672870.0) + } + prop(MapBounds::max).all { + prop(Point::x).isEqualTo(result.center.x + result.xSize / 2) + prop(Point::x).isEqualTo(25497405.0) + prop(Point::y).isEqualTo(6673100.0 + 30) + prop(Point::y).isEqualTo(6673130.0) + } + } + } + + @Test + fun `returns correct bounds when there are many areas`() { + val result = MapGenerator.calculateBounds(manyAreas, 1000, 500) + + assertThat(result).all { + // Y-direction is the one dictating the image scale. The height of the extremes + // (6672760 and 6673002) of the areas is 242, so 302 with padding. + // This gives us a width of 302 * (1000 / 500) = 604. + // The height is then expanded to match the width. + prop(MapBounds::xSize).isEqualTo(604.0) + prop(MapBounds::ySize).isEqualTo(604.0) + prop(MapBounds::center).all { + prop(Point::x).isEqualTo(25497022.0) + prop(Point::y).isEqualTo(6672881.0) + } + prop(MapBounds::min).all { + prop(Point::x).isEqualTo(result.center.x - result.xSize / 2) + prop(Point::x).isEqualTo(25496720.0) + prop(Point::y).isEqualTo(result.center.y - result.ySize / 2) + prop(Point::y).isEqualTo(6672579.0) + } + prop(MapBounds::max).all { + prop(Point::x).isEqualTo(result.center.x + result.xSize / 2) + prop(Point::x).isEqualTo(25497324.0) + prop(Point::y).isEqualTo(result.center.y + result.ySize / 2) + prop(Point::y).isEqualTo(6673183.0) + } + } + } + } + + @Nested + inner class SelectColorStyle { + @Test + fun `returns blue when parameter is null`() { + val result = MapGenerator.selectColorStyle(null) + + assertThat(result).isSameInstanceAs(NuisanceColor.BLUE.style) + } + + @Test + fun `returns blue when one of the indexes is NaN`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy( + linjaautoliikenneindeksi = Float.NaN + ) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.BLUE.style) + } + + @Test + fun `returns blue when all indexes are negative`() { + val tormaystarkasteluTulos = + TormaystarkasteluTulos(Autoliikenneluokittelu(-1f, 0, 0, 0, 0, 0), -3f, -2f, -1f) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.BLUE.style) + } + + @Test + fun `returns gray when all indexes are zero`() { + val tormaystarkasteluTulos = HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.GRAY.style) + } + + @Test + fun `returns green when highest index is just above zero`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy( + linjaautoliikenneindeksi = 0.001f + ) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.GREEN.style) + } + + @Test + fun `returns green when highest index is just below three`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy(pyoraliikenneindeksi = 2.99f) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.GREEN.style) + } + + @Test + fun `returns yellow when highest index is exactly three`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy(pyoraliikenneindeksi = 3f) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.YELLOW.style) + } + + @Test + fun `returns yellow when highest index is just below four`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy( + raitioliikenneindeksi = 3.999f + ) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.YELLOW.style) + } + + @Test + fun `returns red when highest index is exactly four`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy(pyoraliikenneindeksi = 4f) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.RED.style) + } + + @Test + fun `returns red when highest index is 5`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy(raitioliikenneindeksi = 5f) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.RED.style) + } + + @Test + fun `returns red when highest index is way above five`() { + val tormaystarkasteluTulos = + HaittaFactory.TORMAYSTARKASTELUTULOS_WITH_ZEROES.copy(raitioliikenneindeksi = 999f) + + val result = MapGenerator.selectColorStyle(tormaystarkasteluTulos) + + assertThat(result).isSameInstanceAs(NuisanceColor.RED.style) + } + } + + class GeoserverDispatcher(private val baseUrl: HttpUrl) : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return request.path?.let { path -> + if (path.startsWith("/capabilities")) { + capabilityResponse() + } else if (path.startsWith("/image")) { + imageResponse() + } else null + } ?: MockResponse().setResponseCode(404) + } + + private fun capabilityResponse(): MockResponse { + val responseTemplate = + Template.parse( + "/fi/hel/haitaton/hanke/pdf-test-data/capabilities.xml".getResourceAsText() + ) + val params = mapOf("baseUrl" to baseUrl) + + return MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/vnd.ogc.wms_xml") + .setBody(responseTemplate.processToString(params)) + } + + private fun imageResponse(): MockResponse { + val bytes = "/fi/hel/haitaton/hanke/pdf-test-data/blank.png".getResourceAsBytes() + + return MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "image/png") + .setBody(Buffer().write(bytes)) + } + } +} diff --git a/services/hanke-service/src/test/resources/application-test.yml b/services/hanke-service/src/test/resources/application-test.yml index 6dcc45bd0..4dd8a8bce 100644 --- a/services/hanke-service/src/test/resources/application-test.yml +++ b/services/hanke-service/src/test/resources/application-test.yml @@ -14,6 +14,8 @@ haitaton: hanke-editing: true gdpr: disabled: true + map-service: + url: http://localhost:3004 profiili-api: api-tokens-url: http://localhost:14678/api-tokens/ graph-ql-url: http://localhost:14678/graphql/ diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/blank.png b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/blank.png new file mode 100644 index 000000000..33fb4d29a Binary files /dev/null and b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/blank.png differ diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/capabilities.xml b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/capabilities.xml new file mode 100644 index 000000000..3e40325e4 --- /dev/null +++ b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/capabilities.xml @@ -0,0 +1,380 @@ + + + + WMS + Helsinki_WMS + Helsingin kaupunkimittauspalveluiden ylläpitämä WMS-rajapintapalvelu internet-käyttäjille. + + + + + Kaupunkimittauspalvelut + Helsingin kaupunki + + + + +
+ Helsinki + + + Finland + + + + paikkatieto@hel.fi + + NONE + NONE + + + + + text/xml + + + + + + + + + + + + + image/png + application/atom+xml + application/json;type=geojson + application/json;type=topojson + application/json;type=utfgrid + application/pdf + application/rss+xml + application/vnd.google-earth.kml+xml + application/vnd.google-earth.kml+xml;mode=networklink + application/vnd.google-earth.kmz + application/vnd.mapbox-vector-tile + image/geotiff + image/geotiff8 + image/gif + image/jpeg + image/png; mode=8bit + image/svg+xml + image/tiff + image/tiff8 + image/vnd.jpeg-png + image/vnd.jpeg-png8 + text/html; subtype=openlayers + text/html; subtype=openlayers2 + text/html; subtype=openlayers3 + + + + + + + + + + text/plain + application/vnd.ogc.gml + text/xml + application/vnd.ogc.gml/3.1.1 + text/xml; subtype=gml/3.1.1 + text/html + text/javascript + application/json + + + + + + + + + + + XML + INIMAGE + BLANK + JSON + JSONP + + + + + http://www.paikkatietohakemisto.fi/geonetwork/srv/fin/catalog.search#/metadata/5db8b2fd-3a05-422f-b008-0d86bda2d17a + + application/vnd.iso.19139+xml + + + + fin + + + + fin + + + + Helsinki_WMS + Helsingin kaupunkimittauspalveluiden ylläpitämä WMS-rajapintapalvelu internet-käyttäjille. + + + EPSG:3067 + EPSG:3857 + EPSG:3873 + EPSG:3874 + EPSG:3875 + EPSG:3876 + EPSG:3877 + EPSG:3878 + EPSG:3879 + EPSG:3880 + EPSG:3881 + EPSG:3882 + EPSG:3883 + EPSG:3884 + EPSG:3885 + EPSG:4258 + EPSG:4123 + EPSG:3386 + EPSG:2391 + EPSG:2392 + EPSG:2393 + EPSG:2394 + EPSG:3387 + EPSG:3046 + EPSG:3047 + EPSG:3048 + EPSG:4326 + CRS:84 + + 24.080259974026852 + 25.695412558717038 + 59.78091399811141 + 68.90073337242026 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kiinteistokartat + + + EPSG:3879 + + 24.76473294586304 + 25.289558715996222 + 59.90927901305111 + 60.32245988225446 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + avoindata:Kiinteistokartan_maastotiedot + Kiinteistokartan_maastotiedot + + + EPSG:3879 + + 24.76473294586304 + 25.289558715996222 + 59.90927901305111 + 60.32245988225446 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/plain + + + + + + + + diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/hankealueet.json b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/hankealueet.json new file mode 100644 index 000000000..de49d5304 --- /dev/null +++ b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/hankealueet.json @@ -0,0 +1,376 @@ +[ + { + "id": 105, + "hankeId": 46, + "haittaAlkuPvm": "2025-01-02T00:00:00Z", + "haittaLoppuPvm": "2025-01-31T00:00:00Z", + "geometriat": { + "id": 118, + "featureCollection": { + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "features": [ + { + "type": "Feature", + "properties": { + "hankeTunnus": "HAI25-2" + }, + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "EPSG:3879" + } + }, + "coordinates": [ + [ + [ + 25496824.139549617, + 6672755.6806356 + ], + [ + 25497219.580001418, + 6672755.6806356 + ], + [ + 25497219.580001418, + 6673014.740453159 + ], + [ + 25496824.139549617, + 6673014.740453159 + ], + [ + 25496824.139549617, + 6672755.6806356 + ] + ] + ] + } + } + ] + }, + "version": 11, + "createdByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "createdAt": "2025-01-02T07:53:50.902Z", + "modifiedByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "modifiedAt": "2025-01-02T14:05:59.945Z" + }, + "kaistaHaitta": "EI_VAIKUTA", + "kaistaPituusHaitta": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "meluHaitta": "EI_MELUHAITTAA", + "polyHaitta": "EI_POLYHAITTAA", + "tarinaHaitta": "EI_TARINAHAITTAA", + "nimi": "Hankealue 3", + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 2.1, + "haitanKesto": 3, + "katuluokka": 4, + "liikennemaara": 4, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 5, + "liikennehaittaindeksi": { + "indeksi": 5, + "tyyppi": "RAITIOLIIKENNEINDEKSI" + } + }, + "haittojenhallintasuunnitelma": { + "MUUT": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "RAITIOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "YLEINEN": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "AUTOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "PYORALIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi." + } + }, + { + "id": 103, + "hankeId": 46, + "haittaAlkuPvm": "2014-10-01T00:00:00Z", + "haittaLoppuPvm": "2025-11-18T00:00:00Z", + "geometriat": { + "id": 116, + "featureCollection": { + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "features": [ + { + "type": "Feature", + "properties": { + "hankeTunnus": "HAI25-2" + }, + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "EPSG:3879" + } + }, + "coordinates": [ + [ + [ + 25496400, + 6673000 + ], + [ + 25496900, + 6672950 + ], + [ + 25497000, + 6673100 + ], + [ + 25496500, + 6673150 + ], + [ + 25496400, + 6673000 + ] + ] + ] + } + } + ] + }, + "version": 11, + "createdByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "createdAt": "2025-01-02T07:53:50.898Z", + "modifiedByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "modifiedAt": "2025-01-02T14:05:59.967Z" + }, + "kaistaHaitta": "EI_VAIKUTA", + "kaistaPituusHaitta": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "meluHaitta": "EI_MELUHAITTAA", + "polyHaitta": "EI_POLYHAITTAA", + "tarinaHaitta": "EI_TARINAHAITTAA", + "nimi": "Hankealue 1", + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 2.5, + "haitanKesto": 5, + "katuluokka": 4, + "liikennemaara": 5, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 5, + "raitioliikenneindeksi": 5, + "liikennehaittaindeksi": { + "indeksi": 5, + "tyyppi": "LINJAAUTOLIIKENNEINDEKSI" + } + }, + "haittojenhallintasuunnitelma": { + "MUUT": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "RAITIOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "YLEINEN": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "LINJAAUTOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "AUTOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "PYORALIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi." + } + }, + { + "id": 104, + "hankeId": 46, + "haittaAlkuPvm": "2025-01-02T00:00:00Z", + "haittaLoppuPvm": "2025-01-31T00:00:00Z", + "geometriat": { + "id": 117, + "featureCollection": { + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "features": [ + { + "type": "Feature", + "properties": { + "hankeTunnus": "HAI25-2" + }, + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "EPSG:3879" + } + }, + "coordinates": [ + [ + [ + 25497250, + 6672900 + ], + [ + 25497300, + 6672900 + ], + [ + 25497300, + 6673100 + ], + [ + 25497250, + 6673100 + ], + [ + 25497250, + 6672900 + ] + ] + ] + } + } + ] + }, + "version": 11, + "createdByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "createdAt": "2025-01-02T07:53:50.901Z", + "modifiedByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "modifiedAt": "2025-01-02T14:05:59.969Z" + }, + "kaistaHaitta": "EI_VAIKUTA", + "kaistaPituusHaitta": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "meluHaitta": "EI_MELUHAITTAA", + "polyHaitta": "EI_POLYHAITTAA", + "tarinaHaitta": "EI_TARINAHAITTAA", + "nimi": "Hankealue 2", + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 2.3, + "haitanKesto": 3, + "katuluokka": 4, + "liikennemaara": 5, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 5, + "liikennehaittaindeksi": { + "indeksi": 5, + "tyyppi": "RAITIOLIIKENNEINDEKSI" + } + }, + "haittojenhallintasuunnitelma": { + "MUUT": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "RAITIOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "YLEINEN": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "AUTOLIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "PYORALIIKENNE": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi." + } + }, + { + "id": 106, + "hankeId": 46, + "haittaAlkuPvm": "2025-01-03T00:00:00Z", + "haittaLoppuPvm": "2025-01-31T00:00:00Z", + "geometriat": { + "id": 119, + "featureCollection": { + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "features": [ + { + "type": "Feature", + "properties": { + "hankeTunnus": "HAI25-2" + }, + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "EPSG:3879" + } + }, + "coordinates": [ + [ + [ + 25497468.5556505, + 6673353.730021486 + ], + [ + 25497520.04488658, + 6673299.757124501 + ], + [ + 25497537.72820804, + 6673467.673836828 + ], + [ + 25497468.5556505, + 6673396.161895422 + ], + [ + 25497468.5556505, + 6673353.730021486 + ] + ] + ] + } + } + ] + }, + "version": 9, + "createdByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "createdAt": "2025-01-02T07:55:00.536Z", + "modifiedByUserId": "38310af1-0242-4efd-8b7b-5c2e01684f40", + "modifiedAt": "2025-01-02T14:05:59.971Z" + }, + "kaistaHaitta": "EI_VAIKUTA", + "kaistaPituusHaitta": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "meluHaitta": "EI_MELUHAITTAA", + "polyHaitta": "EI_POLYHAITTAA", + "tarinaHaitta": "EI_TARINAHAITTAA", + "nimi": "Hankealue 4", + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 0, + "haitanKesto": 3, + "katuluokka": 0, + "liikennemaara": 0, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 0, + "tyyppi": "LINJAAUTOLIIKENNEINDEKSI" + } + }, + "haittojenhallintasuunnitelma": { + "MUUT": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi.", + "YLEINEN": "Tähdellä * merkityt kentät vaaditaan hankkeen julkaisemiseksi." + } + } +] diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/many-areas.json b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/many-areas.json new file mode 100644 index 000000000..348b96e6f --- /dev/null +++ b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/many-areas.json @@ -0,0 +1,450 @@ +[ + { + "name": "Hankealue 3", + "hankealueId": 105, + "tyoalueet": [ + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25496842, + 6672974 + ], + [ + 25496855, + 6672974 + ], + [ + 25496855, + 6672992 + ], + [ + 25496842, + 6672992 + ], + [ + 25496842, + 6672974 + ] + ] + ] + }, + "area": 250.9791226760525, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 0.7, + "haitanKesto": 3, + "katuluokka": 2, + "liikennemaara": 0, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 0.7, + "tyyppi": "AUTOLIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25496867, + 6672804 + ], + [ + 25496877, + 6672804 + ], + [ + 25496877, + 6672818 + ], + [ + 25496867, + 6672818 + ], + [ + 25496867, + 6672804 + ] + ] + ] + }, + "area": 148.5957615267796, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 0, + "haitanKesto": 3, + "katuluokka": 0, + "liikennemaara": 0, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 0, + "tyyppi": "LINJAAUTOLIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25496978, + 6672808 + ], + [ + 25496996, + 6672808 + ], + [ + 25496996, + 6672817 + ], + [ + 25496978, + 6672817 + ], + [ + 25496978, + 6672808 + ] + ] + ] + }, + "area": 159.521427118922, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 0.7, + "haitanKesto": 3, + "katuluokka": 2, + "liikennemaara": 0, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 3, + "tyyppi": "PYORALIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25497185, + 6672820 + ], + [ + 25497202, + 6672820 + ], + [ + 25497202, + 6672846 + ], + [ + 25497185, + 6672846 + ], + [ + 25497185, + 6672820 + ] + ] + ] + }, + "area": 431.1831230811498, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 1.5, + "haitanKesto": 3, + "katuluokka": 2, + "liikennemaara": 3, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 1.5, + "tyyppi": "AUTOLIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25496961, + 6672760 + ], + [ + 25496999, + 6672760 + ], + [ + 25496999, + 6672771 + ], + [ + 25496961, + 6672771 + ], + [ + 25496961, + 6672760 + ] + ] + ] + }, + "area": 430.7122447388991, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 2.1, + "haitanKesto": 3, + "katuluokka": 4, + "liikennemaara": 4, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 3, + "tyyppi": "PYORALIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25497041, + 6672880 + ], + [ + 25497071, + 6672880 + ], + [ + 25497071, + 6672902 + ], + [ + 25497041, + 6672902 + ], + [ + 25497041, + 6672880 + ] + ] + ] + }, + "area": 654.9746946369295, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 1.2, + "haitanKesto": 3, + "katuluokka": 2, + "liikennemaara": 2, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 5, + "liikennehaittaindeksi": { + "indeksi": 5, + "tyyppi": "RAITIOLIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25497170, + 6672937 + ], + [ + 25497189, + 6672937 + ], + [ + 25497189, + 6672961 + ], + [ + 25497170, + 6672961 + ], + [ + 25497170, + 6672937 + ] + ] + ] + }, + "area": 434.7214573028518, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 1.5, + "haitanKesto": 3, + "katuluokka": 2, + "liikennemaara": 3, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 1.5, + "tyyppi": "AUTOLIIKENNEINDEKSI" + } + } + }, + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25497030, + 6672977 + ], + [ + 25497059, + 6672977 + ], + [ + 25497059, + 6673002 + ], + [ + 25497030, + 6673002 + ], + [ + 25497030, + 6672977 + ] + ] + ] + }, + "area": 742.3960700839747, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 1.5, + "haitanKesto": 3, + "katuluokka": 2, + "liikennemaara": 3, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 1.5, + "tyyppi": "AUTOLIIKENNEINDEKSI" + } + } + } + ], + "katuosoite": "Kaivokatu 12", + "tyonTarkoitukset": [ + "SAHKO" + ], + "meluhaitta": "EI_MELUHAITTAA", + "polyhaitta": "EI_POLYHAITTAA", + "tarinahaitta": "EI_TARINAHAITTAA", + "kaistahaitta": "EI_VAIKUTA", + "kaistahaittojenPituus": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "lisatiedot": "", + "haittojenhallintasuunnitelma": { + "MUUT": "Monta aluetta", + "YLEINEN": "Monta aluetta", + "AUTOLIIKENNE": "Monta aluetta", + "PYORALIIKENNE": "Monta aluetta", + "RAITIOLIIKENNE": "Monta aluetta", + "LINJAAUTOLIIKENNE": "" + } + } +] diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/small-area.json b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/small-area.json new file mode 100644 index 000000000..a81b4f0db --- /dev/null +++ b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/small-area.json @@ -0,0 +1,79 @@ +[ + { + "name": "Hankealue 4", + "hankealueId": 106, + "tyoalueet": [ + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25497473.99385086, + 6673378.826759577 + ], + [ + 25497474.784632027, + 6673378.348939784 + ], + [ + 25497475.573286522, + 6673379.463177532 + ], + [ + 25497474.421387173, + 6673380.052215219 + ], + [ + 25497473.99385086, + 6673378.826759577 + ] + ] + ] + }, + "area": 1.4525061049297503, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 0, + "haitanKesto": 3, + "katuluokka": 0, + "liikennemaara": 0, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 0, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 0, + "liikennehaittaindeksi": { + "indeksi": 0, + "tyyppi": "LINJAAUTOLIIKENNEINDEKSI" + } + } + } + ], + "katuosoite": "Kaivokatu 12", + "tyonTarkoitukset": [ + "SAHKO" + ], + "meluhaitta": "EI_MELUHAITTAA", + "polyhaitta": "EI_POLYHAITTAA", + "tarinahaitta": "EI_TARINAHAITTAA", + "kaistahaitta": "EI_VAIKUTA", + "kaistahaittojenPituus": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "lisatiedot": "", + "haittojenhallintasuunnitelma": { + "MUUT": "Pieni alue", + "YLEINEN": "Pieni alue", + "AUTOLIIKENNE": "", + "PYORALIIKENNE": "", + "RAITIOLIIKENNE": "", + "LINJAAUTOLIIKENNE": "" + } + } +] diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/tall-area.json b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/tall-area.json new file mode 100644 index 000000000..fc7623049 --- /dev/null +++ b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/tall-area.json @@ -0,0 +1,79 @@ +[ + { + "name": "Hankealue 2", + "hankealueId": 104, + "tyoalueet": [ + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25497250, + 6672900 + ], + [ + 25497300, + 6672900 + ], + [ + 25497300, + 6673100 + ], + [ + 25497250, + 6673100 + ], + [ + 25497250, + 6672900 + ] + ] + ] + }, + "area": 20591.567851680044, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 2.3, + "haitanKesto": 3, + "katuluokka": 4, + "liikennemaara": 5, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 0, + "raitioliikenneindeksi": 5, + "liikennehaittaindeksi": { + "indeksi": 5, + "tyyppi": "RAITIOLIIKENNEINDEKSI" + } + } + } + ], + "katuosoite": "Kaivokatu 12", + "tyonTarkoitukset": [ + "SAHKO" + ], + "meluhaitta": "EI_MELUHAITTAA", + "polyhaitta": "EI_POLYHAITTAA", + "tarinahaitta": "EI_TARINAHAITTAA", + "kaistahaitta": "EI_VAIKUTA", + "kaistahaittojenPituus": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "lisatiedot": "", + "haittojenhallintasuunnitelma": { + "MUUT": "Pystysuora", + "YLEINEN": "Pystysuora", + "AUTOLIIKENNE": "Pystysuora", + "PYORALIIKENNE": "Pystysuora", + "RAITIOLIIKENNE": "Pystysuora", + "LINJAAUTOLIIKENNE": "" + } + } +] diff --git a/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/wide-area.json b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/wide-area.json new file mode 100644 index 000000000..61c09d20f --- /dev/null +++ b/services/hanke-service/src/test/resources/fi/hel/haitaton/hanke/pdf-test-data/wide-area.json @@ -0,0 +1,79 @@ +[ + { + "name": "Hankealue 1", + "hankealueId": 103, + "tyoalueet": [ + { + "geometry": { + "type": "Polygon", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::3879" + } + }, + "coordinates": [ + [ + [ + 25496400, + 6673000 + ], + [ + 25496900, + 6672950 + ], + [ + 25497000, + 6673100 + ], + [ + 25496500, + 6673150 + ], + [ + 25496400, + 6673000 + ] + ] + ] + }, + "area": 17435.08397593583, + "tormaystarkasteluTulos": { + "autoliikenne": { + "indeksi": 2.3, + "haitanKesto": 3, + "katuluokka": 4, + "liikennemaara": 5, + "kaistahaitta": 0, + "kaistapituushaitta": 0 + }, + "pyoraliikenneindeksi": 3, + "linjaautoliikenneindeksi": 5, + "raitioliikenneindeksi": 5, + "liikennehaittaindeksi": { + "indeksi": 5, + "tyyppi": "LINJAAUTOLIIKENNEINDEKSI" + } + } + } + ], + "katuosoite": "Kaivokatu 12", + "tyonTarkoitukset": [ + "SAHKO" + ], + "meluhaitta": "EI_MELUHAITTAA", + "polyhaitta": "EI_POLYHAITTAA", + "tarinahaitta": "EI_TARINAHAITTAA", + "kaistahaitta": "EI_VAIKUTA", + "kaistahaittojenPituus": "EI_VAIKUTA_KAISTAJARJESTELYIHIN", + "lisatiedot": "", + "haittojenhallintasuunnitelma": { + "MUUT": "Vaakasuora", + "YLEINEN": "Vaakasuora", + "AUTOLIIKENNE": "Vaakasuora", + "PYORALIIKENNE": "Vaakasuora", + "RAITIOLIIKENNE": "Vaakasuora", + "LINJAAUTOLIIKENNE": "Vaakasuora" + } + } +]