You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@baremaps.apache.org by bc...@apache.org on 2022/12/17 13:54:25 UTC

[incubator-baremaps] 01/01: Add maputnik editor

This is an automated email from the ASF dual-hosted git repository.

bchapuis pushed a commit to branch 560-maputnik
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git

commit 0143097cc738858a953111103e9a2a989bdd0884
Author: Bertil Chapuis <bc...@gmail.com>
AuthorDate: Sat Dec 17 14:53:55 2022 +0100

    Add maputnik editor
---
 .run/map-dev.run.xml                               |   2 +-
 .run/map-maputnik.run.xml                          |  16 +++
 ...workflow init.run.xml => workflow-init.run.xml} |   4 +-
 .../main/java/org/apache/baremaps/cli/map/Dev.java |   1 -
 .../main/java/org/apache/baremaps/cli/map/Map.java |   3 +-
 .../baremaps/cli/map/{Dev.java => Maputnik.java}   |  27 +++--
 .../apache/baremaps/server/MaputnikResources.java  | 132 +++++++++++++++++++++
 .../resources/maputnik/app.45afceafeedad888c128.js |  63 ++++++++++
 .../src/main/resources/maputnik/favicon.ico        | Bin 0 -> 1150 bytes
 .../resources/maputnik/fonts/Roboto-Medium.ttf     | Bin 0 -> 511592 bytes
 .../resources/maputnik/fonts/Roboto-Regular.ttf    | Bin 0 -> 162876 bytes
 .../src/main/resources/maputnik/index.html         |  52 ++++++++
 .../src/main/resources/maputnik/manifest.json      |   9 ++
 .../openstreetmap/liechtenstein-latest.osm.pbf     | Bin 0 -> 2738903 bytes
 examples/openstreetmap/style.json                  |  13 +-
 15 files changed, 299 insertions(+), 23 deletions(-)

diff --git a/.run/map-dev.run.xml b/.run/map-dev.run.xml
index 2e54818a..1fc1d3d2 100644
--- a/.run/map-dev.run.xml
+++ b/.run/map-dev.run.xml
@@ -3,7 +3,7 @@
     <option name="MAIN_CLASS_NAME" value="org.apache.baremaps.cli.Baremaps" />
     <module name="baremaps-cli" />
     <option name="PROGRAM_PARAMETERS" value="map dev --database jdbc:postgresql://localhost:5432/baremaps?user=baremaps&amp;password=baremaps --tileset tileset.js --style style.js" />
-    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/map" />
+    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/basemap" />
     <extension name="coverage">
       <pattern>
         <option name="PATTERN" value="org.apache.baremaps.server.ogcapi.*" />
diff --git a/.run/map-maputnik.run.xml b/.run/map-maputnik.run.xml
new file mode 100644
index 00000000..79a26e58
--- /dev/null
+++ b/.run/map-maputnik.run.xml
@@ -0,0 +1,16 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="map-maputnik" type="Application" factoryName="Application">
+    <option name="MAIN_CLASS_NAME" value="org.apache.baremaps.cli.Baremaps" />
+    <module name="baremaps-cli" />
+    <option name="PROGRAM_PARAMETERS" value="map maputnik --database jdbc:postgresql://localhost:5432/baremaps?user=baremaps&amp;password=baremaps --tileset tileset.json --style style.json" />
+    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/examples/openstreetmap" />
+    <extension name="software.aws.toolkits.jetbrains.core.execution.JavaAwsConnectionExtension">
+      <option name="credential" />
+      <option name="region" />
+      <option name="useCurrentConnection" value="false" />
+    </extension>
+    <method v="2">
+      <option name="Make" enabled="true" />
+    </method>
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/.run/workflow init.run.xml b/.run/workflow-init.run.xml
similarity index 82%
rename from .run/workflow init.run.xml
rename to .run/workflow-init.run.xml
index a8642cdb..c9a68ff7 100644
--- a/.run/workflow init.run.xml	
+++ b/.run/workflow-init.run.xml
@@ -1,9 +1,9 @@
 <component name="ProjectRunConfigurationManager">
   <configuration default="false" name="workflow-init" type="Application" factoryName="Application">
-    <option name="MAIN_CLASS_NAME" value="org.apache.baremaps.Baremaps" />
+    <option name="MAIN_CLASS_NAME" value="org.apache.baremaps.cli.Baremaps" />
     <module name="baremaps-benchmark" />
     <option name="PROGRAM_PARAMETERS" value="workflow init --file sample-workflow.json" />
