--- /dev/null
+package org.simantics.district.maps.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+import org.zeroturnaround.exec.ProcessExecutor;
+import org.zeroturnaround.exec.StartedProcess;
+import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
+import org.zeroturnaround.process.PidProcess;
+import org.zeroturnaround.process.PidUtil;
+import org.zeroturnaround.process.Processes;
+import org.zeroturnaround.process.SystemProcess;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TileserverMapnik {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
+ private static final String[] ADDITIONAL_DEPENDENCIES = new String[] { "tilelive-vector@3.9.4", "tilelive-tmstyle@0.6.0" };
+
+ private SystemProcess process;
+ private Path serverRoot;
+ private int port;
+
+ private AtomicBoolean running = new AtomicBoolean(false);
+
+ TileserverMapnik(Path serverRoot, int port) {
+ this.serverRoot = serverRoot.normalize();
+ this.port = port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public boolean isRunning() throws IOException, InterruptedException {
+ return process == null ? false : process.isAlive() && isReady();
+ }
+
+ public boolean isReady() {
+ return running.get();
+ }
+
+ public void start() throws Exception {
+ // check if existing server is left hanging
+ if (Files.exists(getPid())) {
+ String pid = new String(Files.readAllBytes(getPid()));
+ PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid));
+ pr.destroyForcefully();
+ }
+
+ // check that npm dependencies are satisfied
+ if (checkAndInstall(null)) {
+ checkAndInstall(ADDITIONAL_DEPENDENCIES[0]);
+ checkAndInstall(ADDITIONAL_DEPENDENCIES[1]);
+ }
+
+ checkConfigJson();
+ checkTm2Styles();
+
+ if (process != null && process.isAlive())
+ return;
+
+ StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("tileserver-mapnik").toFile()).destroyOnExit().environment(getEnv())
+ .command(NodeJS.executable().toString(), getTessera().toString(), "-c", getConfigJson().toString())
+ .redirectOutput(Slf4jStream.ofCaller().asDebug()).start();
+
+ Process nativeProcess = startedProcess.getProcess();
+ process = Processes.newStandardProcess(nativeProcess);
+ int pid = PidUtil.getPid(nativeProcess);
+ Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ running.set(true);
+ }
+
+ private Map<String, String> getEnv() {
+ Map<String, String> env = new HashMap<>();
+ env.put("MAPNIK_FONT_PATH", getFonts().toString());
+ env.put("ICU_DATA", getICU().toString());
+ return env;
+ }
+
+ public void stop() throws IOException, InterruptedException {
+ if (process != null && process.isAlive()) {
+ // gracefully not supported on windows
+ process.destroyForcefully();
+ if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) {
+ process.destroyForcefully();
+ if (process.isAlive())
+ LOGGER.error("Could not shutdown TileserverMapnik!");
+ }
+ process = null;
+ Files.delete(getPid());
+ running.set(false);
+ }
+ }
+
+ private Path getPid() {
+ return serverRoot.resolve("pid");
+ }
+
+ public void restart() throws Exception {
+ stop();
+ start();
+ }
+
+ private boolean checkIfInstalled(String module) throws Exception {
+ String tileserverMapnik = tileserverMapnikRoot().toString();
+ int retVal;
+ try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+ if (module == null) {
+ retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
+ } else {
+ retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
+ }
+ String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
+ }
+ return retVal == 0;
+ }
+
+ private boolean install(String module) throws Exception {
+ String tileserverMapnik = tileserverMapnikRoot().toString();
+ int retVal;
+ if (module == null)
+ retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
+ else
+ retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
+ if (retVal != 0)
+ LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
+ return retVal == 0;
+ }
+
+ private boolean checkAndInstall(String module) throws Exception {
+ boolean installed = checkIfInstalled(module);
+ if (!installed)
+ install(module);
+ return !installed;
+ }
+
+
+ private Path tileserverMapnikRoot() {
+ return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
+ }
+
+ private Path getFonts() {
+ return serverRoot.resolve("fonts").toAbsolutePath();
+ }
+
+ private Path getICU() {
+ return serverRoot.resolve("tileserver-mapnik/node_modules/tilelive-vector/node_modules/mapnik/lib/binding/node-v46-win32-x64/share/icu").toAbsolutePath();
+ }
+
+ private Path getTessera() {
+ return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
+ }
+
+ private Path getConfigJson() {
+ return serverRoot.resolve("config.json").toAbsolutePath();
+ }
+
+ public List<String> availableMBTiles() throws IOException {
+ Path data = serverRoot.resolve("data").toAbsolutePath();
+ List<String> result = new ArrayList<>();
+ try (Stream<Path> paths = Files.walk(data)) {
+ paths.forEach(p -> {
+ if (!p.equals(data)) {
+ String tiles = p.getFileName().toString();
+ result.add(tiles);
+ }
+ });
+ }
+ return result;
+ }
+
+ private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException {
+ Path configJson = getConfigJson();
+ Map<String, Object> config = new HashMap<>();
+ Path tm2 = serverRoot.resolve("tm2").toAbsolutePath();
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
+ stream.forEach(p -> {
+ Path projectYaml = p.resolve("project.yml");
+ if (Files.exists(projectYaml)) {
+ // Good
+
+ Map<String, String> source = new HashMap<>();
+
+ Path tm2style = serverRoot.relativize(projectYaml.getParent());
+
+ String prefix = "tmstyle://../";
+ String tmStyle = prefix + tm2style.toString();
+ source.put("source", tmStyle);
+ config.put("/" + projectYaml.getParent().getFileName().toString(), source);
+ }
+ });
+ }
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
+ }
+
+ public void checkTm2Styles() {
+ Path tm2 = serverRoot.resolve("tm2").toAbsolutePath();
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
+ stream.forEach(p -> {
+ Path projectYaml = p.resolve("project.yml");
+ Yaml yaml = new Yaml();
+ Map<String, String> data = null;
+ try (InputStream input = Files.newInputStream(projectYaml, StandardOpenOption.READ)) {
+ data = yaml.loadAs(input, Map.class);
+
+ Path tiles = serverRoot.relativize(serverRoot.resolve("data").resolve("helsinki_finland.mbtiles"));//.toAbsolutePath().toString().replace("\\", "/");
+
+
+ String tmStyle = "mbtiles://../" + tiles.toString();
+ data.put("source", tmStyle);
+
+ } catch (IOException e) {
+ LOGGER.error("Could not read yaml", e);
+ }
+ try {
+ Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
+ } catch (IOException e) {
+ LOGGER.error("Could not write yaml", e);
+ }
+ });
+ } catch (IOException e) {
+ LOGGER.error("Could not browse tm2 styles", e);
+ }
+ }
+
+ public List<String> listStyles() throws IOException {
+ List<String> results = new ArrayList<>();
+ Path tm2 = serverRoot.resolve("tm2").toAbsolutePath();
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
+ stream.forEach(p -> {
+ results.add(p.getFileName().toString());
+ });
+ }
+ return results;
+ }
+
+}