]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.maps.server/src/org/simantics/district/maps/server/TileserverMapnik.java
b047626a53253c82561992c71f2a39855460f889
[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.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;
30
31 import com.fasterxml.jackson.core.JsonParseException;
32 import com.fasterxml.jackson.databind.JsonMappingException;
33 import com.fasterxml.jackson.databind.ObjectMapper;
34
35 public class TileserverMapnik {
36
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" };
39     
40     private SystemProcess process;
41     private Path serverRoot;
42     
43     private AtomicBoolean running = new AtomicBoolean(false);
44     
45     TileserverMapnik(Path serverRoot) {
46         this.serverRoot = serverRoot.normalize();
47     }
48
49     public boolean isRunning() throws IOException, InterruptedException {
50         return process == null ? false : process.isAlive() && isReady();
51     }
52     
53     public boolean isReady() {
54         return running.get();
55     }
56     
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();
63         }
64         
65         // check that npm dependencies are satisfied
66         if (checkAndInstall(null)) {
67             checkAndInstall(ADDITIONAL_DEPENDENCIES[0]);
68             checkAndInstall(ADDITIONAL_DEPENDENCIES[1]);
69         }
70         
71         checkConfigJson();
72         checkTm2Styles();
73         
74         if (process != null && process.isAlive())
75             return;
76         
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) {
80                     
81                     @Override
82                     protected void processLine(String line) {
83                         // Convert to UTF-8 string
84                         String utf8Line = new String(line.getBytes(), StandardCharsets.UTF_8);
85                         log.debug(utf8Line);
86                     }
87                 }).start();
88         
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);
93         running.set(true);
94     }
95
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());
100         return env;
101     }
102
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!");
111             }
112             process = null;
113             Files.delete(getPid());
114             running.set(false);
115         }
116     }
117
118     private Path getPid() {
119         return serverRoot.resolve("pid");
120     }
121
122     public void restart() throws Exception {
123         stop();
124         start();
125     }
126     
127     private boolean checkIfInstalled(String module) throws Exception {
128         String tileserverMapnik = tileserverMapnikRoot().toString();
129         int retVal;
130         try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
131             if (module == null) {
132                 retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list");
133             } else {
134                 retVal = NodeJS.npm(output, "--prefix", tileserverMapnik, "list", module);
135             }
136             String outputString = new String(output.toByteArray(), StandardCharsets.UTF_8);
137         }
138         return retVal == 0;
139     }
140     
141     private boolean install(String module) throws Exception {
142         String tileserverMapnik = tileserverMapnikRoot().toString();
143         int retVal;
144         if (module == null)
145             retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", "--save");
146         else 
147             retVal = NodeJS.npm(null, "--prefix", tileserverMapnik, "install", module, "--save");
148         if (retVal != 0)
149             LOGGER.warn("Could not install module " + module == null ? "package.json" : module + "! " + retVal);
150         return retVal == 0;
151     }
152     
153     private boolean checkAndInstall(String module) throws Exception {
154         boolean installed = checkIfInstalled(module); 
155         if (!installed)
156             install(module);
157         return !installed;
158     }
159
160     
161     private Path tileserverMapnikRoot() {
162         return serverRoot.resolve("tileserver-mapnik").toAbsolutePath();
163     }
164     
165     private Path getFonts() {
166         return serverRoot.resolve("fonts").toAbsolutePath();
167     }
168     
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();
171     }
172     
173     private Path getTessera() {
174         return serverRoot.resolve("tileserver-mapnik/bin/tessera.js").toAbsolutePath();
175     }
176     
177     private Path getConfigJson() {
178         return serverRoot.resolve("config.json").toAbsolutePath();
179     }
180     
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)) {
185             paths.forEach(p -> {
186                 if (!p.equals(data)) {
187                     String tiles = p.getFileName().toString();
188                     result.add(tiles);
189                 }
190             });
191         }
192         return result;
193     }
194     
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)) {
203                     // Good
204                     
205                     Map<String, String> source = new HashMap<>();
206                     
207                     Path tm2style = serverRoot.relativize(projectYaml.getParent());
208                     
209                     String prefix = "tmstyle://../";
210                     String tmStyle = prefix + tm2style.toString(); 
211                     source.put("source", tmStyle);
212                     config.put("/" + projectYaml.getParent().getFileName().toString(), source);
213                 }
214             });
215         }
216         ObjectMapper mapper = new ObjectMapper();
217         mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newOutputStream(configJson, StandardOpenOption.TRUNCATE_EXISTING), config);
218     }
219     
220     public Path getStyleDirectory() {
221         return serverRoot.resolve("tm2");
222     }
223     
224     public Path getDataDirectory() {
225         return serverRoot.resolve("data");
226     }
227     
228     public Path getCurrentTiles() {
229         return getDataDirectory().resolve(MapsServerPreferences.currentMBTiles());
230     }
231     
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);
241                     
242                     Path tiles = serverRoot.relativize(getCurrentTiles());
243                     
244                     String tmStyle = "mbtiles://../" + tiles.toString();
245                     data.put("source", tmStyle);
246                     
247                 } catch (IOException e) {
248                     LOGGER.error("Could not read yaml", e);
249                 }
250                 try {
251                     Files.write(projectYaml, yaml.dump(data).getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
252                 } catch (IOException e) {
253                     LOGGER.error("Could not write yaml", e);
254                 }
255             });
256         } catch (IOException e) {
257             LOGGER.error("Could not browse tm2 styles", e);
258         }
259     }
260
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());
267             });
268         }
269         return results;
270     }
271
272 }