⇧ 規模がえぐいて...。
GeoJSONとは?
Wikipediaさんによりますと、
GeoJSON is an open standard format designed for representing simple geographical features, along with their non-spatial attributes. It is based on the JSON format.
The GeoJSON format differs from other GIS standards in that it was written and is maintained not by a formal standards organization, but by an Internet working group of developers.
A notable offspring of GeoJSON is TopoJSON, an extension of GeoJSON that encodes geospatial topology and that typically provides smaller file sizes.
⇧ 地理情報に関する情報を持ったJSONということらしい。管理してる機関というものが無いってのが危険な臭いがしますな...
一応、
The GeoJSON format working group and discussion were begun in March 2007 and the format specification was finalized in June 2008.
In April 2015 the Internet Engineering Task Force has founded the Geographic JSON working group which released GeoJSON as RFC 7946 in August 2016.
⇧ 2016年8月に、RFC 7946で定義はなされている模様。
経緯としては、
The GeoJSON Specification (RFC 7946)
In 2015, the Internet Engineering Task Force (IETF), in conjunction with the original specification authors, formed a GeoJSON WG to standardize GeoJSON. RFC 7946 was published in August 2016 and is the new standard specification of the GeoJSON format, replacing the 2008 GeoJSON specification.
⇧ 2008年に仕様が決まってたんだけど、2016年8月に改めて仕様を策定したってことっぽい。
RFC 7946 によると、
Applications that use this media type: No known applications
currently use this media type. This media type is intended for
GeoJSON applications currently using the "application/
vnd.geo+json" or "application/json" media types, of which there
are several categories: web mapping, geospatial databases,
geographic data processing APIs, data analysis and storage
services, and data dissemination.
どっちでも良いらしいが、「application/json」を指定しておけば良いんかな?
で、肝心の「GeoJSON」のフォーマットなのですが、
GeoJSON always consists of a single object. This object (referred to as the GeoJSON object below) represents a geometry, feature, or collection of features.
-
The GeoJSON object may have any number of members (name/value pairs).
-
The GeoJSON object must have a member with the name
"type"
. This member's value is a string that determines the type of the GeoJSON object. -
The value of the type member must be one of:
"Point"
,"MultiPoint"
,"LineString"
,"MultiLineString"
,"Polygon"
,"MultiPolygon"
,"GeometryCollection"
,"Feature"
, or"FeatureCollection"
. The case of the type member values must be as shown here. -
A GeoJSON object may have an optional
"crs"
member, the value of which must be a coordinate reference system object (see 3. Coordinate Reference System Objects). -
A GeoJSON object may have a
"bbox"
member, the value of which must be a bounding box array (see 4. Bounding Boxes).
https://web.archive.org/web/20160827120507/http://geojson.org/geojson-spec.html#definitions
⇧「type」というプロパティは必須で、
- Point
- MultiPoint
- LineString
- MultiLineString
- Polygon
- MultiPolygon
- GeometryCollection
- Feature
- FeatureCollection
⇧「type」というプロパティの値が9つあるらしいってことは何となく分かるものの、「GeoJSON」のプロパティの全量が見えてこない...
このあたり、もうちょっと整備されたドキュメントが欲しいですな...
JacksonがGeoJSONに対応していないらしい...
JSONからJavaのオブジェクトにデシリアライズする場合に、Jacksonというライブラリが利用されることが多いとは思うのだけど、JacksonはGeoJSONに対応していないらしい...。
Spring Web MVCで、リクエストにJSONを受け取る場合に、@RequestBodyを使うって情報がネットにあるのだけど、
@RequestBody
You can use the @RequestBody annotation to have the request body read and deserialized into an Object through an HttpMessageReader.
⇧@requestBodyに指定したJavaのオブジェクトのデシリアライズは、「HttpMessageReader」ってのが受け持ってるらしく、
Codecs
The spring-web and spring-core modules provide support for serializing and deserializing byte content to and from higher level objects through non-blocking I/O with Reactive Streams back pressure.
https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-codecs
The spring-core module provides byte[], ByteBuffer, DataBuffer, Resource, and String encoder and decoder implementations. The spring-web module provides Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers and other encoders and decoders along with web-only HTTP message reader and writer implementations for form data, multipart content, server-sent events, and others.
https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-codecs
⇧ おそらく、@RequestBodyのデシリアライズにJacksonを使っていそうな気がする。
Spring Web MVCを使っている場合は、
HTTP message codecs
ServerCodecConfigurer provides a set of default readers and writers. You can use it to add more readers and writers, customize the default ones, or replace the default ones completely. For Jackson JSON and XML, consider using Jackson2ObjectMapperBuilder, which customizes Jackson’s default properties with the following ones:
- DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES is disabled.
- MapperFeature.DEFAULT_VIEW_INCLUSION is disabled.
It also automatically registers the following well-known modules if they are detected on the classpath:
⇧ 追加の仕方は説明ないんね...
ちなみに、JSONからJavaオブジェクトへデシリアライズされる流れについては、
⇧ 上記サイト様の図がイメージしやすいかと。
話が脱線しましたが、Jacksonは、GeoJSONに対応していないらしいですと。
GeoJSONをデシリアライズしてSpring Data JPAでPostgreSQLにINSERTしてみる
なので、GeoJSONをデシリアライズする部分は、自分で用意する必要がありますと。
⇧ 前回の記事の「Spring Boot」プロジェクトに追加・修正をして、GeoJSONのデシリアライズに対応して、PostgreSQLへINSERTする処理を追加しました。
追加したファイルなど。
ソースコードなど。
■/geometry/build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.2' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' // https://mvnrepository.com/artifact/net.postgis/postgis-jdbc implementation group: 'net.postgis', name: 'postgis-jdbc', version: '2021.1.0' annotationProcessor 'org.projectlombok:lombok' // https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-spatial implementation group: 'org.hibernate.orm', name: 'hibernate-spatial', version: '6.3.0.CR1' // https://mvnrepository.com/artifact/com.graphhopper.external/jackson-datatype-jts implementation group: 'com.graphhopper.external', name: 'jackson-datatype-jts', version: '2.14' // https://mvnrepository.com/artifact/org.locationtech.jts/jts-core //simplementation group: 'org.locationtech.jts', name: 'jts-core', version: '1.19.0' implementation 'org.locationtech.jts.io:jts-io-common:1.18.2' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.13.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
■/geometry/src/main/resources/application.properties
# データベース接続情報 spring.jpa.database=POSTGRESQL spring.datasource.url=jdbc:postgresql://ubuntuhost:5432/sample spring.datasource.username=dev_web spring.datasource.password=password #spring.jpa.database-platform=org.hibernate.spatial.dialect.postgis.PostgisDialect spring.jpa.show-sql=true #spring.jpa.properties.hibernate.dialect=org.hibernate.spatial.dialect.postgis.PostgisDialect logging.level.org.springframework.web=debug logging.level.org.hibernate=error logging.level.tomcat=trace #spring.main.banner-mode=off spring.output.ansi.enabled=ALWAYS # 「空間参照系識別コード(SRID:Spatial Reference System IDentifier)」 srid.JGD2011.B.L=6668
■/geometry/src/main/java/com/example/demo/config/JtsConfig.java
package com.example.demo.config; import org.locationtech.jts.geom.Geometry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import com.example.demo.dto.flood.CustomGeometryDeserializer; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; @Configuration public class JtsConfig { @Bean @Primary public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(Geometry.class, new CustomGeometryDeserializer()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); mapper.registerModule(module); return mapper; } }
■/geometry/src/main/java/com/example/demo/constants/flood/FloodConstant.java
package com.example.demo.constants.flood; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import lombok.Data; @Component @Data public class FloodConstant { @Value("${srid.JGD2011.B.L}") public static String srid; public final static String FLOOD_ASSUMED_AREA_SHIZUOKA = "flood_assumed_area_shizuoka"; public final static String FLOOD_ASSUMED_AREA_SHIZUOKA_SEQ = "flood_assumed_area_shizuoka_gid_seq"; }
■/geometry/src/main/java/com/example/demo/dto/flood/CustomGeometryDeserializer.java
package com.example.demo.dto.flood; import java.io.IOException; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.geojson.GeoJsonReader; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; public class CustomGeometryDeserializer extends JsonDeserializer<geometry> { @Override public Geometry deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); GeoJsonReader reader = new GeoJsonReader(); Geometry geometry = null; try { geometry = castTypeForGeometry(reader.read(node.toString())); } catch (ParseException e) { e.printStackTrace(); // 例外処理 } return geometry; } private static Geometry castTypeForGeometry (Geometry geometry) { if (geometry instanceof Point) { return (Point)geometry; } else if (geometry instanceof MultiPoint) { return (MultiPoint)geometry; } else if (geometry instanceof LineString) { return (LineString)geometry; } else if (geometry instanceof LinearRing) { return (LinearRing)geometry; } else if (geometry instanceof MultiLineString) { return (MultiLineString)geometry; } else if (geometry instanceof Polygon) { return (Polygon)geometry; } else if (geometry instanceof MultiPolygon) { return (MultiPolygon)geometry; } else if (geometry instanceof GeometryCollection) { return (GeometryCollection)geometry; } return geometry; } }
■/geometry/src/main/java/com/example/demo/dto/flood/FloodGeoJsonDto.java
package com.example.demo.dto.flood; import java.io.Serializable; import org.locationtech.jts.geom.Geometry; import org.springframework.stereotype.Component; import com.bedatadriven.jackson.datatype.jts.serialization.GeometrySerializer; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Component @Data @NoArgsConstructor @AllArgsConstructor public class FloodGeoJsonDto implements Serializable { @JsonProperty(value="srid") private Integer srid; @JsonInclude(JsonInclude.Include.ALWAYS) @JsonProperty(value="geometryId") private Long geometryId; @JsonProperty(value="hazardousAreaClassification") private short hazardousAreaClassification; @JsonProperty(value="geometry") @JsonSerialize(using = GeometrySerializer.class) @JsonDeserialize(using = CustomGeometryDeserializer.class) //@JsonBackReference private Geometry geometryInfo; }
■/geometry/src/main/java/com/example/demo/entity/flood/FloodAssumedAreaShizuoka.java
package com.example.demo.entity.flood; import org.locationtech.jts.geom.Geometry; import com.bedatadriven.jackson.datatype.jts.serialization.GeometryDeserializer; import com.bedatadriven.jackson.datatype.jts.serialization.GeometrySerializer; import com.example.demo.constants.flood.FloodConstant; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; //import net.postgis.jdbc.geometry.Geometry; @Entity @Table(name="flood_assumed_area_shizuoka") @Data @NoArgsConstructor @AllArgsConstructor public class FloodAssumedAreaShizuoka { @Id @SequenceGenerator(name = FloodConstant.FLOOD_ASSUMED_AREA_SHIZUOKA , sequenceName = FloodConstant.FLOOD_ASSUMED_AREA_SHIZUOKA_SEQ , allocationSize = 1) @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="gid") private Long geometryId; /** 危険区域区分 */ @Column(name="a31_401") private short hazardousAreaClassification; @Column(name="geom", columnDefinition ="geometry(Multipolygon, 6668)") @JsonSerialize(using = GeometrySerializer.class) @JsonDeserialize(using = GeometryDeserializer.class) private Geometry geometry; }
■/geometry/src/main/java/com/example/demo/repository/flood/FloodAssumedAreaShizuokaRepository.java
package com.example.demo.repository.flood; import org.springframework.data.jpa.repository.JpaRepository; import com.example.demo.entity.flood.FloodAssumedAreaShizuoka; public interface FloodAssumedAreaShizuokaRepository extends JpaRepository<FloodAssumedAreaShizuoka, Long> { }
■/geometry/src/main/java/com/example/demo/service/flood/FloodAssumedAreaShizuokaService.java
package com.example.demo.service.flood; import java.util.List; import com.example.demo.entity.flood.FloodAssumedAreaShizuoka; public interface FloodAssumedAreaShizuokaService { /** * 全件検索 * @return */ public List<FloodAssumedAreaShizuoka> findAll(); /** * 検索(gid) * @param gid geometry identifer * @return */ public FloodAssumedAreaShizuoka findById(Long gid); public int insert(FloodAssumedAreaShizuoka floodAssumedAreaShizuoka); }
■/geometry/src/main/java/com/example/demo/service/impl/flood/FloodAssumedAreaShizuokaServiceImpl.java
package com.example.demo.service.impl.flood; import java.util.List; import java.util.Objects; import java.util.Optional; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Polygon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.example.demo.dto.flood.FloodGeoJsonDto; import com.example.demo.entity.flood.FloodAssumedAreaShizuoka; import com.example.demo.repository.flood.FloodAssumedAreaShizuokaRepository; import com.example.demo.service.flood.FloodAssumedAreaShizuokaService; @Service public class FloodAssumedAreaShizuokaServiceImpl implements FloodAssumedAreaShizuokaService { @Autowired private FloodAssumedAreaShizuokaRepository floodAssumedAreaShizuokaRepository; @Override public List<FloodAssumedAreaShizuoka> findAll() { // 全件検索 List<FloodAssumedAreaShizuoka> floodAssumedAreaShizuoka = floodAssumedAreaShizuokaRepository.findAll(); return floodAssumedAreaShizuoka; } @Override public FloodAssumedAreaShizuoka findById(Long gid) { // 検索(gid) Optional<FloodAssumedAreaShizuoka> floodAssumedAreaShizuoka = floodAssumedAreaShizuokaRepository.findById(gid); return floodAssumedAreaShizuoka.isPresent() ? floodAssumedAreaShizuoka.get() : null; } @Override public int insert(FloodAssumedAreaShizuoka floodAssumedAreaShizuoka) { // 登録 floodAssumedAreaShizuokaRepository.save(floodAssumedAreaShizuoka); return 1; } public FloodAssumedAreaShizuoka createSaveData(FloodGeoJsonDto floodGeoJsonDto) { // input check if (Objects.isNull(floodGeoJsonDto) || Objects.isNull(floodGeoJsonDto.getGeometryInfo()) || Objects.isNull(floodGeoJsonDto.getGeometryInfo().getCoordinates())) { return null; } // create a MultiPolygon object GeometryFactory geometryFactory = new GeometryFactory(); Coordinate[] coordinates = floodGeoJsonDto.getGeometryInfo().getCoordinates(); LinearRing linearRing = geometryFactory.createLinearRing(coordinates); Polygon polygon = geometryFactory.createPolygon(linearRing); MultiPolygon multiPolygon = geometryFactory.createMultiPolygon(new Polygon[] {polygon}); FloodAssumedAreaShizuoka floodAssumedAreaShizuoka = new FloodAssumedAreaShizuoka(); multiPolygon.setSRID( Objects.nonNull(floodGeoJsonDto.getSrid()) ? floodGeoJsonDto.getSrid() : 6668 ); floodAssumedAreaShizuoka.setGeometry(multiPolygon); return floodAssumedAreaShizuoka; } }
■/geometry/src/main/java/com/example/demo/controller/flood/FloodAssumedAreaController.java
package com.example.demo.controller.flood; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.example.demo.dto.flood.FloodGeoJsonDto; import com.example.demo.entity.flood.FloodAssumedAreaShizuoka; import com.example.demo.service.impl.flood.FloodAssumedAreaShizuokaServiceImpl; @RestController @RequestMapping("floodAssumedArea") public class FloodAssumedAreaController { @Autowired FloodAssumedAreaShizuokaServiceImpl floodAssumedAreaShizuokaServiceImpl; @GetMapping(value="/shizuoka") public List<FloodAssumedAreaShizuoka> serachFloodForShizuoka() { List<FloodAssumedAreaShizuoka> floodAssumedAreaShizuoka = floodAssumedAreaShizuokaServiceImpl.findAll(); return floodAssumedAreaShizuoka; } @GetMapping(value="/shizuoka/{gid}") public FloodAssumedAreaShizuoka searchbyIdForShizuoka(@PathVariable("gid") Long gid) { FloodAssumedAreaShizuoka floodAssumedAreaShizuoka = floodAssumedAreaShizuokaServiceImpl.findById(gid); return floodAssumedAreaShizuoka; } @PostMapping(value="/shizuoka/save", consumes = "application/json") public int saveForShizuoka(@RequestBody FloodGeoJsonDto floodGeoJsonDto) { FloodAssumedAreaShizuoka floodAssumedAreaShizuoka = floodAssumedAreaShizuokaServiceImpl.createSaveData(floodGeoJsonDto); int result = 0; if (Objects.nonNull(floodAssumedAreaShizuoka)) { result = floodAssumedAreaShizuokaServiceImpl.insert(floodAssumedAreaShizuoka); } return result; } }
■/geometry/src/main/java/com/example/demo/GeometryApplication.java
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GeometryApplication { public static void main(String[] args) { SpringApplication.run(GeometryApplication.class, args); } }
で、「WSL 2(Windows SubSystem for Linux 2)」のUbuntu を起動しておく。(PostgreSQLに接続できるように)
そしたらば、Eclipseで「Spring Boot」プロジェクトを起動して、
Postmanからリクエストして、INSERTされました。
{ "srid": 6668 ,"geometryId": null ,"hazardousAreaClassification":1 ,"geometry":{ "type": "MultiPolygon", "coordinates": [ [ [ [ 138.7328640796455, 35.14438644107201 ], [ 138.73281250000002, 35.14395833335422 ], [ 138.73271182134192, 35.143912300393865 ], [ 138.73256132323525, 35.14390846901091 ], [ 138.73239579992799, 35.14391290687336 ], [ 138.73228545494794, 35.143917238751285 ], [ 138.73218750000007, 35.14395833335422 ], [ 138.7321164773103, 35.144425906965054 ], [ 138.7322178234432, 35.144419947497475 ], [ 138.73247075234394, 35.144406571804595 ], [ 138.7327697723395, 35.14439229575155 ], [ 138.7328640796455, 35.14438644107201 ] ] ] ] } }
⇧ GeoJSONとしてデシリアライズ対象の部分は、
{ "type": "MultiPolygon", "coordinates": [ [ [ [ 138.7328640796455, 35.14438644107201 ], [ 138.73281250000002, 35.14395833335422 ], [ 138.73271182134192, 35.143912300393865 ], [ 138.73256132323525, 35.14390846901091 ], [ 138.73239579992799, 35.14391290687336 ], [ 138.73228545494794, 35.143917238751285 ], [ 138.73218750000007, 35.14395833335422 ], [ 138.7321164773103, 35.144425906965054 ], [ 138.7322178234432, 35.144419947497475 ], [ 138.73247075234394, 35.144406571804595 ], [ 138.7327697723395, 35.14439229575155 ], [ 138.7328640796455, 35.14438644107201 ] ] ] ] }
になるかと。
テーブルを確認すると、1件のレコードが追加されていたので、JavaのプログラムでINSERTできています。
というわけで、Javaに限らないかもしれないですが、リクエストでGeoJSONが連携された場合に、デシリアライズに対応する必要があるようです。
何て言うか、Jackson側でGeoJSONに対応して欲しいんだが...
もしや、
地理空間概念を表す値型。
⇧ Spring Framework側で、地理情報に対応してるんかね?
だけど、「org.springframework.data.geo」パッケージを見た感じ、MultiPolygonとか見当たらないし、そもそもGeoJSONとの関係がよく分からん...
何やら、
⇧「geotools」ってライブラリを依存関係に追加して、GeoJSONをファイル形式で受け取る形でデシリアライズするって方法もあるとか。
う~む、地理情報、闇が深そう...
フロントエンドで必要としている情報が、
⇧ 上記の通りだとすると、リクエストのインプット(サーバサイドに渡ってくるデータ)も変わるから、DTOの構造も変更する必要があるし、インプット・アウトプットのインターフェイスの仕様が決まらんと厳しいっすな...
毎度モヤモヤ感が半端ない...
今回はこのへんで。