1 package org.simantics.district.maps.server;
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.nio.charset.StandardCharsets;
7 import java.nio.file.DirectoryStream;
8 import java.nio.file.Files;
9 import java.nio.file.Path;
10 import java.nio.file.StandardOpenOption;
11 import java.util.ArrayList;
12 import java.util.HashMap;
13 import java.util.List;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicBoolean;
17 import java.util.stream.Stream;
19 import org.simantics.district.maps.server.prefs.MapsServerPreferences;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import org.yaml.snakeyaml.Yaml;
23 import org.zeroturnaround.exec.ProcessExecutor;
24 import org.zeroturnaround.exec.StartedProcess;
25 import org.zeroturnaround.exec.stream.slf4j.Slf4jDebugOutputStream;
26 import org.zeroturnaround.process.PidProcess;
27 import org.zeroturnaround.process.PidUtil;
28 import org.zeroturnaround.process.Processes;
29 import org.zeroturnaround.process.SystemProcess;
31 import com.fasterxml.jackson.core.JsonParseException;
32 import com.fasterxml.jackson.databind.JsonMappingException;
33 import com.fasterxml.jackson.databind.ObjectMapper;
35 public class TileserverMapnik {
37 private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
38 private static final String[] ADDITIONAL_DEPENDENCIES = new String[] { "tilelive-vector@3.9.4", "tilelive-tmstyle@0.6.0" };
40 private SystemProcess process;
41 private Path serverRoot;
43 private AtomicBoolean running = new AtomicBoolean(false);
45 TileserverMapnik(Path serverRoot) {
46 this.serverRoot = serverRoot.normalize();
49 public boolean isRunning() throws IOException, InterruptedException {
50 return process == null ? false : process.isAlive() && isReady();
53 public boolean isReady() {
57 public void start() throws Exception {
58 // check if existing server is left hanging
59 if (Files.exists(getPid())) {
60 String pid = new String(Files.readAllBytes(getPid()));
61 PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid));
62 pr.destroyForcefully();
65 // check that npm dependencies are satisfied
66 if (checkAndInstall(null)) {
67 checkAndInstall(ADDITIONAL_DEPENDENCIES[0]);
68 checkAndInstall(ADDITIONAL_DEPENDENCIES[1]);
74 if (process != null && process.isAlive())
77 StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("tileserver-mapnik").toFile()).destroyOnExit().environment(getEnv())
78 .command(NodeJS.executable().toString(), getTessera().toString(), "-c", getConfigJson().toString(), "-p", Integer.toString(MapsServerPreferences.defaultPort()))
79 .redirectOutput(new Slf4jDebugOutputStream(LOGGER) {
82 protected void processLine(String line) {
83 // Convert to UTF-8 string
84 String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8);
89 Process nativeProcess = startedProcess.getProcess();
90 process = Processes.newStandardProcess(nativeProcess);
91 int pid = PidUtil.getPid(nativeProcess);
92 Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
96 private Map<String, String> getEnv() {
97 Map<String, String> env = new HashMap<>();
98 env.put("MAPNIK_FONT_PATH", getFonts().toString());
99 env.put("ICU_DATA", getICU().toString());
103 public void stop() throws IOException, InterruptedException {
104 if (process != null && process.isAlive()) {
105 // gracefully not supported on windows
106 process.destroyForcefully();
107 if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) {
108 process.destroyForcefully();
109 if (process.isAlive())
110 LOGGER.error("Could not shutdown TileserverMapnik!");
113 Files.delete(getPid());
118 private Path getPid() {
119 return serverRoot.resolve("pid");
122 public void restart() throws Exception {
127 private boolean checkIfInstalled(String module) throws Exception {
128 String tileserverMapnik = tileserverMapnikRoot().toString();
130 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
131 if (module == null) {
132 retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
134 retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
136 String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
141 private boolean install(String module) throws Exception {
142 String tileserverMapnik = tileserverMapnikRoot().toString();
145 retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
147 retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
149 LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
153 private boolean checkAndInstall(String module) throws Exception {
154 boolean installed = checkIfInstalled(module);
161 private Path tileserverMapnikRoot() {
162 return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
165 private Path getFonts() {
166 return serverRoot.resolve("fonts").toAbsolutePath();
169 private Path getICU() {
170 return serverRoot.resolve("tileserver-mapnik/node_modules/tilelive-vector/node_modules/mapnik/lib/binding/node-v46-win32-x64/share/icu").toAbsolutePath();
173 private Path getTessera() {
174 return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
177 private Path getConfigJson() {
178 return serverRoot.resolve("config.json").toAbsolutePath();
181 public List<String> availableMBTiles() throws IOException {
182 Path data = getDataDirectory();
183 List<String> result = new ArrayList<>();
184 try (Stream<Path> paths = Files.walk(data)) {
186 if (!p.equals(data)) {
187 String tiles = p.getFileName().toString();
195 private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException {
196 Path configJson = getConfigJson();
197 Map<String, Object> config = new HashMap<>();
198 Path tm2 = getStyleDirectory();
199 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
200 stream.forEach(p -> {
201 Path projectYaml = p.resolve("project.yml");
202 if (Files.exists(projectYaml)) {
205 Map<String, String> source = new HashMap<>();
207 Path tm2style = serverRoot.relativize(projectYaml.getParent());
209 String prefix = "tmstyle://../";
210 String tmStyle = prefix + tm2style.toString();
211 source.put("source", tmStyle);
212 config.put("/" + projectYaml.getParent().getFileName().toString(), source);
216 ObjectMapper mapper = new ObjectMapper();
217 mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
220 public Path getStyleDirectory() {
221 return serverRoot.resolve("tm2");
224 public Path getDataDirectory() {
225 return serverRoot.resolve("data");
228 public Path getCurrentTiles() {
229 return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles());
232 public void checkTm2Styles() {
233 Path tm2 = getStyleDirectory();
234 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
235 stream.forEach(p -> {
236 Path projectYaml = p.resolve("project.yml");
237 Yaml yaml = new Yaml();
238 Map<String, String> data = null;
239 try (InputStream input = Files.newInputStream(projectYaml, StandardOpenOption.READ)) {
240 data = yaml.loadAs(input, Map.class);
242 Path tiles = serverRoot.relativize(getCurrentTiles());
244 String tmStyle = "mbtiles://../" + tiles.toString();
245 data.put("source", tmStyle);
247 } catch (IOException e) {
248 LOGGER.error("Could not read yaml", e);
251 Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
252 } catch (IOException e) {
253 LOGGER.error("Could not write yaml", e);
256 } catch (IOException e) {
257 LOGGER.error("Could not browse tm2 styles", e);
261 public List<String> listStyles() throws IOException {
262 List<String> results = new ArrayList<>();
263 Path tm2 = getStyleDirectory();
264 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
265 stream.forEach(p -> {
266 results.add(p.getFileName().toString());