text: #!/usr/bin/env node
  "use strict";
  
  process.env.UV_THREADPOOL_SIZE = Math.ceil(
    Math.max(4, require("os").cpus().length * 1.5)
  );
  
  var fs = require("fs"),
    path = require("path");
  
  var clone = require("clone"),
    cors = require("cors"),
    enableShutdown = require("http-shutdown"),
    express = require("express"),
    handlebars = require("handlebars"),
    mercator = new (require("@mapbox/sphericalmercator"))(),
    morgan = require("morgan");
  
  var packageJson = require("../package"),
    serve_font = require("./serve_font"),
    serve_rendered = null,
    serve_style = require("./serve_style"),
    serve_data = require("./serve_data"),
    utils = require("./utils");
  
  var isLight = packageJson.name.slice(-6) == "-light";
  if (!isLight) {
    // do not require `serve_rendered` in the light package
    serve_rendered = require("./serve_rendered");
  }
  
  function start(opts) {
    console.log("Starting server");
  
    var app = express().disable("x-powered-by"),
      serving = {
        styles: {},
        rendered: {},
        data: {},
        fonts: {}
      };
  
    app.enable("trust proxy");
  
    if (process.env.NODE_ENV == "production") {
      app.use(
        morgan("tiny", {
          skip: function(req, res) {
            return (
              opts.silent && (res.statusCode == 200 || res.statusCode == 304)
            );
          }
        })
      );
    } else if (process.env.NODE_ENV !== "test") {
      app.use(
        morgan("dev", {
          skip: function(req, res) {
            return (
              opts.silent && (res.statusCode == 200 || res.statusCode == 304)
            );
          }
        })
      );
    }
  
    var config = opts.config || null;
    var configPath = null;
    if (opts.configPath) {
      configPath = path.resolve(opts.configPath);
      try {
        config = clone(require(configPath));
      } catch (e) {
        console.log("ERROR: Config file not found or invalid!");
        console.log("       See README.md for instructions and sample data.");
        process.exit(1);
      }
    }
    if (!config) {
      console.log("ERROR: No config file not specified!");
      process.exit(1);
    }
  
    var options = config.options || {};
    var paths = options.paths || {};
    options.paths = paths;
    paths.root = path.resolve(
      configPath ? path.dirname(configPath) : process.cwd(),
      paths.root || ""
    );
    paths.styles = path.resolve(paths.root, paths.styles || "");
    paths.fonts = path.resolve(paths.root, paths.fonts || "");
    paths.sprites = path.resolve(paths.root, paths.sprites || "");
    paths.mbtiles = path.resolve(paths.root, paths.mbtiles || "");
  
    var startupPromises = [];
  
    var checkPath = function(type) {
      if (!fs.existsSync(paths[type])) {
        console.error(
          'The specified path for "' +
            type +
            '" does not exist (' +
            paths[type] +
            ")."
        );
        process.exit(1);
      }
    };
    checkPath("styles");
    checkPath("fonts");
    checkPath("sprites");
    checkPath("mbtiles");
  
    if (options.dataDecorator) {
      try {
        options.dataDecoratorFunc = require(path.resolve(
          paths.root,
          options.dataDecorator
        ));
      } catch (e) {}
    }
  
    var data = clone(config.data || {});
  
    if (opts.cors) {
      app.use(cors());
    }
  
    Object.keys(config.styles || {}).forEach(function(id) {
      var item = config.styles[id];
      if (!item.style || item.style.length == 0) {
        console.log('Missing "style" property for ' + id);
        return;
      }
  
      if (item.serve_data !== false) {
        startupPromises.push(
          serve_style(
            options,
            serving.styles,
            item,
            id,
            opts.publicUrl,
            function(mbtiles, fromData) {
              var dataItemId;
              Object.keys(data).forEach(function(id) {
                if (fromData) {
                  if (id == mbtiles) {
                    dataItemId = id;
                  }
                } else {
                  if (data[id].mbtiles == mbtiles) {
                    dataItemId = id;
                  }
                }
              });
              if (dataItemId) {
                // mbtiles exist in the data config
                return dataItemId;
              } else if (fromData) {
                console.log('ERROR: data "' + mbtiles + '" not found!');
                process.exit(1);
              } else {
                var id = mbtiles.substr(0, mbtiles.lastIndexOf(".")) || mbtiles;
                while (data[id]) id += "_";
                data[id] = {
                  mbtiles: mbtiles
                };
                return id;
              }
            },
            function(font) {
              serving.fonts[font] = true;
            }
          ).then(function(sub) {
            app.use("/styles/", sub);
          })
        );
      }
      if (item.serve_rendered !== false) {
        if (serve_rendered) {
          startupPromises.push(
            serve_rendered(
              options,
              serving.rendered,
              item,
              id,
              opts.publicUrl,
              function(mbtiles) {
                var mbtilesFile;
                Object.keys(data).forEach(function(id) {
                  if (id == mbtiles) {
                    mbtilesFile = data[id].mbtiles;
                  }
                });
                return mbtilesFile;
              }
            ).then(function(sub) {
              app.use("/styles/", sub);
            })
          );
        } else {
          item.serve_rendered = false;
        }
      }
    });
  
    startupPromises.push(
      serve_font(options, serving.fonts).then(function(sub) {
        app.use("/", sub);
      })
    );
  
    Object.keys(data).forEach(function(id) {
      var item = data[id];
      if (!item.mbtiles || item.mbtiles.length == 0) {
        console.log('Missing "mbtiles" property for ' + id);
        return;
      }
  
      startupPromises.push(
        serve_data(
          options,
          serving.data,
          item,
          id,
          serving.styles,
          opts.publicUrl
        ).then(function(sub) {
          app.use("/data/", sub);
        })
      );
    });
  
    app.get("/styles.json", function(req, res, next) {
      var result = [];
      var query = req.query.key ? "?key=" + req.query.key : "";
      Object.keys(serving.styles).forEach(function(id) {
        var styleJSON = serving.styles[id];
        result.push({
          version: styleJSON.version,
          name: styleJSON.name,
          id: id,
          url:
            utils.getPublicUrl(opts.publicUrl, req) +
            "/styles/" +
            id +
            "/style.json" +
            query
        });
      });
      res.send(result);
    });
  
    var addTileJSONs = function(arr, req, type) {
      Object.keys(serving[type]).forEach(function(id) {
        var info = clone(serving[type][id]);
        var path = "";
        if (type == "rendered") {
          path = "styles/" + id;
        } else {
          path = type + "/" + id;
        }
        info.tiles = utils.getTileUrls(
          req,
          info.tiles,
          path,
          info.format,
          opts.publicUrl,
          {
            pbf: options.pbfAlias
          }
        );
        arr.push(info);
      });
      return arr;
    };
  
    app.get("/rendered.json", function(req, res, next) {
      res.send(addTileJSONs([], req, "rendered"));
    });
    app.get("/data.json", function(req, res, next) {
      res.send(addTileJSONs([], req, "data"));
    });
    app.get("/index.json", function(req, res, next) {
      res.send(addTileJSONs(addTileJSONs([], req, "rendered"), req, "data"));
    });
  
    //------------------------------------
    // serve web presentations
    app.use("/", express.static(path.join(__dirname, "../public/resources")));
  
    var templates = path.join(__dirname, "../public/templates");
    var serveTemplate = function(urlPath, template, dataGetter) {
      var templateFile = templates + "/" + template + ".tmpl";
      if (template == "index") {
        if (options.frontPage === false) {
          return;
        } else if (
          options.frontPage &&
          options.frontPage.constructor === String
        ) {
          templateFile = path.resolve(paths.root, options.frontPage);
        }
      }
      startupPromises.push(
        new Promise(function(resolve, reject) {
          fs.readFile(templateFile, function(err, content) {
            if (err) {
              err = new Error("Template not found: " + err.message);
              reject(err);
              return;
            }
            var compiled = handlebars.compile(content.toString());
  
            app.use(urlPath, function(req, res, next) {
              var data = {};
              if (dataGetter) {
                data = dataGetter(req);
                if (!data) {
                  return res.status(404).send("Not found");
                }
              }
              data["server_version"] =
                packageJson.name + " v" + packageJson.version;
              data["public_url"] = opts.publicUrl || "/";
              data["is_light"] = isLight;
              data["key_query_part"] = req.query.key
                ? "key=" + req.query.key + "&amp;"
                : "";
              data["key_query"] = req.query.key ? "?key=" + req.query.key : "";
              if (template === "wmts") res.set("Content-Type", "text/xml");
              return res.status(200).send(compiled(data));
            });
            resolve();
          });
        })
      );
    };
  
    serveTemplate("/$", "index", function(req) {
      var styles = clone(config.styles || {});
      Object.keys(styles).forEach(function(id) {
        var style = styles[id];
        style.name = (serving.styles[id] || serving.rendered[id] || {}).name;
        style.serving_data = serving.styles[id];
        style.serving_rendered = serving.rendered[id];
        if (style.serving_rendered) {
          var center = style.serving_rendered.center;
          if (center) {
            style.viewer_hash =
              "#" +
              center[2] +
              "/" +
              center[1].toFixed(5) +
              "/" +
              center[0].toFixed(5);
  
            var centerPx = mercator.px([center[0], center[1]], center[2]);
            style.thumbnail =
              center[2] +
              "/" +
              Math.floor(centerPx[0] / 256) +
              "/" +
              Math.floor(centerPx[1] / 256) +
              ".png";
          }
  
          var tiles = utils.getTileUrls(
            req,
            style.serving_rendered.tiles,
            "styles/" + id,
            style.serving_rendered.format,
            opts.publicUrl
          );
          style.xyz_link = tiles[0];
        }
      });
      var data = clone(serving.data || {});
      Object.keys(data).forEach(function(id) {
        var data_ = data[id];
        var center = data_.center;
        if (center) {
          data_.viewer_hash =
            "#" +
            center[2] +
            "/" +
            center[1].toFixed(5) +
            "/" +
            center[0].toFixed(5);
        }
        data_.is_vector = data_.format == "pbf";
        if (!data_.is_vector) {
          if (center) {
            var centerPx = mercator.px([center[0], center[1]], center[2]);
            data_.thumbnail =
              center[2] +
              "/" +
              Math.floor(centerPx[0] / 256) +
              "/" +
              Math.floor(centerPx[1] / 256) +
              "." +
              data_.format;
          }
  
          var tiles = utils.getTileUrls(
            req,
            data_.tiles,
            "data/" + id,
            data_.format,
            opts.publicUrl,
            {
              pbf: options.pbfAlias
            }
          );
          data_.xyz_link = tiles[0];
        }
        if (data_.filesize) {
          var suffix = "kB";
          var size = parseInt(data_.filesize, 10) / 1024;
          if (size > 1024) {
            suffix = "MB";
            size /= 1024;
          }
          if (size > 1024) {
            suffix = "GB";
            size /= 1024;
          }
          data_.formatted_filesize = size.toFixed(2) + " " + suffix;
        }
      });
      return {
        styles: Object.keys(styles).length ? styles : null,
        data: Object.keys(data).length ? data : null
      };
    });
  
    serveTemplate("/styles/:id/$", "viewer", function(req) {
      var id = req.params.id;
      var style = clone((config.styles || {})[id]);
      if (!style) {
        return null;
      }
      style.id = id;
      style.name = (serving.styles[id] || serving.rendered[id]).name;
      style.serving_data = serving.styles[id];
      style.serving_rendered = serving.rendered[id];
      return style;
    });
  
    /*
    app.use('/rendered/:id/$', function(req, res, next) {
      return res.redirect(301, '/styles/' + req.params.id + '/');
    });
    */
    serveTemplate("/styles/:id/wmts.xml", "wmts", function(req) {
      var id = req.params.id;
      var wmts = clone((config.styles || {})[id]);
      if (!wmts) {
        return null;
      }
      if (wmts.hasOwnProperty("serve_rendered") && !wmts.serve_rendered) {
        return null;
      }
      wmts.id = id;
      wmts.name = (serving.styles[id] || serving.rendered[id]).name;
      wmts.baseUrl =
        (req.get("X-Forwarded-Protocol")
          ? req.get("X-Forwarded-Protocol")
          : req.protocol) +
        "://" +
        req.get("host");
      return wmts;
    });
  
    serveTemplate("/data/:id/$", "data", function(req) {
      var id = req.params.id;
      var data = clone(serving.data[id]);
      if (!data) {
        return null;
      }
      data.id = id;
      data.is_vector = data.format == "pbf";
      return data;
    });
  
    var startupComplete = false;
    var startupPromise = Promise.all(startupPromises).then(function() {
      console.log("Startup complete");
      startupComplete = true;
    });
    app.get("/health", function(req, res, next) {
      if (startupComplete) {
        return res.status(200).send("OK");
      } else {
        return res.status(503).send("Starting");
      }
    });
  
    var server = app.listen(
      process.env.PORT || opts.port,
      process.env.BIND || opts.bind,
      function() {
        var address = this.address().address;
        if (address.indexOf("::") === 0) {
          address = "[" + address + "]"; // literal IPv6 address
        }
        console.log("Listening at http://%s:%d/", address, this.address().port);
      }
    );
  
    // add server.shutdown() to gracefully stop serving
    enableShutdown(server);
  
    return {
      app: app,
      server: server,
      startupPromise: startupPromise
    };
  }
  
  module.exports = function(opts) {
    var running = start(opts);
  
    running.startupPromise.catch(function(err) {
      console.error(err.message);
      process.exit(1);
    });
  
    process.on("SIGINT", function() {
      process.exit();
    });
  
    process.on("SIGHUP", function() {
      console.log("Stopping server and reloading config");
  
      running.server.shutdown(function() {
        for (var key in require.cache) {
          delete require.cache[key];
        }
  
        var restarted = start(opts);
        running.server = restarted.server;
        running.app = restarted.app;
      });
    });
  
    return running;
  };
  