X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=org.simantics.maps.server%2Fsrc%2Forg%2Fsimantics%2Fdistrict%2Fmaps%2Fserver%2FTileserverMapnik.java;fp=org.simantics.maps.server%2Fsrc%2Forg%2Fsimantics%2Fdistrict%2Fmaps%2Fserver%2FTileserverMapnik.java;h=8f3135dc567c2ddf18124bfaa7a70c14c77dbab1;hb=2529be6d456deeb07c128603ce4971f1dc29b695;hp=0000000000000000000000000000000000000000;hpb=2636fc31c16c23711cf2b06a4ae8537bba9c1d35;p=simantics%2Fdistrict.git 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 index 00000000..8f3135dc --- /dev/null +++ b/org.simantics.maps.server/src/org/simantics/district/maps/server/TileserverMapnik.java @@ -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 getEnv() { + Map 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 availableMBTiles() throws IOException { + Path data = serverRoot.resolve("data").toAbsolutePath(); + List result = new ArrayList<>(); + try (Stream 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 config = new HashMap<>(); + Path tm2 = serverRoot.resolve("tm2").toAbsolutePath(); + try (DirectoryStream stream = Files.newDirectoryStream(tm2)) { + stream.forEach(p -> { + Path projectYaml = p.resolve("project.yml"); + if (Files.exists(projectYaml)) { + // Good + + Map 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 stream = Files.newDirectoryStream(tm2)) { + stream.forEach(p -> { + Path projectYaml = p.resolve("project.yml"); + Yaml yaml = new Yaml(); + Map 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 listStyles() throws IOException { + List results = new ArrayList<>(); + Path tm2 = serverRoot.resolve("tm2").toAbsolutePath(); + try (DirectoryStream stream = Files.newDirectoryStream(tm2)) { + stream.forEach(p -> { + results.add(p.getFileName().toString()); + }); + } + return results; + } + +}