[
  {
    "path": ".gitignore",
    "content": ".idea/\n_misc/\nnode_modules/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 3.0.0 @ 2020-03-20 WIP\n\n- refactoring for ES6\n- npm package creation\n\n\n## 0.2.2b @ 2014-11-25\n\n**Features**\n\n- detection for circular objects\n\n**Fixes**\n\n- several fixes for radius handling\n- several click-hit issues fixed\n\n\n## 0.2.1b @ 2014-09-24\n\n**Features**\n\n- pyramidal buildungs and roofs\n- buildings parts are a logical unit now (i.e. OSM relations)\n- explicitly injecting OSMBuildings class into global namespace [#66](https://github.com/kekscom/osmbuildings/issues/66)\n\n\n## 0.2.0b @ 2014-09-04\n\n*From this version on, OSM Buildings is entering beta phase \\o/*\n\n**Features**\n\n- Buildings are clickable now, use .click(function(featureId) {...})\n- Massive improvements in GeoJSON reading, bigger set of properties and GeometryCollections are supported\n- Ambient shadows for buildings added\n- Introduced a data service for filtering and caching OSM data, results in massive speedup for loading\n- Geometry: cones enabled, also used as an interim replacement for domes\n- Tested device accelerated perspective aka Amazon's 'Dynamic Perspective', but disabled again in favor for performance\n- Successfully tested with LeafletJS 0.8 and OpenLayers 2.13.1\n- Code size reduced from 10.23 to 9.44k (all gzipped)\n\n**Fixes**\n\n- Height scale fixed\n- Relation properties precedence fixed\n- Simple buildings layer refactored\n- Flipped perspective on some latitudes fixed\n- Tried requestAnimationFrame, but needed to drop again for IE and iOS\n\n\n## 0.1.9a @ 2013-10-17\n\n- multipolygon support added\n- backend removed, now using web services with GeoJSON or OSM Overpass XAPI\n- vector data is subdivided into tiles\n- data tiles are cached\n- fix for chained method calls\n- fix for flat buildings from rendering tall buildings too\n- min zoom level decreased to 15\n- fix for setStyle() removing shadows\n- material color mapping added\n- HSLA color support added\n- support for W3C named colors added\n- CORS-XHR support for MSIE added\n- cylindric object rendering added\n- API is now documented in GitHub README\n- map engine adapters simplified\n- minHeight and height units for GeoJSON enabled\n- very simple fix for building occlusion\n- successful tests with LeafletJS 0.6.4\n\n\n## 0.1.8a @ 2013-03-10\n\n- on layer removal from map, OSM Buildings is not destroyed anymore\n- introduced multiple rendering layers\n- improved simplification algorithm, inspired by Vladimir Agafonkin (https://mourner.github.com/simplify-js)\n- initial version of objects draw order (farthest first, lower first)\n- directional wall shading added\n- building shadows added\n- shadow date / time dependency added, inspired by Vladimir Agafonkin (https://github.com/mourner/suncalc)\n- `min_height` support added (requires backend change)\n- color / style table handling improved\n- rendering tests added\n- successful tests with LeafletJS 0.5.1\n- recommendation: reduce building `$heightScale` in backend server config down to 1.2\n\n\n## 0.1.7a @ 2012-10-10\n\n- adding OpenLayers support, credits to Jérémy Judéaux (https://github.com/Volune)\n- aligning Layer naming convention to engines\n- fixing some rare cases where layer got removed\n\n\n## 0.1.6a @ 2012-09-04\n\n- GeoJSON: min zoom removed\n- GeoJSON: height property re-enabled\n- GeoJSON: multi polygons enabled\n- Examples are rebuilt entirely\n- Roof colors are re-enbled\n- JSHint is now part of the build process\n\n\n## 0.1.5a\n\n- support for GeoJSON improved\n- deep integration with Leaflet in order to avoid jittery movement\n- enabled individual building colors\n- polygon winding fixed\n\n\n## 0.1.0a\n\n- GeoJSON support added\n- method chaining added\n- adding converter PostGIS > MySQL\n- data for Frankfurt added\n- made either MySQL or PostGIS (Mapnik) fully configurable\n- lat/lon order of your coordinates is configurable\n- polygon direction is forced to be clockwise\n- simpler initialization process\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright (c) 2020, OSM Buildings, Jan Marsch <mail@osmbuildings.org>\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are\npermitted provided that the following conditions are met:\n\n  1. Redistributions of source code must retain the above copyright notice, this list of\n     conditions and the following disclaimer.\n\n  2. Redistributions in binary form must reproduce the above copyright notice, this list\n     of conditions and the following disclaimer in the documentation and/or other materials\n     provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\nHOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR\nTORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "PERFORMANCE.md",
    "content": "# Performance\n\nWhat's done & why in terms of performance.\n\n## IDEAS\n\n- combine canvases\n- render items of same style at once\n- delete and redraw animated objects\n- split off animated shadows\n- combine simple buildings in low zoom\n- use web workers for data processing\n- adapt features dynamically\n- pre-calc face direction\n- consider using a render queue\n- style lookup index\n\n\n## Projection cache\n\nLooks like this has been a bad idea - removed.\nProbably develop the concept into rendering pipelines.\n(http://jsperf.com/projcache)\n\n\n## Readable keys (for compact data structures)\n\nSeemed to be a good idea making these a bit more readable.\nLoss expected but turned out to be a slight gain.\nhttp://jsperf.com/readable-keys/2\nTODO: Not true anymore. Refactor to objects/properties. DONE\n\n\n## Combined 2d faces\n\niPhone4, iOS5: seperate faces are 56% slower\nMBA 2010, Chrome: combined faces are 68% slower\nStaying with combined so far, probably add system detection.\n(http://jsperf.com/canvas-polygon-combiner)\n\n\n## Typed arrays\n\nUsing slice() is not worth it, but it turned out dropping some typed arrays is a good idea.\nEspecially when these are created on each rendering pass.\n(http://jsperf.com/slice-access)\n\n\n## Canvas anti alias\n\nWhile looking slick, it eats performance.\nNot so on iPhone4, iOS5: it doesn't matter\nMBA 2010, Chrome: anti alias is 24% slower\nNode: for *any* test, stroking lines eats ~60% performance\n(http://jsperf.com/canvas-anti-alias)\n\n\n## Math round\n\nUsing ~~ for a while, it turns out, bit shift << 0 is even faster.\niPad4, iOS6: 12% faster\nMBA 2010, Chrome: 25% faster\n(http://jsperf.com/math-round-vs-hack/3)\n\n\n## Public methods vs. closure(d) functions\n\nHuge gain for methods on desktop browsers vs. Safari (mobile).\nWill stay a bit until mobile catches up.\nFinally, Safari 6 mobile beats it too.\nhttp://jsperf.com/osmb-method-vs-function\n\n\n## Considerations for further performance improvement\n\ndegrade instantly, increase slowly (take average score of a few passes)\n\n### FADE IN\n- NO_STROKES\n- NO_SHADING\n- NO_SHADOWS_SCALE\n- NO_SCALE\n\n### MOVE\n\n### STATIC\n- NO_STROKES\n- NO_SHADING\n- NO_FLAT\n- NO_SHADOWS\n\n\nhttp://jsperf.com/osmb-hidden-canvas4"
  },
  {
    "path": "README.md",
    "content": "\n<img src=\"https://osmbuildings.org/logo.svg\" width=\"100\" height=\"88\">\n\nOSM Buildings is a JavaScript library for showing building geometry on interactive maps.\n\nExample: https://osmbuildings.org/\n\n**The standalone WebGL version odf OSM Buildings is located here: https://github.com/OSMBuildings/OSMBuildings**\n\nThere is also documentation of OSM Buildings Server side:\nhttps://github.com/kekscom/osmbuildings/blob/master/docs/server.md\n\n**Example** https://osmbuildings.org/\n\nIt's safe use the [master branch](https://github.com/kekscom/osmbuildings/tree/master/dist/) for production.\n\nFor further information visit https://osmbuildings.org, follow [@osmbuildings](https://twitter.com/osmbuildings/) on Twitter or report issues [here on Github](https://github.com/kekscom/osmbuildings/issues/).\n\n\n## Documentation\n\n### Integration with Leaflet\n\nLink Leaflet and OSM Buildings files in your HTML head section.\n\n~~~ html\n<head>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.css\"/>\n  <script src=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.js\"></script>\n  <script src=\"OSMBuildings-Leaflet.js\"></script>\n</head>\n~~~\n\nInitialize the map engine and add a map tile layer.<br>\nPosition is set to Berlin at zoom level 17, I'm using Mapbox tiles here.\n\n~~~ javascript\nvar map = new L.Map('map').setView([52.52020, 13.37570], 17);\nnew L.TileLayer('https://{s}.tiles.mapbox.com/v3/<YOUR KEY HERE>/{z}/{x}/{y}.png',\n  { attribution: 'Map tiles &copy; <a href=\"https://mapbox.com\">Mapbox</a>', maxZoom: 17 }).addTo(map);\n~~~\n\nAdd the buildings layer.\n\n~~~ javascript\nnew OSMBuildings(map).load();\n~~~\n\nAs a popular alternative, you could pass a <a href=\"http://www.geojson.org/geojson-spec.html\">GeoJSON</a> FeatureCollection object.<br>\nGeometry types Polygon, Multipolygon and GeometryCollection are supported.<br>\nMake sure the building coordinates are projected in <a href=\"http://spatialreference.org/ref/epsg/4326/\">EPSG:4326</a>.<br>\nHeight units m, ft, yd, mi are accepted, no given unit defaults to meters.\n\n~~~ javascript\nvar geoJSON = {\n  \"type\": \"FeatureCollection\",\n  \"features\": [{\n    \"type\": \"Feature\",\n    \"id\": 134,\n    \"geometry\": {\n      \"type\": \"Polygon\",\n      \"coordinates\": [[\n        [13.37356, 52.52064],\n        [13.37350, 52.51971],\n        [13.37664, 52.51973],\n        [13.37594, 52.52062],\n        [13.37356, 52.52064]\n      ]]\n    },\n    \"properties\": {\n      \"wallColor\": \"rgb(255,0,0)\",\n      \"roofColor\": \"rgb(255,128,0)\",\n      \"height\": 500,\n      \"minHeight\": 0\n    }\n  }]\n};\n\nnew OSMBuildings(map).set(geoJSON);\n~~~\n\n\n### Integration with OpenLayers\n\n* NEW: for Integration with OpenLayers 5 see /tests/openlayers-5.3.0 *\n\nLink OpenLayers and OSM Buildings files in your HTML head section.\n\n~~~ html\n<head>\n  <script src=\"http://www.openlayers.org/api/OpenLayers.js\"></script>\n  <script src=\"OSMBuildings-OpenLayers.js\"></script>\n</head>\n~~~\n\nInitialize the map engine and add a map tile layer.<br>\nPosition is set to Berlin at zoom level 17.\n\n~~~ javascript\nvar map = new OpenLayers.Map('map');\nmap.addControl(new OpenLayers.Control.LayerSwitcher());\n\nvar osm = new OpenLayers.Layer.OSM();\nmap.addLayer(osm);\n\nmap.setCenter(\n  new OpenLayers.LonLat(13.37570, 52.52020)\n    .transform(\n      new OpenLayers.Projection('EPSG:4326'),\n      map.getProjectionObject()\n    ),\n  17\n);\n~~~\n\nAdd the buildings layer.\n\n~~~ javascript\nnew OSMBuildings(map).load();\n~~~\n\n\n## API\n\n### Constructors\n\n<table>\n<tr>\n<th>Constructor</th>\n<th>Description</th>\n</tr>\n\n<tr>\n<td>new OSMBuildings(map)</td>\n<td>Initializes the buildings layer for a given map engine.<br>\nCurrently Leaflet and OpenLayers are supported.</td>\n</tr>\n</table>\n\nConstants\n\n<table>\n<tr>\n<th>Option</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n\n<tr>\n<td>ATTRIBUTION</td>\n<td>String</td>\n<td>Holds OSM Buildings copyright information.</td>\n</tr>\n\n<tr>\n<td>VERSION</td>\n<td>String</td>\n<td>Holds current version information.</td>\n</tr>\n</table>\n\nMethods\n\n<table>\n<tr>\n<th>Method</th>\n<th>Description</th>\n</tr>\n\n<tr>\n<td>style({Object})</td>\n<td>Set default styles. See below for details.</td>\n</tr>\n\n<tr>\n<td>date(new Date(2017, 15, 1, 10, 30)))</td>\n<td>Set date/time for shadow projection.</td>\n</tr>\n\n<tr>\n<td>each({Function})</td>\n<td>A callback wrapper to override each feature's properties on read. Return false in order to skip a particular feature.<br>\nCallback receives a feature object as argument.</td>\n</tr>\n\n<tr>\n<td>click({Function})</td>\n<td>A callback wrapper to handle click events on features.<br>\nCallback receives an object { featureId{number,string}, lat{float}, lon{float} } as argument.</td>\n</tr>\n\n<tr>\n<td>set({GeoJSON FeatureCollection})</td>\n<td>Just add GeoJSON data to your map.</td>\n</tr>\n\n<tr>\n<td>load({Provider})</td>\n<td>Without parameter, it loads OpenStreetMap data tiles via an OSM Buildings proxy. This proxy enables transparent data filtering and caching.\nInterface of such provider is to be published.</td>\n</tr>\n</table>\n\nStyles\n\n<table>\n<tr>\n<th>Option</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n\n<tr>\n<td>color/wallColor</td>\n<td>String</td>\n<td>Defines the objects default primary color. I.e. #ffcc00, rgb(255,200,200), rgba(255,200,200,0.9)</td>\n</tr>\n\n<tr>\n<td>roofColor</td>\n<td>String</td>\n<td>Defines the objects default roof color.</td>\n</tr>\n\n<tr>\n<td>shadows</td>\n<td>Boolean</td>\n<td>Enables or disables shadow rendering, default: enabled</td>\n</tr>\n</table>\n\n\n## Data\n\n### OSM Tags used\n\n<table>\n<tr>\n<th>GeoJSON property</th>\n<th>OSM Tags</th>\n</tr>\n\n<tr>\n<td>height</td>\n<td>height, building:height, levels, building:levels</td>\n</tr>\n\n<tr>\n<td>minHeight</td>\n<td>min_height, building:min_height, min_level, building:min_level</td>\n</tr>\n\n<tr>\n<td>color/wallColor</td>\n<td>building:color, building:colour</td>\n</tr>\n\n<tr>\n<td>material</td>\n<td>building:material, building:facade:material, building:cladding</td>\n</tr>\n\n<tr>\n<td>roofColor</td>\n<td>roof:color, roof:colour, building:roof:color, building:roof:colour</td>\n</tr>\n\n<tr>\n<td>roofMaterial</td>\n<td>roof:material, building:roof:material</td>\n</tr>\n\n<tr>\n<td>shape</td>\n<td>building:shape[=cylinder,sphere]</td>\n</tr>\n\n<tr>\n<td>roofShape</td>\n<td>roof:shape[=dome]</td>\n</tr>\n\n<tr>\n<td>roofHeight</td>\n<td>roof:height</td>\n</tr>\n</table>\n"
  },
  {
    "path": "build.js",
    "content": "\nconst fs = require('fs');\nconst Terser = require('terser');\n\n\n//*****************************************************************************\n\nconst package = require('./package.json');\n\nconst src = `${__dirname}/src`;\nconst dist = `${__dirname}/dist`;\n\nconst code = [\n  \"src/shortcuts.js\",\n  \"node_modules/qolor/dist/Qolor.debug.js\",\n  \"src/lib/getSunPosition.js\",\n  \"src/GeoJSON.js\",\n  \"src/variables.js\",\n  \"src/geometry.js\",\n  \"src/functions.js\",\n  \"src/Request.js\",\n  \"src/Data.js\",\n  \"src/geometry/Extrusion.js\",\n  \"src/geometry/Cylinder.js\",\n  \"src/geometry/Pyramid.js\",\n  \"src/layers/index.js\",\n  \"src/layers/Buildings.js\",\n  \"src/layers/Simplified.js\",\n  \"src/layers/Shadows.js\",\n  \"src/layers/Picking.js\",\n  \"src/Debug.js\",\n  \"src/adapter.js\"\n];\n\n\n//*****************************************************************************\n\nfunction joinFiles (files) {\n  if (!files.push) {\n    files = [files];\n  }\n  return files.map(file => fs.readFileSync(file)).join('\\n');\n}\n\nfunction copy (srcFile, distFile) {\n  fs.writeFileSync(distFile, fs.readFileSync(srcFile, 'utf8'));\n}\n\n\n//*****************************************************************************\n\nfunction buildEngine (name, customJS) {\n  const commonJS = joinFiles(code);\n\n  let js = commonJS + '\\n' + customJS;\n  js = js.replace(/\\{\\{VERSION\\}\\}/g, package.version);\n  js = `const OSMBuildings = (function() {\\n${js}\\n return OSMBuildings;\\n}());`;\n\n  fs.writeFileSync(`${dist}/OSMBuildings-${name}.debug.js`, js);\n  fs.writeFileSync(`${dist}/OSMBuildings-${name}.js`, Terser.minify(js).code);\n  copy(`${src}/engines/index-${name}.html`, `${dist}/index-${name}.html`);\n}\n\n\n//*****************************************************************************\n\nif (!fs.existsSync(dist)) {\n  fs.mkdirSync(dist);\n}\n\nbuildEngine('Leaflet', fs.readFileSync(`${src}/engines/Leaflet.js`));\nbuildEngine('OpenLayers', fs.readFileSync(`${src}/engines/OpenLayers.js`));\n\ncopy(`${src}/OSMBuildings.css`, `${dist}/OSMBuildings.css`);\n"
  },
  {
    "path": "dist/OSMBuildings-Leaflet.debug.js",
    "content": "const OSMBuildings = (function() {\n\nconst\n  m = Math,\n  exp = m.exp,\n  log = m.log,\n  sin = m.sin,\n  cos = m.cos,\n  tan = m.tan,\n  atan = m.atan,\n  atan2 = m.atan2,\n  min = m.min,\n  max = m.max,\n  sqrt = m.sqrt,\n  ceil = m.ceil,\n  pow = m.pow;\n\n\n/**\n * @class\n */\nclass Qolor {\n\n  /**\n   * @constructor\n   * @param r {Number} 0.0 .. 1.0 red value of a color\n   * @param g {Number} 0.0 .. 1.0 green value of a color\n   * @param b {Number} 0.0 .. 1.0 blue value of a color\n   * @param a {Number} 0.0 .. 1.0 alpha value of a color, default 1\n   */\n  constructor (r, g, b, a = 1) {\n    this.r = this._clamp(r, 1);\n    this.g = this._clamp(g, 1);\n    this.b = this._clamp(b, 1);\n    this.a = this._clamp(a, 1);\n  }\n\n  /**\n   * @param str {String} can be any color dfinition like: 'red', '#0099ff', 'rgb(64, 128, 255)', 'rgba(64, 128, 255, 0.5)'\n   */\n  static parse (str) {\n    if (typeof str === 'string') {\n      str = str.toLowerCase();\n      str = Qolor.w3cColors[str] || str;\n\n      let m;\n\n      if ((m = str.match(/^#?(\\w{2})(\\w{2})(\\w{2})$/))) {\n        return new Qolor(parseInt(m[1], 16)/255, parseInt(m[2], 16)/255, parseInt(m[3], 16)/255);\n      }\n\n      if ((m = str.match(/^#?(\\w)(\\w)(\\w)$/))) {\n        return new Qolor(parseInt(m[1]+m[1], 16)/255, parseInt(m[2]+m[2], 16)/255, parseInt(m[3]+m[3], 16)/255);\n      }\n\n      if ((m = str.match(/rgba?\\((\\d+)\\D+(\\d+)\\D+(\\d+)(\\D+([\\d.]+))?\\)/))) {\n        return new Qolor(\n          parseFloat(m[1])/255,\n          parseFloat(m[2])/255,\n          parseFloat(m[3])/255,\n          m[4] ? parseFloat(m[5]) : 1\n        );\n      }\n    }\n\n    return new Qolor();\n  }\n\n  static fromHSL (h, s, l, a) {\n    const qolor = new Qolor().fromHSL(h, s, l);\n    qolor.a = a === undefined ? 1 : a;\n    return qolor;\n  }\n\n  //***************************************************************************\n\n  _hue2rgb(p, q, t) {\n    if (t<0) t += 1;\n    if (t>1) t -= 1;\n    if (t<1/6) return p + (q - p)*6*t;\n    if (t<1/2) return q;\n    if (t<2/3) return p + (q - p)*(2/3 - t)*6;\n    return p;\n  }\n\n  _clamp(v, max) {\n    if (v === undefined) {\n      return;\n    }\n    return Math.min(max, Math.max(0, v || 0));\n  }\n\n  //***************************************************************************\n\n  isValid () {\n    return this.r !== undefined && this.g !== undefined && this.b !== undefined;\n  }\n\n  toHSL () {\n    if (!this.isValid()) {\n      return;\n    }\n\n    const max = Math.max(this.r, this.g, this.b);\n    const min = Math.min(this.r, this.g, this.b);\n    const range = max - min;\n    const l = (max + min)/2;\n\n    // achromatic\n    if (!range) {\n      return { h: 0, s: 0, l: l };\n    }\n\n    const s = l > 0.5 ? range/(2 - max - min) : range/(max + min);\n\n    let h;\n    switch (max) {\n      case this.r:\n        h = (this.g - this.b)/range + (this.g<this.b ? 6 : 0);\n        break;\n      case this.g:\n        h = (this.b - this.r)/range + 2;\n        break;\n      case this.b:\n        h = (this.r - this.g)/range + 4;\n        break;\n    }\n    h *= 60;\n\n    return { h: h, s: s, l: l };\n  }\n\n  fromHSL (h, s, l) {\n    // h = this._clamp(h, 360),\n    // s = this._clamp(s, 1),\n    // l = this._clamp(l, 1),\n\n    // achromatic\n    if (s === 0) {\n      this.r = this.g = this.b = l;\n      return this;\n    }\n\n    const q = l<0.5 ? l*(1 + s) : l + s - l*s;\n    const p = 2*l - q;\n\n    h /= 360;\n\n    this.r = this._hue2rgb(p, q, h + 1/3);\n    this.g = this._hue2rgb(p, q, h);\n    this.b = this._hue2rgb(p, q, h - 1/3);\n\n    return this;\n  }\n\n  toString () {\n    if (!this.isValid()) {\n      return;\n    }\n\n    if (this.a === 1) {\n      return '#' + ((1<<24) + (Math.round(this.r*255)<<16) + (Math.round(this.g*255)<<8) + Math.round(this.b*255)).toString(16).slice(1, 7);\n    }\n    return `rgba(${Math.round(this.r*255)},${Math.round(this.g*255)},${Math.round(this.b*255)},${this.a.toFixed(2)})`;\n  }\n\n  toArray () {\n    if (!this.isValid) {\n      return;\n    }\n    return [this.r, this.g, this.b];\n  }\n\n  hue (h) {\n    const hsl = this.toHSL();\n    return this.fromHSL(hsl.h+h, hsl.s, hsl.l);\n  }\n\n  saturation (s) {\n    const hsl = this.toHSL();\n    return this.fromHSL(hsl.h, hsl.s*s, hsl.l);\n  }\n\n  lightness (l) {\n    const hsl = this.toHSL();\n    return this.fromHSL(hsl.h, hsl.s, hsl.l*l);\n  }\n\n  clone () {\n    return new Qolor(this.r, this.g, this.b, this.a);\n  }\n}\n\nQolor.w3cColors = {\n  aliceblue: '#f0f8ff',\n  antiquewhite: '#faebd7',\n  aqua: '#00ffff',\n  aquamarine: '#7fffd4',\n  azure: '#f0ffff',\n  beige: '#f5f5dc',\n  bisque: '#ffe4c4',\n  black: '#000000',\n  blanchedalmond: '#ffebcd',\n  blue: '#0000ff',\n  blueviolet: '#8a2be2',\n  brown: '#a52a2a',\n  burlywood: '#deb887',\n  cadetblue: '#5f9ea0',\n  chartreuse: '#7fff00',\n  chocolate: '#d2691e',\n  coral: '#ff7f50',\n  cornflowerblue: '#6495ed',\n  cornsilk: '#fff8dc',\n  crimson: '#dc143c',\n  cyan: '#00ffff',\n  darkblue: '#00008b',\n  darkcyan: '#008b8b',\n  darkgoldenrod: '#b8860b',\n  darkgray: '#a9a9a9',\n  darkgrey: '#a9a9a9',\n  darkgreen: '#006400',\n  darkkhaki: '#bdb76b',\n  darkmagenta: '#8b008b',\n  darkolivegreen: '#556b2f',\n  darkorange: '#ff8c00',\n  darkorchid: '#9932cc',\n  darkred: '#8b0000',\n  darksalmon: '#e9967a',\n  darkseagreen: '#8fbc8f',\n  darkslateblue: '#483d8b',\n  darkslategray: '#2f4f4f',\n  darkslategrey: '#2f4f4f',\n  darkturquoise: '#00ced1',\n  darkviolet: '#9400d3',\n  deeppink: '#ff1493',\n  deepskyblue: '#00bfff',\n  dimgray: '#696969',\n  dimgrey: '#696969',\n  dodgerblue: '#1e90ff',\n  firebrick: '#b22222',\n  floralwhite: '#fffaf0',\n  forestgreen: '#228b22',\n  fuchsia: '#ff00ff',\n  gainsboro: '#dcdcdc',\n  ghostwhite: '#f8f8ff',\n  gold: '#ffd700',\n  goldenrod: '#daa520',\n  gray: '#808080',\n  grey: '#808080',\n  green: '#008000',\n  greenyellow: '#adff2f',\n  honeydew: '#f0fff0',\n  hotpink: '#ff69b4',\n  indianred: '#cd5c5c',\n  indigo: '#4b0082',\n  ivory: '#fffff0',\n  khaki: '#f0e68c',\n  lavender: '#e6e6fa',\n  lavenderblush: '#fff0f5',\n  lawngreen: '#7cfc00',\n  lemonchiffon: '#fffacd',\n  lightblue: '#add8e6',\n  lightcoral: '#f08080',\n  lightcyan: '#e0ffff',\n  lightgoldenrodyellow: '#fafad2',\n  lightgray: '#d3d3d3',\n  lightgrey: '#d3d3d3',\n  lightgreen: '#90ee90',\n  lightpink: '#ffb6c1',\n  lightsalmon: '#ffa07a',\n  lightseagreen: '#20b2aa',\n  lightskyblue: '#87cefa',\n  lightslategray: '#778899',\n  lightslategrey: '#778899',\n  lightsteelblue: '#b0c4de',\n  lightyellow: '#ffffe0',\n  lime: '#00ff00',\n  limegreen: '#32cd32',\n  linen: '#faf0e6',\n  magenta: '#ff00ff',\n  maroon: '#800000',\n  mediumaquamarine: '#66cdaa',\n  mediumblue: '#0000cd',\n  mediumorchid: '#ba55d3',\n  mediumpurple: '#9370db',\n  mediumseagreen: '#3cb371',\n  mediumslateblue: '#7b68ee',\n  mediumspringgreen: '#00fa9a',\n  mediumturquoise: '#48d1cc',\n  mediumvioletred: '#c71585',\n  midnightblue: '#191970',\n  mintcream: '#f5fffa',\n  mistyrose: '#ffe4e1',\n  moccasin: '#ffe4b5',\n  navajowhite: '#ffdead',\n  navy: '#000080',\n  oldlace: '#fdf5e6',\n  olive: '#808000',\n  olivedrab: '#6b8e23',\n  orange: '#ffa500',\n  orangered: '#ff4500',\n  orchid: '#da70d6',\n  palegoldenrod: '#eee8aa',\n  palegreen: '#98fb98',\n  paleturquoise: '#afeeee',\n  palevioletred: '#db7093',\n  papayawhip: '#ffefd5',\n  peachpuff: '#ffdab9',\n  peru: '#cd853f',\n  pink: '#ffc0cb',\n  plum: '#dda0dd',\n  powderblue: '#b0e0e6',\n  purple: '#800080',\n  rebeccapurple: '#663399',\n  red: '#ff0000',\n  rosybrown: '#bc8f8f',\n  royalblue: '#4169e1',\n  saddlebrown: '#8b4513',\n  salmon: '#fa8072',\n  sandybrown: '#f4a460',\n  seagreen: '#2e8b57',\n  seashell: '#fff5ee',\n  sienna: '#a0522d',\n  silver: '#c0c0c0',\n  skyblue: '#87ceeb',\n  slateblue: '#6a5acd',\n  slategray: '#708090',\n  slategrey: '#708090',\n  snow: '#fffafa',\n  springgreen: '#00ff7f',\n  steelblue: '#4682b4',\n  tan: '#d2b48c',\n  teal: '#008080',\n  thistle: '#d8bfd8',\n  tomato: '#ff6347',\n  turquoise: '#40e0d0',\n  violet: '#ee82ee',\n  wheat: '#f5deb3',\n  white: '#ffffff',\n  whitesmoke: '#f5f5f5',\n  yellow: '#ffff00',\n  yellowgreen: '#9acd32'\n};\n\nif (typeof module !== 'undefined') {\n  module.exports = Qolor;\n}\n\n// calculations are based on http://aa.quae.nl/en/reken/zonpositie.html\n// code credits to Vladimir Agafonkin (@mourner)\n\nfunction getSunPosition () {\n\n  const m = Math,\n    PI = m.PI,\n    sin = m.sin,\n    cos = m.cos,\n    tan = m.tan,\n    asin = m.asin,\n    atan = m.atan2;\n\n  const rad = PI/180,\n    dayMs = 1000*60*60*24,\n    J1970 = 2440588,\n    J2000 = 2451545,\n    e = rad*23.4397; // obliquity of the Earth\n\n  function toJulian(date) {\n    return date.valueOf()/dayMs - 0.5+J1970;\n  }\n  function toDays(date) {\n    return toJulian(date)-J2000;\n  }\n  function getRightAscension(l, b) {\n    return atan(sin(l)*cos(e) - tan(b)*sin(e), cos(l));\n  }\n  function getDeclination(l, b) {\n    return asin(sin(b)*cos(e) + cos(b)*sin(e)*sin(l));\n  }\n  function getAzimuth(H, phi, dec) {\n    return atan(sin(H), cos(H)*sin(phi) - tan(dec)*cos(phi));\n  }\n  function getAltitude(H, phi, dec) {\n    return asin(sin(phi)*sin(dec) + cos(phi)*cos(dec)*cos(H));\n  }\n  function getSiderealTime(d, lw) {\n    return rad * (280.16 + 360.9856235*d) - lw;\n  }\n  function getSolarMeanAnomaly(d) {\n    return rad * (357.5291 + 0.98560028*d);\n  }\n  function getEquationOfCenter(M) {\n    return rad * (1.9148*sin(M) + 0.0200 * sin(2*M) + 0.0003 * sin(3*M));\n  }\n  function getEclipticLongitude(M, C) {\n    const P = rad*102.9372; // perihelion of the Earth\n    return M+C+P+PI;\n  }\n\n  return function getSunPosition(date, lat, lon) {\n    const lw = rad*-lon,\n      phi = rad*lat,\n      d = toDays(date),\n      M = getSolarMeanAnomaly(d),\n      C = getEquationOfCenter(M),\n      L = getEclipticLongitude(M, C),\n      D = getDeclination(L, 0),\n      A = getRightAscension(L, 0),\n      t = getSiderealTime(d, lw),\n      H = t-A;\n\n    return {\n      altitude: getAltitude(H, phi, D),\n      azimuth: getAzimuth(H, phi, D) - PI/2 // origin: north\n    };\n  };\n}\n\n\nconst METERS_PER_LEVEL = 3;\n\nconst materialColors = {\n  brick:'#cc7755',\n  bronze:'#ffeecc',\n  canvas:'#fff8f0',\n  concrete:'#999999',\n  copper:'#a0e0d0',\n  glass:'#e8f8f8',\n  gold:'#ffcc00',\n  plants:'#009933',\n  metal:'#aaaaaa',\n  panel:'#fff8f0',\n  plaster:'#999999',\n  roof_tiles:'#f08060',\n  silver:'#cccccc',\n  slate:'#666666',\n  stone:'#996666',\n  tar_paper:'#333333',\n  wood:'#deb887'\n};\n\nconst baseMaterials = {\n  asphalt:'tar_paper',\n  bitumen:'tar_paper',\n  block:'stone',\n  bricks:'brick',\n  glas:'glass',\n  glassfront:'glass',\n  grass:'plants',\n  masonry:'stone',\n  granite:'stone',\n  panels:'panel',\n  paving_stones:'stone',\n  plastered:'plaster',\n  rooftiles:'roof_tiles',\n  roofingfelt:'tar_paper',\n  sandstone:'stone',\n  sheet:'canvas',\n  sheets:'canvas',\n  shingle:'tar_paper',\n  shingles:'tar_paper',\n  slates:'slate',\n  steel:'metal',\n  tar:'tar_paper',\n  tent:'canvas',\n  thatch:'plants',\n  tile:'roof_tiles',\n  tiles:'roof_tiles'\n};\n// cardboard\n// eternit\n// limestone\n// straw\n\nfunction getMaterialColor (str) {\n  str = str.toLowerCase();\n  if (str[0] === '#') {\n    return str;\n  }\n  return materialColors[baseMaterials[str] || str] || null;\n}\n\nconst WINDING_CLOCKWISE = 'CW';\nconst WINDING_COUNTER_CLOCKWISE = 'CCW';\n\n// detect winding direction: clockwise or counter clockwise\nfunction getWinding (points) {\n  let x1, y1, x2, y2,\n    a = 0;\n  for (let i = 0, il = points.length-3; i < il; i += 2) {\n    x1 = points[i];\n    y1 = points[i+1];\n    x2 = points[i+2];\n    y2 = points[i+3];\n    a += x1*y2 - x2*y1;\n  }\n  return (a/2) > 0 ? WINDING_CLOCKWISE : WINDING_COUNTER_CLOCKWISE;\n}\n\n// enforce a polygon winding direcetion. Needed for proper backface culling.\nfunction makeWinding (points, direction) {\n  let winding = getWinding(points);\n  if (winding === direction) {\n    return points;\n  }\n  let revPoints = [];\n  for (let i = points.length-2; i >= 0; i -= 2) {\n    revPoints.push(points[i], points[i+1]);\n  }\n  return revPoints;\n}\n\nfunction alignProperties(prop) {\n  const item = {};\n\n  prop = prop || {};\n\n  item.height    = prop.height    || (prop.levels   ? prop.levels  *METERS_PER_LEVEL : DEFAULT_HEIGHT);\n  item.minHeight = prop.minHeight || (prop.minLevel ? prop.minLevel*METERS_PER_LEVEL : 0);\n\n  const wallColor = prop.material ? getMaterialColor(prop.material) : (prop.wallColor || prop.color);\n  if (wallColor) {\n    item.wallColor = wallColor;\n  }\n\n  const roofColor = prop.roofMaterial ? getMaterialColor(prop.roofMaterial) : prop.roofColor;\n  if (roofColor) {\n    item.roofColor = roofColor;\n  }\n\n  switch (prop.shape) {\n    case 'cylinder':\n    case 'cone':\n    case 'dome':\n    case 'sphere':\n      item.shape = prop.shape;\n      item.isRotational = true;\n    break;\n\n    case 'pyramid':\n      item.shape = prop.shape;\n    break;\n  }\n\n  switch (prop.roofShape) {\n    case 'cone':\n    case 'dome':\n      item.roofShape = prop.roofShape;\n      item.isRotational = true;\n    break;\n\n    case 'pyramid':\n      item.roofShape = prop.roofShape;\n    break;\n  }\n\n  if (item.roofShape && prop.roofHeight) {\n    item.roofHeight = prop.roofHeight;\n    item.height = max(0, item.height-item.roofHeight);\n  } else {\n    item.roofHeight = 0;\n  }\n\n  return item;\n}\n\nfunction getGeometries (geometry) {\n  let\n    polygon,\n    geometries = [], sub;\n\n  switch (geometry.type) {\n    case 'GeometryCollection':\n      geometries = [];\n      for (let i = 0, il = geometry.geometries.length; i < il; i++) {\n        if ((sub = getGeometries(geometry.geometries[i]))) {\n          geometries.push.apply(geometries, sub);\n        }\n      }\n      return geometries;\n\n    case 'MultiPolygon':\n      geometries = [];\n      for (let i = 0, il = geometry.coordinates.length; i < il; i++) {\n        if ((sub = getGeometries({ type: 'Polygon', coordinates: geometry.coordinates[i] }))) {\n          geometries.push.apply(geometries, sub);\n        }\n      }\n      return geometries;\n\n    case 'Polygon':\n      polygon = geometry.coordinates;\n    break;\n\n    default: return [];\n  }\n\n  let\n    p, lat = 1, lon = 0,\n    outer = [], inner = [];\n\n  p = polygon[0];\n  for (let i = 0, il = p.length; i < il; i++) {\n    outer.push(p[i][lat], p[i][lon]);\n  }\n  outer = makeWinding(outer, WINDING_CLOCKWISE);\n\n  for (let i = 0, il = polygon.length-1; i < il; i++) {\n    p = polygon[i+1];\n    inner[i] = [];\n    for (let j = 0, jl = p.length; j < jl; j++) {\n      inner[i].push(p[j][lat], p[j][lon]);\n    }\n    inner[i] = makeWinding(inner[i], WINDING_COUNTER_CLOCKWISE);\n  }\n\n  return [{\n    outer: outer,\n    inner: inner.length ? inner : null\n  }];\n}\n\nfunction clone (obj) {\n  let res = {};\n  for (const p in obj) {\n    if (obj.hasOwnProperty(p)) {\n      res[p] = obj[p];\n    }\n  }\n  return res;\n}\n\nclass GeoJSON {\n\n  static read (geojson) {\n    if (!geojson || geojson.type !== 'FeatureCollection') {\n      return [];\n    }\n\n    const collection = geojson.features;\n    const res = [];\n\n    for (let i = 0, il = collection.length; i < il; i++) {\n      const feature = collection[i];\n\n      if (feature.type !== 'Feature' || onEach(feature) === false) {\n        continue;\n      }\n\n      const baseItem = alignProperties(feature.properties);\n      const geometries = getGeometries(feature.geometry);\n\n      for (let j = 0, jl = geometries.length; j < jl; j++) {\n        const item = clone(baseItem);\n        item.footprint = geometries[j].outer;\n        if (item.isRotational) {\n          item.radius = getLonDelta(item.footprint);\n        }\n\n        if (geometries[j].inner) {\n          item.holes = geometries[j].inner;\n        }\n        if (feature.id || feature.properties.id) {\n          item.id = feature.id || feature.properties.id;\n        }\n\n        if (feature.properties.relationId) {\n          item.relationId = feature.properties.relationId;\n        }\n\n        res.push(item); // TODO: clone base properties!\n      }\n    }\n\n    return res;\n  }\n}\n\nlet\n  VERSION      = '0.3.2',\n  ATTRIBUTION  = '&copy; <a href=\"https://osmbuildings.org\">OSM Buildings</a>',\n\n  DATA_SRC = 'https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json',\n\n  PI         = Math.PI,\n  HALF_PI    = PI/2,\n  QUARTER_PI = PI/4,\n\n  MAP_TILE_SIZE  = 256,    // map tile size in pixels\n  ZOOM, MAP_SIZE,\n\n  MIN_ZOOM = 15,\n\n  LAT = 'latitude', LON = 'longitude',\n\n  WIDTH = 0, HEIGHT = 0,\n  CENTER_X = 0, CENTER_Y = 0,\n  ORIGIN_X = 0, ORIGIN_Y = 0,\n\n  WALL_COLOR = Qolor.parse('rgba(200, 190, 180)'),\n  ALT_COLOR  = WALL_COLOR.lightness(0.8),\n  ROOF_COLOR = WALL_COLOR.lightness(1.2),\n\n  WALL_COLOR_STR = ''+ WALL_COLOR,\n  ALT_COLOR_STR  = ''+ ALT_COLOR,\n  ROOF_COLOR_STR = ''+ ROOF_COLOR,\n\n  PIXEL_PER_DEG = 0,\n\n  MAX_HEIGHT, // taller buildings will be cut to this\n  DEFAULT_HEIGHT = 5,\n\n  CAM_X, CAM_Y, CAM_Z = 450,\n\n  IS_ZOOMING;\n\nfunction onEach () {}\n\nfunction onClick () {}\n\n\nfunction getDistance (p1, p2) {\n  const\n    dx = p1.x-p2.x,\n    dy = p1.y-p2.y;\n  return dx*dx + dy*dy;\n}\n\nfunction isRotational (polygon) {\n  const length = polygon.length;\n  if (length < 16) {\n    return false;\n  }\n\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n  for (let i = 0; i < length-1; i+=2) {\n    minX = Math.min(minX, polygon[i]);\n    maxX = Math.max(maxX, polygon[i]);\n    minY = Math.min(minY, polygon[i+1]);\n    maxY = Math.max(maxY, polygon[i+1]);\n  }\n\n  const\n    width = maxX-minX,\n    height = (maxY-minY),\n    ratio = width/height;\n\n  if (ratio < 0.85 || ratio > 1.15) {\n    return false;\n  }\n\n  const\n    center = { x:minX+width/2, y:minY+height/2 },\n    radius = (width+height)/4,\n    sqRadius = radius*radius;\n\n  for (let i = 0; i < length-1; i+=2) {\n    const dist = getDistance({ x:polygon[i], y:polygon[i+1] }, center);\n    if (dist/sqRadius < 0.8 || dist/sqRadius > 1.2) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction getSquareSegmentDistance (px, py, p1x, p1y, p2x, p2y) {\n  let\n    dx = p2x-p1x,\n    dy = p2y-p1y,\n    t;\n  if (dx !== 0 || dy !== 0) {\n    t = ((px-p1x) * dx + (py-p1y) * dy) / (dx*dx + dy*dy);\n    if (t > 1) {\n      p1x = p2x;\n      p1y = p2y;\n    } else if (t > 0) {\n      p1x += dx*t;\n      p1y += dy*t;\n    }\n  }\n  dx = px-p1x;\n  dy = py-p1y;\n  return dx*dx + dy*dy;\n}\n\nfunction simplifyPolygon (buffer) {\n  let\n    sqTolerance = 2,\n    len = buffer.length/2,\n    markers = new Uint8Array(len),\n\n    first = 0, last = len-1,\n\n    maxSqDist,\n    sqDist,\n    index,\n    firstStack = [], lastStack  = [],\n    newBuffer  = [];\n\n  markers[first] = markers[last] = 1;\n\n  while (last) {\n    maxSqDist = 0;\n    for (let i = first+1; i < last; i++) {\n      sqDist = getSquareSegmentDistance(\n        buffer[i    *2], buffer[i    *2 + 1],\n        buffer[first*2], buffer[first*2 + 1],\n        buffer[last *2], buffer[last *2 + 1]\n      );\n      if (sqDist > maxSqDist) {\n        index = i;\n        maxSqDist = sqDist;\n      }\n    }\n\n    if (maxSqDist > sqTolerance) {\n      markers[index] = 1;\n\n      firstStack.push(first);\n      lastStack.push(index);\n\n      firstStack.push(index);\n      lastStack.push(last);\n    }\n\n    first = firstStack.pop();\n    last = lastStack.pop();\n  }\n\n  for (let i = 0; i < len; i++) {\n    if (markers[i]) {\n      newBuffer.push(buffer[i*2], buffer[i*2 + 1]);\n    }\n  }\n\n  return newBuffer;\n}\n\nfunction getCenter (footprint) {\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n  for (let i = 0, il = footprint.length-3; i < il; i += 2) {\n    minX = min(minX, footprint[i]);\n    maxX = max(maxX, footprint[i]);\n    minY = min(minY, footprint[i+1]);\n    maxY = max(maxY, footprint[i+1]);\n  }\n  return { x:minX+(maxX-minX)/2 <<0, y:minY+(maxY-minY)/2 <<0 };\n}\n\nlet EARTH_RADIUS = 6378137;\n\nfunction getLonDelta (footprint) {\n  let minLon = 180, maxLon = -180;\n  for (let i = 0, il = footprint.length; i < il; i += 2) {\n    minLon = min(minLon, footprint[i+1]);\n    maxLon = max(maxLon, footprint[i+1]);\n  }\n  return (maxLon-minLon)/2;\n}\n\n\nfunction rad (deg) {\n  return deg * PI / 180;\n}\n\nfunction deg (rad) {\n  return rad / PI * 180;\n}\n\nfunction pixelToGeo (x, y) {\n  const res = {};\n  x /= MAP_SIZE;\n  y /= MAP_SIZE;\n  res[LAT] = y <= 0  ? 90 : y >= 1 ? -90 : deg(2 * atan(exp(PI * (1 - 2*y))) - HALF_PI);\n  res[LON] = (x === 1 ?  1 : (x%1 + 1) % 1) * 360 - 180;\n  return res;\n}\n\nfunction geoToPixel (lat, lon) {\n  const\n    latitude = min(1, max(0, 0.5 - (log(tan(QUARTER_PI + HALF_PI * lat / 180)) / PI) / 2)),\n    longitude = lon/360 + 0.5;\n  return {\n    x: longitude*MAP_SIZE <<0,\n    y: latitude *MAP_SIZE <<0\n  };\n}\n\nfunction fromRange (sVal, sMin, sMax, dMin, dMax) {\n  sVal = min(max(sVal, sMin), sMax);\n  const rel = (sVal-sMin) / (sMax-sMin),\n    range = dMax-dMin;\n  return min(max(dMin + rel*range, dMin), dMax);\n}\n\nfunction isVisible (polygon) {\n  const\n    maxX = WIDTH+ORIGIN_X,\n    maxY = HEIGHT+ORIGIN_Y;\n\n  // TODO: checking footprint is sufficient for visibility - NOT VALID FOR SHADOWS!\n  for (let i = 0, il = polygon.length-3; i < il; i+=2) {\n    if (polygon[i] > ORIGIN_X && polygon[i] < maxX && polygon[i+1] > ORIGIN_Y && polygon[i+1] < maxY) {\n      return true;\n    }\n  }\n  return false;\n}\n\n\nlet cacheData = {};\nlet cacheIndex = [];\nlet cacheSize = 0;\nlet maxCacheSize = 1024*1024 * 5; // 5MB\n\nfunction xhr (url, callback) {\n  if (cacheData[url]) {\n    if (callback) {\n      callback(cacheData[url]);\n    }\n    return;\n  }\n\n  const req = new XMLHttpRequest();\n\n  req.onreadystatechange = function () {\n    if (req.readyState !== 4) {\n      return;\n    }\n    if (!req.status || req.status < 200 || req.status > 299) {\n      return;\n    }\n    if (callback && req.responseText) {\n      const responseText = req.responseText;\n\n      cacheData[url] = responseText;\n      cacheIndex.push({ url: url, size: responseText.length });\n      cacheSize += responseText.length;\n\n      callback(responseText);\n\n      while (cacheSize > maxCacheSize) {\n        let item = cacheIndex.shift();\n        cacheSize -= item.size;\n        delete cacheData[item.url];\n      }\n    }\n  };\n\n  req.open('GET', url);\n  req.send(null);\n\n  return req;\n}\n\nclass Request {\n\n  static loadJSON (url, callback) {\n    return xhr(url, responseText => {\n      let json;\n      try {\n        json = JSON.parse(responseText);\n      } catch(ex) {}\n\n      callback(json);\n    });\n  }\n}\n\n\nclass Data {\n\n  static getPixelFootprint (buffer) {\n    let footprint = new Int32Array(buffer.length),\n      px;\n\n    for (let i = 0, il = buffer.length-1; i < il; i+=2) {\n      px = geoToPixel(buffer[i], buffer[i+1]);\n      footprint[i]   = px.x;\n      footprint[i+1] = px.y;\n    }\n\n    footprint = simplifyPolygon(footprint);\n    if (footprint.length < 8) { // 3 points & end==start (*2)\n      return;\n    }\n\n    return footprint;\n  }\n\n  static resetItems () {\n    this.items = [];\n    this.cache = {};\n    Picking.reset();\n  }\n\n  static addRenderItems (data, allAreNew) {\n    let item, scaledItem, id;\n    let geojson = GeoJSON.read(data);\n    for (let i = 0, il = geojson.length; i < il; i++) {\n      item = geojson[i];\n      id = item.id || [item.footprint[0], item.footprint[1], item.height, item.minHeight].join(',');\n      if (!this.cache[id]) {\n        if ((scaledItem = this.scaleItem(item))) {\n          scaledItem.scale = allAreNew ? 0 : 1;\n          this.items.push(scaledItem);\n          this.cache[id] = 1;\n        }\n      }\n    }\n    fadeIn();\n  }\n\n  static scalePolygon (buffer, factor) {\n    return buffer.map(coord => coord*factor);\n  }\n\n  static scale (factor) {\n    Data.items = Data.items.map(item => {\n      // item.height = Math.min(item.height*factor, MAX_HEIGHT); // TODO: should be filtered by renderer\n\n      item.height *= factor;\n      item.minHeight *= factor;\n\n      item.footprint = Data.scalePolygon(item.footprint, factor);\n      item.center.x *= factor;\n      item.center.y *= factor;\n\n      if (item.radius) {\n        item.radius *= factor;\n      }\n\n      if (item.holes) {\n        for (let i = 0, il = item.holes.length; i < il; i++) {\n          item.holes[i] = Data.scalePolygon(item.holes[i], factor);\n        }\n      }\n\n      item.roofHeight *= factor;\n\n      return item;\n    });\n  }\n\n  static scaleItem (item) {\n    let\n      res = {},\n      // TODO: calculate this on zoom change only\n      zoomScale = 6 / pow(2, ZOOM-MIN_ZOOM); // TODO: consider using HEIGHT / (devicePixelRatio || 1)\n\n    if (item.id) {\n      res.id = item.id;\n    }\n\n    res.height = min(item.height/zoomScale, MAX_HEIGHT);\n\n    res.minHeight = isNaN(item.minHeight) ? 0 : item.minHeight / zoomScale;\n    if (res.minHeight > MAX_HEIGHT) {\n      return;\n    }\n\n    res.footprint = this.getPixelFootprint(item.footprint);\n    if (!res.footprint) {\n      return;\n    }\n    res.center = getCenter(res.footprint);\n\n    if (item.radius) {\n      res.radius = item.radius*PIXEL_PER_DEG;\n    }\n    if (item.shape) {\n      res.shape = item.shape;\n    }\n    if (item.roofShape) {\n      res.roofShape = item.roofShape;\n    }\n    if ((res.roofShape === 'cone' || res.roofShape === 'dome') && !res.shape && isRotational(res.footprint)) {\n      res.shape = 'cylinder';\n    }\n\n    if (item.holes) {\n      res.holes = [];\n      let innerFootprint;\n      for (let i = 0, il = item.holes.length; i < il; i++) {\n        // TODO: simplify\n        if ((innerFootprint = this.getPixelFootprint(item.holes[i]))) {\n          res.holes.push(innerFootprint);\n        }\n      }\n    }\n\n    let color;\n\n    if (item.wallColor) {\n      if ((color = Qolor.parse(item.wallColor))) {\n        res.altColor  = ''+ color.lightness(0.8);\n        res.wallColor = ''+ color;\n      }\n    }\n\n    if (item.roofColor) {\n      if ((color = Qolor.parse(item.roofColor))) {\n        res.roofColor = ''+ color;\n      }\n    }\n\n    if (item.relationId) {\n      res.relationId = item.relationId;\n    }\n    res.hitColor = Picking.idToColor(item.relationId || item.id);\n\n    res.roofHeight = isNaN(item.roofHeight) ? 0 : item.roofHeight/zoomScale;\n\n    if (res.height+res.roofHeight <= res.minHeight) {\n      return;\n    }\n\n    return res;\n  }\n\n  static set (data) {\n    this.resetItems();\n    this._staticData = data;\n    this.addRenderItems(this._staticData, true);\n  }\n\n  static load (src, key) {\n    this.src = src || DATA_SRC.replace('{k}', (key || 'anonymous'));\n    this.update();\n  }\n\n  static update () {\n    this.resetItems();\n\n    if (ZOOM < MIN_ZOOM) {\n      return;\n    }\n\n    if (this._staticData) {\n      this.addRenderItems(this._staticData);\n    }\n\n    if (this.src) {\n      let\n        tileZoom = 16,\n        tileSize = 256,\n        zoomedTileSize = ZOOM > tileZoom ? tileSize << (ZOOM - tileZoom) : tileSize >> (tileZoom - ZOOM),\n        minX = ORIGIN_X / zoomedTileSize << 0,\n        minY = ORIGIN_Y / zoomedTileSize << 0,\n        maxX = ceil((ORIGIN_X + WIDTH) / zoomedTileSize),\n        maxY = ceil((ORIGIN_Y + HEIGHT) / zoomedTileSize),\n        x, y;\n\n      let scope = this;\n\n      function callback (json) {\n        scope.addRenderItems(json);\n      }\n\n      for (y = minY; y <= maxY; y++) {\n        for (x = minX; x <= maxX; x++) {\n          this.loadTile(x, y, tileZoom, callback);\n        }\n      }\n    }\n  }\n\n  static loadTile (x, y, zoom, callback) {\n    let s = 'abcd'[(x+y) % 4];\n    let url = this.src.replace('{s}', s).replace('{x}', x).replace('{y}', y).replace('{z}', zoom);\n    return Request.loadJSON(url, callback);\n  }\n}\n\nData.cache = {}; // maintain a list of cached items in order to avoid duplicates on tile borders\nData.items = [];\n\nclass Extrusion {\n\n  static draw (context, polygon, innerPolygons, height, minHeight, color, altColor, roofColor) {\n    let\n      roof = this._extrude(context, polygon, height, minHeight, color, altColor),\n      innerRoofs = [];\n\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        innerRoofs[i] = this._extrude(context, innerPolygons[i], height, minHeight, color, altColor);\n      }\n    }\n\n    context.fillStyle = roofColor;\n\n    context.beginPath();\n    this._ring(context, roof);\n    if (innerPolygons) {\n      for (let i = 0, il = innerRoofs.length; i < il; i++) {\n        this._ring(context, innerRoofs[i]);\n      }\n    }\n    context.closePath();\n    context.fill();\n  }\n\n  static _extrude (context, polygon, height, minHeight, color, altColor) {\n    let\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      _a, _b,\n      roof = [];\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Buildings.project(a, scale);\n      _b = Buildings.project(b, scale);\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set wall shading\n        if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n          context.fillStyle = altColor;\n        } else {\n          context.fillStyle = color;\n        }\n\n        context.beginPath();\n        this._ring(context, [\n           b.x,  b.y,\n           a.x,  a.y,\n          _a.x, _a.y,\n          _b.x, _b.y\n        ]);\n        context.closePath();\n        context.fill();\n      }\n\n      roof[i]   = _a.x;\n      roof[i+1] = _a.y;\n    }\n\n    return roof;\n  }\n\n  static _ring (context, polygon) {\n    context.moveTo(polygon[0], polygon[1]);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i], polygon[i+1]);\n    }\n  }\n\n  static simplified (context, polygon, innerPolygons) {\n    context.beginPath();\n    this._ringAbs(context, polygon);\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        this._ringAbs(context, innerPolygons[i]);\n      }\n    }\n    context.closePath();\n    context.fill();\n  }\n\n  static _ringAbs (context, polygon) {\n    context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y);\n    }\n  }\n\n  static shadow (context, polygon, innerPolygons, height, minHeight) {\n    let\n      mode = null,\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      _a, _b;\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Shadows.project(a, height);\n      _b = Shadows.project(b, height);\n\n      if (minHeight) {\n        a = Shadows.project(a, minHeight);\n        b = Shadows.project(b, minHeight);\n      }\n\n      // mode 0: floor edges, mode 1: roof edges\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        if (mode === 1) {\n          context.lineTo(a.x, a.y);\n        }\n        mode = 0;\n        if (!i) {\n          context.moveTo(a.x, a.y);\n        }\n        context.lineTo(b.x, b.y);\n      } else {\n        if (mode === 0) {\n          context.lineTo(_a.x, _a.y);\n        }\n        mode = 1;\n        if (!i) {\n          context.moveTo(_a.x, _a.y);\n        }\n        context.lineTo(_b.x, _b.y);\n      }\n    }\n\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        this._ringAbs(context, innerPolygons[i]);\n      }\n    }\n  }\n\n  static hitArea (context, polygon, innerPolygons, height, minHeight, color) {\n    let\n      mode = null,\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      _a, _b;\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Buildings.project(a, scale);\n      _b = Buildings.project(b, scale);\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // mode 0: floor edges, mode 1: roof edges\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        if (mode === 1) { // mode is initially undefined\n          context.lineTo(a.x, a.y);\n        }\n        mode = 0;\n        if (!i) {\n          context.moveTo(a.x, a.y);\n        }\n        context.lineTo(b.x, b.y);\n      } else {\n        if (mode === 0) { // mode is initially undefined\n          context.lineTo(_a.x, _a.y);\n        }\n        mode = 1;\n        if (!i) {\n          context.moveTo(_a.x, _a.y);\n        }\n        context.lineTo(_b.x, _b.y);\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nclass Cylinder {\n\n  static draw (context, center, radius, topRadius, height, minHeight, color, altColor, roofColor) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a1, a2;\n\n    topRadius *= scale;\n\n    if (minHeight) {\n      c = Buildings.project(c, minScale);\n      radius = radius*minScale;\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    // no tangents? top circle is inside bottom circle\n    if (!tangents) {\n      a1 = 1.5*PI;\n      a2 = 1.5*PI;\n    } else {\n      a1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      a2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n    }\n\n    context.fillStyle = color;\n    context.beginPath();\n    context.arc(apex.x, apex.y, topRadius, HALF_PI, a1, true);\n    context.arc(c.x, c.y, radius, a1, HALF_PI);\n    context.closePath();\n    context.fill();\n\n    context.fillStyle = altColor;\n    context.beginPath();\n    context.arc(apex.x, apex.y, topRadius, a2, HALF_PI, true);\n    context.arc(c.x, c.y, radius, HALF_PI, a2);\n    context.closePath();\n    context.fill();\n\n    context.fillStyle = roofColor;\n    this._circle(context, apex, topRadius);\n  }\n\n  static simplified (context, center, radius) {\n    this._circle(context, { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, radius);\n  }\n\n  static shadow (context, center, radius, topRadius, height, minHeight) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      apex = Shadows.project(c, height),\n      p1, p2;\n\n    if (minHeight) {\n      c = Shadows.project(c, minHeight);\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    // TODO: no tangents? roof overlaps everything near cam position\n    if (tangents) {\n      p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n      context.moveTo(tangents[1].x2, tangents[1].y2);\n      context.arc(apex.x, apex.y, topRadius, p2, p1);\n      context.arc(c.x, c.y, radius, p1, p2);\n    } else {\n      context.moveTo(c.x+radius, c.y);\n      context.arc(c.x, c.y, radius, 0, 2*PI);\n    }\n  }\n\n  static hitArea (context, center, radius, topRadius, height, minHeight, color) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      p1, p2;\n\n    topRadius *= scale;\n\n    if (minHeight) {\n      c = Buildings.project(c, minScale);\n      radius = radius*minScale;\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    // TODO: no tangents? roof overlaps everything near cam position\n    if (tangents) {\n      p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n      context.moveTo(tangents[1].x2, tangents[1].y2);\n      context.arc(apex.x, apex.y, topRadius, p2, p1);\n      context.arc(c.x, c.y, radius, p1, p2);\n    } else {\n      context.moveTo(c.x+radius, c.y);\n      context.arc(c.x, c.y, radius, 0, 2*PI);\n    }\n\n    context.closePath();\n    context.fill();\n  }\n\n  static _circle (context, center, radius) {\n    context.beginPath();\n    context.arc(center.x, center.y, radius, 0, PI*2);\n    context.fill();\n  }\n\n    // http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Tangents_between_two_circles\n  static _tangents (c1, r1, c2, r2) {\n    let\n      dx = c1.x-c2.x,\n      dy = c1.y-c2.y,\n      dr = r1-r2,\n      sqdist = (dx*dx) + (dy*dy);\n\n    if (sqdist <= dr*dr) {\n      return;\n    }\n\n    let dist = sqrt(sqdist),\n      vx = -dx/dist,\n      vy = -dy/dist,\n      c  =  dr/dist,\n      res = [],\n      h, nx, ny;\n\n    // Let A, B be the centers, and C, D be points at which the tangent\n    // touches first and second circle, and n be the normal vector to it.\n    //\n    // We have the system:\n    //   n * n = 1    (n is a unit vector)\n    //   C = A + r1 * n\n    //   D = B + r2 * n\n    //   n * CD = 0   (common orthogonality)\n    //\n    // n * CD = n * (AB + r2*n - r1*n) = AB*n - (r1 -/+ r2) = 0,  <=>\n    // AB * n = (r1 -/+ r2), <=>\n    // v * n = (r1 -/+ r2) / d,  where v = AB/|AB| = AB/d\n    // This is a linear equation in unknown vector n.\n    // Now we're just intersecting a line with a circle: v*n=c, n*n=1\n\n    h = sqrt(max(0, 1 - c*c));\n    for (let sign = 1; sign >= -1; sign -= 2) {\n      nx = vx*c - sign*h*vy;\n      ny = vy*c + sign*h*vx;\n      res.push({\n        x1: c1.x + r1*nx <<0,\n        y1: c1.y + r1*ny <<0,\n        x2: c2.x + r2*nx <<0,\n        y2: c2.y + r2*ny <<0\n      });\n    }\n\n    return res;\n  }\n}\n\nclass Pyramid {\n\n  static draw (context, polygon, center, height, minHeight, color, altColor) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 };\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set shading\n        if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n          context.fillStyle = altColor;\n        } else {\n          context.fillStyle = color;\n        }\n\n        context.beginPath();\n        this._triangle(context, a, b, apex);\n        context.closePath();\n        context.fill();\n      }\n    }\n  }\n\n  static _triangle (context, a, b, c) {\n    context.moveTo(a.x, a.y);\n    context.lineTo(b.x, b.y);\n    context.lineTo(c.x, c.y);\n  }\n\n  static _ring (context, polygon) {\n    context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y);\n    }\n  }\n\n  static shadow (context, polygon, center, height, minHeight) {\n    let\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      apex = Shadows.project(c, height);\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Shadows.project(a, minHeight);\n        b = Shadows.project(b, minHeight);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set shading\n        this._triangle(context, a, b, apex);\n      }\n    }\n  }\n\n  static hitArea (context, polygon, center, height, minHeight, color) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 };\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        this._triangle(context, a, b, apex);\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nlet animTimer;\n\nfunction fadeIn() {\n  if (animTimer) {\n    return;\n  }\n\n  animTimer = setInterval(t => {\n    let dataItems = Data.items,\n      isNeeded = false;\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      if (dataItems[i].scale < 1) {\n        dataItems[i].scale += 0.5*0.2; // amount*easing\n        if (dataItems[i].scale > 1) {\n          dataItems[i].scale = 1;\n        }\n        isNeeded = true;\n      }\n    }\n\n    Layers.render();\n\n    if (!isNeeded) {\n      clearInterval(animTimer);\n      animTimer = null;\n    }\n  }, 33);\n}\n\nclass Layers {\n\n  static init () {\n    Layers.container.className = 'osmb-container';\n\n    // TODO: improve this\n    Shadows.init(Layers.createContext(Layers.container));\n    Simplified.init(Layers.createContext(Layers.container));\n    Buildings.init(Layers.createContext(Layers.container));\n    Picking.init(Layers.createContext());\n  }\n\n  static clear () {\n    Shadows.clear();\n    Simplified.clear();\n    Buildings.clear();\n    Picking.clear();\n  }\n\n  static setOpacity (opacity) {\n    Shadows.setOpacity(opacity);\n    Simplified.setOpacity(opacity);\n    Buildings.setOpacity(opacity);\n    Picking.setOpacity(opacity);\n  }\n\n  static render (quick) {\n    // show on high zoom levels only\n    if (ZOOM < MIN_ZOOM) {\n      Layers.clear();\n      return;\n    }\n\n    // don't render during zoom\n    if (IS_ZOOMING) {\n      return;\n    }\n\n    requestAnimationFrame(f => {\n      if (!quick) {\n        Shadows.render();\n        Simplified.render();\n        //HitAreas.render(); // TODO: do this on demand\n      }\n      Buildings.render();\n    });\n  }\n\n  static createContext (container) {\n    let canvas = document.createElement('CANVAS');\n    canvas.className = 'osmb-layer';\n\n    let context = canvas.getContext('2d');\n    context.lineCap   = 'round';\n    context.lineJoin  = 'round';\n    context.lineWidth = 1;\n    context.imageSmoothingEnabled = false;\n\n    Layers.items.push(canvas);\n    if (container) {\n      container.appendChild(canvas);\n    }\n\n    return context;\n  }\n\n  static appendTo (parentNode) {\n    parentNode.appendChild(Layers.container);\n  }\n\n  static remove () {\n    Layers.container.parentNode.removeChild(Layers.container);\n  }\n\n  static setSize (width, height) {\n    Layers.items.forEach(canvas => {\n      canvas.width  = width;\n      canvas.height = height;\n    });\n  }\n\n  // usually called after move: container jumps by move delta, cam is reset\n  static setPosition (x, y) {\n    Layers.container.style.left = x +'px';\n    Layers.container.style.top  = y +'px';\n  }\n}\n\nLayers.container = document.createElement('DIV');\nLayers.items = [];\n\nclass Buildings {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.context.canvas.style.opacity = opacity;\n  }\n\n  static project (p, m) {\n    return {\n      x: (p.x-CAM_X) * m + CAM_X <<0,\n      y: (p.y-CAM_Y) * m + CAM_Y <<0\n    };\n  }\n\n  static render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      item,\n      h, mh,\n      sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y },\n      footprint,\n      wallColor, altColor, roofColor,\n      dataItems = Data.items;\n\n    dataItems.sort((a, b) => {\n      return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height);\n    });\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (Simplified.isSimple(item)) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      // when fading in, use a dynamic height\n      h = item.scale < 1 ? item.height*item.scale : item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight;\n      }\n\n      wallColor = item.wallColor || WALL_COLOR_STR;\n      altColor  = item.altColor  || ALT_COLOR_STR;\n      roofColor = item.roofColor || ROOF_COLOR_STR;\n      context.strokeStyle = altColor;\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break;\n        case 'cone':     Cylinder.draw(context, item.center, item.radius, 0, h, mh, wallColor, altColor);                      break;\n        case 'dome':     Cylinder.draw(context, item.center, item.radius, item.radius/2, h, mh, wallColor, altColor);          break;\n        case 'sphere':   Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break;\n        case 'pyramid':  Pyramid.draw(context, footprint, item.center, h, mh, wallColor, altColor);                            break;\n        default:         Extrusion.draw(context, footprint, item.holes, h, mh, wallColor, altColor, roofColor);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.draw(context, item.center, item.radius, 0, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9));             break;\n        case 'dome':    Cylinder.draw(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9)); break;\n        case 'pyramid': Pyramid.draw(context, footprint, item.center, h+item.roofHeight, h, roofColor, Qolor.parse(roofColor).lightness(0.9));                       break;\n      }\n    }\n  }\n}\n\nclass Simplified {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.context.canvas.style.opacity = opacity;\n  }\n\n  static isSimple (item) {\n    return (ZOOM <= Simplified.MAX_ZOOM && item.height+item.roofHeight < Simplified.MAX_HEIGHT);\n  }\n\n  static render () {\n    this.clear();\n    \n    let context = this.context;\n\n    // show on high zoom levels only and avoid rendering during zoom\n    if (ZOOM > Simplified.MAX_ZOOM) {\n      return;\n    }\n\n    let\n      item,\n      footprint,\n      dataItems = Data.items;\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (item.height >= Simplified.MAX_HEIGHT) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      context.strokeStyle = item.altColor  || ALT_COLOR_STR;\n      context.fillStyle   = item.roofColor || ROOF_COLOR_STR;\n\n      switch (item.shape) {\n        case 'cylinder':\n        case 'cone':\n        case 'dome':\n        case 'sphere': Cylinder.simplified(context, item.center, item.radius);  break;\n        default: Extrusion.simplified(context, footprint, item.holes);\n      }\n    }\n  }\n}\n\nSimplified.MAX_ZOOM = 16; // max zoom where buildings could render simplified\nSimplified.MAX_HEIGHT = 5; // max building height in order to be simple\n\nclass Shadows {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.opacity = opacity;\n  }\n\n  static project (p, h) {\n    return {\n      x: p.x + this.direction.x*h,\n      y: p.y + this.direction.y*h\n    };\n  }\n\n  static render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      screenCenter,\n      sun, length, alpha;\n\n    // TODO: calculate this just on demand\n    screenCenter = pixelToGeo(CENTER_X+ORIGIN_X, CENTER_Y+ORIGIN_Y);\n    sun = getSunPosition(this.date, screenCenter.latitude, screenCenter.longitude);\n\n    if (sun.altitude <= 0) {\n      return;\n    }\n\n    length = 1 / tan(sun.altitude);\n    alpha = length < 5 ? 0.75 : 1/length*5;\n\n    this.direction.x = cos(sun.azimuth) * length;\n    this.direction.y = sin(sun.azimuth) * length;\n\n    let\n      i, il,\n      item,\n      h, mh,\n      footprint,\n      dataItems = Data.items;\n\n    context.canvas.style.opacity = alpha / (this.opacity * 2);\n    context.shadowColor = this.blurColor;\n    context.fillStyle = this.color;\n    context.beginPath();\n\n    for (i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      // when fading in, use a dynamic height\n      h = item.scale < 1 ? item.height*item.scale : item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight;\n      }\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh);   break;\n        case 'cone':     Cylinder.shadow(context, item.center, item.radius, 0, h, mh);             break;\n        case 'dome':     Cylinder.shadow(context, item.center, item.radius, item.radius/2, h, mh); break;\n        case 'sphere':   Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh);   break;\n        case 'pyramid':  Pyramid.shadow(context, footprint, item.center, h, mh);                   break;\n        default:         Extrusion.shadow(context, footprint, item.holes, h, mh);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.shadow(context, item.center, item.radius, 0, h+item.roofHeight, h);             break;\n        case 'dome':    Cylinder.shadow(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h); break;\n        case 'pyramid': Pyramid.shadow(context, footprint, item.center, h+item.roofHeight, h);                   break;\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nShadows.color = '#666666';\nShadows.blurColor = '#000000';\nShadows.date = new Date();\nShadows.direction = { x:0, y:0 };\nShadows.opacity = 1;\n\n\n\nclass Picking {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static setOpacity (opacity) {}\n\n  static clear () {}\n\n  static reset () {\n    this._idMapping = [null];\n  }\n\n  static render () {\n    if (this._timer) {\n      return;\n    }\n    let self = this;\n    this._timer = setTimeout(t => {\n      self._timer = null;\n      self._render();\n    }, 500);\n  }\n\n  static _render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      item,\n      h, mh,\n      sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y },\n      footprint,\n      color,\n      dataItems = Data.items;\n\n    dataItems.sort((a, b) => {\n      return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height);\n    });\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (!(color = item.hitColor)) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      h = item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.minHeight;\n      }\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color);   break;\n        case 'cone':     Cylinder.hitArea(context, item.center, item.radius, 0, h, mh, color);             break;\n        case 'dome':     Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h, mh, color); break;\n        case 'sphere':   Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color);   break;\n        case 'pyramid':  Pyramid.hitArea(context, footprint, item.center, h, mh, color);                   break;\n        default:         Extrusion.hitArea(context, footprint, item.holes, h, mh, color);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.hitArea(context, item.center, item.radius, 0, h+item.roofHeight, h, color);             break;\n        case 'dome':    Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, color); break;\n        case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h+item.roofHeight, h, color);                   break;\n      }\n    }\n\n    // otherwise fails on size 0\n    if (WIDTH && HEIGHT) {\n      this._imageData = this.context.getImageData(0, 0, WIDTH, HEIGHT).data;\n    }\n  }\n\n  static getIdFromXY (x, y) {\n    let imageData = this._imageData;\n    if (!imageData) {\n      return;\n    }\n    let pos = 4*((y|0) * WIDTH + (x|0));\n    let index = imageData[pos] | (imageData[pos+1]<<8) | (imageData[pos+2]<<16);\n    return this._idMapping[index];\n  }\n\n  static idToColor (id) {\n    let index = this._idMapping.indexOf(id);\n    if (index === -1) {\n      this._idMapping.push(id);\n      index = this._idMapping.length-1;\n    }\n    let r =  index       & 0xff;\n    let g = (index >>8)  & 0xff;\n    let b = (index >>16) & 0xff;\n    return 'rgb('+ [r, g, b].join(',') +')';\n  }\n}\n\nPicking._idMapping = [null];\n\n\nclass Debug {\n\n  static point (x, y, color, size) {\n    const context = this.context;\n    context.fillStyle = color || '#ffcc00';\n    context.beginPath();\n    context.arc(x, y, size || 3, 0, 2*PI);\n    context.closePath();\n    context.fill();\n  }\n\n  static line (ax, ay, bx, by, color) {\n    const context = this.context;\n    context.strokeStyle = color || '#ffcc00';\n    context.beginPath();\n    context.moveTo(ax, ay);\n    context.lineTo(bx, by);\n    context.closePath();\n    context.stroke();\n  }\n}\n\n\nfunction setOrigin (origin) {\n  ORIGIN_X = origin.x;\n  ORIGIN_Y = origin.y;\n}\n\nfunction moveCam (offset) {\n  CAM_X = CENTER_X + offset.x;\n  CAM_Y = HEIGHT   + offset.y;\n  Layers.render(true);\n}\n\nfunction setSize (size) {\n  WIDTH  = size.width;\n  HEIGHT = size.height;\n  CENTER_X = WIDTH /2 <<0;\n  CENTER_Y = HEIGHT/2 <<0;\n\n  CAM_X = CENTER_X;\n  CAM_Y = HEIGHT;\n\n  Layers.setSize(WIDTH, HEIGHT);\n  MAX_HEIGHT = CAM_Z-50;\n}\n\nfunction setZoom (z) {\n  ZOOM = z;\n  MAP_SIZE = MAP_TILE_SIZE <<ZOOM;\n\n  const center = pixelToGeo(ORIGIN_X+CENTER_X, ORIGIN_Y+CENTER_Y);\n  const a = geoToPixel(center.latitude, 0);\n  const b = geoToPixel(center.latitude, 1);\n  PIXEL_PER_DEG = b.x-a.x;\n\n  Layers.setOpacity(Math.pow(0.95, ZOOM-MIN_ZOOM));\n\n  WALL_COLOR_STR = ''+ WALL_COLOR;\n  ALT_COLOR_STR  = ''+ ALT_COLOR;\n  ROOF_COLOR_STR = ''+ ROOF_COLOR;\n}\n\nfunction onResize (e) {\n  setSize(e);\n  Layers.render();\n  Data.update();\n}\n\nfunction onMoveEnd (e) {\n  Layers.render();\n  Data.update(); // => fadeIn() => Layers.render()\n}\n\nfunction onZoomStart () {\n  IS_ZOOMING = true;\n}\n\nfunction onZoomEnd (e) {\n  IS_ZOOMING = false;\n  const factor = Math.pow(2, e.zoom-ZOOM);\n\n  setZoom(e.zoom);\n  // Layers.render(); // TODO: requestAnimationFrame() causes flickering because layers are already cleared\n\n  // show on high zoom levels only\n  if (ZOOM <= MIN_ZOOM) {\n    Layers.clear();\n    return;\n  }\n\n  Data.scale(factor);\n\n  Shadows.render();\n  Simplified.render();\n  Buildings.render();\n\n  Data.update(); // => fadeIn()\n}\n\n\nclass OSMBuildings extends L.Layer {\n\n  constructor (map) {\n    super(map);\n\n    this.offset = {x: 0, y: 0};\n    Layers.init();\n    if (map) {\n      map.addLayer(this);\n    }\n  }\n\n  addTo (map) {\n    map.addLayer(this);\n    return this;\n  }\n\n  onAdd (map) {\n    this.map = map;\n    Layers.appendTo(map._panes.overlayPane);\n\n    let\n      off = this.getOffset(),\n      po = map.getPixelOrigin();\n    setSize({width: map._size.x, height: map._size.y});\n    setOrigin({x: po.x - off.x, y: po.y - off.y});\n    setZoom(map._zoom);\n\n    Layers.setPosition(-off.x, -off.y);\n\n    map.on({\n      move: this.onMove,\n      moveend: this.onMoveEnd,\n      zoomstart: this.onZoomStart,\n      zoomend: this.onZoomEnd,\n      resize: this.onResize,\n      viewreset: this.onViewReset,\n      click: this.onClick\n    }, this);\n\n    if (map.options.zoomAnimation) {\n      map.on('zoomanim', this.onZoom, this);\n    }\n\n    if (map.attributionControl) {\n      map.attributionControl.addAttribution(ATTRIBUTION);\n    }\n\n    Data.update();\n  }\n\n  onRemove () {\n    let map = this.map;\n    if (map.attributionControl) {\n      map.attributionControl.removeAttribution(ATTRIBUTION);\n    }\n\n    map.off({\n      move: this.onMove,\n      moveend: this.onMoveEnd,\n      zoomstart: this.onZoomStart,\n      zoomend: this.onZoomEnd,\n      resize: this.onResize,\n      viewreset: this.onViewReset,\n      click: this.onClick\n    }, this);\n\n    if (map.options.zoomAnimation) {\n      map.off('zoomanim', this.onZoom, this);\n    }\n    Layers.remove();\n    map = null;\n  }\n\n  onMove (e) {\n    let off = this.getOffset();\n    moveCam({x: this.offset.x - off.x, y: this.offset.y - off.y});\n  }\n\n  onMoveEnd (e) {\n    if (this.noMoveEnd) { // moveend is also fired after zoom\n      this.noMoveEnd = false;\n      return;\n    }\n\n    let\n      map = this.map,\n      off = this.getOffset(),\n      po = map.getPixelOrigin();\n\n    this.offset = off;\n    Layers.setPosition(-off.x, -off.y);\n    moveCam({x: 0, y: 0});\n\n    setSize({width: map._size.x, height: map._size.y}); // in case this is triggered by resize\n    setOrigin({x: po.x - off.x, y: po.y - off.y});\n    onMoveEnd(e);\n  }\n\n  onZoomStart (e) {\n    onZoomStart(e);\n  }\n\n  onZoom (e) {\n    let center = this.map.latLngToContainerPoint(e.center);\n    let scale = Math.pow(2, e.zoom - ZOOM);\n\n    let dx = WIDTH / 2 - center.x;\n    let dy = HEIGHT / 2 - center.y;\n\n    let x = WIDTH / 2;\n    let y = HEIGHT / 2;\n\n    if (e.zoom > ZOOM) {\n      x -= dx * scale;\n      y -= dy * scale;\n    } else {\n      x += dx;\n      y += dy;\n    }\n\n    Layers.container.classList.add('zoom-animation');\n    Layers.container.style.transformOrigin = x + 'px ' + y + 'px';\n    Layers.container.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')';\n  }\n\n  onZoomEnd (e) {\n    Layers.clear();\n    Layers.container.classList.remove('zoom-animation');\n    Layers.container.style.transform = 'translate3d(0, 0, 0) scale(1)';\n\n    let\n      map = this.map,\n      off = this.getOffset(),\n      po = map.getPixelOrigin();\n\n    setOrigin({x: po.x - off.x, y: po.y - off.y});\n    onZoomEnd({zoom: map._zoom});\n    this.noMoveEnd = true;\n  }\n\n  onResize () {\n  }\n\n  onViewReset () {\n    let off = this.getOffset();\n\n    this.offset = off;\n    Layers.setPosition(-off.x, -off.y);\n    moveCam({x: 0, y: 0});\n  }\n\n  onClick (e) {\n    let id = Picking.getIdFromXY(e.containerPoint.x, e.containerPoint.y);\n    if (id) {\n      onClick({feature: id, lat: e.latlng.lat, lon: e.latlng.lng});\n    }\n  }\n\n  getOffset () {\n    return L.DomUtil.getPosition(this.map._mapPane);\n  }\n\n  //*** COMMON PUBLIC METHODS ***\n\n  style (style) {\n    style = style || {};\n    let color;\n    if ((color = style.color || style.wallColor)) {\n      WALL_COLOR = Qolor.parse(color);\n      WALL_COLOR_STR = '' + WALL_COLOR;\n\n      ALT_COLOR = WALL_COLOR.lightness(0.8);\n      ALT_COLOR_STR = '' + ALT_COLOR;\n\n      ROOF_COLOR = WALL_COLOR.lightness(1.2);\n      ROOF_COLOR_STR = '' + ROOF_COLOR;\n    }\n\n    if (style.roofColor) {\n      ROOF_COLOR = Qolor.parse(style.roofColor);\n      ROOF_COLOR_STR = '' + ROOF_COLOR;\n    }\n\n    Layers.render();\n\n    return this;\n  }\n\n  date (date) {\n    Shadows.date = date;\n    Shadows.render();\n    return this;\n  }\n\n  load (url) {\n    Data.load(url);\n    return this;\n  }\n\n  set (data) {\n    Data.set(data);\n    return this;\n  }\n\n  each (handler) {\n    onEach = function (payload) {\n      return handler(payload);\n    };\n    return this;\n  }\n\n  click (handler) {\n    onClick = function (payload) {\n      return handler(payload);\n    };\n    return this;\n  }\n}\n\nOSMBuildings.VERSION = VERSION;\nOSMBuildings.ATTRIBUTION = ATTRIBUTION;\n\n return OSMBuildings;\n}());"
  },
  {
    "path": "dist/OSMBuildings-Leaflet.js",
    "content": "const OSMBuildings=function(){const e=Math,t=e.exp,i=e.log,r=e.sin,a=e.cos,o=e.tan,s=e.atan,n=e.atan2,l=e.min,c=e.max,h=e.sqrt,f=e.ceil,d=e.pow;class u{constructor(e,t,i,r=1){this.r=this._clamp(e,1),this.g=this._clamp(t,1),this.b=this._clamp(i,1),this.a=this._clamp(r,1)}static parse(e){if(\"string\"==typeof e){let t;if(e=e.toLowerCase(),t=(e=u.w3cColors[e]||e).match(/^#?(\\w{2})(\\w{2})(\\w{2})$/))return new u(parseInt(t[1],16)/255,parseInt(t[2],16)/255,parseInt(t[3],16)/255);if(t=e.match(/^#?(\\w)(\\w)(\\w)$/))return new u(parseInt(t[1]+t[1],16)/255,parseInt(t[2]+t[2],16)/255,parseInt(t[3]+t[3],16)/255);if(t=e.match(/rgba?\\((\\d+)\\D+(\\d+)\\D+(\\d+)(\\D+([\\d.]+))?\\)/))return new u(parseFloat(t[1])/255,parseFloat(t[2])/255,parseFloat(t[3])/255,t[4]?parseFloat(t[5]):1)}return new u}static fromHSL(e,t,i,r){const a=(new u).fromHSL(e,t,i);return a.a=void 0===r?1:r,a}_hue2rgb(e,t,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?e+6*(t-e)*i:i<.5?t:i<2/3?e+(t-e)*(2/3-i)*6:e}_clamp(e,t){if(void 0!==e)return Math.min(t,Math.max(0,e||0))}isValid(){return void 0!==this.r&&void 0!==this.g&&void 0!==this.b}toHSL(){if(!this.isValid())return;const e=Math.max(this.r,this.g,this.b),t=Math.min(this.r,this.g,this.b),i=e-t,r=(e+t)/2;if(!i)return{h:0,s:0,l:r};const a=r>.5?i/(2-e-t):i/(e+t);let o;switch(e){case this.r:o=(this.g-this.b)/i+(this.g<this.b?6:0);break;case this.g:o=(this.b-this.r)/i+2;break;case this.b:o=(this.r-this.g)/i+4}return o*=60,{h:o,s:a,l:r}}fromHSL(e,t,i){if(0===t)return this.r=this.g=this.b=i,this;const r=i<.5?i*(1+t):i+t-i*t,a=2*i-r;return e/=360,this.r=this._hue2rgb(a,r,e+1/3),this.g=this._hue2rgb(a,r,e),this.b=this._hue2rgb(a,r,e-1/3),this}toString(){if(this.isValid())return 1===this.a?\"#\"+((1<<24)+(Math.round(255*this.r)<<16)+(Math.round(255*this.g)<<8)+Math.round(255*this.b)).toString(16).slice(1,7):`rgba(${Math.round(255*this.r)},${Math.round(255*this.g)},${Math.round(255*this.b)},${this.a.toFixed(2)})`}toArray(){if(this.isValid)return[this.r,this.g,this.b]}hue(e){const t=this.toHSL();return this.fromHSL(t.h+e,t.s,t.l)}saturation(e){const t=this.toHSL();return this.fromHSL(t.h,t.s*e,t.l)}lightness(e){const t=this.toHSL();return this.fromHSL(t.h,t.s,t.l*e)}clone(){return new u(this.r,this.g,this.b,this.a)}}function g(){const e=Math,t=e.PI,i=e.sin,r=e.cos,a=e.tan,o=e.asin,s=e.atan2,n=t/180,l=23.4397*n;function c(e,t,o){return s(i(e),r(e)*i(t)-a(o)*r(t))}function h(e,t,a){return o(i(t)*i(a)+r(t)*r(a)*r(e))}return function(e,f,d){const u=n*-d,g=n*f,p=function(e){return function(e){return e.valueOf()/864e5-.5+2440588}(e)-2451545}(e),y=function(e){return n*(357.5291+.98560028*e)}(p),m=function(e,i){return e+i+102.9372*n+t}(y,function(e){return n*(1.9148*i(e)+.02*i(2*e)+3e-4*i(3*e))}(y)),x=(k=m,o(i(_=0)*r(l)+r(_)*i(l)*i(k))),b=function(e,t){return s(i(e)*r(l)-a(t)*i(l),r(e))}(m,0),w=function(e,t){return n*(280.16+360.9856235*e)-t}(p,u)-b;var k,_;return{altitude:h(w,g,x),azimuth:c(w,g,x)-t/2}}}u.w3cColors={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgrey:\"#a9a9a9\",darkgreen:\"#006400\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",ghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",grey:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgrey:\"#d3d3d3\",lightgreen:\"#90ee90\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",lightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",moccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",rebeccapurple:\"#663399\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",seashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"},\"undefined\"!=typeof module&&(module.exports=u);const p={brick:\"#cc7755\",bronze:\"#ffeecc\",canvas:\"#fff8f0\",concrete:\"#999999\",copper:\"#a0e0d0\",glass:\"#e8f8f8\",gold:\"#ffcc00\",plants:\"#009933\",metal:\"#aaaaaa\",panel:\"#fff8f0\",plaster:\"#999999\",roof_tiles:\"#f08060\",silver:\"#cccccc\",slate:\"#666666\",stone:\"#996666\",tar_paper:\"#333333\",wood:\"#deb887\"},y={asphalt:\"tar_paper\",bitumen:\"tar_paper\",block:\"stone\",bricks:\"brick\",glas:\"glass\",glassfront:\"glass\",grass:\"plants\",masonry:\"stone\",granite:\"stone\",panels:\"panel\",paving_stones:\"stone\",plastered:\"plaster\",rooftiles:\"roof_tiles\",roofingfelt:\"tar_paper\",sandstone:\"stone\",sheet:\"canvas\",sheets:\"canvas\",shingle:\"tar_paper\",shingles:\"tar_paper\",slates:\"slate\",steel:\"metal\",tar:\"tar_paper\",tent:\"canvas\",thatch:\"plants\",tile:\"roof_tiles\",tiles:\"roof_tiles\"};function m(e){return\"#\"===(e=e.toLowerCase())[0]?e:p[y[e]||e]||null}function x(e,t){if(function(e){let t,i,r,a,o=0;for(let s=0,n=e.length-3;s<n;s+=2)t=e[s],i=e[s+1],r=e[s+2],a=e[s+3],o+=t*a-r*i;return o/2>0?\"CW\":\"CCW\"}(e)===t)return e;let i=[];for(let t=e.length-2;t>=0;t-=2)i.push(e[t],e[t+1]);return i}function b(e){const t={};e=e||{},t.height=e.height||(e.levels?3*e.levels:G),t.minHeight=e.minHeight||(e.minLevel?3*e.minLevel:0);const i=e.material?m(e.material):e.wallColor||e.color;i&&(t.wallColor=i);const r=e.roofMaterial?m(e.roofMaterial):e.roofColor;switch(r&&(t.roofColor=r),e.shape){case\"cylinder\":case\"cone\":case\"dome\":case\"sphere\":t.shape=e.shape,t.isRotational=!0;break;case\"pyramid\":t.shape=e.shape}switch(e.roofShape){case\"cone\":case\"dome\":t.roofShape=e.roofShape,t.isRotational=!0;break;case\"pyramid\":t.roofShape=e.roofShape}return t.roofShape&&e.roofHeight?(t.roofHeight=e.roofHeight,t.height=c(0,t.height-t.roofHeight)):t.roofHeight=0,t}function w(e){let t,i,r=[];switch(e.type){case\"GeometryCollection\":r=[];for(let t=0,a=e.geometries.length;t<a;t++)(i=w(e.geometries[t]))&&r.push.apply(r,i);return r;case\"MultiPolygon\":r=[];for(let t=0,a=e.coordinates.length;t<a;t++)(i=w({type:\"Polygon\",coordinates:e.coordinates[t]}))&&r.push.apply(r,i);return r;case\"Polygon\":t=e.coordinates;break;default:return[]}let a,o=[],s=[];a=t[0];for(let e=0,t=a.length;e<t;e++)o.push(a[e][1],a[e][0]);o=x(o,\"CW\");for(let e=0,i=t.length-1;e<i;e++){a=t[e+1],s[e]=[];for(let t=0,i=a.length;t<i;t++)s[e].push(a[t][1],a[t][0]);s[e]=x(s[e],\"CCW\")}return[{outer:o,inner:s.length?s:null}]}function k(e){let t={};for(const i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}let _,v,H,C,S,M,P='&copy; <a href=\"https://osmbuildings.org\">OSM Buildings</a>',T=Math.PI,z=T/2,I=T/4,O=0,A=0,j=0,R=0,E=0,D=0,F=u.parse(\"rgba(200, 190, 180)\"),N=F.lightness(.8),q=F.lightness(1.2),Z=\"\"+F,V=\"\"+N,X=\"\"+q,$=0,G=5;function W(){}function J(){}function B(e,t){const i=e.x-t.x,r=e.y-t.y;return i*i+r*r}function U(e,t,i,r,a,o){let s,n=a-i,l=o-r;return 0===n&&0===l||(s=((e-i)*n+(t-r)*l)/(n*n+l*l),s>1?(i=a,r=o):s>0&&(i+=n*s,r+=l*s)),n=e-i,l=t-r,n*n+l*l}function Y(e){let t=180,i=-180;for(let r=0,a=e.length;r<a;r+=2)t=l(t,e[r+1]),i=c(i,e[r+1]);return(i-t)/2}function K(e,i){const r={};return e/=v,i/=v,r.latitude=i<=0?90:i>=1?-90:function(e){return e/T*180}(2*s(t(T*(1-2*i)))-z),r.longitude=360*(1===e?1:(e%1+1)%1)-180,r}function Q(e,t){const r=l(1,c(0,.5-i(o(I+z*e/180))/T/2));return{x:(t/360+.5)*v<<0,y:r*v<<0}}function ee(e){const t=O+E,i=A+D;for(let r=0,a=e.length-3;r<a;r+=2)if(e[r]>E&&e[r]<t&&e[r+1]>D&&e[r+1]<i)return!0;return!1}let te,ie={},re=[],ae=0;class oe{static getPixelFootprint(e){let t,i=new Int32Array(e.length);for(let r=0,a=e.length-1;r<a;r+=2)t=Q(e[r],e[r+1]),i[r]=t.x,i[r+1]=t.y;if(i=function(e){let t,i,r,a=e.length/2,o=new Uint8Array(a),s=0,n=a-1,l=[],c=[],h=[];for(o[s]=o[n]=1;n;){t=0;for(let a=s+1;a<n;a++)i=U(e[2*a],e[2*a+1],e[2*s],e[2*s+1],e[2*n],e[2*n+1]),i>t&&(r=a,t=i);t>2&&(o[r]=1,l.push(s),c.push(r),l.push(r),c.push(n)),s=l.pop(),n=c.pop()}for(let t=0;t<a;t++)o[t]&&h.push(e[2*t],e[2*t+1]);return h}(i),!(i.length<8))return i}static resetItems(){this.items=[],this.cache={},ue.reset()}static addRenderItems(e,t){let i,r,a,o=class{static read(e){if(!e||\"FeatureCollection\"!==e.type)return[];const t=e.features,i=[];for(let e=0,r=t.length;e<r;e++){const r=t[e];if(\"Feature\"!==r.type||!1===W(r))continue;const a=b(r.properties),o=w(r.geometry);for(let e=0,t=o.length;e<t;e++){const t=k(a);t.footprint=o[e].outer,t.isRotational&&(t.radius=Y(t.footprint)),o[e].inner&&(t.holes=o[e].inner),(r.id||r.properties.id)&&(t.id=r.id||r.properties.id),r.properties.relationId&&(t.relationId=r.properties.relationId),i.push(t)}}return i}}.read(e);for(let e=0,s=o.length;e<s;e++)i=o[e],a=i.id||[i.footprint[0],i.footprint[1],i.height,i.minHeight].join(\",\"),this.cache[a]||(r=this.scaleItem(i))&&(r.scale=t?0:1,this.items.push(r),this.cache[a]=1);!function(){if(te)return;te=setInterval(e=>{let t=oe.items,i=!1;for(let e=0,r=t.length;e<r;e++)t[e].scale<1&&(t[e].scale+=.1,t[e].scale>1&&(t[e].scale=1),i=!0);ce.render(),i||(clearInterval(te),te=null)},33)}()}static scalePolygon(e,t){return e.map(e=>e*t)}static scale(e){oe.items=oe.items.map(t=>{if(t.height*=e,t.minHeight*=e,t.footprint=oe.scalePolygon(t.footprint,e),t.center.x*=e,t.center.y*=e,t.radius&&(t.radius*=e),t.holes)for(let i=0,r=t.holes.length;i<r;i++)t.holes[i]=oe.scalePolygon(t.holes[i],e);return t.roofHeight*=e,t})}static scaleItem(e){let t,i={},r=6/d(2,_-15);if(e.id&&(i.id=e.id),i.height=l(e.height/r,H),i.minHeight=isNaN(e.minHeight)?0:e.minHeight/r,!(i.minHeight>H)&&(i.footprint=this.getPixelFootprint(e.footprint),i.footprint)){if(i.center=function(e){let t=1/0,i=-1/0,r=1/0,a=-1/0;for(let o=0,s=e.length-3;o<s;o+=2)t=l(t,e[o]),i=c(i,e[o]),r=l(r,e[o+1]),a=c(a,e[o+1]);return{x:t+(i-t)/2<<0,y:r+(a-r)/2<<0}}(i.footprint),e.radius&&(i.radius=e.radius*$),e.shape&&(i.shape=e.shape),e.roofShape&&(i.roofShape=e.roofShape),\"cone\"!==i.roofShape&&\"dome\"!==i.roofShape||i.shape||!function(e){const t=e.length;if(t<16)return!1;let i=1/0,r=-1/0,a=1/0,o=-1/0;for(let s=0;s<t-1;s+=2)i=Math.min(i,e[s]),r=Math.max(r,e[s]),a=Math.min(a,e[s+1]),o=Math.max(o,e[s+1]);const s=r-i,n=o-a,l=s/n;if(l<.85||l>1.15)return!1;const c={x:i+s/2,y:a+n/2},h=(s+n)/4,f=h*h;for(let i=0;i<t-1;i+=2){const t=B({x:e[i],y:e[i+1]},c);if(t/f<.8||t/f>1.2)return!1}return!0}(i.footprint)||(i.shape=\"cylinder\"),e.holes){let t;i.holes=[];for(let r=0,a=e.holes.length;r<a;r++)(t=this.getPixelFootprint(e.holes[r]))&&i.holes.push(t)}return e.wallColor&&(t=u.parse(e.wallColor))&&(i.altColor=\"\"+t.lightness(.8),i.wallColor=\"\"+t),e.roofColor&&(t=u.parse(e.roofColor))&&(i.roofColor=\"\"+t),e.relationId&&(i.relationId=e.relationId),i.hitColor=ue.idToColor(e.relationId||e.id),i.roofHeight=isNaN(e.roofHeight)?0:e.roofHeight/r,i.height+i.roofHeight<=i.minHeight?void 0:i}}static set(e){this.resetItems(),this._staticData=e,this.addRenderItems(this._staticData,!0)}static load(e,t){this.src=e||\"https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json\".replace(\"{k}\",t||\"anonymous\"),this.update()}static update(){if(this.resetItems(),!(_<15)&&(this._staticData&&this.addRenderItems(this._staticData),this.src)){let t,i,r=16,a=256,o=_>r?a<<_-r:a>>r-_,s=E/o<<0,n=D/o<<0,l=f((E+O)/o),c=f((D+A)/o),h=this;function e(e){h.addRenderItems(e)}for(i=n;i<=c;i++)for(t=s;t<=l;t++)this.loadTile(t,i,r,e)}}static loadTile(e,t,i,r){let a=\"abcd\"[(e+t)%4],o=this.src.replace(\"{s}\",a).replace(\"{x}\",e).replace(\"{y}\",t).replace(\"{z}\",i);return class{static loadJSON(e,t){return function(e,t){if(ie[e])return void(t&&t(ie[e]));const i=new XMLHttpRequest;return i.onreadystatechange=function(){if(4===i.readyState&&!(!i.status||i.status<200||i.status>299)&&t&&i.responseText){const r=i.responseText;for(ie[e]=r,re.push({url:e,size:r.length}),ae+=r.length,t(r);ae>5242880;){let e=re.shift();ae-=e.size,delete ie[e.url]}}},i.open(\"GET\",e),i.send(null),i}(e,e=>{let i;try{i=JSON.parse(e)}catch(e){}t(i)})}}.loadJSON(o,r)}}oe.cache={},oe.items=[];class se{static draw(e,t,i,r,a,o,s,n){let l=this._extrude(e,t,r,a,o,s),c=[];if(i)for(let t=0,n=i.length;t<n;t++)c[t]=this._extrude(e,i[t],r,a,o,s);if(e.fillStyle=n,e.beginPath(),this._ring(e,l),i)for(let t=0,i=c.length;t<i;t++)this._ring(e,c[t]);e.closePath(),e.fill()}static _extrude(e,t,i,r,a,o){let s,n,l=450/(450-i),c=450/(450-r),h={x:0,y:0},f={x:0,y:0},d=[];for(let i=0,u=t.length-3;i<u;i+=2)h.x=t[i]-E,h.y=t[i+1]-D,f.x=t[i+2]-E,f.y=t[i+3]-D,s=he.project(h,l),n=he.project(f,l),r&&(h=he.project(h,c),f=he.project(f,c)),(f.x-h.x)*(s.y-h.y)>(s.x-h.x)*(f.y-h.y)&&(h.x<f.x&&h.y<f.y||h.x>f.x&&h.y>f.y?e.fillStyle=o:e.fillStyle=a,e.beginPath(),this._ring(e,[f.x,f.y,h.x,h.y,s.x,s.y,n.x,n.y]),e.closePath(),e.fill()),d[i]=s.x,d[i+1]=s.y;return d}static _ring(e,t){e.moveTo(t[0],t[1]);for(let i=2,r=t.length-1;i<r;i+=2)e.lineTo(t[i],t[i+1])}static simplified(e,t,i){if(e.beginPath(),this._ringAbs(e,t),i)for(let t=0,r=i.length;t<r;t++)this._ringAbs(e,i[t]);e.closePath(),e.fill()}static _ringAbs(e,t){e.moveTo(t[0]-E,t[1]-D);for(let i=2,r=t.length-1;i<r;i+=2)e.lineTo(t[i]-E,t[i+1]-D)}static shadow(e,t,i,r,a){let o,s,n=null,l={x:0,y:0},c={x:0,y:0};for(let i=0,h=t.length-3;i<h;i+=2)l.x=t[i]-E,l.y=t[i+1]-D,c.x=t[i+2]-E,c.y=t[i+3]-D,o=de.project(l,r),s=de.project(c,r),a&&(l=de.project(l,a),c=de.project(c,a)),(c.x-l.x)*(o.y-l.y)>(o.x-l.x)*(c.y-l.y)?(1===n&&e.lineTo(l.x,l.y),n=0,i||e.moveTo(l.x,l.y),e.lineTo(c.x,c.y)):(0===n&&e.lineTo(o.x,o.y),n=1,i||e.moveTo(o.x,o.y),e.lineTo(s.x,s.y));if(i)for(let t=0,r=i.length;t<r;t++)this._ringAbs(e,i[t])}static hitArea(e,t,i,r,a,o){let s,n,l=null,c={x:0,y:0},h={x:0,y:0},f=450/(450-r),d=450/(450-a);e.fillStyle=o,e.beginPath();for(let i=0,r=t.length-3;i<r;i+=2)c.x=t[i]-E,c.y=t[i+1]-D,h.x=t[i+2]-E,h.y=t[i+3]-D,s=he.project(c,f),n=he.project(h,f),a&&(c=he.project(c,d),h=he.project(h,d)),(h.x-c.x)*(s.y-c.y)>(s.x-c.x)*(h.y-c.y)?(1===l&&e.lineTo(c.x,c.y),l=0,i||e.moveTo(c.x,c.y),e.lineTo(h.x,h.y)):(0===l&&e.lineTo(s.x,s.y),l=1,i||e.moveTo(s.x,s.y),e.lineTo(n.x,n.y));e.closePath(),e.fill()}}class ne{static draw(e,t,i,r,a,o,s,l,c){let h,f,d={x:t.x-E,y:t.y-D},u=450/(450-a),g=450/(450-o),p=he.project(d,u);r*=u,o&&(d=he.project(d,g),i*=g);let y=this._tangents(d,i,p,r);y?(h=n(y[0].y1-d.y,y[0].x1-d.x),f=n(y[1].y1-d.y,y[1].x1-d.x)):(h=1.5*T,f=1.5*T),e.fillStyle=s,e.beginPath(),e.arc(p.x,p.y,r,z,h,!0),e.arc(d.x,d.y,i,h,z),e.closePath(),e.fill(),e.fillStyle=l,e.beginPath(),e.arc(p.x,p.y,r,f,z,!0),e.arc(d.x,d.y,i,z,f),e.closePath(),e.fill(),e.fillStyle=c,this._circle(e,p,r)}static simplified(e,t,i){this._circle(e,{x:t.x-E,y:t.y-D},i)}static shadow(e,t,i,r,a,o){let s,l,c={x:t.x-E,y:t.y-D},h=de.project(c,a);o&&(c=de.project(c,o));let f=this._tangents(c,i,h,r);f?(s=n(f[0].y1-c.y,f[0].x1-c.x),l=n(f[1].y1-c.y,f[1].x1-c.x),e.moveTo(f[1].x2,f[1].y2),e.arc(h.x,h.y,r,l,s),e.arc(c.x,c.y,i,s,l)):(e.moveTo(c.x+i,c.y),e.arc(c.x,c.y,i,0,2*T))}static hitArea(e,t,i,r,a,o,s){let l,c,h={x:t.x-E,y:t.y-D},f=450/(450-a),d=450/(450-o),u=he.project(h,f);r*=f,o&&(h=he.project(h,d),i*=d);let g=this._tangents(h,i,u,r);e.fillStyle=s,e.beginPath(),g?(l=n(g[0].y1-h.y,g[0].x1-h.x),c=n(g[1].y1-h.y,g[1].x1-h.x),e.moveTo(g[1].x2,g[1].y2),e.arc(u.x,u.y,r,c,l),e.arc(h.x,h.y,i,l,c)):(e.moveTo(h.x+i,h.y),e.arc(h.x,h.y,i,0,2*T)),e.closePath(),e.fill()}static _circle(e,t,i){e.beginPath(),e.arc(t.x,t.y,i,0,2*T),e.fill()}static _tangents(e,t,i,r){let a=e.x-i.x,o=e.y-i.y,s=t-r,n=a*a+o*o;if(n<=s*s)return;let l,f,d,u=h(n),g=-a/u,p=-o/u,y=s/u,m=[];l=h(c(0,1-y*y));for(let a=1;a>=-1;a-=2)f=g*y-a*l*p,d=p*y+a*l*g,m.push({x1:e.x+t*f<<0,y1:e.y+t*d<<0,x2:i.x+r*f<<0,y2:i.y+r*d<<0});return m}}class le{static draw(e,t,i,r,a,o,s){let n={x:i.x-E,y:i.y-D},l=450/(450-r),c=450/(450-a),h=he.project(n,l),f={x:0,y:0},d={x:0,y:0};for(let i=0,r=t.length-3;i<r;i+=2)f.x=t[i]-E,f.y=t[i+1]-D,d.x=t[i+2]-E,d.y=t[i+3]-D,a&&(f=he.project(f,c),d=he.project(d,c)),(d.x-f.x)*(h.y-f.y)>(h.x-f.x)*(d.y-f.y)&&(f.x<d.x&&f.y<d.y||f.x>d.x&&f.y>d.y?e.fillStyle=s:e.fillStyle=o,e.beginPath(),this._triangle(e,f,d,h),e.closePath(),e.fill())}static _triangle(e,t,i,r){e.moveTo(t.x,t.y),e.lineTo(i.x,i.y),e.lineTo(r.x,r.y)}static _ring(e,t){e.moveTo(t[0]-E,t[1]-D);for(let i=2,r=t.length-1;i<r;i+=2)e.lineTo(t[i]-E,t[i+1]-D)}static shadow(e,t,i,r,a){let o={x:0,y:0},s={x:0,y:0},n={x:i.x-E,y:i.y-D},l=de.project(n,r);for(let i=0,r=t.length-3;i<r;i+=2)o.x=t[i]-E,o.y=t[i+1]-D,s.x=t[i+2]-E,s.y=t[i+3]-D,a&&(o=de.project(o,a),s=de.project(s,a)),(s.x-o.x)*(l.y-o.y)>(l.x-o.x)*(s.y-o.y)&&this._triangle(e,o,s,l)}static hitArea(e,t,i,r,a,o){let s={x:i.x-E,y:i.y-D},n=450/(450-r),l=450/(450-a),c=he.project(s,n),h={x:0,y:0},f={x:0,y:0};e.fillStyle=o,e.beginPath();for(let i=0,r=t.length-3;i<r;i+=2)h.x=t[i]-E,h.y=t[i+1]-D,f.x=t[i+2]-E,f.y=t[i+3]-D,a&&(h=he.project(h,l),f=he.project(f,l)),(f.x-h.x)*(c.y-h.y)>(c.x-h.x)*(f.y-h.y)&&this._triangle(e,h,f,c);e.closePath(),e.fill()}}class ce{static init(){ce.container.className=\"osmb-container\",de.init(ce.createContext(ce.container)),fe.init(ce.createContext(ce.container)),he.init(ce.createContext(ce.container)),ue.init(ce.createContext())}static clear(){de.clear(),fe.clear(),he.clear(),ue.clear()}static setOpacity(e){de.setOpacity(e),fe.setOpacity(e),he.setOpacity(e),ue.setOpacity(e)}static render(e){_<15?ce.clear():M||requestAnimationFrame(t=>{e||(de.render(),fe.render()),he.render()})}static createContext(e){let t=document.createElement(\"CANVAS\");t.className=\"osmb-layer\";let i=t.getContext(\"2d\");return i.lineCap=\"round\",i.lineJoin=\"round\",i.lineWidth=1,i.imageSmoothingEnabled=!1,ce.items.push(t),e&&e.appendChild(t),i}static appendTo(e){e.appendChild(ce.container)}static remove(){ce.container.parentNode.removeChild(ce.container)}static setSize(e,t){ce.items.forEach(i=>{i.width=e,i.height=t})}static setPosition(e,t){ce.container.style.left=e+\"px\",ce.container.style.top=t+\"px\"}}ce.container=document.createElement(\"DIV\"),ce.items=[];class he{static init(e){this.context=e}static clear(){this.context.clearRect(0,0,O,A)}static setOpacity(e){this.context.canvas.style.opacity=e}static project(e,t){return{x:(e.x-C)*t+C<<0,y:(e.y-S)*t+S<<0}}static render(){this.clear();let e,t,i,r,a,o,s,n=this.context,l={x:C+E,y:S+D},c=oe.items;c.sort((e,t)=>e.minHeight-t.minHeight||B(t.center,l)-B(e.center,l)||t.height-e.height);for(let l=0,h=c.length;l<h;l++)if(e=c[l],!fe.isSimple(e)&&(r=e.footprint,ee(r))){switch(t=e.scale<1?e.height*e.scale:e.height,i=0,e.minHeight&&(i=e.scale<1?e.minHeight*e.scale:e.minHeight),a=e.wallColor||Z,o=e.altColor||V,s=e.roofColor||X,n.strokeStyle=o,e.shape){case\"cylinder\":ne.draw(n,e.center,e.radius,e.radius,t,i,a,o,s);break;case\"cone\":ne.draw(n,e.center,e.radius,0,t,i,a,o);break;case\"dome\":ne.draw(n,e.center,e.radius,e.radius/2,t,i,a,o);break;case\"sphere\":ne.draw(n,e.center,e.radius,e.radius,t,i,a,o,s);break;case\"pyramid\":le.draw(n,r,e.center,t,i,a,o);break;default:se.draw(n,r,e.holes,t,i,a,o,s)}switch(e.roofShape){case\"cone\":ne.draw(n,e.center,e.radius,0,t+e.roofHeight,t,s,\"\"+u.parse(s).lightness(.9));break;case\"dome\":ne.draw(n,e.center,e.radius,e.radius/2,t+e.roofHeight,t,s,\"\"+u.parse(s).lightness(.9));break;case\"pyramid\":le.draw(n,r,e.center,t+e.roofHeight,t,s,u.parse(s).lightness(.9))}}}}class fe{static init(e){this.context=e}static clear(){this.context.clearRect(0,0,O,A)}static setOpacity(e){this.context.canvas.style.opacity=e}static isSimple(e){return _<=fe.MAX_ZOOM&&e.height+e.roofHeight<fe.MAX_HEIGHT}static render(){this.clear();let e=this.context;if(_>fe.MAX_ZOOM)return;let t,i,r=oe.items;for(let a=0,o=r.length;a<o;a++)if(t=r[a],!(t.height>=fe.MAX_HEIGHT)&&(i=t.footprint,ee(i)))switch(e.strokeStyle=t.altColor||V,e.fillStyle=t.roofColor||X,t.shape){case\"cylinder\":case\"cone\":case\"dome\":case\"sphere\":ne.simplified(e,t.center,t.radius);break;default:se.simplified(e,i,t.holes)}}}fe.MAX_ZOOM=16,fe.MAX_HEIGHT=5;class de{static init(e){this.context=e}static clear(){this.context.clearRect(0,0,O,A)}static setOpacity(e){this.opacity=e}static project(e,t){return{x:e.x+this.direction.x*t,y:e.y+this.direction.y*t}}static render(){this.clear();let e,t,i,s,n=this.context;if(e=K(j+E,R+D),t=g(this.date,e.latitude,e.longitude),t.altitude<=0)return;i=1/o(t.altitude),s=i<5?.75:1/i*5,this.direction.x=a(t.azimuth)*i,this.direction.y=r(t.azimuth)*i;let l,c,h,f,d,u,p=oe.items;for(n.canvas.style.opacity=s/(2*this.opacity),n.shadowColor=this.blurColor,n.fillStyle=this.color,n.beginPath(),l=0,c=p.length;l<c;l++)if(h=p[l],u=h.footprint,ee(u)){switch(f=h.scale<1?h.height*h.scale:h.height,d=0,h.minHeight&&(d=h.scale<1?h.minHeight*h.scale:h.minHeight),h.shape){case\"cylinder\":ne.shadow(n,h.center,h.radius,h.radius,f,d);break;case\"cone\":ne.shadow(n,h.center,h.radius,0,f,d);break;case\"dome\":ne.shadow(n,h.center,h.radius,h.radius/2,f,d);break;case\"sphere\":ne.shadow(n,h.center,h.radius,h.radius,f,d);break;case\"pyramid\":le.shadow(n,u,h.center,f,d);break;default:se.shadow(n,u,h.holes,f,d)}switch(h.roofShape){case\"cone\":ne.shadow(n,h.center,h.radius,0,f+h.roofHeight,f);break;case\"dome\":ne.shadow(n,h.center,h.radius,h.radius/2,f+h.roofHeight,f);break;case\"pyramid\":le.shadow(n,u,h.center,f+h.roofHeight,f)}}n.closePath(),n.fill()}}de.color=\"#666666\",de.blurColor=\"#000000\",de.date=new Date,de.direction={x:0,y:0},de.opacity=1;class ue{static init(e){this.context=e}static setOpacity(e){}static clear(){}static reset(){this._idMapping=[null]}static render(){if(this._timer)return;let e=this;this._timer=setTimeout(t=>{e._timer=null,e._render()},500)}static _render(){this.clear();let e,t,i,r,a,o=this.context,s={x:C+E,y:S+D},n=oe.items;n.sort((e,t)=>e.minHeight-t.minHeight||B(t.center,s)-B(e.center,s)||t.height-e.height);for(let s=0,l=n.length;s<l;s++)if(e=n[s],(a=e.hitColor)&&(r=e.footprint,ee(r))){switch(t=e.height,i=0,e.minHeight&&(i=e.minHeight),e.shape){case\"cylinder\":ne.hitArea(o,e.center,e.radius,e.radius,t,i,a);break;case\"cone\":ne.hitArea(o,e.center,e.radius,0,t,i,a);break;case\"dome\":ne.hitArea(o,e.center,e.radius,e.radius/2,t,i,a);break;case\"sphere\":ne.hitArea(o,e.center,e.radius,e.radius,t,i,a);break;case\"pyramid\":le.hitArea(o,r,e.center,t,i,a);break;default:se.hitArea(o,r,e.holes,t,i,a)}switch(e.roofShape){case\"cone\":ne.hitArea(o,e.center,e.radius,0,t+e.roofHeight,t,a);break;case\"dome\":ne.hitArea(o,e.center,e.radius,e.radius/2,t+e.roofHeight,t,a);break;case\"pyramid\":le.hitArea(o,r,e.center,t+e.roofHeight,t,a)}}O&&A&&(this._imageData=this.context.getImageData(0,0,O,A).data)}static getIdFromXY(e,t){let i=this._imageData;if(!i)return;let r=4*((0|t)*O+(0|e)),a=i[r]|i[r+1]<<8|i[r+2]<<16;return this._idMapping[a]}static idToColor(e){let t=this._idMapping.indexOf(e);return-1===t&&(this._idMapping.push(e),t=this._idMapping.length-1),\"rgb(\"+[255&t,t>>8&255,t>>16&255].join(\",\")+\")\"}}ue._idMapping=[null];function ge(e){E=e.x,D=e.y}function pe(e){C=j+e.x,S=A+e.y,ce.render(!0)}function ye(e){O=e.width,A=e.height,j=O/2<<0,R=A/2<<0,C=j,S=A,ce.setSize(O,A),H=400}function me(e){_=e,v=256<<_;const t=K(E+j,D+R),i=Q(t.latitude,0),r=Q(t.latitude,1);$=r.x-i.x,ce.setOpacity(Math.pow(.95,_-15)),Z=\"\"+F,V=\"\"+N,X=\"\"+q}class xe extends L.Layer{constructor(e){super(e),this.offset={x:0,y:0},ce.init(),e&&e.addLayer(this)}addTo(e){return e.addLayer(this),this}onAdd(e){this.map=e,ce.appendTo(e._panes.overlayPane);let t=this.getOffset(),i=e.getPixelOrigin();ye({width:e._size.x,height:e._size.y}),ge({x:i.x-t.x,y:i.y-t.y}),me(e._zoom),ce.setPosition(-t.x,-t.y),e.on({move:this.onMove,moveend:this.onMoveEnd,zoomstart:this.onZoomStart,zoomend:this.onZoomEnd,resize:this.onResize,viewreset:this.onViewReset,click:this.onClick},this),e.options.zoomAnimation&&e.on(\"zoomanim\",this.onZoom,this),e.attributionControl&&e.attributionControl.addAttribution(P),oe.update()}onRemove(){let e=this.map;e.attributionControl&&e.attributionControl.removeAttribution(P),e.off({move:this.onMove,moveend:this.onMoveEnd,zoomstart:this.onZoomStart,zoomend:this.onZoomEnd,resize:this.onResize,viewreset:this.onViewReset,click:this.onClick},this),e.options.zoomAnimation&&e.off(\"zoomanim\",this.onZoom,this),ce.remove(),e=null}onMove(e){let t=this.getOffset();pe({x:this.offset.x-t.x,y:this.offset.y-t.y})}onMoveEnd(e){if(this.noMoveEnd)return void(this.noMoveEnd=!1);let t=this.map,i=this.getOffset(),r=t.getPixelOrigin();this.offset=i,ce.setPosition(-i.x,-i.y),pe({x:0,y:0}),ye({width:t._size.x,height:t._size.y}),ge({x:r.x-i.x,y:r.y-i.y}),ce.render(),oe.update()}onZoomStart(e){M=!0}onZoom(e){let t=this.map.latLngToContainerPoint(e.center),i=Math.pow(2,e.zoom-_),r=O/2-t.x,a=A/2-t.y,o=O/2,s=A/2;e.zoom>_?(o-=r*i,s-=a*i):(o+=r,s+=a),ce.container.classList.add(\"zoom-animation\"),ce.container.style.transformOrigin=o+\"px \"+s+\"px\",ce.container.style.transform=\"translate3d(0, 0, 0) scale(\"+i+\")\"}onZoomEnd(e){ce.clear(),ce.container.classList.remove(\"zoom-animation\"),ce.container.style.transform=\"translate3d(0, 0, 0) scale(1)\";let t=this.map,i=this.getOffset(),r=t.getPixelOrigin();ge({x:r.x-i.x,y:r.y-i.y}),function(e){M=!1;const t=Math.pow(2,e.zoom-_);me(e.zoom),_<=15?ce.clear():(oe.scale(t),de.render(),fe.render(),he.render(),oe.update())}({zoom:t._zoom}),this.noMoveEnd=!0}onResize(){}onViewReset(){let e=this.getOffset();this.offset=e,ce.setPosition(-e.x,-e.y),pe({x:0,y:0})}onClick(e){let t=ue.getIdFromXY(e.containerPoint.x,e.containerPoint.y);t&&J({feature:t,lat:e.latlng.lat,lon:e.latlng.lng})}getOffset(){return L.DomUtil.getPosition(this.map._mapPane)}style(e){let t;return(t=(e=e||{}).color||e.wallColor)&&(F=u.parse(t),Z=\"\"+F,N=F.lightness(.8),V=\"\"+N,q=F.lightness(1.2),X=\"\"+q),e.roofColor&&(q=u.parse(e.roofColor),X=\"\"+q),ce.render(),this}date(e){return de.date=e,de.render(),this}load(e){return oe.load(e),this}set(e){return oe.set(e),this}each(e){return W=function(t){return e(t)},this}click(e){return J=function(t){return e(t)},this}}return xe.VERSION=\"0.3.2\",xe.ATTRIBUTION=P,xe}();"
  },
  {
    "path": "dist/OSMBuildings-OpenLayers.debug.js",
    "content": "const OSMBuildings = (function() {\n\nconst\n  m = Math,\n  exp = m.exp,\n  log = m.log,\n  sin = m.sin,\n  cos = m.cos,\n  tan = m.tan,\n  atan = m.atan,\n  atan2 = m.atan2,\n  min = m.min,\n  max = m.max,\n  sqrt = m.sqrt,\n  ceil = m.ceil,\n  pow = m.pow;\n\n\n/**\n * @class\n */\nclass Qolor {\n\n  /**\n   * @constructor\n   * @param r {Number} 0.0 .. 1.0 red value of a color\n   * @param g {Number} 0.0 .. 1.0 green value of a color\n   * @param b {Number} 0.0 .. 1.0 blue value of a color\n   * @param a {Number} 0.0 .. 1.0 alpha value of a color, default 1\n   */\n  constructor (r, g, b, a = 1) {\n    this.r = this._clamp(r, 1);\n    this.g = this._clamp(g, 1);\n    this.b = this._clamp(b, 1);\n    this.a = this._clamp(a, 1);\n  }\n\n  /**\n   * @param str {String} can be any color dfinition like: 'red', '#0099ff', 'rgb(64, 128, 255)', 'rgba(64, 128, 255, 0.5)'\n   */\n  static parse (str) {\n    if (typeof str === 'string') {\n      str = str.toLowerCase();\n      str = Qolor.w3cColors[str] || str;\n\n      let m;\n\n      if ((m = str.match(/^#?(\\w{2})(\\w{2})(\\w{2})$/))) {\n        return new Qolor(parseInt(m[1], 16)/255, parseInt(m[2], 16)/255, parseInt(m[3], 16)/255);\n      }\n\n      if ((m = str.match(/^#?(\\w)(\\w)(\\w)$/))) {\n        return new Qolor(parseInt(m[1]+m[1], 16)/255, parseInt(m[2]+m[2], 16)/255, parseInt(m[3]+m[3], 16)/255);\n      }\n\n      if ((m = str.match(/rgba?\\((\\d+)\\D+(\\d+)\\D+(\\d+)(\\D+([\\d.]+))?\\)/))) {\n        return new Qolor(\n          parseFloat(m[1])/255,\n          parseFloat(m[2])/255,\n          parseFloat(m[3])/255,\n          m[4] ? parseFloat(m[5]) : 1\n        );\n      }\n    }\n\n    return new Qolor();\n  }\n\n  static fromHSL (h, s, l, a) {\n    const qolor = new Qolor().fromHSL(h, s, l);\n    qolor.a = a === undefined ? 1 : a;\n    return qolor;\n  }\n\n  //***************************************************************************\n\n  _hue2rgb(p, q, t) {\n    if (t<0) t += 1;\n    if (t>1) t -= 1;\n    if (t<1/6) return p + (q - p)*6*t;\n    if (t<1/2) return q;\n    if (t<2/3) return p + (q - p)*(2/3 - t)*6;\n    return p;\n  }\n\n  _clamp(v, max) {\n    if (v === undefined) {\n      return;\n    }\n    return Math.min(max, Math.max(0, v || 0));\n  }\n\n  //***************************************************************************\n\n  isValid () {\n    return this.r !== undefined && this.g !== undefined && this.b !== undefined;\n  }\n\n  toHSL () {\n    if (!this.isValid()) {\n      return;\n    }\n\n    const max = Math.max(this.r, this.g, this.b);\n    const min = Math.min(this.r, this.g, this.b);\n    const range = max - min;\n    const l = (max + min)/2;\n\n    // achromatic\n    if (!range) {\n      return { h: 0, s: 0, l: l };\n    }\n\n    const s = l > 0.5 ? range/(2 - max - min) : range/(max + min);\n\n    let h;\n    switch (max) {\n      case this.r:\n        h = (this.g - this.b)/range + (this.g<this.b ? 6 : 0);\n        break;\n      case this.g:\n        h = (this.b - this.r)/range + 2;\n        break;\n      case this.b:\n        h = (this.r - this.g)/range + 4;\n        break;\n    }\n    h *= 60;\n\n    return { h: h, s: s, l: l };\n  }\n\n  fromHSL (h, s, l) {\n    // h = this._clamp(h, 360),\n    // s = this._clamp(s, 1),\n    // l = this._clamp(l, 1),\n\n    // achromatic\n    if (s === 0) {\n      this.r = this.g = this.b = l;\n      return this;\n    }\n\n    const q = l<0.5 ? l*(1 + s) : l + s - l*s;\n    const p = 2*l - q;\n\n    h /= 360;\n\n    this.r = this._hue2rgb(p, q, h + 1/3);\n    this.g = this._hue2rgb(p, q, h);\n    this.b = this._hue2rgb(p, q, h - 1/3);\n\n    return this;\n  }\n\n  toString () {\n    if (!this.isValid()) {\n      return;\n    }\n\n    if (this.a === 1) {\n      return '#' + ((1<<24) + (Math.round(this.r*255)<<16) + (Math.round(this.g*255)<<8) + Math.round(this.b*255)).toString(16).slice(1, 7);\n    }\n    return `rgba(${Math.round(this.r*255)},${Math.round(this.g*255)},${Math.round(this.b*255)},${this.a.toFixed(2)})`;\n  }\n\n  toArray () {\n    if (!this.isValid) {\n      return;\n    }\n    return [this.r, this.g, this.b];\n  }\n\n  hue (h) {\n    const hsl = this.toHSL();\n    return this.fromHSL(hsl.h+h, hsl.s, hsl.l);\n  }\n\n  saturation (s) {\n    const hsl = this.toHSL();\n    return this.fromHSL(hsl.h, hsl.s*s, hsl.l);\n  }\n\n  lightness (l) {\n    const hsl = this.toHSL();\n    return this.fromHSL(hsl.h, hsl.s, hsl.l*l);\n  }\n\n  clone () {\n    return new Qolor(this.r, this.g, this.b, this.a);\n  }\n}\n\nQolor.w3cColors = {\n  aliceblue: '#f0f8ff',\n  antiquewhite: '#faebd7',\n  aqua: '#00ffff',\n  aquamarine: '#7fffd4',\n  azure: '#f0ffff',\n  beige: '#f5f5dc',\n  bisque: '#ffe4c4',\n  black: '#000000',\n  blanchedalmond: '#ffebcd',\n  blue: '#0000ff',\n  blueviolet: '#8a2be2',\n  brown: '#a52a2a',\n  burlywood: '#deb887',\n  cadetblue: '#5f9ea0',\n  chartreuse: '#7fff00',\n  chocolate: '#d2691e',\n  coral: '#ff7f50',\n  cornflowerblue: '#6495ed',\n  cornsilk: '#fff8dc',\n  crimson: '#dc143c',\n  cyan: '#00ffff',\n  darkblue: '#00008b',\n  darkcyan: '#008b8b',\n  darkgoldenrod: '#b8860b',\n  darkgray: '#a9a9a9',\n  darkgrey: '#a9a9a9',\n  darkgreen: '#006400',\n  darkkhaki: '#bdb76b',\n  darkmagenta: '#8b008b',\n  darkolivegreen: '#556b2f',\n  darkorange: '#ff8c00',\n  darkorchid: '#9932cc',\n  darkred: '#8b0000',\n  darksalmon: '#e9967a',\n  darkseagreen: '#8fbc8f',\n  darkslateblue: '#483d8b',\n  darkslategray: '#2f4f4f',\n  darkslategrey: '#2f4f4f',\n  darkturquoise: '#00ced1',\n  darkviolet: '#9400d3',\n  deeppink: '#ff1493',\n  deepskyblue: '#00bfff',\n  dimgray: '#696969',\n  dimgrey: '#696969',\n  dodgerblue: '#1e90ff',\n  firebrick: '#b22222',\n  floralwhite: '#fffaf0',\n  forestgreen: '#228b22',\n  fuchsia: '#ff00ff',\n  gainsboro: '#dcdcdc',\n  ghostwhite: '#f8f8ff',\n  gold: '#ffd700',\n  goldenrod: '#daa520',\n  gray: '#808080',\n  grey: '#808080',\n  green: '#008000',\n  greenyellow: '#adff2f',\n  honeydew: '#f0fff0',\n  hotpink: '#ff69b4',\n  indianred: '#cd5c5c',\n  indigo: '#4b0082',\n  ivory: '#fffff0',\n  khaki: '#f0e68c',\n  lavender: '#e6e6fa',\n  lavenderblush: '#fff0f5',\n  lawngreen: '#7cfc00',\n  lemonchiffon: '#fffacd',\n  lightblue: '#add8e6',\n  lightcoral: '#f08080',\n  lightcyan: '#e0ffff',\n  lightgoldenrodyellow: '#fafad2',\n  lightgray: '#d3d3d3',\n  lightgrey: '#d3d3d3',\n  lightgreen: '#90ee90',\n  lightpink: '#ffb6c1',\n  lightsalmon: '#ffa07a',\n  lightseagreen: '#20b2aa',\n  lightskyblue: '#87cefa',\n  lightslategray: '#778899',\n  lightslategrey: '#778899',\n  lightsteelblue: '#b0c4de',\n  lightyellow: '#ffffe0',\n  lime: '#00ff00',\n  limegreen: '#32cd32',\n  linen: '#faf0e6',\n  magenta: '#ff00ff',\n  maroon: '#800000',\n  mediumaquamarine: '#66cdaa',\n  mediumblue: '#0000cd',\n  mediumorchid: '#ba55d3',\n  mediumpurple: '#9370db',\n  mediumseagreen: '#3cb371',\n  mediumslateblue: '#7b68ee',\n  mediumspringgreen: '#00fa9a',\n  mediumturquoise: '#48d1cc',\n  mediumvioletred: '#c71585',\n  midnightblue: '#191970',\n  mintcream: '#f5fffa',\n  mistyrose: '#ffe4e1',\n  moccasin: '#ffe4b5',\n  navajowhite: '#ffdead',\n  navy: '#000080',\n  oldlace: '#fdf5e6',\n  olive: '#808000',\n  olivedrab: '#6b8e23',\n  orange: '#ffa500',\n  orangered: '#ff4500',\n  orchid: '#da70d6',\n  palegoldenrod: '#eee8aa',\n  palegreen: '#98fb98',\n  paleturquoise: '#afeeee',\n  palevioletred: '#db7093',\n  papayawhip: '#ffefd5',\n  peachpuff: '#ffdab9',\n  peru: '#cd853f',\n  pink: '#ffc0cb',\n  plum: '#dda0dd',\n  powderblue: '#b0e0e6',\n  purple: '#800080',\n  rebeccapurple: '#663399',\n  red: '#ff0000',\n  rosybrown: '#bc8f8f',\n  royalblue: '#4169e1',\n  saddlebrown: '#8b4513',\n  salmon: '#fa8072',\n  sandybrown: '#f4a460',\n  seagreen: '#2e8b57',\n  seashell: '#fff5ee',\n  sienna: '#a0522d',\n  silver: '#c0c0c0',\n  skyblue: '#87ceeb',\n  slateblue: '#6a5acd',\n  slategray: '#708090',\n  slategrey: '#708090',\n  snow: '#fffafa',\n  springgreen: '#00ff7f',\n  steelblue: '#4682b4',\n  tan: '#d2b48c',\n  teal: '#008080',\n  thistle: '#d8bfd8',\n  tomato: '#ff6347',\n  turquoise: '#40e0d0',\n  violet: '#ee82ee',\n  wheat: '#f5deb3',\n  white: '#ffffff',\n  whitesmoke: '#f5f5f5',\n  yellow: '#ffff00',\n  yellowgreen: '#9acd32'\n};\n\nif (typeof module !== 'undefined') {\n  module.exports = Qolor;\n}\n\n// calculations are based on http://aa.quae.nl/en/reken/zonpositie.html\n// code credits to Vladimir Agafonkin (@mourner)\n\nfunction getSunPosition () {\n\n  const m = Math,\n    PI = m.PI,\n    sin = m.sin,\n    cos = m.cos,\n    tan = m.tan,\n    asin = m.asin,\n    atan = m.atan2;\n\n  const rad = PI/180,\n    dayMs = 1000*60*60*24,\n    J1970 = 2440588,\n    J2000 = 2451545,\n    e = rad*23.4397; // obliquity of the Earth\n\n  function toJulian(date) {\n    return date.valueOf()/dayMs - 0.5+J1970;\n  }\n  function toDays(date) {\n    return toJulian(date)-J2000;\n  }\n  function getRightAscension(l, b) {\n    return atan(sin(l)*cos(e) - tan(b)*sin(e), cos(l));\n  }\n  function getDeclination(l, b) {\n    return asin(sin(b)*cos(e) + cos(b)*sin(e)*sin(l));\n  }\n  function getAzimuth(H, phi, dec) {\n    return atan(sin(H), cos(H)*sin(phi) - tan(dec)*cos(phi));\n  }\n  function getAltitude(H, phi, dec) {\n    return asin(sin(phi)*sin(dec) + cos(phi)*cos(dec)*cos(H));\n  }\n  function getSiderealTime(d, lw) {\n    return rad * (280.16 + 360.9856235*d) - lw;\n  }\n  function getSolarMeanAnomaly(d) {\n    return rad * (357.5291 + 0.98560028*d);\n  }\n  function getEquationOfCenter(M) {\n    return rad * (1.9148*sin(M) + 0.0200 * sin(2*M) + 0.0003 * sin(3*M));\n  }\n  function getEclipticLongitude(M, C) {\n    const P = rad*102.9372; // perihelion of the Earth\n    return M+C+P+PI;\n  }\n\n  return function getSunPosition(date, lat, lon) {\n    const lw = rad*-lon,\n      phi = rad*lat,\n      d = toDays(date),\n      M = getSolarMeanAnomaly(d),\n      C = getEquationOfCenter(M),\n      L = getEclipticLongitude(M, C),\n      D = getDeclination(L, 0),\n      A = getRightAscension(L, 0),\n      t = getSiderealTime(d, lw),\n      H = t-A;\n\n    return {\n      altitude: getAltitude(H, phi, D),\n      azimuth: getAzimuth(H, phi, D) - PI/2 // origin: north\n    };\n  };\n}\n\n\nconst METERS_PER_LEVEL = 3;\n\nconst materialColors = {\n  brick:'#cc7755',\n  bronze:'#ffeecc',\n  canvas:'#fff8f0',\n  concrete:'#999999',\n  copper:'#a0e0d0',\n  glass:'#e8f8f8',\n  gold:'#ffcc00',\n  plants:'#009933',\n  metal:'#aaaaaa',\n  panel:'#fff8f0',\n  plaster:'#999999',\n  roof_tiles:'#f08060',\n  silver:'#cccccc',\n  slate:'#666666',\n  stone:'#996666',\n  tar_paper:'#333333',\n  wood:'#deb887'\n};\n\nconst baseMaterials = {\n  asphalt:'tar_paper',\n  bitumen:'tar_paper',\n  block:'stone',\n  bricks:'brick',\n  glas:'glass',\n  glassfront:'glass',\n  grass:'plants',\n  masonry:'stone',\n  granite:'stone',\n  panels:'panel',\n  paving_stones:'stone',\n  plastered:'plaster',\n  rooftiles:'roof_tiles',\n  roofingfelt:'tar_paper',\n  sandstone:'stone',\n  sheet:'canvas',\n  sheets:'canvas',\n  shingle:'tar_paper',\n  shingles:'tar_paper',\n  slates:'slate',\n  steel:'metal',\n  tar:'tar_paper',\n  tent:'canvas',\n  thatch:'plants',\n  tile:'roof_tiles',\n  tiles:'roof_tiles'\n};\n// cardboard\n// eternit\n// limestone\n// straw\n\nfunction getMaterialColor (str) {\n  str = str.toLowerCase();\n  if (str[0] === '#') {\n    return str;\n  }\n  return materialColors[baseMaterials[str] || str] || null;\n}\n\nconst WINDING_CLOCKWISE = 'CW';\nconst WINDING_COUNTER_CLOCKWISE = 'CCW';\n\n// detect winding direction: clockwise or counter clockwise\nfunction getWinding (points) {\n  let x1, y1, x2, y2,\n    a = 0;\n  for (let i = 0, il = points.length-3; i < il; i += 2) {\n    x1 = points[i];\n    y1 = points[i+1];\n    x2 = points[i+2];\n    y2 = points[i+3];\n    a += x1*y2 - x2*y1;\n  }\n  return (a/2) > 0 ? WINDING_CLOCKWISE : WINDING_COUNTER_CLOCKWISE;\n}\n\n// enforce a polygon winding direcetion. Needed for proper backface culling.\nfunction makeWinding (points, direction) {\n  let winding = getWinding(points);\n  if (winding === direction) {\n    return points;\n  }\n  let revPoints = [];\n  for (let i = points.length-2; i >= 0; i -= 2) {\n    revPoints.push(points[i], points[i+1]);\n  }\n  return revPoints;\n}\n\nfunction alignProperties(prop) {\n  const item = {};\n\n  prop = prop || {};\n\n  item.height    = prop.height    || (prop.levels   ? prop.levels  *METERS_PER_LEVEL : DEFAULT_HEIGHT);\n  item.minHeight = prop.minHeight || (prop.minLevel ? prop.minLevel*METERS_PER_LEVEL : 0);\n\n  const wallColor = prop.material ? getMaterialColor(prop.material) : (prop.wallColor || prop.color);\n  if (wallColor) {\n    item.wallColor = wallColor;\n  }\n\n  const roofColor = prop.roofMaterial ? getMaterialColor(prop.roofMaterial) : prop.roofColor;\n  if (roofColor) {\n    item.roofColor = roofColor;\n  }\n\n  switch (prop.shape) {\n    case 'cylinder':\n    case 'cone':\n    case 'dome':\n    case 'sphere':\n      item.shape = prop.shape;\n      item.isRotational = true;\n    break;\n\n    case 'pyramid':\n      item.shape = prop.shape;\n    break;\n  }\n\n  switch (prop.roofShape) {\n    case 'cone':\n    case 'dome':\n      item.roofShape = prop.roofShape;\n      item.isRotational = true;\n    break;\n\n    case 'pyramid':\n      item.roofShape = prop.roofShape;\n    break;\n  }\n\n  if (item.roofShape && prop.roofHeight) {\n    item.roofHeight = prop.roofHeight;\n    item.height = max(0, item.height-item.roofHeight);\n  } else {\n    item.roofHeight = 0;\n  }\n\n  return item;\n}\n\nfunction getGeometries (geometry) {\n  let\n    polygon,\n    geometries = [], sub;\n\n  switch (geometry.type) {\n    case 'GeometryCollection':\n      geometries = [];\n      for (let i = 0, il = geometry.geometries.length; i < il; i++) {\n        if ((sub = getGeometries(geometry.geometries[i]))) {\n          geometries.push.apply(geometries, sub);\n        }\n      }\n      return geometries;\n\n    case 'MultiPolygon':\n      geometries = [];\n      for (let i = 0, il = geometry.coordinates.length; i < il; i++) {\n        if ((sub = getGeometries({ type: 'Polygon', coordinates: geometry.coordinates[i] }))) {\n          geometries.push.apply(geometries, sub);\n        }\n      }\n      return geometries;\n\n    case 'Polygon':\n      polygon = geometry.coordinates;\n    break;\n\n    default: return [];\n  }\n\n  let\n    p, lat = 1, lon = 0,\n    outer = [], inner = [];\n\n  p = polygon[0];\n  for (let i = 0, il = p.length; i < il; i++) {\n    outer.push(p[i][lat], p[i][lon]);\n  }\n  outer = makeWinding(outer, WINDING_CLOCKWISE);\n\n  for (let i = 0, il = polygon.length-1; i < il; i++) {\n    p = polygon[i+1];\n    inner[i] = [];\n    for (let j = 0, jl = p.length; j < jl; j++) {\n      inner[i].push(p[j][lat], p[j][lon]);\n    }\n    inner[i] = makeWinding(inner[i], WINDING_COUNTER_CLOCKWISE);\n  }\n\n  return [{\n    outer: outer,\n    inner: inner.length ? inner : null\n  }];\n}\n\nfunction clone (obj) {\n  let res = {};\n  for (const p in obj) {\n    if (obj.hasOwnProperty(p)) {\n      res[p] = obj[p];\n    }\n  }\n  return res;\n}\n\nclass GeoJSON {\n\n  static read (geojson) {\n    if (!geojson || geojson.type !== 'FeatureCollection') {\n      return [];\n    }\n\n    const collection = geojson.features;\n    const res = [];\n\n    for (let i = 0, il = collection.length; i < il; i++) {\n      const feature = collection[i];\n\n      if (feature.type !== 'Feature' || onEach(feature) === false) {\n        continue;\n      }\n\n      const baseItem = alignProperties(feature.properties);\n      const geometries = getGeometries(feature.geometry);\n\n      for (let j = 0, jl = geometries.length; j < jl; j++) {\n        const item = clone(baseItem);\n        item.footprint = geometries[j].outer;\n        if (item.isRotational) {\n          item.radius = getLonDelta(item.footprint);\n        }\n\n        if (geometries[j].inner) {\n          item.holes = geometries[j].inner;\n        }\n        if (feature.id || feature.properties.id) {\n          item.id = feature.id || feature.properties.id;\n        }\n\n        if (feature.properties.relationId) {\n          item.relationId = feature.properties.relationId;\n        }\n\n        res.push(item); // TODO: clone base properties!\n      }\n    }\n\n    return res;\n  }\n}\n\nlet\n  VERSION      = '0.3.2',\n  ATTRIBUTION  = '&copy; <a href=\"https://osmbuildings.org\">OSM Buildings</a>',\n\n  DATA_SRC = 'https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json',\n\n  PI         = Math.PI,\n  HALF_PI    = PI/2,\n  QUARTER_PI = PI/4,\n\n  MAP_TILE_SIZE  = 256,    // map tile size in pixels\n  ZOOM, MAP_SIZE,\n\n  MIN_ZOOM = 15,\n\n  LAT = 'latitude', LON = 'longitude',\n\n  WIDTH = 0, HEIGHT = 0,\n  CENTER_X = 0, CENTER_Y = 0,\n  ORIGIN_X = 0, ORIGIN_Y = 0,\n\n  WALL_COLOR = Qolor.parse('rgba(200, 190, 180)'),\n  ALT_COLOR  = WALL_COLOR.lightness(0.8),\n  ROOF_COLOR = WALL_COLOR.lightness(1.2),\n\n  WALL_COLOR_STR = ''+ WALL_COLOR,\n  ALT_COLOR_STR  = ''+ ALT_COLOR,\n  ROOF_COLOR_STR = ''+ ROOF_COLOR,\n\n  PIXEL_PER_DEG = 0,\n\n  MAX_HEIGHT, // taller buildings will be cut to this\n  DEFAULT_HEIGHT = 5,\n\n  CAM_X, CAM_Y, CAM_Z = 450,\n\n  IS_ZOOMING;\n\nfunction onEach () {}\n\nfunction onClick () {}\n\n\nfunction getDistance (p1, p2) {\n  const\n    dx = p1.x-p2.x,\n    dy = p1.y-p2.y;\n  return dx*dx + dy*dy;\n}\n\nfunction isRotational (polygon) {\n  const length = polygon.length;\n  if (length < 16) {\n    return false;\n  }\n\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n  for (let i = 0; i < length-1; i+=2) {\n    minX = Math.min(minX, polygon[i]);\n    maxX = Math.max(maxX, polygon[i]);\n    minY = Math.min(minY, polygon[i+1]);\n    maxY = Math.max(maxY, polygon[i+1]);\n  }\n\n  const\n    width = maxX-minX,\n    height = (maxY-minY),\n    ratio = width/height;\n\n  if (ratio < 0.85 || ratio > 1.15) {\n    return false;\n  }\n\n  const\n    center = { x:minX+width/2, y:minY+height/2 },\n    radius = (width+height)/4,\n    sqRadius = radius*radius;\n\n  for (let i = 0; i < length-1; i+=2) {\n    const dist = getDistance({ x:polygon[i], y:polygon[i+1] }, center);\n    if (dist/sqRadius < 0.8 || dist/sqRadius > 1.2) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction getSquareSegmentDistance (px, py, p1x, p1y, p2x, p2y) {\n  let\n    dx = p2x-p1x,\n    dy = p2y-p1y,\n    t;\n  if (dx !== 0 || dy !== 0) {\n    t = ((px-p1x) * dx + (py-p1y) * dy) / (dx*dx + dy*dy);\n    if (t > 1) {\n      p1x = p2x;\n      p1y = p2y;\n    } else if (t > 0) {\n      p1x += dx*t;\n      p1y += dy*t;\n    }\n  }\n  dx = px-p1x;\n  dy = py-p1y;\n  return dx*dx + dy*dy;\n}\n\nfunction simplifyPolygon (buffer) {\n  let\n    sqTolerance = 2,\n    len = buffer.length/2,\n    markers = new Uint8Array(len),\n\n    first = 0, last = len-1,\n\n    maxSqDist,\n    sqDist,\n    index,\n    firstStack = [], lastStack  = [],\n    newBuffer  = [];\n\n  markers[first] = markers[last] = 1;\n\n  while (last) {\n    maxSqDist = 0;\n    for (let i = first+1; i < last; i++) {\n      sqDist = getSquareSegmentDistance(\n        buffer[i    *2], buffer[i    *2 + 1],\n        buffer[first*2], buffer[first*2 + 1],\n        buffer[last *2], buffer[last *2 + 1]\n      );\n      if (sqDist > maxSqDist) {\n        index = i;\n        maxSqDist = sqDist;\n      }\n    }\n\n    if (maxSqDist > sqTolerance) {\n      markers[index] = 1;\n\n      firstStack.push(first);\n      lastStack.push(index);\n\n      firstStack.push(index);\n      lastStack.push(last);\n    }\n\n    first = firstStack.pop();\n    last = lastStack.pop();\n  }\n\n  for (let i = 0; i < len; i++) {\n    if (markers[i]) {\n      newBuffer.push(buffer[i*2], buffer[i*2 + 1]);\n    }\n  }\n\n  return newBuffer;\n}\n\nfunction getCenter (footprint) {\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n  for (let i = 0, il = footprint.length-3; i < il; i += 2) {\n    minX = min(minX, footprint[i]);\n    maxX = max(maxX, footprint[i]);\n    minY = min(minY, footprint[i+1]);\n    maxY = max(maxY, footprint[i+1]);\n  }\n  return { x:minX+(maxX-minX)/2 <<0, y:minY+(maxY-minY)/2 <<0 };\n}\n\nlet EARTH_RADIUS = 6378137;\n\nfunction getLonDelta (footprint) {\n  let minLon = 180, maxLon = -180;\n  for (let i = 0, il = footprint.length; i < il; i += 2) {\n    minLon = min(minLon, footprint[i+1]);\n    maxLon = max(maxLon, footprint[i+1]);\n  }\n  return (maxLon-minLon)/2;\n}\n\n\nfunction rad (deg) {\n  return deg * PI / 180;\n}\n\nfunction deg (rad) {\n  return rad / PI * 180;\n}\n\nfunction pixelToGeo (x, y) {\n  const res = {};\n  x /= MAP_SIZE;\n  y /= MAP_SIZE;\n  res[LAT] = y <= 0  ? 90 : y >= 1 ? -90 : deg(2 * atan(exp(PI * (1 - 2*y))) - HALF_PI);\n  res[LON] = (x === 1 ?  1 : (x%1 + 1) % 1) * 360 - 180;\n  return res;\n}\n\nfunction geoToPixel (lat, lon) {\n  const\n    latitude = min(1, max(0, 0.5 - (log(tan(QUARTER_PI + HALF_PI * lat / 180)) / PI) / 2)),\n    longitude = lon/360 + 0.5;\n  return {\n    x: longitude*MAP_SIZE <<0,\n    y: latitude *MAP_SIZE <<0\n  };\n}\n\nfunction fromRange (sVal, sMin, sMax, dMin, dMax) {\n  sVal = min(max(sVal, sMin), sMax);\n  const rel = (sVal-sMin) / (sMax-sMin),\n    range = dMax-dMin;\n  return min(max(dMin + rel*range, dMin), dMax);\n}\n\nfunction isVisible (polygon) {\n  const\n    maxX = WIDTH+ORIGIN_X,\n    maxY = HEIGHT+ORIGIN_Y;\n\n  // TODO: checking footprint is sufficient for visibility - NOT VALID FOR SHADOWS!\n  for (let i = 0, il = polygon.length-3; i < il; i+=2) {\n    if (polygon[i] > ORIGIN_X && polygon[i] < maxX && polygon[i+1] > ORIGIN_Y && polygon[i+1] < maxY) {\n      return true;\n    }\n  }\n  return false;\n}\n\n\nlet cacheData = {};\nlet cacheIndex = [];\nlet cacheSize = 0;\nlet maxCacheSize = 1024*1024 * 5; // 5MB\n\nfunction xhr (url, callback) {\n  if (cacheData[url]) {\n    if (callback) {\n      callback(cacheData[url]);\n    }\n    return;\n  }\n\n  const req = new XMLHttpRequest();\n\n  req.onreadystatechange = function () {\n    if (req.readyState !== 4) {\n      return;\n    }\n    if (!req.status || req.status < 200 || req.status > 299) {\n      return;\n    }\n    if (callback && req.responseText) {\n      const responseText = req.responseText;\n\n      cacheData[url] = responseText;\n      cacheIndex.push({ url: url, size: responseText.length });\n      cacheSize += responseText.length;\n\n      callback(responseText);\n\n      while (cacheSize > maxCacheSize) {\n        let item = cacheIndex.shift();\n        cacheSize -= item.size;\n        delete cacheData[item.url];\n      }\n    }\n  };\n\n  req.open('GET', url);\n  req.send(null);\n\n  return req;\n}\n\nclass Request {\n\n  static loadJSON (url, callback) {\n    return xhr(url, responseText => {\n      let json;\n      try {\n        json = JSON.parse(responseText);\n      } catch(ex) {}\n\n      callback(json);\n    });\n  }\n}\n\n\nclass Data {\n\n  static getPixelFootprint (buffer) {\n    let footprint = new Int32Array(buffer.length),\n      px;\n\n    for (let i = 0, il = buffer.length-1; i < il; i+=2) {\n      px = geoToPixel(buffer[i], buffer[i+1]);\n      footprint[i]   = px.x;\n      footprint[i+1] = px.y;\n    }\n\n    footprint = simplifyPolygon(footprint);\n    if (footprint.length < 8) { // 3 points & end==start (*2)\n      return;\n    }\n\n    return footprint;\n  }\n\n  static resetItems () {\n    this.items = [];\n    this.cache = {};\n    Picking.reset();\n  }\n\n  static addRenderItems (data, allAreNew) {\n    let item, scaledItem, id;\n    let geojson = GeoJSON.read(data);\n    for (let i = 0, il = geojson.length; i < il; i++) {\n      item = geojson[i];\n      id = item.id || [item.footprint[0], item.footprint[1], item.height, item.minHeight].join(',');\n      if (!this.cache[id]) {\n        if ((scaledItem = this.scaleItem(item))) {\n          scaledItem.scale = allAreNew ? 0 : 1;\n          this.items.push(scaledItem);\n          this.cache[id] = 1;\n        }\n      }\n    }\n    fadeIn();\n  }\n\n  static scalePolygon (buffer, factor) {\n    return buffer.map(coord => coord*factor);\n  }\n\n  static scale (factor) {\n    Data.items = Data.items.map(item => {\n      // item.height = Math.min(item.height*factor, MAX_HEIGHT); // TODO: should be filtered by renderer\n\n      item.height *= factor;\n      item.minHeight *= factor;\n\n      item.footprint = Data.scalePolygon(item.footprint, factor);\n      item.center.x *= factor;\n      item.center.y *= factor;\n\n      if (item.radius) {\n        item.radius *= factor;\n      }\n\n      if (item.holes) {\n        for (let i = 0, il = item.holes.length; i < il; i++) {\n          item.holes[i] = Data.scalePolygon(item.holes[i], factor);\n        }\n      }\n\n      item.roofHeight *= factor;\n\n      return item;\n    });\n  }\n\n  static scaleItem (item) {\n    let\n      res = {},\n      // TODO: calculate this on zoom change only\n      zoomScale = 6 / pow(2, ZOOM-MIN_ZOOM); // TODO: consider using HEIGHT / (devicePixelRatio || 1)\n\n    if (item.id) {\n      res.id = item.id;\n    }\n\n    res.height = min(item.height/zoomScale, MAX_HEIGHT);\n\n    res.minHeight = isNaN(item.minHeight) ? 0 : item.minHeight / zoomScale;\n    if (res.minHeight > MAX_HEIGHT) {\n      return;\n    }\n\n    res.footprint = this.getPixelFootprint(item.footprint);\n    if (!res.footprint) {\n      return;\n    }\n    res.center = getCenter(res.footprint);\n\n    if (item.radius) {\n      res.radius = item.radius*PIXEL_PER_DEG;\n    }\n    if (item.shape) {\n      res.shape = item.shape;\n    }\n    if (item.roofShape) {\n      res.roofShape = item.roofShape;\n    }\n    if ((res.roofShape === 'cone' || res.roofShape === 'dome') && !res.shape && isRotational(res.footprint)) {\n      res.shape = 'cylinder';\n    }\n\n    if (item.holes) {\n      res.holes = [];\n      let innerFootprint;\n      for (let i = 0, il = item.holes.length; i < il; i++) {\n        // TODO: simplify\n        if ((innerFootprint = this.getPixelFootprint(item.holes[i]))) {\n          res.holes.push(innerFootprint);\n        }\n      }\n    }\n\n    let color;\n\n    if (item.wallColor) {\n      if ((color = Qolor.parse(item.wallColor))) {\n        res.altColor  = ''+ color.lightness(0.8);\n        res.wallColor = ''+ color;\n      }\n    }\n\n    if (item.roofColor) {\n      if ((color = Qolor.parse(item.roofColor))) {\n        res.roofColor = ''+ color;\n      }\n    }\n\n    if (item.relationId) {\n      res.relationId = item.relationId;\n    }\n    res.hitColor = Picking.idToColor(item.relationId || item.id);\n\n    res.roofHeight = isNaN(item.roofHeight) ? 0 : item.roofHeight/zoomScale;\n\n    if (res.height+res.roofHeight <= res.minHeight) {\n      return;\n    }\n\n    return res;\n  }\n\n  static set (data) {\n    this.resetItems();\n    this._staticData = data;\n    this.addRenderItems(this._staticData, true);\n  }\n\n  static load (src, key) {\n    this.src = src || DATA_SRC.replace('{k}', (key || 'anonymous'));\n    this.update();\n  }\n\n  static update () {\n    this.resetItems();\n\n    if (ZOOM < MIN_ZOOM) {\n      return;\n    }\n\n    if (this._staticData) {\n      this.addRenderItems(this._staticData);\n    }\n\n    if (this.src) {\n      let\n        tileZoom = 16,\n        tileSize = 256,\n        zoomedTileSize = ZOOM > tileZoom ? tileSize << (ZOOM - tileZoom) : tileSize >> (tileZoom - ZOOM),\n        minX = ORIGIN_X / zoomedTileSize << 0,\n        minY = ORIGIN_Y / zoomedTileSize << 0,\n        maxX = ceil((ORIGIN_X + WIDTH) / zoomedTileSize),\n        maxY = ceil((ORIGIN_Y + HEIGHT) / zoomedTileSize),\n        x, y;\n\n      let scope = this;\n\n      function callback (json) {\n        scope.addRenderItems(json);\n      }\n\n      for (y = minY; y <= maxY; y++) {\n        for (x = minX; x <= maxX; x++) {\n          this.loadTile(x, y, tileZoom, callback);\n        }\n      }\n    }\n  }\n\n  static loadTile (x, y, zoom, callback) {\n    let s = 'abcd'[(x+y) % 4];\n    let url = this.src.replace('{s}', s).replace('{x}', x).replace('{y}', y).replace('{z}', zoom);\n    return Request.loadJSON(url, callback);\n  }\n}\n\nData.cache = {}; // maintain a list of cached items in order to avoid duplicates on tile borders\nData.items = [];\n\nclass Extrusion {\n\n  static draw (context, polygon, innerPolygons, height, minHeight, color, altColor, roofColor) {\n    let\n      roof = this._extrude(context, polygon, height, minHeight, color, altColor),\n      innerRoofs = [];\n\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        innerRoofs[i] = this._extrude(context, innerPolygons[i], height, minHeight, color, altColor);\n      }\n    }\n\n    context.fillStyle = roofColor;\n\n    context.beginPath();\n    this._ring(context, roof);\n    if (innerPolygons) {\n      for (let i = 0, il = innerRoofs.length; i < il; i++) {\n        this._ring(context, innerRoofs[i]);\n      }\n    }\n    context.closePath();\n    context.fill();\n  }\n\n  static _extrude (context, polygon, height, minHeight, color, altColor) {\n    let\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      _a, _b,\n      roof = [];\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Buildings.project(a, scale);\n      _b = Buildings.project(b, scale);\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set wall shading\n        if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n          context.fillStyle = altColor;\n        } else {\n          context.fillStyle = color;\n        }\n\n        context.beginPath();\n        this._ring(context, [\n           b.x,  b.y,\n           a.x,  a.y,\n          _a.x, _a.y,\n          _b.x, _b.y\n        ]);\n        context.closePath();\n        context.fill();\n      }\n\n      roof[i]   = _a.x;\n      roof[i+1] = _a.y;\n    }\n\n    return roof;\n  }\n\n  static _ring (context, polygon) {\n    context.moveTo(polygon[0], polygon[1]);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i], polygon[i+1]);\n    }\n  }\n\n  static simplified (context, polygon, innerPolygons) {\n    context.beginPath();\n    this._ringAbs(context, polygon);\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        this._ringAbs(context, innerPolygons[i]);\n      }\n    }\n    context.closePath();\n    context.fill();\n  }\n\n  static _ringAbs (context, polygon) {\n    context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y);\n    }\n  }\n\n  static shadow (context, polygon, innerPolygons, height, minHeight) {\n    let\n      mode = null,\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      _a, _b;\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Shadows.project(a, height);\n      _b = Shadows.project(b, height);\n\n      if (minHeight) {\n        a = Shadows.project(a, minHeight);\n        b = Shadows.project(b, minHeight);\n      }\n\n      // mode 0: floor edges, mode 1: roof edges\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        if (mode === 1) {\n          context.lineTo(a.x, a.y);\n        }\n        mode = 0;\n        if (!i) {\n          context.moveTo(a.x, a.y);\n        }\n        context.lineTo(b.x, b.y);\n      } else {\n        if (mode === 0) {\n          context.lineTo(_a.x, _a.y);\n        }\n        mode = 1;\n        if (!i) {\n          context.moveTo(_a.x, _a.y);\n        }\n        context.lineTo(_b.x, _b.y);\n      }\n    }\n\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        this._ringAbs(context, innerPolygons[i]);\n      }\n    }\n  }\n\n  static hitArea (context, polygon, innerPolygons, height, minHeight, color) {\n    let\n      mode = null,\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      _a, _b;\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Buildings.project(a, scale);\n      _b = Buildings.project(b, scale);\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // mode 0: floor edges, mode 1: roof edges\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        if (mode === 1) { // mode is initially undefined\n          context.lineTo(a.x, a.y);\n        }\n        mode = 0;\n        if (!i) {\n          context.moveTo(a.x, a.y);\n        }\n        context.lineTo(b.x, b.y);\n      } else {\n        if (mode === 0) { // mode is initially undefined\n          context.lineTo(_a.x, _a.y);\n        }\n        mode = 1;\n        if (!i) {\n          context.moveTo(_a.x, _a.y);\n        }\n        context.lineTo(_b.x, _b.y);\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nclass Cylinder {\n\n  static draw (context, center, radius, topRadius, height, minHeight, color, altColor, roofColor) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a1, a2;\n\n    topRadius *= scale;\n\n    if (minHeight) {\n      c = Buildings.project(c, minScale);\n      radius = radius*minScale;\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    // no tangents? top circle is inside bottom circle\n    if (!tangents) {\n      a1 = 1.5*PI;\n      a2 = 1.5*PI;\n    } else {\n      a1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      a2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n    }\n\n    context.fillStyle = color;\n    context.beginPath();\n    context.arc(apex.x, apex.y, topRadius, HALF_PI, a1, true);\n    context.arc(c.x, c.y, radius, a1, HALF_PI);\n    context.closePath();\n    context.fill();\n\n    context.fillStyle = altColor;\n    context.beginPath();\n    context.arc(apex.x, apex.y, topRadius, a2, HALF_PI, true);\n    context.arc(c.x, c.y, radius, HALF_PI, a2);\n    context.closePath();\n    context.fill();\n\n    context.fillStyle = roofColor;\n    this._circle(context, apex, topRadius);\n  }\n\n  static simplified (context, center, radius) {\n    this._circle(context, { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, radius);\n  }\n\n  static shadow (context, center, radius, topRadius, height, minHeight) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      apex = Shadows.project(c, height),\n      p1, p2;\n\n    if (minHeight) {\n      c = Shadows.project(c, minHeight);\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    // TODO: no tangents? roof overlaps everything near cam position\n    if (tangents) {\n      p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n      context.moveTo(tangents[1].x2, tangents[1].y2);\n      context.arc(apex.x, apex.y, topRadius, p2, p1);\n      context.arc(c.x, c.y, radius, p1, p2);\n    } else {\n      context.moveTo(c.x+radius, c.y);\n      context.arc(c.x, c.y, radius, 0, 2*PI);\n    }\n  }\n\n  static hitArea (context, center, radius, topRadius, height, minHeight, color) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      p1, p2;\n\n    topRadius *= scale;\n\n    if (minHeight) {\n      c = Buildings.project(c, minScale);\n      radius = radius*minScale;\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    // TODO: no tangents? roof overlaps everything near cam position\n    if (tangents) {\n      p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n      context.moveTo(tangents[1].x2, tangents[1].y2);\n      context.arc(apex.x, apex.y, topRadius, p2, p1);\n      context.arc(c.x, c.y, radius, p1, p2);\n    } else {\n      context.moveTo(c.x+radius, c.y);\n      context.arc(c.x, c.y, radius, 0, 2*PI);\n    }\n\n    context.closePath();\n    context.fill();\n  }\n\n  static _circle (context, center, radius) {\n    context.beginPath();\n    context.arc(center.x, center.y, radius, 0, PI*2);\n    context.fill();\n  }\n\n    // http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Tangents_between_two_circles\n  static _tangents (c1, r1, c2, r2) {\n    let\n      dx = c1.x-c2.x,\n      dy = c1.y-c2.y,\n      dr = r1-r2,\n      sqdist = (dx*dx) + (dy*dy);\n\n    if (sqdist <= dr*dr) {\n      return;\n    }\n\n    let dist = sqrt(sqdist),\n      vx = -dx/dist,\n      vy = -dy/dist,\n      c  =  dr/dist,\n      res = [],\n      h, nx, ny;\n\n    // Let A, B be the centers, and C, D be points at which the tangent\n    // touches first and second circle, and n be the normal vector to it.\n    //\n    // We have the system:\n    //   n * n = 1    (n is a unit vector)\n    //   C = A + r1 * n\n    //   D = B + r2 * n\n    //   n * CD = 0   (common orthogonality)\n    //\n    // n * CD = n * (AB + r2*n - r1*n) = AB*n - (r1 -/+ r2) = 0,  <=>\n    // AB * n = (r1 -/+ r2), <=>\n    // v * n = (r1 -/+ r2) / d,  where v = AB/|AB| = AB/d\n    // This is a linear equation in unknown vector n.\n    // Now we're just intersecting a line with a circle: v*n=c, n*n=1\n\n    h = sqrt(max(0, 1 - c*c));\n    for (let sign = 1; sign >= -1; sign -= 2) {\n      nx = vx*c - sign*h*vy;\n      ny = vy*c + sign*h*vx;\n      res.push({\n        x1: c1.x + r1*nx <<0,\n        y1: c1.y + r1*ny <<0,\n        x2: c2.x + r2*nx <<0,\n        y2: c2.y + r2*ny <<0\n      });\n    }\n\n    return res;\n  }\n}\n\nclass Pyramid {\n\n  static draw (context, polygon, center, height, minHeight, color, altColor) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 };\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set shading\n        if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n          context.fillStyle = altColor;\n        } else {\n          context.fillStyle = color;\n        }\n\n        context.beginPath();\n        this._triangle(context, a, b, apex);\n        context.closePath();\n        context.fill();\n      }\n    }\n  }\n\n  static _triangle (context, a, b, c) {\n    context.moveTo(a.x, a.y);\n    context.lineTo(b.x, b.y);\n    context.lineTo(c.x, c.y);\n  }\n\n  static _ring (context, polygon) {\n    context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y);\n    }\n  }\n\n  static shadow (context, polygon, center, height, minHeight) {\n    let\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      apex = Shadows.project(c, height);\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Shadows.project(a, minHeight);\n        b = Shadows.project(b, minHeight);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set shading\n        this._triangle(context, a, b, apex);\n      }\n    }\n  }\n\n  static hitArea (context, polygon, center, height, minHeight, color) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 };\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        this._triangle(context, a, b, apex);\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nlet animTimer;\n\nfunction fadeIn() {\n  if (animTimer) {\n    return;\n  }\n\n  animTimer = setInterval(t => {\n    let dataItems = Data.items,\n      isNeeded = false;\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      if (dataItems[i].scale < 1) {\n        dataItems[i].scale += 0.5*0.2; // amount*easing\n        if (dataItems[i].scale > 1) {\n          dataItems[i].scale = 1;\n        }\n        isNeeded = true;\n      }\n    }\n\n    Layers.render();\n\n    if (!isNeeded) {\n      clearInterval(animTimer);\n      animTimer = null;\n    }\n  }, 33);\n}\n\nclass Layers {\n\n  static init () {\n    Layers.container.className = 'osmb-container';\n\n    // TODO: improve this\n    Shadows.init(Layers.createContext(Layers.container));\n    Simplified.init(Layers.createContext(Layers.container));\n    Buildings.init(Layers.createContext(Layers.container));\n    Picking.init(Layers.createContext());\n  }\n\n  static clear () {\n    Shadows.clear();\n    Simplified.clear();\n    Buildings.clear();\n    Picking.clear();\n  }\n\n  static setOpacity (opacity) {\n    Shadows.setOpacity(opacity);\n    Simplified.setOpacity(opacity);\n    Buildings.setOpacity(opacity);\n    Picking.setOpacity(opacity);\n  }\n\n  static render (quick) {\n    // show on high zoom levels only\n    if (ZOOM < MIN_ZOOM) {\n      Layers.clear();\n      return;\n    }\n\n    // don't render during zoom\n    if (IS_ZOOMING) {\n      return;\n    }\n\n    requestAnimationFrame(f => {\n      if (!quick) {\n        Shadows.render();\n        Simplified.render();\n        //HitAreas.render(); // TODO: do this on demand\n      }\n      Buildings.render();\n    });\n  }\n\n  static createContext (container) {\n    let canvas = document.createElement('CANVAS');\n    canvas.className = 'osmb-layer';\n\n    let context = canvas.getContext('2d');\n    context.lineCap   = 'round';\n    context.lineJoin  = 'round';\n    context.lineWidth = 1;\n    context.imageSmoothingEnabled = false;\n\n    Layers.items.push(canvas);\n    if (container) {\n      container.appendChild(canvas);\n    }\n\n    return context;\n  }\n\n  static appendTo (parentNode) {\n    parentNode.appendChild(Layers.container);\n  }\n\n  static remove () {\n    Layers.container.parentNode.removeChild(Layers.container);\n  }\n\n  static setSize (width, height) {\n    Layers.items.forEach(canvas => {\n      canvas.width  = width;\n      canvas.height = height;\n    });\n  }\n\n  // usually called after move: container jumps by move delta, cam is reset\n  static setPosition (x, y) {\n    Layers.container.style.left = x +'px';\n    Layers.container.style.top  = y +'px';\n  }\n}\n\nLayers.container = document.createElement('DIV');\nLayers.items = [];\n\nclass Buildings {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.context.canvas.style.opacity = opacity;\n  }\n\n  static project (p, m) {\n    return {\n      x: (p.x-CAM_X) * m + CAM_X <<0,\n      y: (p.y-CAM_Y) * m + CAM_Y <<0\n    };\n  }\n\n  static render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      item,\n      h, mh,\n      sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y },\n      footprint,\n      wallColor, altColor, roofColor,\n      dataItems = Data.items;\n\n    dataItems.sort((a, b) => {\n      return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height);\n    });\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (Simplified.isSimple(item)) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      // when fading in, use a dynamic height\n      h = item.scale < 1 ? item.height*item.scale : item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight;\n      }\n\n      wallColor = item.wallColor || WALL_COLOR_STR;\n      altColor  = item.altColor  || ALT_COLOR_STR;\n      roofColor = item.roofColor || ROOF_COLOR_STR;\n      context.strokeStyle = altColor;\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break;\n        case 'cone':     Cylinder.draw(context, item.center, item.radius, 0, h, mh, wallColor, altColor);                      break;\n        case 'dome':     Cylinder.draw(context, item.center, item.radius, item.radius/2, h, mh, wallColor, altColor);          break;\n        case 'sphere':   Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break;\n        case 'pyramid':  Pyramid.draw(context, footprint, item.center, h, mh, wallColor, altColor);                            break;\n        default:         Extrusion.draw(context, footprint, item.holes, h, mh, wallColor, altColor, roofColor);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.draw(context, item.center, item.radius, 0, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9));             break;\n        case 'dome':    Cylinder.draw(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9)); break;\n        case 'pyramid': Pyramid.draw(context, footprint, item.center, h+item.roofHeight, h, roofColor, Qolor.parse(roofColor).lightness(0.9));                       break;\n      }\n    }\n  }\n}\n\nclass Simplified {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.context.canvas.style.opacity = opacity;\n  }\n\n  static isSimple (item) {\n    return (ZOOM <= Simplified.MAX_ZOOM && item.height+item.roofHeight < Simplified.MAX_HEIGHT);\n  }\n\n  static render () {\n    this.clear();\n    \n    let context = this.context;\n\n    // show on high zoom levels only and avoid rendering during zoom\n    if (ZOOM > Simplified.MAX_ZOOM) {\n      return;\n    }\n\n    let\n      item,\n      footprint,\n      dataItems = Data.items;\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (item.height >= Simplified.MAX_HEIGHT) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      context.strokeStyle = item.altColor  || ALT_COLOR_STR;\n      context.fillStyle   = item.roofColor || ROOF_COLOR_STR;\n\n      switch (item.shape) {\n        case 'cylinder':\n        case 'cone':\n        case 'dome':\n        case 'sphere': Cylinder.simplified(context, item.center, item.radius);  break;\n        default: Extrusion.simplified(context, footprint, item.holes);\n      }\n    }\n  }\n}\n\nSimplified.MAX_ZOOM = 16; // max zoom where buildings could render simplified\nSimplified.MAX_HEIGHT = 5; // max building height in order to be simple\n\nclass Shadows {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.opacity = opacity;\n  }\n\n  static project (p, h) {\n    return {\n      x: p.x + this.direction.x*h,\n      y: p.y + this.direction.y*h\n    };\n  }\n\n  static render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      screenCenter,\n      sun, length, alpha;\n\n    // TODO: calculate this just on demand\n    screenCenter = pixelToGeo(CENTER_X+ORIGIN_X, CENTER_Y+ORIGIN_Y);\n    sun = getSunPosition(this.date, screenCenter.latitude, screenCenter.longitude);\n\n    if (sun.altitude <= 0) {\n      return;\n    }\n\n    length = 1 / tan(sun.altitude);\n    alpha = length < 5 ? 0.75 : 1/length*5;\n\n    this.direction.x = cos(sun.azimuth) * length;\n    this.direction.y = sin(sun.azimuth) * length;\n\n    let\n      i, il,\n      item,\n      h, mh,\n      footprint,\n      dataItems = Data.items;\n\n    context.canvas.style.opacity = alpha / (this.opacity * 2);\n    context.shadowColor = this.blurColor;\n    context.fillStyle = this.color;\n    context.beginPath();\n\n    for (i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      // when fading in, use a dynamic height\n      h = item.scale < 1 ? item.height*item.scale : item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight;\n      }\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh);   break;\n        case 'cone':     Cylinder.shadow(context, item.center, item.radius, 0, h, mh);             break;\n        case 'dome':     Cylinder.shadow(context, item.center, item.radius, item.radius/2, h, mh); break;\n        case 'sphere':   Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh);   break;\n        case 'pyramid':  Pyramid.shadow(context, footprint, item.center, h, mh);                   break;\n        default:         Extrusion.shadow(context, footprint, item.holes, h, mh);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.shadow(context, item.center, item.radius, 0, h+item.roofHeight, h);             break;\n        case 'dome':    Cylinder.shadow(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h); break;\n        case 'pyramid': Pyramid.shadow(context, footprint, item.center, h+item.roofHeight, h);                   break;\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nShadows.color = '#666666';\nShadows.blurColor = '#000000';\nShadows.date = new Date();\nShadows.direction = { x:0, y:0 };\nShadows.opacity = 1;\n\n\n\nclass Picking {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static setOpacity (opacity) {}\n\n  static clear () {}\n\n  static reset () {\n    this._idMapping = [null];\n  }\n\n  static render () {\n    if (this._timer) {\n      return;\n    }\n    let self = this;\n    this._timer = setTimeout(t => {\n      self._timer = null;\n      self._render();\n    }, 500);\n  }\n\n  static _render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      item,\n      h, mh,\n      sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y },\n      footprint,\n      color,\n      dataItems = Data.items;\n\n    dataItems.sort((a, b) => {\n      return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height);\n    });\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (!(color = item.hitColor)) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      h = item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.minHeight;\n      }\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color);   break;\n        case 'cone':     Cylinder.hitArea(context, item.center, item.radius, 0, h, mh, color);             break;\n        case 'dome':     Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h, mh, color); break;\n        case 'sphere':   Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color);   break;\n        case 'pyramid':  Pyramid.hitArea(context, footprint, item.center, h, mh, color);                   break;\n        default:         Extrusion.hitArea(context, footprint, item.holes, h, mh, color);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.hitArea(context, item.center, item.radius, 0, h+item.roofHeight, h, color);             break;\n        case 'dome':    Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, color); break;\n        case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h+item.roofHeight, h, color);                   break;\n      }\n    }\n\n    // otherwise fails on size 0\n    if (WIDTH && HEIGHT) {\n      this._imageData = this.context.getImageData(0, 0, WIDTH, HEIGHT).data;\n    }\n  }\n\n  static getIdFromXY (x, y) {\n    let imageData = this._imageData;\n    if (!imageData) {\n      return;\n    }\n    let pos = 4*((y|0) * WIDTH + (x|0));\n    let index = imageData[pos] | (imageData[pos+1]<<8) | (imageData[pos+2]<<16);\n    return this._idMapping[index];\n  }\n\n  static idToColor (id) {\n    let index = this._idMapping.indexOf(id);\n    if (index === -1) {\n      this._idMapping.push(id);\n      index = this._idMapping.length-1;\n    }\n    let r =  index       & 0xff;\n    let g = (index >>8)  & 0xff;\n    let b = (index >>16) & 0xff;\n    return 'rgb('+ [r, g, b].join(',') +')';\n  }\n}\n\nPicking._idMapping = [null];\n\n\nclass Debug {\n\n  static point (x, y, color, size) {\n    const context = this.context;\n    context.fillStyle = color || '#ffcc00';\n    context.beginPath();\n    context.arc(x, y, size || 3, 0, 2*PI);\n    context.closePath();\n    context.fill();\n  }\n\n  static line (ax, ay, bx, by, color) {\n    const context = this.context;\n    context.strokeStyle = color || '#ffcc00';\n    context.beginPath();\n    context.moveTo(ax, ay);\n    context.lineTo(bx, by);\n    context.closePath();\n    context.stroke();\n  }\n}\n\n\nfunction setOrigin (origin) {\n  ORIGIN_X = origin.x;\n  ORIGIN_Y = origin.y;\n}\n\nfunction moveCam (offset) {\n  CAM_X = CENTER_X + offset.x;\n  CAM_Y = HEIGHT   + offset.y;\n  Layers.render(true);\n}\n\nfunction setSize (size) {\n  WIDTH  = size.width;\n  HEIGHT = size.height;\n  CENTER_X = WIDTH /2 <<0;\n  CENTER_Y = HEIGHT/2 <<0;\n\n  CAM_X = CENTER_X;\n  CAM_Y = HEIGHT;\n\n  Layers.setSize(WIDTH, HEIGHT);\n  MAX_HEIGHT = CAM_Z-50;\n}\n\nfunction setZoom (z) {\n  ZOOM = z;\n  MAP_SIZE = MAP_TILE_SIZE <<ZOOM;\n\n  const center = pixelToGeo(ORIGIN_X+CENTER_X, ORIGIN_Y+CENTER_Y);\n  const a = geoToPixel(center.latitude, 0);\n  const b = geoToPixel(center.latitude, 1);\n  PIXEL_PER_DEG = b.x-a.x;\n\n  Layers.setOpacity(Math.pow(0.95, ZOOM-MIN_ZOOM));\n\n  WALL_COLOR_STR = ''+ WALL_COLOR;\n  ALT_COLOR_STR  = ''+ ALT_COLOR;\n  ROOF_COLOR_STR = ''+ ROOF_COLOR;\n}\n\nfunction onResize (e) {\n  setSize(e);\n  Layers.render();\n  Data.update();\n}\n\nfunction onMoveEnd (e) {\n  Layers.render();\n  Data.update(); // => fadeIn() => Layers.render()\n}\n\nfunction onZoomStart () {\n  IS_ZOOMING = true;\n}\n\nfunction onZoomEnd (e) {\n  IS_ZOOMING = false;\n  const factor = Math.pow(2, e.zoom-ZOOM);\n\n  setZoom(e.zoom);\n  // Layers.render(); // TODO: requestAnimationFrame() causes flickering because layers are already cleared\n\n  // show on high zoom levels only\n  if (ZOOM <= MIN_ZOOM) {\n    Layers.clear();\n    return;\n  }\n\n  Data.scale(factor);\n\n  Shadows.render();\n  Simplified.render();\n  Buildings.render();\n\n  Data.update(); // => fadeIn()\n}\n\n// based on a pull request from Jérémy Judéaux (https://github.com/Volune)\n\nclass OSMBuildings extends ol.layer.Layer {\n\n  constructor (map) {\n    super(OSMBuildings.name, {projection: 'EPSG:900913'});\n\n    this.offset = {x: 0, y: 0}; // cumulative cam offset during moveBy()\n\n    Layers.init();\n    if (map) {\n      map.addLayer(this);\n    }\n  }\n\n  addTo (map) {\n    this.setMap(map);\n    return this;\n  }\n\n  setOrigin () {\n    let map = this.map,\n      origin = map.getLonLatFromPixel(new OpenLayers.Pixel(0, 0)),\n      res = map.resolution,\n      ext = this.maxExtent,\n      x = (origin.lon - ext.left) / res << 0,\n      y = (ext.top - origin.lat) / res << 0;\n    setOrigin({x: x, y: y});\n  }\n\n  setMap (map) {\n    if (!this.map) {\n      super.setMap.call(this, map);\n    }\n    Layers.appendTo(this.div);\n    setSize({width: map.size.w, height: map.size.h});\n    setZoom(map.zoom);\n    this.setOrigin();\n\n    let layerProjection = this.projection;\n    map.events.register('click', map, e => {\n      let id = Picking.getIdFromXY(e.xy.x, e.xy.y);\n      if (id) {\n        let geo = map.getLonLatFromPixel(e.xy).transform(layerProjection, this.projection);\n        onClick({feature: id, lat: geo.lat, lon: geo.lon});\n      }\n    });\n\n    Data.update();\n  }\n\n  removeMap (map) {\n    Layers.remove();\n    super.removeMap.call(this, map);\n    this.map = null;\n  }\n\n  onMapResize () {\n    let map = this.map;\n    super.onMapResize.call(this);\n    onResize({width: map.size.w, height: map.size.h});\n  }\n\n  moveTo (bounds, zoomChanged, isDragging) {\n    let\n      map = this.map,\n      res = super.moveTo.call(this, bounds, zoomChanged, isDragging);\n\n    if (!isDragging) {\n      let\n        offsetLeft = parseInt(map.layerContainerDiv.style.left, 10),\n        offsetTop = parseInt(map.layerContainerDiv.style.top, 10);\n\n      this.div.style.left = -offsetLeft + 'px';\n      this.div.style.top = -offsetTop + 'px';\n    }\n\n    this.setOrigin();\n    this.offset.x = 0;\n    this.offset.y = 0;\n    moveCam(this.offset);\n\n    if (zoomChanged) {\n      onZoomEnd({zoom: map.zoom});\n    } else {\n      onMoveEnd();\n    }\n\n    return res;\n  }\n\n  moveByPx (dx, dy) {\n    this.offset.x += dx;\n    this.offset.y += dy;\n    let res = super.moveByPx.call(this, dx, dy);\n    moveCam(this.offset);\n    return res;\n  }\n}\n\nOSMBuildings.name = 'OSM Buildings';\nOSMBuildings.attribution = ATTRIBUTION;\nOSMBuildings.isBaseLayer = false;\nOSMBuildings.alwaysInRange = true;\n\n return OSMBuildings;\n}());"
  },
  {
    "path": "dist/OSMBuildings-OpenLayers.js",
    "content": "const OSMBuildings=function(){const e=Math,t=e.exp,i=e.log,r=e.sin,a=e.cos,s=e.tan,o=e.atan,n=e.atan2,l=e.min,c=e.max,h=e.sqrt,f=e.ceil,d=e.pow;class u{constructor(e,t,i,r=1){this.r=this._clamp(e,1),this.g=this._clamp(t,1),this.b=this._clamp(i,1),this.a=this._clamp(r,1)}static parse(e){if(\"string\"==typeof e){let t;if(e=e.toLowerCase(),t=(e=u.w3cColors[e]||e).match(/^#?(\\w{2})(\\w{2})(\\w{2})$/))return new u(parseInt(t[1],16)/255,parseInt(t[2],16)/255,parseInt(t[3],16)/255);if(t=e.match(/^#?(\\w)(\\w)(\\w)$/))return new u(parseInt(t[1]+t[1],16)/255,parseInt(t[2]+t[2],16)/255,parseInt(t[3]+t[3],16)/255);if(t=e.match(/rgba?\\((\\d+)\\D+(\\d+)\\D+(\\d+)(\\D+([\\d.]+))?\\)/))return new u(parseFloat(t[1])/255,parseFloat(t[2])/255,parseFloat(t[3])/255,t[4]?parseFloat(t[5]):1)}return new u}static fromHSL(e,t,i,r){const a=(new u).fromHSL(e,t,i);return a.a=void 0===r?1:r,a}_hue2rgb(e,t,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?e+6*(t-e)*i:i<.5?t:i<2/3?e+(t-e)*(2/3-i)*6:e}_clamp(e,t){if(void 0!==e)return Math.min(t,Math.max(0,e||0))}isValid(){return void 0!==this.r&&void 0!==this.g&&void 0!==this.b}toHSL(){if(!this.isValid())return;const e=Math.max(this.r,this.g,this.b),t=Math.min(this.r,this.g,this.b),i=e-t,r=(e+t)/2;if(!i)return{h:0,s:0,l:r};const a=r>.5?i/(2-e-t):i/(e+t);let s;switch(e){case this.r:s=(this.g-this.b)/i+(this.g<this.b?6:0);break;case this.g:s=(this.b-this.r)/i+2;break;case this.b:s=(this.r-this.g)/i+4}return s*=60,{h:s,s:a,l:r}}fromHSL(e,t,i){if(0===t)return this.r=this.g=this.b=i,this;const r=i<.5?i*(1+t):i+t-i*t,a=2*i-r;return e/=360,this.r=this._hue2rgb(a,r,e+1/3),this.g=this._hue2rgb(a,r,e),this.b=this._hue2rgb(a,r,e-1/3),this}toString(){if(this.isValid())return 1===this.a?\"#\"+((1<<24)+(Math.round(255*this.r)<<16)+(Math.round(255*this.g)<<8)+Math.round(255*this.b)).toString(16).slice(1,7):`rgba(${Math.round(255*this.r)},${Math.round(255*this.g)},${Math.round(255*this.b)},${this.a.toFixed(2)})`}toArray(){if(this.isValid)return[this.r,this.g,this.b]}hue(e){const t=this.toHSL();return this.fromHSL(t.h+e,t.s,t.l)}saturation(e){const t=this.toHSL();return this.fromHSL(t.h,t.s*e,t.l)}lightness(e){const t=this.toHSL();return this.fromHSL(t.h,t.s,t.l*e)}clone(){return new u(this.r,this.g,this.b,this.a)}}function p(){const e=Math,t=e.PI,i=e.sin,r=e.cos,a=e.tan,s=e.asin,o=e.atan2,n=t/180,l=23.4397*n;function c(e,t,s){return o(i(e),r(e)*i(t)-a(s)*r(t))}function h(e,t,a){return s(i(t)*i(a)+r(t)*r(a)*r(e))}return function(e,f,d){const u=n*-d,p=n*f,g=function(e){return function(e){return e.valueOf()/864e5-.5+2440588}(e)-2451545}(e),y=function(e){return n*(357.5291+.98560028*e)}(g),m=function(e,i){return e+i+102.9372*n+t}(y,function(e){return n*(1.9148*i(e)+.02*i(2*e)+3e-4*i(3*e))}(y)),x=(k=m,s(i(_=0)*r(l)+r(_)*i(l)*i(k))),b=function(e,t){return o(i(e)*r(l)-a(t)*i(l),r(e))}(m,0),w=function(e,t){return n*(280.16+360.9856235*e)-t}(g,u)-b;var k,_;return{altitude:h(w,p,x),azimuth:c(w,p,x)-t/2}}}u.w3cColors={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgrey:\"#a9a9a9\",darkgreen:\"#006400\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",ghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",grey:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgrey:\"#d3d3d3\",lightgreen:\"#90ee90\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",lightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",moccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",rebeccapurple:\"#663399\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",seashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"},\"undefined\"!=typeof module&&(module.exports=u);const g={brick:\"#cc7755\",bronze:\"#ffeecc\",canvas:\"#fff8f0\",concrete:\"#999999\",copper:\"#a0e0d0\",glass:\"#e8f8f8\",gold:\"#ffcc00\",plants:\"#009933\",metal:\"#aaaaaa\",panel:\"#fff8f0\",plaster:\"#999999\",roof_tiles:\"#f08060\",silver:\"#cccccc\",slate:\"#666666\",stone:\"#996666\",tar_paper:\"#333333\",wood:\"#deb887\"},y={asphalt:\"tar_paper\",bitumen:\"tar_paper\",block:\"stone\",bricks:\"brick\",glas:\"glass\",glassfront:\"glass\",grass:\"plants\",masonry:\"stone\",granite:\"stone\",panels:\"panel\",paving_stones:\"stone\",plastered:\"plaster\",rooftiles:\"roof_tiles\",roofingfelt:\"tar_paper\",sandstone:\"stone\",sheet:\"canvas\",sheets:\"canvas\",shingle:\"tar_paper\",shingles:\"tar_paper\",slates:\"slate\",steel:\"metal\",tar:\"tar_paper\",tent:\"canvas\",thatch:\"plants\",tile:\"roof_tiles\",tiles:\"roof_tiles\"};function m(e){return\"#\"===(e=e.toLowerCase())[0]?e:g[y[e]||e]||null}function x(e,t){if(function(e){let t,i,r,a,s=0;for(let o=0,n=e.length-3;o<n;o+=2)t=e[o],i=e[o+1],r=e[o+2],a=e[o+3],s+=t*a-r*i;return s/2>0?\"CW\":\"CCW\"}(e)===t)return e;let i=[];for(let t=e.length-2;t>=0;t-=2)i.push(e[t],e[t+1]);return i}function b(e){const t={};e=e||{},t.height=e.height||(e.levels?3*e.levels:G),t.minHeight=e.minHeight||(e.minLevel?3*e.minLevel:0);const i=e.material?m(e.material):e.wallColor||e.color;i&&(t.wallColor=i);const r=e.roofMaterial?m(e.roofMaterial):e.roofColor;switch(r&&(t.roofColor=r),e.shape){case\"cylinder\":case\"cone\":case\"dome\":case\"sphere\":t.shape=e.shape,t.isRotational=!0;break;case\"pyramid\":t.shape=e.shape}switch(e.roofShape){case\"cone\":case\"dome\":t.roofShape=e.roofShape,t.isRotational=!0;break;case\"pyramid\":t.roofShape=e.roofShape}return t.roofShape&&e.roofHeight?(t.roofHeight=e.roofHeight,t.height=c(0,t.height-t.roofHeight)):t.roofHeight=0,t}function w(e){let t,i,r=[];switch(e.type){case\"GeometryCollection\":r=[];for(let t=0,a=e.geometries.length;t<a;t++)(i=w(e.geometries[t]))&&r.push.apply(r,i);return r;case\"MultiPolygon\":r=[];for(let t=0,a=e.coordinates.length;t<a;t++)(i=w({type:\"Polygon\",coordinates:e.coordinates[t]}))&&r.push.apply(r,i);return r;case\"Polygon\":t=e.coordinates;break;default:return[]}let a,s=[],o=[];a=t[0];for(let e=0,t=a.length;e<t;e++)s.push(a[e][1],a[e][0]);s=x(s,\"CW\");for(let e=0,i=t.length-1;e<i;e++){a=t[e+1],o[e]=[];for(let t=0,i=a.length;t<i;t++)o[e].push(a[t][1],a[t][0]);o[e]=x(o[e],\"CCW\")}return[{outer:s,inner:o.length?o:null}]}function k(e){let t={};for(const i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}let _,v,H,S,C,M,T=Math.PI,P=T/2,I=T/4,j=0,O=0,A=0,L=0,z=0,F=0,D=u.parse(\"rgba(200, 190, 180)\"),R=D.lightness(.8),q=D.lightness(1.2),N=\"\"+D,E=\"\"+R,X=\"\"+q,B=0,G=5;function V(e,t){const i=e.x-t.x,r=e.y-t.y;return i*i+r*r}function $(e,t,i,r,a,s){let o,n=a-i,l=s-r;return 0===n&&0===l||(o=((e-i)*n+(t-r)*l)/(n*n+l*l),o>1?(i=a,r=s):o>0&&(i+=n*o,r+=l*o)),n=e-i,l=t-r,n*n+l*l}function W(e){let t=180,i=-180;for(let r=0,a=e.length;r<a;r+=2)t=l(t,e[r+1]),i=c(i,e[r+1]);return(i-t)/2}function J(e,i){const r={};return e/=v,i/=v,r.latitude=i<=0?90:i>=1?-90:function(e){return e/T*180}(2*o(t(T*(1-2*i)))-P),r.longitude=360*(1===e?1:(e%1+1)%1)-180,r}function Z(e,t){const r=l(1,c(0,.5-i(s(I+P*e/180))/T/2));return{x:(t/360+.5)*v<<0,y:r*v<<0}}function Y(e){const t=j+z,i=O+F;for(let r=0,a=e.length-3;r<a;r+=2)if(e[r]>z&&e[r]<t&&e[r+1]>F&&e[r+1]<i)return!0;return!1}let U,K={},Q=[],ee=0;class te{static getPixelFootprint(e){let t,i=new Int32Array(e.length);for(let r=0,a=e.length-1;r<a;r+=2)t=Z(e[r],e[r+1]),i[r]=t.x,i[r+1]=t.y;if(i=function(e){let t,i,r,a=e.length/2,s=new Uint8Array(a),o=0,n=a-1,l=[],c=[],h=[];for(s[o]=s[n]=1;n;){t=0;for(let a=o+1;a<n;a++)i=$(e[2*a],e[2*a+1],e[2*o],e[2*o+1],e[2*n],e[2*n+1]),i>t&&(r=a,t=i);t>2&&(s[r]=1,l.push(o),c.push(r),l.push(r),c.push(n)),o=l.pop(),n=c.pop()}for(let t=0;t<a;t++)s[t]&&h.push(e[2*t],e[2*t+1]);return h}(i),!(i.length<8))return i}static resetItems(){this.items=[],this.cache={},ce.reset()}static addRenderItems(e,t){let i,r,a,s=class{static read(e){if(!e||\"FeatureCollection\"!==e.type)return[];const t=e.features,i=[];for(let e=0,r=t.length;e<r;e++){const r=t[e];if(\"Feature\"!==r.type)continue;const a=b(r.properties),s=w(r.geometry);for(let e=0,t=s.length;e<t;e++){const t=k(a);t.footprint=s[e].outer,t.isRotational&&(t.radius=W(t.footprint)),s[e].inner&&(t.holes=s[e].inner),(r.id||r.properties.id)&&(t.id=r.id||r.properties.id),r.properties.relationId&&(t.relationId=r.properties.relationId),i.push(t)}}return i}}.read(e);for(let e=0,o=s.length;e<o;e++)i=s[e],a=i.id||[i.footprint[0],i.footprint[1],i.height,i.minHeight].join(\",\"),this.cache[a]||(r=this.scaleItem(i))&&(r.scale=t?0:1,this.items.push(r),this.cache[a]=1);!function(){if(U)return;U=setInterval(e=>{let t=te.items,i=!1;for(let e=0,r=t.length;e<r;e++)t[e].scale<1&&(t[e].scale+=.1,t[e].scale>1&&(t[e].scale=1),i=!0);se.render(),i||(clearInterval(U),U=null)},33)}()}static scalePolygon(e,t){return e.map(e=>e*t)}static scale(e){te.items=te.items.map(t=>{if(t.height*=e,t.minHeight*=e,t.footprint=te.scalePolygon(t.footprint,e),t.center.x*=e,t.center.y*=e,t.radius&&(t.radius*=e),t.holes)for(let i=0,r=t.holes.length;i<r;i++)t.holes[i]=te.scalePolygon(t.holes[i],e);return t.roofHeight*=e,t})}static scaleItem(e){let t,i={},r=6/d(2,_-15);if(e.id&&(i.id=e.id),i.height=l(e.height/r,H),i.minHeight=isNaN(e.minHeight)?0:e.minHeight/r,!(i.minHeight>H)&&(i.footprint=this.getPixelFootprint(e.footprint),i.footprint)){if(i.center=function(e){let t=1/0,i=-1/0,r=1/0,a=-1/0;for(let s=0,o=e.length-3;s<o;s+=2)t=l(t,e[s]),i=c(i,e[s]),r=l(r,e[s+1]),a=c(a,e[s+1]);return{x:t+(i-t)/2<<0,y:r+(a-r)/2<<0}}(i.footprint),e.radius&&(i.radius=e.radius*B),e.shape&&(i.shape=e.shape),e.roofShape&&(i.roofShape=e.roofShape),\"cone\"!==i.roofShape&&\"dome\"!==i.roofShape||i.shape||!function(e){const t=e.length;if(t<16)return!1;let i=1/0,r=-1/0,a=1/0,s=-1/0;for(let o=0;o<t-1;o+=2)i=Math.min(i,e[o]),r=Math.max(r,e[o]),a=Math.min(a,e[o+1]),s=Math.max(s,e[o+1]);const o=r-i,n=s-a,l=o/n;if(l<.85||l>1.15)return!1;const c={x:i+o/2,y:a+n/2},h=(o+n)/4,f=h*h;for(let i=0;i<t-1;i+=2){const t=V({x:e[i],y:e[i+1]},c);if(t/f<.8||t/f>1.2)return!1}return!0}(i.footprint)||(i.shape=\"cylinder\"),e.holes){let t;i.holes=[];for(let r=0,a=e.holes.length;r<a;r++)(t=this.getPixelFootprint(e.holes[r]))&&i.holes.push(t)}return e.wallColor&&(t=u.parse(e.wallColor))&&(i.altColor=\"\"+t.lightness(.8),i.wallColor=\"\"+t),e.roofColor&&(t=u.parse(e.roofColor))&&(i.roofColor=\"\"+t),e.relationId&&(i.relationId=e.relationId),i.hitColor=ce.idToColor(e.relationId||e.id),i.roofHeight=isNaN(e.roofHeight)?0:e.roofHeight/r,i.height+i.roofHeight<=i.minHeight?void 0:i}}static set(e){this.resetItems(),this._staticData=e,this.addRenderItems(this._staticData,!0)}static load(e,t){this.src=e||\"https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json\".replace(\"{k}\",t||\"anonymous\"),this.update()}static update(){if(this.resetItems(),!(_<15)&&(this._staticData&&this.addRenderItems(this._staticData),this.src)){let t,i,r=16,a=256,s=_>r?a<<_-r:a>>r-_,o=z/s<<0,n=F/s<<0,l=f((z+j)/s),c=f((F+O)/s),h=this;function e(e){h.addRenderItems(e)}for(i=n;i<=c;i++)for(t=o;t<=l;t++)this.loadTile(t,i,r,e)}}static loadTile(e,t,i,r){let a=\"abcd\"[(e+t)%4],s=this.src.replace(\"{s}\",a).replace(\"{x}\",e).replace(\"{y}\",t).replace(\"{z}\",i);return class{static loadJSON(e,t){return function(e,t){if(K[e])return void(t&&t(K[e]));const i=new XMLHttpRequest;return i.onreadystatechange=function(){if(4===i.readyState&&!(!i.status||i.status<200||i.status>299)&&t&&i.responseText){const r=i.responseText;for(K[e]=r,Q.push({url:e,size:r.length}),ee+=r.length,t(r);ee>5242880;){let e=Q.shift();ee-=e.size,delete K[e.url]}}},i.open(\"GET\",e),i.send(null),i}(e,e=>{let i;try{i=JSON.parse(e)}catch(e){}t(i)})}}.loadJSON(s,r)}}te.cache={},te.items=[];class ie{static draw(e,t,i,r,a,s,o,n){let l=this._extrude(e,t,r,a,s,o),c=[];if(i)for(let t=0,n=i.length;t<n;t++)c[t]=this._extrude(e,i[t],r,a,s,o);if(e.fillStyle=n,e.beginPath(),this._ring(e,l),i)for(let t=0,i=c.length;t<i;t++)this._ring(e,c[t]);e.closePath(),e.fill()}static _extrude(e,t,i,r,a,s){let o,n,l=450/(450-i),c=450/(450-r),h={x:0,y:0},f={x:0,y:0},d=[];for(let i=0,u=t.length-3;i<u;i+=2)h.x=t[i]-z,h.y=t[i+1]-F,f.x=t[i+2]-z,f.y=t[i+3]-F,o=oe.project(h,l),n=oe.project(f,l),r&&(h=oe.project(h,c),f=oe.project(f,c)),(f.x-h.x)*(o.y-h.y)>(o.x-h.x)*(f.y-h.y)&&(h.x<f.x&&h.y<f.y||h.x>f.x&&h.y>f.y?e.fillStyle=s:e.fillStyle=a,e.beginPath(),this._ring(e,[f.x,f.y,h.x,h.y,o.x,o.y,n.x,n.y]),e.closePath(),e.fill()),d[i]=o.x,d[i+1]=o.y;return d}static _ring(e,t){e.moveTo(t[0],t[1]);for(let i=2,r=t.length-1;i<r;i+=2)e.lineTo(t[i],t[i+1])}static simplified(e,t,i){if(e.beginPath(),this._ringAbs(e,t),i)for(let t=0,r=i.length;t<r;t++)this._ringAbs(e,i[t]);e.closePath(),e.fill()}static _ringAbs(e,t){e.moveTo(t[0]-z,t[1]-F);for(let i=2,r=t.length-1;i<r;i+=2)e.lineTo(t[i]-z,t[i+1]-F)}static shadow(e,t,i,r,a){let s,o,n=null,l={x:0,y:0},c={x:0,y:0};for(let i=0,h=t.length-3;i<h;i+=2)l.x=t[i]-z,l.y=t[i+1]-F,c.x=t[i+2]-z,c.y=t[i+3]-F,s=le.project(l,r),o=le.project(c,r),a&&(l=le.project(l,a),c=le.project(c,a)),(c.x-l.x)*(s.y-l.y)>(s.x-l.x)*(c.y-l.y)?(1===n&&e.lineTo(l.x,l.y),n=0,i||e.moveTo(l.x,l.y),e.lineTo(c.x,c.y)):(0===n&&e.lineTo(s.x,s.y),n=1,i||e.moveTo(s.x,s.y),e.lineTo(o.x,o.y));if(i)for(let t=0,r=i.length;t<r;t++)this._ringAbs(e,i[t])}static hitArea(e,t,i,r,a,s){let o,n,l=null,c={x:0,y:0},h={x:0,y:0},f=450/(450-r),d=450/(450-a);e.fillStyle=s,e.beginPath();for(let i=0,r=t.length-3;i<r;i+=2)c.x=t[i]-z,c.y=t[i+1]-F,h.x=t[i+2]-z,h.y=t[i+3]-F,o=oe.project(c,f),n=oe.project(h,f),a&&(c=oe.project(c,d),h=oe.project(h,d)),(h.x-c.x)*(o.y-c.y)>(o.x-c.x)*(h.y-c.y)?(1===l&&e.lineTo(c.x,c.y),l=0,i||e.moveTo(c.x,c.y),e.lineTo(h.x,h.y)):(0===l&&e.lineTo(o.x,o.y),l=1,i||e.moveTo(o.x,o.y),e.lineTo(n.x,n.y));e.closePath(),e.fill()}}class re{static draw(e,t,i,r,a,s,o,l,c){let h,f,d={x:t.x-z,y:t.y-F},u=450/(450-a),p=450/(450-s),g=oe.project(d,u);r*=u,s&&(d=oe.project(d,p),i*=p);let y=this._tangents(d,i,g,r);y?(h=n(y[0].y1-d.y,y[0].x1-d.x),f=n(y[1].y1-d.y,y[1].x1-d.x)):(h=1.5*T,f=1.5*T),e.fillStyle=o,e.beginPath(),e.arc(g.x,g.y,r,P,h,!0),e.arc(d.x,d.y,i,h,P),e.closePath(),e.fill(),e.fillStyle=l,e.beginPath(),e.arc(g.x,g.y,r,f,P,!0),e.arc(d.x,d.y,i,P,f),e.closePath(),e.fill(),e.fillStyle=c,this._circle(e,g,r)}static simplified(e,t,i){this._circle(e,{x:t.x-z,y:t.y-F},i)}static shadow(e,t,i,r,a,s){let o,l,c={x:t.x-z,y:t.y-F},h=le.project(c,a);s&&(c=le.project(c,s));let f=this._tangents(c,i,h,r);f?(o=n(f[0].y1-c.y,f[0].x1-c.x),l=n(f[1].y1-c.y,f[1].x1-c.x),e.moveTo(f[1].x2,f[1].y2),e.arc(h.x,h.y,r,l,o),e.arc(c.x,c.y,i,o,l)):(e.moveTo(c.x+i,c.y),e.arc(c.x,c.y,i,0,2*T))}static hitArea(e,t,i,r,a,s,o){let l,c,h={x:t.x-z,y:t.y-F},f=450/(450-a),d=450/(450-s),u=oe.project(h,f);r*=f,s&&(h=oe.project(h,d),i*=d);let p=this._tangents(h,i,u,r);e.fillStyle=o,e.beginPath(),p?(l=n(p[0].y1-h.y,p[0].x1-h.x),c=n(p[1].y1-h.y,p[1].x1-h.x),e.moveTo(p[1].x2,p[1].y2),e.arc(u.x,u.y,r,c,l),e.arc(h.x,h.y,i,l,c)):(e.moveTo(h.x+i,h.y),e.arc(h.x,h.y,i,0,2*T)),e.closePath(),e.fill()}static _circle(e,t,i){e.beginPath(),e.arc(t.x,t.y,i,0,2*T),e.fill()}static _tangents(e,t,i,r){let a=e.x-i.x,s=e.y-i.y,o=t-r,n=a*a+s*s;if(n<=o*o)return;let l,f,d,u=h(n),p=-a/u,g=-s/u,y=o/u,m=[];l=h(c(0,1-y*y));for(let a=1;a>=-1;a-=2)f=p*y-a*l*g,d=g*y+a*l*p,m.push({x1:e.x+t*f<<0,y1:e.y+t*d<<0,x2:i.x+r*f<<0,y2:i.y+r*d<<0});return m}}class ae{static draw(e,t,i,r,a,s,o){let n={x:i.x-z,y:i.y-F},l=450/(450-r),c=450/(450-a),h=oe.project(n,l),f={x:0,y:0},d={x:0,y:0};for(let i=0,r=t.length-3;i<r;i+=2)f.x=t[i]-z,f.y=t[i+1]-F,d.x=t[i+2]-z,d.y=t[i+3]-F,a&&(f=oe.project(f,c),d=oe.project(d,c)),(d.x-f.x)*(h.y-f.y)>(h.x-f.x)*(d.y-f.y)&&(f.x<d.x&&f.y<d.y||f.x>d.x&&f.y>d.y?e.fillStyle=o:e.fillStyle=s,e.beginPath(),this._triangle(e,f,d,h),e.closePath(),e.fill())}static _triangle(e,t,i,r){e.moveTo(t.x,t.y),e.lineTo(i.x,i.y),e.lineTo(r.x,r.y)}static _ring(e,t){e.moveTo(t[0]-z,t[1]-F);for(let i=2,r=t.length-1;i<r;i+=2)e.lineTo(t[i]-z,t[i+1]-F)}static shadow(e,t,i,r,a){let s={x:0,y:0},o={x:0,y:0},n={x:i.x-z,y:i.y-F},l=le.project(n,r);for(let i=0,r=t.length-3;i<r;i+=2)s.x=t[i]-z,s.y=t[i+1]-F,o.x=t[i+2]-z,o.y=t[i+3]-F,a&&(s=le.project(s,a),o=le.project(o,a)),(o.x-s.x)*(l.y-s.y)>(l.x-s.x)*(o.y-s.y)&&this._triangle(e,s,o,l)}static hitArea(e,t,i,r,a,s){let o={x:i.x-z,y:i.y-F},n=450/(450-r),l=450/(450-a),c=oe.project(o,n),h={x:0,y:0},f={x:0,y:0};e.fillStyle=s,e.beginPath();for(let i=0,r=t.length-3;i<r;i+=2)h.x=t[i]-z,h.y=t[i+1]-F,f.x=t[i+2]-z,f.y=t[i+3]-F,a&&(h=oe.project(h,l),f=oe.project(f,l)),(f.x-h.x)*(c.y-h.y)>(c.x-h.x)*(f.y-h.y)&&this._triangle(e,h,f,c);e.closePath(),e.fill()}}class se{static init(){se.container.className=\"osmb-container\",le.init(se.createContext(se.container)),ne.init(se.createContext(se.container)),oe.init(se.createContext(se.container)),ce.init(se.createContext())}static clear(){le.clear(),ne.clear(),oe.clear(),ce.clear()}static setOpacity(e){le.setOpacity(e),ne.setOpacity(e),oe.setOpacity(e),ce.setOpacity(e)}static render(e){_<15?se.clear():M||requestAnimationFrame(t=>{e||(le.render(),ne.render()),oe.render()})}static createContext(e){let t=document.createElement(\"CANVAS\");t.className=\"osmb-layer\";let i=t.getContext(\"2d\");return i.lineCap=\"round\",i.lineJoin=\"round\",i.lineWidth=1,i.imageSmoothingEnabled=!1,se.items.push(t),e&&e.appendChild(t),i}static appendTo(e){e.appendChild(se.container)}static remove(){se.container.parentNode.removeChild(se.container)}static setSize(e,t){se.items.forEach(i=>{i.width=e,i.height=t})}static setPosition(e,t){se.container.style.left=e+\"px\",se.container.style.top=t+\"px\"}}se.container=document.createElement(\"DIV\"),se.items=[];class oe{static init(e){this.context=e}static clear(){this.context.clearRect(0,0,j,O)}static setOpacity(e){this.context.canvas.style.opacity=e}static project(e,t){return{x:(e.x-S)*t+S<<0,y:(e.y-C)*t+C<<0}}static render(){this.clear();let e,t,i,r,a,s,o,n=this.context,l={x:S+z,y:C+F},c=te.items;c.sort((e,t)=>e.minHeight-t.minHeight||V(t.center,l)-V(e.center,l)||t.height-e.height);for(let l=0,h=c.length;l<h;l++)if(e=c[l],!ne.isSimple(e)&&(r=e.footprint,Y(r))){switch(t=e.scale<1?e.height*e.scale:e.height,i=0,e.minHeight&&(i=e.scale<1?e.minHeight*e.scale:e.minHeight),a=e.wallColor||N,s=e.altColor||E,o=e.roofColor||X,n.strokeStyle=s,e.shape){case\"cylinder\":re.draw(n,e.center,e.radius,e.radius,t,i,a,s,o);break;case\"cone\":re.draw(n,e.center,e.radius,0,t,i,a,s);break;case\"dome\":re.draw(n,e.center,e.radius,e.radius/2,t,i,a,s);break;case\"sphere\":re.draw(n,e.center,e.radius,e.radius,t,i,a,s,o);break;case\"pyramid\":ae.draw(n,r,e.center,t,i,a,s);break;default:ie.draw(n,r,e.holes,t,i,a,s,o)}switch(e.roofShape){case\"cone\":re.draw(n,e.center,e.radius,0,t+e.roofHeight,t,o,\"\"+u.parse(o).lightness(.9));break;case\"dome\":re.draw(n,e.center,e.radius,e.radius/2,t+e.roofHeight,t,o,\"\"+u.parse(o).lightness(.9));break;case\"pyramid\":ae.draw(n,r,e.center,t+e.roofHeight,t,o,u.parse(o).lightness(.9))}}}}class ne{static init(e){this.context=e}static clear(){this.context.clearRect(0,0,j,O)}static setOpacity(e){this.context.canvas.style.opacity=e}static isSimple(e){return _<=ne.MAX_ZOOM&&e.height+e.roofHeight<ne.MAX_HEIGHT}static render(){this.clear();let e=this.context;if(_>ne.MAX_ZOOM)return;let t,i,r=te.items;for(let a=0,s=r.length;a<s;a++)if(t=r[a],!(t.height>=ne.MAX_HEIGHT)&&(i=t.footprint,Y(i)))switch(e.strokeStyle=t.altColor||E,e.fillStyle=t.roofColor||X,t.shape){case\"cylinder\":case\"cone\":case\"dome\":case\"sphere\":re.simplified(e,t.center,t.radius);break;default:ie.simplified(e,i,t.holes)}}}ne.MAX_ZOOM=16,ne.MAX_HEIGHT=5;class le{static init(e){this.context=e}static clear(){this.context.clearRect(0,0,j,O)}static setOpacity(e){this.opacity=e}static project(e,t){return{x:e.x+this.direction.x*t,y:e.y+this.direction.y*t}}static render(){this.clear();let e,t,i,o,n=this.context;if(e=J(A+z,L+F),t=p(this.date,e.latitude,e.longitude),t.altitude<=0)return;i=1/s(t.altitude),o=i<5?.75:1/i*5,this.direction.x=a(t.azimuth)*i,this.direction.y=r(t.azimuth)*i;let l,c,h,f,d,u,g=te.items;for(n.canvas.style.opacity=o/(2*this.opacity),n.shadowColor=this.blurColor,n.fillStyle=this.color,n.beginPath(),l=0,c=g.length;l<c;l++)if(h=g[l],u=h.footprint,Y(u)){switch(f=h.scale<1?h.height*h.scale:h.height,d=0,h.minHeight&&(d=h.scale<1?h.minHeight*h.scale:h.minHeight),h.shape){case\"cylinder\":re.shadow(n,h.center,h.radius,h.radius,f,d);break;case\"cone\":re.shadow(n,h.center,h.radius,0,f,d);break;case\"dome\":re.shadow(n,h.center,h.radius,h.radius/2,f,d);break;case\"sphere\":re.shadow(n,h.center,h.radius,h.radius,f,d);break;case\"pyramid\":ae.shadow(n,u,h.center,f,d);break;default:ie.shadow(n,u,h.holes,f,d)}switch(h.roofShape){case\"cone\":re.shadow(n,h.center,h.radius,0,f+h.roofHeight,f);break;case\"dome\":re.shadow(n,h.center,h.radius,h.radius/2,f+h.roofHeight,f);break;case\"pyramid\":ae.shadow(n,u,h.center,f+h.roofHeight,f)}}n.closePath(),n.fill()}}le.color=\"#666666\",le.blurColor=\"#000000\",le.date=new Date,le.direction={x:0,y:0},le.opacity=1;class ce{static init(e){this.context=e}static setOpacity(e){}static clear(){}static reset(){this._idMapping=[null]}static render(){if(this._timer)return;let e=this;this._timer=setTimeout(t=>{e._timer=null,e._render()},500)}static _render(){this.clear();let e,t,i,r,a,s=this.context,o={x:S+z,y:C+F},n=te.items;n.sort((e,t)=>e.minHeight-t.minHeight||V(t.center,o)-V(e.center,o)||t.height-e.height);for(let o=0,l=n.length;o<l;o++)if(e=n[o],(a=e.hitColor)&&(r=e.footprint,Y(r))){switch(t=e.height,i=0,e.minHeight&&(i=e.minHeight),e.shape){case\"cylinder\":re.hitArea(s,e.center,e.radius,e.radius,t,i,a);break;case\"cone\":re.hitArea(s,e.center,e.radius,0,t,i,a);break;case\"dome\":re.hitArea(s,e.center,e.radius,e.radius/2,t,i,a);break;case\"sphere\":re.hitArea(s,e.center,e.radius,e.radius,t,i,a);break;case\"pyramid\":ae.hitArea(s,r,e.center,t,i,a);break;default:ie.hitArea(s,r,e.holes,t,i,a)}switch(e.roofShape){case\"cone\":re.hitArea(s,e.center,e.radius,0,t+e.roofHeight,t,a);break;case\"dome\":re.hitArea(s,e.center,e.radius,e.radius/2,t+e.roofHeight,t,a);break;case\"pyramid\":ae.hitArea(s,r,e.center,t+e.roofHeight,t,a)}}j&&O&&(this._imageData=this.context.getImageData(0,0,j,O).data)}static getIdFromXY(e,t){let i=this._imageData;if(!i)return;let r=4*((0|t)*j+(0|e)),a=i[r]|i[r+1]<<8|i[r+2]<<16;return this._idMapping[a]}static idToColor(e){let t=this._idMapping.indexOf(e);return-1===t&&(this._idMapping.push(e),t=this._idMapping.length-1),\"rgb(\"+[255&t,t>>8&255,t>>16&255].join(\",\")+\")\"}}ce._idMapping=[null];function he(e){S=A+e.x,C=O+e.y,se.render(!0)}function fe(e){j=e.width,O=e.height,A=j/2<<0,L=O/2<<0,S=A,C=O,se.setSize(j,O),H=400}function de(e){_=e,v=256<<_;const t=J(z+A,F+L),i=Z(t.latitude,0),r=Z(t.latitude,1);B=r.x-i.x,se.setOpacity(Math.pow(.95,_-15)),N=\"\"+D,E=\"\"+R,X=\"\"+q}class ue extends ol.layer.Layer{constructor(e){super(ue.name,{projection:\"EPSG:900913\"}),this.offset={x:0,y:0},se.init(),e&&e.addLayer(this)}addTo(e){return this.setMap(e),this}setOrigin(){let e=this.map,t=e.getLonLatFromPixel(new OpenLayers.Pixel(0,0)),i=e.resolution,r=this.maxExtent;!function(e){z=e.x,F=e.y}({x:(t.lon-r.left)/i<<0,y:(r.top-t.lat)/i<<0})}setMap(e){this.map||super.setMap.call(this,e),se.appendTo(this.div),fe({width:e.size.w,height:e.size.h}),de(e.zoom),this.setOrigin();let t=this.projection;e.events.register(\"click\",e,i=>{let r=ce.getIdFromXY(i.xy.x,i.xy.y);if(r){let r=e.getLonLatFromPixel(i.xy).transform(t,this.projection);r.lat,r.lon}}),te.update()}removeMap(e){se.remove(),super.removeMap.call(this,e),this.map=null}onMapResize(){let e=this.map;super.onMapResize.call(this),fe({width:e.size.w,height:e.size.h}),se.render(),te.update()}moveTo(e,t,i){let r=this.map,a=super.moveTo.call(this,e,t,i);if(!i){let e=parseInt(r.layerContainerDiv.style.left,10),t=parseInt(r.layerContainerDiv.style.top,10);this.div.style.left=-e+\"px\",this.div.style.top=-t+\"px\"}return this.setOrigin(),this.offset.x=0,this.offset.y=0,he(this.offset),t?function(e){M=!1;const t=Math.pow(2,e.zoom-_);de(e.zoom),_<=15?se.clear():(te.scale(t),le.render(),ne.render(),oe.render(),te.update())}({zoom:r.zoom}):(se.render(),te.update()),a}moveByPx(e,t){this.offset.x+=e,this.offset.y+=t;let i=super.moveByPx.call(this,e,t);return he(this.offset),i}}return ue.name=\"OSM Buildings\",ue.attribution='&copy; <a href=\"https://osmbuildings.org\">OSM Buildings</a>',ue.isBaseLayer=!1,ue.alwaysInRange=!0,ue}();"
  },
  {
    "path": "dist/OSMBuildings.css",
    "content": ".osmb-container {\n  transform: translate3d(0, 0, 0);\n  pointerEvents: none;\n  position: absolute;\n  left: 0;\n  top: 0;\n}\n\n.osmb-container.zoom-animation {\n  transition-duration: 250ms;\n  transition-property: transform;\n  transform-origin: 50% 50%;\n}\n\n.osmb-layer {\n  transform: translate3d(0, 0, 0);\n  image-rendering: optimizeSpeed;\n  position: absolute;\n  left: 0;\n  top: 0;\n}\n"
  },
  {
    "path": "dist/index-Leaflet-3db.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>OSM Buildings for Leaflet</title>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.css\"/>\n  <script src=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.js\"></script>\n  <link rel=\"stylesheet\" href=\"OSMBuildings.css\">\n  <script src=\"OSMBuildings-Leaflet.debug.js\"></script>\n  <style>\n    html, body {\n      border: 0;\n      margin: 0;\n      padding: 0;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n    }\n\n    #map {\n      height: 100%;\n    }\n  </style>\n</head>\n\n<body>\n<div id=\"map\"></div>\n\n<script>\n  const map = new L.Map('map').setView([40.711516, -74.011695], 16); // NYC\n\n  // base map source\n  new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);\n\n  const osmb = new OSMBuildings();\n  osmb.addTo(map);\n\n  // osmb.load('https://{s}-data.3dbuildings.com/tile/{z}/{x}/{y}.json?token=xsthpfc');\n  osmb.load();\n\n</script>\n</body>\n</html>"
  },
  {
    "path": "dist/index-Leaflet.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>OSM Buildings Classic for Leaflet</title>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.css\"/>\n  <script src=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.js\"></script>\n  <link rel=\"stylesheet\" href=\"OSMBuildings.css\">\n  <script src=\"OSMBuildings-Leaflet.debug.js\"></script>\n  <style>\n    html, body {\n      border: 0;\n      margin: 0;\n      padding: 0;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n    }\n\n    #map {\n      height: 100%;\n    }\n  </style>\n</head>\n\n<body>\n<div id=\"map\"></div>\n\n<script>\n  // const map = new L.Map('map', { minZoom: 16 }).setView([52.52179, 13.39503], 18); // Berlin\n  const map = new L.Map('map').setView([40.711516, -74.011695], 16); // NYC\n\n  // new L.TileLayer('https://{s}.tiles.mapbox.com/v3/osmbuildings.kbpalbpk/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);\n  new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);\n\n  const osmb = new OSMBuildings();\n\n  L.control.layers({}, { Buildings: osmb }).addTo(map);\n\n  osmb.addTo(map)\n    .date(new Date(2017, 5, 15, 17, 30))\n    .load()\n    .click(function(e) {\n      console.log('feature clicked:', e);\n    });\n\n  // add GeoJSON\n  map.setView([52.52179, 13.39503], 18); // Berlin Bodemuseum\n  const data = {\n    \"type\": \"FeatureCollection\",\n    \"features\": [\n      {\n        \"type\": \"Feature\",\n        \"properties\": { \"height\": 50, \"color\": \"#ffcc00\" },\n        \"geometry\": {\n          \"type\": \"Polygon\",\n          \"coordinates\": [\n            [\n              [\n                13.39631974697113,\n                52.52184840804295\n              ],\n              [\n                13.39496523141861,\n                52.521166220963536\n              ],\n              [\n                13.395150303840637,\n                52.52101770514734\n              ],\n              [\n                13.396652340888977,\n                52.52174559105107\n              ],\n              [\n                13.39631974697113,\n                52.52184840804295\n              ]\n            ]\n          ]\n        }\n      }\n    ]\n  };\n  osmb.set(data);\n</script>\n</body>\n</html>"
  },
  {
    "path": "dist/index-OpenLayers.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>OSM Buildings Classic for OpenLayers</title>\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.2.1/css/ol.css\"/>\n  <script src=\"https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.2.1/build/ol.js\"></script>\n  <link rel=\"stylesheet\" href=\"OSMBuildings.css\">\n  <script src=\"OSMBuildings-OpenLayers.debug.js\"></script>\n  <style>\n    html, body {\n      border: 0;\n      margin: 0;\n      padding: 0;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n    }\n\n    #map {\n      height: 100%;\n    }\n  </style>\n</head>\n\n<body>\n<div id=\"map\"></div>\n\n<script>\n  const map = new ol.Map({\n    target: 'map',\n    layers: [\n      new ol.layer.Tile({\n        source: new ol.source.OSM()\n      })\n    ],\n    view: new ol.View({\n      center: ol.proj.fromLonLat([52.52179, 13.39503]), // Berlin\n      // center: ol.proj.fromLonLat([40.711516, -74.011695]), // NYC\n      zoom: 16\n    })\n  });\n\n  const osmb = new OSMBuildings();\n\n  osmb.addTo(map)\n    .date(new Date(2017, 5, 15, 17, 30))\n    .load()\n    .click(function(e) {\n      console.log('feature clicked:', e);\n    });\n\n  // // GeoJSON\n  // // map.setView([52.52179, 13.39503], 18); // Berlin Bodemuseum\n  // // const data = {\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"id\":1,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.388652,52.520222],[13.388758,52.52021],[13.389204,52.520184],[13.389735,52.520162],[13.390013,52.52015],[13.390153,52.520155],[13.391974,52.520274],[13.392177,52.52029],[13.392477,52.520319],[13.392728,52.520355],[13.393062,52.520414],[13.393416,52.520505],[13.393754,52.520615],[13.394054,52.52073],[13.394226,52.520811],[13.394393,52.520678],[13.394217,52.520597],[13.393905,52.520471],[13.39355,52.52036],[13.393176,52.520268],[13.392811,52.520205],[13.392527,52.520169],[13.392209,52.52014],[13.392006,52.520122],[13.39025,52.520002],[13.390123,52.519993],[13.389317,52.519947],[13.38872,52.5199],[13.388693,52.520002],[13.388652,52.520222]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":2,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.392946,52.521681],[13.393053,52.521847],[13.393228,52.52174],[13.393363,52.521645],[13.393499,52.521549],[13.393439,52.521523],[13.394023,52.521095],[13.39398,52.521074],[13.394229,52.520907],[13.394066,52.520818],[13.3938,52.521001],[13.393842,52.521027],[13.393242,52.521434],[13.393285,52.52146],[13.392946,52.521681]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":3,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393432,52.521863],[13.39371,52.522017],[13.393874,52.521918],[13.393605,52.521756],[13.393432,52.521863]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":4,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394871,52.520148],[13.395216,52.52032],[13.395544,52.520072],[13.395374,52.519995],[13.395111,52.519962],[13.394871,52.520148]],[[13.395008,52.520144],[13.395124,52.520049],[13.395292,52.520078],[13.395128,52.520203],[13.395008,52.520144]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":5,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393699,52.5202],[13.393927,52.520312],[13.394052,52.520218],[13.393866,52.520126],[13.393746,52.520074],[13.393699,52.5202]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":6,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393746,52.520074],[13.393866,52.520126],[13.393871,52.520081],[13.393889,52.520073],[13.393872,52.520056],[13.393903,52.519975],[13.393926,52.519965],[13.394501,52.52004],[13.394479,52.520103],[13.394162,52.520083],[13.394151,52.520148],[13.394319,52.520157],[13.394526,52.520168],[13.394555,52.520146],[13.394623,52.520095],[13.394679,52.520052],[13.394803,52.519958],[13.393834,52.519838],[13.39382,52.519876],[13.393795,52.519942],[13.393746,52.520074]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":7,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393867,52.522186],[13.394056,52.522516],[13.394296,52.522483],[13.394093,52.522153],[13.393867,52.522186]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":9,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393927,52.520312],[13.394078,52.520388],[13.394206,52.520294],[13.394157,52.52027],[13.394052,52.520218],[13.393927,52.520312]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":10,\"height\":15},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393996,52.521925],[13.394018,52.521993],[13.394044,52.522027],[13.394071,52.522054],[13.394154,52.522098],[13.394256,52.522118],[13.394314,52.522121],[13.394502,52.5221],[13.395601,52.52198],[13.395597,52.521963],[13.396059,52.521915],[13.396104,52.521876],[13.396091,52.521831],[13.396035,52.521803],[13.396074,52.521771],[13.394912,52.5212],[13.394077,52.521797],[13.394017,52.521856],[13.393996,52.521925]],[[13.394702,52.521541],[13.394743,52.521509],[13.394794,52.521506],[13.394905,52.521429],[13.394904,52.521409],[13.394881,52.521396],[13.39493,52.521361],[13.395228,52.52151],[13.394873,52.521627],[13.394702,52.521541]],[[13.395524,52.521723],[13.395602,52.521715],[13.395599,52.521701],[13.395618,52.521686],[13.395741,52.521749],[13.395765,52.521837],[13.395565,52.521854],[13.395524,52.521723]],[[13.395005,52.521779],[13.395327,52.521666],[13.395382,52.521847],[13.39504,52.521885],[13.395005,52.521779]],[[13.394604,52.521879],[13.394829,52.5218],[13.394869,52.521931],[13.394676,52.521955],[13.394604,52.521879]],[[13.394447,52.521727],[13.394577,52.521638],[13.394742,52.521724],[13.394526,52.521798],[13.394447,52.521727]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":11,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394078,52.520388],[13.394369,52.520531],[13.394497,52.520438],[13.394299,52.52034],[13.394206,52.520294],[13.394078,52.520388]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":12,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394157,52.52027],[13.394206,52.520294],[13.394299,52.52034],[13.39443,52.52024],[13.394515,52.520176],[13.394526,52.520168],[13.394319,52.520157],[13.394157,52.52027]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":13,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394226,52.520811],[13.394532,52.520957],[13.394655,52.521015],[13.394965,52.521163],[13.395059,52.521209],[13.395088,52.521188],[13.395121,52.521163],[13.395168,52.521128],[13.395212,52.521095],[13.395247,52.52107],[13.395151,52.521027],[13.394841,52.520884],[13.394711,52.520824],[13.394393,52.520678],[13.394226,52.520811]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":14,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394369,52.520531],[13.394712,52.5207],[13.39488,52.520574],[13.394708,52.520488],[13.394665,52.520521],[13.394497,52.520438],[13.394369,52.520531]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":16,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.39443,52.52024],[13.394793,52.520423],[13.394708,52.520488],[13.39488,52.520574],[13.395053,52.520443],[13.394515,52.520176],[13.39443,52.52024]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":17,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394515,52.520176],[13.395053,52.520443],[13.395216,52.52032],[13.394871,52.520148],[13.394803,52.520114],[13.394735,52.52008],[13.394679,52.520052],[13.394623,52.520095],[13.395003,52.520284],[13.39495,52.520323],[13.39476,52.520228],[13.394744,52.52024],[13.394555,52.520146],[13.394526,52.520168],[13.394515,52.520176]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":19,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394585,52.521048],[13.394595,52.521053],[13.394614,52.52104],[13.394604,52.521034],[13.394585,52.521048]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":20,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394679,52.520052],[13.394735,52.52008],[13.394811,52.520019],[13.394902,52.520034],[13.394803,52.520114],[13.394871,52.520148],[13.395111,52.519962],[13.394842,52.519929],[13.394803,52.519958],[13.394679,52.520052]]]]}}]};\n  // // const osmb = new OSMBuildings(map).set(data);\n</script>\n</body>\n</html>"
  },
  {
    "path": "docs/server.md",
    "content": "# OSM Buildings Server #\n\nCode for OSM Buildings data service ist not public.\nHowever, you may use our service for *free*.\n\nBefore hitting us with heavy load, please get in touch at support@osmbuildings.org\n\nThe built in default server URL schema is `https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json`\n\nPlaceholder and adressing schema is described here:\nhttp://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#Tile_Resources\n\n`{s}` in our case stands for a subdomain (a,b,c,d) to extend the number of browser requests per domain.\n\n## Data Source ##\n\nWe are pulling OpenStreetMap data from Overpass API (http://overpass-turbo.eu/).\nThat way we are very flexible in terms of querying and data freshness.\nFor retrieving building data, we convert TMS adressed tiles to geographic bounding boxes (projection EPSG 4326) and opted for a JSON formatted response.\n\n## GeoJSON Properties ##\n\nOSM Buildings Server does a lot of alignments, optimizations and caching and finally returns GeoJSON.\nThe result is fully compatible but has a few conventions, see below.\n\n<table>\n<tr>\n<th>GeoJSON property</th>\n<th>OSM Tags</th>\n</tr>\n\n<tr>\n<td>height</td>\n<td>height, building:height, levels, building:levels</td>\n</tr>\n\n<tr>\n<td>minHeight</td>\n<td>min_height, building:min_height, min_level, building:min_level</td>\n</tr>\n\n<tr>\n<td>color/wallColor</td>\n<td>building:color, building:colour</td>\n</tr>\n\n<tr>\n<td>material</td>\n<td>building:material, building:facade:material, building:cladding</td>\n</tr>\n\n<tr>\n<td>roofColor</td>\n<td>roof:color, roof:colour, building:roof:color, building:roof:colour</td>\n</tr>\n\n<tr>\n<td>roofMaterial</td>\n<td>roof:material, building:roof:material</td>\n</tr>\n\n<tr>\n<td>shape</td>\n<td>building:shape[=cylinder,sphere]</td>\n</tr>\n\n<tr>\n<td>roofShape</td>\n<td>roof:shape[=dome]</td>\n</tr>\n\n<tr>\n<td>roofHeight</td>\n<td>roof:height</td>\n</tr>\n</table>\n\n## Sample Implementation ##\n\nCode by Michael Meier (michael.meier@fau.de)\n\nhttp://git.rrze.uni-erlangen.de/gitweb/?p=osmrrze.git;a=blob;f=scripts/osmbuildings-json-generator.pl\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"osmbuildings-classic\",\n  \"description\": \"OSM Buildings Classic\",\n  \"version\": \"3.0.1\",\n  \"homepage\": \"https://osmbuildings.org\",\n  \"author\": \"@kekscom\",\n  \"contributors\": [\n    \"Jan Marsch <jama@keks.com>\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git@github.com:kekscom/osmbuildings.git\"\n  },\n  \"scripts\": {},\n  \"dependencies\": {\n    \"qolor\": \"^2.3.3\"\n  },\n  \"devDependencies\": {\n    \"terser\": \"^4.6.7\"\n  },\n  \"optionalDependencies\": {}\n}\n"
  },
  {
    "path": "src/Data.js",
    "content": "\nclass Data {\n\n  static getPixelFootprint (buffer) {\n    let footprint = new Int32Array(buffer.length),\n      px;\n\n    for (let i = 0, il = buffer.length-1; i < il; i+=2) {\n      px = geoToPixel(buffer[i], buffer[i+1]);\n      footprint[i]   = px.x;\n      footprint[i+1] = px.y;\n    }\n\n    footprint = simplifyPolygon(footprint);\n    if (footprint.length < 8) { // 3 points & end==start (*2)\n      return;\n    }\n\n    return footprint;\n  }\n\n  static resetItems () {\n    this.items = [];\n    this.cache = {};\n    Picking.reset();\n  }\n\n  static addRenderItems (data, allAreNew) {\n    let item, scaledItem, id;\n    let geojson = GeoJSON.read(data);\n    for (let i = 0, il = geojson.length; i < il; i++) {\n      item = geojson[i];\n      id = item.id || [item.footprint[0], item.footprint[1], item.height, item.minHeight].join(',');\n      if (!this.cache[id]) {\n        if ((scaledItem = this.scaleItem(item))) {\n          scaledItem.scale = allAreNew ? 0 : 1;\n          this.items.push(scaledItem);\n          this.cache[id] = 1;\n        }\n      }\n    }\n    fadeIn();\n  }\n\n  static scalePolygon (buffer, factor) {\n    return buffer.map(coord => coord*factor);\n  }\n\n  static scale (factor) {\n    Data.items = Data.items.map(item => {\n      // item.height = Math.min(item.height*factor, MAX_HEIGHT); // TODO: should be filtered by renderer\n\n      item.height *= factor;\n      item.minHeight *= factor;\n\n      item.footprint = Data.scalePolygon(item.footprint, factor);\n      item.center.x *= factor;\n      item.center.y *= factor;\n\n      if (item.radius) {\n        item.radius *= factor;\n      }\n\n      if (item.holes) {\n        for (let i = 0, il = item.holes.length; i < il; i++) {\n          item.holes[i] = Data.scalePolygon(item.holes[i], factor);\n        }\n      }\n\n      item.roofHeight *= factor;\n\n      return item;\n    });\n  }\n\n  static scaleItem (item) {\n    let\n      res = {},\n      // TODO: calculate this on zoom change only\n      zoomScale = 6 / pow(2, ZOOM-MIN_ZOOM); // TODO: consider using HEIGHT / (devicePixelRatio || 1)\n\n    if (item.id) {\n      res.id = item.id;\n    }\n\n    res.height = min(item.height/zoomScale, MAX_HEIGHT);\n\n    res.minHeight = isNaN(item.minHeight) ? 0 : item.minHeight / zoomScale;\n    if (res.minHeight > MAX_HEIGHT) {\n      return;\n    }\n\n    res.footprint = this.getPixelFootprint(item.footprint);\n    if (!res.footprint) {\n      return;\n    }\n    res.center = getCenter(res.footprint);\n\n    if (item.radius) {\n      res.radius = item.radius*PIXEL_PER_DEG;\n    }\n    if (item.shape) {\n      res.shape = item.shape;\n    }\n    if (item.roofShape) {\n      res.roofShape = item.roofShape;\n    }\n    if ((res.roofShape === 'cone' || res.roofShape === 'dome') && !res.shape && isRotational(res.footprint)) {\n      res.shape = 'cylinder';\n    }\n\n    if (item.holes) {\n      res.holes = [];\n      let innerFootprint;\n      for (let i = 0, il = item.holes.length; i < il; i++) {\n        // TODO: simplify\n        if ((innerFootprint = this.getPixelFootprint(item.holes[i]))) {\n          res.holes.push(innerFootprint);\n        }\n      }\n    }\n\n    let color;\n\n    if (item.wallColor) {\n      if ((color = Qolor.parse(item.wallColor))) {\n        res.altColor  = ''+ color.lightness(0.8);\n        res.wallColor = ''+ color;\n      }\n    }\n\n    if (item.roofColor) {\n      if ((color = Qolor.parse(item.roofColor))) {\n        res.roofColor = ''+ color;\n      }\n    }\n\n    if (item.relationId) {\n      res.relationId = item.relationId;\n    }\n    res.hitColor = Picking.idToColor(item.relationId || item.id);\n\n    res.roofHeight = isNaN(item.roofHeight) ? 0 : item.roofHeight/zoomScale;\n\n    if (res.height+res.roofHeight <= res.minHeight) {\n      return;\n    }\n\n    return res;\n  }\n\n  static set (data) {\n    this.resetItems();\n    this._staticData = data;\n    this.addRenderItems(this._staticData, true);\n  }\n\n  static load (src, key) {\n    this.src = src || DATA_SRC.replace('{k}', (key || 'anonymous'));\n    this.update();\n  }\n\n  static update () {\n    this.resetItems();\n\n    if (ZOOM < MIN_ZOOM) {\n      return;\n    }\n\n    if (this._staticData) {\n      this.addRenderItems(this._staticData);\n    }\n\n    if (this.src) {\n      let\n        tileZoom = 16,\n        tileSize = 256,\n        zoomedTileSize = ZOOM > tileZoom ? tileSize << (ZOOM - tileZoom) : tileSize >> (tileZoom - ZOOM),\n        minX = ORIGIN_X / zoomedTileSize << 0,\n        minY = ORIGIN_Y / zoomedTileSize << 0,\n        maxX = ceil((ORIGIN_X + WIDTH) / zoomedTileSize),\n        maxY = ceil((ORIGIN_Y + HEIGHT) / zoomedTileSize),\n        x, y;\n\n      let scope = this;\n\n      function callback (json) {\n        scope.addRenderItems(json);\n      }\n\n      for (y = minY; y <= maxY; y++) {\n        for (x = minX; x <= maxX; x++) {\n          this.loadTile(x, y, tileZoom, callback);\n        }\n      }\n    }\n  }\n\n  static loadTile (x, y, zoom, callback) {\n    let s = 'abcd'[(x+y) % 4];\n    let url = this.src.replace('{s}', s).replace('{x}', x).replace('{y}', y).replace('{z}', zoom);\n    return Request.loadJSON(url, callback);\n  }\n}\n\nData.cache = {}; // maintain a list of cached items in order to avoid duplicates on tile borders\nData.items = [];\n"
  },
  {
    "path": "src/Debug.js",
    "content": "\nclass Debug {\n\n  static point (x, y, color, size) {\n    const context = this.context;\n    context.fillStyle = color || '#ffcc00';\n    context.beginPath();\n    context.arc(x, y, size || 3, 0, 2*PI);\n    context.closePath();\n    context.fill();\n  }\n\n  static line (ax, ay, bx, by, color) {\n    const context = this.context;\n    context.strokeStyle = color || '#ffcc00';\n    context.beginPath();\n    context.moveTo(ax, ay);\n    context.lineTo(bx, by);\n    context.closePath();\n    context.stroke();\n  }\n}\n"
  },
  {
    "path": "src/GeoJSON.js",
    "content": "\nconst METERS_PER_LEVEL = 3;\n\nconst materialColors = {\n  brick:'#cc7755',\n  bronze:'#ffeecc',\n  canvas:'#fff8f0',\n  concrete:'#999999',\n  copper:'#a0e0d0',\n  glass:'#e8f8f8',\n  gold:'#ffcc00',\n  plants:'#009933',\n  metal:'#aaaaaa',\n  panel:'#fff8f0',\n  plaster:'#999999',\n  roof_tiles:'#f08060',\n  silver:'#cccccc',\n  slate:'#666666',\n  stone:'#996666',\n  tar_paper:'#333333',\n  wood:'#deb887'\n};\n\nconst baseMaterials = {\n  asphalt:'tar_paper',\n  bitumen:'tar_paper',\n  block:'stone',\n  bricks:'brick',\n  glas:'glass',\n  glassfront:'glass',\n  grass:'plants',\n  masonry:'stone',\n  granite:'stone',\n  panels:'panel',\n  paving_stones:'stone',\n  plastered:'plaster',\n  rooftiles:'roof_tiles',\n  roofingfelt:'tar_paper',\n  sandstone:'stone',\n  sheet:'canvas',\n  sheets:'canvas',\n  shingle:'tar_paper',\n  shingles:'tar_paper',\n  slates:'slate',\n  steel:'metal',\n  tar:'tar_paper',\n  tent:'canvas',\n  thatch:'plants',\n  tile:'roof_tiles',\n  tiles:'roof_tiles'\n};\n// cardboard\n// eternit\n// limestone\n// straw\n\nfunction getMaterialColor (str) {\n  str = str.toLowerCase();\n  if (str[0] === '#') {\n    return str;\n  }\n  return materialColors[baseMaterials[str] || str] || null;\n}\n\nconst WINDING_CLOCKWISE = 'CW';\nconst WINDING_COUNTER_CLOCKWISE = 'CCW';\n\n// detect winding direction: clockwise or counter clockwise\nfunction getWinding (points) {\n  let x1, y1, x2, y2,\n    a = 0;\n  for (let i = 0, il = points.length-3; i < il; i += 2) {\n    x1 = points[i];\n    y1 = points[i+1];\n    x2 = points[i+2];\n    y2 = points[i+3];\n    a += x1*y2 - x2*y1;\n  }\n  return (a/2) > 0 ? WINDING_CLOCKWISE : WINDING_COUNTER_CLOCKWISE;\n}\n\n// enforce a polygon winding direcetion. Needed for proper backface culling.\nfunction makeWinding (points, direction) {\n  let winding = getWinding(points);\n  if (winding === direction) {\n    return points;\n  }\n  let revPoints = [];\n  for (let i = points.length-2; i >= 0; i -= 2) {\n    revPoints.push(points[i], points[i+1]);\n  }\n  return revPoints;\n}\n\nfunction alignProperties(prop) {\n  const item = {};\n\n  prop = prop || {};\n\n  item.height    = prop.height    || (prop.levels   ? prop.levels  *METERS_PER_LEVEL : DEFAULT_HEIGHT);\n  item.minHeight = prop.minHeight || (prop.minLevel ? prop.minLevel*METERS_PER_LEVEL : 0);\n\n  const wallColor = prop.material ? getMaterialColor(prop.material) : (prop.wallColor || prop.color);\n  if (wallColor) {\n    item.wallColor = wallColor;\n  }\n\n  const roofColor = prop.roofMaterial ? getMaterialColor(prop.roofMaterial) : prop.roofColor;\n  if (roofColor) {\n    item.roofColor = roofColor;\n  }\n\n  switch (prop.shape) {\n    case 'cylinder':\n    case 'cone':\n    case 'dome':\n    case 'sphere':\n      item.shape = prop.shape;\n      item.isRotational = true;\n    break;\n\n    case 'pyramid':\n      item.shape = prop.shape;\n    break;\n  }\n\n  switch (prop.roofShape) {\n    case 'cone':\n    case 'dome':\n      item.roofShape = prop.roofShape;\n      item.isRotational = true;\n    break;\n\n    case 'pyramid':\n      item.roofShape = prop.roofShape;\n    break;\n  }\n\n  if (item.roofShape && prop.roofHeight) {\n    item.roofHeight = prop.roofHeight;\n    item.height = max(0, item.height-item.roofHeight);\n  } else {\n    item.roofHeight = 0;\n  }\n\n  return item;\n}\n\nfunction getGeometries (geometry) {\n  let\n    polygon,\n    geometries = [], sub;\n\n  switch (geometry.type) {\n    case 'GeometryCollection':\n      geometries = [];\n      for (let i = 0, il = geometry.geometries.length; i < il; i++) {\n        if ((sub = getGeometries(geometry.geometries[i]))) {\n          geometries.push.apply(geometries, sub);\n        }\n      }\n      return geometries;\n\n    case 'MultiPolygon':\n      geometries = [];\n      for (let i = 0, il = geometry.coordinates.length; i < il; i++) {\n        if ((sub = getGeometries({ type: 'Polygon', coordinates: geometry.coordinates[i] }))) {\n          geometries.push.apply(geometries, sub);\n        }\n      }\n      return geometries;\n\n    case 'Polygon':\n      polygon = geometry.coordinates;\n    break;\n\n    default: return [];\n  }\n\n  let\n    p, lat = 1, lon = 0,\n    outer = [], inner = [];\n\n  p = polygon[0];\n  for (let i = 0, il = p.length; i < il; i++) {\n    outer.push(p[i][lat], p[i][lon]);\n  }\n  outer = makeWinding(outer, WINDING_CLOCKWISE);\n\n  for (let i = 0, il = polygon.length-1; i < il; i++) {\n    p = polygon[i+1];\n    inner[i] = [];\n    for (let j = 0, jl = p.length; j < jl; j++) {\n      inner[i].push(p[j][lat], p[j][lon]);\n    }\n    inner[i] = makeWinding(inner[i], WINDING_COUNTER_CLOCKWISE);\n  }\n\n  return [{\n    outer: outer,\n    inner: inner.length ? inner : null\n  }];\n}\n\nfunction clone (obj) {\n  let res = {};\n  for (const p in obj) {\n    if (obj.hasOwnProperty(p)) {\n      res[p] = obj[p];\n    }\n  }\n  return res;\n}\n\nclass GeoJSON {\n\n  static read (geojson) {\n    if (!geojson || geojson.type !== 'FeatureCollection') {\n      return [];\n    }\n\n    const collection = geojson.features;\n    const res = [];\n\n    for (let i = 0, il = collection.length; i < il; i++) {\n      const feature = collection[i];\n\n      if (feature.type !== 'Feature' || onEach(feature) === false) {\n        continue;\n      }\n\n      const baseItem = alignProperties(feature.properties);\n      const geometries = getGeometries(feature.geometry);\n\n      for (let j = 0, jl = geometries.length; j < jl; j++) {\n        const item = clone(baseItem);\n        item.footprint = geometries[j].outer;\n        if (item.isRotational) {\n          item.radius = getLonDelta(item.footprint);\n        }\n\n        if (geometries[j].inner) {\n          item.holes = geometries[j].inner;\n        }\n        if (feature.id || feature.properties.id) {\n          item.id = feature.id || feature.properties.id;\n        }\n\n        if (feature.properties.relationId) {\n          item.relationId = feature.properties.relationId;\n        }\n\n        res.push(item); // TODO: clone base properties!\n      }\n    }\n\n    return res;\n  }\n}\n"
  },
  {
    "path": "src/OSMBuildings.css",
    "content": ".osmb-container {\n  transform: translate3d(0, 0, 0);\n  pointerEvents: none;\n  position: absolute;\n  left: 0;\n  top: 0;\n}\n\n.osmb-container.zoom-animation {\n  transition-duration: 250ms;\n  transition-property: transform;\n  transform-origin: 50% 50%;\n}\n\n.osmb-layer {\n  transform: translate3d(0, 0, 0);\n  image-rendering: optimizeSpeed;\n  position: absolute;\n  left: 0;\n  top: 0;\n}\n"
  },
  {
    "path": "src/Request.js",
    "content": "\nlet cacheData = {};\nlet cacheIndex = [];\nlet cacheSize = 0;\nlet maxCacheSize = 1024*1024 * 5; // 5MB\n\nfunction xhr (url, callback) {\n  if (cacheData[url]) {\n    if (callback) {\n      callback(cacheData[url]);\n    }\n    return;\n  }\n\n  const req = new XMLHttpRequest();\n\n  req.onreadystatechange = function () {\n    if (req.readyState !== 4) {\n      return;\n    }\n    if (!req.status || req.status < 200 || req.status > 299) {\n      return;\n    }\n    if (callback && req.responseText) {\n      const responseText = req.responseText;\n\n      cacheData[url] = responseText;\n      cacheIndex.push({ url: url, size: responseText.length });\n      cacheSize += responseText.length;\n\n      callback(responseText);\n\n      while (cacheSize > maxCacheSize) {\n        let item = cacheIndex.shift();\n        cacheSize -= item.size;\n        delete cacheData[item.url];\n      }\n    }\n  };\n\n  req.open('GET', url);\n  req.send(null);\n\n  return req;\n}\n\nclass Request {\n\n  static loadJSON (url, callback) {\n    return xhr(url, responseText => {\n      let json;\n      try {\n        json = JSON.parse(responseText);\n      } catch(ex) {}\n\n      callback(json);\n    });\n  }\n}\n"
  },
  {
    "path": "src/adapter.js",
    "content": "\nfunction setOrigin (origin) {\n  ORIGIN_X = origin.x;\n  ORIGIN_Y = origin.y;\n}\n\nfunction moveCam (offset) {\n  CAM_X = CENTER_X + offset.x;\n  CAM_Y = HEIGHT   + offset.y;\n  Layers.render(true);\n}\n\nfunction setSize (size) {\n  WIDTH  = size.width;\n  HEIGHT = size.height;\n  CENTER_X = WIDTH /2 <<0;\n  CENTER_Y = HEIGHT/2 <<0;\n\n  CAM_X = CENTER_X;\n  CAM_Y = HEIGHT;\n\n  Layers.setSize(WIDTH, HEIGHT);\n  MAX_HEIGHT = CAM_Z-50;\n}\n\nfunction setZoom (z) {\n  ZOOM = z;\n  MAP_SIZE = MAP_TILE_SIZE <<ZOOM;\n\n  const center = pixelToGeo(ORIGIN_X+CENTER_X, ORIGIN_Y+CENTER_Y);\n  const a = geoToPixel(center.latitude, 0);\n  const b = geoToPixel(center.latitude, 1);\n  PIXEL_PER_DEG = b.x-a.x;\n\n  Layers.setOpacity(Math.pow(0.95, ZOOM-MIN_ZOOM));\n\n  WALL_COLOR_STR = ''+ WALL_COLOR;\n  ALT_COLOR_STR  = ''+ ALT_COLOR;\n  ROOF_COLOR_STR = ''+ ROOF_COLOR;\n}\n\nfunction onResize (e) {\n  setSize(e);\n  Layers.render();\n  Data.update();\n}\n\nfunction onMoveEnd (e) {\n  Layers.render();\n  Data.update(); // => fadeIn() => Layers.render()\n}\n\nfunction onZoomStart () {\n  IS_ZOOMING = true;\n}\n\nfunction onZoomEnd (e) {\n  IS_ZOOMING = false;\n  const factor = Math.pow(2, e.zoom-ZOOM);\n\n  setZoom(e.zoom);\n  // Layers.render(); // TODO: requestAnimationFrame() causes flickering because layers are already cleared\n\n  // show on high zoom levels only\n  if (ZOOM <= MIN_ZOOM) {\n    Layers.clear();\n    return;\n  }\n\n  Data.scale(factor);\n\n  Shadows.render();\n  Simplified.render();\n  Buildings.render();\n\n  Data.update(); // => fadeIn()\n}\n"
  },
  {
    "path": "src/engines/Leaflet.js",
    "content": "\nclass OSMBuildings extends L.Layer {\n\n  constructor (map) {\n    super(map);\n\n    this.offset = {x: 0, y: 0};\n    Layers.init();\n    if (map) {\n      map.addLayer(this);\n    }\n  }\n\n  addTo (map) {\n    map.addLayer(this);\n    return this;\n  }\n\n  onAdd (map) {\n    this.map = map;\n    Layers.appendTo(map._panes.overlayPane);\n\n    let\n      off = this.getOffset(),\n      po = map.getPixelOrigin();\n    setSize({width: map._size.x, height: map._size.y});\n    setOrigin({x: po.x - off.x, y: po.y - off.y});\n    setZoom(map._zoom);\n\n    Layers.setPosition(-off.x, -off.y);\n\n    map.on({\n      move: this.onMove,\n      moveend: this.onMoveEnd,\n      zoomstart: this.onZoomStart,\n      zoomend: this.onZoomEnd,\n      resize: this.onResize,\n      viewreset: this.onViewReset,\n      click: this.onClick\n    }, this);\n\n    if (map.options.zoomAnimation) {\n      map.on('zoomanim', this.onZoom, this);\n    }\n\n    if (map.attributionControl) {\n      map.attributionControl.addAttribution(ATTRIBUTION);\n    }\n\n    Data.update();\n  }\n\n  onRemove () {\n    let map = this.map;\n    if (map.attributionControl) {\n      map.attributionControl.removeAttribution(ATTRIBUTION);\n    }\n\n    map.off({\n      move: this.onMove,\n      moveend: this.onMoveEnd,\n      zoomstart: this.onZoomStart,\n      zoomend: this.onZoomEnd,\n      resize: this.onResize,\n      viewreset: this.onViewReset,\n      click: this.onClick\n    }, this);\n\n    if (map.options.zoomAnimation) {\n      map.off('zoomanim', this.onZoom, this);\n    }\n    Layers.remove();\n    map = null;\n  }\n\n  onMove (e) {\n    let off = this.getOffset();\n    moveCam({x: this.offset.x - off.x, y: this.offset.y - off.y});\n  }\n\n  onMoveEnd (e) {\n    if (this.noMoveEnd) { // moveend is also fired after zoom\n      this.noMoveEnd = false;\n      return;\n    }\n\n    let\n      map = this.map,\n      off = this.getOffset(),\n      po = map.getPixelOrigin();\n\n    this.offset = off;\n    Layers.setPosition(-off.x, -off.y);\n    moveCam({x: 0, y: 0});\n\n    setSize({width: map._size.x, height: map._size.y}); // in case this is triggered by resize\n    setOrigin({x: po.x - off.x, y: po.y - off.y});\n    onMoveEnd(e);\n  }\n\n  onZoomStart (e) {\n    onZoomStart(e);\n  }\n\n  onZoom (e) {\n    let center = this.map.latLngToContainerPoint(e.center);\n    let scale = Math.pow(2, e.zoom - ZOOM);\n\n    let dx = WIDTH / 2 - center.x;\n    let dy = HEIGHT / 2 - center.y;\n\n    let x = WIDTH / 2;\n    let y = HEIGHT / 2;\n\n    if (e.zoom > ZOOM) {\n      x -= dx * scale;\n      y -= dy * scale;\n    } else {\n      x += dx;\n      y += dy;\n    }\n\n    Layers.container.classList.add('zoom-animation');\n    Layers.container.style.transformOrigin = x + 'px ' + y + 'px';\n    Layers.container.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')';\n  }\n\n  onZoomEnd (e) {\n    Layers.clear();\n    Layers.container.classList.remove('zoom-animation');\n    Layers.container.style.transform = 'translate3d(0, 0, 0) scale(1)';\n\n    let\n      map = this.map,\n      off = this.getOffset(),\n      po = map.getPixelOrigin();\n\n    setOrigin({x: po.x - off.x, y: po.y - off.y});\n    onZoomEnd({zoom: map._zoom});\n    this.noMoveEnd = true;\n  }\n\n  onResize () {\n  }\n\n  onViewReset () {\n    let off = this.getOffset();\n\n    this.offset = off;\n    Layers.setPosition(-off.x, -off.y);\n    moveCam({x: 0, y: 0});\n  }\n\n  onClick (e) {\n    let id = Picking.getIdFromXY(e.containerPoint.x, e.containerPoint.y);\n    if (id) {\n      onClick({feature: id, lat: e.latlng.lat, lon: e.latlng.lng});\n    }\n  }\n\n  getOffset () {\n    return L.DomUtil.getPosition(this.map._mapPane);\n  }\n\n  //*** COMMON PUBLIC METHODS ***\n\n  style (style) {\n    style = style || {};\n    let color;\n    if ((color = style.color || style.wallColor)) {\n      WALL_COLOR = Qolor.parse(color);\n      WALL_COLOR_STR = '' + WALL_COLOR;\n\n      ALT_COLOR = WALL_COLOR.lightness(0.8);\n      ALT_COLOR_STR = '' + ALT_COLOR;\n\n      ROOF_COLOR = WALL_COLOR.lightness(1.2);\n      ROOF_COLOR_STR = '' + ROOF_COLOR;\n    }\n\n    if (style.roofColor) {\n      ROOF_COLOR = Qolor.parse(style.roofColor);\n      ROOF_COLOR_STR = '' + ROOF_COLOR;\n    }\n\n    Layers.render();\n\n    return this;\n  }\n\n  date (date) {\n    Shadows.date = date;\n    Shadows.render();\n    return this;\n  }\n\n  load (url) {\n    Data.load(url);\n    return this;\n  }\n\n  set (data) {\n    Data.set(data);\n    return this;\n  }\n\n  each (handler) {\n    onEach = function (payload) {\n      return handler(payload);\n    };\n    return this;\n  }\n\n  click (handler) {\n    onClick = function (payload) {\n      return handler(payload);\n    };\n    return this;\n  }\n}\n\nOSMBuildings.VERSION = VERSION;\nOSMBuildings.ATTRIBUTION = ATTRIBUTION;\n"
  },
  {
    "path": "src/engines/OpenLayers.js",
    "content": "// based on a pull request from Jérémy Judéaux (https://github.com/Volune)\n\nclass OSMBuildings extends ol.layer.Layer {\n\n  constructor (map) {\n    super(OSMBuildings.name, {projection: 'EPSG:900913'});\n\n    this.offset = {x: 0, y: 0}; // cumulative cam offset during moveBy()\n\n    Layers.init();\n    if (map) {\n      map.addLayer(this);\n    }\n  }\n\n  addTo (map) {\n    this.setMap(map);\n    return this;\n  }\n\n  setOrigin () {\n    let map = this.map,\n      origin = map.getLonLatFromPixel(new OpenLayers.Pixel(0, 0)),\n      res = map.resolution,\n      ext = this.maxExtent,\n      x = (origin.lon - ext.left) / res << 0,\n      y = (ext.top - origin.lat) / res << 0;\n    setOrigin({x: x, y: y});\n  }\n\n  setMap (map) {\n    if (!this.map) {\n      super.setMap.call(this, map);\n    }\n    Layers.appendTo(this.div);\n    setSize({width: map.size.w, height: map.size.h});\n    setZoom(map.zoom);\n    this.setOrigin();\n\n    let layerProjection = this.projection;\n    map.events.register('click', map, e => {\n      let id = Picking.getIdFromXY(e.xy.x, e.xy.y);\n      if (id) {\n        let geo = map.getLonLatFromPixel(e.xy).transform(layerProjection, this.projection);\n        onClick({feature: id, lat: geo.lat, lon: geo.lon});\n      }\n    });\n\n    Data.update();\n  }\n\n  removeMap (map) {\n    Layers.remove();\n    super.removeMap.call(this, map);\n    this.map = null;\n  }\n\n  onMapResize () {\n    let map = this.map;\n    super.onMapResize.call(this);\n    onResize({width: map.size.w, height: map.size.h});\n  }\n\n  moveTo (bounds, zoomChanged, isDragging) {\n    let\n      map = this.map,\n      res = super.moveTo.call(this, bounds, zoomChanged, isDragging);\n\n    if (!isDragging) {\n      let\n        offsetLeft = parseInt(map.layerContainerDiv.style.left, 10),\n        offsetTop = parseInt(map.layerContainerDiv.style.top, 10);\n\n      this.div.style.left = -offsetLeft + 'px';\n      this.div.style.top = -offsetTop + 'px';\n    }\n\n    this.setOrigin();\n    this.offset.x = 0;\n    this.offset.y = 0;\n    moveCam(this.offset);\n\n    if (zoomChanged) {\n      onZoomEnd({zoom: map.zoom});\n    } else {\n      onMoveEnd();\n    }\n\n    return res;\n  }\n\n  moveByPx (dx, dy) {\n    this.offset.x += dx;\n    this.offset.y += dy;\n    let res = super.moveByPx.call(this, dx, dy);\n    moveCam(this.offset);\n    return res;\n  }\n}\n\nOSMBuildings.name = 'OSM Buildings';\nOSMBuildings.attribution = ATTRIBUTION;\nOSMBuildings.isBaseLayer = false;\nOSMBuildings.alwaysInRange = true;\n"
  },
  {
    "path": "src/engines/index-Leaflet.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>OSM Buildings Classic for Leaflet</title>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.css\"/>\n  <script src=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.js\"></script>\n  <link rel=\"stylesheet\" href=\"OSMBuildings.css\">\n  <script src=\"OSMBuildings-Leaflet.debug.js\"></script>\n  <style>\n    html, body {\n      border: 0;\n      margin: 0;\n      padding: 0;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n    }\n\n    #map {\n      height: 100%;\n    }\n  </style>\n</head>\n\n<body>\n<div id=\"map\"></div>\n\n<script>\n  // const map = new L.Map('map', { minZoom: 16 }).setView([52.52179, 13.39503], 18); // Berlin\n  const map = new L.Map('map').setView([40.711516, -74.011695], 16); // NYC\n\n  // new L.TileLayer('https://{s}.tiles.mapbox.com/v3/osmbuildings.kbpalbpk/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);\n  new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);\n\n  const osmb = new OSMBuildings();\n\n  L.control.layers({}, { Buildings: osmb }).addTo(map);\n\n  osmb.addTo(map)\n    .date(new Date(2017, 5, 15, 17, 30))\n    .load()\n    .click(function(e) {\n      console.log('feature clicked:', e);\n    });\n\n  // add GeoJSON\n  map.setView([52.52179, 13.39503], 18); // Berlin Bodemuseum\n  const data = {\n    \"type\": \"FeatureCollection\",\n    \"features\": [\n      {\n        \"type\": \"Feature\",\n        \"properties\": { \"height\": 50, \"color\": \"#ffcc00\" },\n        \"geometry\": {\n          \"type\": \"Polygon\",\n          \"coordinates\": [\n            [\n              [\n                13.39631974697113,\n                52.52184840804295\n              ],\n              [\n                13.39496523141861,\n                52.521166220963536\n              ],\n              [\n                13.395150303840637,\n                52.52101770514734\n              ],\n              [\n                13.396652340888977,\n                52.52174559105107\n              ],\n              [\n                13.39631974697113,\n                52.52184840804295\n              ]\n            ]\n          ]\n        }\n      }\n    ]\n  };\n  osmb.set(data);\n</script>\n</body>\n</html>"
  },
  {
    "path": "src/engines/index-OpenLayers.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>OSM Buildings Classic for OpenLayers</title>\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.2.1/css/ol.css\"/>\n  <script src=\"https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.2.1/build/ol.js\"></script>\n  <link rel=\"stylesheet\" href=\"OSMBuildings.css\">\n  <script src=\"OSMBuildings-OpenLayers.debug.js\"></script>\n  <style>\n    html, body {\n      border: 0;\n      margin: 0;\n      padding: 0;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n    }\n\n    #map {\n      height: 100%;\n    }\n  </style>\n</head>\n\n<body>\n<div id=\"map\"></div>\n\n<script>\n  const map = new ol.Map({\n    target: 'map',\n    layers: [\n      new ol.layer.Tile({\n        source: new ol.source.OSM()\n      })\n    ],\n    view: new ol.View({\n      center: ol.proj.fromLonLat([52.52179, 13.39503]), // Berlin\n      // center: ol.proj.fromLonLat([40.711516, -74.011695]), // NYC\n      zoom: 16\n    })\n  });\n\n  const osmb = new OSMBuildings();\n\n  osmb.addTo(map)\n    .date(new Date(2017, 5, 15, 17, 30))\n    .load()\n    .click(function(e) {\n      console.log('feature clicked:', e);\n    });\n\n  // // GeoJSON\n  // // map.setView([52.52179, 13.39503], 18); // Berlin Bodemuseum\n  // // const data = {\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"id\":1,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.388652,52.520222],[13.388758,52.52021],[13.389204,52.520184],[13.389735,52.520162],[13.390013,52.52015],[13.390153,52.520155],[13.391974,52.520274],[13.392177,52.52029],[13.392477,52.520319],[13.392728,52.520355],[13.393062,52.520414],[13.393416,52.520505],[13.393754,52.520615],[13.394054,52.52073],[13.394226,52.520811],[13.394393,52.520678],[13.394217,52.520597],[13.393905,52.520471],[13.39355,52.52036],[13.393176,52.520268],[13.392811,52.520205],[13.392527,52.520169],[13.392209,52.52014],[13.392006,52.520122],[13.39025,52.520002],[13.390123,52.519993],[13.389317,52.519947],[13.38872,52.5199],[13.388693,52.520002],[13.388652,52.520222]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":2,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.392946,52.521681],[13.393053,52.521847],[13.393228,52.52174],[13.393363,52.521645],[13.393499,52.521549],[13.393439,52.521523],[13.394023,52.521095],[13.39398,52.521074],[13.394229,52.520907],[13.394066,52.520818],[13.3938,52.521001],[13.393842,52.521027],[13.393242,52.521434],[13.393285,52.52146],[13.392946,52.521681]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":3,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393432,52.521863],[13.39371,52.522017],[13.393874,52.521918],[13.393605,52.521756],[13.393432,52.521863]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":4,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394871,52.520148],[13.395216,52.52032],[13.395544,52.520072],[13.395374,52.519995],[13.395111,52.519962],[13.394871,52.520148]],[[13.395008,52.520144],[13.395124,52.520049],[13.395292,52.520078],[13.395128,52.520203],[13.395008,52.520144]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":5,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393699,52.5202],[13.393927,52.520312],[13.394052,52.520218],[13.393866,52.520126],[13.393746,52.520074],[13.393699,52.5202]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":6,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393746,52.520074],[13.393866,52.520126],[13.393871,52.520081],[13.393889,52.520073],[13.393872,52.520056],[13.393903,52.519975],[13.393926,52.519965],[13.394501,52.52004],[13.394479,52.520103],[13.394162,52.520083],[13.394151,52.520148],[13.394319,52.520157],[13.394526,52.520168],[13.394555,52.520146],[13.394623,52.520095],[13.394679,52.520052],[13.394803,52.519958],[13.393834,52.519838],[13.39382,52.519876],[13.393795,52.519942],[13.393746,52.520074]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":7,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393867,52.522186],[13.394056,52.522516],[13.394296,52.522483],[13.394093,52.522153],[13.393867,52.522186]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":9,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393927,52.520312],[13.394078,52.520388],[13.394206,52.520294],[13.394157,52.52027],[13.394052,52.520218],[13.393927,52.520312]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":10,\"height\":15},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.393996,52.521925],[13.394018,52.521993],[13.394044,52.522027],[13.394071,52.522054],[13.394154,52.522098],[13.394256,52.522118],[13.394314,52.522121],[13.394502,52.5221],[13.395601,52.52198],[13.395597,52.521963],[13.396059,52.521915],[13.396104,52.521876],[13.396091,52.521831],[13.396035,52.521803],[13.396074,52.521771],[13.394912,52.5212],[13.394077,52.521797],[13.394017,52.521856],[13.393996,52.521925]],[[13.394702,52.521541],[13.394743,52.521509],[13.394794,52.521506],[13.394905,52.521429],[13.394904,52.521409],[13.394881,52.521396],[13.39493,52.521361],[13.395228,52.52151],[13.394873,52.521627],[13.394702,52.521541]],[[13.395524,52.521723],[13.395602,52.521715],[13.395599,52.521701],[13.395618,52.521686],[13.395741,52.521749],[13.395765,52.521837],[13.395565,52.521854],[13.395524,52.521723]],[[13.395005,52.521779],[13.395327,52.521666],[13.395382,52.521847],[13.39504,52.521885],[13.395005,52.521779]],[[13.394604,52.521879],[13.394829,52.5218],[13.394869,52.521931],[13.394676,52.521955],[13.394604,52.521879]],[[13.394447,52.521727],[13.394577,52.521638],[13.394742,52.521724],[13.394526,52.521798],[13.394447,52.521727]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":11,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394078,52.520388],[13.394369,52.520531],[13.394497,52.520438],[13.394299,52.52034],[13.394206,52.520294],[13.394078,52.520388]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":12,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394157,52.52027],[13.394206,52.520294],[13.394299,52.52034],[13.39443,52.52024],[13.394515,52.520176],[13.394526,52.520168],[13.394319,52.520157],[13.394157,52.52027]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":13,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394226,52.520811],[13.394532,52.520957],[13.394655,52.521015],[13.394965,52.521163],[13.395059,52.521209],[13.395088,52.521188],[13.395121,52.521163],[13.395168,52.521128],[13.395212,52.521095],[13.395247,52.52107],[13.395151,52.521027],[13.394841,52.520884],[13.394711,52.520824],[13.394393,52.520678],[13.394226,52.520811]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":14,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394369,52.520531],[13.394712,52.5207],[13.39488,52.520574],[13.394708,52.520488],[13.394665,52.520521],[13.394497,52.520438],[13.394369,52.520531]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":16,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.39443,52.52024],[13.394793,52.520423],[13.394708,52.520488],[13.39488,52.520574],[13.395053,52.520443],[13.394515,52.520176],[13.39443,52.52024]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":17,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394515,52.520176],[13.395053,52.520443],[13.395216,52.52032],[13.394871,52.520148],[13.394803,52.520114],[13.394735,52.52008],[13.394679,52.520052],[13.394623,52.520095],[13.395003,52.520284],[13.39495,52.520323],[13.39476,52.520228],[13.394744,52.52024],[13.394555,52.520146],[13.394526,52.520168],[13.394515,52.520176]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":19,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394585,52.521048],[13.394595,52.521053],[13.394614,52.52104],[13.394604,52.521034],[13.394585,52.521048]]]]}},{\"type\":\"Feature\",\"properties\":{\"id\":20,\"height\":null},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[13.394679,52.520052],[13.394735,52.52008],[13.394811,52.520019],[13.394902,52.520034],[13.394803,52.520114],[13.394871,52.520148],[13.395111,52.519962],[13.394842,52.519929],[13.394803,52.519958],[13.394679,52.520052]]]]}}]};\n  // // const osmb = new OSMBuildings(map).set(data);\n</script>\n</body>\n</html>"
  },
  {
    "path": "src/functions.js",
    "content": "\nfunction rad (deg) {\n  return deg * PI / 180;\n}\n\nfunction deg (rad) {\n  return rad / PI * 180;\n}\n\nfunction pixelToGeo (x, y) {\n  const res = {};\n  x /= MAP_SIZE;\n  y /= MAP_SIZE;\n  res[LAT] = y <= 0  ? 90 : y >= 1 ? -90 : deg(2 * atan(exp(PI * (1 - 2*y))) - HALF_PI);\n  res[LON] = (x === 1 ?  1 : (x%1 + 1) % 1) * 360 - 180;\n  return res;\n}\n\nfunction geoToPixel (lat, lon) {\n  const\n    latitude = min(1, max(0, 0.5 - (log(tan(QUARTER_PI + HALF_PI * lat / 180)) / PI) / 2)),\n    longitude = lon/360 + 0.5;\n  return {\n    x: longitude*MAP_SIZE <<0,\n    y: latitude *MAP_SIZE <<0\n  };\n}\n\nfunction fromRange (sVal, sMin, sMax, dMin, dMax) {\n  sVal = min(max(sVal, sMin), sMax);\n  const rel = (sVal-sMin) / (sMax-sMin),\n    range = dMax-dMin;\n  return min(max(dMin + rel*range, dMin), dMax);\n}\n\nfunction isVisible (polygon) {\n  const\n    maxX = WIDTH+ORIGIN_X,\n    maxY = HEIGHT+ORIGIN_Y;\n\n  // TODO: checking footprint is sufficient for visibility - NOT VALID FOR SHADOWS!\n  for (let i = 0, il = polygon.length-3; i < il; i+=2) {\n    if (polygon[i] > ORIGIN_X && polygon[i] < maxX && polygon[i+1] > ORIGIN_Y && polygon[i+1] < maxY) {\n      return true;\n    }\n  }\n  return false;\n}\n"
  },
  {
    "path": "src/geometry/Cylinder.js",
    "content": "class Cylinder {\n\n  static draw (context, center, radius, topRadius, height, minHeight, color, altColor, roofColor) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a1, a2;\n\n    topRadius *= scale;\n\n    if (minHeight) {\n      c = Buildings.project(c, minScale);\n      radius = radius*minScale;\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    // no tangents? top circle is inside bottom circle\n    if (!tangents) {\n      a1 = 1.5*PI;\n      a2 = 1.5*PI;\n    } else {\n      a1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      a2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n    }\n\n    context.fillStyle = color;\n    context.beginPath();\n    context.arc(apex.x, apex.y, topRadius, HALF_PI, a1, true);\n    context.arc(c.x, c.y, radius, a1, HALF_PI);\n    context.closePath();\n    context.fill();\n\n    context.fillStyle = altColor;\n    context.beginPath();\n    context.arc(apex.x, apex.y, topRadius, a2, HALF_PI, true);\n    context.arc(c.x, c.y, radius, HALF_PI, a2);\n    context.closePath();\n    context.fill();\n\n    context.fillStyle = roofColor;\n    this._circle(context, apex, topRadius);\n  }\n\n  static simplified (context, center, radius) {\n    this._circle(context, { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, radius);\n  }\n\n  static shadow (context, center, radius, topRadius, height, minHeight) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      apex = Shadows.project(c, height),\n      p1, p2;\n\n    if (minHeight) {\n      c = Shadows.project(c, minHeight);\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    // TODO: no tangents? roof overlaps everything near cam position\n    if (tangents) {\n      p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n      context.moveTo(tangents[1].x2, tangents[1].y2);\n      context.arc(apex.x, apex.y, topRadius, p2, p1);\n      context.arc(c.x, c.y, radius, p1, p2);\n    } else {\n      context.moveTo(c.x+radius, c.y);\n      context.arc(c.x, c.y, radius, 0, 2*PI);\n    }\n  }\n\n  static hitArea (context, center, radius, topRadius, height, minHeight, color) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      p1, p2;\n\n    topRadius *= scale;\n\n    if (minHeight) {\n      c = Buildings.project(c, minScale);\n      radius = radius*minScale;\n    }\n\n    // common tangents for ground and roof circle\n    let tangents = this._tangents(c, radius, apex, topRadius);\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    // TODO: no tangents? roof overlaps everything near cam position\n    if (tangents) {\n      p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x);\n      p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x);\n      context.moveTo(tangents[1].x2, tangents[1].y2);\n      context.arc(apex.x, apex.y, topRadius, p2, p1);\n      context.arc(c.x, c.y, radius, p1, p2);\n    } else {\n      context.moveTo(c.x+radius, c.y);\n      context.arc(c.x, c.y, radius, 0, 2*PI);\n    }\n\n    context.closePath();\n    context.fill();\n  }\n\n  static _circle (context, center, radius) {\n    context.beginPath();\n    context.arc(center.x, center.y, radius, 0, PI*2);\n    context.fill();\n  }\n\n    // http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Tangents_between_two_circles\n  static _tangents (c1, r1, c2, r2) {\n    let\n      dx = c1.x-c2.x,\n      dy = c1.y-c2.y,\n      dr = r1-r2,\n      sqdist = (dx*dx) + (dy*dy);\n\n    if (sqdist <= dr*dr) {\n      return;\n    }\n\n    let dist = sqrt(sqdist),\n      vx = -dx/dist,\n      vy = -dy/dist,\n      c  =  dr/dist,\n      res = [],\n      h, nx, ny;\n\n    // Let A, B be the centers, and C, D be points at which the tangent\n    // touches first and second circle, and n be the normal vector to it.\n    //\n    // We have the system:\n    //   n * n = 1    (n is a unit vector)\n    //   C = A + r1 * n\n    //   D = B + r2 * n\n    //   n * CD = 0   (common orthogonality)\n    //\n    // n * CD = n * (AB + r2*n - r1*n) = AB*n - (r1 -/+ r2) = 0,  <=>\n    // AB * n = (r1 -/+ r2), <=>\n    // v * n = (r1 -/+ r2) / d,  where v = AB/|AB| = AB/d\n    // This is a linear equation in unknown vector n.\n    // Now we're just intersecting a line with a circle: v*n=c, n*n=1\n\n    h = sqrt(max(0, 1 - c*c));\n    for (let sign = 1; sign >= -1; sign -= 2) {\n      nx = vx*c - sign*h*vy;\n      ny = vy*c + sign*h*vx;\n      res.push({\n        x1: c1.x + r1*nx <<0,\n        y1: c1.y + r1*ny <<0,\n        x2: c2.x + r2*nx <<0,\n        y2: c2.y + r2*ny <<0\n      });\n    }\n\n    return res;\n  }\n}\n"
  },
  {
    "path": "src/geometry/Extrusion.js",
    "content": "class Extrusion {\n\n  static draw (context, polygon, innerPolygons, height, minHeight, color, altColor, roofColor) {\n    let\n      roof = this._extrude(context, polygon, height, minHeight, color, altColor),\n      innerRoofs = [];\n\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        innerRoofs[i] = this._extrude(context, innerPolygons[i], height, minHeight, color, altColor);\n      }\n    }\n\n    context.fillStyle = roofColor;\n\n    context.beginPath();\n    this._ring(context, roof);\n    if (innerPolygons) {\n      for (let i = 0, il = innerRoofs.length; i < il; i++) {\n        this._ring(context, innerRoofs[i]);\n      }\n    }\n    context.closePath();\n    context.fill();\n  }\n\n  static _extrude (context, polygon, height, minHeight, color, altColor) {\n    let\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      _a, _b,\n      roof = [];\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Buildings.project(a, scale);\n      _b = Buildings.project(b, scale);\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set wall shading\n        if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n          context.fillStyle = altColor;\n        } else {\n          context.fillStyle = color;\n        }\n\n        context.beginPath();\n        this._ring(context, [\n           b.x,  b.y,\n           a.x,  a.y,\n          _a.x, _a.y,\n          _b.x, _b.y\n        ]);\n        context.closePath();\n        context.fill();\n      }\n\n      roof[i]   = _a.x;\n      roof[i+1] = _a.y;\n    }\n\n    return roof;\n  }\n\n  static _ring (context, polygon) {\n    context.moveTo(polygon[0], polygon[1]);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i], polygon[i+1]);\n    }\n  }\n\n  static simplified (context, polygon, innerPolygons) {\n    context.beginPath();\n    this._ringAbs(context, polygon);\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        this._ringAbs(context, innerPolygons[i]);\n      }\n    }\n    context.closePath();\n    context.fill();\n  }\n\n  static _ringAbs (context, polygon) {\n    context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y);\n    }\n  }\n\n  static shadow (context, polygon, innerPolygons, height, minHeight) {\n    let\n      mode = null,\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      _a, _b;\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Shadows.project(a, height);\n      _b = Shadows.project(b, height);\n\n      if (minHeight) {\n        a = Shadows.project(a, minHeight);\n        b = Shadows.project(b, minHeight);\n      }\n\n      // mode 0: floor edges, mode 1: roof edges\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        if (mode === 1) {\n          context.lineTo(a.x, a.y);\n        }\n        mode = 0;\n        if (!i) {\n          context.moveTo(a.x, a.y);\n        }\n        context.lineTo(b.x, b.y);\n      } else {\n        if (mode === 0) {\n          context.lineTo(_a.x, _a.y);\n        }\n        mode = 1;\n        if (!i) {\n          context.moveTo(_a.x, _a.y);\n        }\n        context.lineTo(_b.x, _b.y);\n      }\n    }\n\n    if (innerPolygons) {\n      for (let i = 0, il = innerPolygons.length; i < il; i++) {\n        this._ringAbs(context, innerPolygons[i]);\n      }\n    }\n  }\n\n  static hitArea (context, polygon, innerPolygons, height, minHeight, color) {\n    let\n      mode = null,\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      _a, _b;\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      _a = Buildings.project(a, scale);\n      _b = Buildings.project(b, scale);\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // mode 0: floor edges, mode 1: roof edges\n      if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) {\n        if (mode === 1) { // mode is initially undefined\n          context.lineTo(a.x, a.y);\n        }\n        mode = 0;\n        if (!i) {\n          context.moveTo(a.x, a.y);\n        }\n        context.lineTo(b.x, b.y);\n      } else {\n        if (mode === 0) { // mode is initially undefined\n          context.lineTo(_a.x, _a.y);\n        }\n        mode = 1;\n        if (!i) {\n          context.moveTo(_a.x, _a.y);\n        }\n        context.lineTo(_b.x, _b.y);\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n"
  },
  {
    "path": "src/geometry/Pyramid.js",
    "content": "class Pyramid {\n\n  static draw (context, polygon, center, height, minHeight, color, altColor) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 };\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set shading\n        if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n          context.fillStyle = altColor;\n        } else {\n          context.fillStyle = color;\n        }\n\n        context.beginPath();\n        this._triangle(context, a, b, apex);\n        context.closePath();\n        context.fill();\n      }\n    }\n  }\n\n  static _triangle (context, a, b, c) {\n    context.moveTo(a.x, a.y);\n    context.lineTo(b.x, b.y);\n    context.lineTo(c.x, c.y);\n  }\n\n  static _ring (context, polygon) {\n    context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y);\n    for (let i = 2, il = polygon.length-1; i < il; i += 2) {\n      context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y);\n    }\n  }\n\n  static shadow (context, polygon, center, height, minHeight) {\n    let\n      a = { x:0, y:0 },\n      b = { x:0, y:0 },\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      apex = Shadows.project(c, height);\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Shadows.project(a, minHeight);\n        b = Shadows.project(b, minHeight);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        // depending on direction, set shading\n        this._triangle(context, a, b, apex);\n      }\n    }\n  }\n\n  static hitArea (context, polygon, center, height, minHeight, color) {\n    let\n      c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y },\n      scale = CAM_Z / (CAM_Z-height),\n      minScale = CAM_Z / (CAM_Z-minHeight),\n      apex = Buildings.project(c, scale),\n      a = { x:0, y:0 },\n      b = { x:0, y:0 };\n\n    context.fillStyle = color;\n    context.beginPath();\n\n    for (let i = 0, il = polygon.length-3; i < il; i += 2) {\n      a.x = polygon[i  ]-ORIGIN_X;\n      a.y = polygon[i+1]-ORIGIN_Y;\n      b.x = polygon[i+2]-ORIGIN_X;\n      b.y = polygon[i+3]-ORIGIN_Y;\n\n      if (minHeight) {\n        a = Buildings.project(a, minScale);\n        b = Buildings.project(b, minScale);\n      }\n\n      // backface culling check\n      if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) {\n        this._triangle(context, a, b, apex);\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n"
  },
  {
    "path": "src/geometry/__Dome.js",
    "content": "function rotation (p, c, a) {\n  let ms = sin(a), mc = cos(a);\n  p.x -= c.x;\n  p.y -= c.y;\n  return {\n    x: p.x* mc + p.y*ms + c.x,\n    y: p.x*-ms + p.y*mc + c.y\n  };\n}\n\nlet KAPPA = 0.5522847498;\nfunction dome (c, r, h, minHeight) {\n  if (!h) {\n    h = r;\n  }\n\n  minHeight = minHeight || 0;\n\n  // VERTICAL TANGENT POINTS ON SPHERE:\n  // side view at scenario:\n  // sphere at c.x,c.y & radius => circle at c.y,minHeight\n  // cam  at CAM_X/CAM_Y/CAM_Z => point  at CAM_Y/CAM_Z\n  let t = getEllipseTangent(r, h, CAM_Y-c.y, CAM_Z-minHeight);\n    t.x += c.y;\n    t.y += minHeight;\n\n  if (minHeight) {\n    c = project(c.x, c.y, CAM_Z / (CAM_Z-minHeight));\n    r *= CAM_Z / (CAM_Z-minHeight);\n  }\n\n// radialGradient(c, r, roofColorAlpha)\n  drawCircle(c, r, true);\n\n  let _h = CAM_Z / (CAM_Z-h),\n  hfK = CAM_Z / (CAM_Z-(h*KAPPA));\n\n  let apex = project(c.x, c.y, _h);\ndebugMarker(apex);\n\n  let angle = atan((CAM_X-c.x)/(CAM_Y-c.y));\n\n  context.beginPath();\n\n  // ausgerichteter sichtrand!\n  let _th = CAM_Z / (CAM_Z-t.y);\n  let p = rotation({ x:c.x, y:t.x }, c, angle);\n  let _p = project(p.x, p.y, _th);\n//debugMarker(_p);\n  let p1h = rotation({ x:c.x-r, y:t.x }, c, angle);\n  let _p1h = project(p1h.x, p1h.y, _th);\n//debugMarker(_p1h);\n  let p2h = rotation({ x:c.x+r, y:t.x }, c, angle);\n  let _p2h = project(p2h.x, p2h.y, _th);\n//debugMarker(_p2h);\n  let p1v = rotation({ x:c.x-r, y:c.y }, c, angle);\n//debugMarker(p1v);\n  let p2v = rotation({ x:c.x+r, y:c.y }, c, angle);\n//debugMarker(p2v);\n\n  context.moveTo(p1v.x, p1v.y);\n  context.bezierCurveTo(\n    p1v.x + (_p1h.x-p1v.x) * KAPPA,\n    p1v.y + (_p1h.y-p1v.y) * KAPPA,\n    _p.x + (_p1h.x-_p.x) * KAPPA,\n    _p.y + (_p1h.y-_p.y) * KAPPA,\n    _p.x, _p.y);\n\n  context.moveTo(p2v.x, p2v.y);\n  context.bezierCurveTo(\n    p2v.x + (_p1h.x-p1v.x) * KAPPA,\n    p2v.y + (_p1h.y-p1v.y) * KAPPA,\n    _p.x + (_p2h.x-_p.x) * KAPPA,\n    _p.y + (_p2h.y-_p.y) * KAPPA,\n    _p.x, _p.y);\n\n\n//      drawMeridian(c, r, _h, hfK, apex, rad(45));\n//      drawMeridian(c, r, _h, hfK, apex, rad(135));\n\n  for (let i = 0; i <= 180; i+=30) {\n    drawMeridian(c, r, _h, hfK, apex, rad(i));\n  }\n\n//      for (let i = 0; i <= 180; i+=30) {\n//        drawMeridian(c, r, _h, hfK, apex, rad(i));\n//      }\n\n//      context.fill();\n  context.stroke();\n}\n\nfunction drawMeridian (c, r, _h, hfK, apex, angle) {\n  drawHalfMeridian(c, r, _h, hfK, apex, angle);\n  drawHalfMeridian(c, r, _h, hfK, apex, angle + PI);\n}\n\nfunction drawHalfMeridian (c, r, _h, hfK, apex, angle) {\n  let p1 = rotation({ x:c.x, y:c.y-r },     c, angle);\n  let p2 = rotation({ x:c.x, y:c.y-r*KAPPA }, c, angle);\n  let _p1 = project(p1.x, p1.y, hfK);\n  let _p2 = project(p2.x, p2.y, _h);\n  context.moveTo(p1.x, p1.y);\n  context.bezierCurveTo(_p1.x, _p1.y, _p2.x, _p2.y, apex.x, apex.y);\n}\n\nfunction getEllipseTangent (a, b, x, y) {\n  let\n  C = (x*x) / (a*a) + (y*y) / (b*b),\n    R = Math.sqrt(C-1),\n    yabR = y*(a/b)*R,\n    xbaR = x*(b/a)*R;\n  return {\n    x: (x + (  yabR < 0 ? yabR : -yabR)) / C,\n    y: (y + (y+xbaR > 0 ? xbaR : -xbaR)) / C\n  };\n}\n"
  },
  {
    "path": "src/geometry.js",
    "content": "\nfunction getDistance (p1, p2) {\n  const\n    dx = p1.x-p2.x,\n    dy = p1.y-p2.y;\n  return dx*dx + dy*dy;\n}\n\nfunction isRotational (polygon) {\n  const length = polygon.length;\n  if (length < 16) {\n    return false;\n  }\n\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n  for (let i = 0; i < length-1; i+=2) {\n    minX = Math.min(minX, polygon[i]);\n    maxX = Math.max(maxX, polygon[i]);\n    minY = Math.min(minY, polygon[i+1]);\n    maxY = Math.max(maxY, polygon[i+1]);\n  }\n\n  const\n    width = maxX-minX,\n    height = (maxY-minY),\n    ratio = width/height;\n\n  if (ratio < 0.85 || ratio > 1.15) {\n    return false;\n  }\n\n  const\n    center = { x:minX+width/2, y:minY+height/2 },\n    radius = (width+height)/4,\n    sqRadius = radius*radius;\n\n  for (let i = 0; i < length-1; i+=2) {\n    const dist = getDistance({ x:polygon[i], y:polygon[i+1] }, center);\n    if (dist/sqRadius < 0.8 || dist/sqRadius > 1.2) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction getSquareSegmentDistance (px, py, p1x, p1y, p2x, p2y) {\n  let\n    dx = p2x-p1x,\n    dy = p2y-p1y,\n    t;\n  if (dx !== 0 || dy !== 0) {\n    t = ((px-p1x) * dx + (py-p1y) * dy) / (dx*dx + dy*dy);\n    if (t > 1) {\n      p1x = p2x;\n      p1y = p2y;\n    } else if (t > 0) {\n      p1x += dx*t;\n      p1y += dy*t;\n    }\n  }\n  dx = px-p1x;\n  dy = py-p1y;\n  return dx*dx + dy*dy;\n}\n\nfunction simplifyPolygon (buffer) {\n  let\n    sqTolerance = 2,\n    len = buffer.length/2,\n    markers = new Uint8Array(len),\n\n    first = 0, last = len-1,\n\n    maxSqDist,\n    sqDist,\n    index,\n    firstStack = [], lastStack  = [],\n    newBuffer  = [];\n\n  markers[first] = markers[last] = 1;\n\n  while (last) {\n    maxSqDist = 0;\n    for (let i = first+1; i < last; i++) {\n      sqDist = getSquareSegmentDistance(\n        buffer[i    *2], buffer[i    *2 + 1],\n        buffer[first*2], buffer[first*2 + 1],\n        buffer[last *2], buffer[last *2 + 1]\n      );\n      if (sqDist > maxSqDist) {\n        index = i;\n        maxSqDist = sqDist;\n      }\n    }\n\n    if (maxSqDist > sqTolerance) {\n      markers[index] = 1;\n\n      firstStack.push(first);\n      lastStack.push(index);\n\n      firstStack.push(index);\n      lastStack.push(last);\n    }\n\n    first = firstStack.pop();\n    last = lastStack.pop();\n  }\n\n  for (let i = 0; i < len; i++) {\n    if (markers[i]) {\n      newBuffer.push(buffer[i*2], buffer[i*2 + 1]);\n    }\n  }\n\n  return newBuffer;\n}\n\nfunction getCenter (footprint) {\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n  for (let i = 0, il = footprint.length-3; i < il; i += 2) {\n    minX = min(minX, footprint[i]);\n    maxX = max(maxX, footprint[i]);\n    minY = min(minY, footprint[i+1]);\n    maxY = max(maxY, footprint[i+1]);\n  }\n  return { x:minX+(maxX-minX)/2 <<0, y:minY+(maxY-minY)/2 <<0 };\n}\n\nlet EARTH_RADIUS = 6378137;\n\nfunction getLonDelta (footprint) {\n  let minLon = 180, maxLon = -180;\n  for (let i = 0, il = footprint.length; i < il; i += 2) {\n    minLon = min(minLon, footprint[i+1]);\n    maxLon = max(maxLon, footprint[i+1]);\n  }\n  return (maxLon-minLon)/2;\n}\n"
  },
  {
    "path": "src/layers/Buildings.js",
    "content": "class Buildings {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.context.canvas.style.opacity = opacity;\n  }\n\n  static project (p, m) {\n    return {\n      x: (p.x-CAM_X) * m + CAM_X <<0,\n      y: (p.y-CAM_Y) * m + CAM_Y <<0\n    };\n  }\n\n  static render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      item,\n      h, mh,\n      sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y },\n      footprint,\n      wallColor, altColor, roofColor,\n      dataItems = Data.items;\n\n    dataItems.sort((a, b) => {\n      return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height);\n    });\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (Simplified.isSimple(item)) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      // when fading in, use a dynamic height\n      h = item.scale < 1 ? item.height*item.scale : item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight;\n      }\n\n      wallColor = item.wallColor || WALL_COLOR_STR;\n      altColor  = item.altColor  || ALT_COLOR_STR;\n      roofColor = item.roofColor || ROOF_COLOR_STR;\n      context.strokeStyle = altColor;\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break;\n        case 'cone':     Cylinder.draw(context, item.center, item.radius, 0, h, mh, wallColor, altColor);                      break;\n        case 'dome':     Cylinder.draw(context, item.center, item.radius, item.radius/2, h, mh, wallColor, altColor);          break;\n        case 'sphere':   Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break;\n        case 'pyramid':  Pyramid.draw(context, footprint, item.center, h, mh, wallColor, altColor);                            break;\n        default:         Extrusion.draw(context, footprint, item.holes, h, mh, wallColor, altColor, roofColor);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.draw(context, item.center, item.radius, 0, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9));             break;\n        case 'dome':    Cylinder.draw(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9)); break;\n        case 'pyramid': Pyramid.draw(context, footprint, item.center, h+item.roofHeight, h, roofColor, Qolor.parse(roofColor).lightness(0.9));                       break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/layers/Picking.js",
    "content": "\nclass Picking {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static setOpacity (opacity) {}\n\n  static clear () {}\n\n  static reset () {\n    this._idMapping = [null];\n  }\n\n  static render () {\n    if (this._timer) {\n      return;\n    }\n    let self = this;\n    this._timer = setTimeout(t => {\n      self._timer = null;\n      self._render();\n    }, 500);\n  }\n\n  static _render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      item,\n      h, mh,\n      sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y },\n      footprint,\n      color,\n      dataItems = Data.items;\n\n    dataItems.sort((a, b) => {\n      return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height);\n    });\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (!(color = item.hitColor)) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      h = item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.minHeight;\n      }\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color);   break;\n        case 'cone':     Cylinder.hitArea(context, item.center, item.radius, 0, h, mh, color);             break;\n        case 'dome':     Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h, mh, color); break;\n        case 'sphere':   Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color);   break;\n        case 'pyramid':  Pyramid.hitArea(context, footprint, item.center, h, mh, color);                   break;\n        default:         Extrusion.hitArea(context, footprint, item.holes, h, mh, color);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.hitArea(context, item.center, item.radius, 0, h+item.roofHeight, h, color);             break;\n        case 'dome':    Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, color); break;\n        case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h+item.roofHeight, h, color);                   break;\n      }\n    }\n\n    // otherwise fails on size 0\n    if (WIDTH && HEIGHT) {\n      this._imageData = this.context.getImageData(0, 0, WIDTH, HEIGHT).data;\n    }\n  }\n\n  static getIdFromXY (x, y) {\n    let imageData = this._imageData;\n    if (!imageData) {\n      return;\n    }\n    let pos = 4*((y|0) * WIDTH + (x|0));\n    let index = imageData[pos] | (imageData[pos+1]<<8) | (imageData[pos+2]<<16);\n    return this._idMapping[index];\n  }\n\n  static idToColor (id) {\n    let index = this._idMapping.indexOf(id);\n    if (index === -1) {\n      this._idMapping.push(id);\n      index = this._idMapping.length-1;\n    }\n    let r =  index       & 0xff;\n    let g = (index >>8)  & 0xff;\n    let b = (index >>16) & 0xff;\n    return 'rgb('+ [r, g, b].join(',') +')';\n  }\n}\n\nPicking._idMapping = [null];\n"
  },
  {
    "path": "src/layers/Shadows.js",
    "content": "class Shadows {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.opacity = opacity;\n  }\n\n  static project (p, h) {\n    return {\n      x: p.x + this.direction.x*h,\n      y: p.y + this.direction.y*h\n    };\n  }\n\n  static render () {\n    this.clear();\n    \n    let\n      context = this.context,\n      screenCenter,\n      sun, length, alpha;\n\n    // TODO: calculate this just on demand\n    screenCenter = pixelToGeo(CENTER_X+ORIGIN_X, CENTER_Y+ORIGIN_Y);\n    sun = getSunPosition(this.date, screenCenter.latitude, screenCenter.longitude);\n\n    if (sun.altitude <= 0) {\n      return;\n    }\n\n    length = 1 / tan(sun.altitude);\n    alpha = length < 5 ? 0.75 : 1/length*5;\n\n    this.direction.x = cos(sun.azimuth) * length;\n    this.direction.y = sin(sun.azimuth) * length;\n\n    let\n      i, il,\n      item,\n      h, mh,\n      footprint,\n      dataItems = Data.items;\n\n    context.canvas.style.opacity = alpha / (this.opacity * 2);\n    context.shadowColor = this.blurColor;\n    context.fillStyle = this.color;\n    context.beginPath();\n\n    for (i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      // when fading in, use a dynamic height\n      h = item.scale < 1 ? item.height*item.scale : item.height;\n\n      mh = 0;\n      if (item.minHeight) {\n        mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight;\n      }\n\n      switch (item.shape) {\n        case 'cylinder': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh);   break;\n        case 'cone':     Cylinder.shadow(context, item.center, item.radius, 0, h, mh);             break;\n        case 'dome':     Cylinder.shadow(context, item.center, item.radius, item.radius/2, h, mh); break;\n        case 'sphere':   Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh);   break;\n        case 'pyramid':  Pyramid.shadow(context, footprint, item.center, h, mh);                   break;\n        default:         Extrusion.shadow(context, footprint, item.holes, h, mh);\n      }\n\n      switch (item.roofShape) {\n        case 'cone':    Cylinder.shadow(context, item.center, item.radius, 0, h+item.roofHeight, h);             break;\n        case 'dome':    Cylinder.shadow(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h); break;\n        case 'pyramid': Pyramid.shadow(context, footprint, item.center, h+item.roofHeight, h);                   break;\n      }\n    }\n\n    context.closePath();\n    context.fill();\n  }\n}\n\nShadows.color = '#666666';\nShadows.blurColor = '#000000';\nShadows.date = new Date();\nShadows.direction = { x:0, y:0 };\nShadows.opacity = 1;\n\n"
  },
  {
    "path": "src/layers/Simplified.js",
    "content": "class Simplified {\n\n  static init (context) {\n    this.context = context;\n  }\n\n  static clear () {\n    this.context.clearRect(0, 0, WIDTH, HEIGHT);\n  }\n\n  static setOpacity (opacity) {\n    this.context.canvas.style.opacity = opacity;\n  }\n\n  static isSimple (item) {\n    return (ZOOM <= Simplified.MAX_ZOOM && item.height+item.roofHeight < Simplified.MAX_HEIGHT);\n  }\n\n  static render () {\n    this.clear();\n    \n    let context = this.context;\n\n    // show on high zoom levels only and avoid rendering during zoom\n    if (ZOOM > Simplified.MAX_ZOOM) {\n      return;\n    }\n\n    let\n      item,\n      footprint,\n      dataItems = Data.items;\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      item = dataItems[i];\n\n      if (item.height >= Simplified.MAX_HEIGHT) {\n        continue;\n      }\n\n      footprint = item.footprint;\n\n      if (!isVisible(footprint)) {\n        continue;\n      }\n\n      context.strokeStyle = item.altColor  || ALT_COLOR_STR;\n      context.fillStyle   = item.roofColor || ROOF_COLOR_STR;\n\n      switch (item.shape) {\n        case 'cylinder':\n        case 'cone':\n        case 'dome':\n        case 'sphere': Cylinder.simplified(context, item.center, item.radius);  break;\n        default: Extrusion.simplified(context, footprint, item.holes);\n      }\n    }\n  }\n}\n\nSimplified.MAX_ZOOM = 16; // max zoom where buildings could render simplified\nSimplified.MAX_HEIGHT = 5; // max building height in order to be simple\n"
  },
  {
    "path": "src/layers/index.js",
    "content": "let animTimer;\n\nfunction fadeIn() {\n  if (animTimer) {\n    return;\n  }\n\n  animTimer = setInterval(t => {\n    let dataItems = Data.items,\n      isNeeded = false;\n\n    for (let i = 0, il = dataItems.length; i < il; i++) {\n      if (dataItems[i].scale < 1) {\n        dataItems[i].scale += 0.5*0.2; // amount*easing\n        if (dataItems[i].scale > 1) {\n          dataItems[i].scale = 1;\n        }\n        isNeeded = true;\n      }\n    }\n\n    Layers.render();\n\n    if (!isNeeded) {\n      clearInterval(animTimer);\n      animTimer = null;\n    }\n  }, 33);\n}\n\nclass Layers {\n\n  static init () {\n    Layers.container.className = 'osmb-container';\n\n    // TODO: improve this\n    Shadows.init(Layers.createContext(Layers.container));\n    Simplified.init(Layers.createContext(Layers.container));\n    Buildings.init(Layers.createContext(Layers.container));\n    Picking.init(Layers.createContext());\n  }\n\n  static clear () {\n    Shadows.clear();\n    Simplified.clear();\n    Buildings.clear();\n    Picking.clear();\n  }\n\n  static setOpacity (opacity) {\n    Shadows.setOpacity(opacity);\n    Simplified.setOpacity(opacity);\n    Buildings.setOpacity(opacity);\n    Picking.setOpacity(opacity);\n  }\n\n  static render (quick) {\n    // show on high zoom levels only\n    if (ZOOM < MIN_ZOOM) {\n      Layers.clear();\n      return;\n    }\n\n    // don't render during zoom\n    if (IS_ZOOMING) {\n      return;\n    }\n\n    requestAnimationFrame(f => {\n      if (!quick) {\n        Shadows.render();\n        Simplified.render();\n        //HitAreas.render(); // TODO: do this on demand\n      }\n      Buildings.render();\n    });\n  }\n\n  static createContext (container) {\n    let canvas = document.createElement('CANVAS');\n    canvas.className = 'osmb-layer';\n\n    let context = canvas.getContext('2d');\n    context.lineCap   = 'round';\n    context.lineJoin  = 'round';\n    context.lineWidth = 1;\n    context.imageSmoothingEnabled = false;\n\n    Layers.items.push(canvas);\n    if (container) {\n      container.appendChild(canvas);\n    }\n\n    return context;\n  }\n\n  static appendTo (parentNode) {\n    parentNode.appendChild(Layers.container);\n  }\n\n  static remove () {\n    Layers.container.parentNode.removeChild(Layers.container);\n  }\n\n  static setSize (width, height) {\n    Layers.items.forEach(canvas => {\n      canvas.width  = width;\n      canvas.height = height;\n    });\n  }\n\n  // usually called after move: container jumps by move delta, cam is reset\n  static setPosition (x, y) {\n    Layers.container.style.left = x +'px';\n    Layers.container.style.top  = y +'px';\n  }\n}\n\nLayers.container = document.createElement('DIV');\nLayers.items = [];\n"
  },
  {
    "path": "src/lib/getSunPosition.js",
    "content": "// calculations are based on http://aa.quae.nl/en/reken/zonpositie.html\n// code credits to Vladimir Agafonkin (@mourner)\n\nfunction getSunPosition () {\n\n  const m = Math,\n    PI = m.PI,\n    sin = m.sin,\n    cos = m.cos,\n    tan = m.tan,\n    asin = m.asin,\n    atan = m.atan2;\n\n  const rad = PI/180,\n    dayMs = 1000*60*60*24,\n    J1970 = 2440588,\n    J2000 = 2451545,\n    e = rad*23.4397; // obliquity of the Earth\n\n  function toJulian(date) {\n    return date.valueOf()/dayMs - 0.5+J1970;\n  }\n  function toDays(date) {\n    return toJulian(date)-J2000;\n  }\n  function getRightAscension(l, b) {\n    return atan(sin(l)*cos(e) - tan(b)*sin(e), cos(l));\n  }\n  function getDeclination(l, b) {\n    return asin(sin(b)*cos(e) + cos(b)*sin(e)*sin(l));\n  }\n  function getAzimuth(H, phi, dec) {\n    return atan(sin(H), cos(H)*sin(phi) - tan(dec)*cos(phi));\n  }\n  function getAltitude(H, phi, dec) {\n    return asin(sin(phi)*sin(dec) + cos(phi)*cos(dec)*cos(H));\n  }\n  function getSiderealTime(d, lw) {\n    return rad * (280.16 + 360.9856235*d) - lw;\n  }\n  function getSolarMeanAnomaly(d) {\n    return rad * (357.5291 + 0.98560028*d);\n  }\n  function getEquationOfCenter(M) {\n    return rad * (1.9148*sin(M) + 0.0200 * sin(2*M) + 0.0003 * sin(3*M));\n  }\n  function getEclipticLongitude(M, C) {\n    const P = rad*102.9372; // perihelion of the Earth\n    return M+C+P+PI;\n  }\n\n  return function getSunPosition(date, lat, lon) {\n    const lw = rad*-lon,\n      phi = rad*lat,\n      d = toDays(date),\n      M = getSolarMeanAnomaly(d),\n      C = getEquationOfCenter(M),\n      L = getEclipticLongitude(M, C),\n      D = getDeclination(L, 0),\n      A = getRightAscension(L, 0),\n      t = getSiderealTime(d, lw),\n      H = t-A;\n\n    return {\n      altitude: getAltitude(H, phi, D),\n      azimuth: getAzimuth(H, phi, D) - PI/2 // origin: north\n    };\n  };\n}\n"
  },
  {
    "path": "src/shortcuts.js",
    "content": "\nconst\n  m = Math,\n  exp = m.exp,\n  log = m.log,\n  sin = m.sin,\n  cos = m.cos,\n  tan = m.tan,\n  atan = m.atan,\n  atan2 = m.atan2,\n  min = m.min,\n  max = m.max,\n  sqrt = m.sqrt,\n  ceil = m.ceil,\n  pow = m.pow;\n"
  },
  {
    "path": "src/variables.js",
    "content": "let\n  VERSION      = '0.3.2',\n  ATTRIBUTION  = '&copy; <a href=\"https://osmbuildings.org\">OSM Buildings</a>',\n\n  DATA_SRC = 'https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json',\n\n  PI         = Math.PI,\n  HALF_PI    = PI/2,\n  QUARTER_PI = PI/4,\n\n  MAP_TILE_SIZE  = 256,    // map tile size in pixels\n  ZOOM, MAP_SIZE,\n\n  MIN_ZOOM = 15,\n\n  LAT = 'latitude', LON = 'longitude',\n\n  WIDTH = 0, HEIGHT = 0,\n  CENTER_X = 0, CENTER_Y = 0,\n  ORIGIN_X = 0, ORIGIN_Y = 0,\n\n  WALL_COLOR = Qolor.parse('rgba(200, 190, 180)'),\n  ALT_COLOR  = WALL_COLOR.lightness(0.8),\n  ROOF_COLOR = WALL_COLOR.lightness(1.2),\n\n  WALL_COLOR_STR = ''+ WALL_COLOR,\n  ALT_COLOR_STR  = ''+ ALT_COLOR,\n  ROOF_COLOR_STR = ''+ ROOF_COLOR,\n\n  PIXEL_PER_DEG = 0,\n\n  MAX_HEIGHT, // taller buildings will be cut to this\n  DEFAULT_HEIGHT = 5,\n\n  CAM_X, CAM_Y, CAM_Z = 450,\n\n  IS_ZOOMING;\n\nfunction onEach () {}\n\nfunction onClick () {}\n"
  },
  {
    "path": "tests/openlayers-5.3.0/OSMBuildings-OL5.js",
    "content": "/**\n * Copyright (C) 2019 OSM Buildings, Jan Marsch\n * A JavaScript library for visualizing building geometry on interactive maps.\n * @osmbuildings, http://osmbuildings.org\n */\n\nimport { Vector as VectorLayer } from \"ol/layer.js\";\nimport VectorSource from \"ol/source/Vector.js\";\nimport { inherits as olInherits } from \"ol/util.js\";\nimport * as olProj from \"ol/proj.js\";\n\n//****** file: Block.js ******\nclass Block {\n    constructor() {}\n\n    draw(\n        context,\n        polygon,\n        innerPolygons,\n        height,\n        minHeight,\n        color,\n        altColor,\n        roofColor\n    ) {\n        var i,\n            il,\n            roof = this._extrude(\n                context,\n                polygon,\n                height,\n                minHeight,\n                color,\n                altColor\n            ),\n            innerRoofs = [];\n\n        if (innerPolygons) {\n            for (i = 0, il = innerPolygons.length; i < il; i++) {\n                innerRoofs[i] = this._extrude(\n                    context,\n                    innerPolygons[i],\n                    height,\n                    minHeight,\n                    color,\n                    altColor\n                );\n            }\n        }\n\n        context.fillStyle = roofColor;\n\n        context.beginPath();\n        this._ring(context, roof);\n        if (innerPolygons) {\n            for (i = 0, il = innerRoofs.length; i < il; i++) {\n                this._ring(context, innerRoofs[i]);\n            }\n        }\n        context.closePath();\n        context.stroke();\n        context.fill();\n    }\n\n    _extrude(context, polygon, height, minHeight, color, altColor) {\n        var scale = CAM_Z / (CAM_Z - height),\n            minScale = CAM_Z / (CAM_Z - minHeight),\n            a = {\n                x: 0,\n                y: 0\n            },\n            b = {\n                x: 0,\n                y: 0\n            },\n            _a,\n            _b,\n            roof = [];\n\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            a.x = polygon[i] - ORIGIN_X;\n            a.y = polygon[i + 1] - ORIGIN_Y;\n            b.x = polygon[i + 2] - ORIGIN_X;\n            b.y = polygon[i + 3] - ORIGIN_Y;\n\n            _a = buildings.project(a, scale);\n            _b = buildings.project(b, scale);\n\n            if (minHeight) {\n                a = buildings.project(a, minScale);\n                b = buildings.project(b, minScale);\n            }\n\n            // backface culling check\n            if ((b.x - a.x) * (_a.y - a.y) > (_a.x - a.x) * (b.y - a.y)) {\n                // depending on direction, set wall shading\n                if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n                    context.fillStyle = altColor;\n                } else {\n                    context.fillStyle = color;\n                }\n\n                context.beginPath();\n                this._ring(context, [b.x, b.y, a.x, a.y, _a.x, _a.y, _b.x, _b.y]);\n                context.closePath();\n                context.fill();\n            }\n\n            roof[i] = _a.x;\n            roof[i + 1] = _a.y;\n        }\n\n        return roof;\n    }\n\n    _ring(context, polygon) {\n        context.moveTo(polygon[0], polygon[1]);\n        for (var i = 2, il = polygon.length - 1; i < il; i += 2) {\n            context.lineTo(polygon[i], polygon[i + 1]);\n        }\n    }\n\n    simplified(context, polygon, innerPolygons) {\n        context.beginPath();\n        this._ringAbs(context, polygon);\n        if (innerPolygons) {\n            for (var i = 0, il = innerPolygons.length; i < il; i++) {\n                this._ringAbs(context, innerPolygons[i]);\n            }\n        }\n        context.closePath();\n        context.stroke();\n        context.fill();\n    }\n\n    _ringAbs(context, polygon) {\n        context.moveTo(polygon[0] - ORIGIN_X, polygon[1] - ORIGIN_Y);\n        for (var i = 2, il = polygon.length - 1; i < il; i += 2) {\n            context.lineTo(polygon[i] - ORIGIN_X, polygon[i + 1] - ORIGIN_Y);\n        }\n    }\n\n    shadow(context, polygon, innerPolygons, height, minHeight) {\n        var mode = null,\n            a = {\n                x: 0,\n                y: 0\n            },\n            b = {\n                x: 0,\n                y: 0\n            },\n            _a,\n            _b;\n\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            a.x = polygon[i] - ORIGIN_X;\n            a.y = polygon[i + 1] - ORIGIN_Y;\n            b.x = polygon[i + 2] - ORIGIN_X;\n            b.y = polygon[i + 3] - ORIGIN_Y;\n\n            _a = shadows.project(a, height);\n            _b = shadows.project(b, height);\n\n            if (minHeight) {\n                a = shadows.project(a, minHeight);\n                b = shadows.project(b, minHeight);\n            }\n\n            // mode 0: floor edges, mode 1: roof edges\n            if ((b.x - a.x) * (_a.y - a.y) > (_a.x - a.x) * (b.y - a.y)) {\n                if (mode === 1) {\n                    context.lineTo(a.x, a.y);\n                }\n                mode = 0;\n                if (!i) {\n                    context.moveTo(a.x, a.y);\n                }\n                context.lineTo(b.x, b.y);\n            } else {\n                if (mode === 0) {\n                    context.lineTo(_a.x, _a.y);\n                }\n                mode = 1;\n                if (!i) {\n                    context.moveTo(_a.x, _a.y);\n                }\n                context.lineTo(_b.x, _b.y);\n            }\n        }\n\n        if (innerPolygons) {\n            for (i = 0, il = innerPolygons.length; i < il; i++) {\n                this._ringAbs(context, innerPolygons[i]);\n            }\n        }\n    }\n\n    shadowMask(context, polygon, innerPolygons) {\n        this._ringAbs(context, polygon);\n        if (innerPolygons) {\n            for (var i = 0, il = innerPolygons.length; i < il; i++) {\n                this._ringAbs(context, innerPolygons[i]);\n            }\n        }\n    }\n\n    hitArea(context, polygon, innerPolygons, height, minHeight, color) {\n        var mode = null,\n            a = {\n                x: 0,\n                y: 0\n            },\n            b = {\n                x: 0,\n                y: 0\n            },\n            scale = CAM_Z / (CAM_Z - height),\n            minScale = CAM_Z / (CAM_Z - minHeight),\n            _a,\n            _b;\n\n        context.fillStyle = color;\n        context.beginPath();\n\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            a.x = polygon[i] - ORIGIN_X;\n            a.y = polygon[i + 1] - ORIGIN_Y;\n            b.x = polygon[i + 2] - ORIGIN_X;\n            b.y = polygon[i + 3] - ORIGIN_Y;\n\n            _a = buildings.project(a, scale);\n            _b = buildings.project(b, scale);\n\n            if (minHeight) {\n                a = buildings.project(a, minScale);\n                b = buildings.project(b, minScale);\n            }\n\n            // mode 0: floor edges, mode 1: roof edges\n            if ((b.x - a.x) * (_a.y - a.y) > (_a.x - a.x) * (b.y - a.y)) {\n                if (mode === 1) {\n                    // mode is initially undefined\n                    context.lineTo(a.x, a.y);\n                }\n                mode = 0;\n                if (!i) {\n                    context.moveTo(a.x, a.y);\n                }\n                context.lineTo(b.x, b.y);\n            } else {\n                if (mode === 0) {\n                    // mode is initially undefined\n                    context.lineTo(_a.x, _a.y);\n                }\n                mode = 1;\n                if (!i) {\n                    context.moveTo(_a.x, _a.y);\n                }\n                context.lineTo(_b.x, _b.y);\n            }\n        }\n\n        context.closePath();\n        context.fill();\n    }\n}\n\n//****** file: Buildings.js ******\nclass Buildings {\n    constructor() {\n        this.data;\n    }\n    setData(data) {\n        this.data = data;\n    }\n\n    project(p, m) {\n        return {\n            x: ((p.x - CAM_X) * m + CAM_X) << 0,\n            y: ((p.y - CAM_Y) * m + CAM_Y) << 0\n        };\n    }\n\n    render() {\n        var context = this.context;\n        context.clearRect(0, 0, WIDTH, HEIGHT);\n\n        // show on high zoom levels only and avoid rendering during zoom\n        if (ZOOM < MIN_ZOOM || isZooming) {\n            return;\n        }\n\n        var item,\n            h,\n            mh,\n            sortCam = {\n                x: CAM_X + ORIGIN_X,\n                y: CAM_Y + ORIGIN_Y\n            },\n            footprint,\n            wallColor,\n            altColor,\n            roofColor,\n            dataItems = this.data.getItems();\n\n        dataItems.sort(function(a, b) {\n            return (\n                a.minHeight - b.minHeight ||\n                Geometry.getDistance(b.center, sortCam) - Geometry.getDistance(a.center, sortCam) ||\n                b.height - a.height\n            );\n        });\n\n        var cylinder = new Cylinder();\n        var pyramid = new Pyramid();\n        var block = new Block();\n        for (var i = 0, il = dataItems.length; i < il; i++) {\n            item = dataItems[i];\n\n            if (simplified.isSimple(item)) {\n                continue;\n            }\n\n            footprint = item.footprint;\n\n            if (!Functions.isVisible(footprint)) {\n                continue;\n            }\n\n            // when fading in, use a dynamic height\n            h = item.scale < 1 ? item.height * item.scale : item.height;\n\n            mh = 0;\n            if (item.minHeight) {\n                mh = item.scale < 1 ? item.minHeight * item.scale : item.minHeight;\n            }\n\n            wallColor = item.wallColor || WALL_COLOR_STR;\n            altColor = item.altColor || ALT_COLOR_STR;\n            roofColor = item.roofColor || ROOF_COLOR_STR;\n            context.strokeStyle = altColor;\n\n            switch (item.shape) {\n                case \"cylinder\":\n                    cylinder.draw(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius,\n                        h,\n                        mh,\n                        wallColor,\n                        altColor,\n                        roofColor\n                    );\n                    break;\n                case \"cone\":\n                    cylinder.draw(\n                        context,\n                        item.center,\n                        item.radius,\n                        0,\n                        h,\n                        mh,\n                        wallColor,\n                        altColor\n                    );\n                    break;\n                case \"dome\":\n                    cylinder.draw(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius / 2,\n                        h,\n                        mh,\n                        wallColor,\n                        altColor\n                    );\n                    break;\n                case \"sphere\":\n                    cylinder.draw(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius,\n                        h,\n                        mh,\n                        wallColor,\n                        altColor,\n                        roofColor\n                    );\n                    break;\n                case \"pyramid\":\n                    pyramid.draw(\n                        context,\n                        footprint,\n                        item.center,\n                        h,\n                        mh,\n                        wallColor,\n                        altColor\n                    );\n                    break;\n                default:\n                    block.draw(\n                        context,\n                        footprint,\n                        item.holes,\n                        h,\n                        mh,\n                        wallColor,\n                        altColor,\n                        roofColor\n                    );\n            }\n\n            switch (item.roofShape) {\n                case \"cone\":\n                    cylinder.draw(\n                        context,\n                        item.center,\n                        item.radius,\n                        0,\n                        h + item.roofHeight,\n                        h,\n                        roofColor,\n                        \"\" + Color.parse(roofColor).lightness(0.9)\n                    );\n                    break;\n                case \"dome\":\n                    cylinder.draw(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius / 2,\n                        h + item.roofHeight,\n                        h,\n                        roofColor,\n                        \"\" + Color.parse(roofColor).lightness(0.9)\n                    );\n                    break;\n                case \"pyramid\":\n                    pyramid.draw(\n                        context,\n                        footprint,\n                        item.center,\n                        h + item.roofHeight,\n                        h,\n                        roofColor,\n                        Color.parse(roofColor).lightness(0.9)\n                    );\n                    break;\n            }\n        }\n    }\n    setContext(context) {\n        this.context = context;\n    }\n}\n\n//****** file: Color.debug.js ******\nclass Color {\n    constructor(h, s, l, a) {\n        this.H = h;\n        this.S = s;\n        this.L = l;\n        this.A = a;\n    }\n\n    hue2rgb(p, q, t) {\n        if (t < 0) t += 1;\n        if (t > 1) t -= 1;\n        if (t < 1 / 6) return p + (q - p) * 6 * t;\n        if (t < 1 / 2) return q;\n        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n        return p;\n    }\n\n    clamp(v, max) {\n        return Math.min(max, Math.max(0, v));\n    }\n\n    /*\n     * str can be in any of these:\n     * #0099ff rgb(64, 128, 255) rgba(64, 128, 255, 0.5)\n     */\n    static parse(str) {\n        var r = 0,\n            g = 0,\n            b = 0,\n            a = 1,\n            m;\n        // Static variable\n        var w3cColors = {\n            aqua: \"#00ffff\",\n            black: \"#000000\",\n            blue: \"#0000ff\",\n            fuchsia: \"#ff00ff\",\n            gray: \"#808080\",\n            grey: \"#808080\",\n            green: \"#008000\",\n            lime: \"#00ff00\",\n            maroon: \"#800000\",\n            navy: \"#000080\",\n            olive: \"#808000\",\n            orange: \"#ffa500\",\n            purple: \"#800080\",\n            red: \"#ff0000\",\n            silver: \"#c0c0c0\",\n            teal: \"#008080\",\n            white: \"#ffffff\",\n            yellow: \"#ffff00\"\n        };\n\n        str = (\"\" + str).toLowerCase();\n        str = w3cColors[str] || str;\n\n        if ((m = str.match(/^#(\\w{2})(\\w{2})(\\w{2})$/))) {\n            r = parseInt(m[1], 16);\n            g = parseInt(m[2], 16);\n            b = parseInt(m[3], 16);\n        } else if (\n            (m = str.match(/rgba?\\((\\d+)\\D+(\\d+)\\D+(\\d+)(\\D+([\\d.]+))?\\)/))\n        ) {\n            r = parseInt(m[1], 10);\n            g = parseInt(m[2], 10);\n            b = parseInt(m[3], 10);\n            a = m[4] ? parseFloat(m[5]) : 1;\n        } else {\n            return;\n        }\n\n        return this.fromRGBA(r, g, b, a);\n    }\n\n    toRGBA() {\n        var h = this.clamp(this.H, 360),\n            s = this.clamp(this.S, 1),\n            l = this.clamp(this.L, 1),\n            rgba = {\n                a: this.clamp(this.A, 1)\n            };\n\n        // achromatic\n        if (s === 0) {\n            rgba.r = l;\n            rgba.g = l;\n            rgba.b = l;\n        } else {\n            var q = l < 0.5 ? l * (1 + s) : l + s - l * s,\n                p = 2 * l - q;\n            h /= 360;\n\n            rgba.r = this.hue2rgb(p, q, h + 1 / 3);\n            rgba.g = this.hue2rgb(p, q, h);\n            rgba.b = this.hue2rgb(p, q, h - 1 / 3);\n        }\n\n        return {\n            r: Math.round(rgba.r * 255),\n            g: Math.round(rgba.g * 255),\n            b: Math.round(rgba.b * 255),\n            a: rgba.a\n        };\n    }\n\n    static fromRGBA(r, g, b, a) {\n        if (typeof r === \"object\") {\n            g = r.g / 255;\n            b = r.b / 255;\n            a = r.a;\n            r = r.r / 255;\n        } else {\n            r /= 255;\n            g /= 255;\n            b /= 255;\n        }\n\n        var max = Math.max(r, g, b),\n            min = Math.min(r, g, b),\n            h,\n            s,\n            l = (max + min) / 2,\n            d = max - min;\n\n        if (!d) {\n            h = s = 0; // achromatic\n        } else {\n            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n            switch (max) {\n                case r:\n                    h = (g - b) / d + (g < b ? 6 : 0);\n                    break;\n                case g:\n                    h = (b - r) / d + 2;\n                    break;\n                case b:\n                    h = (r - g) / d + 4;\n                    break;\n            }\n            h *= 60;\n        }\n\n        return new Color(h, s, l, a);\n    }\n\n    toString() {\n        var rgba = this.toRGBA();\n\n        if (rgba.a === 1) {\n            return (\n                \"#\" +\n                ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b)\n                .toString(16)\n                .slice(1, 7)\n            );\n        }\n        return (\n            \"rgba(\" + [rgba.r, rgba.g, rgba.b, rgba.a.toFixed(2)].join(\",\") + \")\"\n        );\n    }\n\n    hue(h) {\n        return new Color(this.H * h, this.S, this.L, this.A);\n    }\n\n    saturation(s) {\n        return new Color(this.H, this.S * s, this.L, this.A);\n    }\n\n    lightness(l) {\n        return new Color(this.H, this.S, this.L * l, this.A);\n    }\n\n    alpha(a) {\n        return new Color(this.H, this.S, this.L, this.A * a);\n    }\n}\n\n//****** file: Cylinder.js ******\nclass Cylinder {\n    constructor() {}\n    draw(\n        context,\n        center,\n        radius,\n        topRadius,\n        height,\n        minHeight,\n        color,\n        altColor,\n        roofColor\n    ) {\n        var c = {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            scale = CAM_Z / (CAM_Z - height),\n            minScale = CAM_Z / (CAM_Z - minHeight),\n            apex = buildings.project(c, scale),\n            a1,\n            a2;\n\n        topRadius *= scale;\n\n        if (minHeight) {\n            c = buildings.project(c, minScale);\n            radius = radius * minScale;\n        }\n\n        // common tangents for ground and roof circle\n        var tangents = this._tangents(c, radius, apex, topRadius);\n\n        // no tangents? top circle is inside bottom circle\n        if (!tangents) {\n            a1 = 1.5 * PI;\n            a2 = 1.5 * PI;\n        } else {\n            a1 = atan2(tangents[0].y1 - c.y, tangents[0].x1 - c.x);\n            a2 = atan2(tangents[1].y1 - c.y, tangents[1].x1 - c.x);\n        }\n\n        context.fillStyle = color;\n        context.beginPath();\n        context.arc(apex.x, apex.y, topRadius, HALF_PI, a1, true);\n        context.arc(c.x, c.y, radius, a1, HALF_PI);\n        context.closePath();\n        context.fill();\n\n        context.fillStyle = altColor;\n        context.beginPath();\n        context.arc(apex.x, apex.y, topRadius, a2, HALF_PI, true);\n        context.arc(c.x, c.y, radius, HALF_PI, a2);\n        context.closePath();\n        context.fill();\n\n        context.fillStyle = roofColor;\n        this._circle(context, apex, topRadius);\n    }\n\n    simplified(context, center, radius) {\n        this._circle(\n            context, {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            radius\n        );\n    }\n\n    shadow(context, center, radius, topRadius, height, minHeight) {\n        var c = {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            apex = shadows.project(c, height),\n            p1,\n            p2;\n\n        if (minHeight) {\n            c = shadows.project(c, minHeight);\n        }\n\n        // common tangents for ground and roof circle\n        var tangents = this._tangents(c, radius, apex, topRadius);\n\n        // TODO: no tangents? roof overlaps everything near cam position\n        if (tangents) {\n            p1 = atan2(tangents[0].y1 - c.y, tangents[0].x1 - c.x);\n            p2 = atan2(tangents[1].y1 - c.y, tangents[1].x1 - c.x);\n            context.moveTo(tangents[1].x2, tangents[1].y2);\n            context.arc(apex.x, apex.y, topRadius, p2, p1);\n            context.arc(c.x, c.y, radius, p1, p2);\n        } else {\n            context.moveTo(c.x + radius, c.y);\n            context.arc(c.x, c.y, radius, 0, 2 * PI);\n        }\n    }\n\n    shadowMask(context, center, radius) {\n        var c = {\n            x: center.x - ORIGIN_X,\n            y: center.y - ORIGIN_Y\n        };\n        context.moveTo(c.x + radius, c.y);\n        context.arc(c.x, c.y, radius, 0, PI * 2);\n    }\n\n    hitArea(context, center, radius, topRadius, height, minHeight, color) {\n        var c = {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            scale = CAM_Z / (CAM_Z - height),\n            minScale = CAM_Z / (CAM_Z - minHeight),\n            apex = buildings.project(c, scale),\n            p1,\n            p2;\n\n        topRadius *= scale;\n\n        if (minHeight) {\n            c = buildings.project(c, minScale);\n            radius = radius * minScale;\n        }\n\n        // common tangents for ground and roof circle\n        var tangents = this._tangents(c, radius, apex, topRadius);\n\n        context.fillStyle = color;\n        context.beginPath();\n\n        // TODO: no tangents? roof overlaps everything near cam position\n        if (tangents) {\n            p1 = atan2(tangents[0].y1 - c.y, tangents[0].x1 - c.x);\n            p2 = atan2(tangents[1].y1 - c.y, tangents[1].x1 - c.x);\n            context.moveTo(tangents[1].x2, tangents[1].y2);\n            context.arc(apex.x, apex.y, topRadius, p2, p1);\n            context.arc(c.x, c.y, radius, p1, p2);\n        } else {\n            context.moveTo(c.x + radius, c.y);\n            context.arc(c.x, c.y, radius, 0, 2 * PI);\n        }\n\n        context.closePath();\n        context.fill();\n    }\n\n    _circle(context, center, radius) {\n        context.beginPath();\n        context.arc(center.x, center.y, radius, 0, PI * 2);\n        context.stroke();\n        context.fill();\n    }\n\n    // http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Tangents_between_two_circles\n    _tangents(c1, r1, c2, r2) {\n        var dx = c1.x - c2.x,\n            dy = c1.y - c2.y,\n            dr = r1 - r2,\n            sqdist = dx * dx + dy * dy;\n\n        if (sqdist <= dr * dr) {\n            return;\n        }\n\n        var dist = sqrt(sqdist),\n            vx = -dx / dist,\n            vy = -dy / dist,\n            c = dr / dist,\n            res = [],\n            h,\n            nx,\n            ny;\n\n        // Let A, B be the centers, and C, D be points at which the tangent\n        // touches first and second circle, and n be the normal vector to it.\n        //\n        // We have the system:\n        //   n * n = 1    (n is a unit vector)\n        //   C = A + r1 * n\n        //   D = B + r2 * n\n        //   n * CD = 0   (common orthogonality)\n        //\n        // n * CD = n * (AB + r2*n - r1*n) = AB*n - (r1 -/+ r2) = 0,  <=>\n        // AB * n = (r1 -/+ r2), <=>\n        // v * n = (r1 -/+ r2) / d,  where v = AB/|AB| = AB/d\n        // This is a linear equation in unknown vector n.\n        // Now we're just intersecting a line with a circle: v*n=c, n*n=1\n\n        h = sqrt(max(0, 1 - c * c));\n        for (var sign = 1; sign >= -1; sign -= 2) {\n            nx = vx * c - sign * h * vy;\n            ny = vy * c + sign * h * vx;\n            res.push({\n                x1: (c1.x + r1 * nx) << 0,\n                y1: (c1.y + r1 * ny) << 0,\n                x2: (c2.x + r2 * nx) << 0,\n                y2: (c2.y + r2 * ny) << 0\n            });\n        }\n\n        return res;\n    }\n}\n\n//****** file: Data.js ******\nclass Data {\n    constructor() {\n\t\tthis.geoJSON = new GeoJSON();\n        shadows.setData(this);\n        simplified.setData(this);\n        buildings.setData(this);\n        hitAreas.setData(this);\n        this.DATA_SRC =\n            \"https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json\";\n        this.animTimer;\n        // Static variables\n        this.loadedItems = {}; // maintain a list of cached items in order to avoid duplicates on tile borders\n        this.items = [];\n        this.request = new Request();\n\n    }\n\n    getPixelFootprint(buffer) {\n        var footprint = new Int32Array(buffer.length),\n            px;\n\n        for (var i = 0, il = buffer.length - 1; i < il; i += 2) {\n            px = Functions.geoToPixel(buffer[i], buffer[i + 1]);\n            footprint[i] = px.x;\n            footprint[i + 1] = px.y;\n        }\n\n        footprint = Geometry.simplifyPolygon(footprint);\n        if (footprint.length < 8) {\n            // 3 points & end==start (*2)\n            return;\n        }\n\n        return footprint;\n    }\n\n    getItems() {\n        return this.items;\n    }\n\n    resetItems() {\n        this.items = [];\n        this.loadedItems = {};\n        hitAreas.reset();\n    }\n\n    fadeIn() {\n        if (this.animTimer) {\n            return;\n        }\n\n        var scope = this;\n        this.animTimer = setInterval(function() {\n            var dataItems = scope.items;\n            var isNeeded = false;\n\n            for (var i = 0, il = dataItems.length; i < il; i++) {\n                if (dataItems[i].scale < 1) {\n                    dataItems[i].scale += 0.5 * 0.2; // amount*easing\n                    if (dataItems[i].scale > 1) {\n                        dataItems[i].scale = 1;\n                    }\n                    isNeeded = true;\n                }\n            }\n\n            requestAnimFrame(function() {\n                shadows.render();\n                simplified.render();\n                hitAreas.render();\n                buildings.render();\n            });\n\n            if (!isNeeded) {\n                clearInterval(this.animTimer);\n                this.animTimer = null;\n            }\n        }, 33);\n    }\n\n    addRenderItems(data, allAreNew) {\n        var item, scaledItem, id;\n\n        var geojson = this.geoJSON.read(data);\n\n        for (var i = 0, il = geojson.length; i < il; i++) {\n            item = geojson[i];\n            id =\n                item.id || [\n                    item.footprint[0],\n                    item.footprint[1],\n                    item.height,\n                    item.minHeight\n                ].join(\",\");\n            if (!this.loadedItems[id]) {\n                if ((scaledItem = this.scale(item))) {\n                    scaledItem.scale = allAreNew ? 0 : 1;\n                    this.items.push(scaledItem);\n                    this.loadedItems[id] = 1;\n                }\n            }\n        }\n        this.fadeIn();\n    }\n\n    scale(item) {\n        var res = {},\n            // TODO: calculate this on zoom change only\n            zoomScale = 6 / pow(2, ZOOM - MIN_ZOOM); // TODO: consider using HEIGHT / (global.devicePixelRatio || 1)\n\n        if (item.id) {\n            res.id = item.id;\n        }\n\n        res.height = min(item.height / zoomScale, MAX_HEIGHT);\n\t\tres.realHeight = item.height;\n\n        res.minHeight = isNaN(item.minHeight) ? 0 : item.minHeight / zoomScale;\n        if (res.minHeight > MAX_HEIGHT) {\n            return;\n        }\n\n        res.footprint = this.getPixelFootprint(item.footprint);\n        if (!res.footprint) {\n            return;\n        }\n        res.center = Geometry.getCenter(res.footprint);\n\n        if (item.radius) {\n            res.radius = item.radius * PIXEL_PER_DEG;\n        }\n        if (item.shape) {\n            res.shape = item.shape;\n        }\n        if (item.roofShape) {\n            res.roofShape = item.roofShape;\n        }\n        if (\n            (res.roofShape === \"cone\" || res.roofShape === \"dome\") &&\n            !res.shape &&\n            Geometry.isRotational(res.footprint)\n        ) {\n            res.shape = \"cylinder\";\n        }\n\n        if (item.holes) {\n            res.holes = [];\n            var innerFootprint;\n            for (var i = 0, il = item.holes.length; i < il; i++) {\n                // TODO: simplify\n                if ((innerFootprint = this.getPixelFootprint(item.holes[i]))) {\n                    res.holes.push(innerFootprint);\n                }\n            }\n        }\n\n        var color;\n\n        if (item.wallColor) {\n            if ((color = Color.parse(item.wallColor))) {\n                color = color.alpha(ZOOM_FACTOR);\n                res.altColor = \"\" + color.lightness(0.8);\n                res.wallColor = \"\" + color;\n            }\n        }\n\n        if (item.roofColor) {\n            if ((color = Color.parse(item.roofColor))) {\n                res.roofColor = \"\" + color.alpha(ZOOM_FACTOR);\n            }\n        }\n\n        if (item.relationId) {\n            res.relationId = item.relationId;\n        }\n        res.hitColor = hitAreas.idToColor(item.relationId || item.id);\n\n        res.roofHeight = isNaN(item.roofHeight) ? 0 : item.roofHeight / zoomScale;\n\n        if (res.height + res.roofHeight <= res.minHeight) {\n            return;\n        }\n\n        return res;\n    }\n\n    set(data) {\n\t\t// Make sure valid json\n\t\ttry\n        {\n            JSON.parse(data);\n        }\n        catch (e)\n        { \n            return;\n        }\n        this.isStatic = true;\n        this.resetItems();\n        this._staticData = data;\n        this.addRenderItems(this._staticData, true);\n    }\n\n    load(src, key) {\n        this.src = src || this.DATA_SRC.replace(\"{k}\", key || \"anonymous\");\n        this.update();\n    }\n\n    update() {\n        this.resetItems();\n\n        if (ZOOM < MIN_ZOOM) {\n            return;\n        }\n\n        if (this.isStatic && this._staticData) {\n            this.addRenderItems(this._staticData);\n            return;\n        }\n\n        if (!this.src) {\n            return;\n        }\n\n        var tileZoom = 16,\n            tileSize = 256,\n            zoomedTileSize =\n            ZOOM > tileZoom ?\n            tileSize << (ZOOM - tileZoom) :\n            tileSize >> (tileZoom - ZOOM),\n            minX = (ORIGIN_X / zoomedTileSize) << 0,\n            minY = (ORIGIN_Y / zoomedTileSize) << 0,\n            maxX = ceil((ORIGIN_X + WIDTH) / zoomedTileSize),\n            maxY = ceil((ORIGIN_Y + HEIGHT) / zoomedTileSize),\n            x,\n            y;\n\n        var scope = this;\n\n        function callback(json) {\n            scope.addRenderItems(json);\n        }\n\n        for (y = minY; y <= maxY; y++) {\n            for (x = minX; x <= maxX; x++) {\n                this.loadTile(x, y, tileZoom, callback);\n            }\n        }\n    }\n\n    loadTile(x, y, zoom, callback) {\n        var s = \"abcd\" [(x + y) % 4];\n        var url = this.src\n            .replace(\"{s}\", s)\n            .replace(\"{x}\", x)\n            .replace(\"{y}\", y)\n            .replace(\"{z}\", zoom);\n        return this.request.loadJSON(url, callback);\n    }\n}\n\n//****** file: Debug.js ******\nclass Debug {\n    constructor() {}\n\n    point(x, y, color, size) {\n        var context = this.context;\n        context.fillStyle = color || \"#ffcc00\";\n        context.beginPath();\n        context.arc(x, y, size || 3, 0, 2 * PI);\n        context.closePath();\n        context.fill();\n    }\n\n    line(ax, ay, bx, by, color) {\n        var context = this.context;\n        context.strokeStyle = color || \"#ffcc00\";\n        context.beginPath();\n        context.moveTo(ax, ay);\n        context.lineTo(bx, by);\n        context.closePath();\n        context.stroke();\n    }\n}\n\n//****** file: functions.js ******\nclass Functions {\n    static rad(deg) {\n        return (deg * PI) / 180;\n    }\n\n    static deg(rad) {\n        return (rad / PI) * 180;\n    }\n\n    static pixelToGeo(x, y) {\n        var res = {};\n        x /= MAP_SIZE;\n        y /= MAP_SIZE;\n        res[LAT] =\n            y <= 0 ?\n            90 :\n            y >= 1 ?\n            -90 :\n            Functions.deg(2 * atan(exp(PI * (1 - 2 * y))) - HALF_PI);\n        res[LON] = (x === 1 ? 1 : ((x % 1) + 1) % 1) * 360 - 180;\n        return res;\n    }\n\n    static geoToPixel(lat, lon) {\n        var latitude = min(\n                1,\n                max(0, 0.5 - log(tan(QUARTER_PI + (HALF_PI * lat) / 180)) / PI / 2)\n            ),\n            longitude = lon / 360 + 0.5;\n        return {\n            x: (longitude * MAP_SIZE) << 0,\n            y: (latitude * MAP_SIZE) << 0\n        };\n    }\n\n    static fromRange(sVal, sMin, sMax, dMin, dMax) {\n        sVal = min(max(sVal, sMin), sMax);\n        var rel = (sVal - sMin) / (sMax - sMin),\n            range = dMax - dMin;\n        return min(max(dMin + rel * range, dMin), dMax);\n    }\n\n    static isVisible(polygon) {\n        var maxX = WIDTH + ORIGIN_X,\n            maxY = HEIGHT + ORIGIN_Y;\n\n        // TODO: checking footprint is sufficient for visibility - NOT VALID FOR SHADOWS!\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            if (\n                polygon[i] > ORIGIN_X &&\n                polygon[i] < maxX &&\n                polygon[i + 1] > ORIGIN_Y &&\n                polygon[i + 1] < maxY\n            ) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n\n//****** file: GeoJSON.js ******\nclass GeoJSON {\n    constructor() {\n        this.METERS_PER_LEVEL = 3;\n\n        this.materialColors = {\n            brick: \"#cc7755\",\n            bronze: \"#ffeecc\",\n            canvas: \"#fff8f0\",\n            concrete: \"#999999\",\n            copper: \"#a0e0d0\",\n            glass: \"#e8f8f8\",\n            gold: \"#ffcc00\",\n            plants: \"#009933\",\n            metal: \"#aaaaaa\",\n            panel: \"#fff8f0\",\n            plaster: \"#999999\",\n            roof_tiles: \"#f08060\",\n            silver: \"#cccccc\",\n            slate: \"#666666\",\n            stone: \"#996666\",\n            tar_paper: \"#333333\",\n            wood: \"#deb887\"\n        };\n\n        this.baseMaterials = {\n            asphalt: \"tar_paper\",\n            bitumen: \"tar_paper\",\n            block: \"stone\",\n            bricks: \"brick\",\n            glas: \"glass\",\n            glassfront: \"glass\",\n            grass: \"plants\",\n            masonry: \"stone\",\n            granite: \"stone\",\n            panels: \"panel\",\n            paving_stones: \"stone\",\n            plastered: \"plaster\",\n            rooftiles: \"roof_tiles\",\n            roofingfelt: \"tar_paper\",\n            sandstone: \"stone\",\n            sheet: \"canvas\",\n            sheets: \"canvas\",\n            shingle: \"tar_paper\",\n            shingles: \"tar_paper\",\n            slates: \"slate\",\n            steel: \"metal\",\n            tar: \"tar_paper\",\n            tent: \"canvas\",\n            thatch: \"plants\",\n            tile: \"roof_tiles\",\n            tiles: \"roof_tiles\"\n        };\n\n        this.WINDING_CLOCKWISE = \"CW\";\n        this.WINDING_COUNTER_CLOCKWISE = \"CCW\";\n        // cardboard\n        // eternit\n        // limestone\n        // straw\n    }\n\n    getMaterialColor(str) {\n        str = str.toLowerCase();\n        if (str[0] === \"#\") {\n            return str;\n        }\n        return this.materialColors[this.baseMaterials[str] || str] || null;\n    }\n\n    // detect winding direction: clockwise or counter clockwise\n    getWinding(points) {\n        var x1,\n            y1,\n            x2,\n            y2,\n            a = 0,\n            i,\n            il;\n        for (i = 0, il = points.length - 3; i < il; i += 2) {\n            x1 = points[i];\n            y1 = points[i + 1];\n            x2 = points[i + 2];\n            y2 = points[i + 3];\n            a += x1 * y2 - x2 * y1;\n        }\n        return a / 2 > 0 ? this.WINDING_CLOCKWISE : this.WINDING_COUNTER_CLOCKWISE;\n    }\n\n    // enforce a polygon winding direcetion. Needed for proper backface culling.\n    makeWinding(points, direction) {\n        var winding = this.getWinding(points);\n        if (winding === direction) {\n            return points;\n        }\n        var revPoints = [];\n        for (var i = points.length - 2; i >= 0; i -= 2) {\n            revPoints.push(points[i], points[i + 1]);\n        }\n        return revPoints;\n    }\n\n    alignProperties(prop) {\n        var item = {};\n\n        prop = prop || {};\n\n        item.height =\n            prop.height ||\n            (prop.levels ? prop.levels * this.METERS_PER_LEVEL : DEFAULT_HEIGHT);\n        item.minHeight =\n            prop.minHeight || (prop.minLevel ? prop.minLevel * this.METERS_PER_LEVEL : 0);\n\n        var wallColor = prop.material ?\n            this.getMaterialColor(prop.material) :\n            prop.wallColor || prop.color;\n        if (wallColor) {\n            item.wallColor = wallColor;\n        }\n\n        var roofColor = prop.roofMaterial ?\n            this.getMaterialColor(prop.roofMaterial) :\n            prop.roofColor;\n        if (roofColor) {\n            item.roofColor = roofColor;\n        }\n\n        switch (prop.shape) {\n            case \"cylinder\":\n            case \"cone\":\n            case \"dome\":\n            case \"sphere\":\n                item.shape = prop.shape;\n                item.isRotational = true;\n                break;\n\n            case \"pyramid\":\n                item.shape = prop.shape;\n                break;\n        }\n\n        switch (prop.roofShape) {\n            case \"cone\":\n            case \"dome\":\n                item.roofShape = prop.roofShape;\n                item.isRotational = true;\n                break;\n\n            case \"pyramid\":\n                item.roofShape = prop.roofShape;\n                break;\n        }\n\n        if (item.roofShape && prop.roofHeight) {\n            item.roofHeight = prop.roofHeight;\n            item.height = max(0, item.height - item.roofHeight);\n        } else {\n            item.roofHeight = 0;\n        }\n\n        return item;\n    }\n\n    getGeometries(geometry) {\n        var i,\n            il,\n            polygon,\n            geometries = [],\n            sub;\n\n        switch (geometry.type) {\n            case \"GeometryCollection\":\n                geometries = [];\n                for (i = 0, il = geometry.geometries.length; i < il; i++) {\n                    if ((sub = getGeometries(geometry.geometries[i]))) {\n                        geometries.push.apply(geometries, sub);\n                    }\n                }\n                return geometries;\n\n            case \"MultiPolygon\":\n                geometries = [];\n                for (i = 0, il = geometry.coordinates.length; i < il; i++) {\n                    if (\n                        (sub = getGeometries({\n                            type: \"Polygon\",\n                            coordinates: geometry.coordinates[i]\n                        }))\n                    ) {\n                        geometries.push.apply(geometries, sub);\n                    }\n                }\n                return geometries;\n\n            case \"Polygon\":\n                polygon = geometry.coordinates;\n                break;\n\n            default:\n                return [];\n        }\n\n        var j,\n            jl,\n            p,\n            lat = 1,\n            lon = 0,\n            outer = [],\n            inner = [];\n\n        p = polygon[0];\n        for (i = 0, il = p.length; i < il; i++) {\n            outer.push(p[i][lat], p[i][lon]);\n        }\n        outer = this.makeWinding(outer, this.WINDING_CLOCKWISE);\n\n        for (i = 0, il = polygon.length - 1; i < il; i++) {\n            p = polygon[i + 1];\n            inner[i] = [];\n            for (j = 0, jl = p.length; j < jl; j++) {\n                inner[i].push(p[j][lat], p[j][lon]);\n            }\n            inner[i] = this.makeWinding(inner[i], this.WINDING_COUNTER_CLOCKWISE);\n        }\n\n        return [{\n            outer: outer,\n            inner: inner.length ? inner : null\n        }];\n    }\n\n    clone(obj) {\n        var res = {};\n        for (var p in obj) {\n            if (obj.hasOwnProperty(p)) {\n                res[p] = obj[p];\n            }\n        }\n        return res;\n    }\n\n    read(geojson) {\n        if (!geojson || geojson.type !== \"FeatureCollection\") {\n            return [];\n        }\n\n        var collection = geojson.features,\n            i,\n            il,\n            j,\n            jl,\n            res = [],\n            feature,\n            geometries,\n            baseItem,\n            item;\n\n        for (i = 0, il = collection.length; i < il; i++) {\n            feature = collection[i];\n\n            // TODO review this commented out code\n            if (feature.type !== \"Feature\") { // || onEach(feature) === false) {\n                continue;\n            }\n\n            baseItem = this.alignProperties(feature.properties);\n            geometries = this.getGeometries(feature.geometry);\n\n            for (j = 0, jl = geometries.length; j < jl; j++) {\n                item = this.clone(baseItem);\n                item.footprint = geometries[j].outer;\n                if (item.isRotational) {\n                    item.radius = Geometry.getLonDelta(item.footprint);\n                }\n\n                if (geometries[j].inner) {\n                    item.holes = geometries[j].inner;\n                }\n                if (feature.id || feature.properties.id) {\n                    item.id = feature.id || feature.properties.id;\n                }\n\n                if (feature.properties.relationId) {\n                    item.relationId = feature.properties.relationId;\n                }\n\n                res.push(item); // TODO: clone base properties!\n            }\n        }\n\n        return res;\n    }\n}\n\n//****** file: geometry.js ******\nclass Geometry {\n    static getDistance(p1, p2) {\n        var dx = p1.x - p2.x,\n            dy = p1.y - p2.y;\n        return dx * dx + dy * dy;\n    }\n\n    static isRotational(polygon) {\n        var length = polygon.length;\n        if (length < 16) {\n            return false;\n        }\n\n        var i;\n\n        var minX = Infinity,\n            maxX = -Infinity,\n            minY = Infinity,\n            maxY = -Infinity;\n        for (i = 0; i < length - 1; i += 2) {\n            minX = Math.min(minX, polygon[i]);\n            maxX = Math.max(maxX, polygon[i]);\n            minY = Math.min(minY, polygon[i + 1]);\n            maxY = Math.max(maxY, polygon[i + 1]);\n        }\n\n        var width = maxX - minX,\n            height = maxY - minY,\n            ratio = width / height;\n\n        if (ratio < 0.85 || ratio > 1.15) {\n            return false;\n        }\n\n        var center = {\n                x: minX + width / 2,\n                y: minY + height / 2\n            },\n            radius = (width + height) / 4,\n            sqRadius = radius * radius;\n\n        for (i = 0; i < length - 1; i += 2) {\n            var dist = Geometry.getDistance({\n                x: polygon[i],\n                y: polygon[i + 1]\n            }, center);\n            if (dist / sqRadius < 0.8 || dist / sqRadius > 1.2) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    static getSquareSegmentDistance(px, py, p1x, p1y, p2x, p2y) {\n        var dx = p2x - p1x,\n            dy = p2y - p1y,\n            t;\n        if (dx !== 0 || dy !== 0) {\n            t = ((px - p1x) * dx + (py - p1y) * dy) / (dx * dx + dy * dy);\n            if (t > 1) {\n                p1x = p2x;\n                p1y = p2y;\n            } else if (t > 0) {\n                p1x += dx * t;\n                p1y += dy * t;\n            }\n        }\n        dx = px - p1x;\n        dy = py - p1y;\n        return dx * dx + dy * dy;\n    }\n\n    static simplifyPolygon(buffer) {\n        var sqTolerance = 2,\n            len = buffer.length / 2,\n            markers = new Uint8Array(len),\n            first = 0,\n            last = len - 1,\n            i,\n            maxSqDist,\n            sqDist,\n            index,\n            firstStack = [],\n            lastStack = [],\n            newBuffer = [];\n\n        markers[first] = markers[last] = 1;\n\n        while (last) {\n            maxSqDist = 0;\n            for (i = first + 1; i < last; i++) {\n                sqDist = Geometry.getSquareSegmentDistance(\n                    buffer[i * 2],\n                    buffer[i * 2 + 1],\n                    buffer[first * 2],\n                    buffer[first * 2 + 1],\n                    buffer[last * 2],\n                    buffer[last * 2 + 1]\n                );\n                if (sqDist > maxSqDist) {\n                    index = i;\n                    maxSqDist = sqDist;\n                }\n            }\n\n            if (maxSqDist > sqTolerance) {\n                markers[index] = 1;\n\n                firstStack.push(first);\n                lastStack.push(index);\n\n                firstStack.push(index);\n                lastStack.push(last);\n            }\n\n            first = firstStack.pop();\n            last = lastStack.pop();\n        }\n\n        for (i = 0; i < len; i++) {\n            if (markers[i]) {\n                newBuffer.push(buffer[i * 2], buffer[i * 2 + 1]);\n            }\n        }\n\n        return newBuffer;\n    }\n\n    static getCenter(footprint) {\n        var minX = Infinity,\n            maxX = -Infinity,\n            minY = Infinity,\n            maxY = -Infinity;\n        for (var i = 0, il = footprint.length - 3; i < il; i += 2) {\n            minX = min(minX, footprint[i]);\n            maxX = max(maxX, footprint[i]);\n            minY = min(minY, footprint[i + 1]);\n            maxY = max(maxY, footprint[i + 1]);\n        }\n        return {\n            x: (minX + (maxX - minX) / 2) << 0,\n            y: (minY + (maxY - minY) / 2) << 0\n        };\n    }\n\n    static getLonDelta(footprint) {\n        var minLon = 180,\n            maxLon = -180;\n        for (var i = 0, il = footprint.length; i < il; i += 2) {\n            minLon = min(minLon, footprint[i + 1]);\n            maxLon = max(maxLon, footprint[i + 1]);\n        }\n        return (maxLon - minLon) / 2;\n    }\n}\n\n//****** file: HitAreas.js ******\nclass HitAreas {\n\n    constructor() {\n        this.data;\n\n        this._idMapping = [null];\n    }\n\n    setData(data) {\n        this.data = data;\n    }\n\n    reset() {\n        this._idMapping = [null];\n    }\n\n    render() {\n        if (this._timer) {\n            return;\n        }\n        var self = this;\n        this._timer = setTimeout(function() {\n            self._timer = null;\n            self._render();\n        }, 500);\n    }\n\n    _render() {\n        var context = this.context;\n\n        context.clearRect(0, 0, WIDTH, HEIGHT);\n\n        // show on high zoom levels only and avoid rendering during zoom\n        if (ZOOM < MIN_ZOOM || isZooming) {\n            return;\n        }\n\n        var item,\n            h,\n            mh,\n            sortCam = {\n                x: CAM_X + ORIGIN_X,\n                y: CAM_Y + ORIGIN_Y\n            },\n            footprint,\n            color,\n            dataItems = this.data.getItems();\n\n        dataItems.sort(function(a, b) {\n            return (\n                a.minHeight - b.minHeight ||\n                Geometry.getDistance(b.center, sortCam) - Geometry.getDistance(a.center, sortCam) ||\n                b.height - a.height\n            );\n        });\n\n        var cylinder = new Cylinder();\n        var pyramid = new Pyramid();\n        var block = new Block();\n        for (var i = 0, il = dataItems.length; i < il; i++) {\n            item = dataItems[i];\n\n            if (!(color = item.hitColor)) {\n                continue;\n            }\n\n            footprint = item.footprint;\n\n            if (!Functions.isVisible(footprint)) {\n                continue;\n            }\n\n            h = item.height;\n\n            mh = 0;\n            if (item.minHeight) {\n                mh = item.minHeight;\n            }\n\n            switch (item.shape) {\n                case \"cylinder\":\n                    cylinder.hitArea(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius,\n                        h,\n                        mh,\n                        color\n                    );\n                    break;\n                case \"cone\":\n                    cylinder.hitArea(context, item.center, item.radius, 0, h, mh, color);\n                    break;\n                case \"dome\":\n                    cylinder.hitArea(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius / 2,\n                        h,\n                        mh,\n                        color\n                    );\n                    break;\n                case \"sphere\":\n                    cylinder.hitArea(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius,\n                        h,\n                        mh,\n                        color\n                    );\n                    break;\n                case \"pyramid\":\n                    pyramid.hitArea(context, footprint, item.center, h, mh, color);\n                    break;\n                default:\n                    block.hitArea(context, footprint, item.holes, h, mh, color);\n            }\n\n            switch (item.roofShape) {\n                case \"cone\":\n                    cylinder.hitArea(\n                        context,\n                        item.center,\n                        item.radius,\n                        0,\n                        h + item.roofHeight,\n                        h,\n                        color\n                    );\n                    break;\n                case \"dome\":\n                    cylinder.hitArea(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius / 2,\n                        h + item.roofHeight,\n                        h,\n                        color\n                    );\n                    break;\n                case \"pyramid\":\n                    pyramid.hitArea(\n                        context,\n                        footprint,\n                        item.center,\n                        h + item.roofHeight,\n                        h,\n                        color\n                    );\n                    break;\n            }\n        }\n\n        // otherwise fails on size 0\n        if (WIDTH && HEIGHT) {\n            this._imageData = this.context.getImageData(0, 0, WIDTH, HEIGHT).data;\n        }\n    }\n\n    getIdFromXY(x, y) {\n        var imageData = this._imageData;\n        if (!imageData) {\n            return;\n        }\n        var pos = 4 * ((y | 0) * WIDTH + (x | 0));\n        var index =\n            imageData[pos] | (imageData[pos + 1] << 8) | (imageData[pos + 2] << 16);\n        return this._idMapping[index];\n    }\n\n    idToColor(id) {\n        var index = this._idMapping.indexOf(id);\n        if (index === -1) {\n            this._idMapping.push(id);\n            index = this._idMapping.length - 1;\n        }\n        var r = index & 0xff;\n        var g = (index >> 8) & 0xff;\n        var b = (index >> 16) & 0xff;\n        return \"rgb(\" + [r, g, b].join(\",\") + \")\";\n    }\n\n    setContext(context) {\n        this.context = context;\n    }\n}\n\n//****** file: Layers.js ******\nclass Layers {\n    constructor() {\n\n        this.container = document.createElement(\"DIV\");\n        this.items = [];\n        this.container.style.pointerEvents = \"none\";\n        this.container.style.position = \"absolute\";\n        this.container.style.left = 0;\n        this.container.style.top = 0;\n\n\n        // TODO: improve this to .setContext(context)\n        shadows.setContext(this.createContext(this.container));\n        simplified.setContext(this.createContext(this.container));\n        buildings.setContext(this.createContext(this.container));\n        hitAreas.setContext(this.createContext());\n        //    Debug.context      = this.createContext(this.container);\n    }\n\n    render(quick) {\n\n        requestAnimFrame(function() {\n            if (!quick) {\n                shadows.render();\n                simplified.render();\n                hitAreas.render();\n            }\n            buildings.render();\n        });\n    }\n\n    createContext(container) {\n        var canvas = document.createElement(\"CANVAS\");\n        canvas.style.transform = \"translate3d(0, 0, 0)\"; // turn on hw acceleration\n        canvas.style.imageRendering = \"optimizeSpeed\";\n        canvas.style.position = \"absolute\";\n        canvas.style.left = 0;\n        canvas.style.top = 0;\n\n        var context = canvas.getContext(\"2d\");\n        context.lineCap = \"round\";\n        context.lineJoin = \"round\";\n        context.lineWidth = 1;\n        context.imageSmoothingEnabled = false;\n\n        this.items.push(canvas);\n        if (container) {\n            container.appendChild(canvas);\n        }\n\n        return context;\n    }\n\n    appendTo(parentNode) {\n        parentNode.appendChild(this.container);\n    }\n\n    remove() {\n        this.container.parentNode.removeChild(this.container);\n    }\n\n    setSize(width, height) {\n        for (var i = 0, il = this.items.length; i < il; i++) {\n            this.items[i].width = width;\n            this.items[i].height = height;\n        }\n    }\n\n    // usually called after move: container jumps by move delta, cam is reset\n    setPosition(x, y) {\n        this.container.style.left = x + \"px\";\n        this.container.style.top = y + \"px\";\n    }\n}\n\n//****** file: Pyramid.js ******\nclass Pyramid {\n    constructor() {}\n\n    draw(context, polygon, center, height, minHeight, color, altColor) {\n        var c = {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            scale = CAM_Z / (CAM_Z - height),\n            minScale = CAM_Z / (CAM_Z - minHeight),\n            apex = buildings.project(c, scale),\n            a = {\n                x: 0,\n                y: 0\n            },\n            b = {\n                x: 0,\n                y: 0\n            };\n\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            a.x = polygon[i] - ORIGIN_X;\n            a.y = polygon[i + 1] - ORIGIN_Y;\n            b.x = polygon[i + 2] - ORIGIN_X;\n            b.y = polygon[i + 3] - ORIGIN_Y;\n\n            if (minHeight) {\n                a = buildings.project(a, minScale);\n                b = buildings.project(b, minScale);\n            }\n\n            // backface culling check\n            if ((b.x - a.x) * (apex.y - a.y) > (apex.x - a.x) * (b.y - a.y)) {\n                // depending on direction, set shading\n                if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) {\n                    context.fillStyle = altColor;\n                } else {\n                    context.fillStyle = color;\n                }\n\n                context.beginPath();\n                this._triangle(context, a, b, apex);\n                context.closePath();\n                context.fill();\n            }\n        }\n    }\n\n    _triangle(context, a, b, c) {\n        context.moveTo(a.x, a.y);\n        context.lineTo(b.x, b.y);\n        context.lineTo(c.x, c.y);\n    }\n\n    _ring(context, polygon) {\n        context.moveTo(polygon[0] - ORIGIN_X, polygon[1] - ORIGIN_Y);\n        for (var i = 2, il = polygon.length - 1; i < il; i += 2) {\n            context.lineTo(polygon[i] - ORIGIN_X, polygon[i + 1] - ORIGIN_Y);\n        }\n    }\n\n    shadow(context, polygon, center, height, minHeight) {\n        var a = {\n                x: 0,\n                y: 0\n            },\n            b = {\n                x: 0,\n                y: 0\n            },\n            c = {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            apex = shadows.project(c, height);\n\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            a.x = polygon[i] - ORIGIN_X;\n            a.y = polygon[i + 1] - ORIGIN_Y;\n            b.x = polygon[i + 2] - ORIGIN_X;\n            b.y = polygon[i + 3] - ORIGIN_Y;\n\n            if (minHeight) {\n                a = shadows.project(a, minHeight);\n                b = shadows.project(b, minHeight);\n            }\n\n            // backface culling check\n            if ((b.x - a.x) * (apex.y - a.y) > (apex.x - a.x) * (b.y - a.y)) {\n                // depending on direction, set shading\n                this._triangle(context, a, b, apex);\n            }\n        }\n    }\n\n    shadowMask(context, polygon) {\n        _ring(context, polygon);\n    }\n\n    hitArea(context, polygon, center, height, minHeight, color) {\n        var c = {\n                x: center.x - ORIGIN_X,\n                y: center.y - ORIGIN_Y\n            },\n            scale = CAM_Z / (CAM_Z - height),\n            minScale = CAM_Z / (CAM_Z - minHeight),\n            apex = buildings.project(c, scale),\n            a = {\n                x: 0,\n                y: 0\n            },\n            b = {\n                x: 0,\n                y: 0\n            };\n\n        context.fillStyle = color;\n        context.beginPath();\n\n        for (var i = 0, il = polygon.length - 3; i < il; i += 2) {\n            a.x = polygon[i] - ORIGIN_X;\n            a.y = polygon[i + 1] - ORIGIN_Y;\n            b.x = polygon[i + 2] - ORIGIN_X;\n            b.y = polygon[i + 3] - ORIGIN_Y;\n\n            if (minHeight) {\n                a = buildings.project(a, minScale);\n                b = buildings.project(b, minScale);\n            }\n\n            // backface culling check\n            if ((b.x - a.x) * (apex.y - a.y) > (apex.x - a.x) * (b.y - a.y)) {\n                this._triangle(context, a, b, apex);\n            }\n        }\n\n        context.closePath();\n        context.fill();\n    }\n}\n\n//****** file: Request.js ******\nclass Request {\n    constructor() {\n        this.cacheData = {};\n        this.cacheIndex = [];\n        this.cacheSize = 0;\n        this.maxCacheSize = 1024 * 1024 * 5; // 5MB\n    }\n\n    xhr(url, callback) {\n\n        if (this.cacheData[url]) {\n            if (callback) {\n                callback(this.cacheData[url]);\n            }\n            return;\n        }\n\n        var req = new XMLHttpRequest();\n        var scope = this;\n        req.onreadystatechange = function() {\n\n            if (req.readyState !== 4) {\n                return;\n            }\n            if (!req.status || req.status < 200 || req.status > 299) {\n                return;\n            }\n            if (callback && req.responseText) {\n                var responseText = req.responseText;\n                scope.cacheData[url] = responseText;\n                scope.cacheIndex.push({\n                    url: url,\n                    size: responseText.length\n                });\n                scope.cacheSize += responseText.length;\n\n                callback(responseText);\n\n                while (scope.cacheSize > scope.maxCacheSize) {\n                    var item = scope.cacheIndex.shift();\n                    scope.cacheSize -= item.size;\n                    delete scope.cacheData[item.url];\n                }\n            }\n        };\n\n        req.open(\"GET\", url);\n        req.send(null);\n\n        return req;\n    }\n\n    loadJSON(url, callback) {\n        return this.xhr(url, function(responseText) {\n            var json;\n            try {\n                json = JSON.parse(responseText);\n            } catch (ex) {}\n            callback(json);\n        });\n    }\n}\n\n//****** file: Shadows.js ******\nclass Shadows {\n    constructor() {\n\t\tthis.sunPosition = new SunPosition();\n        this.enabled = true;\n        this.color = \"#666666\";\n        this.blurColor = \"#000000\";\n        this.blurSize = 15;\n        this.date = new Date();\n        this.direction = {\n            x: 0,\n            y: 0\n        };\n        this.context;\n        this.data;\n    }\n\n    project(p, h) {\n        return {\n            x: p.x + this.direction.x * h,\n            y: p.y + this.direction.y * h\n        };\n    }\n\n    setData(data) {\n        this.data = data;\n    }\n\n    render() {\n        var context = this.context,\n            screenCenter,\n            sun,\n            length,\n            alpha;\n\n        context.clearRect(0, 0, WIDTH, HEIGHT);\n\n        // show on high zoom levels only and avoid rendering during zoom\n        if (!this.enabled || ZOOM < MIN_ZOOM || isZooming) {\n            return;\n        }\n\n        // TODO: calculate this just on demand\n        screenCenter = Functions.pixelToGeo(\n            CENTER_X + ORIGIN_X,\n            CENTER_Y + ORIGIN_Y\n        );\n        sun = this.sunPosition.getSunPosition(\n            this.date,\n            screenCenter.latitude,\n            screenCenter.longitude\n        );\n\n        if (sun.altitude <= 0) {\n            return;\n        }\n\n        length = 1 / tan(sun.altitude);\n        alpha = length < 5 ? 0.75 : (1 / length) * 5;\n\n        this.direction.x = cos(sun.azimuth) * length;\n        this.direction.y = sin(sun.azimuth) * length;\n\n        var i,\n            il,\n            item,\n            h,\n            mh,\n            footprint,\n            dataItems = this.data.getItems();\n\n        context.canvas.style.opacity = alpha / (ZOOM_FACTOR * 2);\n        context.shadowColor = this.blurColor;\n        context.shadowBlur = this.blurSize * (ZOOM_FACTOR / 2);\n        context.fillStyle = this.color;\n        context.beginPath();\n\n        var cylinder = new Cylinder();\n        var pyramid = new Pyramid();\n        var block = new Block();\n        for (i = 0, il = dataItems.length; i < il; i++) {\n            item = dataItems[i];\n\n            footprint = item.footprint;\n\n            if (!Functions.isVisible(footprint)) {\n                continue;\n            }\n\n            // when fading in, use a dynamic height\n            h = item.scale < 1 ? item.height * item.scale : item.height;\n\n            mh = 0;\n            if (item.minHeight) {\n                mh = item.scale < 1 ? item.minHeight * item.scale : item.minHeight;\n            }\n\n            switch (item.shape) {\n                case \"cylinder\":\n                    cylinder.shadow(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius,\n                        h,\n                        mh\n                    );\n                    break;\n                case \"cone\":\n                    cylinder.shadow(context, item.center, item.radius, 0, h, mh);\n                    break;\n                case \"dome\":\n                    cylinder.shadow(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius / 2,\n                        h,\n                        mh\n                    );\n                    break;\n                case \"sphere\":\n                    cylinder.shadow(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius,\n                        h,\n                        mh\n                    );\n                    break;\n                case \"pyramid\":\n                    pyramid.shadow(context, footprint, item.center, h, mh);\n                    break;\n                default:\n                    block.shadow(context, footprint, item.holes, h, mh);\n            }\n\n            switch (item.roofShape) {\n                case \"cone\":\n                    cylinder.shadow(\n                        context,\n                        item.center,\n                        item.radius,\n                        0,\n                        h + item.roofHeight,\n                        h\n                    );\n                    break;\n                case \"dome\":\n                    cylinder.shadow(\n                        context,\n                        item.center,\n                        item.radius,\n                        item.radius / 2,\n                        h + item.roofHeight,\n                        h\n                    );\n                    break;\n                case \"pyramid\":\n                    pyramid.shadow(\n                        context,\n                        footprint,\n                        item.center,\n                        h + item.roofHeight,\n                        h\n                    );\n                    break;\n            }\n        }\n\n        context.closePath();\n        context.fill();\n\n        context.shadowBlur = null;\n\n        // now draw all the footprints as negative clipping mask\n        context.globalCompositeOperation = \"destination-out\";\n        context.beginPath();\n\n        for (i = 0, il = dataItems.length; i < il; i++) {\n            item = dataItems[i];\n\n            footprint = item.footprint;\n\n            if (!Functions.isVisible(footprint)) {\n                continue;\n            }\n\n            // if object is hovered, there is no need to clip it's footprint\n            if (item.minHeight) {\n                continue;\n            }\n\n            switch (item.shape) {\n                case \"cylinder\":\n                case \"cone\":\n                case \"dome\":\n                    cylinder.shadowMask(context, item.center, item.radius);\n                    break;\n                default:\n                    block.shadowMask(context, footprint, item.holes);\n            }\n        }\n\n        context.fillStyle = \"#00ff00\";\n        context.fill();\n        context.globalCompositeOperation = \"source-over\";\n    }\n\n    setContext(context) {\n        this.context = context;\n    }\n}\n\n//****** file: Simplified.js ******\nclass Simplified {\n    constructor() {\n        this.data;\n        this.init();\n    }\n\n    setData(data) {\n        this.data = data;\n    }\n\n    init() {\n        this.maxZoom = this.MIN_ZOOM + 2;\n        this.maxHeight = 5;\n    }\n\n    isSimple(item) {\n        return (\n            ZOOM <= this.maxZoom && item.height + item.roofHeight < this.maxHeight\n        );\n    }\n\n    render() {\n        var context = this.context;\n        context.clearRect(0, 0, WIDTH, HEIGHT);\n\n        // show on high zoom levels only and avoid rendering during zoom\n        if (ZOOM < MIN_ZOOM || isZooming || ZOOM > this.maxZoom) {\n            return;\n        }\n\n        var item,\n            footprint,\n            dataItems = this.data.getItems();\n\n        var cylinder = new Cylinder();\n        var block = new Block();\n        for (var i = 0, il = dataItems.length; i < il; i++) {\n            item = dataItems[i];\n\n            if (item.height >= this.maxHeight) {\n                continue;\n            }\n\n            footprint = item.footprint;\n\n            if (!Functions.isVisible(footprint)) {\n                continue;\n            }\n\n            context.strokeStyle = item.altColor || ALT_COLOR_STR;\n            context.fillStyle = item.roofColor || ROOF_COLOR_STR;\n\n            switch (item.shape) {\n                case \"cylinder\":\n                case \"cone\":\n                case \"dome\":\n                case \"sphere\":\n                    cylinder.simplified(context, item.center, item.radius);\n                    break;\n                default:\n                    block.simplified(context, footprint, item.holes);\n            }\n        }\n    }\n\n    setContext(context) {\n        this.context = context;\n    }\n}\n\n//****** file: SunPosition.js ******\n\n// calculations are based on http://aa.quae.nl/en/reken/zonpositie.html\n// code credits to Vladimir Agafonkin (@mourner)\nclass SunPosition {\n    constructor() {\n        this.init();\n    }\n\n    init() {\n        this.m = Math;\n        (this.PI = m.PI), (this.sin = m.sin);\n        this.cos = m.cos;\n        this.tan = m.tan;\n        this.asin = m.asin;\n        this.atan = m.atan2;\n\n        this.rad = PI / 180;\n        this.dayMs = 1000 * 60 * 60 * 24;\n        this.J1970 = 2440588;\n        this.J2000 = 2451545;\n        this.e = this.rad * 23.4397; // obliquity of the Earth\n    }\n\n    toJulian(date) {\n        return date.valueOf() / this.dayMs - 0.5 + this.J1970;\n    }\n    toDays(date) {\n        return this.toJulian(date) - this.J2000;\n    }\n    getRightAscension(l, b) {\n        return this.atan(\n            this.sin(l) * this.cos(this.e) - this.tan(b) * this.sin(this.e),\n            this.cos(l)\n        );\n    }\n    getDeclination(l, b) {\n        return this.asin(\n            this.sin(b) * this.cos(this.e) +\n            this.cos(b) * this.sin(this.e) * this.sin(l)\n        );\n    }\n    getAzimuth(H, phi, dec) {\n        return this.atan(\n            this.sin(H),\n            this.cos(H) * this.sin(phi) - this.tan(dec) * this.cos(phi)\n        );\n    }\n    getAltitude(H, phi, dec) {\n        return this.asin(\n            this.sin(phi) * this.sin(dec) +\n            this.cos(phi) * this.cos(dec) * this.cos(H)\n        );\n    }\n    getSiderealTime(d, lw) {\n        return this.rad * (280.16 + 360.9856235 * d) - lw;\n    }\n    getSolarMeanAnomaly(d) {\n        return this.rad * (357.5291 + 0.98560028 * d);\n    }\n    getEquationOfCenter(M) {\n        return (\n            this.rad *\n            (1.9148 * this.sin(M) + 0.02 * this.sin(2 * M) + 0.0003 * this.sin(3 * M))\n        );\n    }\n    getEclipticLongitude(M, C) {\n        var P = this.rad * 102.9372; // perihelion of the Earth\n        return M + C + P + this.PI;\n    }\n\n    getSunPosition(date, lat, lon) {\n        var lw = this.rad * -lon,\n            phi = this.rad * lat,\n            d = this.toDays(date),\n            M = this.getSolarMeanAnomaly(d),\n            C = this.getEquationOfCenter(M),\n            L = this.getEclipticLongitude(M, C),\n            D = this.getDeclination(L, 0),\n            A = this.getRightAscension(L, 0),\n            t = this.getSiderealTime(d, lw),\n            H = t - A;\n\n        return {\n            altitude: this.getAltitude(H, phi, D),\n            azimuth: this.getAzimuth(H, phi, D) - this.PI / 2 // origin: north\n        };\n    }\n}\n\n//****** file: prefix.js ******\nexport default class OSMBuildings extends VectorLayer {\n    constructor(map) {\n        super(new VectorSource({\n            projection: olProj.get(\"EPSG:900913\")\n        }));\n        this.map = map;\n        this.maxExtent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]; // MaxExtent of layer\n        try {\n            this.setMap(map);\n            map.addLayer(this);\n\n        } catch (e) {\n            console.log(e);\n        }\n\n        //****** file: variables.js ******\n        this.VERSION = \"0.2.2b\";\n        this.ATTRIBUTION =\n            '&copy; <a href=\"http://osmbuildings.org\">OSM Buildings</a>';\n    }\n\n    //****** file: adapter.js ******\n\n    setGlobalOrigin(origin) {\n        ORIGIN_X = origin.x;\n        ORIGIN_Y = origin.y;\n    }\n\n    moveCam(offset) {\n        CAM_X = CENTER_X + offset.x;\n        CAM_Y = HEIGHT + offset.y;\n        layers.render(true);\n    }\n\n    setSize(size) {\n        WIDTH = size.width;\n        HEIGHT = size.height;\n        CENTER_X = (WIDTH / 2) << 0;\n        CENTER_Y = (HEIGHT / 2) << 0;\n\n        CAM_X = CENTER_X;\n        CAM_Y = HEIGHT;\n\n        layers.setSize(WIDTH, HEIGHT);\n        MAX_HEIGHT = CAM_Z - 50;\n    }\n\n    setZoom(z) {\n        ZOOM = z;\n        MAP_SIZE = MAP_TILE_SIZE << ZOOM;\n\n        var center = Functions.pixelToGeo(ORIGIN_X + CENTER_X, ORIGIN_Y + CENTER_Y);\n        var a = Functions.geoToPixel(center.latitude, 0);\n        var b = Functions.geoToPixel(center.latitude, 1);\n        PIXEL_PER_DEG = b.x - a.x;\n\n        ZOOM_FACTOR = pow(0.95, ZOOM - MIN_ZOOM);\n\n        WALL_COLOR_STR = \"\" + WALL_COLOR.alpha(ZOOM_FACTOR);\n        ALT_COLOR_STR = \"\" + ALT_COLOR.alpha(ZOOM_FACTOR);\n        ROOF_COLOR_STR = \"\" + ROOF_COLOR.alpha(ZOOM_FACTOR);\n    }\n\n    onResize(e) {\n        setSize(e);\n        layers.render();\n        data.update();\n    }\n\n    onMoveEnd(e) {\n        layers.render();\n        data.update(); // => fadeIn() => layers.render()\n    }\n\n    onZoomStart() {\n        isZooming = true;\n        // effectively clears because of isZooming flag\n        // TODO: introduce explicit clear()\n        layers.render();\n    }\n\n    onZoomEnd(e) {\n        isZooming = false;\n        setZoom(e.zoom);\n        data.update(); // => fadeIn()\n        layers.render();\n    }\n\n    setOrigin() {\n        //console.log(\"setOrigin\");\n        var map = this.map;\n        try {\n            var origin = map.getCoordinateFromPixel([0, 0]);\n            var res = map.getView().getResolution();\n            var ext = this.maxExtent;\n            var x = ((origin[0] - ext[0]) / res) << 0;\n            var y = ((ext[3] - origin[1]) / res) << 0;\n            this.setGlobalOrigin({\n                x: x,\n                y: y\n            });\n        } catch (e) {\n            console.log(e);\n        }\n    };\n\n    setMap(map) {\n        //console.log(\"setMap\");\n        var scope = this;\n        layers.appendTo(document.getElementById(map.getTargetElement().id));\n        this.setSize({\n            width: map.getSize()[0],\n            height: map.getSize()[1]\n        });\n\n        var layerProjection = map.getView().getProjection();\n        map.on(\"click\", function(e) {\n            var id = hitAreas.getIdFromXY(e.pixel[0], e.pixel[1]);\n            if (id) {\n                var geo = olProj.transform(\n                    map.getCoordinateFromPixel([e.pixel[0], e.pixel[1]]),\n                    layerProjection,\n                    map.getView().getProjection()\n                );\n                scope.onClick({\n                    feature: id,\n                    lat: geo[0],\n                    lon: geo[1]\n                });\n            }\n        });\n\n        // map.on('moveend', scope.onMoveEnd);\n        // map.on('zoomend', scope.onZoomStart);\n        // map.on('zoomstart', scope.onZoomEnd);\n\n        // TODO why doesn't scope.on work like in OL3\n        map.on(\"precompose\", function(e) {\n            //console.log(\"precompose\");\n            scope.setZoom(map.getView().getZoom());\n            scope.setOrigin();\n            data.resetItems();\n            data.update();\n        });\n\n    }\n\n    //****** file: public.js ******\n\n    style = function(style) {\n        //console.log(\"style\");\n        style = style || {};\n        var color;\n        if ((color = style.color || style.wallColor)) {\n            WALL_COLOR = Color.parse(color);\n            WALL_COLOR_STR = \"\" + WALL_COLOR.alpha(ZOOM_FACTOR);\n\n            ALT_COLOR = WALL_COLOR.lightness(0.8);\n            ALT_COLOR_STR = \"\" + ALT_COLOR.alpha(ZOOM_FACTOR);\n\n            ROOF_COLOR = WALL_COLOR.lightness(1.2);\n            ROOF_COLOR_STR = \"\" + ROOF_COLOR.alpha(ZOOM_FACTOR);\n        }\n\n        if (style.roofColor) {\n            ROOF_COLOR = Color.parse(style.roofColor);\n            ROOF_COLOR_STR = \"\" + ROOF_COLOR.alpha(ZOOM_FACTOR);\n        }\n\n        if (style.shadows !== undefined) {\n            shadows.enabled = !!style.shadows;\n        }\n\n        layers.render();\n\n        return this;\n    };\n\n    date = function(date) {\n        shadows.date = date;\n        shadows.render();\n        return this;\n    };\n\n    load = function(url) {\n        data.load(url);\n        return this;\n    };\n\n    set(dataToSet) {\n        data.set(dataToSet);\n        return this;\n    };\n\t\n\tgetDataItems(){\n\t\treturn data.getItems();\n\t};\n\n    onEach = function() {};\n    each = function(handler) {\n        this.onEach = function(payload) {\n            return handler(payload);\n        };\n        return this;\n    };\n\n    onClick = function(){};\n    click = function(handler) {\n        this.onClick = function(payload) {\n            return handler(payload);\n        };\n        return this;\n    };\n}\n\n// Global vars\n\nvar PI = Math.PI;\nvar HALF_PI = PI / 2;\nvar QUARTER_PI = PI / 4;\n\nvar DATA_TILE_SIZE = 0.0075; // data tile size in geo coordinates, smaller: less data to load but more requests\nvar ZOOM;\nvar MAP_SIZE;\nvar MAP_TILE_SIZE = 256; // map tile size in pixels\n\nvar MIN_ZOOM = 15;\n\nvar LAT = \"latitude\";\nvar LON = \"longitude\";\n\nvar TRUE = true;\nvar FALSE = false;\n\nvar WIDTH = 0;\nvar HEIGHT = 0;\nvar CENTER_X = 0;\nvar CENTER_Y = 0;\nvar ORIGIN_X = 0;\nvar ORIGIN_Y = 0;\n\nvar WALL_COLOR = Color.parse(\"rgba(200, 190, 180)\");\nvar ALT_COLOR = WALL_COLOR.lightness(0.8);\nvar ROOF_COLOR = WALL_COLOR.lightness(1.2);\n\nvar WALL_COLOR_STR = \"\" + WALL_COLOR;\nvar ALT_COLOR_STR = \"\" + ALT_COLOR;\nvar ROOF_COLOR_STR = \"\" + ROOF_COLOR;\n\nvar PIXEL_PER_DEG = 0;\nvar ZOOM_FACTOR = 1;\n\nvar MAX_HEIGHT; // taller buildings will be cut to this\nvar DEFAULT_HEIGHT = 5;\n\nvar CAM_X;\nvar CAM_Y;\nvar CAM_Z = 450;\n\nvar isZooming;\n\nvar EARTH_RADIUS = 6378137;\n\n//****** file: shortcuts.js ******\n\n// object access shortcuts\nvar m = Math;\nvar exp = m.exp;\nvar log = m.log;\nvar sin = m.sin;\nvar cos = m.cos;\nvar tan = m.tan;\nvar atan = m.atan;\nvar atan2 = m.atan2;\nvar min = m.min;\nvar max = m.max;\nvar sqrt = m.sqrt;\nvar ceil = m.ceil;\nvar floor = m.floor;\nvar round = m.round;\nvar pow = m.pow;\n\n// polyfills\n\nvar Int32Array = Int32Array || Array;\nvar Uint8Array = Uint8Array || Array;\n\nvar IS_IOS = /iP(ad|hone|od)/g.test(navigator.userAgent);\nvar IS_MSIE = !!~navigator.userAgent.indexOf(\"Trident\");\nvar requestAnimFrame =\n    window.requestAnimationFrame && !IS_IOS && !IS_MSIE ?\n    window.requestAnimationFrame :\n    function(callback) {\n        callback();\n    };\n\t\n// Objects use for constructing different aspects of buildings \nvar shadows = new Shadows();\nvar simplified = new Simplified();\nvar buildings = new Buildings();\nvar hitAreas = new HitAreas();\n// Object holds the json data\nvar data = new Data();\n// Renders the buildings and properties (e.g. shadows, etc.)\nvar layers = new Layers();"
  },
  {
    "path": "tests/openlayers-5.3.0/README.md",
    "content": "# OpenLayers 5 OSM Buildings Support\n\n1. Install dependencies from package.json.\n```bash\nnpm install\n```\n2. Run local server for testing live updates at localhost:1234.\n```bash\nnpm run-script start\n```\n3. Build the production bundle. Copy the dist/ folder to your production server.\n```bash\nnpm run-script build\n```\n\n# Example code (See index.js and index.html for full example)\n```javascript\nimport OSMBuildings from './OSMBuildings-OL5.js';\n...\nlet osmBuildings = new OSMBuildings(map);\nosmBuildings.date(new Date(2017, 5, 15, 17, 30))\nosmBuildings.load();\n\nosmBuildings.click(function(e) {\n    let result = osmBuildings.getDataItems().filter(obj => {\n        return obj.id === e.feature\n    })\n    alert(\"Height (m): \" + result[0].realHeight);\n  });\n```\n\n\n\n\n\n"
  },
  {
    "path": "tests/openlayers-5.3.0/index.js",
    "content": "// Map\nimport Map from 'ol/Map.js';\nimport View from 'ol/View.js';\n// Layers\nimport { Tile as TileLayer} from 'ol/layer.js';\n// Sources\nimport OSM from 'ol/source/OSM.js';\n// Controls\nimport { defaults as defaultControls, Control } from 'ol/control.js';\n// Proj\nimport * as olProj from \"ol/proj.js\";\n// OSM Buildings\nimport OSMBuildings from './OSMBuildings-OL5.js';\n\nlet map = new Map({\n  layers: [\n    new TileLayer({\n      source: new OSM()\n    })\n  ],\n  controls: defaultControls({\n    attributionOptions: /** @type {olx.control.AttributionOptions} */ ({\n      collapsible: false\n    })\n  }),\n  target: 'map',\n  view: new View({\n    center: olProj.transform([13.33522, 52.50440], 'EPSG:4326', 'EPSG:3857'),\n    zoom: 16\n  })\n});\n\n// Building example\nlet osmBuildings = new OSMBuildings(map);\nosmBuildings.date(new Date(2017, 5, 15, 17, 30))\nosmBuildings.load();\n\nosmBuildings.click(function(e) {\n    let result = osmBuildings.getDataItems().filter(obj => {\n        return obj.id === e.feature\n    })\n    alert(\"Height (m): \" + result[0].realHeight);\n    //console.log(result);\n  });\n\n"
  },
  {
    "path": "tests/shadows.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<title>OSM Buildings - Shadows</title>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n<style type=\"text/css\">\nhtml, body {\n  border: 0;\n  margin: 0;\n  padding: 0;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n#map {\n  height: 100%;\n}\n</style>\n<link rel=\"stylesheet\" href=\"leaflet-0.7/leaflet.css\">\n<script src=\"leaflet-0.7/leaflet-src.js\"></script>\n<script src=\"loader.js\"></script>\n<style>\n#controls {\n  position:absolute;\n  left:0;\n  bottom:0;\n  width:100%;\n  height:30px;\n  text-align:center;\n  background-color:#ffffff;\n}\n#controls label {\n  font-family:sans-serif;\n  font-size:14px;\n  height: 20px;\n}\n</style>\n</head>\n\n<body>\n<div id=\"map\"></div>\n<div id=\"controls\">\n  <input id=\"date\" type=\"range\" min=\"1\" max=\"12\" step=\"1\"><label for=\"date\"></label>\n  <input id=\"time\" type=\"range\" min=\"0\" max=\"23\" step=\"0.25\"><label for=\"time\"></label>\n</div>\n<script>\nvar map = new L.Map('map').setView([52.52179, 13.39503], 18); // Berlin Bodemuseum\n\nnew L.TileLayer('https://{s}.tiles.mapbox.com/v3/osmbuildings.kbpalbpk/{z}/{x}/{y}.png', { maxNativeZoom: 19, maxZoom: 21 }).addTo(map);\nvar now = new Date(2017, 4, 10, 8, 0);\nvar osmb = new OSMBuildings(map).date(now).load();\n\nfunction changeDate() {\n  var\n    Y = now.getFullYear(),\n    M = now.getMonth(),\n    D = 15,\n    h = now.getHours(),\n    m = now.getMinutes();\n\n  timeRangeLabel.innerText = pad(h) + ':' + pad(m);\n  dateRangeLabel.innerText = Y + '-' + pad(M+1);\n  osmb.date(new Date(Y, M, D, h, m));\n}\n\nfunction pad(v) {\n  return (v < 10 ? '0' : '') + v;\n}\n\nvar date, time;\nvar timeRange = document.querySelector('#time');\nvar dateRange = document.querySelector('#date');\nvar timeRangeLabel = document.querySelector('label[for=time]');\nvar dateRangeLabel = document.querySelector('label[for=date]');\n\nchangeDate();\n\n// init with day of year\nvar Jan1 = new Date(now.getFullYear(), 0, 1);\ndateRange.value = now.getMonth()+1;\n\ntimeRange.value = now.getHours();\n\ntimeRange.addEventListener('change', function() {\n  now.setHours(0);\n  now.setMinutes(this.value*60);\n  changeDate();\n}, false);\n\ndateRange.addEventListener('change', function() {\n  now.setMonth(this.value-1);\n  changeDate();\n}, false);\n</script>\n</body>\n</html>"
  }
]