[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintignore",
    "content": "/build/\n/config/\n/dist/\n/*.js\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "/* eslint-env node */\nmodule.exports = {\n  root: true,\n  extends: [\n    'plugin:vue/vue3-essential',\n    'eslint:recommended'\n  ],\n  rules: {\n    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'no-unused-vars': 1\n  },\n  env: {\n    'vue/setup-compiler-macros': true\n  }\n}"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: anvaka\npatreon: anvaka\ncustom: ['https://www.paypal.me/anvakos/3']\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules/\n/dist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\nstats.html"
  },
  {
    "path": "API.md",
    "content": "# Console API\n\n*This is work in progress and subject to change. Please don't rely on it for anything critical* \n\nThe `city-roads` provides additional set of operations for the software engineers, allowing them\nto execute arbitrary OpenStreetMap queries and visualize results.\n\n## Methods\n\nThis section describes available console API methods.\n\n### `scene.load()`\n\nAllows you to load more city roads into the current scene. Before we dive into details, let's explore what\nit takes to render Tokyo and Seattle next to each other. \n\n![Tokyo and Seattle](./images/tokyo_and_seattle.png)\n\nFirst, open [city roads](https://anvaka.github.io/city-roads/)\nand load `Seattle` roads. Then open [developer console](https://developers.google.com/web/tools/chrome-devtools/open) and run the following command:\n\n``` js\nscene.load(Query.Road, 'Tokyo'); // load every single road in Tokyo\n```\n\nMonitor your `Networks` tab and see when request is done. Tokyo bounding box is very large,\nso it will appear very far away on the top left corner. Let's move Tokyo grid next to Seattle:\n\n``` js\n// Find the loaded layer with Tokyo:\ntokyo = scene.queryLayer('Tokyo');\n\n// Exact offset numbers can be found by experimenting\ntokyo.moveBy(/* xOffset = */ 718000, /* yOffset = */ 745000)\n```\n\n`scene.load()` has the following signature:\n\n``` js\nfunction load(wayFilter: String, loadOptions: LoadOptions);\n```\n\n* `wayFilter` is used to filter out OpenStreetMap ways. You can find a list of well-known filters [here](https://github.com/anvaka/city-roads/blob/f543a712a0b88b12751aad691baa5eb9d6c0c664/src/lib/Query.js#L6-L24). If you need \nto know more to create custom filters, here is a complete [language guide](https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL). You can also get good insight into key/value distribution for ways by exploring [taginfo](https://taginfo.openstreetmap.org/tags) (make sure to sort by Ways in descending order to get the most popular combinations);\n* `loadOptions` allows you to have granular control over the bounding box of the loaded results. If this\nvalue is a string, then it is converted to a geocoded area id with nominatim, and then the first match\nis used as a bounding box. This may not be enough sometimes, so you can provide a specific area id, or \na bounding box, by passing an object. For example:\n\n``` js\nscene.load(Query.Road, {areaId: 3600237385}); // Explicitly set area id to Seattle\n\nscene.load(Query.Building, { // Load all buildings...\n  bbox: [       // ...in the given bounding box\n    \"-15.8477\", /* south lat */ \n    \"-47.9841\", /* west  lon */ \n    \"-15.7330\", /* north lat */ \n    \"-47.7970\"  /* east  lon */ \n  ]});\n```\n\n### scene.queryLayerAll()\n\nReturns all layers added to the scene. This is what it takes to assign different colors to each layer:\n\n``` js\nallLayers = scene.queryLayerAll()\nallLayers[0].color = 'deepskyblue'; // color can be a name.\nallLayers[1].color = 'rgb(255, 12, 43)'; // or a any other expression (rgb, hex, hsl, etc.)\n```\n\n### `scene.clear()`\n\nClears the current scene, allowing you to start from scratch.\n\n\n### `scene.saveToPNG(fileName: string)`\n\nTo save the current scene as a PNG file run\n\n``` js\nscene.saveToPNG('hello'); // hello.png is saved\n```\n\n### `scene.saveToSVG(fileName: string, options?: Object)`\n\nThis command allows you to save the scene as an SVG file.\n\n``` js\nscene.saveToSVG('hello'); // hello.svg is saved\n```\n\nIf you are planning to use a pen-plotter or a laser cutter, you can also\ngreatly reduce the print time, by removing very short paths from the final\nexport. To do so, pass `minLength` option:\n\n``` js\nscene.saveToSVG('hello', {minLength: 2}); \n// All paths with length shorter than 2px are removed from the final SVG.\n```\n\n## Examples\n\nHere are a few example of working with the API.\n\n### Loading all bikeways in the current city\n\n``` js\nvar bikes = scene.load('way[highway=\"cycleway\"]', {layer: scene.queryLayer()})\n// Make lines 4 pixels wide\nbikes.lineWidth = 4\n// and red\nbikes.color = 'red'\n```\n\n### Loading all bus routes in the current city\n\nThis script will get all bus routes in the current city, and render them 4px wide, with\nred color:\n\n``` js\nvar areaId = scene.queryLayer().getQueryBounds().areaId;\nvar bus = scene.load('', {\n  layer: scene.queryLayer(),\n  raw: `[out:json][timeout:250];\narea(${areaId});(._; )->.area;\n(nwr[route=bus](area.area););\nout body;>;out skel qt;`\n});\n\nbus.color='red';\nbus.lineWidth = 4;\n```\n\nIf you want a specific bus number, pass additional `ref=bus_number`. For example, bus route #24:\n\n``` js\nvar areaId = scene.queryLayer().getQueryBounds().areaId;\nvar bus = scene.load('', {\n  layer: scene.queryLayer(),\n  raw: `[out:json][timeout:250];\narea(${areaId});(._; )->.area;\n(nwr[route=bus][ref=24](area.area););\nout body;>;out skel qt;`\n});\n\nbus.color = 'green';\nbus.lineWidth = 4;\n```\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Andrei Kashcha\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# city-roads\n\nRender every single road in any city at once: https://anvaka.github.io/city-roads/\n\n![demo](https://i.imgur.com/6bFhX3e.png)\n\n## How it is made?\n\nThe data is fetched from OpenStreetMap using [overpass API](http://overpass-turbo.eu/). While that API\nis free (as long as you follow ODbL licenses), it can be rate-limited and sometimes it is slow. After all\nwe are downloading thousands of roads within an area!\n\nTo improve the performance of download, I indexed ~3,000 cities with population larger than 100,000 people and\nstored into a [very simple](https://github.com/anvaka/index-large-cities/blob/master/proto/place.proto) protobuf format. The cities are stored into a cache in this github [repository](https://github.com/anvaka/index-large-cities).\n\nThe name resolution is done by [nominatim](https://nominatim.openstreetmap.org/) - for any query that you type\ninto the search box it returns list of area ids. I check for the area id in my list of cached cities first,\nand fallback to overpass if area is not present in cache.\n\n## Scripting\n\nBehind simple UI software engineers would also find scripting capabilities. You can develop programs on top\nof the city-roads. A few examples are available in [city-script](https://github.com/anvaka/city-script). Scene\nAPI is documented here: https://github.com/anvaka/city-roads/blob/main/API.md\n\nPlease share your creations and do not hesitate to reach out if you have any questions.\n\n## Limitations\n\nThe rendering of the city is limited by the browser and video card memory capacity. I was able to render Seattle\nroads without a hiccup on a very old samsung phone, though when I tried Tokyo (with 1.4m segments) the phone\nwas very slow.\n\nSelecting area that has millions of roads (e.g. a Washington state) may cause the page to crash even on a\npowerful device.\n\nLuckily, most of the cities can be rendered without problems, resulting in a beautiful art.\n\n## Support\n\nIf you like this work and want to use it in your projects - you are more than welcome to do so!\n\nPlease [let me](https://twitter.com/anvaka) know how it goes. You can also sponsor my projects [here](https://github.com/sponsors/anvaka) - your funds will be dedicated to more awesome and free data visualizations.\n\n## Local development\n\n``` bash\n# install dependencies\nnpm install\n\n# serve with hot reload at localhost:8080\nnpm run dev\n\n# build for production with minification\nnpm run build\n\n# build for production and view the bundle analyzer report\nnpm run build --report\n```\n\n## License\n\nThe source code is licensed under MIT license\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "deploy.sh",
    "content": "#!/bin/sh\nrm -rf ./dist\nnpm run build\ncd ./dist\ngit init\ngit add .\ngit commit -m 'push to gh-pages'\ngit push --force git@github.com:anvaka/city-roads.git main:gh-pages\ncd ../\ngit tag `date \"+release-%Y%m%d%H%M%S\"`\ngit push --tags\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-TXT6313TGG\"></script>\n<script>\n  window.dataLayer = window.dataLayer || [];\n  function gtag(){dataLayer.push(arguments);}\n  gtag('js', new Date());\n\n  gtag('config', 'G-TXT6313TGG');\n</script>\n    <meta property=\"og:title\" content=\"Draw all roads in any city at once\" />\n    <meta property=\"og:image\" content=\"https://i.imgur.com/Fbbe5a6.png\" />\n    <meta property=\"og:description\" content=\"This website allows you to select a city and then draws every single road on a screen.\" />\n    <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" >\n    <meta charset=\"utf-8\">\n    <meta name=\"Description\" content=\"This website allows you to select a city and then draws every single road on a screen.\">\n    <meta name=\"keywords\" content=\"city roads visualization, roads, graph, visualization, anvaka\" />\n    <meta name=\"author\" content=\"Andrei Kashcha\">\n    <title>Draw all roads in a city at once</title>\n    <link href=\"https://fonts.googleapis.com/css?family=Roboto&display=swap\" rel=\"stylesheet\">\n\n    <style>\n      * {\n        box-sizing: border-box;\n      }\n      body {\n        background-color: #F7F2E8;\n        position: absolute;\n        overflow: hidden;\n        font-family: 'Avenir', Helvetica, Arial, sans-serif;\n        -webkit-font-smoothing: antialiased;\n        -moz-osx-font-smoothing: grayscale;\n\n        transition-timing-function: ease-out;\n        transition-property: background-color;\n        transition-duration: 3s;\n      }\n      #canvas, body{\n        margin: 0;\n        position: absolute;\n        width: 100%;\n        height: 100%;\n        top: 0;\n        bottom: 0;\n        left: 0;\n        right: 0;\n      }\n    </style>\n\n  </head>\n  <body>\n    <canvas id='canvas'></canvas>\n    <div id=\"host\"></div>\n\n    <script type=\"module\" src=\"/src/main.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"city-roads\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Visualization of all roads in a city\",\n  \"author\": \"Andrei Kashcha\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"start\": \"vite\",\n    \"lint\": \"eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore\"\n  },\n  \"dependencies\": {\n    \"d3-geo\": \"^3.0.1\",\n    \"d3-require\": \"^1.3.0\",\n    \"ngraph.events\": \"^1.2.1\",\n    \"pbf\": \"^3.2.1\",\n    \"query-state\": \"^4.3.0\",\n    \"tinycolor2\": \"^1.4.2\",\n    \"vue\": \"^3.2.37\",\n    \"w-gl\": \"^0.21.0\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^2.3.1\",\n    \"eslint\": \"^8.5.0\",\n    \"eslint-plugin-vue\": \"^8.2.0\",\n    \"rollup-plugin-visualizer\": \"^5.6.0\",\n    \"stylus\": \"^0.58.1\",\n    \"stylus-loader\": \"^7.0.0\",\n    \"vite\": \"^2.9.5\"\n  }\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <find-place v-if='!placeFound' @loaded='onGridLoaded'></find-place>\n  <div id=\"app\">\n    <div v-if='placeFound'>\n      <div class='controls'>\n        <a href=\"#\" class='print-button' @click.prevent='toggleSettings'>Customize...</a>\n        <a href=\"#\" class='try-another' @click.prevent='startOver'>Try another city</a>\n      </div>\n      <div v-if='showSettings' class='print-window'>\n        <h3>Display</h3>\n        <div class='row'>\n          <div class='col'>Colors</div>\n          <div class='col colors c-2'>\n            <div v-for='layer in layers' :key='layer.name' class='color-container'>\n              <color-picker v-model='layer.color' @change='layer.changeColor'></color-picker>\n              <div class='color-label'>{{layer.name}}</div>\n            </div>\n          </div>\n        </div>\n\n        <h3>Export</h3>\n        <div class='row'>\n          <a href='#' @click.prevent='zazzleMugPrint()' class='col'>Onto a mug</a> \n          <span class='col c-2'>\n            Print what you see onto a mug. <br/>Get a unique gift of your favorite city.\n          </span>\n        </div>\n        <div class='preview-actions message' v-if='zazzleLink || generatingPreview'>\n            <div v-if='zazzleLink' class='padded popup-help'>\n              If your browser has blocked the new window, <br/>please <a :href='zazzleLink' target='_blank'>click here</a>\n              to open it.\n            </div>\n            <div v-if='generatingPreview' class='loading-container'>\n              <loading-icon></loading-icon>\n              Generating preview url...\n            </div>\n        </div>\n        <div class='row'>\n          <a href='#'  @click.prevent='toPNGFile' class='col'>As an image (.png)</a> \n          <span class='col c-2'>\n            Save the current screen as a raster image.\n          </span>\n        </div>\n        \n        <div class='row'>\n          <a href='#'  @click.prevent='toSVGFile' class='col'>As a vector (.svg)</a> \n          <span class='col c-2'>\n            Save the current screen as a vector image.\n          </span>\n        </div>\n        <div v-if='false' class='row'>\n          <a href='#' @click.prevent='toProtobuf' class='col'>To a .PBF file</a> \n          <span class='col c-2'>\n            Save the current data as a protobuf message. For developer use only.\n          </span>\n        </div>\n\n        <h3>About</h3>\n        <div>\n          <p>This website was created by <a href='https://twitter.com/anvaka' target='_blank'>@anvaka</a>.\n          It downloads roads from OpenStreetMap and renders them with WebGL.\n          </p>\n          <p>\n           You can find the entire <a href='https://github.com/anvaka/city-roads'>source code here</a>. \n           If you love this website you can also <a href='https://www.paypal.com/paypalme2/anvakos/3'>buy me a coffee</a> or \n           <a href='https://www.patreon.com/anvaka'>support me on Patreon</a>, but you don't have to.\n          </p>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <editable-label v-if='placeFound' v-model='name' class='city-name' :printable='true' :style='{color: labelColorRGBA}' :overlay-manager='overlayManager'></editable-label>\n  <div v-if='placeFound' class='license printable can-drag' :style='{color: labelColorRGBA}'>data <a href='https://www.openstreetmap.org/about/' target=\"_blank\" :style='{color: labelColorRGBA}'>© OpenStreetMap</a></div>\n</template>\n\n<script>\nimport FindPlace from './components/FindPlace.vue';\nimport LoadingIcon from './components/LoadingIcon.vue';\nimport EditableLabel from './components/EditableLabel.vue';\nimport ColorPicker from './components/ColorPicker.vue';\nimport createScene from './lib/createScene.js';\nimport GridLayer from './lib/GridLayer.js';\nimport generateZazzleLink from './lib/getZazzleLink.js';\nimport appState from './lib/appState.js';\nimport {getPrintableCanvas, getCanvas} from './lib/saveFile.js';\nimport config from './config.js';\nimport './lib/canvas2BlobPolyfill.js';\nimport bus from './lib/bus.js';\nimport createOverlayManager from './createOverlayManager.js';\nimport tinycolor from 'tinycolor2';\n\nclass ColorLayer {\n  constructor(name, color, callback) {\n    this.name = name;\n    this.changeColor = callback;\n    this.color = color;\n  }\n}\n\nexport default {\n  name: 'App',\n  components: {\n    FindPlace,\n    LoadingIcon,\n    EditableLabel,\n    ColorPicker\n  },\n  data() {\n    return {\n      placeFound: false,\n      name: '',\n      zazzleLink: null,\n      generatingPreview: false,\n      showSettings: false,\n      settingsOpen: false,\n      labelColor: config.getLabelColor().toRgb(),\n      backgroundColor: config.getBackgroundColor().toRgb(),\n      layers: []\n    }\n  },\n  computed: {\n    labelColorRGBA() {\n      return toRGBA(this.labelColor);\n    }\n  },\n  created() {\n    bus.on('scene-transform', this.handleSceneTransform);\n    bus.on('background-color', this.syncBackground);\n    bus.on('line-color', this.syncLineColor);\n    this.overlayManager = createOverlayManager();\n  },\n  beforeUnmount() {\n    debugger;\n    this.overlayManager.dispose();\n    this.dispose();\n    bus.off('scene-transform', this.handleSceneTransform);\n    bus.off('background-color', this.syncBackground);\n    bus.off('line-color', this.syncLineColor);\n  },\n  methods: {\n    dispose() {\n      if (this.scene) {\n        this.scene.dispose();\n        window.scene = null;\n      }\n    },\n    toggleSettings() {\n      this.showSettings = !this.showSettings;\n    },\n    handleSceneTransform() {\n      this.zazzleLink = null;\n    },\n    onGridLoaded(grid) {\n      if (grid.isArea) {\n        appState.set('areaId', grid.id);\n        appState.unset('osm_id');\n        appState.unset('bbox');\n      } else if (grid.bboxString) {\n        appState.unset('areaId');\n        appState.set('osm_id', grid.id);\n        appState.set('bbox', grid.bboxString);\n      }\n      this.placeFound = true;\n      this.name = grid.name.split(',')[0];\n      let canvas = getCanvas();\n      canvas.style.visibility = 'visible';\n\n      this.scene = createScene(canvas);\n      this.scene.on('layer-added', this.updateLayers);\n      this.scene.on('layer-removed', this.updateLayers);\n\n      window.scene = this.scene;\n\n      let gridLayer = new GridLayer();\n      gridLayer.id = 'lines';\n      gridLayer.setGrid(grid);\n      this.scene.add(gridLayer)\n    },\n\n    startOver() {\n      appState.unset('areaId');\n      appState.unsetPlace();\n      appState.unset('q');\n      appState.enableCache();\n\n      this.dispose();\n      this.placeFound = false;\n      this.zazzleLink = null;\n      this.showSettings = false;\n      this.backgroundColor = config.getBackgroundColor().toRgb();\n      this.labelColor = config.getLabelColor().toRgb();\n\n      document.body.style.backgroundColor = config.getBackgroundColor().toRgbString();\n      getCanvas().style.visibility = 'hidden';\n    },\n\n    toPNGFile(e) {\n      scene.saveToPNG(this.name)\n    },\n\n    toSVGFile(e) { \n      scene.saveToSVG(this.name)\n    },\n\n    updateLayers() {\n      // TODO: This method likely doesn't belong here\n      let newLayers = [];\n      let lastLayer = 0;\n      let renderer = this.scene.getRenderer();\n      let root = renderer.getRoot();\n      root.children.forEach(layer => {\n        if (!layer.color) return;\n        let name = layer.id;\n        if (!name) {\n          lastLayer += 1;\n          name = 'lines ' + lastLayer;\n        }\n        let layerColor = tinycolor.fromRatio(layer.color);\n        newLayers.push(new ColorLayer(name, layerColor, newColor => {\n          this.zazzleLink = null;\n          layer.color = toRatioColor(newColor);\n          renderer.renderFrame();\n          this.scene.fire('color-change', layer);\n        }));\n      });\n\n      newLayers.push(\n        new ColorLayer('background', this.backgroundColor, this.setBackgroundColor),\n        new ColorLayer('labels', this.labelColor, newColor => this.labelColor = newColor)\n      );\n\n      this.layers = newLayers;\n\n      function toRatioColor(c) {\n        return {r: c.r/0xff, g: c.g/0xff, b: c.b/0xff, a: c.a}\n      }\n      this.zazzleLink = null;\n    },\n\n    syncLineColor() {\n      this.updateLayers();\n    },\n\n    syncBackground(newBackground) {\n      this.backgroundColor = newBackground.toRgb();\n      this.updateLayers()\n    },\n    // TODO: I need two background methods?\n    updateBackground() {\n      this.setBackgroundColor(this.backgroundColor)\n      this.zazzleLink = null;\n    },\n    setBackgroundColor(c) {\n      this.scene.background = c;\n      document.body.style.backgroundColor = toRGBA(c);\n      this.zazzleLink = null;\n    },\n\n    zazzleMugPrint() {\n      if (this.zazzleLink) {\n        window.open(this.zazzleLink, '_blank');\n        recordOpenClick(this.zazzleLink);\n        return;\n      }\n\n      this.generatingPreview = true;\n      getPrintableCanvas(this.scene).then(printableCanvas => {\n        generateZazzleLink(printableCanvas).then(link => {\n          this.zazzleLink = link;\n          window.open(link, '_blank');\n          recordOpenClick(link);\n          this.generatingPreview = false;\n        }).catch(e => {\n          this.error = e;\n          this.generatingPreview = false;\n        });\n      });\n    }\n  }\n}\n\nfunction toRGBA(c) {\n    return `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;\n}\n\nfunction recordOpenClick(link) {\n  if (typeof gtag === 'undefined') return;\n\n  gtag('event', 'click', {\n    'event_category': 'Outbound Link',\n    'event_label': link\n  });\n}\n</script>\n\n<style lang='stylus'>\n@import('./vars.styl');\n\n#app {\n  margin: 8px;\n  max-height: 100vh;\n  position: absolute;\n  z-index: 1;\n  h3 {\n    font-weight: normal;\n  }\n}\n\n.can-drag {\n  border: 1px solid transparent;\n}\n\n.drag-overlay {\n  position: fixed;\n  background: transparent;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n}\n\n.overlay-active {\n  border: 1px dashed highlight-color;\n}\n.overlay-active.exclusive {\n  border-style: solid;\n}\n\n.controls {\n  height: 48px;\n  background: white;\n  display: flex;\n  flex-direction: row;\n  align-items: stretch;\n  width: desktop-controls-width;\n  justify-content: space-around;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.2), 0 -1px 0px rgba(0,0,0,0.02);\n\n  a {\n    text-decoration: none;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    color: highlight-color;\n    margin: 0;\n    border: 0;\n    &:hover {\n      color: emphasis-background;\n      background: highlight-color;\n    }\n  }\n  a.try-another {\n    flex: 1;\n  }\n\n  a.print-button {\n    flex: 1;\n    border-right: 1px solid border-color;\n    &:focus {\n      border: 1px dashed highlight-color;\n    }\n  }\n}\n\n.col {\n    display: flex;\n    flex: 1;\n    select {\n      margin-left: 14px;\n    }\n  }\n.row {\n  margin-top: 4px;\n  display: flex;\n  flex-direction: row;\n  min-height: 32px;\n}\n.colors {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n\n  .color-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 64px;\n  }\n\n  .color-label {\n    font-size: 12px;\n  }\n}\n\na {\n  border: 1px solid transparent;\n  margin: -1px;\n  text-decoration: none;\n  color: highlight-color\n}\na:focus {\n  border: 1px dashed highlight-color;\n  outline: none;\n}\n.print-window {\n  max-height: calc(100vh - 48px);\n  overflow-y: auto;\n  border-top: 1px solid border-color;\n  background: white;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n  width: desktop-controls-width;\n  padding: 8px;\n  .row a {\n    margin-right: 4px;\n  }\n\n  h3 {\n    margin: 8px 0;\n    text-align: right;\n  }\n}\n\n.message {\n  border-top: 1px solid border-color\n  border-bottom: 1px solid border-color\n  background: #F5F5F5;\n}\n\n.preview-actions {\n  display: flex;\n  padding: 8px 0;\n  margin-left: -8px;\n  margin-bottom: 14px;\n  margin-top: 1px;\n  width: desktop-controls-width;\n  flex-direction: column;\n  align-items: stretch;\n  font-size: 14px;\n  align-items: center;\n  display: flex;\n\n  .popup-help {\n    text-align: center;\n  }\n}\n\n.city-name {\n  position: absolute;\n  right: 32px;\n  bottom: 54px;\n  font-size: 24px;\n  color: #434343;\n  input {\n    font-size: 24px;\n  }\n}\n\n.license {\n  text-align: right;\n  position: fixed;\n  font-family: labels-font;\n  right: 32px;\n  bottom: 32px;\n  font-size: 12px;\n  padding-right: 8px;\n  a {\n    text-decoration: none;\n    display: inline-block;\n  }\n}\n\n.c-2 {\n  flex: 2\n}\n\n@media (max-width: small-screen) {\n  #app {\n    width: 100%;\n    margin: 0;\n\n    .preview-actions,.error,\n    .controls, .print-window {\n      width: 100%;\n    }\n    .loading-container {\n      font-size: 12px;\n    }\n\n    .print-window {\n      font-size: 14px;\n    }\n\n  }\n  .city-name  {\n    right: 8px;\n    bottom: 24px;\n  }\n  .license  {\n    right: 8px;\n    bottom: 8px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/NoWebGL.vue",
    "content": "<template>\n  <div class='absolute no-webgl'>\n    <h3>WebGL is not enabled :(</h3>\n    <p>This website renders millions of roads at once.</p>\n    <p>\n      To render this amount of data fast, the website uses <a href='https://get.webgl.org/'>WebGL</a>,\n      which seem to be not supported by the device that you are using.\n    </p>\n    <p>Please try a different device to play with this website</p>\n    <img src=\"https://i.imgur.com/Fbbe5a6.png\" alt=\"demo\">\n  </div>\n</template>\n<style lang=\"stylus\" scoped>\n\n.no-webgl {\n    overflow-y: auto;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    margin: 20px auto;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    width: 100%;\n    height: 100%;\n    text-align: center;\n    padding: 8px;\n\n    h3 {\n      font-weight: normal;\n      font-size: 32px;\n      margin: 16px;\n    }\n    p {\n      max-width: 400px;\n      margin: 8px 0;\n    }\n    img {\n      width: 100%;\n      max-width: 1440px;\n    }\n}\n</style>"
  },
  {
    "path": "src/components/ColorPicker.vue",
    "content": "\n<template>\n<div class='vue-colorpicker' @click='showPicker = true' v-click-outside='hide' >\n  <span class='vue-colorpicker-btn' :style='btnStyle' ref='triggerButton'></span>\n  <div class='vue-colorpicker-panel' v-show='showPicker' :style=\"{left: panelLeft, top: panelTop}\">\n    <component :is='pickerType' :modelValue='colors' @update:modelValue='changeColor'></component>\n  </div>\n</div>\n</template>\n\n<script>\nimport tinycolor from 'tinycolor2'\nimport Sketch from './vue3-color/Sketch.vue'\nimport ClickOutside from './clickOutside.js'\n\nexport default {\n  name: 'vue-colorpicker',\n  components: {\n    'sketch-picker': Sketch,\n  },\n  directives: { ClickOutside },\n  props: {\n    modelValue: {\n      type: Object,\n    },\n  },\n  emits: ['update:modelValue', 'change'],\n  data () {\n    return {\n      showPicker: false,\n      colors: {\n        hex: '#FFFFFF',\n        a: 1\n      },\n      colorValue: '#FFFFFF',\n      panelLeft: '0px',\n      panelTop: '0px'\n    }\n  },\n  computed: {\n    pickerType () {\n      return 'sketch-picker';\n    },\n    isTransparent () {\n      return this.colors.a === 0;\n    },\n    btnStyle () {\n      if (this.isTransparent) {\n        return {\n          background: '#eee',\n          backgroundImage: 'linear-gradient(45deg, rgba(0,0,0,.25) 25%, transparent 0, transparent 75%,rgba(0,0,0,.25)0), linear-gradient(45deg, rgba(0,0,0,.25)25%,transparent 0, transparent 75%,rgba(0,0,0,.25)0)',\n          backgroundPosition: '0 0, 11px 11px',\n          backgroundSize: '22px 22px'\n        }\n      }\n      let {r, g, b, a} = this.colorValue;\n      return {\n        background: `rgba(${r}, ${g}, ${b}, ${a})`\n      }\n    },\n  },\n  watch: {\n    modelValue(val, oldVal) {\n      if (val !== oldVal) {\n        this.updateColorObject(val);\n      }\n    },\n    showPicker(newVal) {\n      if (!newVal) return;\n\n      const PICKER_WIDTH = 220;\n      const PANEL_HEIGHT = 320;\n      let triggerRect = this.$refs.triggerButton.getBoundingClientRect();\n      let desiredLeft = triggerRect.x;\n      let desiredTop = triggerRect.bottom;\n      if (triggerRect.y + PANEL_HEIGHT > window.innerHeight) {\n        desiredTop = Math.max(0, window.innerHeight - PANEL_HEIGHT);\n        desiredLeft += 36; // so that the selector button is still visible;\n      }\n      if (desiredLeft + PICKER_WIDTH > window.innerWidth) {\n        desiredLeft = Math.max(0, window.innerWidth - PICKER_WIDTH);\n      } \n      this.panelLeft = desiredLeft + 'px';\n      this.panelTop = desiredTop + 'px';\n    }\n  },\n\n  methods: {\n    hide () {\n      this.showPicker = false;\n    },\n    changeColor (data) {\n      this.colorValue = data.rgba;\n      this.$emit('update:modelValue', this.colorValue);\n      this.$emit('change', this.colorValue);\n    },\n    updateColorObject (color) {\n      if (!color) return\n      const colorObj = tinycolor(color);\n      if (!color || color === 'transparent') {\n        this.colors = {\n          hex: '#FFFFFF',\n          hsl: { h: 0, s: 0, l: 1, a: 0 },\n          hsv: { h: 0, s: 0, v: 1, a: 0 },\n          rgba: { r: 255, g: 255, b: 255, a: 0 },\n          a: 0\n        };\n      } else {\n        this.colors = {\n          hex: colorObj.toHexString(),\n          hsl: colorObj.toHsl(),\n          hsv: colorObj.toHsv(),\n          rgba: colorObj.toRgb(),\n          a: colorObj.getAlpha()\n        };\n      }\n      this.colorValue = this.colors.rgba;\n    }\n  },\n  mounted () {\n    this.updateColorObject(this.modelValue);\n  }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.vue-colorpicker {\n  display: inline-block;\n  box-sizing: border-box;\n  font-size: 0;\n  cursor: pointer;\n  &-btn {\n    display: inline-block;\n    width: 30px;\n    height: 22px;\n    border: 1px solid #666;\n    background: #FFFFFF;\n  }\n\n  .vue-colorpicker-panel {\n    position: absolute;\n    z-index: 1;\n  }\n}\n</style>"
  },
  {
    "path": "src/components/EditableLabel.vue",
    "content": "<template>\n  <div v-click-outside='looseFocus' class='can-drag'>\n    <div class='editable-label'>\n      <span :class='{printable}'>{{modelValue}}</span>\n      <input\n        v-bind:value=\"modelValue\"\n        v-on:input=\"$emit('update:modelValue', $event.target.value)\"\n        ref='input'\n      >\n    </div>\n  </div>\n</template>\n<script>\nimport ClickOutside from './clickOutside.js'\n\nexport default {\n  name: 'EditableLabel',\n  props: ['modelValue', 'printable', 'overlayManager'],\n  emits: ['update:modelValue'],\n  directives: { ClickOutside },\n  mounted() {\n    this.$el.receiveFocus = this.focus;\n    this.$el.style.pointerEvents = 'none';\n  },\n  methods: {\n    looseFocus() {\n      this.$refs.input.blur();\n    },\n    focus() {\n      this.$refs.input.focus();\n    }\n  }\n}\n</script>\n<style lang=\"stylus\">\n@import('../vars.styl');\n\n.editable-label {\n  position: relative;\n\n  span {\n    position: relative;\n    top: 0;\n    left: 0;\n    display: flex;\n    align-items : center;\n    font-family: labels-font;\n    white-space: pre;\n    padding: 8px;\n    border: 1px solid transparent;\n  }\n\n  input {\n    caret-color: primary-text;\n    color: transparent;\n    font-family: labels-font;\n    background: transparent;\n    display: flex;\n    align-items: center;\n    position: absolute;\n    overflow: hidden;\n    top: 0;\n    left: 0;\n    width: 100%;\n    padding: 8px;\n  }\n}\n  \n</style>"
  },
  {
    "path": "src/components/FindPlace.vue",
    "content": "<template>\n<div class='find-place' :class='{centered: boxInTheMiddle }'>\n  <div v-if='boxInTheMiddle'>\n    <h3 class='site-header'>city roads</h3>\n    <p class='description'>This website renders every single road within a city</p>\n  </div>\n  <form v-on:submit.prevent=\"onSubmit\" class='search-box'>\n      <input class='query-input' v-model='enteredInput' type='text' placeholder='Enter a city name to start' ref='input'>\n      <a type='submit' class='search-submit' href='#' @click.prevent='onSubmit' v-if='enteredInput && !hideInput'>{{mainActionText}}</a>\n  </form>\n  <div v-if='showWarning' class='prompt message note shadow'>\n    Note: Large cities may require 200MB+ of data transfer and may need a powerful device to render.\n  </div>\n  <div class='results' v-if='!loading'>\n    <div v-if='suggestionsLoaded && suggestions.length' class='suggestions shadow'>\n      <div class='prompt message'>\n        <div>Select boundaries below to download all roads within</div>\n        <div class='note'>large cities may require 200MB+ of data transfer and a powerful device</div>\n      </div>\n      <ul>\n        <li v-for='(suggestion, index) in suggestions' :key=\"index\">\n          <a @click.prevent='pickSuggestion(suggestion)' class='suggestion'\n          href='#'>\n          <span>\n          {{suggestion.name}} <small>({{suggestion.type}})</small>\n          </span>\n          </a>\n        </li>\n      </ul>\n    </div>\n    <div v-if='suggestionsLoaded && !suggestions.length && !loading && !error' class='no-results message shadow'>\n      Didn't find matching cities. Try a different query?\n    </div>\n    <div v-if='noRoads' class='no-results message shadow'>\n      Didn't find any roads. Try a different query?\n    </div>\n  </div>\n  <div v-if='error' class='error message shadow'>\n    <div v-if='isServerError(error)'>\n      <div>OpenStreetMap servers are busy or temporarily unavailable.</div>\n      <div class='error-note'>We tried {{error.serversAttempted || 'multiple'}} servers but none responded in time. This usually resolves within a few minutes.</div>\n      <div class='error-actions'>\n        <a href='#' @click.prevent=\"retry\" class='retry-btn'>Retry</a>\n      </div>\n    </div>\n    <div v-else>\n      <div>Sorry, something went wrong while loading data.</div>\n      <div class='error-note'>{{error.message || error.toString()}}</div>\n      <div class='error-actions'>\n        <a href='#' @click.prevent=\"retry\" class='retry-btn'>Retry</a>\n      </div>\n      <div class='error-links'>\n        <a href='https://twitter.com/anvaka/status/1218971717734789120' title='see what it supposed to do' target=\"_blank\">see how it should have worked</a>\n        <a :href='getBugReportURL(error)' :title='\"report error: \" + error' target='_blank'>report this bug</a>\n      </div>\n    </div>\n  </div>\n  <div v-if='loading' class='loading message shadow'>\n    <loading-icon></loading-icon>\n    <span>{{loading}}</span>\n    <a href=\"#\" @click.prevent='cancelRequest' class='cancel-request'>cancel</a>\n    <div class='load-padding' v-if='stillLoading > 0'>\n      Still loading...\n    </div>\n    <div class='load-padding' v-if='stillLoading > 1'>\n      Sorry it takes so long!\n    </div>\n  </div>\n</div>\n</template>\n\n<script>\nimport LoadingIcon from './LoadingIcon.vue';\nimport Query from '../lib/Query.js';\nimport request from '../lib/request.js';\nimport findBoundaryByName from '../lib/findBoundaryByName.js';\nimport appState from '../lib/appState.js';\nimport Grid from '../lib/Grid.js';\nimport queryState from '../lib/appState.js';\nimport config from '../config.js';\nimport Progress from '../lib/Progress.js'\nimport LoadOptions from '../lib/LoadOptions.js';\nimport Pbf from 'pbf';\nimport {place} from '../proto/place.js';\n\nconst FIND_TEXT = 'Find City Bounds';\n\nexport default {\n  name: 'FindPlace',\n  components: {\n    LoadingIcon\n  },\n  data () {\n    const enteredInput = appState.get('q') || '';\n    let hasValidArea = restoreStateFromQueryString();\n\n    return {\n      enteredInput,\n      loading: null,\n      lastCancel: null,\n      suggestionsLoaded: false,\n      boxInTheMiddle: true,\n      stillLoading: 0,\n      error: null,\n      hideInput: false,\n      noRoads: false,\n      clicked: false,\n      showWarning: hasValidArea, \n      mainActionText: hasValidArea ? 'Download Area' : FIND_TEXT,\n      suggestions: []\n    }\n  },\n  watch: {\n    enteredInput() {\n      // As soon as they change it, we need not to download:\n      this.mainActionText = FIND_TEXT;\n      this.showWarning = false;\n      this.hideInput = false;\n      appState.unsetPlace();\n    }\n  },\n  mounted() {\n    this.$refs.input.focus();\n    if (queryState.get('auto')) {\n      this.onSubmit();\n    }\n  },\n  beforeUnmount() {\n    if (this.lastCancel) this.lastCancel();\n    clearInterval(this.notifyStillLoading);\n  },\n  methods: {\n    onSubmit() {\n      queryState.set('q', this.enteredInput);\n      this.cancelRequest()\n      this.suggestions = [];\n      this.noRoads = false;\n      this.error = false;\n      this.showWarning = false;\n\n      const restoredState = restoreStateFromQueryString(this.enteredInput);\n      if (restoredState) {\n        this.pickSuggestion(restoredState);\n        return;\n      }\n\n      this.loading = 'Searching cities that match your query...'\n      findBoundaryByName(this.enteredInput)\n        .then(suggestions => {\n          this.loading = null;\n          this.hideInput = suggestions && suggestions.length;\n          if (this.boxInTheMiddle) {\n            // let animation that moves input box proceed a bit\n            this.boxInTheMiddle = false; // This triggers transition\n            // wait for it and then set the suggestions:\n            setTimeout(() => {\n              this.suggestionsLoaded = true;\n              this.suggestions = suggestions;\n            }, 50)\n          } else {\n              this.suggestionsLoaded = true;\n              this.suggestions = suggestions; \n          }\n        });\n    },\n\n    isServerError(error) {\n      if (!error) return false;\n      // These are transient server issues, not bugs in our code\n      return error.allServersFailed ||\n             error.invalidResponse ||\n             error.statusError ||\n             (error.message && error.message.includes('Failed to download'));\n    },\n\n    getBugReportURL(error) {\n      let title = encodeURIComponent('OSM Error');\n      let body = '';\n      if (error) {\n        body = 'Hello, an error occurred on the website:\\n\\n```\\n' +\n          error.toString() + '\\n```\\n\\n Can you please help?';\n      }\n\n      return `https://github.com/anvaka/city-roads/issues/new?title=${title}&body=${encodeURIComponent(body)}`\n    },\n\n    updateProgress(status) {\n      this.stillLoading = 0;\n      clearInterval(this.notifyStillLoading);\n      if (status.loaded < 0) {\n        this.loading = 'Trying a different server'\n        this.restartLoadingMonitor();\n        return;\n      }\n      if (status.percent !== undefined) {\n        this.loading = 'Loaded ' + Math.round(100 * status.percent) + '% (' + formatNumber(status.loaded) + ' bytes)...';\n      } else {\n        this.loading = 'Loaded ' + formatNumber(status.loaded) + ' bytes...';\n      }\n    },\n\n    retry() {\n      if (this.lastSuggestion) {\n        this.pickSuggestion(this.lastSuggestion);\n      }\n    },\n\n    pickSuggestion(suggestion) {\n      this.lastSuggestion = suggestion;\n      this.error = false;\n      if (appState.isCacheEnabled() && suggestion.areaId) {\n        this.checkCache(suggestion)\n          .catch(error => {\n            if (error.cancelled) return; // no need to do anything. They've cancelled\n\n            // No Cache - fallback\n            return this.useOSM(suggestion);\n          });\n      } else {\n        // we don't have cache for nodes yet.\n        this.useOSM(suggestion);\n      }\n    },\n\n    restartLoadingMonitor() {\n      clearInterval(this.notifyStillLoading);\n      this.stillLoading = 0;\n      this.notifyStillLoading = setInterval(() => {\n        this.stillLoading++;\n      }, 10000);\n    },\n\n    checkCache(suggestion) {\n      this.loading = 'Checking cache...'\n      let areaId = suggestion.areaId;\n\n      return request(config.areaServer + '/' + areaId + '.pbf', {\n        progress: this.generateNewProgressToken(),\n        responseType: 'arraybuffer'\n      }).then(arrayBuffer => {\n        var byteArray = new Uint8Array(arrayBuffer);\n        return byteArray;\n      }).then(byteArray => {\n        var pbf = new Pbf(byteArray);\n        var obj = place.read(pbf);\n        let grid = Grid.fromPBF(obj)\n        this.$emit('loaded', grid);\n      });\n    },\n\n    useOSM(suggestion) {\n      this.loading = 'Connecting to OpenStreetMap...'\n      \n      // it may take a while to load data. \n      this.restartLoadingMonitor();\n      Query.runFromOptions(new LoadOptions({\n        wayFilter: Query.Road,\n        areaId: suggestion.areaId,\n        bbox: suggestion.bbox\n      }), this.generateNewProgressToken())\n      .then(grid => {\n        this.loading = null;\n        if (!grid.hasRoads()) {\n          this.noRoads = true;\n        } else {\n          grid.setName(suggestion.name);\n          grid.setId(suggestion.areaId || suggestion.osm_id);\n          grid.setIsArea(suggestion.areaId); // osm nodes don't have area.\n          grid.setBBox(serializeBBox(suggestion.bbox));\n          this.$emit('loaded', grid);\n        }\n      }).catch(err => {\n        if (err.cancelled) {\n          this.loading = null;\n          return;\n        }\n        console.error(err);\n        this.error = err;\n        this.loading = null;\n        this.suggestions = [];\n      })\n      .finally(() => {\n        clearInterval(this.notifyStillLoading);\n        this.stillLoading = 0;\n      });\n    },\n\n    cancelRequest() {\n      if (this.progressToken) {\n        this.progressToken.cancel();\n        this.progressToken = null;\n        this.loading = false;\n      }\n    },\n\n    generateNewProgressToken() {\n      if (this.progressToken) {\n        this.progressToken.cancel();\n        this.progressToken = null;\n      }\n\n      this.progressToken = new Progress(this.updateProgress);\n      return this.progressToken;\n    }\n  }\n}\n\nfunction serializeBBox(bbox) {\n  return bbox && bbox.join(',');\n}\n\nfunction formatNumber(x) {\n  if (!Number.isFinite(x)) return 'N/A';\n  return x.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n}\n\nfunction restoreStateFromQueryString(name) {\n  let areaId = getCurrentAreaId();\n  if (areaId) {\n    return {name, areaId};\n  }\n\n  let nodeAndBox = getCurrentNodeAndBox();\n  if (nodeAndBox) {\n    return {\n      name,\n      osm_id: nodeAndBox.osm_id,\n      bbox: nodeAndBox.bbox\n    };\n  }\n}\n\nfunction getCurrentAreaId() {\n  let areaId = appState.get('areaId');\n  if (!Number.isFinite(Number.parseInt(areaId, 10))) {\n    areaId = null;\n  }\n  return areaId;\n}\n\nfunction getCurrentNodeAndBox() {\n  let osm_id = appState.get('osm_id');\n  if (!Number.isFinite(Number.parseInt(osm_id, 10))) return;\n\n  let bbox = parseBBox(appState.get('bbox'));\n  if (!bbox) return;\n\n  return { osm_id, bbox };\n}\n\nfunction parseBBox(bboxStr) {\n  if (!bboxStr) return null;\n\n  let bbox = bboxStr.split(',').map(x => Number.parseFloat(x)).filter(x => Number.isFinite(x));\n  return bbox.length === 4 ? bbox : null;\n}\n\n</script>\n\n<style lang=\"stylus\">\n@import('../vars.styl');\n.find-place  {\n  width: desktop-controls-width;\n}\n\nh3.site-header {\n  margin: 0;\n  font-weight: normal;\n  font-size: 32px;\n  text-align: center;\n}\n\ninput {\n  border: none;\n  flex: 1;\n  font-family: 'Avenir', Helvetica, Arial, sans-serif;\n  padding: 0;\n  color: #434343;\n  height: 100%;\n  font-size: 16px;\n  &:focus {\n    outline: none;\n  }\n}\n\n.search-box {\n  position: relative;\n  background-color: emphasis-background;\n  padding: 0 8px;\n  padding: 0 0 0 8px;\n\n  box-shadow: 0 2px 4px rgba(0,0,0,0.2), 0 -1px 0px rgba(0,0,0,0.02);\n  height: 48px;\n  display: flex;\n  font-size: 16px;\n  cursor: text;\n  a {\n    cursor: pointer;\n  }\n  span {\n    display: flex;\n    align-items: center;\n    flex-shrink: 0;\n  }\n}\n\n.prompt {\n  padding: 4px;\n  text-align: center;\n  font-size: 12px;\n}\n\n.search-submit {\n  padding: 0 8px;\n  align-items: center;\n  text-decoration: none;\n  display: flex;\n  flex-shrink: 0;\n  justify-content: center;\n  outline: none;\n  z-index: 1;\n  color: highlight-color\n  &:hover {\n    color: emphasis-background;\n    background: highlight-color;\n  }\n}\n\n.suggestion {\n  display: block\n  min-height: 64px\n  align-items: center;\n  border-bottom: 1px solid border-color\n  display: flex\n  padding: 0 10px;\n  text-decoration: none\n  color: highlight-color\n}\n\n.suggestions {\n  position: relative;\n  background: white\n  .note {\n    font-size: 10px;\n    font-style: italic;\n  }\n\n  ul {\n    list-style-type: none;\n    margin: 0;\n    padding: 0;\n    max-height: calc(100vh - 128px);\n    overflow-y: auto;\n    overflow-x: hidden;\n  }\n}\n\n.message,\n.loading {\n  padding: 4px 8px;\n  position: relative;\n}\n.loading svg {\n  margin-right: 8px;\n}\n\n.shadow {\n  box-shadow: 0 2px 4px rgba(0,0,0,0.2)\n}\n\n.error {\n  overflow-x: auto;\n}\n\n.find-place {\n  position: absolute;\n  display: flex;\n  flex-direction: column;\n  top: 8px;\n  left: 50%;\n\n  transform: translateX(-50%) translateY(0);\n  transition-timing-function: ease-out;\n  transition-property: top left transform;\n  transition-duration: 0.2s;\n}\n\n.find-place.centered {\n  top: 50%;\n  left: 50%;\n  transform: translateX(-50%) translateY(-143px);\n}\n.load-padding {\n  padding-left: 16px;\n}\n.description {\n  padding: 8px;\n  margin: 0;\n  text-align: center;\n}\n\n.cancel-request {\n  position: absolute;\n  right: 4px;\n  top: 4px;\n  font-size: 12px;\n}\n.error-note {\n  font-size: 12px;\n  color: #666;\n  margin: 8px 0;\n}\n.error-actions {\n  margin: 12px 0 8px 0;\n}\n.retry-btn {\n  display: inline-block;\n  padding: 8px 24px;\n  background: highlight-color;\n  color: white;\n  text-decoration: none;\n  border-radius: 4px;\n  font-size: 14px;\n  &:hover {\n    opacity: 0.9;\n  }\n}\n.error-links {\n  display: flex;\n  justify-content: space-between;\n  font-size: 12px;\n  margin-top: 12px;\n  padding-top: 8px;\n  border-top: 1px solid border-color;\n}\n\n@media (max-width: small-screen) {\n  .find-place {\n    width: 100%;\n  }\n  .find-place.centered {\n    top: 8px;\n    left: 0;\n    transform: none;\n  }\n  .message {\n    font-size: 12px;\n  }\n  .prompt {\n    font-size: 12px;\n    .note {\n      font-size: 9px;\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/components/LoadingIcon.vue",
    "content": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" width=\"12\" height=\"12\" fill=\"black\" class='loader'>\n      <path opacity=\".25\" d=\"M16 0 A16 16 0 0 0 16 32 A16 16 0 0 0 16 0 M16 4 A12 12 0 0 1 16 28 A12 12 0 0 1 16 4\"/>\n      <path d=\"M16 0 A16 16 0 0 1 32 16 L28 16 A12 12 0 0 0 16 4z\">\n        <animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0 16 16\" to=\"360 16 16\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n      </path>\n  </svg>\n</template>"
  },
  {
    "path": "src/components/clickOutside.js",
    "content": "// Based on https://github.com/ElemeFE/element/blob/dev/src/utils/clickoutside.js\n// The MIT License (MIT), Copyright (c) 2016 ElemeFE\n// (C) 2022 anvaka\nconst nodeList = [];\nconst ctx = '@@clickoutsideContext';\n\nlet startClick;\nlet seed = 0;\n\ndocument.addEventListener('mousedown', e => (startClick = e), true);\ndocument.addEventListener('mouseup', e => {\n  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));\n}, true);\n\n// Also hide when tapped outside.\ndocument.addEventListener('touchstart', e => {\n  startClick = e;\n}, true);\ndocument.addEventListener('touchend', e => {\n  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));\n}, true);\n\nfunction createDocumentHandler(el, binding, vnode) {\n  return function(mouseup = {}, mousedown = {}) {\n    if (!vnode || !mouseup.target || !mousedown.target ||\n      el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target) return;\n\n    const methodName = el[ctx].handler;\n    if (methodName) methodName()\n  };\n}\n\nexport default {\n  created(el, binding, vnode) {\n    nodeList.push(el);\n    const id = seed++;\n    el[ctx] = {\n      id,\n      documentHandler: createDocumentHandler(el, binding, vnode),\n      handler: binding.value\n    };\n  },\n\n  updated(el, binding, vnode) {\n    el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);\n    el[ctx].handler = binding.value;\n  },\n\n  unmounted(el) {\n    let len = nodeList.length;\n\n    for (let i = 0; i < len; i++) {\n      if (nodeList[i][ctx].id === el[ctx].id) {\n        nodeList.splice(i, 1);\n        break;\n      }\n    }\n    delete el[ctx];\n  }\n};"
  },
  {
    "path": "src/components/vue3-color/LICENSE",
    "content": "Based on @lk77/vue3-color which is licensed under The MIT License.\n\n"
  },
  {
    "path": "src/components/vue3-color/Sketch.vue",
    "content": "<template>\n  <div role=\"application\" aria-label=\"Sketch color picker\" :class=\"['vc-sketch', disableAlpha ? 'vc-sketch__disable-alpha' : '']\">\n    <div class=\"vc-sketch-saturation-wrap\">\n      <saturation v-model=\"colors\" @change=\"childChange\"></saturation>\n    </div>\n    <div class=\"vc-sketch-controls\">\n      <div class=\"vc-sketch-sliders\">\n        <div class=\"vc-sketch-hue-wrap\">\n          <hue v-model=\"colors\" @change=\"childChange\"></hue>\n        </div>\n        <div class=\"vc-sketch-alpha-wrap\" v-if=\"!disableAlpha\">\n          <alpha v-model=\"colors\" @change=\"childChange\"></alpha>\n        </div>\n      </div>\n      <div class=\"vc-sketch-color-wrap\">\n        <div :aria-label=\"`Current color is ${activeColor}`\" class=\"vc-sketch-active-color\" :style=\"{background: activeColor}\"></div>\n        <checkboard></checkboard>\n      </div>\n    </div>\n    <div class=\"vc-sketch-field\" v-if=\"!disableFields\">\n      <!-- rgba -->\n      <div class=\"vc-sketch-field--double\">\n        <ed-in label=\"hex\" :modelValue=\"hex\" @change=\"inputChange\"></ed-in>\n      </div>\n      <div class=\"vc-sketch-field--single\">\n        <ed-in label=\"r\" :modelValue=\"colors.rgba.r\" @change=\"inputChange\"></ed-in>\n      </div>\n      <div class=\"vc-sketch-field--single\">\n        <ed-in label=\"g\" :modelValue=\"colors.rgba.g\" @change=\"inputChange\"></ed-in>\n      </div>\n      <div class=\"vc-sketch-field--single\">\n        <ed-in label=\"b\" :modelValue=\"colors.rgba.b\" @change=\"inputChange\"></ed-in>\n      </div>\n      <div class=\"vc-sketch-field--single\" v-if=\"!disableAlpha\">\n        <ed-in label=\"a\" :modelValue=\"colors.a\" :arrow-offset=\"0.01\" :max=\"1\" @change=\"inputChange\"></ed-in>\n      </div>\n    </div>\n    <div class=\"vc-sketch-presets\" role=\"group\" aria-label=\"A color preset, pick one to set as current color\">\n      <template v-for=\"c in presetColors\">\n        <div\n          v-if=\"!isTransparent(c)\"\n          class=\"vc-sketch-presets-color\"\n          :aria-label=\"'Color:' + c\"\n          :key=\"'if-' + c\"\n          :style=\"{background: c}\"\n          @click=\"handlePreset(c)\">\n        </div>\n        <div\n          v-else\n          :key=\"'else-' + c\"\n          :aria-label=\"'Color:' + c\"\n          class=\"vc-sketch-presets-color\"\n          @click=\"handlePreset(c)\">\n          <checkboard />\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n\n<script>\nimport colorMixin from './mixin/color.js'\nimport editableInput from './common/EditableInput.vue'\nimport saturation from './common/Saturation.vue'\nimport hue from './common/Hue.vue'\nimport alpha from './common/Alpha.vue'\nimport checkboard from './common/Checkboard.vue'\n\nconst presetColors = [\n  '#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321',\n  '#417505', '#BD10E0', '#9013FE', '#4A90E2', '#50E3C2',\n  '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF',\n  'rgba(0,0,0,0)'\n]\n\nexport default {\n  name: 'Sketch',\n  mixins: [colorMixin],\n  components: {\n    saturation,\n    hue,\n    alpha,\n    'ed-in': editableInput,\n    checkboard\n  },\n  props: {\n    presetColors: {\n      type: Array,\n      default () {\n        return presetColors\n      }\n    },\n    disableAlpha: {\n      type: Boolean,\n      default: false\n    },\n    disableFields: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    hex () {\n      let hex\n      if (this.colors.a < 1) {\n        hex = this.colors.hex8\n      } else {\n        hex = this.colors.hex\n      }\n      return hex.replace('#', '')\n    },\n    activeColor () {\n      const rgba = this.colors.rgba\n      return 'rgba(' + [rgba.r, rgba.g, rgba.b, rgba.a].join(',') + ')'\n    }\n  },\n  methods: {\n    handlePreset (c) {\n      this.colorChange({\n        hex: c,\n        source: 'hex'\n      })\n    },\n    childChange (data) {\n      this.colorChange(data)\n    },\n    inputChange (data) {\n      if (!data) {\n        return\n      }\n      if (data.hex) {\n        this.isValidHex(data.hex) && this.colorChange({\n          hex: data.hex,\n          source: 'hex'\n        })\n      } else if (data.r || data.g || data.b || data.a) {\n        this.colorChange({\n          r: data.r || this.colors.rgba.r,\n          g: data.g || this.colors.rgba.g,\n          b: data.b || this.colors.rgba.b,\n          a: data.a || this.colors.rgba.a,\n          source: 'rgba'\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"css\">\n.vc-sketch {\n  position: relative;\n  width: 200px;\n  padding: 10px 10px 0;\n  box-sizing: initial;\n  background: #fff;\n  border-radius: 4px;\n  box-shadow: 0 0 0 1px rgba(0, 0, 0, .15), 0 8px 16px rgba(0, 0, 0, .15);\n}\n\n.vc-sketch-saturation-wrap {\n  width: 100%;\n  padding-bottom: 75%;\n  position: relative;\n  overflow: hidden;\n}\n\n.vc-sketch-controls {\n  display: flex;\n}\n\n.vc-sketch-sliders {\n  padding: 4px 0;\n  flex: 1;\n}\n\n.vc-sketch-sliders .vc-hue,\n.vc-sketch-sliders .vc-alpha-gradient {\n  border-radius: 2px;\n}\n\n.vc-sketch-hue-wrap {\n  position: relative;\n  height: 10px;\n}\n\n.vc-sketch-alpha-wrap {\n  position: relative;\n  height: 10px;\n  margin-top: 4px;\n  overflow: hidden;\n}\n\n.vc-sketch-color-wrap {\n  width: 24px;\n  height: 24px;\n  position: relative;\n  margin-top: 4px;\n  margin-left: 4px;\n  border-radius: 3px;\n}\n\n.vc-sketch-active-color {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  border-radius: 2px;\n  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15), inset 0 0 4px rgba(0, 0, 0, .25);\n  z-index: 2;\n}\n\n.vc-sketch-color-wrap .vc-checkerboard {\n  background-size: auto;\n}\n\n.vc-sketch-field {\n  display: flex;\n  padding-top: 4px;\n}\n\n.vc-sketch-field .vc-input__input {\n  width: 90%;\n  padding: 4px 0 3px 10%;\n  border: none;\n  box-shadow: inset 0 0 0 1px #ccc;\n  font-size: 10px;\n}\n\n.vc-sketch-field .vc-input__label {\n  display: block;\n  text-align: center;\n  font-size: 11px;\n  color: #222;\n  padding-top: 3px;\n  padding-bottom: 4px;\n  text-transform: capitalize;\n}\n\n.vc-sketch-field--single {\n  flex: 1;\n  padding-left: 6px;\n}\n\n.vc-sketch-field--double {\n  flex: 2;\n}\n\n.vc-sketch-presets {\n  margin-right: -10px;\n  margin-left: -10px;\n  padding-left: 10px;\n  padding-top: 10px;\n  border-top: 1px solid #eee;\n}\n\n.vc-sketch-presets-color {\n  border-radius: 3px;\n  overflow: hidden;\n  position: relative;\n  display: inline-block;\n  margin: 0 10px 10px 0;\n  vertical-align: top;\n  cursor: pointer;\n  width: 16px;\n  height: 16px;\n  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15);\n}\n\n.vc-sketch-presets-color .vc-checkerboard {\n  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15);\n  border-radius: 3px;\n}\n\n.vc-sketch__disable-alpha .vc-sketch-color-wrap {\n  height: 10px;\n}\n</style>\n"
  },
  {
    "path": "src/components/vue3-color/common/Alpha.vue",
    "content": "<template>\n  <div class=\"vc-alpha\">\n    <div class=\"vc-alpha-checkboard-wrap\">\n      <checkboard></checkboard>\n    </div>\n    <div class=\"vc-alpha-gradient\" :style=\"{background: gradientColor}\"></div>\n    <div class=\"vc-alpha-container\" ref=\"container\"\n        @mousedown=\"handleMouseDown\"\n        @touchmove=\"handleChange\"\n        @touchstart=\"handleChange\">\n      <div class=\"vc-alpha-pointer\" :style=\"{left: colors.a * 100 + '%'}\">\n        <div class=\"vc-alpha-picker\"></div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport checkboard from './Checkboard.vue'\n\nexport default {\n  name: 'Alpha',\n  props: {\n    modelValue: Object,\n    onChange: Function\n  },\n  components: {\n    checkboard\n  },\n  computed: {\n    colors () {\n      return this.modelValue\n    },\n    gradientColor () {\n      const rgba = this.colors.rgba\n      const rgbStr = [rgba.r, rgba.g, rgba.b].join(',')\n      return 'linear-gradient(to right, rgba(' + rgbStr + ', 0) 0%, rgba(' + rgbStr + ', 1) 100%)'\n    }\n  },\n  methods: {\n    handleChange (e, skip) {\n      !skip && e.preventDefault()\n      const container = this.$refs.container\n      if (!container) {\n        // for some edge cases, container may not exist. see #220\n        return\n      }\n      const containerWidth = container.clientWidth\n\n      const xOffset = container.getBoundingClientRect().left + window.pageXOffset\n      const pageX = e.pageX || (e.touches ? e.touches[0].pageX : 0)\n      const left = pageX - xOffset\n\n      let a\n      if (left < 0) {\n        a = 0\n      } else if (left > containerWidth) {\n        a = 1\n      } else {\n        a = Math.round(left * 100 / containerWidth) / 100\n      }\n\n      if (this.colors.a !== a) {\n        this.$emit('change', {\n          h: this.colors.hsl.h,\n          s: this.colors.hsl.s,\n          l: this.colors.hsl.l,\n          a: a,\n          source: 'rgba'\n        })\n      }\n    },\n    handleMouseDown (e) {\n      this.handleChange(e, true)\n      window.addEventListener('mousemove', this.handleChange)\n      window.addEventListener('mouseup', this.handleMouseUp)\n    },\n    handleMouseUp () {\n      this.unbindEventListeners()\n    },\n    unbindEventListeners () {\n      window.removeEventListener('mousemove', this.handleChange)\n      window.removeEventListener('mouseup', this.handleMouseUp)\n    }\n  }\n}\n\n</script>\n\n<style lang=\"css\">\n.vc-alpha {\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  left: 0px;\n}\n.vc-alpha-checkboard-wrap {\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  left: 0px;\n  overflow: hidden;\n}\n.vc-alpha-gradient {\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  left: 0px;\n}\n.vc-alpha-container {\n  cursor: pointer;\n  position: relative;\n  z-index: 2;\n  height: 100%;\n  margin: 0 3px;\n}\n.vc-alpha-pointer {\n  z-index: 2;\n  position: absolute;\n}\n.vc-alpha-picker {\n  cursor: pointer;\n  width: 4px;\n  border-radius: 1px;\n  height: 8px;\n  box-shadow: 0 0 2px rgba(0, 0, 0, .6);\n  background: #fff;\n  margin-top: 1px;\n  transform: translateX(-2px);\n}\n</style>\n"
  },
  {
    "path": "src/components/vue3-color/common/Checkboard.vue",
    "content": "<template>\n  <div class=\"vc-checkerboard\" :style=\"bgStyle\"></div>\n</template>\n\n<script>\nconst _checkboardCache = {}\n\nexport default {\n  name: 'Checkboard',\n  props: {\n    size: {\n      type: [Number, String],\n      default: 8\n    },\n    white: {\n      type: String,\n      default: '#fff'\n    },\n    grey: {\n      type: String,\n      default: '#e6e6e6'\n    }\n  },\n  computed: {\n    bgStyle () {\n      return {\n        'background-image': 'url(' + getCheckboard(this.white, this.grey, this.size) + ')'\n      }\n    }\n  }\n}\n\n/**\n * get base 64 data by canvas\n *\n * @param {String} c1 hex color\n * @param {String} c2 hex color\n * @param {Number} size\n */\n\nfunction renderCheckboard (c1, c2, size) {\n  // Dont Render On Server\n  if (typeof document === 'undefined') {\n    return null\n  }\n  const canvas = document.createElement('canvas')\n  canvas.width = canvas.height = size * 2\n  const ctx = canvas.getContext('2d')\n  // If no context can be found, return early.\n  if (!ctx) {\n    return null\n  }\n  ctx.fillStyle = c1\n  ctx.fillRect(0, 0, canvas.width, canvas.height)\n  ctx.fillStyle = c2\n  ctx.fillRect(0, 0, size, size)\n  ctx.translate(size, size)\n  ctx.fillRect(0, 0, size, size)\n  return canvas.toDataURL()\n}\n\n/**\n * get checkboard base data and cache\n *\n * @param {String} c1 hex color\n * @param {String} c2 hex color\n * @param {Number} size\n */\n\nfunction getCheckboard (c1, c2, size) {\n  const key = c1 + ',' + c2 + ',' + size\n\n  if (_checkboardCache[key]) {\n    return _checkboardCache[key]\n  } else {\n    const checkboard = renderCheckboard(c1, c2, size)\n    _checkboardCache[key] = checkboard\n    return checkboard\n  }\n}\n\n</script>\n\n<style lang=\"css\">\n.vc-checkerboard {\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  left: 0px;\n  background-size: contain;\n}\n</style>\n"
  },
  {
    "path": "src/components/vue3-color/common/EditableInput.vue",
    "content": "<template>\n  <div class=\"vc-editable-input\">\n    <input\n      :aria-labelledby=\"labelId\"\n      class=\"vc-input__input\"\n      v-model=\"val\"\n      @keydown=\"handleKeyDown\"\n      @input=\"update\"\n      ref=\"input\"\n    >\n    <span :for=\"label\" class=\"vc-input__label\" :id=\"labelId\">{{labelSpanText}}</span>\n    <span class=\"vc-input__desc\">{{desc}}</span>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'editableInput',\n  props: {\n    label: String,\n    labelText: String,\n    desc: String,\n    modelValue: [String, Number],\n    max: Number,\n    min: Number,\n    arrowOffset: {\n      type: Number,\n      default: 1\n    }\n  },\n  computed: {\n    val: {\n      get () {\n        return this.modelValue\n      },\n      set (v) {\n        // TODO: min\n        if (!(this.max === undefined) && +v > this.max) {\n          this.$refs.input.value = this.max\n        } else {\n          return v\n        }\n      }\n    },\n    labelId () {\n      return `input__label__${this.label}__${Math.random().toString().slice(2, 5)}`\n    },\n    labelSpanText () {\n      return this.labelText || this.label\n    }\n  },\n  methods: {\n    update (e) {\n      this.handleChange(e.target.value)\n    },\n    handleChange (newVal) {\n      const data = {}\n      data[this.label] = newVal\n      if (data.hex === undefined && data['#'] === undefined) {\n        this.$emit('change', data)\n      } else if (newVal.length > 5) {\n        this.$emit('change', data)\n      }\n    },\n    // **** unused\n    // handleBlur (e) {\n    //   console.log(e)\n    // },\n    handleKeyDown (e) {\n      let val = this.val\n      const number = Number(val)\n\n      if (number) {\n        const amount = this.arrowOffset || 1\n\n        // Up\n        if (e.keyCode === 38) {\n          val = number + amount\n          this.handleChange(val)\n          e.preventDefault()\n        }\n\n        // Down\n        if (e.keyCode === 40) {\n          val = number - amount\n          this.handleChange(val)\n          e.preventDefault()\n        }\n      }\n    }\n    // **** unused\n    // handleDrag (e) {\n    //   console.log(e)\n    // },\n    // handleMouseDown (e) {\n    //   console.log(e)\n    // }\n  }\n}\n</script>\n\n<style lang=\"css\">\n.vc-editable-input {\n  position: relative;\n}\n.vc-input__input {\n  padding: 0;\n  border: 0;\n  outline: none;\n}\n.vc-input__label {\n  text-transform: capitalize;\n}\n</style>\n"
  },
  {
    "path": "src/components/vue3-color/common/Hue.vue",
    "content": "<template>\n  <div :class=\"['vc-hue', directionClass]\">\n    <div class=\"vc-hue-container\"\n      role=\"slider\"\n      :aria-valuenow=\"colors.hsl.h\"\n      aria-valuemin=\"0\"\n      aria-valuemax=\"360\"\n      ref=\"container\"\n      @mousedown=\"handleMouseDown\"\n      @touchmove=\"handleChange\"\n      @touchstart=\"handleChange\">\n      <div class=\"vc-hue-pointer\" :style=\"{top: pointerTop, left: pointerLeft}\" role=\"presentation\">\n        <div class=\"vc-hue-picker\"></div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Hue',\n  props: {\n    modelValue: Object,\n    direction: {\n      type: String,\n      // [horizontal | vertical]\n      default: 'horizontal'\n    }\n  },\n  data () {\n    return {\n      oldHue: 0,\n      pullDirection: ''\n    }\n  },\n  computed: {\n    colors () {\n      const h = this.modelValue.hsl.h\n      // vue/no-side-effects-in-computed-properties\n      /* eslint-disable */\n      if (h !== 0 && h - this.oldHue > 0) this.pullDirection = 'right'\n      if (h !== 0 && h - this.oldHue < 0) this.pullDirection = 'left'\n      this.oldHue = h\n      /* eslint-enable */\n\n      return this.modelValue\n    },\n    directionClass () {\n      return {\n        'vc-hue--horizontal': this.direction === 'horizontal',\n        'vc-hue--vertical': this.direction === 'vertical'\n      }\n    },\n    pointerTop () {\n      if (this.direction === 'vertical') {\n        if (this.colors.hsl.h === 0 && this.pullDirection === 'right') return 0\n        return -((this.colors.hsl.h * 100) / 360) + 100 + '%'\n      } else {\n        return 0\n      }\n    },\n    pointerLeft () {\n      if (this.direction === 'vertical') {\n        return 0\n      } else {\n        if (this.colors.hsl.h === 0 && this.pullDirection === 'right') return '100%'\n        return (this.colors.hsl.h * 100) / 360 + '%'\n      }\n    }\n  },\n  methods: {\n    handleChange (e, skip) {\n      !skip && e.preventDefault()\n\n      const container = this.$refs.container\n      if (!container) {\n        // for some edge cases, container may not exist. see #220\n        return\n      }\n      const containerWidth = container.clientWidth\n      const containerHeight = container.clientHeight\n\n      const xOffset = container.getBoundingClientRect().left + window.pageXOffset\n      const yOffset = container.getBoundingClientRect().top + window.pageYOffset\n      const pageX = e.pageX || (e.touches ? e.touches[0].pageX : 0)\n      const pageY = e.pageY || (e.touches ? e.touches[0].pageY : 0)\n      const left = pageX - xOffset\n      const top = pageY - yOffset\n\n      let h\n      let percent\n\n      if (this.direction === 'vertical') {\n        if (top < 0) {\n          h = 360\n        } else if (top > containerHeight) {\n          h = 0\n        } else {\n          percent = -(top * 100 / containerHeight) + 100\n          h = (360 * percent / 100)\n        }\n\n        if (this.colors.hsl.h !== h) {\n          this.$emit('change', {\n            h: h,\n            s: this.colors.hsl.s,\n            l: this.colors.hsl.l,\n            a: this.colors.hsl.a,\n            source: 'hsl'\n          })\n        }\n      } else {\n        if (left < 0) {\n          h = 0\n        } else if (left > containerWidth) {\n          h = 360\n        } else {\n          percent = left * 100 / containerWidth\n          h = (360 * percent / 100)\n        }\n\n        if (this.colors.hsl.h !== h) {\n          this.$emit('change', {\n            h: h,\n            s: this.colors.hsl.s,\n            l: this.colors.hsl.l,\n            a: this.colors.hsl.a,\n            source: 'hsl'\n          })\n        }\n      }\n    },\n    handleMouseDown (e) {\n      this.handleChange(e, true)\n      window.addEventListener('mousemove', this.handleChange)\n      window.addEventListener('mouseup', this.handleMouseUp)\n    },\n    handleMouseUp (e) {\n      this.unbindEventListeners()\n    },\n    unbindEventListeners () {\n      window.removeEventListener('mousemove', this.handleChange)\n      window.removeEventListener('mouseup', this.handleMouseUp)\n    }\n  }\n}\n</script>\n\n<style lang=\"css\">\n.vc-hue {\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  left: 0px;\n  border-radius: 2px;\n}\n.vc-hue--horizontal {\n  background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);\n}\n.vc-hue--vertical {\n  background: linear-gradient(to top, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);\n}\n.vc-hue-container {\n  cursor: pointer;\n  margin: 0 2px;\n  position: relative;\n  height: 100%;\n}\n.vc-hue-pointer {\n  z-index: 2;\n  position: absolute;\n}\n.vc-hue-picker {\n  cursor: pointer;\n  margin-top: 1px;\n  width: 4px;\n  border-radius: 1px;\n  height: 8px;\n  box-shadow: 0 0 2px rgba(0, 0, 0, .6);\n  background: #fff;\n  transform: translateX(-2px) ;\n}\n</style>\n"
  },
  {
    "path": "src/components/vue3-color/common/Saturation.vue",
    "content": "<template>\n  <div class=\"vc-saturation\"\n    :style=\"{background: bgColor}\"\n    ref=\"container\"\n    @mousedown=\"handleMouseDown\"\n    @touchmove=\"handleChange\"\n    @touchstart=\"handleChange\">\n    <div class=\"vc-saturation--white\"></div>\n    <div class=\"vc-saturation--black\"></div>\n    <div class=\"vc-saturation-pointer\" :style=\"{top: pointerTop, left: pointerLeft}\">\n      <div class=\"vc-saturation-circle\"></div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Saturation',\n  props: {\n    modelValue: Object\n  },\n  computed: {\n    colors () {\n      return this.modelValue\n    },\n    bgColor () {\n      return `hsl(${this.colors.hsv.h}, 100%, 50%)`\n    },\n    pointerTop () {\n      return (-(this.colors.hsv.v * 100) + 1) + 100 + '%'\n    },\n    pointerLeft () {\n      return this.colors.hsv.s * 100 + '%'\n    }\n  },\n  methods: {\n    throttle: throttle((fn, data) => {\n      fn(data)\n    }, 20),\n    handleChange (e, skip) {\n      !skip && e.preventDefault()\n      const container = this.$refs.container\n      if (!container) {\n        // for some edge cases, container may not exist. see #220\n        return\n      }\n      const containerWidth = container.clientWidth\n      const containerHeight = container.clientHeight\n\n      const xOffset = container.getBoundingClientRect().left + window.pageXOffset\n      const yOffset = container.getBoundingClientRect().top + window.pageYOffset\n      const pageX = e.pageX || (e.touches ? e.touches[0].pageX : 0)\n      const pageY = e.pageY || (e.touches ? e.touches[0].pageY : 0)\n      const left = clamp(pageX - xOffset, 0, containerWidth)\n      const top = clamp(pageY - yOffset, 0, containerHeight)\n      const saturation = left / containerWidth\n      const bright = clamp(-(top / containerHeight) + 1, 0, 1)\n\n      this.throttle(this.onChange, {\n        h: this.colors.hsv.h,\n        s: saturation,\n        v: bright,\n        a: this.colors.hsv.a,\n        source: 'hsva'\n      })\n    },\n    onChange (param) {\n      this.$emit('change', param)\n    },\n    handleMouseDown (e) {\n      // this.handleChange(e, true)\n      window.addEventListener('mousemove', this.handleChange)\n      window.addEventListener('mouseup', this.handleChange)\n      window.addEventListener('mouseup', this.handleMouseUp)\n    },\n    handleMouseUp (e) {\n      this.unbindEventListeners()\n    },\n    unbindEventListeners () {\n      window.removeEventListener('mousemove', this.handleChange)\n      window.removeEventListener('mouseup', this.handleChange)\n      window.removeEventListener('mouseup', this.handleMouseUp)\n    }\n  }\n}\n\nfunction clamp(value, min, max) {\n  return min < max\n    ? (value < min ? min : value > max ? max : value)\n    : (value < max ? max : value > min ? min : value)\n}\n\nfunction throttle(func, timeFrame) {\n  var lastTime = 0;\n  return function (...args) {\n      var now = new Date();\n      if (now - lastTime >= timeFrame) {\n          func(...args);\n          lastTime = now;\n      }\n  };\n}\n</script>\n\n<style lang=\"css\">\n.vc-saturation,\n.vc-saturation--white,\n.vc-saturation--black {\n  cursor: pointer;\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n}\n\n.vc-saturation--white {\n  background: linear-gradient(to right, #fff, rgba(255,255,255,0));\n}\n.vc-saturation--black {\n  background: linear-gradient(to top, #000, rgba(0,0,0,0));\n}\n.vc-saturation-pointer {\n  cursor: pointer;\n  position: absolute;\n}\n.vc-saturation-circle {\n  cursor: head;\n  width: 4px;\n  height: 4px;\n  box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0,0,0,.3), 0 0 1px 2px rgba(0,0,0,.4);\n  border-radius: 50%;\n  transform: translate(-2px, -2px);\n}\n</style>\n"
  },
  {
    "path": "src/components/vue3-color/mixin/color.js",
    "content": "import tinycolor from 'tinycolor2'\n\nfunction _colorChange (data = {}, oldHue = 0) {\n  const alpha = data && data.a\n  let color\n\n  // hsl is better than hex between conversions\n  if (data && data.hsl) {\n    color = tinycolor(data.hsl)\n  } else if (data && data.hex && data.hex.length > 0) {\n    color = tinycolor(data.hex)\n  } else if (data && data.hsv) {\n    color = tinycolor(data.hsv)\n  } else if (data && data.rgba) {\n    color = tinycolor(data.rgba)\n  } else if (data && data.rgb) {\n    color = tinycolor(data.rgb)\n  } else {\n    color = tinycolor(data)\n  }\n\n  if (color && (color._a === undefined || color._a === null)) {\n    color.setAlpha(alpha || 1)\n  }\n\n  const hsl = color.toHsl()\n  const hsv = color.toHsv()\n\n  if (hsl.s === 0) {\n    hsv.h = hsl.h = data.h || (data.hsl && data.hsl.h) || oldHue || 0\n  }\n\n  /* --- comment this block to fix #109, may cause #25 again --- */\n  // when the hsv.v is less than 0.0164 (base on test)\n  // because of possible loss of precision\n  // the result of hue and saturation would be miscalculated\n  // if (hsv.v < 0.0164) {\n  //   hsv.h = data.h || (data.hsv && data.hsv.h) || 0\n  //   hsv.s = data.s || (data.hsv && data.hsv.s) || 0\n  // }\n\n  // if (hsl.l < 0.01) {\n  //   hsl.h = data.h || (data.hsl && data.hsl.h) || 0\n  //   hsl.s = data.s || (data.hsl && data.hsl.s) || 0\n  // }\n  /* ------ */\n\n  return {\n    hsl: hsl,\n    hex: color.toHexString().toUpperCase(),\n    hex8: color.toHex8String().toUpperCase(),\n    rgba: color.toRgb(),\n    hsv: hsv,\n    oldHue: data.h || oldHue || hsl.h,\n    source: data.source,\n    a: data.a || color.getAlpha()\n  }\n}\n\nexport default {\n  props: ['modelValue'],\n  data () {\n    return {\n      val: _colorChange(this.modelValue)\n    }\n  },\n  computed: {\n    colors: {\n      get () {\n        return this.val\n      },\n      set (newVal) {\n        this.val = newVal\n        this.$emit('update:modelValue', newVal)\n      }\n    }\n  },\n  watch: {\n    modelValue (newVal) {\n      this.val = _colorChange(newVal)\n    }\n  },\n  methods: {\n    colorChange (data, oldHue) {\n      this.oldHue = this.colors.hsl.h\n      this.colors = _colorChange(data, oldHue || this.oldHue)\n    },\n    isValidHex (hex) {\n      return tinycolor(hex).isValid()\n    },\n    simpleCheckForValidColor (data) {\n      const keysToCheck = ['r', 'g', 'b', 'a', 'h', 's', 'l', 'v']\n      let checked = 0\n      let passed = 0\n\n      for (let i = 0; i < keysToCheck.length; i++) {\n        const letter = keysToCheck[i]\n        if (data[letter]) {\n          checked++\n          if (!isNaN(data[letter])) {\n            passed++\n          }\n        }\n      }\n\n      if (checked === passed) {\n        return data\n      }\n    },\n    paletteUpperCase (palette) {\n      return palette.map(c => c.toUpperCase())\n    },\n    isTransparent (color) {\n      return tinycolor(color).getAlpha() === 0\n    }\n  }\n}\n"
  },
  {
    "path": "src/config.js",
    "content": "import tinycolor from 'tinycolor2';\n\nexport default {\n  /**\n   * This is our caching backend\n   */\n  // This used to work, but seems like GitHub no longer allows large website hosting:\n  //areaServer: 'https://anvaka.github.io/index-large-cities/data',\n  //areaServer: 'http://localhost:8085', // This is un-commented when I develop cache locally\n  // So, using S3\n  areaServer: 'https://d2uf7yjjctyxf.cloudfront.net/nov-02-2020',\n\n  getDefaultLineColor() {\n    return tinycolor('rgba(26, 26, 26, 0.8)');\n  },\n  getLabelColor() {\n    return tinycolor('#161616');\n  },\n\n  getBackgroundColor() {\n    return tinycolor('#F7F2E8');\n  }\n}"
  },
  {
    "path": "src/createOverlayManager.js",
    "content": "export default function createOverlayManager() {\n  let overlay;\n  let downEvent = {\n    clickedElement: null,\n    x: 0,\n    y: 0,\n    time: Date.now(),\n    left: 0,\n    right: 0\n  };\n\n  document.addEventListener('mousedown', handleMouseDown);\n  document.addEventListener('mouseup', handleMouseUp);\n  document.addEventListener('touchstart', handleTouchStart, {passive: false, capture: true});\n  document.addEventListener('touchend', handleTouchEnd, true);\n  document.addEventListener('touchcancel', handleTouchEnd, true);\n\n  return {\n    track,\n    dispose,\n    clear\n  }\n\n  function clear() {\n    const activeOverlays = document.querySelectorAll('.overlay-active');\n    for (let i = 0; i < activeOverlays.length; ++i) {\n      deselect(activeOverlays[i]);\n    }\n  }\n\n  function handleMouseDown(e) {\n    onPointerDown(e.clientX, e.clientY, e);\n  }\n\n  function handleMouseMove(e) {\n    onPointerMove(e.clientX, e.clientY);\n  }\n\n  function handleMouseUp(e) {\n    onPointerUp(e.clientX, e.clientY)\n  }\n\n  function handleTouchStart(e) {\n    if (e.touches.length > 1) return;\n\n    let touch = e.touches[0];\n    onPointerDown(touch.clientX, touch.clientY, e);\n  }\n\n  function handleTouchEnd(e) {\n    if (e.changedTouches.length > 1) return;\n    let touch = e.changedTouches[0];\n    let gotSomethingSelected = onPointerUp(touch.clientX, touch.clientY);\n    if (gotSomethingSelected) {\n      e.preventDefault();\n      e.stopPropagation();\n    }\n  }\n\n  function handleTouchMove(e) {\n    if (e.touches.length > 1) return;\n    let touch = e.touches[0];\n    onPointerMove(touch.clientX, touch.clientY);\n    e.preventDefault();\n    e.stopPropagation();\n  }\n\n  function onPointerDown(x, y, e) {\n    let foundElement = findTrackedElementUnderCursor(x, y)\n    let activeOverlays = document.querySelectorAll('.overlay-active');\n\n    for (let i = 0; i < activeOverlays.length; ++i) {\n      let el = activeOverlays[i];\n      if (el !== foundElement) deselect(el);\n    }\n    if (activeOverlays.length === 1) downEvent.clickedElement = activeOverlays[0];\n\n    let secondTimeClicking = foundElement && foundElement === downEvent.clickedElement;\n    if (secondTimeClicking) {\n      if (!downEvent.clickedElement.contains(e.target)) {\n        foundElement = null;\n        secondTimeClicking = false;\n      }\n    }\n    let shouldAddOverlay = secondTimeClicking && !foundElement.classList.contains('exclusive');\n    if (shouldAddOverlay) {\n      // prepare for move!\n      addDragOverlay();\n      e.preventDefault();\n      e.stopPropagation();\n    } else {\n      downEvent.clickedElement = foundElement;\n    }\n\n    downEvent.x = x;\n    downEvent.y = y;\n    downEvent.time = Date.now();\n    if (foundElement) {\n      let bBox = foundElement.getBoundingClientRect();\n      downEvent.dx = bBox.right - downEvent.x; \n      downEvent.dy = bBox.bottom - downEvent.y;\n    } else {\n      clear();\n    }\n  }\n\n  function onPointerUp(x, y) {\n    if (!downEvent.clickedElement) return;\n    removeOverlay();\n\n    if (isSingleClick(x, y)) {\n      // forward focus, we didn't move the element\n      select(downEvent.clickedElement, x, y);\n      return true;\n    } else {\n      downEvent.clickedElement = null;\n    }\n  }\n\n  function onPointerMove(x, y) {\n    if (!downEvent.clickedElement) return;\n\n    let style = downEvent.clickedElement.style;\n    style.right = 100*(window.innerWidth - x - downEvent.dx)/window.innerWidth + '%';\n    style.bottom = 100*(window.innerHeight - y - downEvent.dy)/window.innerHeight + '%';\n  }\n\n  function addDragOverlay() {\n    removeOverlay();\n\n    overlay = document.createElement('div');\n    overlay.classList.add('drag-overlay');\n    document.body.appendChild(overlay);\n\n    document.addEventListener('mousemove', handleMouseMove, true);\n    document.addEventListener('touchmove', handleTouchMove, {passive: false, capture: true});\n  }\n\n  function removeOverlay() {\n    if (overlay) {\n      document.body.removeChild(overlay);\n      overlay = null;\n    }\n\n    document.removeEventListener('mousemove', handleMouseMove, true);\n    document.removeEventListener('touchmove', handleTouchMove, {passive: false, capture: true});\n  }\n\n  function isSingleClick(x, y) {\n    let timeDiff = Date.now() - downEvent.time;\n    if (timeDiff > 300) return false; // took too long for a single click;\n\n    // should release roughly in the same place where pressed:\n    return Math.hypot(x - downEvent.x, y - downEvent.y) < 40; \n  }\n\n  function findTrackedElementUnderCursor(x, y) {\n    let autoTrack = document.querySelectorAll('.can-drag');\n    for (let i = 0; i < autoTrack.length; ++i) {\n      let el = autoTrack[i];\n      let rect = getRectangle(el);\n      if (intersects(x, y, rect)) return el;\n    }\n  }\n\n  function deselect(el) {\n    el.style.pointerEvents = 'none';\n    el.classList.remove('overlay-active');\n    el.classList.remove('exclusive')\n  }\n\n  function select(el, x, y) {\n    if (!el) return;\n\n    el.style.pointerEvents = '';\n\n    if (el.classList.contains('overlay-active')) {\n      // When they click second time, we want to forward focus to the element\n      // (if they support focus forwarding)\n      if (el.receiveFocus) el.receiveFocus();\n      // and make the element exclusive owner of the mouse/pointer\n      // (so that native interaction can occur and we don't interfere with dragging)\n      el.classList.add('exclusive')\n    } else {\n      // When they click first time, we enter to \"drag around\" mode\n      el.classList.add('overlay-active');\n      if (el.classList.contains('can-resize')) {\n        // el.resizer = renderResizeHandlers(el);\n      }\n    }\n  }\n\n\n  function intersects(x, y, rect) {\n    return !(x < rect.left || x > rect.right || y < rect.top || y > rect.bottom);\n  }\n\n  function getRectangle(x) {\n    return x.getBoundingClientRect();\n  }\n\n  function track(domElement, options) {\n    domElement.style.pointerEvents = 'none'\n    domElement.classList.add('can-drag');\n\n    if (options) {\n      if (options.receiveFocus) domElement.receiveFocus = options.receiveFocus;\n    }\n  }\n\n  function dispose() {\n    document.removeEventListener('mousedown', handleMouseDown);\n    document.removeEventListener('mouseup', handleMouseUp);\n    document.removeEventListener('touchstart', handleTouchStart);\n    document.removeEventListener('touchend', handleTouchEnd);\n    document.removeEventListener('touchcancel', handleTouchEnd);\n    downEvent.clickedElement = undefined;\n    removeOverlay();\n  }\n}\n\nfunction renderResizeHandlers(el) {\n  el.getBoundingClientRect(el)\n}"
  },
  {
    "path": "src/lib/BoundingBox.js",
    "content": "export default class BBox {\n  constructor() {\n    this.minX = Infinity;\n    this.minY = Infinity;\n    this.maxX = -Infinity;\n    this.maxY = -Infinity;\n  }\n\n  growBy(offset) {\n    this.minX -= offset;\n    this.minY -= offset;\n    this.maxX += offset;\n    this.maxY += offset;\n  }\n\n  get left() {\n    return this.minX;\n  }\n\n  get top() {\n    return this.minY;\n  }\n\n  get right() {\n    return this.maxX;\n  }\n\n  get bottom() {\n    return this.maxY;\n  }\n\n  get width() {\n    return this.maxX - this.minX;\n  }\n\n  get height() {\n    return this.maxY - this.minY;\n  }\n\n  get cx() {\n    return (this.minX + this.maxX)/2;\n  }\n\n  get cy() {\n    return (this.minY + this.maxY)/2;\n  }\n\n  addPoint(xIn, yIn) {\n    if (xIn === undefined) throw new Error('Point is not defined');\n    let x = xIn;\n    let y = yIn;\n    if (y === undefined) {\n      // xIn is a point object\n      x = xIn.x;\n      y = xIn.y;\n    }\n\n    if (x < this.minX) this.minX = x;\n    if (x > this.maxX) this.maxX = x;\n    if (y < this.minY) this.minY = y;\n    if (y > this.maxY) this.maxY = y;\n  }\n\n  addRect(rect) {\n    if (!rect) throw new Error('rect is not defined');\n    this.addPoint(rect.left, rect.top);\n    this.addPoint(rect.right, rect.top);\n    this.addPoint(rect.left, rect.bottom);\n    this.addPoint(rect.right, rect.bottom);\n  }\n\n  merge(otherBBox) {\n    if (otherBBox.minX < this.minX) this.minX = otherBBox.minX;\n    if (otherBBox.minY < this.minY) this.minY = otherBBox.minY;\n    if (otherBBox.maxX > this.maxX) this.maxX = otherBBox.maxX;\n    if (otherBBox.maxY > this.maxY) this.maxY = otherBBox.maxY;\n  }\n}"
  },
  {
    "path": "src/lib/Grid.js",
    "content": "import BoundingBox from './BoundingBox.js';\nimport {geoMercator} from 'd3-geo';\n\n/**\n * All roads in the area\n */\nexport default class Grid {\n  constructor() {\n    this.elements = [];\n    this.bounds = new BoundingBox();\n    this.nodes = new Map();\n    this.wayPointCount = 0;\n    this.id = 0;\n    this.name = '';\n    this.isArea = true;\n    this.projector = undefined; \n  }\n\n  setName(name) {\n    this.name = name;\n  }\n\n  setId(id) {\n    this.id = id;\n  }\n\n  setIsArea(isArea) {\n    this.isArea = isArea;\n  }\n\n  setBBox(bboxString) {\n    this.bboxString = bboxString;\n  }\n\n  hasRoads() {\n    return this.wayPointCount > 0;\n  }\n\n  setProjector(newProjector) {\n    this.projector = newProjector;\n  }\n\n  static fromPBF(pbf) {\n    if (pbf.version !== 1) throw new Error('Unknown version ' + pbf.version);\n    let elementsOfOSMResponse = [];\n    pbf.nodes.forEach(node => {\n      node.type = 'node';\n      elementsOfOSMResponse.push(node)\n    });\n    pbf.ways.forEach(way => {\n      way.type = 'way';\n      elementsOfOSMResponse.push(way);\n    });\n\n    const grid = Grid.fromOSMResponse(elementsOfOSMResponse);\n    grid.setName(pbf.name);\n    grid.setId(pbf.id);\n    return grid;\n  }\n\n  static fromOSMResponse(elementsOfOSMResponse) {\n    let gridInstance = new Grid();\n\n    let nodes = gridInstance.nodes;\n    let bounds = gridInstance.bounds;\n    let wayPointCount = 0;\n\n    // TODO: async?\n    elementsOfOSMResponse.forEach(element => {\n      if (element.type === 'node') {\n        nodes.set(element.id, element);\n        bounds.addPoint(element.lon, element.lat);\n      } else if (element.type === 'way') {\n        wayPointCount += element.nodes.length;\n      }\n    });\n\n    gridInstance.elements = elementsOfOSMResponse;\n    gridInstance.wayPointCount = wayPointCount;\n    return gridInstance;\n  }\n\n  getProjectedRect() {\n    let bounds = this.bounds;\n    let project = this.getProjector();\n    let leftTop = project({lon: bounds.left, lat: bounds.bottom});\n    let rightBottom = project({lon: bounds.right, lat: bounds.top});\n    let left = leftTop.x;\n    let top = leftTop.y;\n    let bottom = rightBottom.y\n    let right = rightBottom.x;\n    return {\n      left, top, right, bottom,\n      width: right - left, height: Math.abs(bottom - top)\n    }\n  }\n\n  forEachElement(callback) {\n    this.elements.forEach(callback);\n  }\n\n  forEachWay(callback, enter, exit) {\n    let positions = this.nodes;\n    let project = this.getProjector();\n    this.elements.forEach(element => {\n      if (element.type !== 'way') return;\n\n      let nodeIds = element.nodes;\n      let node = positions.get(nodeIds[0])\n      if (!node) return;\n\n      let last = project(node);\n      if (enter) enter(element);\n\n      for (let index = 1; index < nodeIds.length; ++index) {\n        node = positions.get(nodeIds[index])\n        if (!node) continue;\n        let next = project(node);\n\n        callback(last, next);\n\n        last = next;\n      }\n      if (exit) exit(element);\n    });\n  }\n\n  getProjector() {\n    let q = [0, 0]; // reuse to avoid GC.\n\n    if (!this.projector) {\n      this.projector = geoMercator();\n      this.projector\n        .center([this.bounds.cx, this.bounds.cy])\n        .scale(6371393); // Radius of Earth\n    }\n\n    let projector = this.projector;\n\n    return project;\n\n    function project({lon, lat}) {\n      q[0] = lon; q[1] = lat;\n\n      let xyPoint = projector(q);\n\n      return {\n        x: xyPoint[0],\n        y: -xyPoint[1]\n      };\n    }\n  }\n}"
  },
  {
    "path": "src/lib/GridLayer.js",
    "content": "import config from '../config.js';\nimport tinycolor from 'tinycolor2';\nimport {WireCollection} from 'w-gl';\n\nlet counter = 0;\n\nexport default class GridLayer {\n  get color() {\n    return this._color;\n  }\n\n  set color(unsafeColor) {\n    let color = tinycolor(unsafeColor);\n    this._color = color;\n    if (this.lines) {\n      this.lines.color = toRatioColor(color.toRgb());\n    }\n    if (this.scene) {\n      this.scene.renderFrame();\n    }\n  }\n\n  get lineWidth() {\n    return this._lineWidth;\n  }\n\n  set lineWidth(newValue) {\n    this._lineWidth = newValue;\n    if (!this.lines || !this.scene) return;\n\n    this.lines.setLineWidth(newValue);\n  }\n\n  constructor() {\n    this._color = config.getDefaultLineColor();\n    this.grid = null;\n    this.lines = null;\n    this.scene = null;\n    this.dx = 0;\n    this.dy = 0;\n    this.scale = 1;\n    this.hidden = false;\n    this.id = 'paths_' + counter;\n    this._lineWidth = 1;\n    counter += 1;\n  }\n\n  getGridProjector() {\n    if (this.grid) return this.grid.projector;\n  }\n\n  getQueryBounds() {\n    const {grid} = this;\n    if (grid) {\n      if (grid.queryBounds) return grid.queryBounds;\n      if (grid.isArea) return {\n        areaId: grid.id\n      };\n    }\n  }\n\n  setGrid(grid) {\n    this.grid = grid;\n    if (this.scene) {\n      this.bindToScene(this.scene);\n    }\n  }\n\n  getViewBox() {\n    if (!this.grid) return null;\n\n    let {width, height} = this.grid.getProjectedRect();\n    let initialSceneSize = Math.max(width, height) / 4;\n    return {\n      left:  -initialSceneSize,\n      top:    initialSceneSize,\n      right:  initialSceneSize,\n      bottom: -initialSceneSize,\n    };\n  }\n\n  moveTo(x, y = 0) {\n    console.warn('Please use moveBy() instead. The moveTo() is under construction');\n    // this.dx = x;\n    // this.dy = y;\n\n    // this._transferTransform();\n  }\n\n  moveBy(dx, dy = 0) {\n    this.dx = dx;\n    this.dy = dy;\n\n    this._transferTransform();\n  }\n\n  buildLinesCollection() {\n    if (this.lines) return this.lines;\n\n    let grid = this.grid;\n    let lines = new WireCollection(grid.wayPointCount, {\n      width: this._lineWidth,\n      allowColors: false,\n      is3D: false\n    });\n    grid.forEachWay(function(from, to) {\n      lines.add({from, to});\n    });\n    let color = tinycolor(this._color).toRgb();\n    lines.color = toRatioColor(color);\n    lines.id = this.id;\n\n    this.lines = lines;\n  }\n\n  destroy() {\n    if (!this.scene || !this.lines) return;\n\n    // TODO: This should remove the grid layer too. Need to clean up how\n    // scene interacts with grid layers.\n    this.scene.removeChild(this.lines);\n  }\n\n  bindToScene(scene) {\n    if (this.scene && this.lines) {\n      console.error('You seem to be adding this layer twice...')\n    }\n\n    this.scene = scene;\n    if (!this.grid) return;\n\n    this.buildLinesCollection();\n\n    if (this.hidden) return;\n    this.scene.appendChild(this.lines);\n  }\n\n  hide() {\n    if (this.hidden) return;\n    this.hidden = true;\n    if (!this.scene || !this.grid) return;\n\n    this.scene.removeChild(this.lines);\n  }\n\n  show() {\n    if (!this.hidden) return;\n    this.hidden = false;\n    if (!this.scene || !this.grid) {\n      console.log('Layer will be shown when grid is available');\n      return;\n    }\n\n    this.scene.appendChild(this.lines);\n  }\n\n  _transferTransform() {\n    if (!this.lines) return;\n\n    this.lines.translate([this.dx, this.dy, 0]);\n    this.lines.updateWorldTransform(true);\n    if (this.scene) {\n      this.scene.renderFrame(true);\n    }\n  }\n}\n\nfunction toRatioColor(c) {\n  return {r: c.r/0xff, g: c.g/0xff, b: c.b/0xff, a: c.a}\n}"
  },
  {
    "path": "src/lib/LoadOptions.js",
    "content": "import findBoundaryByName from \"./findBoundaryByName.js\";\n\n/**\n * For console API we allow a lot of flexibility to fetch data\n * This component normalizes input arguments and turns them into unified\n * options object\n */\n\nexport default class LoadOptions {\n  static parse(scene, wayFilter, rawOptions) {\n    let result = new LoadOptions();\n    if (typeof rawOptions === 'string') {\n      result.place = rawOptions;\n    }\n\n    if (wayFilter) {\n      result.wayFilter = wayFilter;\n    }\n\n    if (!rawOptions) return result;\n\n    Object.assign(result, rawOptions);\n\n    let protoLayer = getProtoLayer(scene, rawOptions.layer);\n    if (protoLayer) {\n      result.projector = protoLayer.getGridProjector();\n      let protoQueryBounds = protoLayer.getQueryBounds();\n      if (protoQueryBounds && !result.place && !result.areaId && !result.bbox) {\n        // use bounds of the parent layer unless we have our own override.\n        result.place = protoQueryBounds.place;\n        result.areaId = protoQueryBounds.areaId;\n        result.bbox = protoQueryBounds.bbox;\n      }\n    }\n\n    if (rawOptions.projector) {\n      // user defined projection. See https://github.com/d3/d3-geo for the projector reference:\n      result.projector = projector;\n    }\n\n    return result;\n  }\n\n  constructor(overrides) {\n    /**\n    * Query that should be translated to area id by nominatim;\n    */\n    this.place = undefined;\n\n    /**\n    * Which projector should be used to map lon/lat to layer's x/y\n    */\n    this.projector = undefined;\n    this.wayFilter = undefined;\n    this.timeout = 900;\n    this.maxHeapByteSize = 1073741824;\n    this.outputMethod = 'skel'; // body\n    Object.assign(this, overrides);\n  }\n\n  getQueryTemplate() {\n    if (this.raw) {\n      // I assume you know what you are doing.\n      return Promise.resolve({\n        queryString: this.raw\n      });\n    }\n\n    if (!this.wayFilter) {\n      throw new Error('Way filter is required');\n    }\n\n    return this.getBounds()\n      .then(bounds => {\n        let queryString;\n        if (bounds.areaId) {\n          queryString = `[timeout:${this.timeout}][maxsize:${this.maxHeapByteSize}][out:json];\narea(${bounds.areaId});\n(._; )->.area;\n(${this.wayFilter}(area.area); node(w););\nout ${this.outputMethod};`;\n        } else if (bounds.bbox) {\n          let bbox = serializeBBox(bounds.bbox);\n          queryString = `[timeout:${this.timeout}][maxsize:${this.maxHeapByteSize}][bbox:${bbox}][out:json];\n(${this.wayFilter}; node(w););\nout ${this.outputMethod};`;\n        }\n\n        return {\n          bounds,\n          queryString\n        }\n      });\n  }\n\n  getBounds() {\n    if (this.place) {\n      return findBoundaryByName(this.place).then(x => x && x[0]);\n    }\n    if (this.areaId) {\n      return Promise.resolve({ areaId: this.areaId });\n    }\n    if (this.bbox) {\n      return Promise.resolve({ bbox: this.bbox });\n    }\n\n    throw new Error('Please specify bounding area for the query (place|areaId|bbox)');\n  }\n}\n\nfunction getProtoLayer(scene, layerDefinition) {\n  if (layerDefinition === undefined) return;\n\n  if (typeof layerDefinition === 'number') {\n    let layers = scene.queryLayerAll();\n    return layers[layerDefinition];\n  } else if (typeof layerDefinition === 'string') {\n    return scene.queryLayer(layerDefinition);\n  } else {\n    // We assume it is a layer instance:\n    return layerDefinition;\n  }\n}\n\nfunction serializeBBox(bbox) {\n  return bbox && bbox.join(',');\n}"
  },
  {
    "path": "src/lib/Progress.js",
    "content": "import eventify from 'ngraph.events';\n\nexport default class Progress {\n  constructor(notify) {\n    eventify(this)\n    this.callback = notify || Function.prototype;\n  }\n\n  cancel() {\n    this.isCancelled = true;\n    this.fire('cancel');\n  }\n\n  notify(progress) {\n    if (!this.isCancelled) {\n      this.callback(progress);\n    }\n  }\n\n  onCancel(callback) {\n    this.on('cancel', callback, this);\n  }\n\n  offCancel(callback) {\n    this.off('cancel', callback);\n  }\n}"
  },
  {
    "path": "src/lib/Query.js",
    "content": "import postData from './postData';\nimport Grid from './Grid.js';\nimport findBoundaryByName from './findBoundaryByName.js';\n\nexport default class Query {\n  /**\n   * Every possible way\n   */\n  static All = 'way';\n\n  /**\n   * Every single building\n   */\n  static Building = 'way[building]';\n  /**\n   * This gets anything marked as a highway, which has its own pros and cons.\n   * See https://github.com/anvaka/city-roads/issues/20\n   */\n  static Road = 'way[highway]';\n\n  /**\n   * Reduced set of roads\n   */\n  static RoadBasic = 'way[highway~\"^(motorway|primary|secondary|tertiary)|residential\"]';\n\n  /**\n   * More accurate representation of the roads by @RicoElectrico.\n   */\n  static RoadStrict = 'way[highway~\"^(((motorway|trunk|primary|secondary|tertiary)(_link)?)|unclassified|residential|living_street|pedestrian|service|track)$\"][area!=yes]';\n\n  static runFromOptions(loadOptions, progress) {\n    return loadOptions.getQueryTemplate().then(boundedQuery => {\n      let q = new Query(boundedQuery, progress);\n      return q.run();\n    });\n  }\n\n  constructor(boundedQuery, progress) {\n    this.queryBounds = boundedQuery.bounds;\n    this.queryString = boundedQuery.queryString;\n    this.progress = progress;\n    this.promise = null;\n  }\n\n  run() {\n    if (this.promise) {\n      return this.promise;\n    }\n    let parts = collectAllNominatimQueries(this.queryString);\n\n    this.promise = runAllNominmantimQueries(parts)\n      .then(resolvedQueryString => postData(resolvedQueryString, this.progress))\n      .then(osmResponse => {\n        if (!osmResponse || !osmResponse.elements) {\n          let err = new Error('OpenStreetMap servers returned an invalid response. Please try again.');\n          err.invalidResponse = true;\n          throw err;\n        }\n        let grid = Grid.fromOSMResponse(osmResponse.elements)\n        grid.queryBounds = this.queryBounds;\n        return grid;\n      });\n\n    return this.promise;\n  }\n}\n\nfunction runAllNominmantimQueries(parts) {\n  let lastProcessed = 0;\n\n  return processNext().then(concat);\n\n  function concat() {\n    return parts.map(part => {\n      if (typeof part === 'string') {\n        return part;\n      } \n      if (part.geoType === 'Area') return `area(${part.areaId})`;\n      if (part.geoType === 'Coords') return part.lat + ',' + part.lon;\n      if (part.geoType === 'Id') return `${part.osmType}(${part.osmId})`;\n      if (part.geoType === 'Bbox') return part.bbox.join(',');\n \n    }).join('');\n  }\n  \n  function processNext() {\n    if (lastProcessed >= parts.length) {\n      return Promise.resolve();\n    }\n    \n    let part = parts[lastProcessed];\n    lastProcessed += 1;\n    if (typeof part === 'string') return processNext();\n\n    return findBoundaryByName(part.name)\n      .then(pickFirstBoundary)\n      .then(first => {\n        if (!first) {\n          throw new Error('No areas found for request ' + part.name);\n        }\n        Object.assign(part, first);\n      })\n      .then(wait(1000)) // per nominatim agreement we are not allowed to issue more tan 1 request per second\n      .then(processNext);\n  }\n}\n\nfunction pickFirstBoundary(boundaries) {\n  if (boundaries.length > 0) {\n    return boundaries[0];\n  }\n}\n\nfunction collectAllNominatimQueries(extendedQuery) {\n  let geoTest = /{{geocode(.+?):(.+?)}}/;\n  let match;\n  let parts = [];\n  let lastIndex = 0;\n  while ((match = extendedQuery.match(geoTest))) {\n    parts.push(extendedQuery.substr(0, match.index));\n    parts.push({\n      geoType: match[1],\n      name: match[2]\n    });\n    extendedQuery = extendedQuery.substr(match.index + match[0].length)\n  }\n\n  parts.push(extendedQuery);\n\n  return parts;\n}\n\nfunction wait(ms) {\n  return function(args) {\n    return new Promise(resolve => {\n      setTimeout(() => resolve(args), ms);\n    });\n  }\n}\n\n"
  },
  {
    "path": "src/lib/appState.js",
    "content": "import createQueryState from 'query-state';\n\nconst queryState = createQueryState({}, {useSearch: true});\n\n/**\n * This is our base state. It just persists default information about\n * custom settings and integrates with query string.\n */\nexport default {\n  isCacheEnabled() {\n    return queryState.get('cache') != 0;\n  },\n  enableCache() {\n    return queryState.unset('cache');\n  },\n  get() {\n    return queryState.get.apply(queryState, arguments);\n  },\n  set() {\n    return queryState.set.apply(queryState, arguments);\n  },\n  unset() {\n    return queryState.unset.apply(queryState, arguments);\n  },\n\n  unsetPlace() {\n    queryState.unset('areaId');\n    queryState.unset('osm_id');\n    queryState.unset('bbox');\n  }\n}"
  },
  {
    "path": "src/lib/bus.js",
    "content": "import eventify from 'ngraph.events';\n\n// we are going to use this as a global message bus inside the app.\nexport default eventify({});"
  },
  {
    "path": "src/lib/canvas2BlobPolyfill.js",
    "content": "/*\n * JavaScript Canvas to Blob\n * https://github.com/blueimp/JavaScript-Canvas-to-Blob\n *\n * Copyright 2012, Sebastian Tschan\n * https://blueimp.net\n *\n * Licensed under the MIT license:\n * https://opensource.org/licenses/MIT\n *\n * Based on stackoverflow user Stoive's code snippet:\n * http://stackoverflow.com/q/4998908\n */\n\n/* global define, Uint8Array, ArrayBuffer, module */\n\n;(function(window) {\n  'use strict'\n\n  var CanvasPrototype =\n    window.HTMLCanvasElement && window.HTMLCanvasElement.prototype\n  var hasBlobConstructor =\n    window.Blob &&\n    (function() {\n      try {\n        return Boolean(new Blob())\n      } catch (e) {\n        return false\n      }\n    })()\n  var hasArrayBufferViewSupport =\n    hasBlobConstructor &&\n    window.Uint8Array &&\n    (function() {\n      try {\n        return new Blob([new Uint8Array(100)]).size === 100\n      } catch (e) {\n        return false\n      }\n    })()\n  var BlobBuilder =\n    window.BlobBuilder ||\n    window.WebKitBlobBuilder ||\n    window.MozBlobBuilder ||\n    window.MSBlobBuilder\n  var dataURIPattern = /^data:((.*?)(;charset=.*?)?)(;base64)?,/\n  var dataURLtoBlob =\n    (hasBlobConstructor || BlobBuilder) &&\n    window.atob &&\n    window.ArrayBuffer &&\n    window.Uint8Array &&\n    function(dataURI) {\n      var matches,\n        mediaType,\n        isBase64,\n        dataString,\n        byteString,\n        arrayBuffer,\n        intArray,\n        i,\n        bb\n      // Parse the dataURI components as per RFC 2397\n      matches = dataURI.match(dataURIPattern)\n      if (!matches) {\n        throw new Error('invalid data URI')\n      }\n      // Default to text/plain;charset=US-ASCII\n      mediaType = matches[2]\n        ? matches[1]\n        : 'text/plain' + (matches[3] || ';charset=US-ASCII')\n      isBase64 = !!matches[4]\n      dataString = dataURI.slice(matches[0].length)\n      if (isBase64) {\n        // Convert base64 to raw binary data held in a string:\n        byteString = atob(dataString)\n      } else {\n        // Convert base64/URLEncoded data component to raw binary:\n        byteString = decodeURIComponent(dataString)\n      }\n      // Write the bytes of the string to an ArrayBuffer:\n      arrayBuffer = new ArrayBuffer(byteString.length)\n      intArray = new Uint8Array(arrayBuffer)\n      for (i = 0; i < byteString.length; i += 1) {\n        intArray[i] = byteString.charCodeAt(i)\n      }\n      // Write the ArrayBuffer (or ArrayBufferView) to a blob:\n      if (hasBlobConstructor) {\n        return new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], {\n          type: mediaType\n        })\n      }\n      bb = new BlobBuilder()\n      bb.append(arrayBuffer)\n      return bb.getBlob(mediaType)\n    }\n  if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) {\n    if (CanvasPrototype.mozGetAsFile) {\n      CanvasPrototype.toBlob = function(callback, type, quality) {\n        var self = this\n        setTimeout(function() {\n          if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) {\n            callback(dataURLtoBlob(self.toDataURL(type, quality)))\n          } else {\n            callback(self.mozGetAsFile('blob', type))\n          }\n        })\n      }\n    } else if (CanvasPrototype.toDataURL && dataURLtoBlob) {\n      CanvasPrototype.toBlob = function(callback, type, quality) {\n        var self = this\n        setTimeout(function() {\n          callback(dataURLtoBlob(self.toDataURL(type, quality)))\n        })\n      }\n    }\n  }\n  if (typeof define === 'function' && define.amd) {\n    define(function() {\n      return dataURLtoBlob\n    })\n  } else if (typeof module === 'object' && module.exports) {\n    module.exports = dataURLtoBlob\n  } else {\n    window.dataURLtoBlob = dataURLtoBlob\n  }\n})(window)"
  },
  {
    "path": "src/lib/createScene.js",
    "content": "import bus from './bus';\nimport GridLayer from './GridLayer';\nimport Query from './Query';\nimport LoadOptions from './LoadOptions.js';\nimport config from '../config.js';\nimport tinycolor from 'tinycolor2';\nimport eventify from 'ngraph.events';\nimport {toSVG, toPNG} from './saveFile.js';\nimport * as wgl from 'w-gl';\n\n/**\n * This file is responsible for rendering of the grid. It uses my silly 2d webgl\n * renderer which is not very well documented, neither popular, yet it is very\n * fast.\n */\n\nexport default function createScene(canvas) {\n  let scene = wgl.createScene(canvas);\n  let lastLineColor = config.getDefaultLineColor();\n  scene.on('transform', triggerTransform);\n  scene.on('append-child', triggerAdd);\n  scene.on('remove-child', triggerRemove);\n\n  scene.setClearColor(0xf7/0xff, 0xf2/0xff, 0xe8/0xff, 1.0);\n  let camera = scene.getCameraController();\n  if (camera.setMoveSpeed) {\n    camera.setMoveSpeed(200);\n    camera.setRotationSpeed(Math.PI/500);\n  }\n\n  let gl = scene.getGL();\n  gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  let slowDownZoom = false;\n  let layers = [];\n  let backgroundColor = config.getBackgroundColor();\n\n  listenToEvents();\n\n  let sceneAPI = {\n    /**\n     * Requests the scene to perform immediate re-render\n     */\n    render() {\n      scene.renderFrame(true);\n    },\n\n    /**\n     * Removes all layers in the scene\n     */\n    clear() {\n      layers.forEach(layer => layer.destroy());\n      layers = [];\n      scene.clear();\n    },\n\n    /**\n     * Returns all layers in the scene.\n     */\n    queryLayerAll,\n\n    /**\n     * Same as `queryLayerAll(filter)` but returns the first found\n     * match. If no matches found - returns undefined.\n     */\n    queryLayer,\n    \n    getRenderer() {\n      return scene;\n    },\n\n    getWGL() {\n      // Let the plugins use the same version of wgl library\n      return wgl;\n    },\n\n    version() {\n      return '0.0.2'; // here be dragons\n    },\n\n    /**\n     * Destroys the scene, cleans up all resources.\n     */\n    dispose() {\n      scene.clear();\n      scene.dispose();\n      sceneAPI.fire('dispose', sceneAPI);\n      unsubscribeFromEvents();\n    },\n\n    /**\n     * Uniformly sets color to all loaded grid layer.\n     */\n    set lineColor(color) {\n      layers.forEach(layer => {\n        layer.color = color;\n      });\n      lastLineColor = tinycolor(color);\n      bus.fire('line-color', lastLineColor);\n      sceneAPI.fire('line-color', lastLineColor);\n    },\n\n    get lineColor() {\n      let firstLayer = queryLayer();\n      return (firstLayer && firstLayer.color) || lastLineColor;\n    },\n\n    /**\n     * Sets the background color of the scene\n     */\n    set background(rawColor) {\n      backgroundColor = tinycolor(rawColor);\n      let c = backgroundColor.toRgb();\n      scene.setClearColor(c.r/0xff, c.g/0xff, c.b/0xff, c.a);\n      scene.renderFrame();\n      bus.fire('background-color', backgroundColor);\n      sceneAPI.fire('background-color', backgroundColor);\n    },\n\n    get background() {\n      return backgroundColor;\n    },\n\n    add,\n\n    /**\n     * Executes an OverPass query and loads results into scene.\n     */\n    load,\n\n    saveToPNG,\n\n    saveToSVG\n  };\n\n  return eventify(sceneAPI); // Public bit is over. Below are just implementation details.\n\n  /**\n   * Experimental API. Can be changed/removed at any point.\n   */\n  function load(queryFilter, rawOptions) {\n    let options = LoadOptions.parse(sceneAPI, queryFilter, rawOptions);\n\n    let layer = new GridLayer();\n    layer.id = options.place;\n\n    // TODO: Cancellation logic?\n    Query.runFromOptions(options).then(grid => {\n      grid.setProjector(options.projector);\n      layer.setGrid(grid);\n    }).catch(e => {\n      console.error(`Could not execute:\n  ${queryFilter}\n  The error was:`);\n      console.error(e);\n      layer.destroy();\n    });\n  \n    add(layer);\n    return layer;\n  }\n\n  function queryLayerAll(filter) {\n    if (!filter) return layers;\n\n    return layers.filter(layer => {\n      return layer.id === filter;\n    });\n  }\n\n  function queryLayer(filter) {\n    let result = queryLayerAll(filter);\n    if (result) return result[0];\n  }\n\n  function add(gridLayer) {\n    if (layers.indexOf(gridLayer) > -1) return; // O(n).\n\n    gridLayer.bindToScene(scene);\n    layers.push(gridLayer);\n\n    if (layers.length === 1) {\n      // TODO: Should I do this for other layers?\n      let viewBox = gridLayer.getViewBox();\n      if (viewBox) {\n        scene.setViewBox(viewBox);\n      }\n    }\n  }\n\n  function saveToPNG(name) {\n    return toPNG(sceneAPI, {name});\n  }\n\n  function saveToSVG(name, options) {\n    return toSVG(sceneAPI, Object.assign({}, {name}, options));\n  }\n\n  function triggerTransform(t) {\n    bus.fire('scene-transform');\n  }\n\n  function triggerAdd(e) {\n    sceneAPI.fire('layer-added', e);\n  }\n\n  function triggerRemove(e) {\n    sceneAPI.fire('layer-removed', e);\n  }\n\n  function listenToEvents() {\n    document.addEventListener('keydown', onKeyDown, true);\n    document.addEventListener('keyup', onKeyUp, true);\n  }\n\n  function unsubscribeFromEvents() {\n    document.removeEventListener('keydown', onKeyDown, true);\n    document.removeEventListener('keyup', onKeyUp, true);\n  }\n\n  function onKeyDown(e) {\n    if (e.shiftKey) {\n      slowDownZoom = true;\n      if (camera.setSpeed) camera.setSpeed(0.1);\n    } \n  }\n\n  function onKeyUp(e) {\n    if (!e.shiftKey && slowDownZoom) {\n      if (camera.setSpeed) camera.setSpeed(1);\n      slowDownZoom = false;\n    }\n  }\n}"
  },
  {
    "path": "src/lib/findBoundaryByName.js",
    "content": "import request from './request.js';\n\nlet cachedResults = new Map();\n\nexport default function findBoundaryByName(inputName) {\n  let results = cachedResults.get(inputName);\n  if (results) return Promise.resolve(results);\n\n  let name = encodeURIComponent(inputName);\n  return request(`https://nominatim.openstreetmap.org/search?format=json&q=${name}`, {responseType: 'json'})\n      .then(extractBoundaries)\n      .then(x => {\n        cachedResults.set(inputName, x);\n        return x;\n      });\n}\n\nfunction extractBoundaries(x) {\n  let areas = x.map(row => {\n      let areaId, bbox;\n      if (row.osm_type === 'relation') {\n        // By convention the area id can be calculated from an existing \n        // OSM way by adding 2400000000 to its OSM id, or in case of a \n        // relation by adding 3600000000 respectively. So we are adding this\n        // https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#By_area_.28area.29\n        // Note: we may want to do another case for osm_type = 'way'. Need to check\n        // if it returns correct values.\n        areaId = row.osm_id + 36e8;\n      } else if (row.osm_type === 'way') {\n        areaId = row.osm_id + 24e8;\n      }\n      if (row.boundingbox) {\n        bbox = [\n          Number.parseFloat(row.boundingbox[0]),\n          Number.parseFloat(row.boundingbox[2]),\n          Number.parseFloat(row.boundingbox[1]),\n          Number.parseFloat(row.boundingbox[3]),\n        ];\n      }\n\n      return {\n        areaId,\n        bbox,\n        lat: row.lat,\n        lon: row.lon,\n        osmId: row.osm_id,\n        osmType: row.osm_type,\n        name: row.display_name,\n        type: row.type,\n      };\n    });\n\n  return areas;\n}"
  },
  {
    "path": "src/lib/getZazzleLink.js",
    "content": "import request from './request.js';\nimport Progress from './Progress.js';\n\nlet imageUrl = 'https://edi6jgnosf.execute-api.us-west-2.amazonaws.com/Stage/put_image'\n\nconst productKinds = {\n  mug: '168739066664861503'\n};\n\nfunction getZazzleLink(kind, imageUrl) {\n  const productCode = productKinds[kind];\n  if (!productCode) {\n    throw new Error('Unknown product kind: ' + kind);\n  }\n\n  const imageEncoded = encodeURIComponent(imageUrl);\n  return `https://www.zazzle.com/api/create/at-238058511445368984?rf=238058511445368984&ax=Linkover&pd=${productCode}&ed=true&tc=&ic=&t_map_iid=${imageEncoded}`;\n}\n\nexport default function generateZazzleLink(canvas) {\n  var imageContent = canvas.toDataURL('image/png').replace(/^data:image\\/(png|jpg);base64,/, '');\n  const form = new FormData();\n  form.append('image', imageContent);\n\n  return request(imageUrl, {\n    method: 'POST',\n    responseType: 'json',\n    progress: new Progress(Function.prototype),\n    body: form,\n  }, 'POST').then(x => {\n    if (!x.success) throw new Error('Failed to upload image');\n    let link = x.data.link; \n    return getZazzleLink('mug', link);\n  }).catch(e => {\n    console.log('error', e);\n    throw e;\n  });\n}"
  },
  {
    "path": "src/lib/postData.js",
    "content": "import request from './request.js';\nimport Progress from './Progress.js';\n\nlet backends = [\n  'https://overpass-api.de/api/interpreter',\n  'https://maps.mail.ru/osm/tools/overpass/api/interpreter',\n  'https://overpass.osm.jp/api/interpreter',\n  'https://overpass.kumi.systems/api/interpreter',\n  'https://overpass.openstreetmap.ru/cgi/interpreter'\n]\n\nexport default function postData(data, progress) {\n  progress = progress || new Progress();\n  const postData = {\n    method: 'POST',\n    responseType: 'json',\n    progress,\n    headers: {\n      'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'\n    },\n    body: 'data=' + encodeURIComponent(data),\n  };\n\n  let serverIndex = 0;\n\n  return fetchFrom(backends[serverIndex]);\n\n  function fetchFrom(overpassUrl) {\n    return request(overpassUrl, postData, 'POST')\n      .catch(handleError);\n  }\n\n  function handleError(err) {\n    if (err.cancelled) throw err;\n\n    if (serverIndex >= backends.length - 1) {\n      // we can't do much anymore - all servers failed\n      err.allServersFailed = true;\n      err.serversAttempted = backends.length;\n      throw err;\n    }\n\n    if (err.statusError) {\n      progress.notify({\n        loaded: -1\n      });\n    }\n\n    serverIndex += 1;\n    return fetchFrom(backends[serverIndex])\n  }\n}"
  },
  {
    "path": "src/lib/protobufExport.js",
    "content": "import Pbf from 'pbf';\nimport {place} from '../proto/place.js';\n\nexport default function protoBufExport(grid) {\n  let nodes = [];\n  let ways = [];\n  let date = (new Date()).toISOString();\n\n  grid.forEachElement(x => {\n    let elementType = 0;\n    if (x.type === 'node') {\n      nodes.push(x)\n    } else if (x.type === 'way') {\n      ways.push(x)\n    }\n  });\n\n  let pbf = new Pbf()\n  place.write({\n    version: 1,\n    id: grid.id,\n    date, \n    name: grid.name,\n    nodes, ways\n  }, pbf);\n  return pbf.finish();\n}"
  },
  {
    "path": "src/lib/request.js",
    "content": "import Progress from './Progress.js';\n\nexport default function request(url, options) {\n  if (!options) options = {};\n  let req;\n  let progress = options.progress || new Progress();\n  let isCancelled = false;\n  if (progress.on) {\n    progress.onCancel(cancelDownload);\n  }\n\n  return new Promise(download);\n\n  function cancelDownload() {\n    isCancelled = true;\n    if (req) {\n      req.abort();\n    }\n  }\n\n  function download(resolve, reject) {\n    req = new XMLHttpRequest();\n\n    if (typeof progress.notify === 'function') {\n      req.addEventListener('progress', updateProgress, false);\n    }\n\n    req.addEventListener('load', transferComplete, false);\n    req.addEventListener('error', transferFailed, false);\n    req.addEventListener('abort', transferCanceled, false);\n\n    req.open(options.method || 'GET', url);\n    if (options.responseType) {\n      req.responseType = options.responseType;\n    }\n\n    if (options.headers) {\n      Object.keys(options.headers).forEach(key => {\n        req.setRequestHeader(key, options.headers[key]);\n      });\n    }\n\n    if (options.method === 'POST') {\n      req.send(options.body);\n    } else {\n      req.send(null);\n    }\n\n    function updateProgress(e) {\n      if (e.lengthComputable) {\n        progress.notify({\n          loaded: e.loaded,\n          total: e.total,\n          percent: e.loaded / e.total,\n          lengthComputable: true\n        });\n      } else {\n        progress.notify({\n          loaded: e.loaded,\n          lengthComputable: false\n        });\n      }\n    }\n\n    function transferComplete() {\n      progress.offCancel(cancelDownload);\n\n      if (progress.isCancelled) return;\n\n      if (req.status !== 200) {\n        reject({\n          statusError: req.status,\n          message: `Unexpected status code ${req.status} when calling ${url}`\n        });\n        return;\n      }\n      var response = req.response;\n\n      if (options.responseType === 'json' && typeof response === 'string') {\n        // IE\n        response = JSON.parse(response);\n      }\n\n      setTimeout(() => resolve(response), 0);\n    }\n\n    function transferFailed() {\n      reject(`Failed to download ${url}`);\n    }\n\n    function transferCanceled() {\n      reject({\n        cancelled: true,\n        message: `Cancelled download of ${url}`\n      });\n    }\n  }\n}"
  },
  {
    "path": "src/lib/saveFile.js",
    "content": "// import protobufExport from './protobufExport.js';\nimport svgExport from './svgExport.js';\n\nexport function toSVG(scene, options) {\n  options = options || {};\n  let svg = svgExport(scene, { \n    printable: collectPrintable(),\n    ...options\n  });\n  let blob = new Blob([svg], {type: \"image/svg+xml\"});\n  let url = window.URL.createObjectURL(blob);\n  let fileName = getFileName(options.name, '.svg');\n  // For some reason, safari doesn't like when download happens on the same\n  // event loop cycle. Pushing it to the next one.\n  setTimeout(() => {\n    let a = document.createElement(\"a\");\n    a.href = url;\n    a.download = fileName;\n    a.click();\n    revokeLater(url);\n  }, 30)\n}\n\nexport function toPNG(scene, options) {\n  options = options || {};\n\n  getPrintableCanvas(scene).then((printableCanvas) => {\n    let fileName = getFileName(options.name, '.png');\n\n    printableCanvas.toBlob(function(blob) {\n      let url = window.URL.createObjectURL(blob);\n      let a = document.createElement(\"a\");\n      a.href = url;\n      a.download = fileName;\n      a.click();\n      revokeLater(url);\n    }, 'image/png')\n  })\n}\n\nexport function getPrintableCanvas(scene) {\n  let cityCanvas = getCanvas();\n  let width = cityCanvas.width;\n  let height = cityCanvas.height;\n\n  let printable = document.createElement('canvas');\n  let ctx = printable.getContext('2d');\n  printable.width = width;\n  printable.height = height;\n  scene.render();\n  ctx.drawImage(cityCanvas, 0, 0, cityCanvas.width, cityCanvas.height, 0, 0, width, height);\n\n  return Promise.all(collectPrintable().map(label => drawTextLabel(label, ctx))).then(() => {\n    return printable;\n  });\n}\n\nexport function getCanvas() {\n  return document.querySelector('#canvas')\n}\n\nfunction getFileName(name, extension) {\n  let fileName = escapeFileName(name || new Date().toISOString());\n  return fileName + (extension || '');\n}\n\nfunction escapeFileName(str) {\n  if (!str) return '';\n\n  return str.replace(/[#%&{}\\\\/?*><$!'\":@+`|=]/g, '_');\n}\n\n\nfunction drawTextLabel(element, ctx) {\n  if (!element) return Promise.resolve();\n\n  return new Promise((resolve, reject) => {\n    let dpr = window.devicePixelRatio || 1;\n\n    if (element.element instanceof SVGSVGElement) {\n      let svg = element.element;\n      let rect = element.bounds;\n      let image = new Image();\n      image.width = rect.width * dpr;\n      image.height = rect.height * dpr;\n      image.onload = () => {\n        ctx.drawImage(image, rect.left * dpr, rect.top * dpr, image.width, image.height);\n        svg.removeAttribute('width');\n        svg.removeAttribute('height');\n        resolve();\n      };\n\n      // Need to set width, otherwise firefox doesn't work: https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage\n      svg.setAttribute('width', image.width);\n      svg.setAttribute('height', image.height);\n      image.src = 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(svg));\n    } else {\n      ctx.save();\n\n      ctx.font = dpr * element.fontSize + 'px ' + element.fontFamily;\n      ctx.fillStyle = element.color;\n      ctx.textAlign = 'end'\n      ctx.fillText(\n        element.text, \n        (element.bounds.right - element.paddingRight) * dpr, \n        (element.bounds.bottom - element.paddingBottom) * dpr\n      )\n      ctx.restore();\n      resolve();\n    }\n  });\n}\n\nfunction collectPrintable() {\n  return Array.from(document.querySelectorAll('.printable')).map(element => {\n    let computedStyle = window.getComputedStyle(element);\n    let bounds = element.getBoundingClientRect();\n    let fontSize = Number.parseInt(computedStyle.fontSize, 10);\n    let paddingRight = Number.parseInt(computedStyle.paddingRight, 10);\n    // TODO: I don't know why I need to multiply by 2, it's just\n    // not aligned right if I don't multiply. Need to figure out this.\n    let paddingBottom = Number.parseInt(computedStyle.paddingBottom, 10) * 2;\n\n    return {\n      text: element.innerText,\n      bounds,\n      fontSize,\n      paddingBottom,\n      paddingRight,\n      color: computedStyle.color,\n      fontFamily: computedStyle.fontFamily,\n      fill: computedStyle.color,\n      element\n    }\n  });\n}\n\nfunction revokeLater(url) {\n  // In iOS immediately revoked URLs cause \"WebKitBlobResource error 1.\" error\n  // Setting a timeout to revoke URL in the future fixes the error:\n  setTimeout(() => {\n    window.URL.revokeObjectURL(url);\n  }, 45000);\n}\n\n// function toProtobuf() {\n//   if (!lastGrid) return;\n\n//   let arrayBuffer = protobufExport(lastGrid);\n//   let blob = new Blob([arrayBuffer.buffer], {type: \"application/octet-stream\"});\n//   let url = window.URL.createObjectURL(blob);\n//   let a = document.createElement(\"a\");\n//   a.href = url;\n//   a.download = lastGrid.id + '.pbf';\n//   a.click();\n//   revokeLater(url);\n// }\n"
  },
  {
    "path": "src/lib/svgExport.js",
    "content": "import {toSVG} from 'w-gl';\n\nexport default function svgExport(scene, options) {\n  const renderer = scene.getRenderer();\n  const svgExportSettings = {\n    open() {\n      return `<!-- Generator: https://github.com/anvaka/city-roads\nData © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\n-->`;\n    },\n    close() {\n      return getPrintableElements();\n    }\n  };\n\n  if (options.minLength) {\n    svgExportSettings.beforeWrite = path => {\n      let pathLength = 0;\n      for (let i = 1; i < path.length; ++i) {\n        pathLength += Math.hypot(path[i].x - path[i - 1].x, path[i].y - path[i - 1].y);\n        if (pathLength > options.minLength) return true;\n      }\n      return pathLength > options.minLength;\n    }\n  }\n  svgExportSettings.round = options.round;\n\n  const svg = toSVG(renderer, svgExportSettings);\n\n  return svg;\n\n  function getPrintableElements() {\n    let dpr = renderer.getPixelRatio();\n\n    return options.printable.map(el => {\n      if (el.element instanceof SVGSVGElement) {\n        let bounds = el.bounds;\n        let x = bounds.left * dpr;\n        let y = bounds.top * dpr;\n        let svg = el.element;\n        svg.setAttribute('x', bounds.left * dpr);\n        svg.setAttribute('y', bounds.top * dpr);\n        svg.setAttribute('width', bounds.width * dpr);\n        svg.setAttribute('height', bounds.height * dpr);\n        let content = new XMLSerializer().serializeToString(el.element);\n        svg.removeAttribute('x');\n        svg.removeAttribute('y');\n        svg.removeAttribute('width');\n        svg.removeAttribute('height');\n        return content;\n      } else {\n        let label = el;\n        if (!label.text) return;\n        let insecurelyEscaped = label.text\n          .replace(/&/g, '&amp;')\n          .replace(/</g, '&lt;')\n          .replace(/>/g, '&gt;')\n        \n        // Note: this is not 100% accurate, might need to be fixed eventually\n        let bounds = label.bounds;\n        let leftOffset = (bounds.right - label.paddingRight) * dpr;\n        let bottomOffset = (bounds.bottom - label.paddingBottom) * dpr;\n        let fontSize = label.fontSize * dpr;\n\n        let fontFamily = label.fontFamily.replace(/\"/g, '\\'');\n        return `<text text-anchor=\"end\" x=\"${leftOffset}\" y=\"${bottomOffset}\" fill=\"${label.color}\" font-family=\"${fontFamily}\" font-size=\"${fontSize}\">${insecurelyEscaped}</text>`\n      }\n    }).filter(x => x).join('\\n')\n  }\n}"
  },
  {
    "path": "src/main.js",
    "content": "// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base.conf with an alias.\nimport {createApp} from 'vue';\nimport {require as d3Require} from 'd3-require';\nimport {isWebGLEnabled} from 'w-gl';\nimport App from './App.vue';\nimport NoWebGL from './NoWebGL.vue';\nimport Query from './lib/Query.js';\n\n// const wgl = require('w-gl');\n\nwindow.addEventListener('error', logError);\n\n// expose the console API\nwindow.requireModule = d3Require;\nwindow.Query = Query;\n\nif (isWebGLEnabled(document.querySelector('#canvas'))) {\n  createApp(App).mount('#host');\n} else {\n  createApp(NoWebGL).mount('#host');\n}\n\nfunction logError(e) {\n  if (typeof gtag !== 'function') return;\n\n  const exDescription = e ? `${e.message} in ${e.filename}:${e.lineno}` : 'Unknown exception';\n\n  gtag('send', 'exception', {\n    description: exDescription,\n    fatal: false\n  });\n}"
  },
  {
    "path": "src/proto/decode.js",
    "content": "let fs = require('fs');\nlet data = require('./test-data.json');\nvar Pbf = require('pbf');\nvar place = require('./place.js').place;\nlet buffer = fs.readFileSync(process.argv[2] || 'out1.pbf');\nvar pbf = new Pbf(buffer);\nvar obj = place.read(pbf);\nconsole.log(obj);\n"
  },
  {
    "path": "src/proto/encode.js",
    "content": "let fs = require('fs');\nlet data = require('./test-data.json');\nvar Pbf = require('pbf');\nvar place = require('./place.js').place;\nvar pbf = new Pbf()\nlet nodes = [];\nlet ways = [];\n\ndata.forEach(x => {\n  let elementType = 0;\n  if (x.type === 'node') {\n    nodes.push(x)\n  } else if (x.type === 'way') {\n    ways.push(x)\n  }\n});\n\nplace.write({\n  name: 'test',\n  nodes, ways\n}, pbf)\nvar buffer = pbf.finish();\nconsole.log(buffer.length);\nfs.writeFileSync('out1.pbf', buffer);\n"
  },
  {
    "path": "src/proto/place.js",
    "content": "'use strict'; // code generated by pbf v3.2.1\n\n// place ========================================\n\nexport const place = {};\n\nplace.read = function (pbf, end) {\n    return pbf.readFields(place._readField, {version: 0, name: \"\", date: \"\", id: \"\", nodes: [], ways: []}, end);\n};\nplace._readField = function (tag, obj, pbf) {\n    if (tag === 1) obj.version = pbf.readVarint();\n    else if (tag === 2) obj.name = pbf.readString();\n    else if (tag === 3) obj.date = pbf.readString();\n    else if (tag === 4) obj.id = pbf.readString();\n    else if (tag === 5) obj.nodes.push(place.node.read(pbf, pbf.readVarint() + pbf.pos));\n    else if (tag === 6) obj.ways.push(place.way.read(pbf, pbf.readVarint() + pbf.pos));\n};\nplace.write = function (obj, pbf) {\n    if (obj.version) pbf.writeVarintField(1, obj.version);\n    if (obj.name) pbf.writeStringField(2, obj.name);\n    if (obj.date) pbf.writeStringField(3, obj.date);\n    if (obj.id) pbf.writeStringField(4, obj.id);\n    if (obj.nodes) for (var i = 0; i < obj.nodes.length; i++) pbf.writeMessage(5, place.node.write, obj.nodes[i]);\n    if (obj.ways) for (i = 0; i < obj.ways.length; i++) pbf.writeMessage(6, place.way.write, obj.ways[i]);\n};\n\n// place.node ========================================\n\nplace.node = {};\n\nplace.node.read = function (pbf, end) {\n    return pbf.readFields(place.node._readField, {id: 0, lat: 0, lon: 0}, end);\n};\nplace.node._readField = function (tag, obj, pbf) {\n    if (tag === 1) obj.id = pbf.readVarint();\n    else if (tag === 2) obj.lat = pbf.readFloat();\n    else if (tag === 3) obj.lon = pbf.readFloat();\n};\nplace.node.write = function (obj, pbf) {\n    if (obj.id) pbf.writeVarintField(1, obj.id);\n    if (obj.lat) pbf.writeFloatField(2, obj.lat);\n    if (obj.lon) pbf.writeFloatField(3, obj.lon);\n};\n\n// place.way ========================================\n\nplace.way = {};\n\nplace.way.read = function (pbf, end) {\n    return pbf.readFields(place.way._readField, {nodes: []}, end);\n};\nplace.way._readField = function (tag, obj, pbf) {\n    if (tag === 1) pbf.readPackedVarint(obj.nodes);\n};\nplace.way.write = function (obj, pbf) {\n    if (obj.nodes) pbf.writePackedVarint(1, obj.nodes);\n};\n"
  },
  {
    "path": "src/proto/place.proto",
    "content": "message place {\n    message node {\n        optional uint64 id = 1;\n        optional float lat = 2;\n        optional float lon = 3;\n    }\n\n    message way {\n        repeated uint64 nodes = 1 [packed = true];\n    }\n\n    required uint32 version = 1 [ default = 1 ];\n\n    required string name = 2;\n    required string date = 3;\n    required string id = 4;\n    repeated node nodes = 5;\n    repeated way ways = 6;\n\n    extensions 16 to 8191;\n}"
  },
  {
    "path": "src/vars.styl",
    "content": "small-screen = 450px;\ndesktop-controls-width = 442px;\nlabels-font = 'Roboto', sans-serif;\n\nhighlight-color = #ff4081;\nprimary-text = rgb(33, 33, 33);\nsecondary-color = rgba(0,0,0,.54);\nemphasis-background = white;\nbackground-color = #F7F2E8;\nborder-color = #E9EAED;"
  },
  {
    "path": "static/.gitkeep",
    "content": ""
  },
  {
    "path": "vite.config.js",
    "content": "import { fileURLToPath, URL } from 'url'\n\nimport { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport { visualizer } from \"rollup-plugin-visualizer\";\n\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [vue(), visualizer({\n  //  template: 'network'\n  })],\n  base: '',\n  server: {\n    port: 8080\n  },\n  resolve: {\n    alias: {\n      '@': fileURLToPath(new URL('./src', import.meta.url))\n    }\n  }\n})\n"
  }
]