Commit d7d412aa authored by Matthias Betz's avatar Matthias Betz
Browse files

initial release

parent 025a7b17
DB0_HOST=localhost
DB0_PORT=5432
DB0_USER=postgres
DB0_PASSWORD=changeme
DB1_USER=postgres
DB1_PASSWORD=changeme
.env
target/
*.log
*.log.gz
.idea/
*.iml
.vscode/
.settings/
.classpath
.project
FROM eclipse-temurin:17
RUN addgroup citygmlstore && \
adduser --no-create-home --system --ingroup citygmlstore citygmlstore
# Set working directory
WORKDIR /citygmlstore
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} citygmlstore-backend.jar
# Set ownership
RUN chown -R citygmlstore:citygmlstore /citygmlstore
USER citygmlstore:citygmlstore
ENTRYPOINT ["java", "-jar","/citygmlstore/citygmlstore-backend.jar"]
\ No newline at end of file
# CityGML Server # CityGML Server
A Spring Boot service that extracts [CityGML](https://www.ogc.org/standards/citygml) building data from a
[3D City Database (3DCityDB v5)](https://www.3dcitydb.org/) instance for an arbitrary area of interest and
streams it back to the caller as a CityGML file.
## How it works
## Getting started 1. A client sends a polygon (WKT, in WGS84 / `EPSG:4326`) describing the area of interest.
2. The polygon is reprojected to `EPSG:25832` (UTM zone 32N / ETRS89), the CRS the databases are stored in.
3. A spatial `S_INTERSECTS` query is run against the configured 3DCityDB, selecting every feature whose
envelope intersects the polygon.
4. Matching features are exported and streamed back to the client as a CityGML 2.0 document.
To make it easy for you to get started with GitLab, here's a list of recommended next steps. Multiple databases can be registered, each tagged with a **year**, so callers can request data for a
specific dataset vintage (e.g. `2019` or `2025`).
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! During export, `Building` features that carry a `dateOfConstruction` are annotated with an additional
`YearOfConstruction_Source` generic attribute (`Census 2022`).
## Add your files ## API
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files ### `GET /citygml`
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
| Parameter | Required | Default | Description |
|--------------|----------|---------|--------------------------------------------------------------------|
| `wktPolygon` | yes | — | Area of interest as a WKT `POLYGON`/`MULTIPOLYGON` in WGS84. |
| `year` | no | `2019` | Which yearly database to query. Returns `404` if no DB is registered for that year. |
| `attributes` | no | — | Optional list of attribute names. |
**Example**
```bash
curl -G "http://localhost:8110/citygml" \
--data-urlencode "wktPolygon=POLYGON((9.17 48.78, 9.18 48.78, 9.18 48.79, 9.17 48.79, 9.17 48.78))" \
--data-urlencode "year=2019" \
-o citygml.gml
``` ```
cd existing_repo
git remote add origin https://transfer.hft-stuttgart.de/gitlab/sektorsim/citygml-server.git ### `POST /citygml`
git branch -M master
git push -uf origin master Identical behaviour to `GET`, but the WKT polygon is sent in the request body instead of as a query
parameter (useful for large polygons). `year` and `attributes` remain query parameters.
```bash
curl -X POST "http://localhost:8110/citygml?year=2019" \
-H "Content-Type: text/plain" \
--data "POLYGON((9.17 48.78, 9.18 48.78, 9.18 48.79, 9.17 48.79, 9.17 48.78))" \
-o citygml.gml
``` ```
## Integrate with your tools **Responses**
- [ ] [Set up project integrations](https://transfer.hft-stuttgart.de/gitlab/sektorsim/citygml-server/-/settings/integrations) | Status | Meaning |
|--------|----------------------------------------------------------------------|
| `200` | CityGML document streamed as an attachment (`citygml.gml`). |
| `400` | WKT could not be parsed, was empty, or was not a polygon. |
| `404` | No database registered for the requested `year`. |
## Collaborate with your team ### Web UI
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) A simple Leaflet-based map is served from `/` (`src/main/resources/static/index.html`). It lets you draw a
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) polygon on a map, generates the WKT, and downloads the corresponding CityGML data from the server.
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy ## Requirements
Use the built-in continuous integration in GitLab. - Java 17
- Maven 3.9+
- A reachable [3DCityDB v5](https://www.3dcitydb.org/) PostgreSQL/PostGIS instance (SRID `25832`) per year
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) The build pulls dependencies from the OSGeo and 3DCityDB Maven repositories (configured in `pom.xml`).
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
*** ## Configuration
# Editing this README Configuration uses standard Spring profiles. The active profile is chosen via `SPRING_PROFILES_ACTIVE`
(defaults to `dev`).
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. | Profile | File | Port |
|----------|-----------------------------------|------|
| `dev` | `application-dev.properties` | 8110 |
| `docker` | `application-docker.properties` | 80 |
## Suggestions for a good README Database connections are defined as an indexed list under the `conn.databases` prefix:
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name ```properties
Choose a self-explaining name for your project. conn.databases[0].year=2019
conn.databases[0].host=localhost
conn.databases[0].port=5432
conn.databases[0].user=${DB0_USER:postgres}
conn.databases[0].password=${DB0_PASSWORD}
conn.databases[0].database=postgres
conn.databases[0].schema=citydb
```
## Description Secrets are read from an optional `.env` file at the project root. Copy the template and fill in your
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. credentials:
## Badges ```bash
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. cp .env.example .env
```
## Visuals ```
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. DB0_HOST=localhost
DB0_PORT=5432
DB0_USER=postgres
DB0_PASSWORD=changeme
## Installation DB1_USER=postgres
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. DB1_PASSWORD=changeme
```
## Usage ## Running
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support ### Locally (dev profile)
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap ```bash
If you have ideas for releases in the future, it is a good idea to list them in the README. mvn spring-boot:run
```
## Contributing The server starts on <http://localhost:8110>.
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. ### Build a JAR
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. ```bash
mvn clean package
java -jar target/citygml-server-1.0.0.jar
```
### Docker Compose
The provided `docker-compose.yml` starts a 3DCityDB PostgreSQL container (`store`) alongside the server:
```bash
cp .env.example .env # set your passwords first
docker compose up --build
```
## Authors and acknowledgment The server is exposed on port `80` and runs with the `docker` profile.
Show your appreciation to those who have contributed to the project.
> **Note:** `docker-compose.yml` and `application-docker.properties` expect database hosts named
> `citygmlstore-citydb5` (2019) and `citygmlstore-citydb5_2025` (2025). Adjust the service names,
> hostnames, and `conn.databases[*]` entries to match your actual database setup.
## Testing
```bash
mvn test
```
## License ## Tech stack
For open source projects, say how it is licensed.
## Project status - **Spring Boot 3.4.4** (Web, Log4j2) — REST API and streaming responses
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. - **[citydb-*](https://www.3dcitydb.org/) 1.1.0-rc.1** — 3DCityDB access and CityGML export
- **[citygml4j](https://github.com/citygml4j/citygml4j) 3.2.4** — CityGML model and (de)serialization
- **[JTS](https://github.com/locationtech/jts) 1.19.0** — geometry handling
- **[proj4j](https://github.com/locationtech/proj4j) 1.4.1** — coordinate reprojection (WGS84 → UTM32N)
- **[Jimfs](https://github.com/google/jimfs)** — in-memory filesystem for temporary export files
services:
store:
image: 3dcitydb/3dcitydb-pg:5
container_name: store
environment:
POSTGRES_PASSWORD: ${DB0_PASSWORD}
SRID: 25832
restart: unless-stopped
citygml-server:
build:
context: .
dockerfile: Dockerfile
container_name: citygmlstore
env_file:
- .env
ports:
- "80:80"
depends_on:
- store
environment:
SPRING_PROFILES_ACTIVE: docker
restart: unless-stopped
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath />
</parent>
<groupId>de.hft.stuttgart.sektorsim</groupId>
<artifactId>citygml-server</artifactId>
<version>1.0.0</version>
<name>citygml-server</name>
<description>CityGML Server</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>OSgeo</id>
<url>https://repo.osgeo.org/repository/release/</url>
</repository>
<repository>
<id>CityDB</id>
<url>https://3dcitydb.org/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.citygml4j/citygml4j-core -->
<dependency>
<groupId>org.citygml4j</groupId>
<artifactId>citygml4j-core</artifactId>
<version>3.2.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.citygml4j/citygml4j-xml -->
<dependency>
<groupId>org.citygml4j</groupId>
<artifactId>citygml4j-xml</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>org.citydb</groupId>
<artifactId>citydb-database</artifactId>
<version>1.1.0-rc.1</version>
</dependency>
<dependency>
<groupId>org.citydb</groupId>
<artifactId>citydb-cli</artifactId>
<version>1.1.0-rc.1</version>
</dependency>
<dependency>
<groupId>org.citydb</groupId>
<artifactId>citydb-database-postgres</artifactId>
<version>1.1.0-rc.1</version>
</dependency>
<dependency>
<groupId>org.citydb</groupId>
<artifactId>citydb-io-citygml</artifactId>
<version>1.1.0-rc.1</version>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.locationtech.proj4j</groupId>
<artifactId>proj4j</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.locationtech.proj4j</groupId>
<artifactId>proj4j-epsg</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package de.hft.stuttgart.sektorsim.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan
public class CityGMLServerApplication {
public static void main(String[] args) {
SpringApplication.run(CityGMLServerApplication.class, args);
}
}
package de.hft.stuttgart.sektorsim.server.controller;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.citydb.cli.ExecutionException;
import org.citydb.cli.exporter.ExportOptions;
import org.citydb.cli.util.CommandHelper;
import org.citydb.database.DatabaseException;
import org.citydb.database.DatabaseManager;
import org.citydb.database.adapter.DatabaseAdapterException;
import org.citydb.database.adapter.DatabaseAdapterManager;
import org.citydb.database.connection.ConnectionDetails;
import org.citydb.database.connection.PoolOptions;
import org.citydb.database.postgres.PostgresqlAdapter;
import org.citydb.io.IOAdapterException;
import org.citydb.io.IOAdapterManager;
import org.citydb.io.citygml.CityGMLAdapter;
import org.citydb.io.citygml.writer.CityGMLFormatOptions;
import org.citydb.io.writer.FeatureWriter;
import org.citydb.io.writer.WriteOptions;
import org.citydb.model.common.Name;
import org.citydb.model.feature.Feature;
import org.citydb.model.property.Attribute;
import org.citydb.operation.exporter.Exporter;
import org.citydb.query.Query;
import org.citydb.query.builder.sql.SqlBuildOptions;
import org.citydb.query.executor.QueryExecutor;
import org.citydb.query.executor.QueryResult;
import org.citydb.query.filter.Filter;
import org.citydb.query.filter.encoding.FilterParseException;
import org.citygml4j.core.model.CityGMLVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Service
public class CityDB5Connector {
private static final Logger logger = LoggerFactory.getLogger(CityDB5Connector.class);
private static final Name dateOfConstructionName =
Name.of("dateOfConstruction", "http://3dcitydb.org/3dcitydb/construction/5.0");
private static final Name dateOfConstructionNameSource =
Name.of("YearOfConstruction_Source", "http://3dcitydb.org/3dcitydb/generics/5.0");
private ExportOptions exportOptions;
private FileSystem fs;
private CommandHelper helper = CommandHelper.newInstance();
private Path tempDirectory;
private CityGMLAdapter ioAdapter;
private WriteOptions writeOptions;
@Autowired
private ConnectionStore connectionStore;
private Map<Integer, DatabaseManager> managerMap = new HashMap<>();
@PostConstruct void setup() throws ExecutionException, IOAdapterException, IOException {
for (DatabaseConnection databaseConnection : connectionStore.getDatabases()) {
ConnectionDetails details = new ConnectionDetails();
details.setHost(databaseConnection.getHost());
details.setPort(databaseConnection.getPort());
details.setUser(databaseConnection.getUser());
details.setPassword(databaseConnection.getPassword());
details.setDatabase(databaseConnection.getDatabase());
details.setSchema(databaseConnection.getSchema());
details.getProperties().put("ssl", false);
PoolOptions poolOptions = new PoolOptions();
poolOptions.setLoginTimeout(120);
details.setPoolOptions(poolOptions);
DatabaseManager databaseManager = connect(details);
managerMap.put(databaseConnection.getYear(), databaseManager);
}
exportOptions = getExportOptions();
IOAdapterManager ioManager = IOAdapterManager.newInstance();
ioAdapter = new CityGMLAdapter();
ioManager.register(ioAdapter);
fs = Jimfs.newFileSystem(Configuration.unix());
tempDirectory = fs.getPath("/temp");
Files.createDirectory(tempDirectory);
writeOptions = new WriteOptions();
String encoding = StandardCharsets.UTF_8.name();
writeOptions.setEncoding(encoding);
writeOptions.setNumberOfThreads(4);
writeOptions.setSrsName("EPSG:25832");
CityGMLFormatOptions options = new CityGMLFormatOptions().setVersion(CityGMLVersion.v2_0).setPrettyPrint(true);
writeOptions.getFormatOptions().set(options);
}
public boolean hasDatabaseForYear(int year) {
return managerMap.containsKey(year);
}
public void exportCityGML(String filterString, OutputStream out, int year) throws ExecutionException {
logger.info("Create query");
Query query = getQuery(exportOptions, filterString);
logger.info("Query created");
DatabaseManager databaseManager = managerMap.get(year);
if (databaseManager == null) {
throw new ExecutionException("No database manager found for year: " + year);
}
logger.info("Fetching query executer");
QueryExecutor executor = helper.getQueryExecutor(query,
SqlBuildOptions.defaults().omitDistinct(true).withColumn(null), tempDirectory,
databaseManager.getAdapter());
logger.info("Fetched query executer");
try (BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
FeatureWriter writer = ioAdapter.createWriter(new OutputFileDummy(bufferedOut), writeOptions)) {
logger.info("Creating exporter");
Exporter exporter = Exporter.newInstance();
logger.info("Executing request query");
long sequenceId = 1;
try (QueryResult result = executor.executeQuery()) {
exporter.startSession(databaseManager.getAdapter(), exportOptions);
AtomicBoolean shouldRun = new AtomicBoolean(true);
while (result.hasNext() && shouldRun.get()) {
long id = result.getId();
exporter.exportFeature(id, sequenceId++).whenComplete((feature, t) -> {
if (feature != null) {
try {
addAttributes(feature);
writer.write(feature, (success, e) -> {
if (success == Boolean.FALSE) {
shouldRun.set(false);
abort(feature, id, e);
}
});
} catch (Throwable e) {
shouldRun.set(false);
abort(feature, id, e);
}
} else {
shouldRun.set(false);
abort(null, id, t);
}
});
}
} finally {
logger.info("Closing session");
exporter.closeSession();
}
} catch (Throwable e) {
logger.warn("Database export aborted due to an error.");
throw new ExecutionException("A fatal error has occurred during export.", e);
}
}
private void addAttributes(Feature feature) {
if ("Building".equals(feature.getFeatureType().getLocalName()) &&
feature.getAttributes().getFirst(dateOfConstructionName).isPresent()) {
Attribute yocSource = Attribute.of(dateOfConstructionNameSource);
yocSource.setStringValue("Census 2022");
feature.getAttributes().put(yocSource);
}
}
private static void abort(Feature feature, long id, Throwable e) {
logger.error("Failed to export feature {}, id: {}", feature, id, e);
}
private DatabaseManager connect(ConnectionDetails connectionDetails) throws ExecutionException {
try {
DatabaseManager databaseManager = DatabaseManager.newInstance();
DatabaseAdapterManager adapterManager = DatabaseAdapterManager.newInstance().load();
adapterManager.register(new PostgresqlAdapter(), false);
databaseManager.connect(connectionDetails, adapterManager);
return databaseManager;
} catch (DatabaseException | SQLException | DatabaseAdapterException e) {
throw new ExecutionException("Failed to connect to the database.", e);
}
}
private ExportOptions getExportOptions() throws ExecutionException {
ExportOptions exportOptions = new ExportOptions();
exportOptions.setNumberOfThreads(4);
// SrsReference srs = new SrsReference();
// srs.setIdentifier("EPSG:25832");
// exportOptions.setTargetSrs(srs);
return exportOptions;
}
private Query getQuery(ExportOptions exportOptions, String filterString) throws ExecutionException {
try {
String textFilter = filterString;
Filter filter = Filter.ofText(textFilter);
Query query = new Query();
query.setFilter(filter);
return query;
} catch (FilterParseException e) {
throw new ExecutionException("Failed to parse the provided CQL2 filter expression.", e);
}
}
@PreDestroy
public void cleanup() throws IOException {
for (DatabaseManager databaseManager : managerMap.values()) {
databaseManager.disconnect();
}
fs.close();
}
}
package de.hft.stuttgart.sektorsim.server.controller;
public class CityGMLRetrieveException extends Exception {
private static final long serialVersionUID = 7839646792750970844L;
public CityGMLRetrieveException() {
super();
}
public CityGMLRetrieveException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public CityGMLRetrieveException(String message, Throwable cause) {
super(message, cause);
}
public CityGMLRetrieveException(String message) {
super(message);
}
public CityGMLRetrieveException(Throwable cause) {
super(cause);
}
}
package de.hft.stuttgart.sektorsim.server.controller;
import java.util.List;
import org.citydb.cli.ExecutionException;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.util.GeometryEditor;
import org.locationtech.jts.geom.util.GeometryEditor.CoordinateOperation;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.proj4j.CRSFactory;
import org.locationtech.proj4j.CoordinateReferenceSystem;
import org.locationtech.proj4j.CoordinateTransform;
import org.locationtech.proj4j.CoordinateTransformFactory;
import org.locationtech.proj4j.ProjCoordinate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@RestController
public class CityGMLServerController {
private static final Logger logger = LoggerFactory.getLogger(CityGMLServerController.class);
private static final CRSFactory crsFactory = new CRSFactory();
private static final CoordinateReferenceSystem srcCrs = crsFactory.createFromName("EPSG:4326"); // WGS84
private static final CoordinateReferenceSystem dstCrs = crsFactory.createFromName("EPSG:25832"); // UTM32N / ETRS89
private static final CoordinateTransformFactory ctFactory = new CoordinateTransformFactory();
private static final CoordinateTransform transform = ctFactory.createTransform(srcCrs, dstCrs);
private WKTReader reader = new WKTReader();
private CityDB5Connector cityDbConnector;
private static final Object lock = new Object();
@Autowired
public CityGMLServerController(CityDB5Connector cityDbConnector) {
this.cityDbConnector = cityDbConnector;
}
@GetMapping("/citygml")
public ResponseEntity<StreamingResponseBody> getCityGML(@RequestParam String wktPolygon,
@RequestParam(required = false) List<String> attributes, @RequestParam(defaultValue = "2019") int year) {
logger.info("getCityGML called with polygon: {}, attributes: {}, year {}", wktPolygon, attributes, year);
try {
Geometry geometry = reader.read(wktPolygon);
if (geometry.isEmpty()) {
logger.info("Empty geometry: {}", wktPolygon);
return ResponseEntity.badRequest().build();
}
if (!(geometry instanceof Polygon || geometry instanceof MultiPolygon)) {
logger.info("geometry not a polygon: {}", wktPolygon);
return ResponseEntity.badRequest().build();
}
if (!cityDbConnector.hasDatabaseForYear(year)) {
logger.info("No database found for year: {}", year);
return ResponseEntity.notFound().build();
}
StreamingResponseBody stream = out -> {
try {
logger.info("transforming geometry");
transformGeometry(geometry);
logger.info("Geometry transformed");
String transformedWkt = geometry.toText();
cityDbConnector.exportCityGML("S_INTERSECTS(Envelope," + transformedWkt + ")", out, year);
logger.info("Finished request export");
} catch (ExecutionException e) {
logger.error("Failed to retrieve CityGML", e);
throw new RuntimeException("Failed to retrieve CityGML", e);
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"citygml.gml\"")
.contentType(MediaType.APPLICATION_XML)
.body(stream);
} catch (ParseException e) {
logger.error("Error parsing WKT: {}", wktPolygon, e);
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/citygml")
public ResponseEntity<StreamingResponseBody> getCityGMLPost(@RequestBody String wktPolygon,
@RequestParam(required = false) List<String> attributes, @RequestParam(defaultValue = "2019") int year) {
logger.info("getCityGMLPost called with polygon: {}, attributes: {}, year {}", wktPolygon, attributes, year);
try {
Geometry geometry = reader.read(wktPolygon);
if (geometry.isEmpty()) {
logger.info("Empty geometry: {}", wktPolygon);
return ResponseEntity.badRequest().build();
}
if (!(geometry instanceof Polygon || geometry instanceof MultiPolygon)) {
logger.info("geometry not a polygon: {} instead is {}", wktPolygon, geometry);
return ResponseEntity.badRequest().build();
}
if (!cityDbConnector.hasDatabaseForYear(year)) {
logger.info("No database found for year: {}", year);
return ResponseEntity.notFound().build();
}
StreamingResponseBody stream = out -> {
try {
transformGeometry(geometry);
String transformedWkt = geometry.toText();
cityDbConnector.exportCityGML("S_INTERSECTS(Envelope," + transformedWkt + ")", out, year);
} catch (ExecutionException e) {
logger.error("Failed to retrieve CityGML", e);
throw new RuntimeException("Failed to retrieve CityGML", e);
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"citygml.gml\"")
.contentType(MediaType.APPLICATION_XML)
.body(stream);
} catch (ParseException e) {
logger.error("Error parsing WKT: {}", wktPolygon, e);
return ResponseEntity.badRequest().build();
}
}
private void transformGeometry(Geometry geom) {
synchronized (lock) {
GeometryEditor ed = new GeometryEditor(geom.getFactory());
ed.edit(geom, new CoordinateOperation() {
@Override
public Coordinate[] edit(Coordinate[] coordinates, Geometry geometry) {
for (int i = 0; i < coordinates.length; i++) {
ProjCoordinate src = new ProjCoordinate(coordinates[i].x, coordinates[i].y);
ProjCoordinate dst = new ProjCoordinate();
transform.transform(src, dst);
coordinates[i] = new Coordinate(dst.x, dst.y);
}
return coordinates;
}
});
}
}
}
package de.hft.stuttgart.sektorsim.server.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "conn")
public class ConnectionStore {
private final List<DatabaseConnection> databases = new ArrayList<>();
public List<DatabaseConnection> getDatabases() {
return databases;
}
}
package de.hft.stuttgart.sektorsim.server.controller;
public class DatabaseConnection {
private int year;
private String host;
private int port;
private String user;
private String password;
private String database;
private String schema;
public int getYear() {
return year;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
public String getDatabase() {
return database;
}
public String getSchema() {
return schema;
}
public void setYear(int year) {
this.year = year;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setUser(String user) {
this.user = user;
}
public void setPassword(String password) {
this.password = password;
}
public void setDatabase(String database) {
this.database = database;
}
public void setSchema(String schema) {
this.schema = schema;
}
}
package de.hft.stuttgart.sektorsim.server.controller;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import org.citydb.core.file.FileType;
import org.citydb.core.file.OutputFile;
public class OutputFileDummy extends OutputFile {
private OutputStream out;
public OutputFileDummy(OutputStream out) {
super(Path.of("NULL"), FileType.REGULAR);
this.out = out;
}
@Override
public OutputStream openStream() throws IOException {
return out;
}
@Override
public String resolve(String... paths) {
throw new UnsupportedOperationException();
}
@Override
public void createDirectories(String path) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public OutputStream newOutputStream(String file) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void close() throws IOException {
out.close();
}
@Override
public Path getFile() {
throw new UnsupportedOperationException();
}
@Override
public FileType getFileType() {
throw new UnsupportedOperationException();
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
@XmlType(name = "exterior", namespace = "http://www.opengis.net/gml/3.2")
public class ExteriorXml {
@XmlElement(name = "LinearRing", namespace = "http://www.opengis.net/gml/3.2")
private LinearRingValue linearRing;
public LinearRingValue getLinearRing() {
return linearRing;
}
public void setLinearRing(LinearRingValue linearRing) {
this.linearRing = linearRing;
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
@XmlType(name = "Filter", namespace = "http://www.opengis.net/fes/2.0")
public class Filter {
@XmlElement(name = "Intersects", namespace = "http://www.opengis.net/fes/2.0")
private Intersects intersects;
public void setIntersects(Intersects intersects) {
this.intersects = intersects;
}
public Intersects getIntersects() {
return intersects;
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
import java.io.InputStream;
import java.io.OutputStream;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "GetFeature")
public class GetFeatureRequest {
private static final JAXBContext CONTEXT;
@XmlAttribute
private String version;
@XmlAttribute
private String service;
@XmlAttribute
private String handle;
@XmlElement(name = "Query")
private Query query;
static {
try {
CONTEXT = JAXBContext.newInstance(GetFeatureRequest.class);
} catch (JAXBException e) {
throw new RuntimeException("Failed to create JAXBContext", e);
}
}
public static GetFeatureRequest parse(InputStream inputStream) throws JAXBException {
return (GetFeatureRequest) CONTEXT.createUnmarshaller().unmarshal(inputStream);
}
public void writeTo(OutputStream outputStream) throws JAXBException {
CONTEXT.createMarshaller().marshal(this, outputStream);
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getHandle() {
return handle;
}
public void setHandle(String handle) {
this.handle = handle;
}
public Query getQuery() {
return query;
}
public void setQuery(Query query) {
this.query = query;
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
import org.locationtech.jts.geom.Polygon;
import de.hft.stuttgart.sektorsim.server.wfs.converter.PolygonAdapter;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlType(name = "Intersects", namespace = "http://www.opengis.net/fes/2.0")
public class Intersects {
@XmlElement(name = "ValueReference")
private String valueReference;
@XmlElement(name = "Polygon", namespace = "http://www.opengis.net/gml/3.2")
@XmlJavaTypeAdapter(PolygonAdapter.class)
private Polygon polygon;
public void setValueReference(String valueReference) {
this.valueReference = valueReference;
}
public String getValueReference() {
return valueReference;
}
public void setPolygon(Polygon polygon) {
this.polygon = polygon;
}
public Polygon getPolygon() {
return polygon;
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
import java.util.ArrayList;
import java.util.List;
import de.hft.stuttgart.sektorsim.server.wfs.converter.PosListAdapter;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
public class LinearRingValue {
@XmlElement(name = "posList", namespace = "http://www.opengis.net/gml/3.2")
@XmlJavaTypeAdapter(PosListAdapter.class)
private List<Vector2d> posList;
public List<Vector2d> getPosList() {
if (posList == null) {
posList = new ArrayList<>();
}
return posList;
}
public void setPosList(List<Vector2d> posList) {
this.posList = posList;
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
public class PolygonAttributes {
private String id;
private String srsName;
public PolygonAttributes(String id, String srsName) {
this.id = id;
this.srsName = srsName;
}
public String getId() {
return id;
}
public String getSrsName() {
return srsName;
}
}
package de.hft.stuttgart.sektorsim.server.wfs;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
@XmlType(name = "Polygon", namespace = "http://www.opengis.net/gml/3.2")
public class PolygonValue {
@XmlElement(name = "exterior", namespace = "http://www.opengis.net/gml/3.2")
private ExteriorXml exterior;
@XmlAttribute(name = "srsName")
private String srsName;
public ExteriorXml getExterior() {
return exterior;
}
public void setExterior(ExteriorXml exterior) {
this.exterior = exterior;
}
public String getSrsName() {
return srsName;
}
public void setSrsName(String srsName) {
this.srsName = srsName;
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment