]> gerrit.simantics Code Review - simantics/district.git/blob - 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
1 package org.simantics.district.maps.server;
2
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;
14 import java.util.Map;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicBoolean;
17 import java.util.stream.Stream;
18
19 import org.slf4j.Logger;
20 import org.slf4j.LoggerFactory;
21 import org.yaml.snakeyaml.Yaml;
22 import org.zeroturnaround.exec.ProcessExecutor;
23 import org.zeroturnaround.exec.StartedProcess;
24 import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
25 import org.zeroturnaround.process.PidProcess;
26 import org.zeroturnaround.process.PidUtil;
27 import org.zeroturnaround.process.Processes;
28 import org.zeroturnaround.process.SystemProcess;
29
30 import com.fasterxml.jackson.core.JsonParseException;
31 import com.fasterxml.jackson.databind.JsonMappingException;
32 import com.fasterxml.jackson.databind.ObjectMapper;
33
34 public class TileserverMapnik {
35
36     private static final Logger LOGGER = LoggerFactory.getLogger(TileserverMapnik.class);
37     private static final String[] ADDITIONAL_DEPENDENCIES = new String[] { "tilelive-vector@3.9.4", "tilelive-tmstyle@0.6.0" };
38     
39     private SystemProcess process;
40     private Path serverRoot;
41     private int port;
42     
43     private AtomicBoolean running = new AtomicBoolean(false);
44     
45     TileserverMapnik(Path serverRoot, int port) {
46         this.serverRoot = serverRoot.normalize();
47         this.port = port;
48     }
49     
50     public void setPort(int port) {
51         this.port = port;
52     }
53
54     public boolean isRunning() throws IOException, InterruptedException {
55         return process == null ? false : process.isAlive() && isReady();
56     }
57     
58     public boolean isReady() {
59         return running.get();
60     }
61     
62     public void start() throws Exception {
63         // check if existing server is left hanging
64         if (Files.exists(getPid())) {
65             String pid = new String(Files.readAllBytes(getPid()));
66             PidProcess pr = Processes.newPidProcess(Integer.parseInt(pid));
67             pr.destroyForcefully();
68         }
69         
70         // check that npm dependencies are satisfied
71         if (checkAndInstall(null)) {
72             checkAndInstall(ADDITIONAL_DEPENDENCIES[0]);
73             checkAndInstall(ADDITIONAL_DEPENDENCIES[1]);
74         }
75         
76         checkConfigJson();
77         checkTm2Styles();
78         
79         if (process != null && process.isAlive())
80             return;
81         
82         StartedProcess startedProcess = new ProcessExecutor().directory(serverRoot.resolve("tileserver-mapnik").toFile()).destroyOnExit().environment(getEnv())
83                 .command(NodeJS.executable().toString(), getTessera().toString(), "-c", getConfigJson().toString())
84                 .redirectOutput(Slf4jStream.ofCaller().asDebug()).start();
85         
86         Process nativeProcess = startedProcess.getProcess();
87         process = Processes.newStandardProcess(nativeProcess);
88         int pid = PidUtil.getPid(nativeProcess);
89         Files.write(getPid(), (pid + "").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
90         running.set(true);
91     }
92
93     private Map<String, String> getEnv() {
94         Map<String, String> env = new HashMap<>();
95         env.put("MAPNIK_FONT_PATH", getFonts().toString());
96         env.put("ICU_DATA", getICU().toString());
97         return env;
98     }
99
100     public void stop() throws IOException, InterruptedException {
101         if (process != null && process.isAlive()) {
102             // gracefully not supported on windows
103             process.destroyForcefully();
104             if (!process.waitFor(2000, TimeUnit.MILLISECONDS)) {
105                 process.destroyForcefully();
106                 if (process.isAlive())
107                     LOGGER.error("Could not shutdown TileserverMapnik!");
108             }
109             process = null;
110             Files.delete(getPid());
111             running.set(false);
112         }
113     }
114
115     private Path getPid() {
116         return serverRoot.resolve("pid");
117     }
118
119     public void restart() throws Exception {
120         stop();
121         start();
122     }
123     
124     private boolean checkIfInstalled(String module) throws Exception {
125         String tileserverMapnik = tileserverMapnikRoot().toString();
126         int retVal;
127         try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
128             if (module == null) {
129                 retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
130             } else {
131                 retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
132             }
133             String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
134         }
135         return retVal == 0;
136     }
137     
138     private boolean install(String module) throws Exception {
139         String tileserverMapnik = tileserverMapnikRoot().toString();
140         int retVal;
141         if (module == null)
142             retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
143         else 
144             retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
145         if (retVal != 0)
146             LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
147         return retVal == 0;
148     }
149     
150     private boolean checkAndInstall(String module) throws Exception {
151         boolean installed = checkIfInstalled(module); 
152         if (!installed)
153             install(module);
154         return !installed;
155     }
156
157     
158     private Path tileserverMapnikRoot() {
159         return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
160     }
161     
162     private Path getFonts() {
163         return serverRoot.resolve("fonts").toAbsolutePath();
164     }
165     
166     private Path getICU() {
167         return serverRoot.resolve("tileserver-mapnik/node_modules/tilelive-vector/node_modules/mapnik/lib/binding/node-v46-win32-x64/share/icu").toAbsolutePath();
168     }
169     
170     private Path getTessera() {
171         return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
172     }
173     
174     private Path getConfigJson() {
175         return serverRoot.resolve("config.json").toAbsolutePath();
176     }
177     
178     public List<String> availableMBTiles() throws IOException {
179         Path data = serverRoot.resolve("data").toAbsolutePath();
180         List<String> result = new ArrayList<>();
181         try (Stream<Path> paths = Files.walk(data)) {
182             paths.forEach(p -> {
183                 if (!p.equals(data)) {
184                     String tiles = p.getFileName().toString();
185                     result.add(tiles);
186                 }
187             });
188         }
189         return result;
190     }
191     
192     private void checkConfigJson() throws JsonParseException, JsonMappingException, IOException {
193         Path configJson = getConfigJson();
194         Map<String, Object> config = new HashMap<>();
195         Path tm2 = serverRoot.resolve("tm2").toAbsolutePath();
196         try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
197             stream.forEach(p -> {
198                 Path projectYaml = p.resolve("project.yml");
199                 if (Files.exists(projectYaml)) {
200                     // Good
201                     
202                     Map<String, String> source = new HashMap<>();
203                     
204                     Path tm2style = serverRoot.relativize(projectYaml.getParent());
205                     
206                     String prefix = "tmstyle://../";
207                     String tmStyle = prefix + tm2style.toString(); 
208                     source.put("source", tmStyle);
209                     config.put("/" + projectYaml.getParent().getFileName().toString(), source);
210                 }
211             });
212         }
213         ObjectMapper mapper = new ObjectMapper();
214         mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
215     }
216     
217     public void checkTm2Styles() {
218         Path tm2 = serverRoot.resolve("tm2").toAbsolutePath();
219         try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
220             stream.forEach(p -> {
221                 Path projectYaml = p.resolve("project.yml");
222                 Yaml yaml = new Yaml();
223                 Map<String, String> data = null;
224                 try (InputStream input = Files.newInputStream(projectYaml, StandardOpenOption.READ)) {
225                     data = yaml.loadAs(input, Map.class);
226                     
227                     Path tiles = serverRoot.relativize(serverRoot.resolve("data").resolve("helsinki_finland.mbtiles"));//.toAbsolutePath().toString().replace("\\", "/");
228                     
229                     
230                     String tmStyle = "mbtiles://../" + tiles.toString();
231                     data.put("source", tmStyle);
232                     
233                 } catch (IOException e) {
234                     LOGGER.error("Could not read yaml", e);
235                 }
236                 try {
237                     Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
238                 } catch (IOException e) {
239                     LOGGER.error("Could not write yaml", e);
240                 }
241             });
242         } catch (IOException e) {
243             LOGGER.error("Could not browse tm2 styles", e);
244         }
245     }
246
247     public List<String> listStyles() throws IOException {
248         List<String> results = new ArrayList<>();
249         Path tm2 = serverRoot.resolve("tm2").toAbsolutePath();
250         try (DirectoryStream<Path> stream = Files.newDirectoryStream(tm2)) {
251             stream.forEach(p -> {
252                 results.add(p.getFileName().toString());
253             });
254         }
255         return results;
256     }
257
258 }