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.Paths;
11 import java.nio.file.StandardOpenOption;
12 import java.util.ArrayList;
13 import java.util.HashMap;
14 import java.util.List;
16 import java.util.Optional;
17 import java.util.concurrent.TimeUnit;
18 import java.util.concurrent.atomic.AtomicBoolean;
19 import java.util.stream.Stream;
21 import org.simantics.district.maps.server.prefs.MapsServerPreferences;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24 import org.yaml.snakeyaml.Yaml;
25 import org.zeroturnaround.exec.ProcessExecutor;
26 import org.zeroturnaround.exec.StartedProcess;
27 import org.zeroturnaround.exec.stream.slf4j.Slf4jDebugOutputStream;
28 import org.zeroturnaround.process.PidProcess;
29 import org.zeroturnaround.process.PidUtil;
30 import org.zeroturnaround.process.Processes;
31 import org.zeroturnaround.process.SystemProcess;
33 import com.fasterxml.jackson.core.JsonParseException;
34 import com.fasterxml.jackson.databind.JsonMappingException;
35 import com.fasterxml.jackson.databind.ObjectMapper;
37 public class TileserverMapnik {
39 private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
40 private static final String[] ADDITIONAL_DEPENDENCIES = new String[] { "tilelive-vector@3.9.4", "tilelive-tmstyle@0.6.0" };
42 private SystemProcess process;
43 private Path serverRoot;
45 private AtomicBoolean running = new AtomicBoolean(false);
47 TileserverMapnik(Path serverRoot) {
48 this.serverRoot = serverRoot.normalize();
51 public boolean isRunning() throws IOException, InterruptedException {
52 return process == null ? false : process.isAlive() && isReady();
55 public boolean isReady() {
59 public void start(Optional<TileserverStartListener> listener) throws Exception {
60 // check if existing server is left hanging
61 if (Files.exists(getPid())) {
62 String pid = new String(Files.readAllBytes(getPid()));
63 PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid));
64 pr.destroyForcefully();
67 // check that npm dependencies are satisfied
68 // if (checkAndInstall(null, listener)) {
69 // checkAndInstall(ADDITIONAL_DEPENDENCIES[0], listener);
70 // checkAndInstall(ADDITIONAL_DEPENDENCIES[1], listener);
76 if (process != null && process.isAlive())
79 Path tileliveTesseraWin = serverRoot.resolve("dist").resolve("node.exe").normalize().toAbsolutePath();
80 StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("dist").toFile()).destroyOnExit().environment(getEnv())
81 .command(tileliveTesseraWin.toString(), "-c", getConfigJson().toString(), "-p", Integer.toString(MapsServerPreferences.defaultPort()))
82 .redirectOutput(new Slf4jDebugOutputStream(LOGGER) {
85 protected void processLine(String line) {
86 // Convert to UTF-8 string
87 String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8);
92 Process nativeProcess = startedProcess.getProcess();
93 process = Processes.newStandardProcess(nativeProcess);
94 int pid = PidUtil.getPid(nativeProcess);
95 LOGGER.info("Writing pid-file to {} with pid={}", getPid(), pid);
96 Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
98 listener.ifPresent(TileserverStartListener::started);
101 private Map<String, String> getEnv() {
102 Map<String, String> env = new HashMap<>();
103 env.put("MAPNIK_FONT_PATH", getFonts().toString());
104 env.put("ICU_DATA", getICU().toString());
108 public void stop() throws IOException, InterruptedException {
109 if (process != null && process.isAlive()) {
110 // gracefully not supported on windows
111 process.destroyForcefully();
112 if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) {
113 process.destroyForcefully();
114 if (process.isAlive())
115 LOGGER.error("Could not shutdown TileserverMapnik!");
118 Files.delete(getPid());
123 private Path getPid() {
124 return serverRoot.resolve("pid");
127 public void restart(Optional<TileserverStartListener> listener) throws Exception {
132 // private boolean checkIfInstalled(String module) throws Exception {
133 // String tileserverMapnik = tileserverMapnikRoot().toString();
135 // try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
136 // if (module == null) {
137 // retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
139 // retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
141 // String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
143 // return retVal == 0;
146 // private int install(String module, Optional<TileserverStartListener> listener) throws Exception {
147 // String tileserverMapnik = tileserverMapnikRoot().toString();
149 // if (module == null) {
150 // listener.ifPresent(l -> l.installing("Installing tileserver-mapnik"));
151 // retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
153 // retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
156 // LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
160 // private boolean checkAndInstall(String module, Optional<TileserverStartListener> listener) throws Exception {
161 // LOGGER.info("Installing module {}", String.valueOf(module));
162 // boolean installed = checkIfInstalled(module);
164 // int installSuccessfull = install(module, listener);
165 // if (installSuccessfull != 0) {
166 // LOGGER.warn("Installation of module {} failed!", String.valueOf(module));
167 // listener.ifPresent(l -> l.installationFailed(module, installSuccessfull));
170 // LOGGER.info("Module {} was already installed", String.valueOf(module));
172 // return !installed;
176 private Path tileserverMapnikRoot() {
177 return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
180 private Path getFonts() {
181 return serverRoot.resolve("fonts").toAbsolutePath();
184 private Path getICU() {
185 return serverRoot.resolve("dist/share/icu").toAbsolutePath();
188 private Path getTessera() {
189 return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
192 private Path getConfigJson() {
193 return serverRoot.resolve("config.json").toAbsolutePath();
196 public List<String> availableMBTiles() throws IOException {
197 Path data = getDataDirectory();
198 List<String> result = new ArrayList<>();
199 try (Stream<Path> paths = Files.walk(data)) {
201 if (!p.equals(data)) {
202 String tiles = p.getFileName().toString();
210 private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException {
211 Path configJson = getConfigJson();
212 Map<String, Object> config = new HashMap<>();
213 Path tm2 = getStyleDirectory();
214 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
215 stream.forEach(p -> {
216 Path projectYaml = p.resolve("project.yml");
217 if (Files.exists(projectYaml)) {
220 Map<String, String> source = new HashMap<>();
222 Path tm2style = serverRoot.relativize(projectYaml.getParent());
224 String prefix = "tmstyle://../";
225 String tmStyle = prefix + tm2style.toString();
226 source.put("source", tmStyle);
227 config.put("/" + projectYaml.getParent().getFileName().toString(), source);
231 ObjectMapper mapper = new ObjectMapper();
232 mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
235 public Path getStyleDirectory() {
236 return serverRoot.resolve("tm2");
239 public Path getDataDirectory() {
240 return serverRoot.resolve("data");
243 public Path getCurrentTiles() {
244 return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles());
247 public void checkTm2Styles() {
248 Path tm2 = getStyleDirectory();
249 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
250 stream.forEach(p -> {
251 Path projectYaml = p.resolve("project.yml");
252 Yaml yaml = new Yaml();
253 Map<String, String> data = null;
254 try (InputStream input = Files.newInputStream(projectYaml, StandardOpenOption.READ)) {
255 data = yaml.loadAs(input, Map.class);
257 Path tiles = serverRoot.relativize(getCurrentTiles());
259 String tmStyle = "mbtiles://../" + tiles.toString();
260 data.put("source", tmStyle);
262 } catch (IOException e) {
263 LOGGER.error("Could not read yaml", e);
266 Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
267 } catch (IOException e) {
268 LOGGER.error("Could not write yaml", e);
271 } catch (IOException e) {
272 LOGGER.error("Could not browse tm2 styles", e);
276 public List<String> listStyles() throws IOException {
277 List<String> results = new ArrayList<>();
278 Path tm2 = getStyleDirectory();
279 try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
280 stream.forEach(p -> {
281 results.add(p.getFileName().toString());
287 public static interface TileserverStartListener {
289 void installing(String module);
291 void installationFailed(String module, int returnValue);