├── priv ├── static │ ├── favicon.ico │ └── js │ │ └── map_logic.js └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── test ├── test_helper.exs ├── tile_server_web │ └── views │ │ └── error_view_test.exs └── support │ ├── channel_case.ex │ └── conn_case.ex ├── .formatter.exs ├── lib ├── tile_server.ex ├── tile_server_web │ ├── views │ │ ├── error_view.ex │ │ └── error_helpers.ex │ ├── gettext.ex │ ├── router.ex │ ├── channels │ │ └── user_socket.ex │ ├── telemetry.ex │ ├── endpoint.ex │ └── controllers │ │ └── map_controller.ex ├── tile_server │ └── application.ex └── tile_server_web.ex ├── config ├── test.exs ├── prod.secret.exs ├── config.exs ├── dev.exs └── prod.exs ├── .gitignore ├── mix.exs ├── README.md └── mix.lock /priv/static/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/tile_server.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServer do 2 | @moduledoc """ 3 | TileServer keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :tile_server, TileServerWeb.Endpoint, 6 | http: [port: 4002], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /test/tile_server_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.ErrorViewTest do 2 | use TileServerWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.json" do 8 | assert render(TileServerWeb.ErrorView, "404.json", []) == %{errors: %{detail: "Not Found"}} 9 | end 10 | 11 | test "renders 500.json" do 12 | assert render(TileServerWeb.ErrorView, "500.json", []) == 13 | %{errors: %{detail: "Internal Server Error"}} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/tile_server_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.ErrorView do 2 | use TileServerWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.json", _assigns) do 7 | # %{errors: %{detail: "Internal Server Error"}} 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.json" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tile_server_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import TileServerWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :tile_server 24 | end 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | tile_server-*.tar 24 | 25 | priv/static/static_tiles/ 26 | priv/*mbtiles 27 | # Since we are building assets from assets/, 28 | # we ignore priv/static. You may want to comment 29 | # this depending on your deployment strategy. 30 | 31 | -------------------------------------------------------------------------------- /config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | secret_key_base = 8 | System.get_env("SECRET_KEY_BASE") || 9 | raise """ 10 | environment variable SECRET_KEY_BASE is missing. 11 | You can generate one by calling: mix phx.gen.secret 12 | """ 13 | 14 | config :tile_server, TileServerWeb.Endpoint, 15 | http: [ 16 | port: String.to_integer(System.get_env("PORT") || "4000"), 17 | transport_options: [socket_opts: [:inet6]] 18 | ], 19 | secret_key_base: secret_key_base 20 | 21 | # ## Using releases (Elixir v1.9+) 22 | # 23 | # If you are doing OTP releases, you need to instruct Phoenix 24 | # to start each relevant endpoint: 25 | # 26 | # config :tile_server, TileServerWeb.Endpoint, server: true 27 | # 28 | # Then you can assemble a release by calling `mix release`. 29 | # See `mix help release` for more information. 30 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use TileServerWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import TileServerWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint TileServerWeb.Endpoint 28 | end 29 | end 30 | 31 | setup _tags do 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tile_server/application.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServer.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | children = [ 10 | # Start the Telemetry supervisor 11 | TileServerWeb.Telemetry, 12 | # Start the PubSub system 13 | {Phoenix.PubSub, name: TileServer.PubSub}, 14 | # Start the Endpoint (http/https) 15 | TileServerWeb.Endpoint, 16 | # Start a worker by calling: TileServer.Worker.start_link(arg) 17 | # {TileServer.Worker, arg} 18 | ] 19 | 20 | # See https://hexdocs.pm/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: TileServer.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | # Tell Phoenix to update the endpoint configuration 27 | # whenever the application is updated. 28 | def config_change(changed, _new, removed) do 29 | TileServerWeb.Endpoint.config_change(changed, removed) 30 | :ok 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/tile_server_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.Router do 2 | use TileServerWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | end 7 | 8 | pipeline :api do 9 | plug :accepts, ["json"] 10 | end 11 | 12 | scope "/api", TileServerWeb do 13 | pipe_through :api 14 | end 15 | 16 | scope "/", TileServerWeb do 17 | pipe_through :browser 18 | get "/", MapController, :index 19 | get "/tiles/:z/:x/:y", MapController, :tile 20 | end 21 | 22 | # Enables LiveDashboard only for development 23 | # 24 | # If you want to use the LiveDashboard in production, you should put 25 | # it behind authentication and allow only admins to access it. 26 | # If your application does not have an admins-only section yet, 27 | # you can use Plug.BasicAuth to set up some basic authentication 28 | # as long as you are also using SSL (which you should anyway). 29 | if Mix.env() in [:dev, :test] do 30 | import Phoenix.LiveDashboard.Router 31 | 32 | scope "/" do 33 | pipe_through [:fetch_session, :protect_from_forgery] 34 | live_dashboard "/dashboard", metrics: TileServerWeb.Telemetry 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use TileServerWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import TileServerWeb.ConnCase 26 | 27 | alias TileServerWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint TileServerWeb.Endpoint 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/tile_server_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", TileServerWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | @impl true 19 | def connect(_params, socket, _connect_info) do 20 | {:ok, socket} 21 | end 22 | 23 | # Socket id's are topics that allow you to identify all sockets for a given user: 24 | # 25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 26 | # 27 | # Would allow you to broadcast a "disconnect" event and terminate 28 | # all active sockets and channels for a given user: 29 | # 30 | # TileServerWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 31 | # 32 | # Returning `nil` makes this socket anonymous. 33 | @impl true 34 | def id(_socket), do: nil 35 | end 36 | -------------------------------------------------------------------------------- /lib/tile_server_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | @doc """ 7 | Translates an error message using gettext. 8 | """ 9 | def translate_error({msg, opts}) do 10 | # When using gettext, we typically pass the strings we want 11 | # to translate as a static argument: 12 | # 13 | # # Translate "is invalid" in the "errors" domain 14 | # dgettext("errors", "is invalid") 15 | # 16 | # # Translate the number of files with plural rules 17 | # dngettext("errors", "1 file", "%{count} files", count) 18 | # 19 | # Because the error messages we show in our forms and APIs 20 | # are defined inside Ecto, we need to translate them dynamically. 21 | # This requires us to call the Gettext module passing our gettext 22 | # backend as first argument. 23 | # 24 | # Note we use the "errors" domain, which means translations 25 | # should be written to the errors.po file. The :count option is 26 | # set by Ecto and indicates we should also apply plural rules. 27 | if count = opts[:count] do 28 | Gettext.dngettext(TileServerWeb.Gettext, "errors", msg, msg, count, opts) 29 | else 30 | Gettext.dgettext(TileServerWeb.Gettext, "errors", msg, opts) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | use Mix.Config 9 | 10 | # Configures the endpoint 11 | config :tile_server, TileServerWeb.Endpoint, 12 | url: [host: "localhost"], 13 | secret_key_base: "XUgpYQQW12iGu1bLzLfTgJxGfgS1bx2ILuuWLvQ4X/wCNMeEeUd+vP2yXX0Bz0ul", 14 | render_errors: [view: TileServerWeb.ErrorView, accepts: ~w(json), layout: false], 15 | pubsub_server: TileServer.PubSub, 16 | live_view: [signing_salt: "JLOovgbF"] 17 | 18 | config :mbtiles, :mbtiles_path, "priv/united_kingdom.mbtiles" 19 | #config :mbtiles, :mbtiles_path, "priv/poland_katowice.mbtiles" 20 | #config :mbtiles, :mbtiles_path, 'priv/test.mbtiles' 21 | 22 | 23 | #config :mbtiles, mbtiles_path: "priv/poland_katowice.mbtiles" 24 | # Configures Elixir's Logger 25 | config :logger, :console, 26 | format: "$time $metadata[$level] $message\n", 27 | metadata: [:request_id] 28 | 29 | # Use Jason for JSON parsing in Phoenix 30 | config :phoenix, :json_library, Jason 31 | 32 | config :tile_server, 33 | database: "osm", 34 | hostname: "localhost", 35 | port: "5432" 36 | # Import environment specific config. This must remain at the bottom 37 | # of this file so it overrides the configuration defined above. 38 | import_config "#{Mix.env()}.exs" 39 | -------------------------------------------------------------------------------- /lib/tile_server_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # VM Metrics 34 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 35 | summary("vm.total_run_queue_lengths.total"), 36 | summary("vm.total_run_queue_lengths.cpu"), 37 | summary("vm.total_run_queue_lengths.io") 38 | ] 39 | end 40 | 41 | defp periodic_measurements do 42 | [ 43 | # A module, function and arguments to be invoked periodically. 44 | # This function must call :telemetry.execute/3 and a metric must be added above. 45 | # {TileServerWeb, :count_users, []} 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with webpack to recompile .js and .css sources. 9 | config :tile_server, TileServerWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [] 15 | 16 | # ## SSL Support 17 | # 18 | # In order to use HTTPS in development, a self-signed 19 | # certificate can be generated by running the following 20 | # Mix task: 21 | # 22 | # mix phx.gen.cert 23 | # 24 | # Note that this task requires Erlang/OTP 20 or later. 25 | # Run `mix help phx.gen.cert` for more information. 26 | # 27 | # The `http:` config above can be replaced with: 28 | # 29 | # https: [ 30 | # port: 4001, 31 | # cipher_suite: :strong, 32 | # keyfile: "priv/cert/selfsigned_key.pem", 33 | # certfile: "priv/cert/selfsigned.pem" 34 | # ], 35 | # 36 | # If desired, both `http:` and `https:` keys can be 37 | # configured to run both http and https servers on 38 | # different ports. 39 | 40 | # Do not include metadata nor timestamps in development logs 41 | config :logger, :console, format: "[$level] $message\n" 42 | 43 | # Set a higher stacktrace during development. Avoid configuring such 44 | # in production as building large stacktraces may be expensive. 45 | config :phoenix, :stacktrace_depth, 20 46 | 47 | # Initialize plugs at runtime for faster development compilation 48 | config :phoenix, :plug_init_mode, :runtime 49 | -------------------------------------------------------------------------------- /lib/tile_server_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :tile_server 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_tile_server_key", 10 | signing_salt: "pFupu9B1" 11 | ] 12 | 13 | socket "/socket", TileServerWeb.UserSocket, 14 | websocket: true, 15 | longpoll: false 16 | 17 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 18 | 19 | # Serve at "/" the static files from "priv/static" directory. 20 | # 21 | # You should set gzip to true if you are running phx.digest 22 | # when deploying your static files in production. 23 | plug Plug.Static, 24 | at: "/", 25 | from: :tile_server, 26 | gzip: true, 27 | only: ~w(css static_tiles fonts images js favicon.ico robots.txt) 28 | 29 | # Code reloading can be explicitly enabled under the 30 | # :code_reloader configuration of your endpoint. 31 | if code_reloading? do 32 | plug Phoenix.CodeReloader 33 | end 34 | 35 | plug Phoenix.LiveDashboard.RequestLogger, 36 | param_key: "request_logger", 37 | cookie_key: "request_logger" 38 | 39 | plug Plug.RequestId 40 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 41 | 42 | plug Plug.Parsers, 43 | parsers: [:urlencoded, :multipart, :json], 44 | pass: ["*/*"], 45 | json_decoder: Phoenix.json_library() 46 | 47 | plug Plug.MethodOverride 48 | plug Plug.Head 49 | plug Plug.Session, @session_options 50 | plug TileServerWeb.Router 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TileServer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :tile_server, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {TileServer.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.5.7"}, 37 | {:phoenix_live_dashboard, "~> 0.4"}, 38 | {:telemetry_metrics, "~> 0.4"}, 39 | {:telemetry_poller, "~> 0.4"}, 40 | {:gettext, "~> 0.11"}, 41 | {:jason, "~> 1.0"}, 42 | {:plug_cowboy, "~> 2.0"}, 43 | {:mbtiles, "~> 0.2"}, 44 | {:postgrex, "~> 0.15.7"}, 45 | {:geo_postgis, "~> 3.1"} 46 | ] 47 | end 48 | 49 | # Aliases are shortcuts or tasks specific to the current project. 50 | # For example, to install project dependencies and perform other setup tasks, run: 51 | # 52 | # $ mix setup 53 | # 54 | # See the documentation for `Mix` for more info on aliases. 55 | defp aliases do 56 | [ 57 | setup: ["deps.get"] 58 | ] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/tile_server_web.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use TileServerWeb, :controller 9 | use TileServerWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: TileServerWeb 23 | 24 | import Plug.Conn 25 | import TileServerWeb.Gettext 26 | alias TileServerWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/tile_server_web/templates", 34 | namespace: TileServerWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def router do 46 | quote do 47 | use Phoenix.Router 48 | 49 | import Plug.Conn 50 | import Phoenix.Controller 51 | end 52 | end 53 | 54 | def channel do 55 | quote do 56 | use Phoenix.Channel 57 | import TileServerWeb.Gettext 58 | end 59 | end 60 | 61 | defp view_helpers do 62 | quote do 63 | # Import basic rendering functionality (render, render_layout, etc) 64 | import Phoenix.View 65 | 66 | import TileServerWeb.ErrorHelpers 67 | import TileServerWeb.Gettext 68 | alias TileServerWeb.Router.Helpers, as: Routes 69 | end 70 | end 71 | 72 | @doc """ 73 | When used, dispatch to the appropriate controller/view/etc. 74 | """ 75 | defmacro __using__(which) when is_atom(which) do 76 | apply(__MODULE__, which, []) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :tile_server, TileServerWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :tile_server, TileServerWeb.Endpoint, 25 | # ... 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # port: 443, 29 | # cipher_suite: :strong, 30 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 31 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), 32 | # transport_options: [socket_opts: [:inet6]] 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :tile_server, TileServerWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | 53 | # Finally import the config/prod.secret.exs which loads secrets 54 | # and configuration from environment variables. 55 | import_config "prod.secret.exs" 56 | -------------------------------------------------------------------------------- /lib/tile_server_web/controllers/map_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TileServerWeb.MapController do 2 | use TileServerWeb, :controller 3 | import Phoenix.HTML 4 | 5 | @doc """ 6 | https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md 7 | """ 8 | def index(conn, _params) do 9 | meta = Mbtiles.DB.get_metadata() 10 | [w, s, e, n] = String.split(Map.get(meta, :bounds, "-180,90,180,90"), ",") 11 | [lon, lat, zoom] = String.split(Map.get(meta, :center, "0,0,0"), ",") 12 | layers = get_layer_ids(Map.get(meta, :json)) 13 | IO.inspect(layers) 14 | 15 | html( 16 | conn, 17 | ~E""" 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 38 | 39 | 40 | """ 41 | |> safe_to_string 42 | ) 43 | end 44 | 45 | def tile(conn, params) do 46 | %{z: z, x: x, y: y} = parse_tile_params(params) 47 | allow_gzip = allow_gzip?(conn) 48 | 49 | case Mbtiles.DB.get_images(z, x, y, tms: true, gzip: allow_gzip) do 50 | :error -> 51 | conn |> send_resp(404, "tile not found") 52 | 53 | {file_type, tile} -> 54 | conn 55 | |> gzip_header(file_type) 56 | |> put_resp_content_type(get_content_type(file_type)) 57 | |> send_resp(200, tile) 58 | end 59 | end 60 | 61 | defp gzip_header(conn, :pbf_gz) do 62 | prepend_resp_headers(conn, [{"content-encoding", "gzip"}]) 63 | end 64 | 65 | defp gzip_header(conn, _), do: conn 66 | 67 | defp parse_tile_params(params) do 68 | params 69 | |> Enum.map(fn {k, v} -> {String.to_atom(k), String.to_integer(v)} end) 70 | |> Map.new() 71 | end 72 | 73 | def get_layer_ids(nil), do: "" 74 | def get_layer_ids(json) do 75 | json 76 | |> Jason.decode!() 77 | |> Map.get("vector_layers") 78 | |> Enum.map(&Map.get(&1, "id")) 79 | |> Enum.sort() 80 | end 81 | 82 | defp allow_gzip?(conn), do: inspect(get_req_header(conn, "accept-encoding")) =~ "gzip" 83 | defp get_content_type(:png), do: "image/png" 84 | defp get_content_type(:jpeg), do: "image/jpeg" 85 | defp get_content_type(:pbf), do: "application/vnd.mapbox-vector-tile" 86 | defp get_content_type(:pbf_gz), do: "application/vnd.mapbox-vector-tile" 87 | end 88 | -------------------------------------------------------------------------------- /priv/static/js/map_logic.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var vectorTileStyling = { 4 | water: { 5 | fill: true, 6 | weight: 1, 7 | fillColor: '#06cccc', 8 | color: '#06cccc', 9 | fillOpacity: 0.2, 10 | opacity: 0.4, 11 | }, 12 | admin: { 13 | weight: 1, 14 | fillColor: 'pink', 15 | color: 'pink', 16 | fillOpacity: 0.2, 17 | opacity: 0.4 18 | }, 19 | waterway: { 20 | weight: 1, 21 | fillColor: '#2375e0', 22 | color: '#2375e0', 23 | fillOpacity: 0.2, 24 | opacity: 0.4 25 | }, 26 | landcover: { 27 | fill: true, 28 | weight: 1, 29 | fillColor: '#53e033', 30 | color: '#53e033', 31 | fillOpacity: 0.2, 32 | opacity: 0.4, 33 | }, 34 | landuse: { 35 | fill: true, 36 | weight: 1, 37 | fillColor: '#e5b404', 38 | color: '#e5b404', 39 | fillOpacity: 0.2, 40 | opacity: 0.4 41 | }, 42 | park: { 43 | fill: true, 44 | weight: 1, 45 | fillColor: '#84ea5b', 46 | color: '#84ea5b', 47 | fillOpacity: 0.2, 48 | opacity: 0.4 49 | }, 50 | boundary: { 51 | weight: 1, 52 | fillColor: '#c545d3', 53 | color: '#c545d3', 54 | fillOpacity: 0.2, 55 | opacity: 0.4 56 | }, 57 | aeroway: { 58 | weight: 1, 59 | fillColor: '#51aeb5', 60 | color: '#51aeb5', 61 | fillOpacity: 0.2, 62 | opacity: 0.4 63 | }, 64 | road: { // mapbox & nextzen only 65 | weight: 1, 66 | fillColor: '#f2b648', 67 | color: '#f2b648', 68 | fillOpacity: 0.2, 69 | opacity: 0.4 70 | }, 71 | tunnel: { // mapbox only 72 | weight: 0.5, 73 | fillColor: '#f2b648', 74 | color: '#f2b648', 75 | fillOpacity: 0.2, 76 | opacity: 0.4, 77 | // dashArray: [4, 4] 78 | }, 79 | bridge: { // mapbox only 80 | weight: 0.5, 81 | fillColor: '#f2b648', 82 | color: '#f2b648', 83 | fillOpacity: 0.2, 84 | opacity: 0.4, 85 | // dashArray: [4, 4] 86 | }, 87 | transportation: { // openmaptiles only 88 | weight: 0.5, 89 | fillColor: '#f2b648', 90 | color: '#f2b648', 91 | fillOpacity: 0.2, 92 | opacity: 0.4, 93 | // dashArray: [4, 4] 94 | }, 95 | transit: { // nextzen only 96 | weight: 0.5, 97 | fillColor: '#f2b648', 98 | color: '#f2b648', 99 | fillOpacity: 0.2, 100 | opacity: 0.4, 101 | // dashArray: [4, 4] 102 | }, 103 | building: { 104 | fill: true, 105 | weight: 1, 106 | fillColor: '#2b2b2b', 107 | color: '#2b2b2b', 108 | fillOpacity: 0.2, 109 | opacity: 0.4 110 | }, 111 | water_name: { 112 | weight: 1, 113 | fillColor: '#022c5b', 114 | color: '#022c5b', 115 | fillOpacity: 0.2, 116 | opacity: 0.4 117 | }, 118 | transportation_name: { 119 | weight: 1, 120 | fillColor: '#bc6b38', 121 | color: '#bc6b38', 122 | fillOpacity: 0.2, 123 | opacity: 0.4 124 | }, 125 | place: { 126 | weight: 1, 127 | fillColor: '#f20e93', 128 | color: '#f20e93', 129 | fillOpacity: 0.2, 130 | opacity: 0.4 131 | }, 132 | housenumber: { 133 | weight: 1, 134 | fillColor: '#ef4c8b', 135 | color: '#ef4c8b', 136 | fillOpacity: 0.2, 137 | opacity: 0.4 138 | }, 139 | poi: { 140 | weight: 1, 141 | fillColor: '#3bb50a', 142 | color: '#3bb50a', 143 | fillOpacity: 0.2, 144 | opacity: 0.4 145 | }, 146 | earth: { // nextzen only 147 | fill: true, 148 | weight: 1, 149 | fillColor: '#c0c0c0', 150 | color: '#c0c0c0', 151 | fillOpacity: 0.2, 152 | opacity: 0.4 153 | }, 154 | 155 | // Do not symbolize some stuff for mapbox 156 | country_label: [], 157 | marine_label: [], 158 | state_label: [], 159 | place_label: [], 160 | waterway_label: [], 161 | poi_label: [], 162 | road_label: [], 163 | housenum_label: [], 164 | 165 | 166 | // Do not symbolize some stuff for openmaptiles 167 | country_name: [], 168 | marine_name: [], 169 | state_name: [], 170 | place_name: [], 171 | waterway_name: [], 172 | poi_name: [], 173 | road_name: [], 174 | housenum_name: [], 175 | }; 176 | // Monkey-patch some properties for nextzen layer names, because 177 | // instead of "building" the data layer is called "buildings" and so on 178 | 179 | vectorTileStyling.buildings = vectorTileStyling.building; 180 | vectorTileStyling.boundaries = vectorTileStyling.boundary; 181 | vectorTileStyling.places = vectorTileStyling.place; 182 | vectorTileStyling.pois = vectorTileStyling.poi; 183 | vectorTileStyling.roads = vectorTileStyling.road; 184 | 185 | function get(param){ 186 | var dataDiv = document.getElementById('data-div'); 187 | dataParam = dataDiv.getAttribute('data-' + param); 188 | try { 189 | return Number(dataParam) 190 | } 191 | catch { 192 | return dataParam 193 | } 194 | } 195 | 196 | var lat = get("lat"); 197 | var lon = get("lon"); 198 | var minZoom = get("minzoom") 199 | var maxZoom = get("maxzoom") 200 | var zoom = get("zoom") 201 | var s = get("s") 202 | var n = get("n") 203 | var e = get("e") 204 | var w = get("w") 205 | var attribution = document.getElementById('data-div').innerHTML; 206 | 207 | var southWest = L.latLng(w, s), 208 | northEast = L.latLng(e, n), 209 | bounds = L.latLngBounds(southWest, northEast); 210 | 211 | var map = L.map('map').setView([lat, lon], zoom) 212 | 213 | //Choose if controller or static 214 | var openMapTilesUrl = "http://localhost:4000/tiles/{z}/{x}/{y}"; 215 | //var openMapTilesUrl = "http://localhost:4000/static_tiles/{z}/{x}/{y}.pbf"; 216 | 217 | //Choose if images or vectors 218 | //var openMapTilesLayer = L.tileLayer(openMapTilesUrl, { 219 | var openMapTilesLayer = L.vectorGrid.protobuf(openMapTilesUrl, { 220 | attribution: attribution, 221 | minZoom: minZoom, 222 | maxZoom: maxZoom, 223 | vectorTileLayerStyles: vectorTileStyling, 224 | maxBounds: bounds 225 | }); 226 | openMapTilesLayer.addTo(map); 227 | 228 | map.on('click', function(e){ 229 | var coord = e.latlng; 230 | var lat = coord.lat; 231 | var lng = coord.lng; 232 | console.log("You clicked the map at latitude: " + lat + " and longitude: " + lng); 233 | }); 234 | function onLocationFound(e) { 235 | var radius = e.accuracy / 2; 236 | L.marker(e.latlng).addTo(map) 237 | .bindPopup("You are within " + radius + " meters from this point").openPopup(); 238 | L.circle(e.latlng, radius).addTo(map); 239 | } 240 | 241 | //map.on('locationfound', onLocationFound); 242 | //map.locate({setView: true, watch: true, maxZoom: zoom}); 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Some background on mbtiles files from [mapbox/mbtiles-spec](https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md) 2 | 3 | > MBTiles is a specification for storing tiled map data in SQLite databases for immediate usage and for transfer. 4 | > ... 5 | > The metadata table is used as a key/value store for settings. It MUST contain these two rows: 6 | 7 | > name (string): The human-readable name of the tileset. 8 | > format (string): The file format of the tile data: pbf, jpg, png, webp, or an IETF media type for other formats. 9 | 10 | We can download an example file from [openmaptiles](https://openmaptiles.org/) or render some ourselves from openstreetmap data. In this tutorial I'll use a file has bundled the assets as `*.pbf` because it's the most complicated format to set up. With images you don't need any plugins for leaflet and the rendering should be much faster in user browser. 11 | 12 | `# sqlite3 priv/united_kingdom.mbtiles` 13 | ``` 14 | After downloading a file we can have a look whats really inside: 15 | sqlite> .headers on 16 | sqlite> .tables 17 | gpkg_contents gpkg_tile_matrix omtm 18 | gpkg_geometry_columns gpkg_tile_matrix_setpackage_tiles 19 | gpkg_metadata images tiles 20 | gpkg_metadata_reference map 21 | gpkg_spatial_ref_sysmetadata 22 | ``` 23 | 24 | We are interested in tiles and metadata tables: 25 | `sqlite> select * from tiles limit 1;` 26 | 27 | zoom_level | tile_column | tile_row | tile_data 28 | --- | --- | --- | --- 29 | 14 | 7763 | 10757 | 30 | 31 | `sqlite> select * from metadata where name is not 'json';` 32 | 33 | name | value 34 | --- | --- 35 | attribution | 1.7"}` 57 | we want to run it under superisor, for that we need to change the config.exs and application.ex 58 | ```elixir 59 | config :tile_server, mbtiles_path: "priv/united_kingdom.mbtiles" 60 | ``` 61 | ```elixir 62 | children = [ 63 | ....., 64 | %{ 65 | id: Sqlitex.Server, 66 | start: {Sqlitex.Server, :start_link, 67 | [Application.get_env(:tile_server, :mbtiles_path), [name: TilesDB]]} 68 | } 69 | ] 70 | ``` 71 | First thing we need to do is parse the metadata - in a new file `TileServer.Mbtiles` I'll create a function that does it 72 | ```elixir 73 | def get_metadata do 74 | query = "SELECT * FROM metadata" 75 | 76 | with {:ok, rows} <- Sqlitex.Server.query(TilesDB, query) do 77 | Enum.reduce(rows, %{}, fn [name: name, value: value], acc -> 78 | Map.put(acc, String.to_atom(name), value) 79 | end) 80 | else 81 | error -> IO.inspect(error) 82 | end 83 | end 84 | ``` 85 | to check if its working we can display it on the homepage, the router needs to be modified: 86 | ```elixir 87 | pipeline :browser do 88 | plug :accepts, ["html"] 89 | end 90 | scope "/", TileServerWeb do 91 | pipe_through :browser 92 | get "/", MapController, :index 93 | end 94 | ``` 95 | and new map_controller.ex 96 | ```elixir 97 | defmodule TileServerWeb.MapController do 98 | use TileServerWeb, :controller 99 | alias TileServer.Mbtiles 100 | 101 | def index(conn, _params) do 102 | meta = Mbtiles.get_metadata() 103 | text(conn, inspect(meta)) 104 | end 105 | end 106 | ``` 107 | Ok, this works, other route is for getting the tiles, we need to create another function in our context: 108 | ```elixir 109 | def get_images(z, x, y) do 110 | query = 111 | "SELECT tile_data FROM tiles where zoom_level = ? and tile_column = ? and tile_row = ?" 112 | 113 | with {:ok, [data]} <- Sqlitex.Server.query(TilesDB, query, bind: [z, x, y]), 114 | [tile_data: tile_blob] <- data, 115 | {:blob, tile} <- tile_blob, do: tile 116 | end 117 | ``` 118 | Here we are using bind params to avoid sql injection attacks. We also are returning the data from the database without unpacking. To allow the client to process the compressed files we need to set a setting a content encoding header 119 | `{"content-encoding", "gzip"}` 120 | ```elixir 121 | def tile(conn, params) do 122 | %{z: z, x: x, y: y} = parse_tile_params(params) 123 | 124 | case Mbtiles.get_images(z, x, get_tms_y(z, y)) do 125 | :error -> 126 | conn |> send_resp(404, "tile not found") 127 | 128 | tile -> 129 | conn 130 | |> prepend_resp_headers([{"content-encoding", "gzip"}]) 131 | |> put_resp_content_type("application/octet-stream") 132 | |> send_resp(200, tile) 133 | end 134 | end 135 | 136 | defp get_tms_y(z, y), do: round(:math.pow(2, z) - 1 - y) 137 | 138 | defp parse_tile_params(params) do 139 | params 140 | |> Enum.map(fn {k, v} -> {String.to_atom(k), String.to_integer(v)} end) 141 | |> Map.new() 142 | end 143 | ``` 144 | The other thing that may be weird here is the `get_tms_y/2` function: many of the mbtiles files are stored with the y coordinate in reverse order this can be easily seen if the tiles look weird on the map like completly not aligned between rows when displayed. 145 | Last puzzle piece is the router entry for our tiles 146 | ``` 147 | get "/", MapController, :index 148 | get "/tiles/:z/:x/:y", MapController, :tile 149 | ``` 150 | Now we are good to go, The only bottleneck I spotted is the the vector tiles need to be processed by javascript full screen needs processing power and this takes few seconds because the browser needs to process all the data. 151 | Otherwise phoenix does a good job here: 152 | ``` 153 | [info] GET /tiles/14/9050/5531 154 | [info] Sent 200 in 3ms 155 | [info] GET /tiles/14/9048/5528 156 | [info] GET /tiles/14/9048/5530 157 | [info] GET /tiles/14/9049/5527 158 | [info] GET /tiles/14/9051/5527 159 | [info] Sent 200 in 3ms 160 | [info] Sent 200 in 4ms 161 | [info] Sent 200 in 4ms 162 | [info] Sent 200 in 5ms 163 | [info] GET /tiles/14/9051/5528 164 | [info] Sent 200 in 6ms 165 | [info] GET /tiles/14/9048/5529 166 | [info] GET /tiles/14/9049/5530 167 | [info] GET /tiles/14/9052/5529 168 | [info] GET /tiles/14/9050/5527 169 | [info] GET /tiles/14/9051/5530 170 | [info] Sent 200 in 3ms 171 | [info] Sent 200 in 4ms 172 | [info] Sent 200 in 4ms 173 | [info] Sent 200 in 5ms 174 | [info] Sent 200 in 5ms 175 | ``` 176 | There is also an alternative to just unpack the files and serve it form the `/priv/static` folder. 177 | To serve it this way we need [mb-util](https://github.com/mapbox/mbutil) app installed - The files are compressed, so we need either unpack them or rename to have `.gz` extension - then phoenix will serve it gzipped and add the header for us. 178 | ``` 179 | ./mb-util --image_format=pbf countries.mbtiles static_tiles 180 | ``` 181 | ``` 182 | # add gz extension 183 | find . -type f -exec mv {} {}.gz ';' 184 | 185 | or 186 | # unpack and store unpacked 187 | gzip -d -r -S .pbf * 188 | find . -type f -exec mv '{}' '{}'.pbf \; 189 | ``` 190 | The endpoint.ex file needs to be modified: 191 | ```elixir 192 | plug Plug.Static, 193 | at: "/", 194 | only: ~w(css static_tiles fonts images js) 195 | ``` 196 | When playing with it I thought to myself that this may be also done using elixir and I created a mix task that does it. By running `mix mbtiles_unpack` in the repo directory the task will create a `priv/static/static_files` directory with a structure that can be served without any controllers. 197 | 198 | I packaged the backend code into a library, it can be installed using hex - source code is [here](https://github.com/dkuku/mbtiles) and also created a demo phoenix project that shows a how to use it. 199 | The main files in phoenix are the map_controller and /priv/static/map_logic.js where I added the javascript part, all is done using [leaflet](https://leafletjs.com/) and the [vector grid plugin](https://github.com/Leaflet/Leaflet.VectorGrid). Demo repository can be found [tile_server](https://github.com/dkuku/tile_server) 200 | 201 | 202 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, 3 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 4 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, 5 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 6 | "db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"}, 7 | "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, 8 | "esqlite": {:hex, :esqlite, "0.4.1", "ba5d0bab6b9c8432ffe1bf12fee8e154a50f1c3c40eadc3a9c870c23ca94d961", [:rebar3], [], "hexpm", "3584ca33172f4815ce56e96eed9835f5d8c987a9000fbc8c376c86acef8bf965"}, 9 | "geo": {:hex, :geo, "3.3.7", "d0354e099bdecc4138d1e01ac4d5aee8bccdb7cb8c9f840b6eb7b5ebbc328111", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ec57118fd9de27c52d4a046e75ee6f0ecb1cdc28cab03642228ed1aa09bb30bc"}, 10 | "geo_postgis": {:hex, :geo_postgis, "3.3.1", "45bc96b9121d0647341685dc9d44956d61338707482d655c803500676b0413a1", [:mix], [{:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "3c3957d8750e3effd565f068ee658ef0e881f9a07084a23f6c5ef8262d09b8e9"}, 11 | "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, 12 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 13 | "mbtiles": {:hex, :mbtiles, "0.2.0", "977168851a426da883cd0710ed46d8fc517dd48e0c5b16fb751066edc0077491", [:mix], [{:sqlitex, "~> 1.7", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm", "9236edd4a6b1cfb1bb3c06202352b49a1857526ca15307f064f1b90489320a2f"}, 14 | "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, 15 | "phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"}, 16 | "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, 17 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"}, 18 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.3", "70c7917e5c421e32d1a1c8ddf8123378bb741748cd8091eb9d557fb4be92a94f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cabcfb6738419a08600009219a5f0d861de97507fc1232121e1d5221aba849bd"}, 19 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 20 | "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, 21 | "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, 22 | "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, 23 | "postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"}, 24 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 25 | "sqlitex": {:hex, :sqlitex, "1.7.1", "022d477aab2ae999c43ae6fbd1782ff1457e0e95c251c7b5fa6f7b7b102040ff", [:mix], [{:decimal, "~> 1.7", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm", "ef16cda37b151136a47a6c0830dc9eb5e5f8f5f029b649e9f3a58a6eed634b80"}, 26 | "stream_gzip": {:hex, :stream_gzip, "0.4.1", "d5f611b3fa8f5c9d928db4c8446edb7e22bebdf38d9914b4017a6fff44887b26", [:mix], [], "hexpm", "343dee3cc30dc78562bb524e8ea802a13d6377fc6ef1c05ac4c9d9fb1f58044b"}, 27 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 28 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"}, 29 | "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, 30 | "unzip": {:hex, :unzip, "0.6.0", "8b32aaaeb35996ec8cf4c2c1ed36729dd22cae14b69505467502cb6622327ddb", [:mix], [], "hexpm", "e0a32d162fc696a51a71c2b08ab788d3b07442ca09d2cf7ef99c7488cb28182a"}, 31 | } 32 | --------------------------------------------------------------------------------