-    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/docs/examples/openstreetmap" />
+    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/examples/openstreetmap" />
     <extension name="coverage">
       <pattern>
         <option name="PATTERN" value="org.apache.baremaps.server.ogcapi.*" />
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java
index d26ca110..571c2e53 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java
@@ -74,7 +74,6 @@ public class Dev implements Callable<Integer> {
           .register(contextResolverFor(objectMapper)).register(new AbstractBinder() {
             @Override
             protected void configure() {
-              bind("viewer").to(String.class).named("assets");
               bind(tileset.toAbsolutePath()).to(Path.class).named("tileset");
               bind(style.toAbsolutePath()).to(Path.class).named("style");
               bind(dataSource).to(DataSource.class);
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Map.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Map.java
index 9ed602d0..cfdf9387 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Map.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Map.java
@@ -18,7 +18,8 @@ import picocli.CommandLine;
 import picocli.CommandLine.Command;
 
 @Command(name = "map", description = "Map commands.",
-    subcommands = {Init.class, Export.class, Serve.class, Dev.class}, sortOptions = false)
+    subcommands = {Init.class, Export.class, Serve.class, Dev.class, Maputnik.class},
+    sortOptions = false)
 public class Map implements Runnable {
 
   @Override
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Maputnik.java
similarity index 85%
copy from baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java
copy to baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Maputnik.java
index d26ca110..2f9256f9 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Dev.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Maputnik.java
@@ -21,25 +21,20 @@ import io.servicetalk.http.router.jersey.HttpJerseyRouterBuilder;
 import java.nio.file.Path;
 import java.util.concurrent.Callable;
 import javax.sql.DataSource;
-import org.apache.baremaps.cli.Options;
 import org.apache.baremaps.database.PostgresUtils;
 import org.apache.baremaps.server.CorsFilter;
-import org.apache.baremaps.server.DevResources;
+import org.apache.baremaps.server.MaputnikResources;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Mixin;
 import picocli.CommandLine.Option;
 
-@Command(name = "dev", description = "Start a development server with live reload.")
-public class Dev implements Callable<Integer> {
+@Command(name = "maputnik", description = "Start a maputnik editor.")
+public class Maputnik implements Callable<Integer> {
 
-  private static final Logger logger = LoggerFactory.getLogger(Dev.class);
-
-  @Mixin
-  private Options options;
+  private static final Logger logger = LoggerFactory.getLogger(Maputnik.class);
 
   @Option(names = {"--database"}, paramLabel = "DATABASE",
       description = "The JDBC url of Postgres.", required = true)
@@ -64,17 +59,22 @@ public class Dev implements Callable<Integer> {
 
   @Override
   public Integer call() throws Exception {
-    try (var dataSource = PostgresUtils.dataSource(database)) {
+    // Maputnik only supports json style files
+    if (!style.endsWith(".json")) {
+      logger.error("{} is not a JSON file", style);
+      return 1;
+    }
 
+    // Create the data source
+    try (var dataSource = PostgresUtils.dataSource(database)) {
       // Configure serialization
       var objectMapper = defaultObjectMapper();
 
       // Configure the application
-      var application = new ResourceConfig().register(CorsFilter.class).register(DevResources.class)
+      var application = new ResourceConfig().register(CorsFilter.class).register(MaputnikResources.class)
           .register(contextResolverFor(objectMapper)).register(new AbstractBinder() {
             @Override
             protected void configure() {
-              bind("viewer").to(String.class).named("assets");
               bind(tileset.toAbsolutePath()).to(Path.class).named("tileset");
               bind(style.toAbsolutePath()).to(Path.class).named("style");
               bind(dataSource).to(DataSource.class);
@@ -82,12 +82,15 @@ public class Dev implements Callable<Integer> {
             }
           });
 
+      // Start the server
       var httpService = new HttpJerseyRouterBuilder().buildBlockingStreaming(application);
       var serverContext = HttpServers.forPort(port).listenBlockingStreamingAndAwait(httpService);
 
+      // Wait for the server to be closed
       logger.info("Listening on {}", serverContext.listenAddress());
       serverContext.awaitShutdown();
     }
+
     return 0;
   }
 }
diff --git a/baremaps-server/src/main/java/org/apache/baremaps/server/MaputnikResources.java b/baremaps-server/src/main/java/org/apache/baremaps/server/MaputnikResources.java
new file mode 100644
index 00000000..e6219aa9
--- /dev/null
+++ b/baremaps-server/src/main/java/org/apache/baremaps/server/MaputnikResources.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.server;
+
+import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
+import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.sql.DataSource;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.sse.Sse;
+import org.apache.baremaps.database.tile.PostgresTileStore;
+import org.apache.baremaps.database.tile.Tile;
+import org.apache.baremaps.database.tile.TileStore;
+import org.apache.baremaps.style.Style;
+import org.apache.baremaps.tileset.Tileset;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+@javax.ws.rs.Path("/")
+public class MaputnikResources {
+
+  private static final Logger logger = LoggerFactory.getLogger(MaputnikResources.class);
+
+  private final Path style;
+
+  private final Path tileset;
+
+  private final DataSource dataSource;
+
+  private final ObjectMapper objectMapper;
+
+  public static final String TILE_ENCODING = "gzip";
+
+  public static final String TILE_TYPE = "application/vnd.mapbox-vector-tile";
+
+  @Inject
+  public MaputnikResources(@Named("tileset") Path tileset, @Named("style") Path style,
+      DataSource dataSource, ObjectMapper objectMapper, Sse sse) {
+    this.tileset = tileset.toAbsolutePath();
+    this.style = style.toAbsolutePath();
+    this.dataSource = dataSource;
+    this.objectMapper = objectMapper;
+  }
+
+  @PUT
+  @Consumes(MediaType.APPLICATION_JSON)
+  @javax.ws.rs.Path("style.json")
+  public void putStyle(JsonNode json) throws IOException {
+    byte[] value = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(json);
+    Files.write(style, value);
+  }
+
+  @PUT
+  @javax.ws.rs.Path("tiles.json")
+  public void putTiles(JsonNode json) throws IOException {
+    byte[] value = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(json);
+    Files.write(tileset, value);
+  }
+
+  @GET
+  @javax.ws.rs.Path("style.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Style getStyle() throws IOException {
+    return objectMapper.readValue(style.toFile(), Style.class);
+  }
+
+  @GET
+  @javax.ws.rs.Path("tiles.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Tileset getTileset() throws IOException {
+    return objectMapper.readValue(tileset.toFile(), Tileset.class);
+  }
+
+  @GET
+  @javax.ws.rs.Path("/tiles/{z}/{x}/{y}.mvt")
+  public Response getTile(@PathParam("z") int z, @PathParam("x") int x, @PathParam("y") int y) {
+    try {
+      TileStore tileStore = new PostgresTileStore(dataSource, getTileset());
+      Tile tile = new Tile(x, y, z);
+      ByteBuffer blob = tileStore.read(tile);
+      if (blob != null) {
+        return Response.status(200).header(CONTENT_TYPE, TILE_TYPE)
+            .header(CONTENT_ENCODING, TILE_ENCODING).entity(blob.array()).build();
+      } else {
+        return Response.status(204).build();
+      }
+    } catch (Exception ex) {
+      logger.error("Tile error", ex);
+      return Response.status(404).build();
+    }
+  }
+
+  @GET
+  @javax.ws.rs.Path("{path:.*}")
+  public Response get(@PathParam("path") String path) throws IOException {
+    System.out.println(path);
+    if (path.equals("") || path.endsWith("/")) {
+      path += "index.html";
+    }
+    path = String.format("maputnik/%s", path);
+    try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path)) {
+      var bytes = inputStream.readAllBytes();
+      return Response.ok().entity(bytes).build();
+    } catch (IOException e) {
+      return Response.status(404).build();
+    }
+  }
+}
diff --git a/baremaps-server/src/main/resources/maputnik/app.45afceafeedad888c128.js b/baremaps-server/src/main/resources/maputnik/app.45afceafeedad888c128.js
new file mode 100644
index 00000000..3a64a3c4
--- /dev/null
+++ b/baremaps-server/src/main/resources/maputnik/app.45afceafeedad888c128.js
@@ -0,0 +1,63 @@
+!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typ [...]
+/*! https://mths.be/punycode v1.3.2 by @mathias */var p=u((function(e,t){!function(r){var n=t&&!t.nodeType&&t,o=e&&!e.nodeType&&e,i="object"==typeof l&&l;i.global!==i&&i.window!==i&&i.self!==i||(r=i);var s,a,u=2147483647,c=/^xn--/,p=/[^\x20-\x7E]/,_=/[\x2E\u3002\uFF0E\uFF61]/g,d={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},h=Math.floor,g=String.fromCharCode;function A(e){throw Ran [...]
+/*!
+  Copyright (c) 2018 Jed Watson.
+  Licensed under the MIT License (MIT), see
+  http://jedwatson.github.io/classnames
+*/!function(){"use strict";var r={}.hasOwnProperty;function o(){for(var e=[],t=0;t<arguments.length;t++){var n=arguments[t];if(n){var i=typeof n;if("string"===i||"number"===i)e.push(n);else if(Array.isArray(n)){if(n.length){var s=o.apply(null,n);s&&e.push(s)}}else if("object"===i)if(n.toString===Object.prototype.toString)for(var a in n)r.call(n,a)&&n[a]&&e.push(a);else e.push(n.toString())}}return e.join(" ")}e.exports?(o.default=o,e.exports=o):void 0===(n=function(){return o}.apply(t,[] [...]
+/**
+ * @license
+ * Lodash <https://lodash.com/>
+ * Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
+ * Released under MIT license <https://lodash.com/license>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ */(function(){var i="Expected a function",s="__lodash_placeholder__",a=[["ary",128],["bind",1],["bindKey",2],["curry",8],["curryRight",16],["flip",512],["partial",32],["partialRight",64],["rearg",256]],l="[object Arguments]",u="[object Array]",c="[object Boolean]",p="[object Date]",_="[object Error]",d="[object Function]",h="[object GeneratorFunction]",g="[object Map]",A="[object Number]",f="[object Object]",m="[object RegExp]",y="[object Set]",v="[object String]",b="[object Symbol]",E= [...]
+/*
+object-assign
+(c) Sindre Sorhus
+@license MIT
+*/var n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;function s(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},r=0;r<10;r++)t["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){re [...]
+/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
+t.read=function(e,t,r,n,o){var i,s,a=8*o-n-1,l=(1<<a)-1,u=l>>1,c=-7,p=r?o-1:0,_=r?-1:1,d=e[t+p];for(p+=_,i=d&(1<<-c)-1,d>>=-c,c+=a;c>0;i=256*i+e[t+p],p+=_,c-=8);for(s=i&(1<<-c)-1,i>>=-c,c+=n;c>0;s=256*s+e[t+p],p+=_,c-=8);if(0===i)i=1-u;else{if(i===l)return s?NaN:1/0*(d?-1:1);s+=Math.pow(2,n),i-=u}return(d?-1:1)*s*Math.pow(2,i-n)},t.write=function(e,t,r,n,o,i){var s,a,l,u=8*i-o-1,c=(1<<u)-1,p=c>>1,_=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,d=n?0:i-1,h=n?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t= [...]
+/** @license React v16.14.0
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var n=r(218),o="function"==typeof Symbol&&Symbol.for,i=o?Symbol.for("react.element"):60103,s=o?Symbol.for("react.portal"):60106,a=o?Symbol.for("react.fragment"):60107,l=o?Symbol.for("react.strict_mode"):60108,u=o?Symbol.for("react.profiler"):60114,c=o?Symbol.for("react.provider"):60109,p=o?Symbol.for("react.context"):60110,_=o?Symbol.for("react.forward_ref"):60112,d=o?Symbol.for("react.suspense"):60113,h=o?Symbol.for("react.memo"):60115,g=o?Symbol.for("react.lazy"):60116,A="function"= [...]
+/** @license React v16.14.0
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var n=r(1),o=r(218),i=r(345);function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r<arguments.length;r++)t+="&args[]="+encodeURIComponent(arguments[r]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}if(!n)throw Error(s(227));function a(e,t,r,n,o,i,s,a,l){var u=Array.prototype.slice.call(arguments,3);try{t.apply(r,u)}catch(e){this.onError(e)} [...]
+/** @license React v0.19.1
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var n,o,i,s,a;if("undefined"==typeof window||"function"!=typeof MessageChannel){var l=null,u=null,c=function(){if(null!==l)try{var e=t.unstable_now();l(!0,e),l=null}catch(e){throw setTimeout(c,0),e}},p=Date.now();t.unstable_now=function(){return Date.now()-p},n=function(e){null!==l?setTimeout(n,0,e):(l=e,setTimeout(c,0))},o=function(e,t){u=setTimeout(e,t)},i=function(){clearTimeout(u)},s=function(){return!1},a=t.unstable_forceFrameRate=function(){}}else{var _=window.performance,d=wind [...]
+/*!
+ * The buffer module from node.js, for the browser.
+ *
+ * @author   Feross Aboukhadijeh <http://feross.org>
+ * @license  MIT
+ */
+var n=r(363),o=r(221),i=r(364);function s(){return l.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function a(e,t){if(s()<t)throw new RangeError("Invalid typed array length");return l.TYPED_ARRAY_SUPPORT?(e=new Uint8Array(t)).__proto__=l.prototype:(null===e&&(e=new l(t)),e.length=t),e}function l(e,t,r){if(!(l.TYPED_ARRAY_SUPPORT||this instanceof l))return new l(e,t,r);if("number"==typeof e){if("string"==typeof t)throw new Error("If encoding is specified then the first argument must be a stri [...]
+/*!
+* tabbable 5.2.1
+* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+*/
+var n=["input","select","textarea","a[href]","button","[tabindex]","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])',"details>summary:first-of-type","details"],o=n.join(","),i="undefined"==typeof Element?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,s=function(e,t,r){var n=Array.prototype.slice.apply(e.querySelectorAll(o));return t&&i.call(e,o)&&n.unshift(e),n=n.filter(r)},a=functi [...]
+/*!
+* focus-trap 6.7.3
+* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+*/
+function A(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function f(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var m,y=(m=[],{activateTrap:function(e){if(m.length>0){var t=m[m.length-1];t!==e&&t.pause()}var r=m.indexOf(e);-1===r||m.splice(r,1),m.push(e)},deacti [...]
\ No newline at end of file
diff --git a/baremaps-server/src/main/resources/maputnik/favicon.ico b/baremaps-server/src/main/resources/maputnik/favicon.ico
new file mode 100644
index 00000000..f7aaa432
Binary files /dev/null and b/baremaps-server/src/main/resources/maputnik/favicon.ico differ
diff --git a/baremaps-server/src/main/resources/maputnik/fonts/Roboto-Medium.ttf b/baremaps-server/src/main/resources/maputnik/fonts/Roboto-Medium.ttf
new file mode 100644
index 00000000..6a951337
Binary files /dev/null and b/baremaps-server/src/main/resources/maputnik/fonts/Roboto-Medium.ttf differ
diff --git a/baremaps-server/src/main/resources/maputnik/fonts/Roboto-Regular.ttf b/baremaps-server/src/main/resources/maputnik/fonts/Roboto-Regular.ttf
new file mode 100644
index 00000000..8c082c8d
Binary files /dev/null and b/baremaps-server/src/main/resources/maputnik/fonts/Roboto-Regular.ttf differ
diff --git a/baremaps-server/src/main/resources/maputnik/index.html b/baremaps-server/src/main/resources/maputnik/index.html
new file mode 100644
index 00000000..7e2abc86
--- /dev/null
+++ b/baremaps-server/src/main/resources/maputnik/index.html
@@ -0,0 +1,52 @@
+<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Maputnik</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="manifest" href="manifest.json"><style>html {
+      background-color: rgb(28, 31, 36);
+    }
+
+    .loading {
+      text-align: center;
+      position: absolute;
+      width: 100vw;
+      height: 100vh;
+      top: 0;
+      left: 0;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .loading__logo svg {
+      width: 200px;
+      height: 200px;
+    }
+
+    .loading__text {
+      font-family: sans-serif;
+      color: white;
+      font-size: 1.2em;
+      padding-bottom: 2em;
+    }</style></head><body><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" version="1.1"><defs><filter id="protanopia"><feColorMatrix in="SourceGraphic" type="matrix" values="0.567, 0.433, 0,     0, 0
+                  0.558, 0.442, 0,     0, 0
+                  0,     0.242, 0.758, 0, 0
+                  0,     0,     0,     1, 0"/></filter><filter id="protanomaly"><feColorMatrix in="SourceGraphic" type="matrix" values="0.817, 0.183, 0,     0, 0
+                  0.333, 0.667, 0,     0, 0
+                  0,     0.125, 0.875, 0, 0
+                  0,     0,     0,     1, 0"/></filter><filter id="deuteranopia"><feColorMatrix in="SourceGraphic" type="matrix" values="0.625, 0.375, 0,   0, 0
+                  0.7,   0.3,   0,   0, 0
+                  0,     0.3,   0.7, 0, 0
+                  0,     0,     0,   1, 0"/></filter><filter id="deuteranomaly"><feColorMatrix in="SourceGraphic" type="matrix" values="0.8,   0.2,   0,     0, 0
+                  0.258, 0.742, 0,     0, 0
+                  0,     0.142, 0.858, 0, 0
+                  0,     0,     0,     1, 0"/></filter><filter id="tritanopia"><feColorMatrix in="SourceGraphic" type="matrix" values="0.95, 0.05,  0,     0, 0
+                  0,    0.433, 0.567, 0, 0
+                  0,    0.475, 0.525, 0, 0
+                  0,    0,     0,     1, 0"/></filter><filter id="tritanomaly"><feColorMatrix in="SourceGraphic" type="matrix" values="0.967, 0.033, 0,     0, 0
+                  0,     0.733, 0.267, 0, 0
+                  0,     0.183, 0.817, 0, 0
+                  0,     0,     0,     1, 0"/></filter><filter id="achromatopsia"><feColorMatrix in="SourceGraphic" type="matrix" values="0.299, 0.587, 0.114, 0, 0
+                  0.299, 0.587, 0.114, 0, 0
+                  0.299, 0.587, 0.114, 0, 0
+                  0,     0,     0,     1, 0"/></filter><filter id="achromatomaly"><feColorMatrix in="SourceGraphic" type="matrix" values="0.618, 0.320, 0.062, 0, 0
+                  0.163, 0.775, 0.062, 0, 0
+                  0.163, 0.320, 0.516, 0, 0
+                  0,     0,     0,     1, 0"/></filter></defs></svg><div id="app"></div><div class="loading"><div class="loading__logo"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1200" height="1200" viewBox="0 0 100 100"><style>@keyframes circle-anim{0%,40%{fill-opacity:0}60%{fill-opacity:1}}</style><g class="map" stroke="#000"><use xlink:href="#ref-1--map__main" fill="#4eba6f"/><use xlink:href="#ref-1--map__line1" fill="none"/><use xlink:hr [...]
\ No newline at end of file
diff --git a/baremaps-server/src/main/resources/maputnik/manifest.json b/baremaps-server/src/main/resources/maputnik/manifest.json
new file mode 100644
index 00000000..bd9be0f1
--- /dev/null
+++ b/baremaps-server/src/main/resources/maputnik/manifest.json
@@ -0,0 +1,9 @@
+{
+  "name": "Maputnik",
+  "short_name": "Maputnik",
+  "description": "Visual Map Designer",
+  "start_url": ".",
+  "display": "browser",
+  "background_color": "#1c1f24",
+  "theme_color": "#1c1f24"
+}
diff --git a/examples/openstreetmap/liechtenstein-latest.osm.pbf b/examples/openstreetmap/liechtenstein-latest.osm.pbf
new file mode 100644
index 00000000..89d330f3
Binary files /dev/null and b/examples/openstreetmap/liechtenstein-latest.osm.pbf differ
diff --git a/examples/openstreetmap/style.json b/examples/openstreetmap/style.json
index f64b679f..d0047549 100644
--- a/examples/openstreetmap/style.json
+++ b/examples/openstreetmap/style.json
@@ -1,5 +1,10 @@
 {
   "version" : 8,
+  "metadata" : {
+    "maputnik:renderer" : "mbgljs"
+  },
+  "center" : [ 9.5554, 47.166 ],
+  "zoom" : 14,
   "sources" : {
     "baremaps" : {
       "type" : "vector",
@@ -15,12 +20,8 @@
       "visibility" : "visible"
     },
     "paint" : {
-      "fill-color" : "rgba(255, 0, 0, 1)"
+      "fill-color" : "rgba(109, 8, 8, 1)"
     }
   } ],
-  "center" : [ 9.5554, 47.166 ],
-  "metadata" : {
-    "maputnik:renderer" : "mbgljs"
-  },
-  "zoom" : 14
+  "id" : "amqplfmqy"
 }
\ No newline at end of file