]> gerrit.simantics Code Review - simantics/district.git/blobdiff - org.simantics.maps.server/src/org/simantics/district/maps/server/TileserverMapnik.java
Adding integrated tile server
[simantics/district.git] / org.simantics.maps.server / src / org / simantics / district / maps / server / TileserverMapnik.java
diff --git a/org.simantics.maps.server/src/org/simantics/district/maps/server/TileserverMapnik.java b/org.simantics.maps.server/src/org/simantics/district/maps/server/TileserverMapnik.java
new file mode 100644 (file)
index 0000000..8f3135d
--- /dev/null
@@ -0,0 +1,258 @@
+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;
+    }
+
+}