-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HAI-3353 Add map of all areas to haittojenhallintasuunnitelma PDF (#916)
Add a map with all tyoalueet near the top of the PDF. Other maps for the actual haittojenhallintasuunnietlma will be added later. The base map is fetched from Helsinki Karttapalvelu, by calling their Web Map Service with GeoTools. The areas a rendered on top as polygons, also using GeoTools for the rendering.
- Loading branch information
Showing
23 changed files
with
2,376 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapBounds.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} | ||
} |
180 changes: 180 additions & 0 deletions
180
services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/MapGenerator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<KaivuilmoitusAlue>, | ||
hankealueet: List<SavedHankealue>, | ||
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<JsonPolygon>() ?: 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<KaivuilmoitusAlue>, | ||
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<JsonPolygon>): SimpleFeatureCollection { | ||
val reader = GeoJsonReader() | ||
|
||
val features: MutableList<SimpleFeature> = 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() | ||
} | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/pdf/NuisanceColor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.