diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..496ee2ca --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 9b933b68..9529741b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,43 @@ -# OSM Latest Changes +![Intro Pic][social-media-pic] +# Introduction +*OSM Latest Changes* is a web application that helps to check recent OSM changes within a certain cartographic boundary, for example your home town. It displays all changesets in a list and on a map. The elements can be selected on the map whereby a tag comparison table opens up that highlights all created, modified or deleted tags. Furthermore a "Vandalism Checker" helps to find suspicious edits. -Shows recent changes on OpenStreetMap. +![Picture of App][screenshot] -Objects that have been modified (or created or deleted) during the last week (or month or day) are displayed alongside with their [changeset](https://wiki.openstreetmap.org/wiki/Changeset) meta data. +# Visualization +Newer edits are displayed in more saturated colors than older modifications. Deleted objects as well as the "previous state" of modified map features are displayed semi-transparently. Intermediate states of objects that have been modified more than once in the selected time period are not shown. -Newer edits are displayed in more saturated colors than older modifications. Deleted objects as well as the "previous state" of modified map features are displayed semi-transparently. Intermediate states of objects that have been modified more than once in the selected time period are not shown. The site currently doesn't show OSM modifications of relation objects. +# How to use +1. Zoom to the area of interest and click the *Get Changesets* button to see the changes of the last 7 days (or 1 day, 3 days, 1 month). +2. Analyse changesets by... +- filtering for suspicous edits (hover over red traffic light to get infos) +- selecting elements on the map and see tag modifications +3. Bookmark the URL to regularly come back and monitor your area of interest. -This is based on an earlier prototype by [@lxbarth](https://github.com/lxbarth), see http://www.openstreetmap.org/user/lxbarth/diary/19185 for background. +## Vandalism Checker +A simple tool that verifies the integrity of downloaded changesets. It calculates the total number of elements, tags and iD warnings added and deleted in each changeset. If the net change is below a specified threshold (currently -3), a red traffic light alert is triggered to notify users about the suspicious changeset. -## Running +### Examples +| Type | Case | Sum | Result | +|---------------|---------------------------------------------------------------------------------|--------|:-------------:| +| Element | A user added **one** new supermarket and deleted **two** roads. | -1 | 🟢⚫️ | +| Tag | A user added **three** tags to a restaurant but deleted **seven** of a library. | -4 | ⚫️🔴 | +| iD Warning | A user fixed **one** iD warning but caused **five** new ones. | -4 | ⚫️🔴 | -Just `git clone` and [boot up a quick development server](https://gist.github.com/tmcw/4989751). +# Limitations +- The site currently doesn't show OSM modifications of relation objects. +- Too many or too large download requests can cause the Overpass server to deny the request. + +# Running +Just `git clone` and [boot up a quick development server](https://gist.github.com/tmcw/4989751). If you use *Visual Studio Code* you can alternatively install the *Live Server* extension. + +# History +## 2013 +It was first [prototyped](https://osmlab.github.io/latest-changes/) by [@lxbarth](https://github.com/lxbarth) and [@tmcw](https://github.com/tmcw) during the [Chicago Hack weekend](https://wiki.openstreetmap.org/wiki/Chicago_Hack_Weekend_April_2013) on the 26th-28th April 2013 (see [diary entry of lxbarth](http://www.openstreetmap.org/user/lxbarth/diary/19185) for background). This was conceived to be an enhancement to the OSM history tab with other notable users of the community working on coming up with a solution for a history tab that visualizes local changesets in an easy to understand way. +## 2013-2021 +A hacked version of this prototype has been created with aims into saving bandwith and rendering time. Furthermore it allows lower zoom levels. It will show changes made in the last 24h, 3 days, 7 days or 30 days. Those enhancements were mainly implemented by [@tyrasd](https://github.com/tyrasd) +## 2022-now +As of July 2022 another [updated version](https://www.openstreetmap.org/user/rene78/diary/399505) has been created, that offers added functionality, i.e. a tag comparison table, a vandalism checker and a filter functionality. + +[social-media-pic]: img/SocialMedia-Latest-Changes.png "Intro Pic" +[screenshot]: img/multi-devices.png "Picture of the App" \ No newline at end of file diff --git a/css/images/layers-2x.png b/css/images/layers-2x.png new file mode 100644 index 00000000..200c333d Binary files /dev/null and b/css/images/layers-2x.png differ diff --git a/css/images/layers.png b/css/images/layers.png new file mode 100644 index 00000000..1a72e578 Binary files /dev/null and b/css/images/layers.png differ diff --git a/css/leaflet.css b/css/leaflet.css new file mode 100644 index 00000000..440b6a95 --- /dev/null +++ b/css/leaflet.css @@ -0,0 +1,661 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + } \ No newline at end of file diff --git a/css/site.css b/css/site.css index 39c41e49..522be885 100644 --- a/css/site.css +++ b/css/site.css @@ -1,10 +1,33 @@ +/* #region CSS RELATED TO GENERAL PAGE LAYOUT */ +body { + margin: 0; + font: 15px/1.67 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.container { + display: flex; +} + +@media (max-width: 600px) { + .container { + flex-direction: column; + } +} + a, a:hover, a:visited { - text-decoration:underline; - color:inherit; + /* text-decoration: underline; */ + color: inherit; } +.hide { + display: none; +} + +/* #endregion */ + +/* #region CSS RELATED TO INFO/WARNING MODAL */ .infobox { font-family: Arial, Helvetica, sans-serif; text-align: center; @@ -12,64 +35,76 @@ a:visited { top: -100px; left: 0; right: 0; - margin: 5px 30px 0px;/* top / left,right / bottom */ + margin: 5px 30px 0px; + /* top / left,right / bottom */ padding: 5px 20px; border-radius: 10px; - background-color: #FFFFFF; + color: white; cursor: Default; - box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); - z-index: 1; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + z-index: 1001; } .show { animation-name: animateShowHide; - animation-duration: 2s; + animation-duration: 4s; animation-iteration-count: 2; animation-direction: alternate; } @keyframes animateShowHide { - 0% {top: -100px;} - 30% {top: 0px;} - 100% {top: 0px;} + 0% { + top: -100px; + } + + 30% { + top: 0px; + } + + 100% { + top: 0px; + } } .alarm { background-color: red; - color: white; } .success { background-color: green; - color: black; +} + +/* #endregion */ + +/* #region CSS RELATED TO CHANGESETS COLUMN */ + +.background { + background-color: #e5e5e5; } .heading { position: relative; + width: 293px; + height: auto; + padding: 5px 0; + margin: 0px auto; } -.heading h3 { - text-align: center; -} -@media (min-width: 601px){ - .heading h3 { - position: absolute; - left: 65px; - } +.logo-latest-changes { + display: inline-flex; + align-items: center; + text-decoration: none; + font-size: 25px; + gap: 2px; + margin: 0px 10px; + font-weight: bold; } -.heading img { - width: 60px; - height: 60px; -} -@media (max-width: 600px){ - .heading img { - position: relative; - margin-left: auto; - margin-right: auto; - display: block; - margin-bottom: 10px; - } +#clock { + width: 30px; + height: 30px; + border: 4px solid black; + border-radius: 20px; } .heading select { @@ -77,20 +112,523 @@ a:visited { margin-left: auto; margin-right: auto; display: block; + height: 30px; + width: 95%; + max-width: 400px; + margin-bottom: 10px; + font-size: 13px; + font-weight: 500; + line-height: 20px; + color: #a0a0a0; + padding: 4px 6px; + border-radius: 1px; } #resolution { cursor: pointer; } +.circular-arrow { + width: 16px; + height: 16px; +} + +#download-changesets-button { + display: block; + width: 270px; + padding: 10px 50px; + margin: 5px auto; + font-size: 20px; + color: #f3f0f0; + background-color: #72befb; + border: none; + border-radius: 6px; + cursor: pointer; +} + +#download-changesets-button:hover { + background-color: #008dff; +} + +#download-changesets-button:disabled { + cursor: not-allowed; + opacity: .3; +} + +.sidebar { + flex: 0 1 310px; + overflow: auto; + height: 100vh; +} + +/* Make sure that sidebar is shown on small screens and hide the "toggle sidebar"-icon */ +@media (max-width: 600px) { + .sidebar { + display: block; + flex: 1 1 auto; + height: 50vh; + } + + /* Hide toggle sidebar icon on small screens*/ + .leaflet-control-toggle { + display: none; + } +} + +#results { + list-style-type: none; + margin: 0; + padding: 0; +} + +.result { + display: grid; + padding: 2px 6px; + grid-template-columns: 0fr 0fr 0fr auto 0fr; + grid-template-rows: auto auto auto; + /* | Loupe | Traffic light | Text bubble | User Name & Time | Arrow | */ + /* | Loupe | Changeset Comment | Arrow | */ + /* | Changeset Details | */ + border-bottom: 1px solid #eee; + transition: background-color .2s ease-in-out; + -moz-transition: background-color .2s ease-in-out; + -webkit-transition: background-color .2s ease-in-out; +} + +.result.active, +.result:hover { + background-color: #f5f5f5; +} + +.result.active { + color: #008dff !important; +} + +.result .zoom { + grid-column: 1; + grid-row: 1 / span 2; + justify-content: center; + + display: flex; + align-items: center; + height: 30px; + width: 30px; + padding: 5px; + border-radius: 50%; + margin-right: 5px; + margin-top: 5px; + background-color: #e5e5e5; + stroke: #777; + box-shadow: 0 2px 4px darkslategrey; + cursor: pointer; + transition: all 0.2s ease; +} + +.result .zoom:active { + box-shadow: 0 0 2px darkslategray; + transform: translateY(2px); +} + +.result .zoom:hover { + background-color: #eee; +} + +.loupe { + width: 20px; + height: 20px; + margin-left: 6px; +} + +.traffic-light-container { + grid-column: 2; + grid-row: 1; + display: flex; + align-items: center; + justify-content: left; + + /* display: inline-block; */ + margin-right: 3px; + cursor: help; +} + +.traffic-light { + background-color: black; + width: 25px; + height: 16px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px 2px; + border-radius: 7px; +} + +.traffic-light span { + width: 12px; + height: 12px; + border-radius: 100%; +} + +.gray { + background-color: gray; +} + +.green { + background-color: green; +} + +.red { + background-color: red; +} + +.result .text-bubble { + grid-column: 3; + grid-row: 1; + display: flex; + align-items: center; + justify-content: center; + + cursor: help; +} + +.result .text-bubble .text-bubble-svg { + width: 16px; + height: 14px; + margin-right: 2px; +} + +.container-name-date { + grid-column: 4; + grid-row: 1; + display: flex; + align-items: center; + min-width: 0; + /* prevents flex item overflow issues */ + margin-right: 8px; +} + +/*In order to avoid that 'user name' and 'time since creation' jumps to a second line the +user name is truncated if it takes up too much space*/ +.user-name { + flex: 1 1 0; + min-width: 0; + /* prevents flex item overflow issues */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.result .date { + flex: 0 0 auto; + white-space: nowrap; + + padding-left: 8px; + text-decoration: underline dotted; + cursor: help; +} + +.result .arrow { + grid-column: 5; + grid-row: 1 / span 2; + justify-content: center; + display: flex; + align-items: center; + height: 30px; + width: 30px; + padding: 5px; + border-radius: 50%; + margin-top: 5px; + background-color: #e5e5e5; + box-shadow: 0 2px 4px darkslategrey; + cursor: pointer; + transition: all 0.2s ease; +} + +.result .arrow:active { + box-shadow: 0 0 2px darkslategray; + transform: translateY(2px); +} + +.result .arrow:hover { + background-color: #eee; +} + +.arrow-up-svg { + width: 24px; + height: 24px; + transition: transform 0.3s ease; + transform: rotate(0deg); + fill: #777777; +} + +.arrow-up-svg.rotated { + transform: rotate(180deg); + /* Turn arrow counterclockwise when expanding */ +} + +.changeset-comment { + grid-column: 2 / span 3; + grid-row: 2; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-decoration: none; + font-weight: bold; + font-size: 18px; +} + +.changeset-comment.expanded { + text-align: center; +} + +.table-container { + grid-column: 1 / span 5; + grid-row: 3; + height: calc-size(max-content, size); + /* calc-size is only in draft status as of 2025-08. Thus limited browser compatibility. */ + transition: height 0.3s ease; + overflow: hidden; + color: #333; +} + +.table-container.hidden { + height: 0; +} + +.changeset-table { + border-collapse: collapse; + margin: 0 auto; + background-color: white; + width: 100%; +} + +/* Color the right border of the first column in black. Increase the font weight of all the text in this column. +Exclude all table headings and the changeset discussion section from this rule*/ +.changeset-table tr:not(.table-heading):not(:has(td.changeset-discussion)) td:nth-child(1) { + border-right: 1px solid black; + font-weight: 600; +} + +.changeset-table td { + padding: 0 3px; +} + +.changeset-table .border-bottom { + border-bottom: 1px solid black; +} + +.table-heading { + background-color: #e5e5e5; + text-align: center; + font-weight: 600; + font-size: 16px; +} + +/* Sometimes the content of 'comment' can be quite long and without whitespaces +(e.g. the url of a website). Without the CSS declaration +below the long text would expand the table width. */ +.table-changeset-comment { + overflow-wrap: anywhere; +} + +.imagery-list { + padding-left: 0px; + list-style-position: inside; + overflow-wrap: anywhere; + /* To avoid overflow issues with long text without whitespaces, e.g. URL's */ +} + +/* Sometimes the content of 'editor' can be quite long and without whitespaces +(e.g. 'reverter_plugin/36447;JOSM/1.5 (19431 de)'). Without the CSS declaration +below the long text would expand the table width. */ +.editor { + overflow-wrap: anywhere; +} + +.integrity { + white-space: nowrap; +} + +.changeset-discussion { + font-weight: initial; +} + +.comment-container { + margin-top: 10px; +} + +.comment-info { + font-weight: 600; +} + +.comment-text { + margin-left: 7px; + white-space: pre-line; +} + +/* Custom scroll bar + ------------------------------------------------------- */ +/* Designing for scroll-bar */ +::-webkit-scrollbar { + width: 6px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: gainsboro; + border-radius: 5px; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: black; + border-radius: 5px; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Back to top button */ +.to-top { + cursor: pointer; + position: absolute; + background: #008dff; + color: white; + opacity: 0.8; + width: 40px; + height: 40px; + border-radius: 50%; + left: 210px; + bottom: 12px; + transition: background 0.5s; +} + +.to-top:hover { + color: #008dff; + background: white; +} + +/* Position "Back to top" button differently on small screens */ +@media (max-width: 600px) { + .to-top { + left: auto; + bottom: calc(50% + 7px); + right: 65px; + } +} + +/* #endregion */ + +/* #region CSS RELATED TO FILTER CHANGESETS TOOLBAR */ +.filter-container { + padding: 5px 12px; +} + +#filter-red-checkbox { + display: none; +} + +.filter-red-checkbox-label { + width: 42px; + height: 42px; + background-color: #72befb; + display: inline-flex; + justify-content: center; + align-items: center; + cursor: pointer; + border-radius: 6px; +} + +.filter-red-color { + filter: grayscale(100%); +} + +.filter-red-checkbox-label:hover { + background-color: #008dff; +} + +.search-changesets-field-container { + position: absolute; + left: 60px; + color: #f3f0f0; + display: inline-block; +} + +.filter-symbol { + position: absolute; + pointer-events: none; + left: 8px; + top: 12px; + width: 16px; + height: 16px; +} + +.delete-filter-input { + position: absolute; + cursor: pointer; + left: 197px; + top: 11px; + fill: #f3f0f0; +} + +.delete-filter-input:hover { + fill: red; +} + +/* Make sure that the whole input field remains in the darker color when hovering of the x */ +.delete-filter-input:hover~.search-changesets-field { + background-color: #008dff; +} + +.delete-filter-input-symbol { + width: 16px; + height: 16px; +} + +.search-changesets-field { + padding: 0 30px 0 33px; + background-color: #72befb; + color: inherit; + border-radius: 6px; + border: none; + font-size: 18px; + width: 159px; + height: 42px; + vertical-align: top; + /*needed to keep the height of the element unchanged (https://stackoverflow.com/a/28157789/5263954)*/ +} + +::placeholder { + color: #cad9e7; + opacity: 1; +} + +.search-changesets-field:hover { + background-color: #008dff; +} + +/* #endregion */ + +/* #region CSS RELATED TO LEAFLET MAP LAYOUT */ +.map-container { + position: relative; + flex: 1 1 auto; + height: 100vh; +} + #map { + background: white; + position: relative; + height: 100%; + width: 100%; opacity: 1; transition: opacity .5s ease-in-out; -moz-transition: opacity .5s ease-in-out; -webkit-transition: opacity .5s ease-in-out; - flex: 1 1 auto; - height: 100vh; } + #map.faded { opacity: .5; } @@ -98,145 +636,155 @@ a:visited { #zoom-in { position: absolute; left: 0; - top: 40px; + top: 0; text-align: center; font-size: 25px; font-weight: bold; pointer-events: none; - opacity: .5; + opacity: .6; padding-left: 44px; z-index: 444; width: 100%; + color: white; + background-color: red; + box-sizing: border-box; } -.hide { - display: none; +#loading-animation { + position: absolute; + left: calc(50% - 25px); + top: 40%; + border: 16px solid white; + border-radius: 50%; + border-top: 16px solid #2074B6; + width: 50px; + height: 50px; + -webkit-animation: spin 2s linear infinite; + /* Safari */ + animation: spin 2s linear infinite; + z-index: 444; + pointer-events: none; } -.result { - border-bottom:1px solid #eee; +@media (max-width: 600px) { + #loading-animation { + top: 0%; + } } -.affix { - position:fixed; +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } -span.load { - cursor:pointer; +/* Toggle sidebar icon */ +.leaflet-control-toggle-icon { + display: inline-block; + width: 20px; + height: 20px; + margin: 5px; + background-color: #000; + -webkit-mask-image: url(../img/icon-sidebar-left.svg); + mask-image: url(../img/icon-sidebar-left.svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; } -a.l { - padding:2px; - text-decoration:underline; - font-size:13px; - margin:0 4px; +.stylePopup .leaflet-popup-content-wrapper { + opacity: 0.9 } -h2, p { - margin:0; - padding:0 20px; +.tag-table-container { + border-collapse: collapse; + margin: 5px 0; + border-top: 1px solid #c9c8c8; } -.result { - padding: 5px 0 0 0; - transition: background-color .2s ease-in-out; - -moz-transition: background-color .2s ease-in-out; - -webkit-transition: background-color .2s ease-in-out; - } - .result.active, - .result:hover { - background-color: #f5f5f5; - } - .result.active { - color: #00c500 !important; - } - .result .date { - padding-left: 5px; - } +.tag-table-container tr { + border-bottom: 1px solid black; +} -.changeset { - padding: 0px 0px 5px 20px; - height: 35px; - overflow: hidden; - } - .changeset a { text-decoration: none;} - .changeset a.comment:first-letter { text-transform: capitalize; } - .result.active .changeset, - .result:hover .changeset { height: auto; min-height: 35px;} - .changeset:first-line { font-weight: bold; font-size: 18px; } - -.reveal { visibility: hidden; } - a.reveal { text-decoration: none; opacity: 0.6; } - a.reveal:hover { text-decoration: underline; opacity: 1; } - .result.active .reveal, - .result:hover .reveal { visibility: visible; } - -h2 select { - width: auto; - height: auto; - border: 0; - background: transparent; - color: black; - font-weight: bold; - font-size: 28px; - padding: 0; - margin: 0; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - vertical-align: baseline; +.tag-table-container td { + padding: 0 3px; } -.col { - flex: 0 1 310px; - overflow: auto; - height: 100vh; +/* Color the right border of the first column in black. Increase the font weight of all the text in this column. */ +.tag-table-container td:nth-child(1) { + border-right: 1px solid black; + font-weight: 600; } -@media (max-width: 600px){ - .col { - flex: 1 1 auto; - height: 50vh; - } + +/* Color the right border of the second column in a brighter shade of gray */ +.tag-table-container td:nth-child(2) { + border-right: 1px solid rgb(170, 170, 170); } -.container { - /* margin: 0 auto; - overflow: hidden; */ - display: flex; - /* flex-wrap: wrap; */ - /* max-width: 100%; */ +/* Wrap long text in the 'Old' and 'New' column if there is not enough space */ +.tag-table-container td:nth-child(2), +.tag-table-container td:nth-child(3) { + overflow-wrap: anywhere; } -@media (max-width: 600px){ - .container { - flex-direction: column; +.tag-table-container a { + text-decoration: none; + color: #004764; +} + +.tag-table-container a:hover { + text-decoration: underline; +} + +/* Truncate text in table on small screens */ +@media (max-width: 600px) { + .tag-table-container td { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } -/* Custom scroll bar -------------------------------------------------------- */ -/* Designing for scroll-bar */ -::-webkit-scrollbar { - width: 6px; -} - -/*Increase size of scroll bar on hover*/ -*:hover::-webkit-scrollbar { - width: auto; - /* height: 10px; */ -} - -/* Track */ -::-webkit-scrollbar-track { - background: gainsboro; - border-radius: 5px; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: black; - border-radius: 5px; -} - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: #555; -} \ No newline at end of file +.create { + background-color: #c8ffc8; +} + +.delete { + background-color: #ffcccc; +} + +.modify { + background-color: #ffffae; +} + +.capitalize { + display: inline-block; +} + +.capitalize::first-letter { + text-transform: capitalize; +} + +.metatags { + background-color: #e5e5e5; +} + +.clock-with-circular-arrow-symbol { + vertical-align: top; + width: 16px; + height: 16px; + padding-left: 3px; + fill: #333; +} + +.clock-with-circular-arrow-symbol:hover { + fill: black; +} + +/* #endregion */ \ No newline at end of file diff --git a/css/style.css b/css/style.css deleted file mode 100644 index 3872f5ce..00000000 --- a/css/style.css +++ /dev/null @@ -1,440 +0,0 @@ -/* Reset ♥ - http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -------------------------------------------------------- */ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin:0; - padding:0; - border:0; - font-size:100%; - font:inherit; - vertical-align:baseline; - } -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display:block; - } -body { line-height:1; } -ol, ul { list-style:none; } -blockquote, q { quotes:none; } -blockquote:before, blockquote:after, -q:before, q:after { content:''; content:none; } -/* tables still need 'cellspacing="0"' in the markup */ -table { border-collapse: collapse; border-spacing:0; } -/* remember to define focus styles. Hee Haw */ -:focus { outline:0; } - -/* Inline Elements & Typography -------------------------------------------------------- */ -body, -input, -textarea { - color:#333; - font:15px/1.67 'Helvetica Neue', Helvetica, Arial, sans-serif; - -webkit-font-smoothing:antialiased; - } -*, *:after, *:before { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - -h1, -h2, -h3, -h4, -h5, -h6 { - margin:0; - font-weight:bold; - } - -h1 { - font-size:32px; - margin-bottom:20px; - line-height:1em; - } - -h2 { - font-size:28px; - margin-bottom:20px; - line-height:1.25em; - } - -h3 { - font-size:20px; - margin-bottom:20px; - line-height:1.5em; - } - -h4, h5 { - font-size:15px; - margin-bottom:0; - line-height:1.67em; - } - -p { - margin:0 0 20px; - } - p:last-child { margin-bottom:0;} - -/* Links */ -a { - color:#77c453; - text-decoration:none; - } - a:visited { - color:#9ed485; - } - a:hover { - color:#54af29; - } - a:active { - color:#9ed485; - } - -abbr { - border-bottom:1px dotted #000; - cursor:help; - } - -address { font-style:italic;} -small { font-size:11px;} -strong { font-weight:bold;} -em { font-style:italic;} - -hr { - margin:0 0 20px; - border:0; - height:1px; - background:#f8f8f8; - } - -/* Block Quotes */ -blockquote, -q { - quotes:none; - font-style:italic; - padding-left:20px; - margin:10px; - } - -blockquote:before, -blockquote:after, -q:before, -q:after { - content:''; - } - -/* Code Blocks & Pre */ -code, -pre { - padding:5px; - font-family:Menlo, Bitstream Vera Sans Mono, Monaco, Consolas, monospace; - font-size:12px; - border-radius:3px; - } -code { - padding:5px; - background:#f8f8f8; - border:1px solid #ddd; - } -pre { - display:block; - padding:10px; - margin-bottom:10px; - font-size:12px; - word-break:break-all; - word-wrap:break-word; - white-space:pre; - white-space:pre-wrap; - background:#f8f8f8; - border:1px solid #ddd; - border-radius:3px; - } - pre code { - padding:0; - color:inherit; - background-color:transparent; - border:0; - } -.pre-scrollable { - max-height:300px; - overflow-y:scroll; - } - -/* sub/superscripts */ -sup, -sub { - height:0; - line-height:1; - vertical-align:baseline; - _vertical-align:bottom; - position:relative; - font-size:75%; - } -sup { - top:.5em; - bottom:1em; - } - -label { - display:block; - } -select, -textarea, -input[type=text] { - display:inline-block; - height:30px; - width:95%; - max-width:400px; - margin-bottom:10px; - font-size:13px; - font-weight:500; - line-height:20px; - /* color:#a0a0a0; */ - vertical-align:middle; - padding:4px 6px; - -webkit-border-radius:1px; - border-radius:1px; - } -textarea, -input[type=text] { - background-color:#fff; - border:1px solid #ccc; - -webkit-box-shadow:1px 1px 2px rgba(0,0,0,0.1); - -moz-box-shadow:1px 1px 2px rgba(0,0,0,0.1); - box-shadow:1px 1px 2px rgba(0,0,0,0.1); - -webkit-transition:border linear .2s, box-shadow linear .2s; - -moz-transition:border linear .2s, box-shadow linear .2s; - -o-transition:border linear .2s, box-shadow linear .2s; - transition:border linear .2s, box-shadow linear .2s; - } - textarea:focus, - input[type=text]:focus { - outline:thin dotted\8; /* ie8 below */ - color:#404040; - border-color:#00395D; - border-width:1px; - } - -textarea { - height:200px; - max-width:none; - } -input[type=submit] { - background-color:#00395D; - cursor:pointer; - color:#fff; - font-weight:bold; - text-transform:uppercase; - border:none; - padding:9px 20px; - -webkit-box-shadow:2px 2px 4px rgba(0,0,0,0.1); - -moz-box-shadow:2px 2px 4px rgba(0,0,0,0.1); - box-shadow:2px 2px 4px rgba(0,0,0,0.1); - } - input[type=submit]:hover { - background-color:#002f4c; - } - input[type=submit]:active { - position:relative; - top:1px; - } - -table { - width:100%; - background-color:transparent; - border-collapse:collapse; - border-spacing:0; - margin-bottom:20px; - table-layout:fixed; - } - th, - td { - padding:4px 0; - line-height:20px; - text-align:left; - vertical-align:top; - border-bottom:1px solid #d5d5d5; - } - th { - font-weight:bold; - } - thead th { - vertical-align:bottom; - color:#57594D; - } - -/* Read content styling */ -.prose ul { - list-style:disc; - margin-left:40px; - } -.prose ol { - list-style:decimal; - } -.prose p { - margin:0 0 10px; - } - -.icon { - background:transparent url(img/sprite.png) no-repeat 0 0; - display:block; - width:30px; - height:30px; - text-indent:-999em; - } - -/* Layout -------------------------------------------------------- */ -/* .container { - max-width:1600px; - margin:0 auto; - overflow:hidden; - } */ - -/* Columns -------------------------------------------------------- */ -.col0 { float:left; width:04.1666%; } -.col1 { float:left; width:08.3333%; } -.col2 { float:left; width:16.6666%; } -.col3 { float:left; width:25.0000%; } -.col4 { float:left; width:33.3333%; } -.col5 { float:left; width:41.6666%; } -.col6 { float:left; width:50.0000%; } -.col7 { float:left; width:58.3333%; } -.col8 { float:left; width:66.6666%; } -.col9 { float:left; width:75.0000%; } -.col10 { float:left; width:83.3333%; } -.col11 { float:left; width:91.6666%; } -.col12 { width:100%; } -.margin0 { margin-left:04.1666%; } -.margin1 { margin-left:08.3333%; } -.margin2 { margin-left:16.6666%; } -.margin3 { margin-left:25.0000%; } -.margin4 { margin-left:33.3333%; } -.margin5 { margin-left:41.6666%; } -.margin6 { margin-left:50.0000%; } -.margin7 { margin-left:58.3333%; } -.margin8 { margin-left:66.6666%; } -.margin9 { margin-left:75.0000%; } -.margin10 { margin-left:83.3333%; } -.margin11 { margin-left:91.6666%; } -.margin12 { margin-left:100.0000%; } - -/* Padding -------------------------------------------------------- */ -.pad1 { padding:10px; } -.pad2 { padding:5px; } -.pad21h { padding:10px 20px; } -.pad2h { padding:0 20px; } -.pad4 { padding:40px; } -.pad4h { padding-left:40px; padding-right:40px; } -.pad8 { padding:80px 40px; } -.pad4c { padding:40px; } - -/* Additional Utility Classes -------------------------------------------------------- */ -.fr { float:right; } -.fl { float:left; } -.show { display:block; } -.hide { display:none; } -.deemphasize { color:#888; } -.center { text-align:center; } - -.tip-top:after, -.tip-right:after, -.tip-bottom:after, -.tip-left:after { - content:''; - border-width:0 5px 5px; - border-style:solid; - position:absolute; - border-color:#333 transparent; - } - .tip-bottom:after { - border-width:5px 5px 0; - } - .tip-left:after { - border-width:5px 5px 5px 0; - border-color:transparent #333; - } - .tip-right:after { - border-width:5px 0 5px 5px; - } - -/* Markup free clearing -Details: http://www.positioniseverything.net/easyclearing.html -------------------------------------------------------- */ -.clearfix:after { - content:'.'; - display:block; - height:0; - clear:both; - visibility:hidden; - } - -.clearfix { display:inline-block; } - -/* Tablet Layout -------------------------------------------------------- */ -@media only screen and (max-width:770px) { - .hide-tablet { display:none; } - .show-tablet { display:block; } - } - -/* Mobile Layout -------------------------------------------------------- */ -@media only screen and (max-width:640px) { - .col1, - .col2, - .col3, - .col4, - .col5, - .col6, - .col7, - .col8, - .col9, - .col10, - .col11, - .col12 { width:100%; max-width:100%; } - .margin0, - .margin1, - .margin2, - .margin3, - .margin4, - .margin5, - .margin6, - .margin7, - .margin8, - .margin9, - .margin10, - .margin11, - .margin12 { margin-left:0; } - .pad4c { padding:0; } - .pad4, - .pad4h { padding-left:20px; padding-right:20px; } - .pad8 { padding:40px 20px; } - .hide-mobile { display:none; } - .show-mobile { display:block; } - .icon { - background-image:url(img/sprite@2x.png); - background-size:240px 240px; - } - } - diff --git a/doc/Equirectangular_Distance_Explanation.md b/doc/Equirectangular_Distance_Explanation.md new file mode 100644 index 00000000..c6a990df --- /dev/null +++ b/doc/Equirectangular_Distance_Explanation.md @@ -0,0 +1,95 @@ + +# Understanding the Equirectangular Approximation for Distance Calculation + +This JavaScript function calculates the approximate distance between two points on the Earth's surface, given their latitude and longitude. It uses the **equirectangular approximation**, which is simpler and faster than more accurate formulas like the **Haversine formula**, but still sufficiently precise for short distances. + +--- + +## JS Function +```js +function distance(λ1, φ1, λ2, φ2) { + const R = 6371000; + const Δλ = (λ2 - λ1) * Math.PI / 180; + φ1 = φ1 * Math.PI / 180; + φ2 = φ2 * Math.PI / 180; + const x = Δλ * Math.cos((φ1 + φ2) / 2); + const y = (φ2 - φ1); + const d = Math.sqrt(x * x + y * y); + return R * d; +}; +``` + +## Key Concepts + +### 1. **The Radius of the Earth (`R`)** + +```js +const R = 6371000; +``` +- The Earth's mean radius is approximately **6371 kilometers** or **6,371,000 meters**. +- In this function, distance is returned in meters because `R` is defined in meters. + +--- + +### 2. **Converting Degrees to Radians** + +```js +Δλ = (λ2 - λ1) * Math.PI / 180; +φ1 = φ1 * Math.PI / 180; +φ2 = φ2 * Math.PI / 180; +``` +- Latitude (`φ`) and longitude (`λ`) are usually given in **degrees**, but trigonometric functions in JavaScript (and most programming languages) operate in **radians**. +- To convert degrees to radians, we multiply by `π / 180`. + +--- + +### 3. **Why Use the Cosine Function for Longitude Differences?** + +```js +const x = Δλ * Math.cos((φ1 + φ2) / 2); +``` + +- When calculating the distance between two points on a sphere, the distance covered by a change in **longitude** depends on the latitude. +- **Longitude lines** converge at the poles, so the distance between two points with the same longitude difference is smaller as you move closer to the poles. +- To account for this, the function multiplies the difference in longitude `Δλ` by the **cosine of the average latitude** `(φ1 + φ2) / 2`. + - This ensures the longitudinal difference is scaled correctly according to the latitude. + +> **Why cosine?** +> - At the equator, `cos(0°) = 1`, so longitude differences correspond directly to distances. +> - Moving towards the poles, `cos(90°) = 0`, meaning longitude differences have virtually no effect on distance, as longitude lines meet at the poles. + +--- + +### 4. **Latitude Differences are Simpler** + +```js +const y = (φ2 - φ1); +``` +- **Latitude lines** are **parallel**, meaning the distance between degrees of latitude remains roughly constant regardless of where you are on Earth. +- Thus, the difference in latitude can be taken directly in radians, without any additional scaling. + +--- + +### 5. **Calculating the Final Distance** + +```js +const d = Math.sqrt(x * x + y * y); +return R * d; +``` +- The function treats the small section of the Earth's surface between the two points as a flat triangle. +- It uses the **Pythagorean theorem** to combine the distances in the x (longitude) and y (latitude) directions. +- Finally, multiplying by `R` gives the actual distance on the Earth's surface. + +--- + +## Summary + +- **Latitude difference:** Direct subtraction in radians (latitude lines are parallel). +- **Longitude difference:** Scaled by `cos(latitude)` because longitude lines converge towards the poles. +- **Distance:** Calculated using the Pythagorean theorem and scaled by the Earth's radius. + +This method offers a good trade-off between simplicity and accuracy for short distances, making it suitable for many applications in geospatial analysis. + +## Credit + +This formula has been found on the website [Movable Type](http://www.movable-type.co.uk/scripts/latlong.html#equirectangular). Thanks Chris Veness! \ No newline at end of file diff --git a/examples/exampleDiscussions.json b/examples/exampleDiscussions.json new file mode 100644 index 00000000..e33aecfb --- /dev/null +++ b/examples/exampleDiscussions.json @@ -0,0 +1,47 @@ +{ + "version": "0.6", + "generator": "openstreetmap-cgimap 2.1.0 (113219 spike-06.openstreetmap.org)", + "copyright": "OpenStreetMap and contributors", + "attribution": "http://www.openstreetmap.org/copyright", + "license": "http://opendatacommons.org/licenses/odbl/1-0/", + "changeset": { + "id": 122803935, + "created_at": "2022-06-24T13:36:38Z", + "open": false, + "comments_count": 2, + "changes_count": 1, + "closed_at": "2022-06-24T13:36:38Z", + "min_lat": 47.6521043, + "min_lon": 9.4757572, + "max_lat": 47.6521043, + "max_lon": 9.4757572, + "uid": 257555, + "user": "rene78", + "tags": { + "changesets_count": "20932", + "comment": "Schinacher immer noch geöffnet. Aber eher Kinderkleidungsladen als Spielwarengeschäft. Abgeändert.", + "created_by": "iD 2.21.1", + "host": "https://www.openstreetmap.org/edit", + "imagery_used": "Bing Maps Aerial", + "locale": "de" + }, + "comments": [ + { + "id": 948812, + "visible": true, + "date": "2022-11-24T08:08:55Z", + "uid": 257555, + "user": "rene78", + "text": "Kommentar zu Testwecken für https://github.com/rene78/latest-changes" + }, + { + "id": 1512892, + "visible": true, + "date": "2025-11-16T08:36:24Z", + "uid": 257555, + "user": "rene78", + "text": "Noch ein Kommentar zu Testzwecken. Jetzt kommen 2 Leerzeilen:\n\nDas sollte richtig gerendert werden." + } + ] + } +} \ No newline at end of file diff --git a/examples/exampleOSMAPI.xml b/examples/exampleOSMAPI.xml new file mode 100644 index 00000000..d255d0ed --- /dev/null +++ b/examples/exampleOSMAPI.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/exampleOverpassAPI.xml b/examples/exampleOverpassAPI.xml new file mode 100644 index 00000000..ca05dcd2 --- /dev/null +++ b/examples/exampleOverpassAPI.xml @@ -0,0 +1,652 @@ + + +The data included in this document is from www.openstreetmap.org. The data is made available under ODbL. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/exampleUsers.json b/examples/exampleUsers.json new file mode 100644 index 00000000..fbcaee23 --- /dev/null +++ b/examples/exampleUsers.json @@ -0,0 +1,63 @@ +{ + "version": "0.6", + "generator": "OpenStreetMap server", + "copyright": "OpenStreetMap and contributors", + "attribution": "http://www.openstreetmap.org/copyright", + "license": "http://opendatacommons.org/licenses/odbl/1-0/", + "users": [ + { + "user": { + "id": 257555, + "display_name": "rene78", + "account_created": "2010-04-03T20:43:12Z", + "description": "# About me\r\nJust a guy fascinated by OSM.\r\n\r\n# Useful sites\r\n\r\n## Data quality\r\n* [Latest Changes](https://rene78.github.io/latest-changes) (Check recent OSM changes within a certain cartographic boundary)\r\n\r\n## Mapping facilitation\r\n* [Opening hours evaluation tool](https://openingh.openstreetmap.de/evaluation_tool/) (Get opening hours formatted correctly for OSM)\r\n* [Relatify](https://relatify.monicz.dev/) (Tool to automatically improve bus relations)\r\n\r\n\r\n## OSM news\r\n* [Weekly OSM](https://weeklyosm.eu/)", + "contributor_terms": { + "agreed": true + }, + "img": { + "href": "https://www.openstreetmap.org/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NzUzNSwicHVyIjoiYmxvYl9pZCJ9fQ==--c47865bd781391cf901465eb37823a92afd7bd69/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fbGltaXQiOlsxMDAsMTAwXX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--71e417a328c02e35ec87b6c7cd4e4524c74e4ec8/axl-rose2-e508623_1_.jpg" + }, + "roles": [], + "changesets": { + "count": 27162 + }, + "traces": { + "count": 113 + }, + "blocks": { + "received": { + "count": 0, + "active": 0 + } + } + } + }, + { + "user": { + "id": 1213571, + "display_name": "mcliquid", + "account_created": "2013-01-28T13:01:25Z", + "description": "###About me\r\nI use OpenStreetMap intensively for various purposes and try to enrich the data with a lot of dedication. I always try to keep my changes in line with the wiki and stay in touch with the community via the forum or various local channels like Matrix, Telegram or similar. Feel free to contact me if you have any questions.\r\n\r\n###My favorite OSM tools:\r\n* StreetComplete [Wiki](https://wiki.openstreetmap.org/wiki/StreetComplete) [GitHub (direct download)](https://github.com/streetcomplete/StreetComplete) [Google PlayStore](https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete)\r\n* [NotesReview](https://ent8r.github.io/NotesReview/)\r\n\r\n###OSM-related accounts:\r\n\r\n* [OSM Community](https://community.openstreetmap.org/u/mcliquid/)\r\n* [OSM Forum](https://forum.openstreetmap.org/profile.php?id=115687)\r\n* [OSM Wiki](https://wiki.openstreetmap.org/wiki/User:Mcliquid)\r\n* [Missing Maps](https://www.missingmaps.org/users/#/mcliquid)\r\n* [OSM help](https://help.openstreetmap.org/users/22192/mcliquid)\r\n* [git](https://github.com/mcliquid)\r\n* [hdyc](https://hdyc.neis-one.org/?mcliquid)\r\n* Mastodon", + "contributor_terms": { + "agreed": true + }, + "img": { + "href": "https://www.gravatar.com/avatar/69a0c6c5e3d205d9338ebae6d34c766a.jpg?s=100&d=https%3A%2F%2Fapi.openstreetmap.org%2Fassets%2Favatar_large-54d681ddaf47c4181b05dbfae378dc0201b393bbad3ff0e68143c3d5f3880ace.png" + }, + "roles": [], + "changesets": { + "count": 72004 + }, + "traces": { + "count": 47 + }, + "blocks": { + "received": { + "count": 0, + "active": 0 + } + } + } + } + ] +} \ No newline at end of file diff --git a/examples/geoJsonExample.js b/examples/geoJsonExample.js new file mode 100644 index 00000000..14f827b8 --- /dev/null +++ b/examples/geoJsonExample.js @@ -0,0 +1,1801 @@ +//Example of the GeoJSON code created by site.js. newGeoJSON and oldGeoJSON are displayed with leaflet.js +const newGeoJSON = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "way/437103103", + "properties": { + "type": "way", + "id": 437103103, + "tags": { + "access": "no", + "area": "yes", + "construction:public_transport": "platform", + "construction:railway": "platform", + "level": "0", + "note": "Aufgrund von Bahnsteig-Bauarbeiten gesperrt.", + "ref": "1", + "ref:IFOPT": "de:08435:44002:2:1", + "ref:IFOPT:description": "Gleis 1", + "tactile_paving": "no", + "wheelchair": "limited", + "width": "2" + }, + "relations": [], + "meta": { + "timestamp": "2024-04-15T12:16:12Z", + "version": "6", + "changeset": "122800892", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.4706024, + 47.6532025 + ], + [ + 9.4706017, + 47.653169 + ], + [ + 9.4710014, + 47.65315 + ], + [ + 9.4718369, + 47.653113 + ], + [ + 9.4729943, + 47.6530677 + ], + [ + 9.4737966, + 47.6530304 + ], + [ + 9.4738814, + 47.6530265 + ], + [ + 9.473957, + 47.6530225 + ], + [ + 9.4744499, + 47.653 + ], + [ + 9.4747243, + 47.6529874 + ], + [ + 9.4747223, + 47.6529739 + ], + [ + 9.4747349, + 47.6529729 + ], + [ + 9.4747887, + 47.6529714 + ], + [ + 9.4748711, + 47.6529685 + ], + [ + 9.4749543, + 47.6529657 + ], + [ + 9.4754599, + 47.6529481 + ], + [ + 9.475, + 47.654 + ], + [ + 9.4752889, + 47.6529852 + ], + [ + 9.4743743, + 47.6530303 + ], + [ + 9.4729869, + 47.6530972 + ], + [ + 9.4721361, + 47.6531387 + ], + [ + 9.4710155, + 47.6531934 + ], + [ + 9.4706024, + 47.6532025 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/1072310202", + "properties": { + "type": "way", + "id": 1072310202, + "tags": { + "addr:city": "Friedrichshafen", + "addr:housenumber": "9/1", + "addr:postcode": "88046", + "addr:street": "Gutenbergstraße", + "building": "yes" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-22T13:47:54Z", + "version": "1", + "changeset": "122713614", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.4938169, + 47.6693701 + ], + [ + 9.4938151, + 47.6693517 + ], + [ + 9.4938004, + 47.6693466 + ], + [ + 9.4937909, + 47.6693406 + ], + [ + 9.4937877, + 47.6693303 + ], + [ + 9.4937909, + 47.6693212 + ], + [ + 9.493803, + 47.6693152 + ], + [ + 9.4938119, + 47.6693109 + ], + [ + 9.4938119, + 47.6692559 + ], + [ + 9.4939701, + 47.6692529 + ], + [ + 9.4939701, + 47.6692448 + ], + [ + 9.494032, + 47.6692435 + ], + [ + 9.4940339, + 47.6692688 + ], + [ + 9.4941596, + 47.669265 + ], + [ + 9.4941668, + 47.6693615 + ], + [ + 9.4938169, + 47.6693701 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/290918616", + "properties": { + "type": "way", + "id": 290918616, + "tags": { + "building": "kindergarten" + }, + "relations": [], + "meta": { + "timestamp": "2024-07-29T13:56:35Z", + "version": "5", + "changeset": "154564334", + "user": "mcliquid", + "uid": "1213571" + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.5044805, + 47.6561876 + ], + [ + 9.5044805, + 47.6561932 + ], + [ + 9.5044941, + 47.6561932 + ], + [ + 9.5044941, + 47.6561951 + ], + [ + 9.5044941, + 47.6562054 + ], + [ + 9.5044383, + 47.6562055 + ], + [ + 9.5044383, + 47.6562462 + ], + [ + 9.504309, + 47.6562462 + ], + [ + 9.5043089, + 47.656135 + ], + [ + 9.504436, + 47.6561349 + ], + [ + 9.504436, + 47.6561876 + ], + [ + 9.5044805, + 47.6561876 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/290918617", + "properties": { + "type": "way", + "id": 290918617, + "tags": { + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "66", + "addr:postcode": "88046", + "addr:street": "Länderöschstraße", + "building": "kindergarten" + }, + "relations": [], + "meta": { + "timestamp": "2024-07-29T13:56:35Z", + "version": "7", + "changeset": "154564334", + "user": "mcliquid", + "uid": "1213571" + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.5044941, + 47.6561951 + ], + [ + 9.5046029, + 47.6561951 + ], + [ + 9.5047564, + 47.656195 + ], + [ + 9.5047565, + 47.6562775 + ], + [ + 9.5044942, + 47.6562776 + ], + [ + 9.5044941, + 47.6562054 + ], + [ + 9.5044941, + 47.6561951 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/332742460", + "properties": { + "type": "way", + "id": 332742460, + "tags": { + "barrier": "fence", + "landuse": "education" + }, + "relations": [], + "meta": { + "timestamp": "2024-07-29T13:57:05Z", + "version": "4", + "changeset": "154564373", + "user": "mcliquid", + "uid": "1213571" + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.5043791, + 47.6559249 + ], + [ + 9.5044464, + 47.6558819 + ], + [ + 9.5046301, + 47.6557544 + ], + [ + 9.5049169, + 47.6556337 + ], + [ + 9.5053414, + 47.655977 + ], + [ + 9.5052697, + 47.6560155 + ], + [ + 9.5051644, + 47.6560759 + ], + [ + 9.505067, + 47.6561309 + ], + [ + 9.5049605, + 47.6561951 + ], + [ + 9.5049274, + 47.6562154 + ], + [ + 9.5049101, + 47.656226 + ], + [ + 9.5047589, + 47.6563188 + ], + [ + 9.5045905, + 47.6564251 + ], + [ + 9.5044169, + 47.6565473 + ], + [ + 9.5043983, + 47.6565607 + ], + [ + 9.504297, + 47.6566341 + ], + [ + 9.5041939, + 47.6567133 + ], + [ + 9.5041166, + 47.6567843 + ], + [ + 9.5040214, + 47.6568801 + ], + [ + 9.5039676, + 47.6569488 + ], + [ + 9.5039419, + 47.6569412 + ], + [ + 9.5039262, + 47.656779 + ], + [ + 9.5039206, + 47.6566024 + ], + [ + 9.5039139, + 47.656456 + ], + [ + 9.5039195, + 47.65637 + ], + [ + 9.5039441, + 47.6562825 + ], + [ + 9.5040158, + 47.6561836 + ], + [ + 9.5041133, + 47.6561067 + ], + [ + 9.5042432, + 47.6560184 + ], + [ + 9.5043791, + 47.6559249 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/1072310203", + "properties": { + "type": "way", + "id": 1072310203, + "tags": { + "access": "destination", + "highway": "service", + "service": "driveway" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-22T13:47:54Z", + "version": "1", + "changeset": "122713614", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 9.4942065, + 47.6700922 + ], + [ + 9.4932077, + 47.6701004 + ], + [ + 9.4930957, + 47.6701029 + ] + ] + } + }, + { + "type": "Feature", + "id": "way/999999999", + "properties": { + "type": "way", + "id": 999999999, + "tags": { + "highway": "service", + "service": "driveway", + "name": "Pennyroad Lane" + }, + "relations": [], + "meta": { + "timestamp": "2025-04-01T21:20:54Z", + "version": "2", + "changeset": "103017971", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 9.4785357, + 47.6568163 + ], + [ + 9.4775609, + 47.6566572 + ] + ] + } + }, + { + "type": "Feature", + "id": "node/2193083033", + "properties": { + "type": "node", + "id": "2193083033", + "tags": { + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "57", + "addr:postcode": "88045", + "addr:street": "Friedrichstraße", + "clothes": "children", + "contact:facebook": "https://www.facebook.com/SpielwarenDauth/", + "contact:instagram": "https://www.instagram.com/spielwarenschinacher/", + "contact:phone": "+49 7541 92210", + "contact:website": "https://www.schinacher.de/", + "name": "Schinacher", + "opening_hours": "Mo off; Tu-Fr 09:30-16:30; Sa 09:30-16:00; Su,PH off", + "shop": "clothes", + "source": "HiRes aerial imagery;LGL, www.lgl-bw.de", + "wheelchair": "limited" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:36:38Z", + "version": "19", + "changeset": "122803935", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4757572, + 47.6521043 + ] + } + }, + { + "type": "Feature", + "id": "node/2428180099", + "properties": { + "type": "node", + "id": "2428180099", + "tags": { + "addr:city": "Friedrichshafen", + "addr:housenumber": "17", + "addr:postcode": "88045", + "addr:street": "Karlstraße", + "beauty": "permanent_makeup;cosmetics", + "contact:mobile": "+4915164764760", + "email": "janka.g@gmx.net", + "name": "Simplicity by Janine", + "opening_hours": "\"nach Vereinbarung\"", + "operator": "Janine Föhr", + "shop": "beauty", + "website": "http://www.simplicitybyjanine.de/", + "wheelchair": "no" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:31:34Z", + "version": "7", + "changeset": "122803735", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4783183, + 47.6505193 + ] + } + }, + { + "type": "Feature", + "id": "node/3611264529", + "properties": { + "type": "node", + "id": "3611264529", + "tags": { + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "17", + "addr:postcode": "88045", + "addr:street": "Wilhelmstraße", + "brand": "orsay", + "brand:wikidata": "Q883245", + "brand:wikipedia": "de:Orsay (Modeunternehmen)", + "contact:phone": "+49 7541 391713", + "contact:website": "http://world.orsay.com/", + "disused:shop": "clothes", + "name": "orsay", + "note": "Hat geschlossen seit 2022-07-01", + "opening_hours": "Mo-We 09:30-19:00; Th-Fr 09:30-20:00; Sa 09:30-18:30; PH off", + "toilets:wheelchair": "no", + "wheelchair": "yes", + "wheelchair:description": "keine ausgewiesenen behindertengerechten Umkleidekabinen" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T11:57:19Z", + "version": "12", + "changeset": "122800198", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4808659, + 47.6506943 + ] + } + }, + { + "type": "Feature", + "id": "node/3796637528", + "properties": { + "type": "node", + "id": "3796637528", + "tags": { + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "81", + "addr:postcode": "88045", + "addr:street": "Friedrichstraße", + "brand": "Hallhuber", + "brand:wikidata": "Q1571714", + "brand:wikipedia": "de:Hallhuber (Bekleidungshaus)", + "clothes": "women", + "disused:shop": "clothes", + "name": "Hallhuber", + "opening_hours": "09:30-18:30; Sa 09:30-17:00; Su,PH off", + "phone": "+49 7541 3748875", + "start_date": "14.03.2019", + "website": "https://www.hallhuber.com/de/", + "wheelchair": "limited" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T11:57:19Z", + "version": "8", + "changeset": "122800198", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4788388, + 47.6518755 + ] + } + }, + { + "type": "Feature", + "id": "node/3835594967", + "properties": { + "type": "node", + "id": "3835594967", + "tags": { + "addr:city": "Friedrichshafen", + "addr:housenumber": "1", + "addr:postcode": "88045", + "addr:street": "Karlstraße", + "contact:instagram": "https://www.instagram.com/dizi_mode/", + "email": "kontakt@boutique-pinos.de", + "name": "DIZI Boutique Pinos", + "opening_hours": "Mo-Fr 10:00-18:00; Sa 10:00-16:00; Su,PH off", + "operator": "Patrizia Pinos-Schwarzott", + "phone": "+49 7541 25276", + "shop": "clothes", + "website": "https://dizimode.de/", + "wheelchair": "limited" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-18T11:43:14Z", + "version": "6", + "changeset": "122543467", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4774224, + 47.6516663 + ] + } + }, + { + "type": "Feature", + "id": "node/4272229178", + "properties": { + "type": "node", + "id": "4272229178", + "tags": { + "amenity": "luggage_locker" + }, + "relations": [], + "meta": { + "timestamp": "2024-02-12T12:16:12Z", + "version": "3", + "changeset": "122800892", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4742035, + 47.6529935 + ] + } + }, + { + "type": "Feature", + "id": "node/4802419050", + "properties": { + "type": "node", + "id": "4802419050", + "tags": { + "amenity": "social_facility", + "brand": "Bahnhofsmission", + "brand:wikidata": "Q801713", + "email": "friedrichshafen@bahnhofsmission.de", + "fixme": "Die Bahnhofsmission Friedrichshafen hat ende Juni 2022 neueröffnet. Bin mir aber nicht sicher, ob hier oder im östlichen Bereich des Bahnhofs nahe der Polizeistation.", + "name": "Bahnhofsmission", + "opening_hours": "Mo-Fr 09:00-18:00; Sa 09:00-12:00; Su,PH off", + "phone": "+49 7541 21976", + "social_facility": "outreach", + "social_facility:for": "homeless;underprivileged", + "website": "https://www.bahnhofsmission.de/index.php?id=99&woher=&bm=20" + }, + "relations": [], + "meta": { + "timestamp": "2024-02-12T12:16:12Z", + "version": "6", + "changeset": "122800892", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4728883, + 47.6530057 + ] + } + }, + { + "type": "Feature", + "id": "node/4852612818", + "properties": { + "type": "node", + "id": "4852612818", + "tags": { + "amenity": "cafe", + "contact:instagram": "https://www.instagram.com/thesecretcafefn/", + "contact:phone": "+4975419555606", + "name": "The Secret Café", + "opening_hours": "Mo-Fr 09:00-18:00; Sa 10:00-18:00", + "operator": "Maria Arnaldos", + "website": "https://linktr.ee/thesecretcafe" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-18T12:17:18Z", + "version": "8", + "changeset": "122544637", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4789354, + 47.6525494 + ] + } + }, + { + "type": "Feature", + "id": "node/9374440942", + "properties": { + "type": "node", + "id": "9374440942", + "tags": { + "addr:city": "Friedrichshafen", + "addr:housenumber": "17", + "addr:postcode": "88045", + "addr:street": "Karlstraße", + "alt_name": "STUDIO EYE WEAR SUN", + "description": "Sonnenbrillenabteilung von \"Studio Eye Wear\"", + "email": "info@studioeyewear.de", + "facebook": "https://de-de.facebook.com/seeeyewearfn/", + "name": "STUDIO EYE WEAR", + "old_name": "Brillen Maurer", + "opening_date": "2022", + "opening_hours": "Tu-Sa 12:00-18:00", + "operator": "Renate Rauschendorfer-Lux", + "phone": "+49 7541 22109", + "shop": "optician", + "website": "https://www.studioeyewear.de/", + "wheelchair": "no" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:31:34Z", + "version": "4", + "changeset": "122803735", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.478376, + 47.6504567 + ] + } + }, + { + "type": "Feature", + "id": "node/9379214001", + "properties": { + "type": "node", + "id": "9379214001", + "tags": { + "contact:mobile": "+49 151 11350030", + "description": "Angebotene Tests:\nAntigentest Schnelltest: Kostenloser Bürgertest – Corona Test (15-30 Min)", + "disused:healthcare": "centre", + "email": "testandseebodensee@gmail.com", + "healthcare:speciality": "covid19;testing", + "name": "Test & See Testzentrum", + "opening_hours": "Mo 09:00-15:00; Tu-Fr 09:00-19:00; Sa 09:00-18:00; Su 09:00-17:00", + "operator": "Dominik Kleine", + "website": "https://testkalender.de/test-und-see" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:31:34Z", + "version": "2", + "changeset": "122803735", + "user": "rene78", + "uid": "257555" + } + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4783964, + 47.6504628 + ] + } + } + ] +}; + +const oldGeoJSON = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "way/437103103", + "properties": { + "type": "way", + "id": 437103103, + "tags": { + "access": "no", + "area": "yes", + "construction:public_transport": "platform", + "construction:railway": "platform", + "level": "0", + "note": "Aufgrund von Bahnsteig-Bauarbeiten gesperrt.", + "ref": "1", + "ref:IFOPT": "de:08435:44002:2:1", + "ref:IFOPT:description": "Gleis 1", + "tactile_paving": "no", + "wheelchair": "limited", + "width": "2" + }, + "relations": [], + "meta": { + "timestamp": "2024-04-15T12:16:12Z", + "version": "5", + "changeset": "122800892", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.4706024, + 47.6532025 + ], + [ + 9.4706017, + 47.653169 + ], + [ + 9.4710014, + 47.65315 + ], + [ + 9.4718369, + 47.653113 + ], + [ + 9.4729943, + 47.6530677 + ], + [ + 9.4737966, + 47.6530304 + ], + [ + 9.4738814, + 47.6530265 + ], + [ + 9.473957, + 47.6530225 + ], + [ + 9.4747243, + 47.6529874 + ], + [ + 9.4747223, + 47.6529739 + ], + [ + 9.4747349, + 47.6529729 + ], + [ + 9.4747887, + 47.6529714 + ], + [ + 9.4748711, + 47.6529685 + ], + [ + 9.4749543, + 47.6529657 + ], + [ + 9.4754599, + 47.6529481 + ], + [ + 9.4754633, + 47.6529775 + ], + [ + 9.4752889, + 47.6529852 + ], + [ + 9.4743743, + 47.6530303 + ], + [ + 9.4729869, + 47.6530972 + ], + [ + 9.4721361, + 47.6531387 + ], + [ + 9.4710155, + 47.6531934 + ], + [ + 9.4706024, + 47.6532025 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/290918616", + "properties": { + "type": "way", + "id": 290918616, + "tags": { + "amenity": "kindergarten", + "building": "kindergarten" + }, + "relations": [], + "meta": { + "timestamp": "2024-07-29T13:56:35Z", + "version": "4", + "changeset": "154564334", + "user": "mcliquid", + "uid": "1213571" + }, + "__is_old__": true + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.5044805, + 47.6561876 + ], + [ + 9.5044805, + 47.6561932 + ], + [ + 9.5044941, + 47.6561932 + ], + [ + 9.5044941, + 47.6561951 + ], + [ + 9.5044941, + 47.6562054 + ], + [ + 9.5044383, + 47.6562055 + ], + [ + 9.5044383, + 47.6562462 + ], + [ + 9.504309, + 47.6562462 + ], + [ + 9.5043089, + 47.656135 + ], + [ + 9.504436, + 47.6561349 + ], + [ + 9.504436, + 47.6561876 + ], + [ + 9.5044805, + 47.6561876 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/290918617", + "properties": { + "type": "way", + "id": 290918617, + "tags": { + "amenity": "kindergarten", + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "66", + "addr:postcode": "88046", + "addr:street": "Länderöschstraße", + "building": "kindergarten" + }, + "relations": [], + "meta": { + "timestamp": "2024-07-29T13:56:35Z", + "version": "6", + "changeset": "154564334", + "user": "mcliquid", + "uid": "1213571" + }, + "__is_old__": true + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.5044941, + 47.6561951 + ], + [ + 9.5046029, + 47.6561951 + ], + [ + 9.5047564, + 47.656195 + ], + [ + 9.5047565, + 47.6562775 + ], + [ + 9.5044942, + 47.6562776 + ], + [ + 9.5044941, + 47.6562054 + ], + [ + 9.5044941, + 47.6561951 + ] + ] + ] + } + }, + { + "type": "Feature", + "id": "way/999999999", + "properties": { + "type": "way", + "id": 999999999, + "tags": { + "highway": "service", + "service": "driveway" + }, + "relations": [], + "meta": { + "timestamp": "2025-04-01T21:20:54Z", + "version": "1", + "changeset": "103017971", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 9.4785357, + 47.6568163 + ], + [ + 9.477, + 47.656 + ] + ] + } + }, + { + "type": "Feature", + "id": "way/332742460", + "properties": { + "type": "way", + "id": 332742460, + "tags": { + "barrier": "fence" + }, + "relations": [], + "meta": { + "timestamp": "2024-07-29T13:57:05Z", + "version": "3", + "changeset": "154564373", + "user": "mcliquid", + "uid": "1213571" + }, + "__is_old__": true + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 9.5043791, + 47.6559249 + ], + [ + 9.5042432, + 47.6560184 + ], + [ + 9.5041133, + 47.6561067 + ], + [ + 9.5040158, + 47.6561836 + ], + [ + 9.5039441, + 47.6562825 + ], + [ + 9.5039195, + 47.65637 + ], + [ + 9.5039139, + 47.656456 + ], + [ + 9.5039206, + 47.6566024 + ], + [ + 9.5039262, + 47.656779 + ], + [ + 9.5039419, + 47.6569412 + ], + [ + 9.5039676, + 47.6569488 + ], + [ + 9.5040214, + 47.6568801 + ], + [ + 9.5041166, + 47.6567843 + ], + [ + 9.5041939, + 47.6567133 + ], + [ + 9.504297, + 47.6566341 + ], + [ + 9.5043983, + 47.6565607 + ], + [ + 9.5044169, + 47.6565473 + ], + [ + 9.5045905, + 47.6564251 + ], + [ + 9.5047589, + 47.6563188 + ], + [ + 9.5049101, + 47.656226 + ], + [ + 9.5049274, + 47.6562154 + ], + [ + 9.5049605, + 47.6561951 + ], + [ + 9.505067, + 47.6561309 + ], + [ + 9.5051644, + 47.6560759 + ], + [ + 9.5052697, + 47.6560155 + ], + [ + 9.5053414, + 47.655977 + ], + [ + 9.5049169, + 47.6556337 + ], + [ + 9.5046301, + 47.6557544 + ], + [ + 9.5044464, + 47.6558819 + ], + [ + 9.5043791, + 47.6559249 + ] + ] + } + }, + { + "type": "Feature", + "id": "node/1501", + "properties": { + "type": "node", + "id": "1501", + "tags": { + "amenity": "bar", + "name": "foobar", + "description": "Only a test", + "foo": "bar" + }, + "relations": [], + "meta": { + "timestamp": "2025-04-05T19:02:11Z", + "version": "1", + "changeset": "493", + "user": "thisIsASuperLongUserName", + "uid": "47" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.47, + 47.65 + ] + } + }, + { + "type": "Feature", + "id": "node/2193083033", + "properties": { + "type": "node", + "id": "2193083033", + "tags": { + "wheelchair": "limited", + "disused:shop": "toys", + "source": "HiRes aerial imagery;LGL, www.lgl-bw.de", + "opening_hours": "Mo off; Tu-Fr 09:30-16:30; Sa 09:30-16:00; Su,PH off", + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "57", + "addr:postcode": "88045", + "addr:street": "Friedrichstraße", + "contact:facebook": "https://www.facebook.com/SpielwarenDauth/", + "contact:instagram": "https://www.instagram.com/spielwarenschinacher/", + "contact:phone": "+49 7541 92210", + "contact:website": "https://www.schinacher.de/" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:36:38Z", + "version": "17", + "changeset": "122803935", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4757572, + 47.6521043 + ] + } + }, + { + "type": "Feature", + "id": "node/2428180099", + "properties": { + "type": "node", + "id": "2428180099", + "tags": { + "wheelchair": "no", + "addr:street": "Karlstraße", + "addr:postcode": "88045", + "fixme": "Keine Ahnung was das ist. Massagesalon?", + "name": "Natural Touch", + "addr:housenumber": "17", + "addr:city": "Friedrichshafen", + "shop": "massage" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:31:34Z", + "version": "6", + "changeset": "122803735", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4783183, + 47.6505193 + ] + } + }, + { + "type": "Feature", + "id": "node/3611264529", + "properties": { + "type": "node", + "id": "3611264529", + "tags": { + "name": "orsay", + "wheelchair:description": "keine ausgewiesenen behindertengerechten Umkleidekabinen", + "wheelchair": "yes", + "shop": "clothes", + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "17", + "addr:postcode": "88045", + "addr:street": "Wilhelmstraße", + "brand": "orsay", + "brand:wikidata": "Q883245", + "brand:wikipedia": "de:Orsay (Modeunternehmen)", + "contact:phone": "+49 7541 391713", + "contact:website": "http://world.orsay.com/", + "opening_hours": "Mo-We 09:30-19:00; Th-Fr 09:30-20:00; Sa 09:30-18:30; PH off", + "toilets:wheelchair": "no" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T11:57:19Z", + "version": "10", + "changeset": "122800198", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4808659, + 47.6506943 + ] + } + }, + { + "type": "Feature", + "id": "node/3796637528", + "properties": { + "type": "node", + "id": "3796637528", + "tags": { + "name": "Hallhuber", + "wheelchair": "limited", + "shop": "clothes", + "addr:city": "Friedrichshafen", + "addr:country": "DE", + "addr:housenumber": "81", + "addr:postcode": "88045", + "addr:street": "Friedrichstraße", + "brand": "Hallhuber", + "brand:wikidata": "Q1571714", + "brand:wikipedia": "de:Hallhuber (Bekleidungshaus)", + "clothes": "women", + "opening_hours": "09:30-18:30; Sa 09:30-17:00; Su,PH off", + "phone": "+49 7541 3748875", + "start_date": "14.03.2019", + "website": "https://www.hallhuber.com/de/" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T11:57:19Z", + "version": "6", + "changeset": "122800198", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4788388, + 47.6518755 + ] + } + }, + { + "type": "Feature", + "id": "node/3835594967", + "properties": { + "type": "node", + "id": "3835594967", + "tags": { + "name": "dizi", + "addr:city": "Friedrichshafen", + "addr:housenumber": "1", + "addr:postcode": "88045", + "addr:street": "Karlstraße", + "contact:instagram": "https://www.instagram.com/dizi_mode/", + "email": "kontakt@boutique-pinos.de", + "opening_hours": "Mo-Fr 10:00-18:00; Sa 10:00-16:00; Su,PH off", + "operator": "Patrizia Pinos-Schwarzott", + "phone": "+49 7541 25276", + "shop": "clothes", + "website": "https://dizimode.de/", + "wheelchair": "limited" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-18T11:43:14Z", + "version": "5", + "changeset": "122543467", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4774224, + 47.6516663 + ] + } + }, + { + "type": "Feature", + "id": "node/4272229178", + "properties": { + "type": "node", + "id": "4272229178", + "tags": { + "amenity": "luggage_locker" + }, + "relations": [], + "meta": { + "timestamp": "2024-02-12T12:16:12Z", + "version": "2", + "changeset": "122800892", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.474201, + 47.6530305 + ] + } + }, + { + "type": "Feature", + "id": "node/4802419050", + "properties": { + "type": "node", + "id": "4802419050", + "tags": { + "website": "https://www.bahnhofsmission.de/index.php?id=99&woher=&bm=20", + "social_facility": "outreach", + "phone": "+49 7541 21976", + "disused:amenity": "social_facility", + "opening_hours": "Mo-Fr 09:00-18:00; Sa 09:00-12:00; Su,PH off", + "note": "Die Bahnhofsmission Friedrichshafen ist bis auf Weiteres geschlossen.", + "social_facility:for": "underprivileged", + "email": "friedrichshafen@bahnhofsmission.de", + "name": "Bahnhofsmission" + }, + "relations": [], + "meta": { + "timestamp": "2024-02-12T12:16:12Z", + "version": "5", + "changeset": "122800892", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4728883, + 47.6530057 + ] + } + }, + { + "type": "Feature", + "id": "node/4852612818", + "properties": { + "type": "node", + "id": "4852612818", + "tags": { + "start_date": "2017-05-01", + "contact:email": "info@mut-fn.de", + "contact:facebook": "https://www.facebook.com/mutfriedrichshafen/", + "operator": "Nazlı Yucad", + "contact:phone": "+49 7541 3760366", + "description": "Laden, der Geschenke, Karten, Kaftans, Babysachen, Deko und Kleinigkeiten verkauft.", + "disused:amenity": "cafe", + "disused:shop": "interior_decoration", + "name": "mut - Kostbares aus Nah und Fern", + "note": "In Wilhelmstraße 9 umgezogen", + "opening_hours": "Mo,Tu,Th,Fr 10:00-19:00; Sa 10:00-14:00" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-18T12:17:18Z", + "version": "7", + "changeset": "122544637", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4789354, + 47.6525494 + ] + } + }, + { + "type": "Feature", + "id": "node/9374440942", + "properties": { + "type": "node", + "id": "9374440942", + "tags": { + "description": "Wird wohl mal die Sonnenbrillenabteilung von \"Studio Eye Wear\". Momentan (2021-12) Corona Testzentrum", + "disused:shop": "optician", + "wheelchair": "no", + "addr:city": "Friedrichshafen", + "addr:housenumber": "17", + "addr:postcode": "88045", + "addr:street": "Karlstraße", + "alt_name": "STUDIO EYE WEAR SUN", + "email": "info@studioeyewear.de", + "facebook": "https://de-de.facebook.com/seeeyewearfn/", + "name": "STUDIO EYE WEAR", + "old_name": "Brillen Maurer", + "opening_date": "2022", + "opening_hours": "Tu-Sa 12:00-18:00", + "operator": "Renate Rauschendorfer-Lux", + "phone": "+49 7541 22109", + "website": "https://www.studioeyewear.de/" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:31:34Z", + "version": "3", + "changeset": "122803735", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4784087, + 47.6504568 + ] + } + }, + { + "type": "Feature", + "id": "node/9379214001", + "properties": { + "type": "node", + "id": "9379214001", + "tags": { + "website": "https://testkalender.de/test-und-see", + "healthcare": "centre", + "contact:mobile": "+49 151 11350030", + "description": "Angebotene Tests:\nAntigentest Schnelltest: Kostenloser Bürgertest – Corona Test (15-30 Min)", + "email": "testandseebodensee@gmail.com", + "healthcare:speciality": "covid19;testing", + "name": "Test & See Testzentrum", + "opening_hours": "Mo 09:00-15:00; Tu-Fr 09:00-19:00; Sa 09:00-18:00; Su 09:00-17:00", + "operator": "Dominik Kleine" + }, + "relations": [], + "meta": { + "timestamp": "2022-06-24T13:31:34Z", + "version": "1", + "changeset": "122803735", + "user": "rene78", + "uid": "257555" + }, + "__is_old__": true + }, + "geometry": { + "type": "Point", + "coordinates": [ + 9.4783798, + 47.6504572 + ] + } + } + ] +}; \ No newline at end of file diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index 8618121c..00000000 Binary files a/favicon.ico and /dev/null differ diff --git a/img/SocialMedia-Latest-Changes.png b/img/SocialMedia-Latest-Changes.png new file mode 100644 index 00000000..a2b43fd5 Binary files /dev/null and b/img/SocialMedia-Latest-Changes.png differ diff --git a/img/SocialMedia-Latest-Changes.svg b/img/SocialMedia-Latest-Changes.svg new file mode 100644 index 00000000..c24bd025 --- /dev/null +++ b/img/SocialMedia-Latest-Changes.svg @@ -0,0 +1,131 @@ + + + + + + + + image/svg+xml + + + + + + + + Latest Changes + Help to keep OSM clean + + diff --git a/img/Swiss_railway_clock.svg b/img/Swiss_railway_clock.svg new file mode 100644 index 00000000..c4222ea2 --- /dev/null +++ b/img/Swiss_railway_clock.svg @@ -0,0 +1,53 @@ + + + Swiss Railway Clock + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/android-chrome-192x192.png b/img/android-chrome-192x192.png new file mode 100644 index 00000000..ce9bd8ff Binary files /dev/null and b/img/android-chrome-192x192.png differ diff --git a/img/android-chrome-512x512.png b/img/android-chrome-512x512.png new file mode 100644 index 00000000..bd330869 Binary files /dev/null and b/img/android-chrome-512x512.png differ diff --git a/img/apple-touch-icon.png b/img/apple-touch-icon.png new file mode 100644 index 00000000..0452edcb Binary files /dev/null and b/img/apple-touch-icon.png differ diff --git a/img/browserconfig.xml b/img/browserconfig.xml new file mode 100644 index 00000000..1affccc5 --- /dev/null +++ b/img/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/img/favicon-16x16.png b/img/favicon-16x16.png new file mode 100644 index 00000000..5ae3c877 Binary files /dev/null and b/img/favicon-16x16.png differ diff --git a/img/favicon-32x32.png b/img/favicon-32x32.png new file mode 100644 index 00000000..61980c07 Binary files /dev/null and b/img/favicon-32x32.png differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 00000000..83da6ae5 Binary files /dev/null and b/img/favicon.ico differ diff --git a/img/gmaps-logo.png b/img/gmaps-logo.png new file mode 100644 index 00000000..c0238e1b Binary files /dev/null and b/img/gmaps-logo.png differ diff --git a/img/icon-sidebar-left.svg b/img/icon-sidebar-left.svg new file mode 100644 index 00000000..618ce80f --- /dev/null +++ b/img/icon-sidebar-left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/img/icons.svg b/img/icons.svg new file mode 100644 index 00000000..adfe734b --- /dev/null +++ b/img/icons.svg @@ -0,0 +1,32 @@ + + + + + +> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/mstile-144x144.png b/img/mstile-144x144.png new file mode 100644 index 00000000..58fcf8f4 Binary files /dev/null and b/img/mstile-144x144.png differ diff --git a/img/mstile-150x150.png b/img/mstile-150x150.png new file mode 100644 index 00000000..42a720e3 Binary files /dev/null and b/img/mstile-150x150.png differ diff --git a/img/mstile-310x150.png b/img/mstile-310x150.png new file mode 100644 index 00000000..4ecb5812 Binary files /dev/null and b/img/mstile-310x150.png differ diff --git a/img/mstile-310x310.png b/img/mstile-310x310.png new file mode 100644 index 00000000..23ae4ca2 Binary files /dev/null and b/img/mstile-310x310.png differ diff --git a/img/mstile-70x70.png b/img/mstile-70x70.png new file mode 100644 index 00000000..0a7be54e Binary files /dev/null and b/img/mstile-70x70.png differ diff --git a/img/multi-devices(2023).png b/img/multi-devices(2023).png new file mode 100644 index 00000000..023b801c Binary files /dev/null and b/img/multi-devices(2023).png differ diff --git a/img/multi-devices.png b/img/multi-devices.png new file mode 100644 index 00000000..f26ab3da Binary files /dev/null and b/img/multi-devices.png differ diff --git a/img/osm-logo.png b/img/osm-logo.png new file mode 100644 index 00000000..2a81559c Binary files /dev/null and b/img/osm-logo.png differ diff --git a/img/safari-pinned-tab.svg b/img/safari-pinned-tab.svg new file mode 100644 index 00000000..7904b675 --- /dev/null +++ b/img/safari-pinned-tab.svg @@ -0,0 +1,162 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/site.webmanifest b/img/site.webmanifest new file mode 100644 index 00000000..673e97fe --- /dev/null +++ b/img/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "./android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/index.html b/index.html index dec19d69..63c67479 100644 --- a/index.html +++ b/index.html @@ -2,54 +2,138 @@ + - - Latest changes on OpenStreetMap - - + + OSM Latest Changes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + -
-
-
-

Explore latest changes on OpenStreetMap

- Logo - - - -
-
+
+ -
-
Zoom in to view changes
+
+
+
Zoom in to download changes
+
- - - - - - - \ No newline at end of file diff --git a/js/L.Control.Locate.min.js b/js/L.Control.Locate.min.js new file mode 100644 index 00000000..c5a6c01c --- /dev/null +++ b/js/L.Control.Locate.min.js @@ -0,0 +1,5 @@ +/*! Version: 0.76.0 +Copyright (c) 2016 Dominik Moritz */ + +!function(t,i){"function"==typeof define&&define.amd?define(["leaflet"],t):"object"==typeof exports&&(void 0!==i&&i.L?module.exports=t(L):module.exports=t(require("leaflet"))),void 0!==i&&i.L&&(i.L.Control.Locate=t(L))}(function(l){const s=(i,s,t)=>{(t=t.split(" ")).forEach(function(t){l.DomUtil[i].call(this,s,t)})},i=(t,i)=>s("addClass",t,i),o=(t,i)=>s("removeClass",t,i),t=l.Marker.extend({initialize(t,i){l.Util.setOptions(this,i),this._latlng=t,this.createIcon()},createIcon(){var t=this.options;let i="";void 0!==t.color&&(i+=`stroke:${t.color};`),void 0!==t.weight&&(i+=`stroke-width:${t.weight};`),void 0!==t.fillColor&&(i+=`fill:${t.fillColor};`),void 0!==t.fillOpacity&&(i+=`fill-opacity:${t.fillOpacity};`),void 0!==t.opacity&&(i+=`opacity:${t.opacity};`);t=this._getIconSVG(t,i);this._locationIcon=l.divIcon({className:t.className,html:t.svg,iconSize:[t.w,t.h]}),this.setIcon(this._locationIcon)},_getIconSVG(t,i){var s=t.radius,o=s+t.weight,t=2*o;return{className:"leaflet-control-locate-location",svg:``+'',w:t,h:t}},setStyle(t){l.Util.setOptions(this,t),this.createIcon()}});var e=t.extend({initialize(t,i,s){l.Util.setOptions(this,s),this._latlng=t,this._heading=i,this.createIcon()},setHeading(t){this._heading=t},_getIconSVG(t,i){var s=t.radius,o=t.width+t.weight,s=2*(s+t.depth+t.weight),t=`M0,0 l${t.width/2},${t.depth} l-${o},0 z`;return{className:"leaflet-control-locate-heading",svg:``+'',w:o,h:s}}}),e=l.Control.extend({options:{position:"topleft",layer:void 0,setView:"untilPanOrZoom",keepCurrentZoomLevel:!1,initialZoomLevel:!1,getLocationBounds(t){return t.bounds},flyTo:!1,clickBehavior:{inView:"stop",outOfView:"setView",inViewNotFollowing:"inView"},returnToPrevBounds:!1,cacheLocation:!0,drawCircle:!0,drawMarker:!0,showCompass:!0,markerClass:t,compassClass:e,circleStyle:{className:"leaflet-control-locate-circle",color:"#136AEC",fillColor:"#136AEC",fillOpacity:.15,weight:0},markerStyle:{className:"leaflet-control-locate-marker",color:"#fff",fillColor:"#2A93EE",fillOpacity:1,weight:3,opacity:1,radius:9},compassStyle:{fillColor:"#2A93EE",fillOpacity:1,weight:0,color:"#fff",opacity:1,radius:9,width:9,depth:6},followCircleStyle:{},followMarkerStyle:{},followCompassStyle:{},icon:"leaflet-control-locate-location-arrow",iconLoading:"leaflet-control-locate-spinner",iconElementTag:"span",textElementTag:"small",circlePadding:[0,0],metric:!0,createButtonCallback(t,i){const s=l.DomUtil.create("a","leaflet-bar-part leaflet-bar-part-single",t);s.title=i.strings.title,s.role="button",s.href="#";const o=l.DomUtil.create(i.iconElementTag,i.icon,s);if(void 0!==i.strings.text){const e=l.DomUtil.create(i.textElementTag,"leaflet-locate-text",s);e.textContent=i.strings.text,s.classList.add("leaflet-locate-text-active"),s.parentNode.style.display="flex",0new l.Control.Locate(t),e},window); +//# sourceMappingURL=L.Control.Locate.min.js.map \ No newline at end of file diff --git a/js/d3-queue.v3.min.js b/js/d3-queue.v3.min.js deleted file mode 100644 index ba88d874..00000000 --- a/js/d3-queue.v3.min.js +++ /dev/null @@ -1,2 +0,0 @@ -// https://d3js.org/d3-queue/ Version 3.0.7. Copyright 2017 Mike Bostock. -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.d3=t.d3||{})}(this,function(t){"use strict";function i(t){this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function r(t){if(!t._start)try{n(t)}catch(i){if(t._tasks[t._ended+t._active-1])e(t,i);else if(!t._data)throw i}}function n(t){for(;t._start=t._waiting&&t._active=0;)if((r=t._tasks[n])&&(t._tasks[n]=null,r.abort))try{r.abort()}catch(i){}t._active=NaN,o(t)}function o(t){if(!t._active&&t._call){var i=t._data;t._data=void 0,t._call(t._error,i)}}function s(t){if(null==t)t=1/0;else if(!((t=+t)>=1))throw new Error("invalid concurrency");return new i(t)}var l=[].slice,c={};i.prototype=s.prototype={constructor:i,defer:function(t){if("function"!=typeof t)throw new Error("invalid callback");if(this._call)throw new Error("defer after await");if(null!=this._error)return this;var i=l.call(arguments,1);return i.push(t),++this._waiting,this._tasks.push(i),r(this),this},abort:function(){return null==this._error&&e(this,new Error("abort")),this},await:function(t){if("function"!=typeof t)throw new Error("invalid callback");if(this._call)throw new Error("multiple await");return this._call=function(i,r){t.apply(null,[i].concat(r))},o(this),this},awaitAll:function(t){if("function"!=typeof t)throw new Error("invalid callback");if(this._call)throw new Error("multiple await");return this._call=t,o(this),this}},t.queue=s,Object.defineProperty(t,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/js/d3-selection.min.js b/js/d3-selection.min.js new file mode 100644 index 00000000..8a660b55 --- /dev/null +++ b/js/d3-selection.min.js @@ -0,0 +1,2 @@ +// https://d3js.org/d3-selection/ v3.0.0 Copyright 2010-2021 Mike Bostock +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";var n="http://www.w3.org/1999/xhtml",e={svg:"http://www.w3.org/2000/svg",xhtml:n,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function r(t){var n=t+="",r=n.indexOf(":");return r>=0&&"xmlns"!==(n=t.slice(0,r))&&(t=t.slice(r+1)),e.hasOwnProperty(n)?{space:e[n],local:t}:t}function i(t){return function(){var e=this.ownerDocument,r=this.namespaceURI;return r===n&&e.documentElement.namespaceURI===n?e.createElement(t):e.createElementNS(r,t)}}function o(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function u(t){var n=r(t);return(n.local?o:i)(n)}function s(){}function c(t){return null==t?s:function(){return this.querySelector(t)}}function l(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function a(){return[]}function f(t){return null==t?a:function(){return this.querySelectorAll(t)}}function h(t){return function(){return this.matches(t)}}function p(t){return function(n){return n.matches(t)}}var _=Array.prototype.find;function d(){return this.firstElementChild}var y=Array.prototype.filter;function m(){return Array.from(this.children)}function v(t){return new Array(t.length)}function g(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function w(t){return function(){return t}}function A(t,n,e,r,i,o){for(var u,s=0,c=n.length,l=o.length;sn?1:t>=n?0:NaN}function N(t){return function(){this.removeAttribute(t)}}function C(t){return function(){this.removeAttributeNS(t.space,t.local)}}function L(t,n){return function(){this.setAttribute(t,n)}}function P(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function T(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function B(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function M(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function q(t){return function(){this.style.removeProperty(t)}}function D(t,n,e){return function(){this.style.setProperty(t,n,e)}}function O(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function V(t,n){return t.style.getPropertyValue(n)||M(t).getComputedStyle(t,null).getPropertyValue(n)}function j(t){return function(){delete this[t]}}function R(t,n){return function(){this[t]=n}}function H(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function I(t){return t.trim().split(/^|\s+/)}function U(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=I(t.getAttribute("class")||"")}function G(t,n){for(var e=U(t),r=-1,i=n.length;++r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}function st(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var ht=[null];function pt(t,n){this._groups=t,this._parents=n}function _t(){return new pt([[document.documentElement]],ht)}function dt(t){return"string"==typeof t?new pt([[document.querySelector(t)]],[document.documentElement]):new pt([[t]],ht)}pt.prototype=_t.prototype={constructor:pt,select:function(t){"function"!=typeof t&&(t=c(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=N&&(N=E+1);!(g=y[N])&&++N<_;);v._next=g||null}}return(u=new pt(u,r))._enter=s,u._exit=c,u},enter:function(){return new pt(this._enter||this._groups.map(v),this._parents)},exit:function(){return new pt(this._exit||this._groups.map(v),this._parents)},join:function(t,n,e){var r=this.enter(),i=this,o=this.exit();return"function"==typeof t?(r=t(r))&&(r=r.selection()):r=r.append(t+""),null!=n&&(i=n(i))&&(i=i.selection()),null==e?o.remove():e(o),r&&i?r.merge(i).order():i},merge:function(t){for(var n=t.selection?t.selection():t,e=this._groups,r=n._groups,i=e.length,o=r.length,u=Math.min(i,o),s=new Array(i),c=0;c=0;)(r=i[o])&&(u&&4^r.compareDocumentPosition(u)&&u.parentNode.insertBefore(r,u),u=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=E);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?q:"function"==typeof n?O:D)(t,n,null==e?"":e)):V(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?j:"function"==typeof n?H:R)(t,n)):this.node()[t]},classed:function(t,n){var e=I(t+"");if(arguments.length<2){for(var r=U(this.node()),i=-1,o=e.length;++iwt(t,n)))},t.select=dt,t.selectAll=function(t){return"string"==typeof t?new pt([document.querySelectorAll(t)],[document.documentElement]):new pt([l(t)],ht)},t.selection=_t,t.selector=c,t.selectorAll=f,t.style=V,t.window=M,Object.defineProperty(t,"__esModule",{value:!0})})); \ No newline at end of file diff --git a/js/d3.v3.min.js b/js/d3.v3.min.js deleted file mode 100644 index 1ff6ccd3..00000000 --- a/js/d3.v3.min.js +++ /dev/null @@ -1,5 +0,0 @@ -d3=function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(){}function a(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function o(){}function c(n){function t(){for(var t,r=e,u=-1,i=r.length;++uva&&(fa.scrollX||fa.scrollY)){e=ca.select(la.body).append("svg").style("position","absolute").style("top",0).style("left",0);var u=e[0][0].getScreenCTM();va=!(u.f||u.e),e.remove()}return va?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function p(n){for(var t=-1,e=n.length,r=[];++t0&&(n=n.substring(0,o));var l=Na.get(n);return l&&(n=l,c=D),o?t?u:r:t?T:i}function z(n,t){return function(e){var r=ca.event;ca.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ca.event=r}}}function D(n,t){var e=z(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||r.compareDocumentPosition(t)&8)||e.call(t,n)}}function j(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],a=0,o=i.length;o>a;a++)(u=i[a])&&t(u,a,e);return n}function L(n){return xa(n,Ta),n}function F(){}function H(n,t,e){return new P(n,t,e)}function P(n,t,e){this.h=n,this.s=t,this.l=e}function R(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(a-i)*n/60:180>n?a:240>n?i+(a-i)*(240-n)/60:i}function u(n){return Math.round(r(n)*255)}var i,a;return n%=360,0>n&&(n+=360),t=0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,i=2*e-a,et(u(n+120),u(n),u(n-120))}function O(n){return n>0?1:0>n?-1:0}function Y(n){return Math.acos(Math.max(-1,Math.min(1,n)))}function U(n){return n>1?Fa/2:-1>n?-Fa/2:Math.asin(n)}function I(n){return(Math.exp(n)-Math.exp(-n))/2}function V(n){return(Math.exp(n)+Math.exp(-n))/2}function X(n){return(n=Math.sin(n/2))*n}function Z(n,t,e){return new B(n,t,e)}function B(n,t,e){this.h=n,this.c=t,this.l=e}function $(n,t,e){return J(e,Math.cos(n*=Pa)*t,Math.sin(n)*t)}function J(n,t,e){return new G(n,t,e)}function G(n,t,e){this.l=n,this.a=t,this.b=e}function K(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=Q(u)*Ua,r=Q(r)*Ia,i=Q(i)*Va,et(tt(3.2404542*u-1.5371385*r-.4985314*i),tt(-.969266*u+1.8760108*r+.041556*i),tt(.0556434*u-.2040259*r+1.0572252*i))}function W(n,t,e){return Z(Math.atan2(e,t)*Ra,Math.sqrt(t*t+e*e),n)}function Q(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function nt(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function tt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function et(n,t,e){return new rt(n,t,e)}function rt(n,t,e){this.r=n,this.g=t,this.b=e}function ut(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function it(n,t,e){var r,u,i,a=0,o=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(lt(u[0]),lt(u[1]),lt(u[2]))}return(i=Ba.get(n))?t(i.r,i.g,i.b):(null!=n&&n.charAt(0)==="#"&&(n.length===4?(a=n.charAt(1),a+=a,o=n.charAt(2),o+=o,c=n.charAt(3),c+=c):n.length===7&&(a=n.substring(1,3),o=n.substring(3,5),c=n.substring(5,7)),a=parseInt(a,16),o=parseInt(o,16),c=parseInt(c,16)),t(a,o,c))}function at(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-i,c=(a+i)/2;return o?(u=.5>c?o/(a+i):o/(2-a-i),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):u=r=0,H(r,u,c)}function ot(n,t,e){n=ct(n),t=ct(t),e=ct(e);var r=nt((.4124564*n+.3575761*t+.1804375*e)/Ua),u=nt((.2126729*n+.7151522*t+.072175*e)/Ia),i=nt((.0193339*n+.119192*t+.9503041*e)/Va);return J(116*u-16,500*(r-u),200*(u-i))}function ct(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function lt(n){var t=parseFloat(n);return n.charAt(n.length-1)==="%"?Math.round(2.55*t):t}function ft(n){return"function"==typeof n?n:function(){return n}}function st(n){return n}function ht(n){return n.length===1?function(t,e){n(null==t?e:null)}:n}function gt(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var a=ca.xhr(n,t,i);return a.row=function(n){return arguments.length?a.response((e=n)==null?r:u(n)):e},a.row(e)}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function a(t){return t.map(o).join(n)}function o(n){return c.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var c=RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(f>=c)return a;if(u)return u=!1,i;var t=f;if(n.charCodeAt(t)===34){for(var e=t;e++f;){var r=n.charCodeAt(f++),o=1;if(10===r)u=!0;else if(13===r)u=!0,n.charCodeAt(f)===10&&(++f,++o);else if(r!==l)continue;return n.substring(t,f-o)}return n.substring(t)}for(var r,u,i={},a={},o=[],c=n.length,f=0,s=0;(r=e())!==a;){for(var h=[];r!==i&&r!==a;)h.push(r),r=e();(!t||(h=t(h,s++)))&&o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new i,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(a).join("\n")},e}function pt(){for(var n,t=Date.now(),e=Wa;e;)n=t-e.then,n>=e.delay&&(e.flush=e.callback(n)),e=e.next;var r=dt()-t;r>24?(isFinite(r)&&(clearTimeout(Ja),Ja=setTimeout(pt,r)),$a=0):($a=1,Qa(pt))}function dt(){for(var n=null,t=Wa,e=1/0;t;)t.flush?(delete Ka[t.callback.id],t=n?n.next=t.next:Wa=t.next):(e=Math.min(e,t.then+t.delay),t=(n=t).next);return e}function mt(n,t){var e=Math.pow(10,Math.abs(8-t)*3);return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function vt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function yt(n){return n+""}function Mt(n,t){n&&lo.hasOwnProperty(n.type)&&lo[n.type](n,t)}function xt(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++un&&(r=n),n>i&&(i=n),u>t&&(u=t),t>a&&(a=t)}function e(){o.point=o.lineEnd=T}var r,u,i,a,o={point:t,lineStart:T,lineEnd:T,polygonStart:function(){o.lineEnd=e},polygonEnd:function(){o.point=t}};return function(t){return a=i=-(r=u=1/0),ca.geo.stream(t,n(o)),[[r,u],[i,a]]}}function St(n,t){if(!po){++mo,n*=Pa;var e=Math.cos(t*=Pa);vo+=(e*Math.cos(n)-vo)/mo,yo+=(e*Math.sin(n)-yo)/mo,Mo+=(Math.sin(t)-Mo)/mo}}function Et(){var n,t;po=1,kt(),po=2;var e=xo.point;xo.point=function(r,u){e(n=r,t=u)},xo.lineEnd=function(){xo.point(n,t),At(),xo.lineEnd=At}}function kt(){function n(n,u){n*=Pa;var i=Math.cos(u*=Pa),a=i*Math.cos(n),o=i*Math.sin(n),c=Math.sin(u),l=Math.atan2(Math.sqrt((l=e*c-r*o)*l+(l=r*a-t*c)*l+(l=t*o-e*a)*l),t*a+e*o+r*c);mo+=l,vo+=l*(t+(t=a)),yo+=l*(e+(e=o)),Mo+=l*(r+(r=c))}var t,e,r;po>1||(1>po&&(po=1,mo=vo=yo=Mo=0),xo.point=function(u,i){u*=Pa;var a=Math.cos(i*=Pa);t=a*Math.cos(u),e=a*Math.sin(u),r=Math.sin(i),xo.point=n})}function At(){xo.point=St}function qt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function Nt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function Tt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Ct(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function zt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function Dt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function jt(){return!0}function Lt(n){return[Math.atan2(n[1],n[0]),Math.asin(Math.max(-1,Math.min(1,n[2])))]}function Ft(n,t){return Math.abs(n[0]-t[0])o;++o)u.point((e=n[o])[0],e[1]);return u.lineEnd(),void 0}var c={point:e,points:n,other:null,visited:!1,entry:!0,subject:!0},l={point:e,points:[e],other:c,visited:!1,entry:!1,subject:!1};c.other=l,i.push(c),a.push(l),c={point:r,points:[r],other:null,visited:!1,entry:!1,subject:!0},l={point:r,points:[r],other:c,visited:!1,entry:!0,subject:!1},c.other=l,i.push(c),a.push(l)}}),a.sort(t),Pt(i),Pt(a),i.length){if(e)for(var o=1,c=!e(a[0].point),l=a.length;l>o;++o)a[o].entry=c=!c;for(var f,s,h,g=i[0];;){for(f=g;f.visited;)if((f=f.next)===g)return;s=f.points,u.lineStart();do{if(f.visited=f.other.visited=!0,f.entry){if(f.subject)for(var o=0;o=0;)u.point((h=s[o])[0],h[1])}else r(f.point,f.prev.point,-1,u);f=f.prev}f=f.other,s=f.points}while(!f.visited);u.lineEnd()}}}function Pt(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r1&&2&t&&e.push(e.pop().concat(e.shift())),s.push(e.filter(Ot))}var s,h,g,p,d,m=t(r),v={point:u,lineStart:a,lineEnd:o,polygonStart:function(){v.point=c,v.lineStart=l,v.lineEnd=f,p=!1,g=h=0,s=[],r.polygonStart()},polygonEnd:function(){v.point=u,v.lineStart=a,v.lineEnd=o,s=ca.merge(s),s.length?Ht(s,It,null,e,r):(-Ha>h||p&&-Ha>g)&&(r.lineStart(),e(null,null,1,r),r.lineEnd()),r.polygonEnd(),s=null},sphere:function(){r.polygonStart(),r.lineStart(),e(null,null,1,r),r.lineEnd(),r.polygonEnd()}},y=Yt(),M=t(y);return v}}function Ot(n){return n.length>1}function Yt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:T,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){if(!(e=n.length))return 0;for(var e,r,u,i=0,a=0,o=n[0],c=o[0],l=o[1],f=Math.cos(l),s=Math.atan2(t*Math.sin(c)*f,Math.sin(l)),h=1-t*Math.cos(c)*f,g=s;++i2&&(a+=4*(r-s)):a+=Math.abs(h-2)0?Fa:-Fa,c=Math.abs(i-e);Math.abs(c-Fa)0?Fa/2:-Fa/2),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(i,r),t=0):u!==o&&c>=Fa&&(Math.abs(e-u)Ha?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*a)):(t+r)/2}function Zt(n,t,e,r){var u;if(null==n)u=e*Fa/2,r.point(-Fa,u),r.point(0,u),r.point(Fa,u),r.point(Fa,0),r.point(Fa,-u),r.point(0,-u),r.point(-Fa,-u),r.point(-Fa,0),r.point(-Fa,u);else if(Math.abs(n[0]-t[0])>Ha){var i=(n[0]i}function e(n){var e,i,c,l,f;return{lineStart:function(){l=c=!1,f=1},point:function(s,h){var g,p=[s,h],d=t(s,h),m=a?d?0:u(s,h):d?u(s+(0>s?Fa:-Fa),h):0;if(!e&&(l=c=d)&&n.lineStart(),d!==c&&(g=r(e,p),(Ft(e,g)||Ft(p,g))&&(p[0]+=Ha,p[1]+=Ha,d=t(p[0],p[1]))),d!==c)f=0,d?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(o&&e&&a^d){var v;m&i||!(v=r(p,e,!0))||(f=0,a?(n.lineStart(),n.point(v[0][0],v[0][1]),n.point(v[1][0],v[1][1]),n.lineEnd()):(n.point(v[1][0],v[1][1]),n.lineEnd(),n.lineStart(),n.point(v[0][0],v[0][1])))}!d||e&&Ft(e,p)||n.point(p[0],p[1]),e=p,c=d,i=m},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return f|(l&&c)<<1}}}function r(n,t,e){var r=qt(n),u=qt(t),a=[1,0,0],o=Tt(r,u),c=Nt(o,o),l=o[0],f=c-l*l;if(!f)return!e&&n;var s=i*c/f,h=-i*l/f,g=Tt(a,o),p=zt(a,s),d=zt(o,h);Ct(p,d);var m=g,v=Nt(p,m),y=Nt(m,m),M=v*v-y*(Nt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=zt(m,(-v-x)/y);if(Ct(b,p),b=Lt(b),!e)return b;var _,w=n[0],S=t[0],E=n[1],k=t[1];w>S&&(_=w,w=S,S=_);var A=S-w,q=Math.abs(A-Fa)A;if(!q&&E>k&&(_=E,E=k,k=_),N?q?E+k>0^b[1]<(Math.abs(b[0]-w)Fa^(w<=b[0]&&b[0]<=S)){var T=zt(m,(-v+x)/y);return Ct(T,p),[b,Lt(T)]}}}function u(t,e){var r=a?n:Fa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),a=i>0,o=Math.abs(i)>Ha,c=ae(n,6*Pa);return Rt(t,e,c)}function $t(n,t,e,r){function u(r,u){return Math.abs(r[0]-n)0?0:3:Math.abs(r[0]-e)0?2:1:Math.abs(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return a(n.point,t.point)}function a(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}function o(u,i){var a=i[0]-u[0],o=i[1]-u[1],c=[0,1];return Math.abs(a)0&&(u[0]+=c[0]*a,u[1]+=c[0]*o),!0):!1}return function(c){function l(i){var a=u(i,-1),o=f([0===a||3===a?n:e,a>1?r:t]);return o}function f(n){for(var t=0,e=M.length,r=n[1],u=0;e>u;++u)for(var i=1,a=M[u],o=a.length,c=a[0];o>i;++i)b=a[i],c[1]<=r?b[1]>r&&s(c,b,n)>0&&++t:b[1]<=r&&s(c,b,n)<0&&--t,c=b;return 0!==t}function s(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(e[0]-n[0])*(t[1]-n[1])}function h(i,o,c,l){var f=0,s=0;if(null==i||(f=u(i,c))!==(s=u(o,c))||a(i,o)<0^c>0){do l.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+c+4)%4)!==s)}else l.point(o[0],o[1])}function g(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function p(n,t){g(n,t)&&c.point(n,t)}function d(){C.point=v,M&&M.push(x=[]),q=!0,A=!1,E=k=0/0}function m(){y&&(v(_,w),S&&A&&T.rejoin(),y.push(T.buffer())),C.point=p,A&&c.lineEnd()}function v(n,t){n=Math.max(-_o,Math.min(_o,n)),t=Math.max(-_o,Math.min(_o,t));var e=g(n,t);if(M&&x.push([n,t]),q)_=n,w=t,S=e,q=!1,e&&(c.lineStart(),c.point(n,t));else if(e&&A)c.point(n,t);else{var r=[E,k],u=[n,t];o(r,u)?(A||(c.lineStart(),c.point(r[0],r[1])),c.point(u[0],u[1]),e||c.lineEnd()):(c.lineStart(),c.point(n,t))}E=n,k=t,A=e}var y,M,x,_,w,S,E,k,A,q,N=c,T=Yt(),C={point:p,lineStart:d,lineEnd:m,polygonStart:function(){c=T,y=[],M=[]},polygonEnd:function(){c=N,(y=ca.merge(y)).length?(c.polygonStart(),Ht(y,i,l,h,c),c.polygonEnd()):f([n,t])&&(c.polygonStart(),c.lineStart(),h(null,null,1,c),c.lineEnd(),c.polygonEnd()),y=M=x=null}};return C}}function Jt(n,t,e){if(Math.abs(t)=n;var r=n/t;if(t>0){if(r>e[1])return!1;r>e[0]&&(e[0]=r)}else{if(r4*r&&d--){var x=a+h,b=o+g,_=c+p,w=Math.sqrt(x*x+b*b+_*_),S=Math.asin(_/=w),E=Math.abs(Math.abs(_)-1)r||Math.abs((v*N+y*T)/M-.5)>.3)&&(e(t,u,i,a,o,c,A,q,E,x/=w,b/=w,_,d,m),m.point(A,q),e(A,q,E,x,b,_,l,f,s,h,g,p,d,m))}}var r=.5,u=16;return t.precision=function(n){return arguments.length?(u=(r=n*n)>0&&16,t):Math.sqrt(r)},t}function Wt(n){return Qt(function(){return n})()}function Qt(n){function t(n){return n=a(n[0]*Pa,n[1]*Pa),[n[0]*f+o,c-n[1]*f]}function e(n){return n=a.invert((n[0]-o)/f,(c-n[1])/f),n&&[n[0]*Ra,n[1]*Ra]}function r(){a=Gt(i=ee(d,m,v),u);var n=u(g,p);return o=s-n[0]*f,c=h+n[1]*f,t}var u,i,a,o,c,l=Kt(function(n,t){return n=u(n,t),[n[0]*f+o,c-n[1]*f]}),f=150,s=480,h=250,g=0,p=0,d=0,m=0,v=0,y=bo,M=st,x=null,b=null;return t.stream=function(n){return ne(i,y(l(M(n))))},t.clipAngle=function(n){return arguments.length?(y=null==n?(x=n,bo):Bt((x=+n)*Pa),t):x},t.clipExtent=function(n){return arguments.length?(b=n,M=null==n?st:$t(n[0][0],n[0][1],n[1][0],n[1][1]),t):b},t.scale=function(n){return arguments.length?(f=+n,r()):f},t.translate=function(n){return arguments.length?(s=+n[0],h=+n[1],r()):[s,h]},t.center=function(n){return arguments.length?(g=n[0]%360*Pa,p=n[1]%360*Pa,r()):[g*Ra,p*Ra]},t.rotate=function(n){return arguments.length?(d=n[0]%360*Pa,m=n[1]%360*Pa,v=n.length>2?n[2]%360*Pa:0,r()):[d*Ra,m*Ra,v*Ra]},ca.rebind(t,l,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function ne(n,t){return{point:function(e,r){r=n(e*Pa,r*Pa),e=r[0],t.point(e>Fa?e-2*Fa:-Fa>e?e+2*Fa:e,r[1])},sphere:function(){t.sphere()},lineStart:function(){t.lineStart()},lineEnd:function(){t.lineEnd()},polygonStart:function(){t.polygonStart()},polygonEnd:function(){t.polygonEnd()}}}function te(n,t){return[n,t]}function ee(n,t,e){return n?t||e?Gt(ue(n),ie(t,e)):ue(n):t||e?ie(t,e):te}function re(n){return function(t,e){return t+=n,[t>Fa?t-2*Fa:-Fa>t?t+2*Fa:t,e]}}function ue(n){var t=re(n);return t.invert=re(-n),t}function ie(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),f=l*r+o*u;return[Math.atan2(c*i-f*a,o*r-l*u),Math.asin(Math.max(-1,Math.min(1,f*i+c*a)))]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),f=l*i-c*a;return[Math.atan2(c*i+l*a,o*r+f*u),Math.asin(Math.max(-1,Math.min(1,f*r-o*u)))]},e}function ae(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,a,o){null!=u?(u=oe(e,u),i=oe(e,i),(a>0?i>u:u>i)&&(u+=2*a*Fa)):(u=n+2*a*Fa,i=n);for(var c,l=a*t,f=u;a>0?f>i:i>f;f-=l)o.point((c=Lt([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function oe(n,t){var e=qt(t);e[0]-=n,Dt(e);var r=Y(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Ha)%(2*Math.PI)}function ce(n,t,e){var r=ca.range(n,t-Ha,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function le(n,t,e){var r=ca.range(n,t-Ha,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function fe(n){return n.source}function se(n){return n.target}function he(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),a=Math.cos(r),o=Math.sin(r),c=u*Math.cos(n),l=u*Math.sin(n),f=a*Math.cos(e),s=a*Math.sin(e),h=2*Math.asin(Math.sqrt(X(r-t)+u*a*X(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*f,u=e*l+t*s,a=e*i+t*o;return[Math.atan2(u,r)*Ra,Math.atan2(a,Math.sqrt(r*r+u*u))*Ra]}:function(){return[n*Ra,t*Ra]};return p.distance=h,p}function ge(){function n(n,u){var i=Math.sin(u*=Pa),a=Math.cos(u),o=Math.abs((n*=Pa)-t),c=Math.cos(o);wo+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*i-e*a*c)*o),e*i+r*a*c),t=n,e=i,r=a}var t,e,r;So.point=function(u,i){t=u*Pa,e=Math.sin(i*=Pa),r=Math.cos(i),So.point=n},So.lineEnd=function(){So.point=So.lineEnd=T}}function pe(n){var t=0,e=Fa/3,r=Qt(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Fa/180,e=n[1]*Fa/180):[180*(t/Fa),180*(e/Fa)]},u}function de(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),a-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),a=Math.sqrt(i)/u;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/u,Math.asin((i-(n*n+e*e)*u*u)/(2*u))]},e}function me(n,t){var e=n(t[0]),r=n([.5*(t[0][0]+t[1][0]),t[0][1]]),u=n([t[1][0],t[0][1]]),i=n(t[1]),a=r[1]-e[1],o=r[0]-e[0],c=u[1]-r[1],l=u[0]-r[0],f=a/o,s=c/l,h=.5*(f*s*(e[1]-u[1])+s*(e[0]+r[0])-f*(r[0]+u[0]))/(s-f),g=(.5*(e[0]+r[0])-h)/f+.5*(e[1]+r[1]),p=i[0]-h,d=i[1]-g,m=e[0]-h,v=e[1]-g,y=p*p+d*d,M=m*m+v*v,x=Math.atan2(d,p),b=Math.atan2(v,m);return function(t){var e=t[0]-h,r=t[1]-g,u=e*e+r*r,i=Math.atan2(r,e);return u>y&&M>u&&i>x&&b>i?n.invert(t):void 0}}function ve(){function n(n,t){ko+=u*n-r*t,r=n,u=t}var t,e,r,u;Ao.point=function(i,a){Ao.point=n,t=r=i,e=u=a},Ao.lineEnd=function(){n(t,e)}}function ye(){function n(n,t){a.push("M",n,",",t,i)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function u(){a.push("Z")}var i=Se(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return i=Se(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function Me(n,t){po||(vo+=n,yo+=t,++Mo)}function xe(){function n(n,r){var u=n-t,i=r-e,a=Math.sqrt(u*u+i*i);vo+=a*(t+n)/2,yo+=a*(e+r)/2,Mo+=a,t=n,e=r}var t,e;if(1!==po){if(!(1>po))return;po=1,vo=yo=Mo=0}qo.point=function(r,u){qo.point=n,t=r,e=u}}function be(){qo.point=Me}function _e(){function n(n,t){var e=u*n-r*t;vo+=e*(r+n),yo+=e*(u+t),Mo+=3*e,r=n,u=t}var t,e,r,u;2>po&&(po=2,vo=yo=Mo=0),qo.point=function(i,a){qo.point=n,t=r=i,e=u=a},qo.lineEnd=function(){n(t,e)}}function we(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,a,0,2*Fa)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function u(){o.point=t}function i(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:u,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=u,o.point=t},pointRadius:function(n){return a=n,o},result:T};return o}function Se(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ee(n){var t=Kt(function(t,e){return n([t*Ra,e*Ra])});return function(n){return n=t(n),{point:function(t,e){n.point(t*Pa,e*Pa)},sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}}function ke(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),a=Math.cos(u);return[Math.atan2(n*i,r*a),Math.asin(r&&e*i/r)]},e}function Ae(n,t){function e(n,t){var e=Math.abs(Math.abs(t)-Fa/2)1){o=t[1],i=n[c],c++,r+="C"+(u[0]+a[0])+","+(u[1]+a[1])+","+(i[0]-o[0])+","+(i[1]-o[1])+","+i[0]+","+i[1];for(var l=2;l9&&(u=3*t/Math.sqrt(u),a[o]=u*e,a[o+1]=u*r));for(o=-1;++o<=c;)u=(n[Math.min(c,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),i.push([u||0,a[o]*u||0]);return i}function Qe(n){return n.length<3?Le(n):n[0]+Ue(n,We(n))}function nr(n,t,e,r){var u,i,a,o,c,l,f;return u=r[n],i=u[0],a=u[1],u=r[t],o=u[0],c=u[1],u=r[e],l=u[0],f=u[1],(f-a)*(o-i)-(c-a)*(l-i)>0}function tr(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function er(n,t,e,r){var u=n[0],i=e[0],a=t[0]-u,o=r[0]-i,c=n[1],l=e[1],f=t[1]-c,s=r[1]-l,h=(o*(c-l)-s*(u-i))/(s*a-o*f); -return[u+h*a,c+h*f]}function rr(n,t){var e={list:n.map(function(n,t){return{index:t,x:n[0],y:n[1]}}).sort(function(n,t){return n.yt.y?1:n.xt.x?1:0}),bottomSite:null},r={list:[],leftEnd:null,rightEnd:null,init:function(){r.leftEnd=r.createHalfEdge(null,"l"),r.rightEnd=r.createHalfEdge(null,"l"),r.leftEnd.r=r.rightEnd,r.rightEnd.l=r.leftEnd,r.list.unshift(r.leftEnd,r.rightEnd)},createHalfEdge:function(n,t){return{edge:n,side:t,vertex:null,l:null,r:null}},insert:function(n,t){t.l=n,t.r=n.r,n.r.l=t,n.r=t},leftBound:function(n){var t=r.leftEnd;do t=t.r;while(t!=r.rightEnd&&u.rightOf(t,n));return t=t.l},del:function(n){n.l.r=n.r,n.r.l=n.l,n.edge=null},right:function(n){return n.r},left:function(n){return n.l},leftRegion:function(n){return n.edge==null?e.bottomSite:n.edge.region[n.side]},rightRegion:function(n){return n.edge==null?e.bottomSite:n.edge.region[Po[n.side]]}},u={bisect:function(n,t){var e={region:{l:n,r:t},ep:{l:null,r:null}},r=t.x-n.x,u=t.y-n.y,i=r>0?r:-r,a=u>0?u:-u;return e.c=n.x*r+n.y*u+.5*(r*r+u*u),i>a?(e.a=1,e.b=u/r,e.c/=r):(e.b=1,e.a=r/u,e.c/=u),e},intersect:function(n,t){var e=n.edge,r=t.edge;if(!e||!r||e.region.r==r.region.r)return null;var u=e.a*r.b-e.b*r.a;if(Math.abs(u)<1e-10)return null;var i,a,o=(e.c*r.b-r.c*e.b)/u,c=(r.c*e.a-e.c*r.a)/u,l=e.region.r,f=r.region.r;l.y=a.region.r.x;return s&&i.side==="l"||!s&&i.side==="r"?null:{x:o,y:c}},rightOf:function(n,t){var e=n.edge,r=e.region.r,u=t.x>r.x;if(u&&n.side==="l")return 1;if(!u&&n.side==="r")return 0;if(e.a===1){var i=t.y-r.y,a=t.x-r.x,o=0,c=0;if(!u&&e.b<0||u&&e.b>=0?c=o=i>=e.b*a:(c=t.x+t.y*e.b>e.c,e.b<0&&(c=!c),c||(o=1)),!o){var l=r.x-e.region.l.x;c=e.b*(a*a-i*i)h*h+g*g}return n.side==="l"?c:!c},endPoint:function(n,e,r){n.ep[e]=r,n.ep[Po[e]]&&t(n)},distance:function(n,t){var e=n.x-t.x,r=n.y-t.y;return Math.sqrt(e*e+r*r)}},i={list:[],insert:function(n,t,e){n.vertex=t,n.ystar=t.y+e;for(var r=0,u=i.list,a=u.length;a>r;r++){var o=u[r];if(!(n.ystar>o.ystar||n.ystar==o.ystar&&t.x>o.vertex.x))break}u.splice(r,0,n)},del:function(n){for(var t=0,e=i.list,r=e.length;r>t&&e[t]!=n;++t);e.splice(t,1)},empty:function(){return i.list.length===0},nextEvent:function(n){for(var t=0,e=i.list,r=e.length;r>t;++t)if(e[t]==n)return e[t+1];return null},min:function(){var n=i.list[0];return{x:n.vertex.x,y:n.ystar}},extractMin:function(){return i.list.shift()}};r.init(),e.bottomSite=e.list.shift();for(var a,o,c,l,f,s,h,g,p,d,m,v,y,M=e.list.shift();;)if(i.empty()||(a=i.min()),M&&(i.empty()||M.yg.y&&(p=h,h=g,g=p,y="r"),v=u.bisect(h,g),s=r.createHalfEdge(v,y),r.insert(l,s),u.endPoint(v,Po[y],m),d=u.intersect(l,s),d&&(i.del(l),i.insert(l,d,u.distance(d,h))),d=u.intersect(s,f),d&&i.insert(s,d,u.distance(d,h))}for(o=r.right(r.leftEnd);o!=r.rightEnd;o=r.right(o))t(o.edge)}function ur(n){return n.x}function ir(n){return n.y}function ar(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function or(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var a=.5*(e+u),o=.5*(r+i),c=t.nodes;c[0]&&or(n,c[0],e,r,a,o),c[1]&&or(n,c[1],a,r,u,o),c[2]&&or(n,c[2],e,o,a,i),c[3]&&or(n,c[3],a,o,u,i)}}function cr(n,t){n=ca.rgb(n),t=ca.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,a=t.g-r,o=t.b-u;return function(n){return"#"+ut(Math.round(e+i*n))+ut(Math.round(r+a*n))+ut(Math.round(u+o*n))}}function lr(n){var t=[n.a,n.b],e=[n.c,n.d],r=sr(t),u=fr(t,e),i=sr(hr(e,t,-u))||0;t[0]*e[1]180?f+=360:f-l>180&&(l+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:gr(l,f)})):f&&r.push(r.pop()+"rotate("+f+")"),s!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:gr(s,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:gr(g[0],p[0])},{i:e-2,x:gr(g[1],p[1])})):(p[0]!=1||p[1]!=1)&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++ir;++r)if(a=f[r],a.x==e[0]){if(a.i)if(l[a.i+1]==null)for(l[a.i-1]+=a.x,l.splice(a.i,1),u=r+1;i>u;++u)f[u].i--;else for(l[a.i-1]+=a.x+l[a.i+1],l.splice(a.i,2),u=r+1;i>u;++u)f[u].i-=2;else if(l[a.i+1]==null)l[a.i]=a.x;else for(l[a.i]=a.x+l[a.i+1],l.splice(a.i+1,1),u=r+1;i>u;++u)f[u].i--;f.splice(r,1),i--,r--}else a.x=gr(parseFloat(e[0]),parseFloat(a.x));for(;i>r;)a=f.pop(),l[a.i+1]==null?l[a.i]=a.x:(l[a.i]=a.x+l[a.i+1],l.splice(a.i+1,1)),i--;return l.length===1?l[0]==null?f[0].x:function(){return t}:function(n){for(r=0;i>r;++r)l[(a=f[r]).i]=a.x(n);return l.join("")}}function vr(n,t){for(var e,r=ca.interpolators.length;--r>=0&&!(e=ca.interpolators[r](n,t)););return e}function yr(n){return"transform"==n?pr:vr}function Mr(n,t){var e,r=[],u=[],i=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(vr(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;a>e;++e)u[e]=t[e];return function(n){for(e=0;o>e;++e)u[e]=r[e](n);return u}}function xr(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function br(n){return function(t){return 1-n(1-t)}}function _r(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function wr(n){return n*n}function Sr(n){return n*n*n}function Er(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function kr(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Fa/2)}function qr(n){return Math.pow(2,10*(n-1))}function Nr(n){return 1-Math.sqrt(1-n*n)}function Tr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/(2*Fa)*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,10*-r)*Math.sin(2*(r-e)*Fa/t)}}function Cr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function zr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Dr(n,t){n=ca.hcl(n),t=ca.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,a=t.c-r,o=t.l-u;return i>180?i-=360:-180>i&&(i+=360),function(n){return $(e+i*n,r+a*n,u+o*n)+""}}function jr(n,t){n=ca.hsl(n),t=ca.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,a=t.s-r,o=t.l-u;return i>180?i-=360:-180>i&&(i+=360),function(n){return R(e+i*n,r+a*n,u+o*n)+""}}function Lr(n,t){n=ca.lab(n),t=ca.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,a=t.a-r,o=t.b-u;return function(n){return K(e+i*n,r+a*n,u+o*n)+""}}function Fr(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Hr(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Pr(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Rr(n){for(var t=n.source,e=n.target,r=Yr(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function Or(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Yr(n,t){if(n===t)return n;for(var e=Or(n),r=Or(t),u=e.pop(),i=r.pop(),a=null;u===i;)a=u,u=e.pop(),i=r.pop();return a}function Ur(n){n.fixed|=2}function Ir(n){n.fixed&=-7}function Vr(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Xr(n){n.fixed&=-5}function Zr(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,a=n.nodes,o=a.length,c=-1;++ce;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function uu(n){return n.reduce(iu,0)}function iu(n,t){return n+t[1]}function au(n,t){return ou(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ou(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function cu(n){return[ca.min(n),ca.max(n)]}function lu(n,t){return n.parent==t.parent?1:2}function fu(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function su(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function hu(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i0&&(n=r);return n}function gu(n,t){return n.x-t.x}function pu(n,t){return t.x-n.x}function du(n,t){return n.depth-t.depth}function mu(n,t){function e(n,r){var u=n.children;if(u&&(a=u.length))for(var i,a,o=null,c=-1;++c=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function yu(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function Mu(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function xu(n,t){return n.value-t.value}function bu(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function _u(n,t){n._pack_next=t,t._pack_prev=n}function wu(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return u*u-e*e-r*r>.001}function Su(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(l=e.length)){var e,r,u,i,a,o,c,l,f=1/0,s=-1/0,h=1/0,g=-1/0;if(e.forEach(Eu),r=e[0],r.x=-r.r,r.y=0,t(r),l>1&&(u=e[1],u.x=u.r,u.y=0,t(u),l>2))for(i=e[2],qu(r,u,i),t(i),bu(r,i),r._pack_prev=i,bu(i,u),u=r._pack_next,a=3;l>a;a++){qu(r,u,i=e[a]);var p=0,d=1,m=1;for(o=u._pack_next;o!==u;o=o._pack_next,d++)if(wu(o,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==o._pack_prev&&!wu(c,i);c=c._pack_prev,m++);p?(m>d||d==m&&u.ra;a++)i=e[a],i.x-=v,i.y-=y,M=Math.max(M,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=M,e.forEach(ku)}}function Eu(n){n._pack_next=n._pack_prev=n}function ku(n){delete n._pack_next,delete n._pack_prev}function Au(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,a=u.length;++iu&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Lu(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Fu(n){return n.rangeExtent?n.rangeExtent():Lu(n.range())}function Hu(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pu(n,t){var e,r=0,u=n.length-1,i=n[r],a=n[u];return i>a&&(e=r,r=u,u=e,e=i,i=a,a=e),(t=t(a-i))&&(n[r]=t.floor(i),n[u]=t.ceil(a)),n}function Ru(n,t,e,r){var u=[],i=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]2?Ru:Hu,c=r?Pr:Hr;return a=u(n,t,c,e),o=u(t,n,c,vr),i}function i(n){return a(n)}var a,o;return i.invert=function(n){return o(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Fr)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Vu(n,t)},i.tickFormat=function(t,e){return Xu(n,t,e)},i.nice=function(){return Pu(n,Uu),u()},i.copy=function(){return Ou(n,t,e,r)},u()}function Yu(n,t){return ca.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Uu(n){return n=Math.pow(10,Math.round(Math.log(n)/Math.LN10)-1),n&&{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}}function Iu(n,t){var e=Lu(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Vu(n,t){return ca.range.apply(ca,Iu(n,t))}function Xu(n,t,e){var r=-Math.floor(Math.log(Iu(n,t)[2])/Math.LN10+.01);return ca.format(e?e.replace(uo,function(n,t,e,u,i,a,o,c,l,f){return[t,e,u,i,a,o,c,l||"."+(r-2*("%"===f)),f].join("")}):",."+r+"f")}function Zu(n,t,e,r){function u(t){return n(e(t))}return u.invert=function(t){return r(n.invert(t))},u.domain=function(t){return arguments.length?(t[0]<0?(e=Ju,r=Gu):(e=Bu,r=$u),n.domain(t.map(e)),u):n.domain().map(r)},u.base=function(n){return arguments.length?(t=+n,u):t},u.nice=function(){return n.domain(Pu(n.domain(),Ku(t))),u},u.ticks=function(){var u=Lu(n.domain()),i=[];if(u.every(isFinite)){var a=Math.log(t),o=Math.floor(u[0]/a),c=Math.ceil(u[1]/a),l=r(u[0]),f=r(u[1]),s=t%1?2:t;if(e===Ju)for(i.push(-Math.pow(t,-o));o++0;h--)i.push(-Math.pow(t,-o)*h);else{for(;c>o;o++)for(var h=1;s>h;h++)i.push(Math.pow(t,o)*h);i.push(Math.pow(t,o))}for(o=0;i[o]f;c--);i=i.slice(o,c)}return i},u.tickFormat=function(n,i){if(arguments.length<2&&(i=Jo),!arguments.length)return i;var a,o=Math.log(t),c=Math.max(.1,n/u.ticks().length),l=e===Ju?(a=-1e-12,Math.floor):(a=1e-12,Math.ceil);return function(n){return n/r(o*l(e(n)/o+a))<=c?i(n):""}},u.copy=function(){return Zu(n.copy(),t,e,r)},Yu(u,n)}function Bu(n){return Math.log(0>n?0:n)}function $u(n){return Math.exp(n)}function Ju(n){return-Math.log(n>0?0:-n)}function Gu(n){return-Math.exp(-n)}function Ku(n){n=Math.log(n);var t={floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}};return function(){return t}}function Wu(n,t){function e(t){return n(r(t))}var r=Qu(t),u=Qu(1/t);return e.invert=function(t){return u(n.invert(t))},e.domain=function(t){return arguments.length?(n.domain(t.map(r)),e):n.domain().map(u)},e.ticks=function(n){return Vu(e.domain(),n)},e.tickFormat=function(n,t){return Xu(e.domain(),n,t)},e.nice=function(){return e.domain(Pu(e.domain(),Uu))},e.exponent=function(n){if(!arguments.length)return t;var i=e.domain();return r=Qu(t=n),u=Qu(1/t),e.domain(i)},e.copy=function(){return Wu(n.copy(),t)},Yu(e,n)}function Qu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ni(n,t){function e(t){return a[((i.get(t)||i.set(t,n.push(t)))-1)%a.length]}function r(t,e){return ca.range(n.length).map(function(n){return t+e*n})}var i,a,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var a,o=-1,c=r.length;++oe?l():(i.active=e,h.start.call(n,f,t),a.tween.forEach(function(e,r){(r=r.call(n,f,t))&&d.push(r)}),c(r)||ca.timer(c,0,o),1)}function c(r){if(i.active!==e)return l();for(var u=(r-g)/p,a=s(u),o=d.length;o>0;)d[--o].call(n,a);return u>=1?(l(),h.end.call(n,f,t),1):void 0}function l(){return--i.count?delete i[e]:delete n.__transition__,1}var f=n.__data__,s=a.ease,h=a.event,g=a.delay,p=a.duration,d=[];return r>=g?u(r):ca.timer(u,g,o),1},0,o),a}}function bi(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function _i(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function wi(n,t,e){if(r=[],e&&t.length>1){for(var r,u,i,a=Lu(n.domain()),o=-1,c=t.length,l=(t[1]-t[0])/++e;++o0;)(i=+t[o]-u*l)>=a[0]&&r.push(i);for(--o,u=0;++u1?Date.UTC.apply(this,arguments):arguments[0])}function Ei(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new gc(e-1)),1),e}function i(n,e){return t(n=new gc(+n),e),n}function a(n,r,i){var a=u(n),o=[];if(i>1)for(;r>a;)e(a)%i||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{gc=Si;var r=new Si;return r._=n,a(r,t,e)}finally{gc=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=a;var c=n.utc=ki(n);return c.floor=c,c.round=ki(r),c.ceil=ki(u),c.offset=ki(i),c.range=o,n}function ki(n){return function(t,e){try{gc=Si;var r=new Si;return r._=t,n(r,e)._}finally{gc=Date}}}function Ai(n,t,e,r){for(var u,i,a=0,o=t.length,c=e.length;o>a;){if(r>=c)return-1;if(u=t.charCodeAt(a++),37===u){if(i=Cc[t.charAt(a++)],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function qi(n){return RegExp("^(?:"+n.map(ca.requote).join("|")+")","i")}function Ni(n){for(var t=new u,e=-1,r=n.length;++er?Array(e-r+1).join(t)+n:n}function Ci(n,t,e){Sc.lastIndex=0;var r=Sc.exec(t.substring(e));return r?e+=r[0].length:-1}function zi(n,t,e){wc.lastIndex=0;var r=wc.exec(t.substring(e));return r?e+=r[0].length:-1}function Di(n,t,e){Ac.lastIndex=0;var r=Ac.exec(t.substring(e));return r?(n.m=qc.get(r[0].toLowerCase()),e+=r[0].length):-1}function ji(n,t,e){Ec.lastIndex=0;var r=Ec.exec(t.substring(e));return r?(n.m=kc.get(r[0].toLowerCase()),e+=r[0].length):-1}function Li(n,t,e){return Ai(n,""+Tc.c,t,e)}function Fi(n,t,e){return Ai(n,""+Tc.x,t,e)}function Hi(n,t,e){return Ai(n,""+Tc.X,t,e)}function Pi(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+=r[0].length):-1}function Ri(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+2));return r?(n.y=Oi(+r[0]),e+=r[0].length):-1}function Oi(n){return n+(n>68?1900:2e3)}function Yi(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+=r[0].length):-1}function Ui(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+=r[0].length):-1}function Ii(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+=r[0].length):-1}function Vi(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+=r[0].length):-1}function Xi(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+=r[0].length):-1}function Zi(n,t,e){zc.lastIndex=0;var r=zc.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+=r[0].length):-1}function Bi(n,t,e){var r=Dc.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}function $i(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(Math.abs(t)/60),u=Math.abs(t)%60;return e+Ti(r,"0",2)+Ti(u,"0",2)}function Ji(n){return n.toISOString()}function Gi(n,t,e){function r(t){return n(t)}return r.invert=function(t){return Wi(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Wi)},r.nice=function(n){return r.domain(Pu(r.domain(),function(){return n}))},r.ticks=function(e,u){var i=Ki(r.domain());if("function"!=typeof e){var a=i[1]-i[0],o=a/e,c=ca.bisect(Lc,o);if(c==Lc.length)return t.year(i,e);if(!c)return n.ticks(e).map(Wi);Math.log(o/Lc[c-1])t?[t,e]:[e,t]}function Wi(n){return new Date(n)}function Qi(n){return function(t){for(var e=n.length-1,r=n[e];!r[1](t);)r=n[--e];return r[0](t)}}function na(n){var t=new Date(n,0,1);return t.setFullYear(n),t}function ta(n){var t=n.getFullYear(),e=na(t),r=na(t+1);return t+(n-e)/(r-e)}function ea(n){var t=new Date(Date.UTC(n,0,1));return t.setUTCFullYear(n),t}function ra(n){var t=n.getUTCFullYear(),e=ea(t),r=ea(t+1);return t+(n-e)/(r-e)}function ua(n){return n.responseText}function ia(n){return JSON.parse(n.responseText)}function aa(n){var t=la.createRange();return t.selectNode(la.body),t.createContextualFragment(n.responseText)}function oa(n){return n.responseXML}var ca={version:"3.1.5"};Date.now||(Date.now=function(){return+new Date});var la=document,fa=window;try{la.createElement("div").style.setProperty("opacity",0,"")}catch(sa){var ha=fa.CSSStyleDeclaration.prototype,ga=ha.setProperty;ha.setProperty=function(n,t,e){ga.call(this,n,t+"",e)}}ca.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},ca.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},ca.min=function(n,t){var e,r,u=-1,i=n.length;if(arguments.length===1){for(;++ur&&(e=r)}else{for(;++ur&&(e=r)}return e},ca.max=function(n,t){var e,r,u=-1,i=n.length;if(arguments.length===1){for(;++ue&&(e=r)}else{for(;++ue&&(e=r)}return e},ca.extent=function(n,t){var e,r,u,i=-1,a=n.length;if(arguments.length===1){for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},ca.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(arguments.length===1)for(;++i1&&(t=t.map(e)),t=t.filter(n),t.length?ca.quantile(t.sort(ca.ascending),.5):void 0},ca.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)r;){var i=r+u>>>1;er)for(;(u=n+r*++o)>t;)i.push(u/a);else for(;(u=n+r*++o)=a.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,g=-1,p=o.length,d=a[c++],m=new u;++g=a.length)return n;var r=[],u=o[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},a=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ca.map,e,0),0)},i.key=function(n){return a.push(n),i},i.sortKeys=function(n){return o[a.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ca.set=function(n){var t=new i;if(n)for(var e=0;e=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(arguments.length===2){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ca.event=null,ca.mouse=function(n){return g(n,f())};var va=/WebKit/.test(fa.navigator.userAgent)?-1:0,ya=d; -try{ya(la.documentElement.childNodes)[0].nodeType}catch(Ma){ya=p}var xa=[].__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]};ca.touches=function(n,t){return arguments.length<2&&(t=f().touches),t?ya(t).map(function(t){var e=g(n,t);return e.identifier=t.identifier,e}):[]},ca.behavior.drag=function(){function n(){this.on("mousedown.drag",t).on("touchstart.drag",t)}function t(){function n(){var n=a.parentNode;return null!=f?ca.touches(n).filter(function(n){return n.identifier===f})[0]:ca.mouse(n)}function t(){if(!a.parentNode)return u();var t=n(),e=t[0]-h[0],r=t[1]-h[1];g|=e|r,h=t,l(),o({type:"drag",x:t[0]+i[0],y:t[1]+i[1],dx:e,dy:r})}function u(){o({type:"dragend"}),g&&(l(),ca.event.target===c&&s(p,"click")),p.on(null!=f?"touchmove.drag-"+f:"mousemove.drag",null).on(null!=f?"touchend.drag-"+f:"mouseup.drag",null)}var i,a=this,o=e.of(a,arguments),c=ca.event.target,f=ca.event.touches?ca.event.changedTouches[0].identifier:null,h=n(),g=0,p=ca.select(fa).on(null!=f?"touchmove.drag-"+f:"mousemove.drag",t).on(null!=f?"touchend.drag-"+f:"mouseup.drag",u,!0);r?(i=r.apply(a,arguments),i=[i.x-h[0],i.y-h[1]]):i=[0,0],null==f&&l(),o({type:"dragstart"})}var e=h(n,"drag","dragstart","dragend"),r=null;return n.origin=function(t){return arguments.length?(r=t,n):r},ca.rebind(n,e,"on")};var ba=function(n,t){return t.querySelector(n)},_a=function(n,t){return t.querySelectorAll(n)},wa=la.documentElement,Sa=wa.matchesSelector||wa.webkitMatchesSelector||wa.mozMatchesSelector||wa.msMatchesSelector||wa.oMatchesSelector,Ea=function(n,t){return Sa.call(n,t)};"function"==typeof Sizzle&&(ba=function(n,t){return Sizzle(n,t)[0]||null},_a=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},Ea=Sizzle.matchesSelector);var ka=[];ca.selection=function(){return Ca},ca.selection.prototype=ka,ka.select=function(n){var t,e,r,u,i=[];"function"!=typeof n&&(n=v(n));for(var a=-1,o=this.length;++a=0&&(e=n.substring(0,t),n=n.substring(t+1)),Aa.hasOwnProperty(e)?{space:Aa[e],local:n}:n}},ka.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ca.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(M(t,n[t]));return this}return this.each(M(n,t))},ca.requote=function(n){return n.replace(qa,"\\$&")};var qa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;ka.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=n.trim().split(/^|\s+/g)).length,u=-1;if(t=e.classList){for(;++ur){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(E(e,n[e],t));return this}if(2>r)return fa.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(E(n,t,e))},ka.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(k(t,n[t]));return this}return this.each(k(n,t))},ka.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},ka.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},ka.append=function(n){function t(){return this.appendChild(la.createElementNS(this.namespaceURI,n))}function e(){return this.appendChild(la.createElementNS(n.space,n.local))}return n=ca.ns.qualify(n),this.select(n.local?e:t)},ka.insert=function(n,t){function e(e,r){return this.insertBefore(la.createElementNS(this.namespaceURI,n),t.call(this,e,r))}function r(e,r){return this.insertBefore(la.createElementNS(n.space,n.local),t.call(this,e,r))}return n=ca.ns.qualify(n),"function"!=typeof t&&(t=v(t)),this.select(n.local?r:e)},ka.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},ka.data=function(n,t){function e(n,e){var r,i,a,o=n.length,s=e.length,h=Math.min(o,s),g=Array(s),p=Array(s),d=Array(o);if(t){var m,v=new u,y=new u,M=[];for(r=-1;++rr;++r)p[r]=A(e[r]);for(;o>r;++r)d[r]=n[r]}p.update=g,p.parentNode=g.parentNode=d.parentNode=n.parentNode,c.push(p),l.push(g),f.push(d)}var r,i,a=-1,o=this.length;if(!arguments.length){for(n=Array(o=(r=this[0]).length);++ai;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var o=0,c=e.length;c>o;o++)(r=e[o])&&n.call(r,r.__data__,o)&&t.push(r)}return m(u)},ka.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},ka.sort=function(n){n=N.apply(this,arguments);for(var t=-1,e=this.length;++tr){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(C(n,t,e))};var Na=ca.map({mouseenter:"mouseover",mouseleave:"mouseout"});Na.forEach(function(n){"on"+n in la&&Na.remove(n)}),ka.each=function(n){return j(this,function(t,e,r){n.call(t,t.__data__,e,r)})},ka.call=function(n){var t=ya(arguments);return n.apply(t[0]=this,t),this},ka.empty=function(){return!this.node()},ka.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null};var Ta=[];ca.selection.enter=L,ca.selection.enter.prototype=Ta,Ta.append=ka.append,Ta.insert=ka.insert,Ta.empty=ka.empty,Ta.node=ka.node,Ta.select=function(n){for(var t,e,r,u,i,a=[],o=-1,c=this.length;++oe-_){var i=n[0],o=t(n[0]);r(2*S),u(i,o),a(k.of(this,arguments))}_=e}}function d(){var n=ca.touches(this),t=n[0],e=m[t.identifier];if(i=n[1]){var i,o=m[i.identifier];t=[(t[0]+i[0])/2,(t[1]+i[1])/2],e=[(e[0]+o[0])/2,(e[1]+o[1])/2],r(ca.event.scale*v)}u(t,e),_=null,a(k.of(this,arguments))}var m,v,y,M,x,b,_,w=[0,0],S=1,E=Da,k=h(n,"zoom");return n.translate=function(t){return arguments.length?(w=t.map(Number),i(),n):w},n.scale=function(t){return arguments.length?(S=+t,i(),n):S},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:t.map(Number),n):E},n.x=function(t){return arguments.length?(M=t,y=t.copy(),w=[0,0],S=1,n):M},n.y=function(t){return arguments.length?(b=t,x=t.copy(),w=[0,0],S=1,n):b},ca.rebind(n,k,"on")};var za,Da=[0,1/0],ja="onwheel"in la?(za=function(){return-ca.event.deltaY*(ca.event.deltaMode?120:1)},"wheel"):"onmousewheel"in la?(za=function(){return ca.event.wheelDelta},"mousewheel"):(za=function(){return-ca.event.detail},"MozMousePixelScroll");F.prototype.toString=function(){return this.rgb()+""},ca.hsl=function(n,t,e){return arguments.length===1?n instanceof P?H(n.h,n.s,n.l):it(""+n,at,H):H(+n,+t,+e)};var La=P.prototype=new F;La.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),H(this.h,this.s,this.l/n)},La.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),H(this.h,this.s,n*this.l)},La.rgb=function(){return R(this.h,this.s,this.l)};var Fa=Math.PI,Ha=1e-6,Pa=Fa/180,Ra=180/Fa;ca.hcl=function(n,t,e){return arguments.length===1?n instanceof B?Z(n.h,n.c,n.l):n instanceof G?W(n.l,n.a,n.b):W((n=ot((n=ca.rgb(n)).r,n.g,n.b)).l,n.a,n.b):Z(+n,+t,+e)};var Oa=B.prototype=new F;Oa.brighter=function(n){return Z(this.h,this.c,Math.min(100,this.l+Ya*(arguments.length?n:1)))},Oa.darker=function(n){return Z(this.h,this.c,Math.max(0,this.l-Ya*(arguments.length?n:1)))},Oa.rgb=function(){return $(this.h,this.c,this.l).rgb()},ca.lab=function(n,t,e){return arguments.length===1?n instanceof G?J(n.l,n.a,n.b):n instanceof B?$(n.l,n.c,n.h):ot((n=ca.rgb(n)).r,n.g,n.b):J(+n,+t,+e)};var Ya=18,Ua=.95047,Ia=1,Va=1.08883,Xa=G.prototype=new F;Xa.brighter=function(n){return J(Math.min(100,this.l+Ya*(arguments.length?n:1)),this.a,this.b)},Xa.darker=function(n){return J(Math.max(0,this.l-Ya*(arguments.length?n:1)),this.a,this.b)},Xa.rgb=function(){return K(this.l,this.a,this.b)},ca.rgb=function(n,t,e){return arguments.length===1?n instanceof rt?et(n.r,n.g,n.b):it(""+n,et,R):et(~~n,~~t,~~e)};var Za=rt.prototype=new F;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),et(Math.min(255,Math.floor(t/n)),Math.min(255,Math.floor(e/n)),Math.min(255,Math.floor(r/n)))):et(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),et(Math.floor(n*this.r),Math.floor(n*this.g),Math.floor(n*this.b))},Za.hsl=function(){return at(this.r,this.g,this.b)},Za.toString=function(){return"#"+ut(this.r)+ut(this.g)+ut(this.b)};var Ba=ca.map({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",darkgreen:"#006400",darkgrey:"#a9a9a9",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",green:"#008000",greenyellow:"#adff2f",grey:"#808080",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",lightgreen:"#90ee90",lightgrey:"#d3d3d3",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",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"});Ba.forEach(function(n,t){Ba.set(n,it(t,et,R))}),ca.functor=ft,ca.xhr=function(n,t,e){function r(){var n=c.status;!n&&c.responseText||n>=200&&300>n||304===n?i.load.call(u,o.call(u,c)):i.error.call(u,c)}var u={},i=ca.dispatch("progress","load","error"),a={},o=st,c=new(fa.XDomainRequest&&/^(http(s)?:)?\/\//.test(n)?XDomainRequest:XMLHttpRequest);return"onload"in c?c.onload=c.onerror=r:c.onreadystatechange=function(){c.readyState>3&&r()},c.onprogress=function(n){var t=ca.event;ca.event=n;try{i.progress.call(u,c)}finally{ca.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.response=function(n){return o=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(ya(arguments)))}}),u.send=function(e,r,i){if(arguments.length===2&&"function"==typeof r&&(i=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var o in a)c.setRequestHeader(o,a[o]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),c.send(null==r?null:r),u},u.abort=function(){return c.abort(),u},ca.rebind(u,i,"on"),arguments.length===2&&"function"==typeof t&&(e=t,t=null),null==e?u:u.get(ht(e))},ca.csv=gt(",","text/csv"),ca.tsv=gt(" ","text/tab-separated-values");var $a,Ja,Ga=0,Ka={},Wa=null;ca.timer=function(n,t,e){if(arguments.length<3){if(arguments.length<2)t=0;else if(!isFinite(t))return;e=Date.now()}var r=Ka[n.id];r&&r.callback===n?(r.then=e,r.delay=t):Ka[n.id=++Ga]=Wa={callback:n,then:e,delay:t,next:Wa},$a||(Ja=clearTimeout(Ja),$a=1,Qa(pt))},ca.timer.flush=function(){for(var n,t=Date.now(),e=Wa;e;)n=t-e.then,e.delay||(e.flush=e.callback(n)),e=e.next;dt()};var Qa=fa.requestAnimationFrame||fa.webkitRequestAnimationFrame||fa.mozRequestAnimationFrame||fa.oRequestAnimationFrame||fa.msRequestAnimationFrame||function(n){setTimeout(n,17)},no=".",to=",",eo=[3,3],ro=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"].map(mt);ca.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=ca.round(n,vt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,Math.floor((0>=e?e+1:e-1)/3)*3))),ro[8+e/3]},ca.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)},ca.format=function(n){var t=uo.exec(n),e=t[1]||" ",r=t[2]||">",u=t[3]||"",i=t[4]||"",a=t[5],o=+t[6],c=t[7],l=t[8],f=t[9],s=1,h="",g=!1;switch(l&&(l=+l.substring(1)),(a||"0"===e&&"="===r)&&(a=e="0",r="=",c&&(o-=Math.floor((o-1)/4))),f){case"n":c=!0,f="g";break;case"%":s=100,h="%",f="f";break;case"p":s=100,h="%",f="r";break;case"b":case"o":case"x":case"X":i&&(i="0"+f.toLowerCase());case"c":case"d":g=!0,l=0;break;case"s":s=-1,f="r"}"#"===i&&(i=""),"r"!=f||l||(f="g"),null!=l&&("g"==f?l=Math.max(1,Math.min(21,l)):("e"==f||"f"==f)&&(l=Math.max(0,Math.min(20,l)))),f=io.get(f)||yt;var p=a&&c;return function(n){if(g&&n%1)return"";var t=0>n||0===n&&0>1/n?(n=-n,"-"):u;if(0>s){var d=ca.formatPrefix(n,l);n=d.scale(n),h=d.symbol}else n*=s;n=f(n,l),!a&&c&&(n=ao(n));var m=i.length+n.length+(p?0:t.length),v=o>m?Array(m=o-m+1).join(e):"";return p&&(n=ao(v+n)),no&&n.replace(".",no),t+=i,("<"===r?t+n+v:">"===r?v+t+n:"^"===r?v.substring(0,m>>=1)+t+n+v.substring(m):t+(p?n:v+n))+h}};var uo=/(?:([^{])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,io=ca.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ca.round(n,vt(n,t))).toFixed(Math.max(0,Math.min(20,vt(n*(1+1e-15),t))))}}),ao=st;if(eo){var oo=eo.length;ao=function(n){for(var t=n.lastIndexOf("."),e=t>=0?"."+n.substring(t+1):(t=n.length,""),r=[],u=0,i=eo[0];t>0&&i>0;)r.push(n.substring(t-=i,t+i)),i=eo[u=(u+1)%oo];return r.reverse().join(to||"")+e}}ca.geo={},ca.geo.stream=function(n,t){n&&co.hasOwnProperty(n.type)?co[n.type](n,t):Mt(n,t)};var co={Feature:function(n,t){Mt(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*Fa+n:n,go.lineStart=go.lineEnd=go.point=T}};ca.geo.bounds=wt(st),ca.geo.centroid=function(n){po=mo=vo=yo=Mo=0,ca.geo.stream(n,xo);var t;return mo&&Math.abs(t=Math.sqrt(vo*vo+yo*yo+Mo*Mo))>Ha?[Math.atan2(yo,vo)*Ra,Math.asin(Math.max(-1,Math.min(1,Mo/t)))*Ra]:void 0};var po,mo,vo,yo,Mo,xo={sphere:function(){2>po&&(po=2,mo=vo=yo=Mo=0)},point:St,lineStart:kt,lineEnd:At,polygonStart:function(){2>po&&(po=2,mo=vo=yo=Mo=0),xo.lineStart=Et},polygonEnd:function(){xo.lineStart=kt}},bo=Rt(jt,Vt,Zt),_o=1e9;ca.geo.projection=Wt,ca.geo.projectionMutator=Qt,(ca.geo.equirectangular=function(){return Wt(te)}).raw=te.invert=te,ca.geo.rotation=function(n){function t(t){return t=n(t[0]*Pa,t[1]*Pa),t[0]*=Ra,t[1]*=Ra,t}return n=ee(n[0]%360*Pa,n[1]*Pa,n.length>2?n[2]*Pa:0),t.invert=function(t){return t=n.invert(t[0]*Pa,t[1]*Pa),t[0]*=Ra,t[1]*=Ra,t},t},ca.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ee(-n[0]*Pa,-n[1]*Pa,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ra,n[1]*=Ra}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ae((t=+r)*Pa,u*Pa),n):t},n.precision=function(r){return arguments.length?(e=ae(t*Pa,(u=+r)*Pa),n):u},n.angle(90)},ca.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Pa,u=n[1]*Pa,i=t[1]*Pa,a=Math.sin(r),o=Math.cos(r),c=Math.sin(u),l=Math.cos(u),f=Math.sin(i),s=Math.cos(i);return Math.atan2(Math.sqrt((e=s*a)*e+(e=l*f-c*s*o)*e),c*f+l*s*o)},ca.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ca.range(Math.ceil(i/m)*m,u,m).map(h).concat(ca.range(Math.ceil(l/v)*v,c,v).map(g)).concat(ca.range(Math.ceil(r/p)*p,e,p).filter(function(n){return Math.abs(n%m)>Ha}).map(f)).concat(ca.range(Math.ceil(o/d)*d,a,d).filter(function(n){return Math.abs(n%v)>Ha}).map(s))}var e,r,u,i,a,o,c,l,f,s,h,g,p=10,d=p,m=90,v=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(l).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],l=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),l>c&&(t=l,l=c,c=t),n.precision(y)):[[i,l],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(m=+t[0],v=+t[1],n):[m,v]},n.minorStep=function(t){return arguments.length?(p=+t[0],d=+t[1],n):[p,d]},n.precision=function(t){return arguments.length?(y=+t,f=ce(o,a,90),s=le(r,e,y),h=ce(l,c,90),g=le(i,u,y),n):y},n.majorExtent([[-180,-90+Ha],[180,90-Ha]]).minorExtent([[-180,-80-Ha],[180,80+Ha]])},ca.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=fe,u=se;return n.distance=function(){return ca.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},ca.geo.interpolate=function(n,t){return he(n[0]*Pa,n[1]*Pa,t[0]*Pa,t[1]*Pa)},ca.geo.length=function(n){return wo=0,ca.geo.stream(n,So),wo};var wo,So={sphere:T,point:T,lineStart:ge,lineEnd:T,polygonStart:T,polygonEnd:T};(ca.geo.conicEqualArea=function(){return pe(de)}).raw=de,ca.geo.albersUsa=function(){function n(n){return t(n)(n)}function t(n){var t=n[0],e=n[1];return e>50?a:-140>t?o:21>e?c:i}var e,r,u,i=ca.geo.conicEqualArea().rotate([98,0]).center([0,38]).parallels([29.5,45.5]),a=ca.geo.conicEqualArea().rotate([160,0]).center([0,60]).parallels([55,65]),o=ca.geo.conicEqualArea().rotate([160,0]).center([0,20]).parallels([8,18]),c=ca.geo.conicEqualArea().rotate([60,0]).center([0,10]).parallels([8,18]);return n.invert=function(n){return e(n)||r(n)||u(n)||i.invert(n)},n.scale=function(t){return arguments.length?(i.scale(t),a.scale(.6*t),o.scale(t),c.scale(1.5*t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var l=i.scale(),f=t[0],s=t[1];return i.translate(t),a.translate([f-.4*l,s+.17*l]),o.translate([f-.19*l,s+.2*l]),c.translate([f+.58*l,s+.43*l]),e=me(a,[[-180,50],[-130,72]]),r=me(o,[[-164,18],[-154,24]]),u=me(c,[[-67.5,17.5],[-65,19]]),n},n.scale(1e3)};var Eo,ko,Ao={point:T,lineStart:T,lineEnd:T,polygonStart:function(){ko=0,Ao.lineStart=ve},polygonEnd:function(){Ao.lineStart=Ao.lineEnd=Ao.point=T,Eo+=Math.abs(ko/2)}},qo={point:Me,lineStart:xe,lineEnd:be,polygonStart:function(){qo.lineStart=_e},polygonEnd:function(){qo.point=Me,qo.lineStart=xe,qo.lineEnd=be}};ca.geo.path=function(){function n(n){return n&&ca.geo.stream(n,r(u.pointRadius("function"==typeof i?+i.apply(this,arguments):i))),u.result()}var t,e,r,u,i=4.5;return n.area=function(n){return Eo=0,ca.geo.stream(n,r(Ao)),Eo},n.centroid=function(n){return po=vo=yo=Mo=0,ca.geo.stream(n,r(qo)),Mo?[vo/Mo,yo/Mo]:void 0},n.bounds=function(n){return wt(r)(n)},n.projection=function(e){return arguments.length?(r=(t=e)?e.stream||Ee(e):st,n):t},n.context=function(t){return arguments.length?(u=(e=t)==null?new ye:new we(t),n):e},n.pointRadius=function(t){return arguments.length?(i="function"==typeof t?t:+t,n):i},n.projection(ca.geo.albersUsa()).context(null)},ca.geo.albers=function(){return ca.geo.conicEqualArea().parallels([29.5,45.5]).rotate([98,0]).center([0,38]).scale(1e3)};var No=ke(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ca.geo.azimuthalEqualArea=function(){return Wt(No)}).raw=No;var To=ke(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},st);(ca.geo.azimuthalEquidistant=function(){return Wt(To)}).raw=To,(ca.geo.conicConformal=function(){return pe(Ae)}).raw=Ae,(ca.geo.conicEquidistant=function(){return pe(qe)}).raw=qe;var Co=ke(function(n){return 1/n},Math.atan);(ca.geo.gnomonic=function(){return Wt(Co)}).raw=Co,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Fa/2]},(ca.geo.mercator=function(){return Te(Ne)}).raw=Ne;var zo=ke(function(){return 1},Math.asin);(ca.geo.orthographic=function(){return Wt(zo)}).raw=zo;var Do=ke(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ca.geo.stereographic=function(){return Wt(Do)}).raw=Do,Ce.invert=function(n,t){return[Math.atan2(I(n),Math.cos(t)),U(Math.sin(t)/V(n))]},(ca.geo.transverseMercator=function(){return Te(Ce)}).raw=Ce,ca.geom={},ca.svg={},ca.svg.line=function(){return ze(st)};var jo=ca.map({linear:Le,"linear-closed":Fe,"step-before":He,"step-after":Pe,basis:Ve,"basis-open":Xe,"basis-closed":Ze,bundle:Be,cardinal:Ye,"cardinal-open":Re,"cardinal-closed":Oe,monotone:Qe});jo.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Lo=[0,2/3,1/3,0],Fo=[0,1/3,2/3,0],Ho=[0,1/6,2/3,1/6];ca.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u,i,a,o,c,l,f,s,h,g,p,d=ft(e),m=ft(r),v=n.length,y=v-1,M=[],x=[],b=0;if(d===De&&r===je)t=n;else for(i=0,t=[];v>i;++i)t.push([+d.call(this,u=n[i],i),+m.call(this,u,i)]);for(i=1;v>i;++i)t[i][1]i;++i)i!==b&&(c=t[i][1]-t[b][1],o=t[i][0]-t[b][0],M.push({angle:Math.atan2(c,o),index:i}));for(M.sort(function(n,t){return n.angle-t.angle}),g=M[0].angle,h=M[0].index,s=0,i=1;y>i;++i)a=M[i].index,g==M[i].angle?(o=t[h][0]-t[b][0],c=t[h][1]-t[b][1],l=t[a][0]-t[b][0],f=t[a][1]-t[b][1],o*o+c*c>=l*l+f*f?M[i].index=-1:(M[s].index=-1,g=M[i].angle,s=i,h=a)):(g=M[i].angle,s=i,h=a);for(x.push(b),i=0,a=0;2>i;++a)M[a].index!==-1&&(x.push(M[a].index),i++);for(p=x.length;y>a;++a)if(M[a].index!==-1){for(;!nr(x[p-2],x[p-1],M[a].index,t);)--p;x[p++]=M[a].index}var _=[];for(i=0;p>i;++i)_.push(n[x[i]]);return _}var e=De,r=je;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},ca.geom.polygon=function(n){return n.area=function(){for(var t=0,e=n.length,r=n[e-1][1]*n[0][0]-n[e-1][0]*n[0][1];++to;o++)e.push([u,t[o],t[o+1]])}),e},ca.geom.voronoi=function(n){function t(n){var t,r,a,o=n.map(function(){return[]}),c=ft(u),l=ft(i),f=n.length,s=1e6;if(c===De&&l===je)t=n;else for(t=[],a=0;f>a;++a)t.push([+c.call(this,r=n[a],a),+l.call(this,r,a)]);if(rr(t,function(n){var t,e,r,u,i,a;n.a===1&&n.b>=0?(t=n.ep.r,e=n.ep.l):(t=n.ep.l,e=n.ep.r),n.a===1?(i=t?t.y:-s,r=n.c-n.b*i,a=e?e.y:s,u=n.c-n.b*a):(r=t?t.x:-s,i=n.c-n.a*r,u=e?e.x:s,a=n.c-n.a*u);var c=[r,i],l=[u,a];o[n.region.l.index].push(c,l),o[n.region.r.index].push(c,l)}),o=o.map(function(n,e){var r=t[e][0],u=t[e][1],i=n.map(function(n){return Math.atan2(n[0]-r,n[1]-u)}),a=ca.range(n.length).sort(function(n,t){return i[n]-i[t]});return a.filter(function(n,t){return!t||i[n]-i[a[t-1]]>Ha}).map(function(t){return n[t]})}),o.forEach(function(n,e){var r=n.length;if(!r)return n.push([-s,-s],[-s,s],[s,s],[s,-s]);if(!(r>2)){var u=t[e],i=n[0],a=n[1],o=u[0],c=u[1],l=i[0],f=i[1],h=a[0],g=a[1],p=Math.abs(h-l),d=g-f;if(Math.abs(d)c?-s:s;n.push([-s,m],[s,m])}else if(Ha>p){var v=l>o?-s:s;n.push([v,-s],[v,s])}else{var m=(l-o)*(g-f)>(h-l)*(f-c)?s:-s,y=Math.abs(d)-p;Math.abs(y)d?m:-m,m]):(y>0&&(m*=-1),n.push([-s,m],[s,m]))}}}),e)for(a=0;f>a;++a)e(o[a]);for(a=0;f>a;++a)o[a].point=n[a];return o}var e,r=null,u=De,i=je;return arguments.length?t(n):(t.x=function(n){return arguments.length?(u=n,t):u},t.y=function(n){return arguments.length?(i=n,t):i},t.size=function(n){return arguments.length?(null==n?e=null:(r=[+n[0],+n[1]],e=ca.geom.polygon([[0,0],[0,r[1]],r,[r[0],0]]).clip),t):r},t.links=function(n){var t,e,r,a=n.map(function(){return[]}),o=[],c=ft(u),l=ft(i),f=n.length;if(c===De&&l===je)t=n;else for(r=0;f>r;++r)t.push([+c.call(this,e=n[r],r),+l.call(this,e,r)]);return rr(t,function(t){var e=t.region.l.index,r=t.region.r.index;a[e][r]||(a[e][r]=a[r][e]=!0,o.push({source:n[e],target:n[r]}))}),o},t.triangles=function(n){if(u===De&&i===je)return ca.geom.delaunay(n);var t,e,r,a,o,c=ft(u),l=ft(i);for(a=0,t=[],o=n.length;o>a;++a)e=[+c.call(this,r=n[a],a),+l.call(this,r,a)],e.data=r,t.push(e);return ca.geom.delaunay(t).map(function(n){return n.map(function(n){return n.data})})},t)};var Po={l:"r",r:"l"};ca.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,a,o){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,f=n.y;if(null!=c)if(Math.abs(c-e)+Math.abs(f-r)<.01)l(n,t,e,r,u,i,a,o);else{var s=n.point;n.x=n.y=n.point=null,l(n,s,c,f,u,i,a,o),l(n,t,e,r,u,i,a,o) -}else n.x=e,n.y=r,n.point=t}else l(n,t,e,r,u,i,a,o)}function l(n,t,e,r,u,a,o,c){var l=.5*(u+o),f=.5*(a+c),s=e>=l,h=r>=f,g=(h<<1)+s;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=ar()),s?u=l:o=l,h?a=f:c=f,i(n,t,e,r,u,a,o,c)}var f,s,h,g,p,d,m,v,y,M=ft(o),x=ft(c);if(null!=t)d=t,m=e,v=r,y=u;else if(v=y=-(d=m=1/0),s=[],h=[],p=n.length,a)for(g=0;p>g;++g)f=n[g],f.xv&&(v=f.x),f.y>y&&(y=f.y),s.push(f.x),h.push(f.y);else for(g=0;p>g;++g){var b=+M(f=n[g],g),_=+x(f,g);d>b&&(d=b),m>_&&(m=_),b>v&&(v=b),_>y&&(y=_),s.push(b),h.push(_)}var w=v-d,S=y-m;w>S?y=m+w:v=d+S;var E=ar();if(E.add=function(n){i(E,n,+M(n,++g),+x(n,g),d,m,v,y)},E.visit=function(n){or(n,E,d,m,v,y)},g=-1,null==t){for(;++g=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=Uo.get(e)||Yo,r=Io.get(r)||st,xr(r(e.apply(null,Array.prototype.slice.call(arguments,1))))},ca.interpolateHcl=Dr,ca.interpolateHsl=jr,ca.interpolateLab=Lr,ca.interpolateRound=Fr,ca.layout={},ca.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e(u-e)*o){var c=t.charge*o*o;return n.px-=i*c,n.py-=a*c,!0}if(t.point&&isFinite(o)){var c=t.pointCharge*o*o;n.px-=i*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=ca.event.x,n.py=ca.event.y,o.resume()}var e,r,u,i,a,o={},c=ca.dispatch("start","tick","end"),l=[1,1],f=.9,s=Vo,h=Xo,g=-30,p=.1,d=.8,m=[],v=[];return o.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,o,s,h,d,y,M,x,b=m.length,_=v.length;for(e=0;_>e;++e)o=v[e],s=o.source,h=o.target,M=h.x-s.x,x=h.y-s.y,(d=M*M+x*x)&&(d=r*i[e]*((d=Math.sqrt(d))-u[e])/d,M*=d,x*=d,h.x-=M*(y=s.weight/(h.weight+s.weight)),h.y-=x*y,s.x+=M*(y=1-y),s.y+=x*y);if((y=r*p)&&(M=l[0]/2,x=l[1]/2,e=-1,y))for(;++e0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),ca.timer(o.tick)),o):r},o.start=function(){function n(n,r){for(var u,i=t(e),a=-1,o=i.length;++ar;++r)c[r]=[];for(r=0;d>r;++r){var n=v[r];c[n.source.index].push(n.target),c[n.target.index].push(n.source)}}return c[e]}var e,r,c,f,p=m.length,d=v.length,y=l[0],M=l[1];for(e=0;p>e;++e)(f=m[e]).index=e,f.weight=0;for(e=0;d>e;++e)f=v[e],typeof f.source=="number"&&(f.source=m[f.source]),typeof f.target=="number"&&(f.target=m[f.target]),++f.source.weight,++f.target.weight;for(e=0;p>e;++e)f=m[e],isNaN(f.x)&&(f.x=n("x",y)),isNaN(f.y)&&(f.y=n("y",M)),isNaN(f.px)&&(f.px=f.x),isNaN(f.py)&&(f.py=f.y);if(u=[],"function"==typeof s)for(e=0;d>e;++e)u[e]=+s.call(this,v[e],e);else for(e=0;d>e;++e)u[e]=s;if(i=[],"function"==typeof h)for(e=0;d>e;++e)i[e]=+h.call(this,v[e],e);else for(e=0;d>e;++e)i[e]=h;if(a=[],"function"==typeof g)for(e=0;p>e;++e)a[e]=+g.call(this,m[e],e);else for(e=0;p>e;++e)a[e]=g;return o.resume()},o.resume=function(){return o.alpha(.1)},o.stop=function(){return o.alpha(0)},o.drag=function(){return e||(e=ca.behavior.drag().origin(st).on("dragstart.force",Ur).on("drag.force",t).on("dragend.force",Ir)),arguments.length?(this.on("mouseover.force",Vr).on("mouseout.force",Xr).call(e),void 0):e},ca.rebind(o,c,"on")};var Vo=20,Xo=1;ca.layout.hierarchy=function(){function n(t,a,o){var c=u.call(e,t,a);if(t.depth=a,o.push(t),c&&(l=c.length)){for(var l,f,s=-1,h=t.children=[],g=0,p=a+1;++sg;++g)for(u.call(n,l[0][g],p=d[g],f[0][g][1]),h=1;m>h;++h)u.call(n,l[h][g],p+=f[h-1][g][1],f[h][g][1]);return o}var t=st,e=tu,r=eu,u=nu,i=Wr,a=Qr;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:Bo.get(t)||tu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:$o.get(t)||eu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var Bo=ca.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ru),i=n.map(uu),a=ca.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,c=0,l=[],f=[];for(t=0;r>t;++t)e=a[t],c>o?(o+=i[e],l.push(e)):(c+=i[e],f.push(e));return f.reverse().concat(l)},reverse:function(n){return ca.range(n.length).reverse()},"default":tu}),$o=ca.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)c[e]=(o-a[e])/2;return c},wiggle:function(n){var t,e,r,u,i,a,o,c,l,f=n.length,s=n[0],h=s.length,g=[];for(g[0]=c=l=0,e=1;h>e;++e){for(t=0,u=0;f>t;++t)u+=n[t][e][1];for(t=0,i=0,o=s[e][0]-s[e-1][0];f>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=c-=u?i/u*o:0,l>c&&(l=c)}for(e=0;h>e;++e)g[e]-=l;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:eu});ca.layout.histogram=function(){function n(n,i){for(var a,o,c=[],l=n.map(e,this),f=r.call(this,l,i),s=u.call(this,f,l,i),i=-1,h=l.length,g=s.length-1,p=t?1:1/h;++i0)for(i=-1;++i=f[0]&&o<=f[1]&&(a=c[ca.bisect(s,o,1,g)-1],a.y+=p,a.push(n[i]));return c}var t=!0,e=Number,r=cu,u=au;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=ft(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ou(n,t)}:ft(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ca.layout.tree=function(){function n(n,u){function i(n,t){var r=n.children,u=n._tree;if(r&&(a=r.length)){for(var a,c,l,f=r[0],s=f,h=-1;++h0&&(yu(Mu(o,n,r),n,u),l+=u,f+=u),s+=o._tree.mod,l+=i._tree.mod,h+=c._tree.mod,f+=a._tree.mod;o&&!su(a)&&(a._tree.thread=o,a._tree.mod+=s-f),i&&!fu(c)&&(c._tree.thread=i,c._tree.mod+=l-h,r=n)}return r}var c=t.call(this,n,u),l=c[0];mu(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),i(l),a(l,-l._tree.prelim);var f=hu(l,pu),s=hu(l,gu),h=hu(l,du),g=f.x-e(f,s)/2,p=s.x+e(s,f)/2,d=h.depth||1;return mu(l,function(n){n.x=(n.x-g)/(p-g)*r[0],n.y=n.depth/d*r[1],delete n._tree}),c}var t=ca.layout.hierarchy().sort(null).value(null),e=lu,r=[1,1];return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(r=t,n):r},Br(n,t)},ca.layout.pack=function(){function n(n,u){var i=t.call(this,n,u),a=i[0];a.x=0,a.y=0,mu(a,function(n){n.r=Math.sqrt(n.value)}),mu(a,Su);var o=r[0],c=r[1],l=Math.max(2*a.r/o,2*a.r/c);if(e>0){var f=e*l/2;mu(a,function(n){n.r+=f}),mu(a,Su),mu(a,function(n){n.r-=f}),l=Math.max(2*a.r/o,2*a.r/c)}return Au(a,o/2,c/2,1/l),i}var t=ca.layout.hierarchy().sort(xu),e=0,r=[1,1];return n.size=function(t){return arguments.length?(r=t,n):r},n.padding=function(t){return arguments.length?(e=+t,n):e},Br(n,t)},ca.layout.cluster=function(){function n(n,u){var i,a=t.call(this,n,u),o=a[0],c=0;mu(o,function(n){var t=n.children;t&&t.length?(n.x=Tu(t),n.y=Nu(t)):(n.x=i?c+=e(n,i):0,n.y=0,i=n)});var l=Cu(o),f=zu(o),s=l.x-e(l,f)/2,h=f.x+e(f,l)/2;return mu(o,function(n){n.x=(n.x-s)/(h-s)*r[0],n.y=(1-(o.y?n.y/o.y:1))*r[1]}),a}var t=ca.layout.hierarchy().sort(null).value(null),e=lu,r=[1,1];return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(r=t,n):r},Br(n,t)},ca.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var a,o,c,l=s(e),f=[],h=i.slice(),p=1/0,d="slice"===g?l.dx:"dice"===g?l.dy:"slice-dice"===g?e.depth&1?l.dy:l.dx:Math.min(l.dx,l.dy);for(n(h,l.dx*l.dy/e.value),f.area=0;(c=h.length)>0;)f.push(a=h[c-1]),f.area+=a.area,"squarify"!==g||(o=r(f,d))<=p?(h.pop(),p=o):(f.area-=f.pop().area,u(f,d,l,!1),d=Math.min(l.dx,l.dy),f.length=f.area=0,p=1/0);f.length&&(u(f,d,l,!0),f.length=f.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,a=s(t),o=r.slice(),c=[];for(n(o,a.dx*a.dy/t.value),c.area=0;i=o.pop();)c.push(i),c.area+=i.area,i.z!=null&&(u(c,i.z?a.dx:a.dy,a,!o.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,a=-1,o=n.length;++ae&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,a=n.length,o=e.x,l=e.y,f=t?c(n.area/t):0;if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ie.dx)&&(f=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=Math.random()*2-1,r=Math.random()*2-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=ca.random.normal.apply(ca,arguments);return function(){return Math.exp(n())}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t/n}}},ca.scale={},ca.scale.linear=function(){return Ou([0,1],[0,1],vr,!1)},ca.scale.log=function(){return Zu(ca.scale.linear().domain([0,Math.LN10]),10,Bu,$u)};var Jo=ca.format(".0e");ca.scale.pow=function(){return Wu(ca.scale.linear(),1)},ca.scale.sqrt=function(){return ca.scale.pow().exponent(.5)},ca.scale.ordinal=function(){return ni([],{t:"range",a:[[]]})},ca.scale.category10=function(){return ca.scale.ordinal().range(Go)},ca.scale.category20=function(){return ca.scale.ordinal().range(Ko)},ca.scale.category20b=function(){return ca.scale.ordinal().range(Wo)},ca.scale.category20c=function(){return ca.scale.ordinal().range(Qo)};var Go=["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],Ko=["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"],Wo=["#393b79","#5254a3","#6b6ecf","#9c9ede","#637939","#8ca252","#b5cf6b","#cedb9c","#8c6d31","#bd9e39","#e7ba52","#e7cb94","#843c39","#ad494a","#d6616b","#e7969c","#7b4173","#a55194","#ce6dbd","#de9ed6"],Qo=["#3182bd","#6baed6","#9ecae1","#c6dbef","#e6550d","#fd8d3c","#fdae6b","#fdd0a2","#31a354","#74c476","#a1d99b","#c7e9c0","#756bb1","#9e9ac8","#bcbddc","#dadaeb","#636363","#969696","#bdbdbd","#d9d9d9"];ca.scale.quantile=function(){return ti([],[])},ca.scale.quantize=function(){return ei(0,1,[0,1])},ca.scale.threshold=function(){return ri([.5],[0,1])},ca.scale.identity=function(){return ui([0,1])},ca.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),a=r.apply(this,arguments)+nc,o=u.apply(this,arguments)+nc,c=(a>o&&(c=a,a=o,o=c),o-a),l=Fa>c?"0":"1",f=Math.cos(a),s=Math.sin(a),h=Math.cos(o),g=Math.sin(o);return c>=tc?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*f+","+i*s+"A"+i+","+i+" 0 "+l+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+l+",0 "+n*f+","+n*s+"Z":"M"+i*f+","+i*s+"A"+i+","+i+" 0 "+l+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=ii,e=ai,r=oi,u=ci;return n.innerRadius=function(e){return arguments.length?(t=ft(e),n):t},n.outerRadius=function(t){return arguments.length?(e=ft(t),n):e},n.startAngle=function(t){return arguments.length?(r=ft(t),n):r},n.endAngle=function(t){return arguments.length?(u=ft(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+nc;return[Math.cos(i)*n,Math.sin(i)*n]},n};var nc=-Fa/2,tc=2*Fa-1e-6;ca.svg.line.radial=function(){var n=ze(li);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},He.reverse=Pe,Pe.reverse=He,ca.svg.area=function(){return fi(st)},ca.svg.area.radial=function(){var n=fi(li);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ca.svg.chord=function(){function n(n,o){var c=t(this,i,n,o),l=t(this,a,n,o);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,l)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,l.r,l.p0)+r(l.r,l.p1,l.a1-l.a0)+u(l.r,l.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=o.call(n,u,r),a=c.call(n,u,r)+nc,f=l.call(n,u,r)+nc;return{r:i,a0:a,a1:f,p0:[i*Math.cos(a),i*Math.sin(a)],p1:[i*Math.cos(f),i*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=fe,a=se,o=si,c=oi,l=ci;return n.radius=function(t){return arguments.length?(o=ft(t),n):o},n.source=function(t){return arguments.length?(i=ft(t),n):i},n.target=function(t){return arguments.length?(a=ft(t),n):a},n.startAngle=function(t){return arguments.length?(c=ft(t),n):c},n.endAngle=function(t){return arguments.length?(l=ft(t),n):l},n},ca.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),a=e.call(this,n,u),o=(i.y+a.y)/2,c=[i,{x:i.x,y:o},{x:a.x,y:o},a];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=fe,e=se,r=hi;return n.source=function(e){return arguments.length?(t=ft(e),n):t},n.target=function(t){return arguments.length?(e=ft(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ca.svg.diagonal.radial=function(){var n=ca.svg.diagonal(),t=hi,e=n.projection;return n.projection=function(n){return arguments.length?e(gi(t=n)):t},n},ca.svg.symbol=function(){function n(n,r){return(ec.get(t.call(this,n,r))||mi)(e.call(this,n,r))}var t=di,e=pi;return n.type=function(e){return arguments.length?(t=ft(e),n):t},n.size=function(t){return arguments.length?(e=ft(t),n):e},n};var ec=ca.map({circle:mi,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*ic)),e=t*ic;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/uc),e=t*uc/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/uc),e=t*uc/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ca.svg.symbolTypes=ec.keys();var rc,uc=Math.sqrt(3),ic=Math.tan(30*Pa),ac=[],oc=0,cc={ease:Er,delay:0,duration:250};ac.call=ka.call,ac.empty=ka.empty,ac.node=ka.node,ca.transition=function(n){return arguments.length?rc?n.transition():n:Ca.transition()},ca.transition.prototype=ac,ac.select=function(n){var t,e,r,u=this.id,i=[];"function"!=typeof n&&(n=v(n));for(var a=-1,o=this.length;++ai;i++){u.push(t=[]);for(var e=this[i],o=0,c=e.length;c>o;o++)(r=e[o])&&n.call(r,r.__data__,o)&&t.push(r)}return vi(u,this.id,this.time).ease(this.ease())},ac.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):j(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},ac.attr=function(n,t){function e(){this.removeAttribute(i)}function r(){this.removeAttributeNS(i.space,i.local)}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var u=yr(n),i=ca.ns.qualify(n);return yi(this,"attr."+n,t,function(n){function t(){var t,e=this.getAttribute(i);return e!==n&&(t=u(e,n),function(n){this.setAttribute(i,t(n))})}function a(){var t,e=this.getAttributeNS(i.space,i.local);return e!==n&&(t=u(e,n),function(n){this.setAttributeNS(i.space,i.local,t(n))})}return null==n?i.local?r:e:(n+="",i.local?a:t)})},ac.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=ca.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},ac.style=function(n,t,e){function r(){this.style.removeProperty(n)}var u=arguments.length;if(3>u){if("string"!=typeof n){2>u&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}var i=yr(n);return yi(this,"style."+n,t,function(t){function u(){var r,u=fa.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=i(u,t),function(t){this.style.setProperty(n,r(t),e)})}return null==t?r:(t+="",u)})},ac.styleTween=function(n,t,e){return arguments.length<3&&(e=""),this.tween("style."+n,function(r,u){var i=t.call(this,r,u,fa.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}})},ac.text=function(n){return yi(this,"text",n,Mi)},ac.remove=function(){return this.each("end.transition",function(){var n;!this.__transition__&&(n=this.parentNode)&&n.removeChild(this)})},ac.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=ca.ease.apply(ca,arguments)),j(this,function(e){e.__transition__[t].ease=n}))},ac.delay=function(n){var t=this.id;return j(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=n.call(e,e.__data__,r,u)|0}:(n|=0,function(e){e.__transition__[t].delay=n}))},ac.duration=function(n){var t=this.id;return j(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u)|0)}:(n=Math.max(1,0|n),function(e){e.__transition__[t].duration=n}))},ac.each=function(n,t){var e=this.id;if(arguments.length<2){var r=cc,u=rc;rc=e,j(this,function(t,r,u){cc=t.__transition__[e],n.call(t,t.__data__,r,u)}),cc=r,rc=u}else j(this,function(r){r.__transition__[e].event.on(n,t)});return this},ac.transition=function(){for(var n,t,e,r,u=this.id,i=++oc,a=[],o=0,c=this.length;c>o;o++){a.push(n=[]);for(var t=this[o],l=0,f=t.length;f>l;l++)(e=t[l])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,xi(e,l,i,r)),n.push(e)}return vi(a,i)},ca.svg.axis=function(){function n(n){n.each(function(){var n,s=ca.select(this),h=null==l?e.ticks?e.ticks.apply(e,c):e.domain():l,g=null==t?e.tickFormat?e.tickFormat.apply(e,c):String:t,p=wi(e,h,f),d=s.selectAll(".tick.minor").data(p,String),m=d.enter().insert("line",".tick").attr("class","tick minor").style("opacity",1e-6),v=ca.transition(d.exit()).style("opacity",1e-6).remove(),y=ca.transition(d).style("opacity",1),M=s.selectAll(".tick.major").data(h,String),x=M.enter().insert("g","path").attr("class","tick major").style("opacity",1e-6),b=ca.transition(M.exit()).style("opacity",1e-6).remove(),_=ca.transition(M).style("opacity",1),w=Fu(e),S=s.selectAll(".domain").data([0]),E=(S.enter().append("path").attr("class","domain"),ca.transition(S)),k=e.copy(),A=this.__chart__||k;this.__chart__=k,x.append("line"),x.append("text");var q=x.select("line"),N=_.select("line"),T=M.select("text").text(g),C=x.select("text"),z=_.select("text");switch(r){case"bottom":n=bi,m.attr("y2",i),y.attr("x2",0).attr("y2",i),q.attr("y2",u),C.attr("y",Math.max(u,0)+o),N.attr("x2",0).attr("y2",u),z.attr("x",0).attr("y",Math.max(u,0)+o),T.attr("dy",".71em").style("text-anchor","middle"),E.attr("d","M"+w[0]+","+a+"V0H"+w[1]+"V"+a);break;case"top":n=bi,m.attr("y2",-i),y.attr("x2",0).attr("y2",-i),q.attr("y2",-u),C.attr("y",-(Math.max(u,0)+o)),N.attr("x2",0).attr("y2",-u),z.attr("x",0).attr("y",-(Math.max(u,0)+o)),T.attr("dy","0em").style("text-anchor","middle"),E.attr("d","M"+w[0]+","+-a+"V0H"+w[1]+"V"+-a);break;case"left":n=_i,m.attr("x2",-i),y.attr("x2",-i).attr("y2",0),q.attr("x2",-u),C.attr("x",-(Math.max(u,0)+o)),N.attr("x2",-u).attr("y2",0),z.attr("x",-(Math.max(u,0)+o)).attr("y",0),T.attr("dy",".32em").style("text-anchor","end"),E.attr("d","M"+-a+","+w[0]+"H0V"+w[1]+"H"+-a);break;case"right":n=_i,m.attr("x2",i),y.attr("x2",i).attr("y2",0),q.attr("x2",u),C.attr("x",Math.max(u,0)+o),N.attr("x2",u).attr("y2",0),z.attr("x",Math.max(u,0)+o).attr("y",0),T.attr("dy",".32em").style("text-anchor","start"),E.attr("d","M"+a+","+w[0]+"H0V"+w[1]+"H"+a)}if(e.ticks)x.call(n,A),_.call(n,k),b.call(n,k),m.call(n,A),y.call(n,k),v.call(n,k);else{var D=k.rangeBand()/2,j=function(n){return k(n)+D};x.call(n,j),_.call(n,j)}})}var t,e=ca.scale.linear(),r=lc,u=6,i=6,a=6,o=3,c=[10],l=null,f=0;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in fc?t+"":lc,n):r},n.ticks=function(){return arguments.length?(c=arguments,n):c},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t,e){if(!arguments.length)return u;var r=arguments.length-1;return u=+t,i=r>1?+e:u,a=r>0?+arguments[r]:u,n},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(t){return arguments.length?(f=+t,n):f},n};var lc="bottom",fc={top:1,right:1,bottom:1,left:1};ca.svg.brush=function(){function n(i){i.each(function(){var i,a=ca.select(this),l=a.selectAll(".background").data([0]),s=a.selectAll(".extent").data([0]),h=a.selectAll(".resize").data(f,String);a.style("pointer-events","all").on("mousedown.brush",u).on("touchstart.brush",u),l.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),s.enter().append("rect").attr("class","extent").style("cursor","move"),h.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return sc[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),h.style("display",n.empty()?"none":null),h.exit().remove(),o&&(i=Fu(o),l.attr("x",i[0]).attr("width",i[1]-i[0]),e(a)),c&&(i=Fu(c),l.attr("y",i[0]).attr("height",i[1]-i[0]),r(a)),t(a)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)][0]+","+s[+/^s/.test(n)][1]+")"})}function e(n){n.select(".extent").attr("x",s[0][0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1][0]-s[0][0])}function r(n){n.select(".extent").attr("y",s[0][1]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",s[1][1]-s[0][1])}function u(){function u(){var n=ca.event.changedTouches;return n?ca.touches(y,n)[0]:ca.mouse(y)}function f(){ca.event.keyCode==32&&(E||(m=null,k[0]-=s[1][0],k[1]-=s[1][1],E=2),l())}function h(){ca.event.keyCode==32&&2==E&&(k[0]+=s[1][0],k[1]+=s[1][1],E=0,l())}function g(){var n=u(),i=!1;v&&(n[0]+=v[0],n[1]+=v[1]),E||(ca.event.altKey?(m||(m=[(s[0][0]+s[1][0])/2,(s[0][1]+s[1][1])/2]),k[0]=s[+(n[0]l?(u=r,r=l):u=l),s[0][e]!==r||s[1][e]!==u?(i=null,s[0][e]=r,s[1][e]=u,!0):void 0}function d(){g(),b.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ca.select("body").style("cursor",null),A.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),x({type:"brushend"}),l()}var m,v,y=this,M=ca.select(ca.event.target),x=a.of(y,arguments),b=ca.select(y),_=M.datum(),w=!/^(n|s)$/.test(_)&&o,S=!/^(e|w)$/.test(_)&&c,E=M.classed("extent"),k=u(),A=ca.select(fa).on("mousemove.brush",g).on("mouseup.brush",d).on("touchmove.brush",g).on("touchend.brush",d).on("keydown.brush",f).on("keyup.brush",h);if(E)k[0]=s[0][0]-k[0],k[1]=s[0][1]-k[1];else if(_){var q=+/w$/.test(_),N=+/^n/.test(_);v=[s[1-q][0]-k[0],s[1-N][1]-k[1]],k[0]=s[q][0],k[1]=s[N][1]}else ca.event.altKey&&(m=k.slice());b.style("pointer-events","none").selectAll(".resize").style("display",null),ca.select("body").style("cursor",M.style("cursor")),x({type:"brushstart"}),g(),l()}var i,a=h(n,"brushstart","brush","brushend"),o=null,c=null,f=hc[0],s=[[0,0],[0,0]];return n.x=function(t){return arguments.length?(o=t,f=hc[!o<<1|!c],n):o -},n.y=function(t){return arguments.length?(c=t,f=hc[!o<<1|!c],n):c},n.extent=function(t){var e,r,u,a,l;return arguments.length?(i=[[0,0],[0,0]],o&&(e=t[0],r=t[1],c&&(e=e[0],r=r[0]),i[0][0]=e,i[1][0]=r,o.invert&&(e=o(e),r=o(r)),e>r&&(l=e,e=r,r=l),s[0][0]=0|e,s[1][0]=0|r),c&&(u=t[0],a=t[1],o&&(u=u[1],a=a[1]),i[0][1]=u,i[1][1]=a,c.invert&&(u=c(u),a=c(a)),u>a&&(l=u,u=a,a=l),s[0][1]=0|u,s[1][1]=0|a),n):(t=i||s,o&&(e=t[0][0],r=t[1][0],i||(e=s[0][0],r=s[1][0],o.invert&&(e=o.invert(e),r=o.invert(r)),e>r&&(l=e,e=r,r=l))),c&&(u=t[0][1],a=t[1][1],i||(u=s[0][1],a=s[1][1],c.invert&&(u=c.invert(u),a=c.invert(a)),u>a&&(l=u,u=a,a=l))),o&&c?[[e,u],[r,a]]:o?[e,r]:c&&[u,a])},n.clear=function(){return i=null,s[0][0]=s[0][1]=s[1][0]=s[1][1]=0,n},n.empty=function(){return o&&s[0][0]===s[1][0]||c&&s[0][1]===s[1][1]},ca.rebind(n,a,"on")};var sc={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},hc=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]];ca.time={};var gc=Date,pc=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];Si.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){dc.setUTCDate.apply(this._,arguments)},setDay:function(){dc.setUTCDay.apply(this._,arguments)},setFullYear:function(){dc.setUTCFullYear.apply(this._,arguments)},setHours:function(){dc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){dc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){dc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){dc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){dc.setUTCSeconds.apply(this._,arguments)},setTime:function(){dc.setTime.apply(this._,arguments)}};var dc=Date.prototype,mc="%a %b %e %X %Y",vc="%m/%d/%Y",yc="%H:%M:%S",Mc=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],xc=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],bc=["January","February","March","April","May","June","July","August","September","October","November","December"],_c=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];ca.time.year=Ei(function(n){return n=ca.time.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ca.time.years=ca.time.year.range,ca.time.years.utc=ca.time.year.utc.range,ca.time.day=Ei(function(n){var t=new gc(1970,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ca.time.days=ca.time.day.range,ca.time.days.utc=ca.time.day.utc.range,ca.time.dayOfYear=function(n){var t=ca.time.year(n);return Math.floor((n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/864e5)},pc.forEach(function(n,t){n=n.toLowerCase(),t=7-t;var e=ca.time[n]=Ei(function(n){return(n=ca.time.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+Math.floor(t)*7)},function(n){var e=ca.time.year(n).getDay();return Math.floor((ca.time.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ca.time[n+"s"]=e.range,ca.time[n+"s"].utc=e.utc.range,ca.time[n+"OfYear"]=function(n){var e=ca.time.year(n).getDay();return Math.floor((ca.time.dayOfYear(n)+(e+t)%7)/7)}}),ca.time.week=ca.time.sunday,ca.time.weeks=ca.time.sunday.range,ca.time.weeks.utc=ca.time.sunday.utc.range,ca.time.weekOfYear=ca.time.sundayOfYear,ca.time.format=function(n){function t(t){for(var r,u,i,a=[],o=-1,c=0;++o=12?"PM":"AM"},S:function(n,t){return Ti(n.getSeconds(),t,2)},U:function(n,t){return Ti(ca.time.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ti(ca.time.mondayOfYear(n),t,2)},x:ca.time.format(vc),X:ca.time.format(yc),y:function(n,t){return Ti(n.getFullYear()%100,t,2)},Y:function(n,t){return Ti(n.getFullYear()%1e4,t,4)},Z:$i,"%":function(){return"%"}},Cc={a:Ci,A:zi,b:Di,B:ji,c:Li,d:Ui,e:Ui,H:Ii,I:Ii,L:Zi,m:Yi,M:Vi,p:Bi,S:Xi,x:Fi,X:Hi,y:Ri,Y:Pi},zc=/^\s*\d+/,Dc=ca.map({am:0,pm:1});ca.time.format.utc=function(n){function t(n){try{gc=Si;var t=new gc;return t._=n,e(t)}finally{gc=Date}}var e=ca.time.format(n);return t.parse=function(n){try{gc=Si;var t=e.parse(n);return t&&t._}finally{gc=Date}},t.toString=e.toString,t};var jc=ca.time.format.utc("%Y-%m-%dT%H:%M:%S.%LZ");ca.time.format.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Ji:jc,Ji.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Ji.toString=jc.toString,ca.time.second=Ei(function(n){return new gc(Math.floor(n/1e3)*1e3)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*1e3)},function(n){return n.getSeconds()}),ca.time.seconds=ca.time.second.range,ca.time.seconds.utc=ca.time.second.utc.range,ca.time.minute=Ei(function(n){return new gc(Math.floor(n/6e4)*6e4)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*6e4)},function(n){return n.getMinutes()}),ca.time.minutes=ca.time.minute.range,ca.time.minutes.utc=ca.time.minute.utc.range,ca.time.hour=Ei(function(n){var t=n.getTimezoneOffset()/60;return new gc((Math.floor(n/36e5-t)+t)*36e5)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*36e5)},function(n){return n.getHours()}),ca.time.hours=ca.time.hour.range,ca.time.hours.utc=ca.time.hour.utc.range,ca.time.month=Ei(function(n){return n=ca.time.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ca.time.months=ca.time.month.range,ca.time.months.utc=ca.time.month.utc.range;var Lc=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Fc=[[ca.time.second,1],[ca.time.second,5],[ca.time.second,15],[ca.time.second,30],[ca.time.minute,1],[ca.time.minute,5],[ca.time.minute,15],[ca.time.minute,30],[ca.time.hour,1],[ca.time.hour,3],[ca.time.hour,6],[ca.time.hour,12],[ca.time.day,1],[ca.time.day,2],[ca.time.week,1],[ca.time.month,1],[ca.time.month,3],[ca.time.year,1]],Hc=[[ca.time.format("%Y"),jt],[ca.time.format("%B"),function(n){return n.getMonth()}],[ca.time.format("%b %d"),function(n){return n.getDate()!=1}],[ca.time.format("%a %d"),function(n){return n.getDay()&&n.getDate()!=1}],[ca.time.format("%I %p"),function(n){return n.getHours()}],[ca.time.format("%I:%M"),function(n){return n.getMinutes()}],[ca.time.format(":%S"),function(n){return n.getSeconds()}],[ca.time.format(".%L"),function(n){return n.getMilliseconds()}]],Pc=ca.scale.linear(),Rc=Qi(Hc);Fc.year=function(n,t){return Pc.domain(n.map(ta)).ticks(t).map(na)},ca.time.scale=function(){return Gi(ca.scale.linear(),Fc,Rc)};var Oc=Fc.map(function(n){return[n[0].utc,n[1]]}),Yc=[[ca.time.format.utc("%Y"),jt],[ca.time.format.utc("%B"),function(n){return n.getUTCMonth()}],[ca.time.format.utc("%b %d"),function(n){return n.getUTCDate()!=1}],[ca.time.format.utc("%a %d"),function(n){return n.getUTCDay()&&n.getUTCDate()!=1}],[ca.time.format.utc("%I %p"),function(n){return n.getUTCHours()}],[ca.time.format.utc("%I:%M"),function(n){return n.getUTCMinutes()}],[ca.time.format.utc(":%S"),function(n){return n.getUTCSeconds()}],[ca.time.format.utc(".%L"),function(n){return n.getUTCMilliseconds()}]],Uc=Qi(Yc);return Oc.year=function(n,t){return Pc.domain(n.map(ra)).ticks(t).map(ea)},ca.time.scale.utc=function(){return Gi(ca.scale.linear(),Oc,Uc)},ca.text=function(){return ca.xhr.apply(ca,arguments).response(ua)},ca.json=function(n,t){return ca.xhr(n,"application/json",t).response(ia)},ca.html=function(n,t){return ca.xhr(n,"text/html",t).response(aa)},ca.xml=function(){return ca.xhr.apply(ca,arguments).response(oa)},ca}(); \ No newline at end of file diff --git a/js/leaflet-hash.js b/js/leaflet-hash.js index ccd98544..564bd9e7 100644 --- a/js/leaflet-hash.js +++ b/js/leaflet-hash.js @@ -1,159 +1,228 @@ -(function(window) { - var HAS_HASHCHANGE = (function() { - var doc_mode = window.documentMode; - return ('onhashchange' in window) && - (doc_mode === undefined || doc_mode > 7); - })(); - - L.Hash = function(map) { - this.onHashChange = L.Util.bind(this.onHashChange, this); - - if (map) { - this.init(map); - } - }; - - L.Hash.prototype = { - map: null, - lastHash: null, - - parseHash: function(hash) { - if(hash.indexOf('#') === 0) { - hash = hash.substr(1); - } - var args = hash.split("/"); - if (args.length == 3) { - var zoom = parseInt(args[0], 10), - lat = parseFloat(args[1]), - lon = parseFloat(args[2]); - if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { - return false; - } else { - return { - center: new L.LatLng(lat, lon), - zoom: zoom - }; - } - } else { - return false; - } - }, - - formatHash: function(map) { - var center = map.getCenter(), - zoom = map.getZoom(), - precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); - - return "#" + [zoom, - center.lat.toFixed(precision), - center.lng.toFixed(precision) - ].join("/"); - }, - - init: function(map) { - this.map = map; - - // reset the hash - this.lastHash = null; - this.onHashChange(); - - if (!this.isListening) { - this.startListening(); - } - }, - - remove: function() { - if (this.changeTimeout) { - clearTimeout(this.changeTimeout); - } - - if (this.isListening) { - this.stopListening(); - } - - this.map = null; - }, - - onMapMove: function() { - // bail if we're moving the map (updating from a hash), - // or if the map is not yet loaded - - if (this.movingMap || !this.map._loaded) { - return false; - } - - var hash = this.formatHash(this.map); - if (this.lastHash != hash) { - location.replace(hash); - this.lastHash = hash; - } - }, - - movingMap: false, - update: function() { - var hash = location.hash; - if (hash === this.lastHash) { - return; - } - var parsed = this.parseHash(hash); - if (parsed) { - this.movingMap = true; - - this.map.setView(parsed.center, parsed.zoom); - - this.movingMap = false; - } else { - this.onMapMove(this.map); - } - }, - - // defer hash change updates every 100ms - changeDefer: 100, - changeTimeout: null, - onHashChange: function() { - // throttle calls to update() so that they only happen every - // `changeDefer` ms - if (!this.changeTimeout) { - var that = this; - this.changeTimeout = setTimeout(function() { - that.update(); - that.changeTimeout = null; - }, this.changeDefer); - } - }, - - isListening: false, - hashChangeInterval: null, - startListening: function() { - this.map.on("moveend", this.onMapMove, this); - - if (HAS_HASHCHANGE) { - L.DomEvent.addListener(window, "hashchange", this.onHashChange); - } else { - clearInterval(this.hashChangeInterval); - this.hashChangeInterval = setInterval(this.onHashChange, 50); - } - this.isListening = true; - }, - - stopListening: function() { - this.map.off("moveend", this.onMapMove, this); - - if (HAS_HASHCHANGE) { - L.DomEvent.removeListener(window, "hashchange", this.onHashChange); - } else { - clearInterval(this.hashChangeInterval); - } - this.isListening = false; - } - }; - L.hash = function(map) { - return new L.Hash(map); - }; - L.Map.prototype.addHash = function() { - this._hash = L.hash(this); - }; - L.Map.prototype.removeHash = function() { - this._hash.remove(); - }; -})(window); +//leaflet-hash (https://github.com/calvinmetcalf/leaflet-hash) +(function() { + + L.Hash = L.Class.extend({ + initialize: function(map, options) { + this.map = map; + this.options = options || {}; + if (!this.options.path) { + if (this.options.lc) { + this.options.path = '{base}/{z}/{lat}/{lng}'; + } else { + this.options.path = '{z}/{lat}/{lng}'; + } + } + if (this.options.lc && !this.options.formatBase) { + this.options.formatBase = [ + /[\s\:A-Z]/g, function(match) { + if (match.match(/\s/)) { + return "_"; + } else if (match.match(/\:/)) { + return ""; + } + if (match.match(/[A-Z]/)) { + return match.toLowerCase(); + } + } + ]; + }if (location.hash) { + this.updateFromState(this.parseHash(location.hash));//sets the view + const pstate = this.formatState(); + history.replaceState.apply(history, pstate);//write current location to browser history cache (for back button) + } + if (this.map._loaded) { + return this.startListning(); + } else { + return this.map.on("load", this.startListning,this); + } + }, + startListning: function() { + var onHashChange, + _this = this; + if (location.hash) { + this.updateFromState(this.parseHash(location.hash)); + } + if (history.pushState) { + if (!location.hash) { + history.replaceState.apply(history, this.formatState()); + } + window.onpopstate = function(event) { + + if (event.state) { + return _this.updateFromState(event.state); + } + }; + this.map.on("moveend", function() { + var pstate; + pstate = _this.formatState(); + if (location.hash !== pstate[2] && !_this.moving) { + return history.pushState.apply(history, pstate); + } + }); + } else { + if (!location.hash) { + location.hash = this.formatState()[2]; + } + onHashChange = function() { + + var pstate; + pstate = _this.formatState(); + if (location.hash !== pstate[2] && !_this.moving) { + return location.hash = pstate[2]; + } + }; + this.map.on("moveend", onHashChange); + if (('onhashchange' in window) && (window.documentMode === void 0 || window.documentMode > 7)) { + window.onhashchange = function() { + if (location.hash) { + + return _this.updateFromState(_this.parseHash(location.hash)); + } + }; + } else { + this.hashChangeInterval = setInterval(onHashChange, 50); + } + } + return this.map.on("baselayerchange", function(e) { + var pstate, _ref; + //without the guard below leaflet-hash throws an error. + if (!_this.options.lc) return + _this.base = (_ref = _this.options.lc._layers[e.layer._leaflet_id].name).replace.apply(_ref, _this.options.formatBase); + pstate = _this.formatState(); + if (history.pushState) { + if (location.hash !== pstate[2] && !_this.moving) { + return history.pushState.apply(history, pstate); + } + } else { + if (location.hash !== pstate[2] && !_this.moving) { + return location.hash = pstate[2]; + } + } + }); + }, + parseHash: function(hash) { + + var args, lat, latIndex, lngIndex, lon, out, path, zIndex, zoom; + path = this.options.path.split("/"); + zIndex = path.indexOf("{z}"); + latIndex = path.indexOf("{lat}"); + lngIndex = path.indexOf("{lng}"); + if (hash.indexOf("#") === 0) { + hash = hash.substr(1); + } + args = hash.split("/"); + + if (args.length > 2) { + zoom = parseInt(args[zIndex], 10); + lat = parseFloat(args[latIndex]); + lon = parseFloat(args[lngIndex]); + if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { + return false; + } else { + out = { + center: new L.LatLng(lat, lon), + zoom: zoom + }; + if (args.length > 3) { + out.base = args[path.indexOf("{base}")]; + return out; + } else { + return out; + } + } + } else { + + return false; + } + }, + updateFromState: function(state) { + if (this.moving || !state) { + return; + } + this.moving = true; + this.map.setView(state.center, state.zoom); + if (state.base) { + this.setBase(state.base); + } + this.moving = false; + return true; + }, + formatState: function() { + var center, precision, state, template, zoom; + center = this.map.getCenter(); + zoom = this.map.getZoom(); + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); + state = { + center: center, + zoom: zoom + }; + template = { + lat: center.lat.toFixed(precision), + lng: center.lng.toFixed(precision), + z: zoom + }; + if (this.options.path.indexOf("{base}") > -1) { + state.base = this.getBase(); + template.base = state.base; + } + return [state, "a", '#' + L.Util.template(this.options.path, template)]; + }, + setBase: function(base) { + var i, inputs, len, _ref; + this.base = base; + inputs = this.options.lc._form.getElementsByTagName('input'); + len = inputs.length; + i = 0; + while (i < len) { + if (inputs[i].name === 'leaflet-base-layers' && (_ref = this.options.lc._layers[inputs[i].layerId].name).replace.apply(_ref, this.options.formatBase) === base) { + inputs[i].checked = true; + this.options.lc._onInputClick(); + return true; + } + i++; + } + }, + getBase: function() { + var i, inputs, len, _ref; + if (this.base) { + return this.base; + } + inputs = this.options.lc._form.getElementsByTagName('input'); + len = inputs.length; + i = 0; + while (i < len) { + if (inputs[i].name === 'leaflet-base-layers' && inputs[i].checked) { + this.base = (_ref = this.options.lc._layers[inputs[i].layerId].name).replace.apply(_ref, this.options.formatBase); + return this.base; + } + } + return false; + }, + remove: function() { + this.map.off("moveend"); + if (window.onpopstate) { + window.onpopstate = null; + } + location.hash = ""; + return clearInterval(this.hashChangeInterval); + } + }); + + L.hash = function(map, options) { + return new L.Hash(map, options); + }; + + L.Map.include({ + addHash: function(options) { + + this._hash = L.hash(this, options); + + return this; + }, + removeHash: function() { + this._hash.remove(); + return this; + } + }); + +}).call(this); \ No newline at end of file diff --git a/js/leaflet-osm.js b/js/leaflet-osm.js deleted file mode 100644 index 44e6c363..00000000 --- a/js/leaflet-osm.js +++ /dev/null @@ -1,167 +0,0 @@ -L.OSM = {}; - -L.OSM.DataLayer = L.FeatureGroup.extend({ - options: { - areaTags: ['area', 'building', 'leisure', 'tourism', 'ruins', 'historic', 'landuse', 'military', 'natural', 'sport'], - uninterestingTags: ['source', 'source_ref', 'source:ref', 'history', 'attribution', 'created_by', 'tiger:county', 'tiger:tlid', 'tiger:upload_uuid'], - styles: {} - }, - - initialize: function (xml, options) { - L.Util.setOptions(this, options); - - L.FeatureGroup.prototype.initialize.call(this); - - if (xml) { - this.addData(xml); - } - }, - - addData: function (features) { - if (!(features instanceof Array)) { - features = this.buildFeatures(features); - } - - for (var i = 0; i < features.length; i++) { - var feature = features[i], layer; - - if (feature.type === "node") { - layer = L.circleMarker(feature.latLng, this.options.styles.node); - } else { - var latLngs = new Array(feature.nodes.length); - - for (var j = 0; j < feature.nodes.length; j++) { - latLngs[j] = feature.nodes[j].latLng; - } - - if (this.isWayArea(feature)) { - latLngs.pop(); // Remove last == first. - layer = L.polygon(latLngs, this.options.styles.area); - } else { - layer = L.polyline(latLngs, this.options.styles.way); - } - } - - layer.addTo(this); - layer.feature = feature; - } - }, - - buildFeatures: function (xml) { - var features = [], - nodes = L.OSM.getNodes(xml), - ways = L.OSM.getWays(xml, nodes); - - for (var node_id in nodes) { - var node = nodes[node_id]; - if (this.interestingNode(node, ways)) { - features.push(node); - } - } - - for (var i = 0; i < ways.length; i++) { - var way = ways[i]; - features.push(way); - } - - return features; - }, - - isWayArea: function (way) { - if (way.nodes[0] != way.nodes[way.nodes.length - 1]) { - return false; - } - - for (var key in way.tags) { - if (~this.options.areaTags.indexOf(key)) { - return true; - } - } - - return false; - }, - - interestingNode: function (node, ways) { - var used = false; - - for (var i = 0; i < ways.length; i++) { - if (ways[i].nodes.indexOf(node) >= 0) { - used = true; - break; - } - } - - if (!used) { - return true; - } - - for (var key in node.tags) { - if (this.options.uninterestingTags.indexOf(key) < 0) { - return true; - } - } - - return false; - } -}); - -L.Util.extend(L.OSM, { - getNodes: function (xml) { - var result = {}; - - var nodes = xml.getElementsByTagName("node"); - for (var i = 0; i < nodes.length; i++) { - var node = nodes[i], id = node.getAttribute("id"); - result[id] = { - id: id, - type: 'node', - latLng: L.latLng(node.getAttribute("lat"), - node.getAttribute("lon"), true), - timestamp: node.getAttribute('timestamp'), - changeset: node.getAttribute('changeset'), - user: node.getAttribute('user'), - tags: this.getTags(node) - }; - } - - return result; - }, - - getWays: function (xml, nodes) { - var result = []; - - var ways = xml.getElementsByTagName("way"); - for (var i = 0; i < ways.length; i++) { - var way = ways[i], nds = way.getElementsByTagName("nd"); - - var way_object = { - id: way.getAttribute("id"), - type: "way", - nodes: new Array(nds.length), - timestamp: way.getAttribute('timestamp'), - changeset: way.getAttribute('changeset'), - user: way.getAttribute('user'), - tags: this.getTags(way) - }; - - for (var j = 0; j < nds.length; j++) { - way_object.nodes[j] = nodes[nds[j].getAttribute("ref")]; - } - - result.push(way_object); - } - - return result; - }, - - getTags: function (xml) { - var result = {}; - - var tags = xml.getElementsByTagName("tag"); - for (var j = 0; j < tags.length; j++) { - result[tags[j].getAttribute("k")] = tags[j].getAttribute("v"); - } - - return result; - } -}); diff --git a/js/leaflet.js b/js/leaflet.js new file mode 100644 index 00000000..a3bf693d --- /dev/null +++ b/js/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1 Activate gesture handling if screen width is < 601px +window.addEventListener("resize", () => { + // console.log(screen.width); + if (screen.width < 601) map.gestureHandling.enable(); + else map.gestureHandling.disable(); +}); + +//Add scale +L.control.scale().addTo(map); + +//Add "set current location" button to map L.control.locate({ initialZoomLevel: 14, strings: { - title: "Show changes around my current location" + title: "Go to my current location" } }).addTo(map); -var overpass_server = '//overpass-api.de/api/'; //'https://overpass.kumi.systems/api/'; +//Toggle sidebar button on map +let sidebar = document.querySelector(".sidebar"); +L.Control.toggleSidebarButton = L.Control.extend( + { + options: + { + position: 'topleft', + }, + onAdd: function (map) { + let controlDiv = L.DomUtil.create('div', 'leaflet-control-toggle leaflet-bar'); + L.DomEvent + .addListener(controlDiv, 'click', L.DomEvent.stopPropagation) + .addListener(controlDiv, 'click', L.DomEvent.preventDefault) + .addListener(controlDiv, 'click', toggleSidebar); + + let controlUI = L.DomUtil.create('a', 'leaflet-bar-part leaflet-bar-part-single', controlDiv); + controlUI.title = 'Toggle display of sidebar '; + controlUI.href = '#'; + + let barIcon = L.DomUtil.create('span', 'leaflet-control-toggle-icon', controlUI); + + return controlDiv; + } + }); +let toggleSidebarButton = new L.Control.toggleSidebarButton(); +map.addControl(toggleSidebarButton); + +/*Toggle sidebar class and update Leaflet map size +(but quite useless because the download button is hidden anyway, thus cannot make use of the added map real estate to download changesets of a larger region)*/ +function toggleSidebar() { + sidebar.classList.toggle("hide"); + // let size = map.getSize(); + // console.log(size); + // let bounds = map.getBounds(); + // console.log(bounds); + map.invalidateSize();//make sure that leaflet updates map size + // size = map.getSize(); + // console.log(size); + // bounds = map.getBounds(); + // console.log(bounds); +} -var days_to_show; -// load resolution_from_local_storage from local storage, if available -var resolution_from_local_storage = localStorage.getItem("resolution"); -if (resolution_from_local_storage) { - days_to_show = resolution_from_local_storage; +//1. Toggle display of sidebar on press of Spacebar +//2. On typing a letter or number: Focus on filter changeset input field and input first letter +document.addEventListener("keyup", event => { + // console.log(event.key); + const isLetter = (event.key >= 'a' && event.key <= 'z'); + const isNumber = (event.key >= '0' && event.key <= '9'); + event.preventDefault(); //prevent default, i.e. "page down" for spacebar + if (event.key == " ") { + toggleSidebar(); + } else if (isLetter || isNumber) { + let inputField = document.querySelector(".search-changesets-field"); + //If the input field is empty AND doesn't have focus: Add focus, write typed letter in field and launch filter function. + if (!inputField.value && document.activeElement !== inputField) { + inputField.focus(); + inputField.value = event.key; + showHideCrossThenFilter(); + } + } +}); + +//Open link to OSM when "RMB --> Open with..." +function openOSM(e) { + const url = `https://www.openstreetmap.org/?mlat=${e.latlng.lat}&mlon=${e.latlng.lng}#map=${map.getZoom()}/${e.latlng.lat}/${e.latlng.lng}`; + window.open(url, '_blank').focus(); +} + +//Open link to Google Maps when "RMB --> Open with..." +function openGmaps(e) { + const url = `https://maps.google.com/maps?q=loc:${e.latlng.lat},${e.latlng.lng}`; + window.open(url, '_blank').focus(); +} + +//Timeframe +let daysToShow; +// load resolutionFromLocalStorage from local storage, if available +const resolutionFromLocalStorage = localStorage.getItem("resolution"); +if (resolutionFromLocalStorage) { + daysToShow = resolutionFromLocalStorage; // select value from local storage in drop down menu - document.getElementById('resolution').value = days_to_show; + document.getElementById('resolution').value = daysToShow; } else { // else, i.e. no resolution saved in local storage: Default to 7 days - days_to_show = 7; + daysToShow = 7; } -var layer = null; +//Calculate point in time from where to start analysis +function calculateAnalysisStartTime() { + const analysisStartTime = (new Date(new Date() - 1000 * 60 * 60 * 24 * daysToShow)); + // console.log(analysisStartTime); + return analysisStartTime; +} + +//Since we want to display a custom attribution: Hide the "Leaflet |" prefix when displaying attribution. map.attributionControl.setPrefix(''); -L.hash(map); -L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, + +//Add the standard OSM Carto tile layer to the map +const osmCartoLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 21,//Allow zoom levels up to 21 even though tiles are only available until z19. Leaflet will interpolate. + maxNativeZoom: 19, attribution: "" -}) - .addTo(map); +}).addTo(map); + +//Create a blank (i.e. white) tile layer. This makes it sometimes easier to find highlighted geometries. +const whiteTileUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwCAIAAADYVYZuAAAAKUlEQVR4nO3BMQEAAAwCoNm/9HIwAAAAAAAAAAAAAAAAAAAAAAAAAHwFtXIAARgZGQoAAAAASUVORK5CYII="; + +const whiteTileLayer = L.tileLayer(whiteTileUrl, { + tileSize: 256, + maxZoom: 21, + attribution: '' +}); + +const baseMaps = { + "OSM Carto": osmCartoLayer, + "Blank": whiteTileLayer +}; + +//Add the layer control to the top right of the map. +L.control.layers(baseMaps).addTo(map); +//Update hash on map pan/zoom (functionality from leaflet-hash.js) +map.addHash(); + +//Update map display on zoom and page load function updateMap() { - if (map.getZoom() > 11) { - d3.select('#map').classed('faded', true); - d3.select('#zoom-in').classed('hide', true); - run(); + // console.log('updatemap called'); + const mapHtml = document.querySelector("#map"); + const currentZoomLevel = map.getZoom(); + const button = document.getElementById('download-changesets-button'); + const infoText = document.getElementById("zoom-in"); + localStorage.setItem('location-hash', location.hash); + if (currentZoomLevel > 11) { + // console.log("Zoom level > 11"); + mapHtml.classList.remove("faded"); + infoText.classList.add("hide"); + button.disabled = ""; + button.title = "Download recent changesets in this region"; + return true; } else { - d3.select('#map').classed('faded', true); - d3.select('#zoom-in').classed('hide', false); - abort(); - layer && map.removeLayer(layer); - layer = null; + // console.log("Zoom level <= 11"); + mapHtml.classList.add("faded"); + infoText.classList.remove("hide"); + button.disabled = "disabled"; + button.title = "Zoom in to download changes"; + return false; } - localStorage.setItem('location-hash', location.hash); } -updateMap(); +/** +* Linearly interpolates between two colors based on a factor. +* @param {string} rgb1 - Start rgb color string. +* @param {string} rgb2 - End rgb color string. +* @param {number} factor - Interpolation factor (0.0 to 1.0). 0 = color1, 1 = color2. +* @returns {string} - Interpolated rgb color string. +*/ +function interpolateColor(rgb1, rgb2, factor) { + // Get r, g, b values as numbers + // Explanation: + // rgb1.match(/\d+/g) returns an array with the 3 numbers as strings, e.g. ['119', '119', '119'] + // .map(Number) is a shorthand for ['119', '119', '119'].map(str => Number(str)) and converts all strings to numbers + const [rgb1R, rgb1G, rgb1B] = rgb1.match(/\d+/g).map(Number); + const [rgb2R, rgb2G, rgb2B] = rgb2.match(/\d+/g).map(Number); + + // Find the 'in-between-color' for r, g and b based on the age of the changeset. + const r = Math.round(rgb1R + (rgb2R - rgb1R) * factor); + const g = Math.round(rgb1G + (rgb2G - rgb1G) * factor); + const b = Math.round(rgb1B + (rgb2B - rgb1B) * factor); + + // console.log(`rgb(${r},${g},${b})`); + return `rgb(${r},${g},${b})`; +} + +// Return color depending on age of changeset. New: bright red, old: dark red/gray +function defineColor(date) { + // console.log('defineColor() called'); + + // Ensure input 'date' is a Date object + if (!(date instanceof Date)) { + date = new Date(date); // Attempt to convert if not already a Date + if (isNaN(date)) { // Check if conversion failed + console.error("Invalid date passed to defineColor:", date); + return '#777777'; // Return default gray on error + } + } + + const colorOldest = 'rgb(119, 119, 119)'; // Gray for oldest + const colorNewest = 'rgb(255, 0, 0)'; // Red for newest + + const startDate = calculateAnalysisStartTime(); // Calculate analysis start date + // console.log(startDate); + const endDate = new Date(); // Now + + // Get timestamps (milliseconds) + const startTimestamp = startDate.getTime(); + const endTimestamp = endDate.getTime(); + const changesetTimestamp = date.getTime(); + + // Calculate total duration of the time window + const totalDuration = endTimestamp - startTimestamp; + + // Handle edge case where start and end times are the same (or invalid) + if (totalDuration <= 0) { + return colorNewest; // If no duration, default to newest color + } + + // Calculate how far the input date is into the duration + const elapsedTime = changesetTimestamp - startTimestamp; -var xhr; + // Calculate the proportion (0 to 1) + let proportion = elapsedTime / totalDuration; + + // Clamp the proportion to be strictly between 0 and 1 + proportion = Math.max(0, Math.min(1, proportion)); + + // Interpolate the color based on the proportion + return interpolateColor(colorOldest, colorNewest, proportion); +} + +//Toggle display of loading animation and appearance of download button +function toggleWaitingScreen() { + const loadingAnimation = document.querySelector("#loading-animation"); + const button = document.getElementById('download-changesets-button'); + const buttonText = document.getElementById('download-changesets-button-text'); + + loadingAnimation.classList.toggle("hide");//display loading spinner + buttonText.innerText = (buttonText.innerText == "Get Changesets") ? "Loading..." : "Get Changesets";//Toggle button text + button.disabled = (button.disabled) ? button.disabled = "" : button.disabled = "disabled";//Toggle button disable +} + +//On page load: Check if map is zoomed in enough. If yes: Download OSM changeset data from overpass +//There are multiple public Overpass instances available. See https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances +const overpassServer = '//overpass-api.de/api/'; +const vandalismThreshold = -3; //If 3 more elements or tags have been deleted than added, the traffic light will change to red +const debugMode = false; //False (default): Do an API call to Overpass. True: Use locally saved xml files for debugging/development purposes + +// Reset AbortController to null during load of script. Needed to reset all Promise requests. +window.currentAbortController = null; + +// All relevant changeset information will be saved in this object +let changesets = {}; + +//Check if map is zoomed in enough (only executed on initial page load) +const isMapZoomedInEnough = updateMap(); +if (isMapZoomedInEnough) run(); + +//Start download of changeset data and initiate rendering of changesets list and GeoJSON data on map function run() { - if (xhr) xhr.abort(); - var bounds = map.getBounds(); - var bbox = bounds.getSouthWest().lat + ',' + + d3.select('#map').classed('faded', true);//Map displayed greyish (Cannot go into "toggleWaitingScreen()" because we want to keep the map greyed out in case of unsuccessful overpass query) + document.querySelector(".filter-container").classList.add("hide");//Hide filter changesets toolbar until load of changesets has been completed successfully + document.querySelector("#results").innerHTML = "";//Empty old results list (same happens in renderChangeSetsList() later on when filtering) + + toggleWaitingScreen();//Show loading animation and make download button unavailable + + // 1. Abort previous request if it exists + if (window.currentAbortController) { + window.currentAbortController.abort(); + console.log("Previous request aborted."); + } + + // 2. Create a NEW controller for THIS run + window.currentAbortController = new AbortController(); + const signal = window.currentAbortController.signal; // Get the signal for this run + + // Clear old GeoJSON data from map and reset the main object + if (changesets.allLeafletLayers) { + map.removeLayer(changesets.allLeafletLayers); + } + changesets = {}; // Reset the main object for the new run + + // Get current extension of displayed leaflet map + const bounds = map.getBounds(); + const bbox = bounds.getSouthWest().lat + ',' + bounds.getSouthWest().wrap().lng + ',' + bounds.getNorthEast().lat + ',' + bounds.getNorthEast().wrap().lng; - var last_week = (new Date(new Date() - 1000 * 60 * 60 * 24 * days_to_show)).toISOString(); - //var overpass_query = '[out:json];way(' + bbox + ')(newer:"' + last_week + '");out meta;node(w);out skel;node(' + bbox + ')(newer:"' + last_week + '");out meta;'; - var overpass_query = '[adiff:"' + last_week + '"][bbox:' + bbox + '][out:xml][timeout:22];way->.ways;(.ways>;node;);out meta;.ways out geom meta;'; - xhr = d3.xml(overpass_server + 'interpreter?data=' + overpass_query - ).on("error", function (error) { message("alarm", error); }) - .on('load', function (data) { - var newData = document.implementation.createDocument(null, 'osm'); - var oldData = document.implementation.createDocument(null, 'osm'); - var elements = data.querySelectorAll('action'); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; + // const overpass_query = '[adiff:"' + calculateAnalysisStartTime() + '"][bbox:' + bbox + '][out:xml][timeout:22];way->.ways;(.ways>;node;);out meta;.ways out geom meta;';//This query sometimes only returned nodes. Thus replaced with query below + const overpass_query = '[adiff:"' + calculateAnalysisStartTime().toISOString() + '"][bbox:' + bbox + '][out:xml];nw;out geom meta;'; + // console.log(overpass_server + 'interpreter?data=' + overpass_query); + + //Either do an API call to Overpass or use a locally saved xml file for debugging purposes + let xmlDataLocation; + if (debugMode) xmlDataLocation = "./examples/exampleOverpassAPI.xml"; //Load example xml for debugging purposes. Works offline + else xmlDataLocation = overpassServer + 'interpreter?data=' + overpass_query; //API call to overpass + + let allOverpassXMLDataElements; + + // --- First API call: Fetch GeoJSON data with OSM tags from Overpass --- + fetch(xmlDataLocation, { signal: signal })//start fetch, return Promise. Pass signal for abort controlling. + .then(response => { + if (!response.ok) { // Check if the HTTP request was successful + throw new Error(`HTTP error fetching Overpass data! Status: ${response.status} ${response.statusText || ''}`); + } + return response.text(); // Get the response body as text (returns a Promise) + }) + .then(text => new window.DOMParser().parseFromString(text, "text/xml")) // Parse the text as XML + .then(function (data) { // SUCCESS callback - 'data' is now the parsed XML Document + // Check if aborted before processing (already done after fetch, but good to keep) + if (signal.aborted) { + throw new DOMException('Aborted', 'AbortError'); + } + // console.log('Parsed Overpass XML:', data); // Optional: check the parsed data + let newData = document.implementation.createDocument(null, 'osm'); + let oldData = document.implementation.createDocument(null, 'osm'); + allOverpassXMLDataElements = data.querySelectorAll('action'); + // console.log(allOverpassXMLDataElements); + //Separate changed (new) and pre-change (old) OSM map data into respective xml objects + for (let i = 0; i < allOverpassXMLDataElements.length; i++) { + const element = allOverpassXMLDataElements[i]; + // console.log(element); switch (element.getAttribute('type')) { case 'create': - newData.documentElement.appendChild(element.querySelector("*")); + newData.documentElement.appendChild(element.querySelector("*").cloneNode(true));//cloneNode needed to keep "data" unchanged break; case 'modify': case 'delete': - var newElement = element.querySelector('new > *'); - var oldElement = element.querySelector('old > *'); + var newElement = element.querySelector('new > *').cloneNode(true);//cloneNode needed to keep "data" unchanged + var oldElement = element.querySelector('old > *').cloneNode(true); // fake changeset id on new data var newestTs = +new Date(newElement.getAttribute("timestamp")); if (newElement.tagName == 'way') { // inherit meta data from newest child node - var nds = newElement.getElementsByTagName('nd'); - for (var j = 0; j < nds.length; j++) { - var nodeId = nds[j].getAttribute('ref'); + var nodes = newElement.getElementsByTagName('nd'); + for (var j = 0; j < nodes.length; j++) { + var nodeId = nodes[j].getAttribute('ref'); var node = newData.querySelector('node[id="' + nodeId + '"]'); if (node === null) continue; var nodeTs = +new Date(node.getAttribute("timestamp")); @@ -102,257 +386,1553 @@ function run() { } } } - // fake changeset id on old data + // fake changeset meta data on old data oldElement.setAttribute('changeset', newElement.getAttribute('changeset')); oldElement.setAttribute('user', newElement.getAttribute('user')); oldElement.setAttribute('uid', newElement.getAttribute('uid')); oldElement.setAttribute('timestamp', newElement.getAttribute('timestamp')); + + // add node to 'newData' and 'oldData' if (element.getAttribute('type') == 'modify') newData.documentElement.appendChild(newElement); oldData.documentElement.appendChild(oldElement); } } - var newGeojson = osmtogeojson.toGeojson(newData); - var oldGeojson = osmtogeojson.toGeojson(oldData); + const newGeojson = osmtogeojson.toGeojson(newData); + const oldGeojson = osmtogeojson.toGeojson(oldData); + + // Add a property to old features to identify them later for styling purposes oldGeojson.features.forEach(function (feature) { feature.properties.__is_old__ = true; }); - d3.select('#map').classed('faded', false); - layer && map.removeLayer(layer); + //Combine old and new GeoJSON into one + let allGeojsonFeatures = [].concat(oldGeojson.features).concat(newGeojson.features); + // console.log(allGeojsonFeatures); + + // In rare edge cases the reference to an old node in a way might be missing in the overpass data which leads to osmtogeojson returning Null coordinates. + // The creation of the Leaflet layer will fail when Null coordinates are present ('Error: Invalid LatLng object: (NaN, NaN)'). Thus we filter them out here. + // Filter out features that contain null coordinates + allGeojsonFeatures = allGeojsonFeatures.filter(feature => { + // Check for null values within the coordinates array, handling nested arrays for Polygons/MultiLineStrings + const nullCoordsFound = JSON.stringify(feature.geometry.coordinates).includes('null') + if (nullCoordsFound) console.log("Feature with null coordinates found. This feature will not be displayed in the results:", feature); + // Filter the feature if null coordinates have been found, i.e. 'nullcoordsFound' is true + // (The .filter() method filters an item out if the expression evaluates to false. Thus we return the inverse of 'nullCoordsFound') + return !nullCoordsFound; + }); + + //Sort allGeojsonFeatures before writing it into the changesets object + allGeojsonFeatures.sort(sortGeoJsonFeatures); // Sort features before creating the Leaflet GeoJSON layer + + //Options to create the GeoJSON layers on the map + const sharedGeoJsonOptions = { + onEachFeature: onEachFeature, + style: setStyle, + pointToLayer: function (feature, latlng) { + return L.circleMarker(latlng, { radius: 8 }); + } + }; + + //Add all GeoJSON features to the map and store them in the changesets object + changesets.allLeafletLayers = L.geoJSON(allGeojsonFeatures, sharedGeoJsonOptions).addTo(map); + + //Define what functionality is given to each polygon/way/marker + function onEachFeature(feature, layer) { + // console.log(layer); + + // Traverse all the layers that have been added to the map and create an entry for each changeset in 'changesets' + // Create a logical featureGroup for each changeset (not added to the map directly). + // This featureGroup can later be used to color all layers at once. + + // Guard against features without changeset meta, which can happen if osmtogeojson couldn't associate them + if (!layer.feature.properties.meta || !layer.feature.properties.meta.changeset) { + // console.warn("Feature without changeset meta:", l.feature); + return; + } + const changesetNumber = layer.feature.properties.meta.changeset; + + if (changesets[changesetNumber] === undefined) { + //Note: The color for each changeset is calculated twice - here and in setStyle(). Not very clean. See if this redundancy can somehow be removed. + const color = defineColor(new Date(layer.feature.properties.meta.timestamp)); + // console.log(color); + + changesets[changesetNumber] = { + color, + comment: '', + deltaInIdWarningsAndResolves: 0, // Initialize + deltaInNodesWays: 0, + deltaInTags: 0, + discussionCount: 0, // Initialize + id: changesetNumber, + imageryUsed: [], + leafletFeatureGroup: L.featureGroup(), // Logical group, not added to map + osmEditor: '', + possibleVandalism: false, + time: new Date(layer.feature.properties.meta.timestamp), + user: layer.feature.properties.meta.user, + userId: layer.feature.properties.meta.uid + }; + } + //Add the layer to their respective changeset feature groups in changesets + changesets[changesetNumber].leafletFeatureGroup.addLayer(layer); + + // Assign a unique layer ID to each Leaflet layer using the OSM feature ID, appended with 'o' for old features and 'n' for new ones. + const OsmIdOfHoveredElement = feature.properties.id;//OSM element ID + const isOld = feature.properties.__is_old__; + layer._leaflet_id = OsmIdOfHoveredElement + (isOld ? 'o' : 'n'); - var datescale = d3.time.scale() - .domain([new Date(last_week), new Date()]) - .range([0, 1]); - var colint = d3.interpolateRgb('#777', '#f00'); + //Define what happens on mouseover: Increase line weight when feature gets focus. Also highlight twin element if feature has been moved + layer.on('mouseover', function (e) { + // console.log(layer); + layer.setStyle({ weight: 6 }); //increase line weight of hovered layer + //Now we check if the hovered element got modified (i.e. not deleted nor created). + //Write both XML nodes of old and new element into variable "xmlElements" + const xmlElements = data.querySelectorAll('[id="' + OsmIdOfHoveredElement + '"]'); + // console.log(xmlElements); + // Guard against missing elements or parent structure + if (!xmlElements || xmlElements.length === 0 || !xmlElements[0].parentNode || !xmlElements[0].parentNode.parentNode) return; + const action = xmlElements[0].parentNode.parentNode.getAttribute('type'); + // console.log(action); + //Check if action is 'modify'. If yes: Highlight non-hovered twin element of nodes, lines and polygons as well. Skip unmoved elements. + if (action === 'modify') { + // console.log('Modify! Highlight non-hovered twin element as well (i.e. old or new version). If the element is a node: Only highlight twin element if node has been moved'); + //First check if element is a node. If it is: Check if coordinates have changed. If yes: Highlight twin node. If no: stop function execution (no need to traverse layer object and waste resources) + if (xmlElements[0].tagName === "node") { + // console.log('This is a NODE! Now check if the node has been moved'); + const latEl0 = xmlElements[0].getAttribute('lat'); + const latEl1 = xmlElements[1].getAttribute('lat'); + const lonEl0 = xmlElements[0].getAttribute('lon'); + const lonEl1 = xmlElements[1].getAttribute('lon'); + if (latEl0 !== latEl1 || lonEl0 !== lonEl1) { + // console.log('Coordinates of old and new node are NOT identical --> node has been moved! Highlight twin element.'); + // Get the Leaflet layer of the twin element. It shares the same ID number but has the opposite suffix from the hovered element. + const leafletLayerOfTwin = changesets.allLeafletLayers.getLayer(OsmIdOfHoveredElement + (isOld ? 'n' : 'o')); + + if (!leafletLayerOfTwin) { + // console.log('Twin element is not available. This usually means that 1. The node is part of way 2. The old version of the node had no tags. "osmtogeojson" will NOT convert those nodes into GeoJSON'); + return; + } + highlightTwinElementLocation(layer, leafletLayerOfTwin); + } + } + //All other elements (i.e. lines and polygons): Highlight twin element if the geometry has been changed. + //A way to check if a line or polygon has been changed is to compare the length of new and old element. + //If there is a difference the element has been changed. Solution below is from https://github.com/tyrasd/geojson-length. + else if (xmlElements[0].tagName === "way") { + // Get the Leaflet layer of the twin element. It shares the same ID number but has the opposite suffix from the hovered element. + const leafletLayerOfTwin = changesets.allLeafletLayers.getLayer(OsmIdOfHoveredElement + (isOld ? 'n' : 'o')); + // console.log('Name of leafletLayerOfTwin: ' + OsmIdOfHoveredElement + (isOld ? 'n' : 'o')); + if (!leafletLayerOfTwin) return; // Return if there is no twin. Should never happen. + + // Compare length and position of hovered and twin element. Check if comparison for those features has happened before. + // If yes: Use stored value. If no: calculate and store result in layer. + const geometryIsDifferent = + layer.feature.geometryIsDifferent !== undefined + ? layer.feature.geometryIsDifferent + : ( + // Execute length comparison and write result into 'feature.geometryIsDifferent' property of new and old layer. + // For subsequent calls the length calculation does not have to be performed again + layer.feature.geometryIsDifferent = + leafletLayerOfTwin.feature.geometryIsDifferent = + checkIfLengthOfLineOrPolygonHasChanged(layer, leafletLayerOfTwin) + ); + // console.log(layer.feature, leafletLayerOfTwin.feature); + // If the geometry of old and new layers are different highlight the twin layer. + if (geometryIsDifferent) highlightTwinElementLocation(layer, leafletLayerOfTwin); + } + } + }); + + //Change line weight back when feature loses focus + layer.on('mouseout', function (e) { + layer.setStyle({ weight: 3 }); + // The highlightTwinElementLocation function handles resetting the twin's style more comprehensively + }); + } + + //Define style of GeoJSON elements function setStyle(f) { + // console.log('setStyle called'); + const cs = changesets[f.properties.meta.changeset]; return { - color: colint(datescale(new Date(f.properties.meta.timestamp))), + // If specific color has already been defined in the 'changesets' object then use it. Else calculate in defineColor() + // setStyle() is also called when using .resetStyle(). By saving the color in 'changesets' we can make sure that + // the whole defineColor() does not have to be re-executed on .resetStyle(). + color: cs?.color !== undefined //"If cs exists, and it has a color property, use it. Otherwise, fall back." + ? cs.color + : defineColor(new Date(f.properties.meta.timestamp)), opacity: f.properties.__is_old__ === true ? 0.2 : 1, weight: 3 } }; - layer = new L.GeoJSON({ - type: 'FeatureCollection', - features: [].concat(oldGeojson.features).concat(newGeojson.features) - }, { - style: setStyle, - pointToLayer: function (feature, latlng) { - return L.circleMarker(latlng, { radius: 8 }); - } - }) - .addTo(map); - var bytime = []; - var changesets = {}; + //Change app display from "loading" to "ready" + d3.select('#map').classed('faded', false); + toggleWaitingScreen(); + + //Display filter changesets toolbar + document.querySelector(".filter-container").classList.remove("hide"); + //Remove content of "filter changeset input" and hide X (in case old text from a previous download is still there) + document.querySelector(".search-changesets-field").value = ""; + document.querySelector(".delete-filter-input").classList.add("hide"); + //Reset display of "filter-red-checkbox" (in case it has been checked on a previous download) + document.querySelector(".traffic-light-filter").classList.add("filter-red-color"); + document.querySelector("#filter-red-checkbox").checked = false; - layer.eachLayer(function (l) { - if (!l.feature.properties.meta.changeset) + //Compare length and position of hovered and twin element. + //return true: Geometry has been changed + //return false: Geometry is unchanged + function checkIfLengthOfLineOrPolygonHasChanged(leafletLayerOfHoveredElement, leafletLayerOfTwin) { + // console.log('checkIfLengthOfLineOrPolygonHasChanged called'); + /* + //The length check can be computationally demanding on complex LineStrings and polgyons. Thus simply highlight all + //twin elements - regardless of whether the geometry has been changed or not - if node count is greater than 50. + //Check if node count of line/polygon is greater than 50. if yes just highlight twin without measuring + const nNodes = xmlElements[0].getElementsByTagName('nd').length; + // console.log(nNodes); + if (nNodes > 50) { + // console.log('Node count is large (>50). So skip the length comparison and highlight twin geometry no matter if there is a length difference or not.'); + highlightTwinElementLocation(layer, leafletLayerOfTwin); return; - changesets[l.feature.properties.meta.changeset] = changesets[l.feature.properties.meta.changeset] || { - id: l.feature.properties.meta.changeset, - time: new Date(l.feature.properties.meta.timestamp), - user: l.feature.properties.meta.user, - comment: '', - features: [] }; - changesets[l.feature.properties.meta.changeset].features.push(l); - }); - for (var k in changesets) { - bytime.push(changesets[k]); + */ + + //Helper function to determine if the geometry is a LineString or Polygon and return the appropriate coordinate path. + const getCoordinates = layer => + layer.feature.geometry.type === 'LineString' + ? layer.feature.geometry.coordinates + : layer.feature.geometry.coordinates[0]; // For Polygons + + //Calculate length of both elements + const coordinatesOfHoveredElement = getCoordinates(leafletLayerOfHoveredElement); + // console.log(coordinatesOfHoveredElement); + const coordinatesOfTwinElement = getCoordinates(leafletLayerOfTwin); + // console.log(coordinatesOfTwinElement); + + //Do a first quick check of the first coordinate. If the first coordinate of hovered and twin element is more than 1m apart: + //Highlight twin element. Note: This quick check produces false positives for ways with reversed direction. + // console.log(`Distance between first nodes: ${distance(coordinatesOfHoveredElement[0][0], coordinatesOfHoveredElement[0][1], coordinatesOfTwinElement[0][0], coordinatesOfTwinElement[0][1])}`); + if (distance(coordinatesOfHoveredElement[0][0], coordinatesOfHoveredElement[0][1], coordinatesOfTwinElement[0][0], coordinatesOfTwinElement[0][1]) > 1) return true; + + //If they are identical do a more thorough length comparison. + //Get length of hovered element + const lengthOfHoveredElement = calculateLength(coordinatesOfHoveredElement); + // console.log('lengthOfHoveredElement: ' + lengthOfHoveredElement); + + //Get length of twin element + const lengthOfTwinElement = calculateLength(coordinatesOfTwinElement); + // console.log('lengthOfTwinElement: ' + lengthOfTwinElement); + + //Compare the two. First round to 0.1 meters. If length is different: Highlight twin element. + // console.log('lengthOfHoveredElement (rounded): ' + +lengthOfHoveredElement.toFixed(1)); + if (+lengthOfHoveredElement.toFixed(1) !== +lengthOfTwinElement.toFixed(1)) { + // console.log('The elements have different lengths, thus the geometry most probably got changed. Highlight the twin element.'); + return true; + } + + //Else it can be assumed that the geometry is unchanged. Do not highlight twin element. + else return false; + + //Calculate length of LineString or polygon. Needed to compare 2 LineStrings or polygons to see if they have different lengths + function calculateLength(lineString) { + // console.log(lineString); + if (lineString.length < 2) + return 0; + let result = 0; + for (let i = 1; i < lineString.length; i++) { + // console.log(result); + result += distance(lineString[i - 1][0], lineString[i - 1][1], + lineString[i][0], lineString[i][1]); + } + return result; + } + + /** + * Calculate the approximate distance between two coordinates (lat/lon) + * Further explanation see /doc/Equirectangular_Distance_Explanation.md + * © Chris Veness, MIT-licensed, + * http://www.movable-type.co.uk/scripts/latlong.html#equirectangular + */ + function distance(λ1, φ1, λ2, φ2) { + const R = 6371000; + const Δλ = (λ2 - λ1) * Math.PI / 180; + φ1 = φ1 * Math.PI / 180; + φ2 = φ2 * Math.PI / 180; + const x = Δλ * Math.cos((φ1 + φ2) / 2); + const y = (φ2 - φ1); + const d = Math.sqrt(x * x + y * y); + return R * d; + }; } - layer.on('click', function (e) { - click({ feature: e.layer }); - }); + //Highlight the non-hovered twin element (i.e. the new or old geometry) as well. + function highlightTwinElementLocation(leafletLayerOfHoveredElement, leafletLayerOfTwin) { + let count = 3; + let direction = 1; // 1 for counting up, -1 for counting down - bytime.sort(function (a, b) { - return (+b.time) - (+a.time); - }); + //if it is blue: change back to blue on mouseout, else change back to shade of red + const preHoverColor = leafletLayerOfTwin.options.color; + //console.log('%c Pre-hover color: ' + preHoverColor + '', 'background: ' + preHoverColor + '; color: #000000'); - var results = d3.select('#results'); - var allresults = results - .selectAll('div.result') - .data(bytime, function (d) { - return d.id; - }) - .attr('class', 'result') - .style('color', function (l) { - return colint(datescale(l.time)); - }); - allresults.exit().remove(); + const interval = setInterval(function () { + if (count === 10) { + direction = -1; // Change direction to count down + } else if (count === 3) { + direction = 1; // Change direction to count up + } - var rl = allresults.enter() - .append('div') - .attr('class', 'result') - .style('color', function (l) { - return colint(datescale(l.time)); - }); - allresults.order(); + count += direction; // Increment or decrement count based on direction - function click(d) { - results - .selectAll('div.result') - .classed('active', function (_) { - return _.id == (d.id || d.feature.feature.properties.meta.changeset); + // Set the weight property dynamically + leafletLayerOfTwin.setStyle({ + color: '#ff9900',//Highlight twin geometry in a bright orange + opacity: 1, + weight: count }); - layer.eachLayer(function (l) { - layer.resetStyle(l); - }) - var id = d.id ? d.id : d.feature.feature.properties.meta.changeset; - layer.eachLayer(function (l) { - if (l.feature.properties.meta.changeset == id) { - l.setStyle({ color: '#00c500' }); + + }, 100); // Change the interval duration (in milliseconds) as needed + + // Clear the interval when mouse is not hovering + leafletLayerOfHoveredElement.once('mouseout', function () { + //'Once' is needed to remove the mouseout event listener after execution. Else multiple mouseout + //event listeners are created on each mouseover event. This would lead to the following situation: + //This mouseout function would be called multiple times on subsequent mouseouts, + //e.g. after 'mouseovering' over the same element the 6th time this mouseout function is called 6 times instead of once. + + // console.log('clearInterval called!'); + clearInterval(interval);//stop oscillation of line weight + + //Reset style of oscillating geometry, i.e. line weight back to 3, opacity back to previous value and color back shade of red + changesets.allLeafletLayers.resetStyle(leafletLayerOfTwin); + + //If this geometry belongs to highlighted changeset --> change it back to the highlighting color (i.e. blue) + if (preHoverColor === '#008dff') { + // console.log('%c The color is blue! ', 'background: ' + preHoverColor + '; color: #000000', 'So change color back to blue'); + leafletLayerOfTwin.setStyle({ + color: '#008dff',//Change color of twin geometry back to blue + }); } }); + } - //Scroll selected element into view - document.querySelector('.active').scrollIntoView({ - behavior: 'smooth' + // Attach click listener to the main GeoJSON layer + changesets.allLeafletLayers.on('click', function (e) { + //Highlight clicked layer on map and in sidebar + click(e.layer.feature, null); // Pass feature as first arg, null for d to match signature + //Scroll selected element into view in sidebar + //(Only on large screens. On small screens the map would scroll out of view, which is annoying) + if (screen.width > 600) { + const activeEl = document.querySelector('.active'); + if (activeEl) activeEl.scrollIntoView({ behavior: 'smooth' }); + } + + const tableHtml = createTable(e.layer.feature.properties.id) + let mapContainer = document.querySelector(".map-container"); + + //Popup with tag comparison table + L.popup({ + maxWidth: mapContainer.clientWidth - 45, + maxHeight: mapContainer.clientHeight - 40, + className: "stylePopup" }) + .setLatLng(e.latlng) + .setContent(tableHtml) + .openOn(map); + }); + + //Create tag comparison table + function createTable(id) { + const node = data.querySelectorAll('[id="' + id + '"]'); + //First check what type of action has been performed on the element (i.e. create, modify, delete) + let action = node[0].parentNode.parentNode.getAttribute('type');//Check if action is "modify", "delete" or "null" + if (!action) action = "create";//The xml data structure is different for "create" nodes, thus action will be "null" in the line above + // console.log(action); + + // Helper functions to create OSM wiki links from OSM tags and URL links if URL is detected + // Creation of link from key (e.g. highway --> link to OSM wiki page of "Key:highway") + function linkKey(k) { + const url = `https://wiki.openstreetmap.org/wiki/Key:${encodeURIComponent(k)}`; + return `${k}`; + }; + + // Creation of link from value (e.g. motorway --> link to OSM wiki page of "Value:highway=motorway" or linkify URL) + function linkValue(k, v) { + if (!v || v === "") return ""; + + // 1. Direct URL keys: treat the whole value as a link + if (/^(website|contact:website|url|image|mapillary|flickr)$/i.test(k)) { + let url = v; + // Prepend http:// if missing (for www. entries in website tags) + if (!/^https?:\/\//i.test(url) && !/^ftp:\/\//i.test(url)) { + url = 'http://' + url; + } + return `${v}`; + } + + // 2. Free text fields that might CONTAIN URLs: use linkify() + if (/^(note|description|comment|source|fixme|todo|opening_hours|color|contact)/i.test(k)) { + return linkify(escapeHtml(v)); + } + + // 3. Skip Wiki linking for names, refs, phones, emails, addr:*, numeric values, etc. + const isNoWikiLink = /^(name|note|description|comment|source|ref|phone|mobile|website|email|addr:|tiger:|gnis:|created_by|fixme|todo|opening_hours|color|operator|contact:|start_date|brand|disused:|demolished:|branch|check_date)/i.test(k) || !isNaN(v.replace(",", ".")); + + if (isNoWikiLink) { + return v; + } + + // 4. Default: Wiki Link + const url = `https://wiki.openstreetmap.org/wiki/Tag:${encodeURIComponent(k)}=${encodeURIComponent(v)}`; + return `${v}`; + }; + + //Create header with type of action (i.e. create, modify or delete), type of element (i.e. node or way), OSM id and link to 'OSM Deep History'. + let tableHtml = ` + ${action} + ${node[0].nodeName} + ${node[0].getAttribute("id")} + + + + + `; + + //Object with all key-value pairs for new and old feature and relevant meta tags for table + const keyvalues = { old: { meta: {}, tags: {} }, new: { meta: {}, tags: {} } }; + + //1 CREATE + if (action === "create") { + //Copy meta tags + const keysNew = node[0].querySelectorAll("tag"); + + //Create table + tableHtml += ` + + + + + + + + + + + + + + + + + + + + `; + + for (let i = 0; i < keysNew.length; i++) { + const k = keysNew[i].getAttribute('k'); + const v = keysNew[i].getAttribute('v'); + tableHtml += ` + + + + + `; + } + } + + //2 MODIFY/DELETE + else { + //Create Set with all unique key-values from old and new + const uniqueKeysSet = new Set(); + //Start with old + //Copy meta tags + keyvalues.old.meta["version"] = node[0].getAttribute("version"); + keyvalues.old.meta["timestamp"] = moment(node[0].getAttribute("timestamp")).fromNow(); + keyvalues.old.meta["user"] = node[0].getAttribute("user"); + const keysOld = node[0].querySelectorAll("tag"); + for (let i = 0; i < keysOld.length; i++) { + keyvalues.old.tags[keysOld[i].getAttribute('k')] = keysOld[i].getAttribute('v'); + uniqueKeysSet.add(keysOld[i].getAttribute('k')); + } + //Continue with new + //Copy meta tags + keyvalues.new.meta["version"] = node[1].getAttribute("version"); + keyvalues.new.meta["timestamp"] = moment(node[1].getAttribute("timestamp")).fromNow(); + keyvalues.new.meta["user"] = node[1].getAttribute("user"); + const keysNew = node[1].querySelectorAll("tag"); + for (let i = 0; i < keysNew.length; i++) { + keyvalues.new.tags[keysNew[i].getAttribute('k')] = keysNew[i].getAttribute('v'); + uniqueKeysSet.add(keysNew[i].getAttribute('k')); + } + // console.log(keyvalues); + //Create array in which all keys are ordered alphabetically + const uniqueKeysArr = Array.from(uniqueKeysSet).sort(); + // console.log(uniqueKeysArr); + + //Create table + tableHtml += ` + + + + + + + + + + + + + + + + + + + + + + + + `; + + //Traverse uniqueKeysArray and check in object which value this key has in new and old + for (let i = 0; i < uniqueKeysArr.length; i++) { + const key = uniqueKeysArr[i]; + let oldTag = keyvalues.old.tags[key]; + let newTag = keyvalues.new.tags[key]; + let cssClass; + //Case 1: Tag deleted in new --> Background color red, change from "undefined" to "" + if (!newTag) { + cssClass = "delete"; + newTag = ""; + } + //Case 2: Tag created in new --> Background color green, change from "undefined" to "" + else if (!oldTag) { + cssClass = "create"; + oldTag = ""; + } + //Case 3: Tag different in new --> Background color yellow + else if (oldTag !== newTag) cssClass = "modify"; + + //Case 4: Tags similar --> Default (i.e. no) background color + else cssClass = "unchanged"; + + tableHtml += ` + + + + + + `; + } + } + + tableHtml += ` + +
TagNew
version${node[0].getAttribute("version")}
timestamp${moment(node[0].getAttribute("timestamp")).fromNow()}
user + ${node[0].getAttribute("user")} +
${linkKey(k)}${linkValue(k, v)}
TagOldNew
version${keyvalues.old.meta["version"]}${keyvalues.new.meta["version"]}
timestamp${keyvalues.old.meta["timestamp"]}${keyvalues.new.meta["timestamp"]}
user
+ ${keyvalues.old.meta["user"]} + + ${keyvalues.new.meta["user"]} +
${linkKey(key)}${linkValue(key, oldTag)}${linkValue(key, newTag)}
+ `; + + //Create link to edit geometry in iD editor (only if element has not been deleted - deleted elements cannot be edited) + if (action !== "delete") { + tableHtml += `Edit in iD`; + } + + return tableHtml; } - rl.on('click', click); + // --- Start of the second async API call --- + // Download changeset text and changeset comment count from OSM API. + // Once done render changesets list on the left side - rl.append('span') - .classed('load', true) - .attr('title', 'Click to highlight changeset on map') - .html('→ '); + //Write changeset id's in an array. This is used to create URL for API call. + const changesetIds = []; + for (const key in changesets) { + changesetIds.push(key); + } + // console.log(changesetIds); - rl.append('a').text(function (d) { - return d.user; - }) - .attr('target', '_blank') - .attr('href', function (d) { - return '//openstreetmap.org/user/' + d.user; - }); + const promises = []; // Array to hold promises for changeset details + const fetchChangesetBatch = (ids) => { + const url = debugMode + ? "./examples/exampleOSMAPI.xml" // Handle debug mode (only makes one request) + : 'https://api.openstreetmap.org/api/0.6/changesets?changesets=' + ids.join(','); - rl.append('span').text(' '); + // Use fetch, check response, parse XML + return fetch(url, { signal: signal }) // Return the promise chain + .then(response => { + if (!response.ok) { // Check if the HTTP request was successful + throw new Error(`HTTP error fetching changeset details! Status: ${response.status} ${response.statusText || ''} URL: ${url}`); + } + return response.text(); // Get the response body as text + }) + .then(text => new window.DOMParser().parseFromString(text, "text/xml")); // Parse the text as XML + }; + //Load a local example file if debug mode is on. If not call the OSM API. + if (debugMode) { + promises.push(fetchChangesetBatch([])); // Pass empty array if URL is fixed + } else { + //Fetch data from OSM database. If there are more than 100 changesets to query make multiple API requests with a hundred CS each. + while (changesetIds.length > 0) { + promises.push(fetchChangesetBatch(changesetIds.splice(0, 100))); + } + } + // Use Promise.all to wait for all changeset detail requests + return Promise.all(promises); // IMPORTANT: Return this promise to chain correctly + // --- End of the second async operation --- - rl.append('span') - .attr('title', function (d) { - return moment(d.time).format('MMM Do YYYY, h:mm:ss a'); - }) - .attr('class', 'date').text(function (d) { - return moment(d.time).fromNow(); - }); - //Zoom link removed in order to have more space for map on small screens. - /* rl.append('span').text(' '); - - rl.append('a').attr('class', 'reveal').text('«zoom»') - .attr('target', '_blank') - .attr('href', '#') - .on('click', function(d) { - d3.event.preventDefault(); - map.fitBounds(d.features.reduce(function(a, b) { - return a.extend(b.getBounds()); - }, new L.LatLngBounds())); - }); - - */ - rl.append('span').text(' '); - rl.append('a').attr('class', 'reveal').text('«achavi»') - .attr('target', '_blank') - .attr('title', 'Get details about this changeset on Achavi') - .attr('href', function(d) { - return 'https://overpass-api.de/achavi/?changeset=' + d.id; - }); - - rl.append('div').attr('class', 'changeset'); - var queue = d3.queue(); - var changesetIds = rl.data() - .map(function (d) { return d.id }) - .filter(function (changesetId) { return changesetId !== ''; }); - while (changesetIds.length > 0) { - queue.defer(d3.xml, 'https://www.openstreetmap.org/api/0.6/changesets?changesets=' + changesetIds.splice(0, 100).join(',')); + }) // This .then() receives the result of Promise.all(promises) + .then(function (xmls) { // SUCCESS callback for the *second* async operation (Promise.all) + // Check if aborted before processing results + if (signal.aborted) { + throw new DOMException('Aborted', 'AbortError'); } - queue.awaitAll(function (error, xmls) { - if (error) return console.error(error); - - var changesets = {}; - xmls.forEach(function (xml) { - var css = xml.getElementsByTagName('changeset'); - for (var i = 0; i < css.length; i++) { - var cid = css[i].getAttribute('id'); - changesets[cid] = { - discussionCount: +css[i].getAttribute("comments_count") - }; - var tag = css[i].querySelector('tag[k="comment"]'); - if (tag) - changesets[cid].comment = tag.getAttribute('v'); + // console.log(xmls); + // 'xmls' is the array of parsed XML documents from the changeset details + // the forEach below is needed if there have been more than 100 changesets fetched and the length of the xmls array is > 1. + xmls.forEach(function (xmlDoc) { + // console.log(xmlDoc); + const css = xmlDoc.getElementsByTagName('changeset'); + // console.log(css); + for (let i = 0; i < css.length; i++) { + const cid = css[i].getAttribute('id'); + if (changesets[cid]) { // Check if changeset exists in our main object + //Write discussion count to changesets object after converting it to a number + changesets[cid].discussionCount = +css[i].getAttribute("comments_count"); + //Write changeset comment to changesets object + const commentTag = css[i].querySelector('tag[k="comment"]'); + if (commentTag) changesets[cid].comment = commentTag.getAttribute('v'); + + //Get sum of warnings and resolved iD warnings for each changeset and write the delta into changesets object + // console.log(css[i]); + const tagsWithResolvedIdWarnings = css[i].querySelectorAll('tag[k^="resolved"]'); + const nIdResolvedWarnings = getSumOfiDWarningsAndResolvedWarnings(tagsWithResolvedIdWarnings); + const tagsWithIdWarnings = css[i].querySelectorAll('tag[k^="warning"]'); + const nIdWarnings = getSumOfiDWarningsAndResolvedWarnings(tagsWithIdWarnings); + + function getSumOfiDWarningsAndResolvedWarnings(tagsWithWarnings) { + let counter = 0; + tagsWithWarnings.forEach(tag => { // Use the passed parameter + const value = +tag.getAttribute('v'); + // console.log(value); + counter += value; + }); + return counter; + } + changesets[cid].deltaInIdWarningsAndResolves = nIdResolvedWarnings - nIdWarnings; + + //Get name of used OSM editor + const osmEditorTag = css[i].querySelector('tag[k="created_by"]'); + if (osmEditorTag) + changesets[cid].osmEditor = osmEditorTag.getAttribute('v'); + + //Write the names of the used satellite imagery providers into changeset object + const imageryUsedTag = css[i].querySelector('tag[k="imagery_used"]'); + const sourceTag = css[i].querySelector('tag[k="source"]'); + + const imageryUsedValue = imageryUsedTag?.getAttribute('v') || ""; + const sourceValue = sourceTag?.getAttribute('v') || ""; + + // Combine both strings with a semicolon (only if both are non-empty) + const combined = [imageryUsedValue, sourceValue] + .filter(s => s.trim() !== "") // remove empty strings + .join(';'); + + const imageryUsedArray = combined + .split(';') + .map(s => s.trim()) + .filter(s => s); // final cleanup + + // console.log(cid + ': ' + imageryUsedArray + ' length: ' + imageryUsedArray.length); + // console.log(imageryUsedArray); + changesets[cid].imageryUsed = imageryUsedArray; } + } + }); + + /*Vandalism Checker + Simple sanity checker for all the downloaded changesets. 3 things are checked: + 1. It summarizes the number of all elements which have been added or deleted + 2. It summarizes the number of all tags which have been added or deleted + 3. It summarizes the number of all iD warnings and resolved iD warnings + for each changeset. If the sum is below a certain treshold (currently -3) then a traffic light changes to red to alert the user + about this changeset.*/ + function vandalismChecker() { + // console.log(elements); + for (let i = 0; i < allOverpassXMLDataElements.length; i++) { + //Get type, i.e. "create", "modify" or "delete" + const type = allOverpassXMLDataElements[i].getAttribute("type"); + // console.log(type); + + //Get changeset number + let changesetNumber; + if (type === "create") changesetNumber = allOverpassXMLDataElements[i].lastElementChild.getAttribute("changeset"); + else changesetNumber = allOverpassXMLDataElements[i].lastElementChild.firstElementChild.getAttribute("changeset"); + // console.log(changesetNumber); + + if (changesets[changesetNumber]) { // Check if changeset exists in our main object + //Check which action is performed + //"Create" + //deltaInNodesWays++ + //deltaInTags += nTagsAdded + if (type === "create") { + //Check the amount of tags that have been added + const nTagsAdded = allOverpassXMLDataElements[i].lastElementChild.querySelectorAll("tag").length; + // console.log(nTagsAdded); + changesets[changesetNumber].deltaInTags += nTagsAdded; + + //If a node with 0 tags has been created: Do not add it to deltaInNodesWays + //(normally it is just a newly created node of an already existing way) + + //Get element type (i.e. "node" or "way") + const elementType = allOverpassXMLDataElements[i].firstElementChild.nodeName; + // console.log(elementType); + + if (elementType === "node" && nTagsAdded == 0) continue; + else changesets[changesetNumber].deltaInNodesWays++; + } + + // "Modify" + //deltaInNodesWays = unchanged + //deltaInTags += nTagsNew - nTagsOld + if (type === "modify") { + const nTagsNew = allOverpassXMLDataElements[i].lastElementChild.firstElementChild.querySelectorAll("tag").length; + const nTagsOld = allOverpassXMLDataElements[i].firstElementChild.firstElementChild.querySelectorAll("tag").length; + // console.log(nTagsNew); + // console.log(nTagsOld); + changesets[changesetNumber].deltaInTags += (nTagsNew - nTagsOld); + } + + // "Delete" + //deltaInNodesWays-- + //deltaInTags -= nTags + if (type === "delete") { + // The 'old' element in a delete action is under element.firstElementChild.firstElementChild + const oldElementNode = allOverpassXMLDataElements[i].firstElementChild.firstElementChild; + const nTagsDeleted = oldElementNode.querySelectorAll("tag").length;//Number of deleted tags + // console.log(nTagsDeleted); + changesets[changesetNumber].deltaInTags -= nTagsDeleted; + + //Get element type (i.e. "node" or "way") + const elementType = oldElementNode.nodeName; + // console.log(elementType); + + //If a node with 0 tags has been deleted: Do not subtract it from deltaInNodesWays + // (normally it is just a newly deleted node of an already existing way) + if (elementType === "node" && nTagsDeleted == 0) continue; + else changesets[changesetNumber].deltaInNodesWays--; + } + } + } + + //Write boolean value 'possibleVandalims' into 'changesets' + for (const changesetId in changesets) { + const cs = changesets[changesetId]; + // console.log(changesetId); + //Check how many warnings and resolves iD editor produced. + // console.log("Changeset number: " + changesetId + ", deltaInIdWarningsAndResolves: " + cs.deltaInIdWarningsAndResolves); + + if ((cs.deltaInNodesWays < vandalismThreshold) || (cs.deltaInTags < vandalismThreshold) || (cs.deltaInIdWarningsAndResolves < vandalismThreshold)) { + cs.possibleVandalism = true; + } + } + } + + // console.log(changesets); + vandalismChecker();//Analyse each changeset and create boolean "possibleVandalism" within "changesets" object + renderChangesetsList(changesets);//Render changesets list on the left side + fetchAndDisplayUserDetails(changesets);//After the whole data is loaded and rendered download some 'nice-to-have' user data (total edits & sign-up date) and add it to the changeset information + fetchAndDisplayChangesetDiscussions(changesets);//After the whole data is loaded and rendered download changeset discussions and add it to the changeset information + }) + .catch(function (error) { // ERROR handler for ANY error in the Promise chain above + if (error.name === 'AbortError') { + console.log("Fetch aborted."); + // Don't show a user error for deliberate aborts, just ensure UI is reset + // (toggleWaitingScreen might already be handled if the abort happens early) + // May need explicit UI reset here depending on state. + d3.select('#map').classed('faded', false); // Un-fade map + // Ensure loading animation is off if it was turned on + const loadingAnimation = document.querySelector("#loading-animation"); + if (!loadingAnimation.classList.contains("hide")) { + toggleWaitingScreen(); + } + + } else { + // Handle actual network or processing errors + toggleWaitingScreen(); // Ensure loading screen is off + console.error("Error fetching or processing data:", error); // Log the actual error + message("alarm", "Server error: " + (error.message || "Could not load data.")); + } + }) + .finally(() => { + // Cleanup: Clear the controller reference ONLY if it's the one from this run + if (window.currentAbortController && signal === window.currentAbortController.signal) { + window.currentAbortController = null; + // console.log("AbortController reference cleared."); + } + }); +} + +// --- Third API call: Fetch extended user details (total edits & sign-up date) from the OSM API and update the UI --- +function fetchAndDisplayUserDetails(changesetsToDisplay) { + // 1. Collect all unique user IDs from the visible changesets + const userIds = new Set(); + for (const id in changesetsToDisplay) { + if (changesetsToDisplay[id] && changesetsToDisplay[id].userId) { + userIds.add(changesetsToDisplay[id].userId); + } + } + + if (userIds.size === 0) return; // No users to fetch + + // 2. Form the URL for the OSM User API + const userIdsArray = Array.from(userIds); + const url = debugMode + ? "./examples/exampleUsers.json" // Use a local file for debugging + : `https://api.openstreetmap.org/api/0.6/users.json?users=${userIdsArray.join(',')}`; + + // 3. Fetch the data. This is a "nice-to-have" call, so we handle errors gracefully. + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error fetching user data! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + // 4. Write the response into our 'changesets' object + // console.log(data); + changesets.userData = {}; + if (data && data.users) { + data.users.forEach(user => { + // console.log(user.user.id); + changesets.userData[user.user.id] = { accountCreated: user.user.account_created, changesetCount: user.user.changesets.count }; }); - rl.select('span.load').each(function (d) { - if (changesets[d.id].discussionCount > 0) - d3.select(this).html('⇒ '); - }); - rl.select('div.changeset').each(function (d) { - d3.select(this).html( - '' + - (changesets[d.id].comment || '') + - '' - ); - }); + } + + // 5. Update the DOM with the fetched data + // Select all result list items that have a 'osm-user-id' attribute + d3.selectAll('.result[user-id]').each(function () { + const li = d3.select(this); + const userId = li.attr('user-id'); + // console.log(userId); + const userData = changesets.userData[userId]; + // console.log(userData); + + if (userData) { + // Unhide the "User Experience" section + li.selectAll('.user-experience-heading, .user-experience-row').classed('hide', false); + + // Update the table cells with the new data + li.select('.user-changesets-count').text(userData.changesetCount.toLocaleString()); + li.select('.user-time-since-signup').text(moment(userData.accountCreated).fromNow()); + + // Update the title of '.user-name' link to include total changesets and signup date + const userNameLink = li.select('.user-name'); + const originalTitle = userNameLink.attr('title') || ''; + const newTitle = `\n\nTotal changesets: ${userData.changesetCount.toLocaleString()}\nSigned up: ${moment(userData.accountCreated).fromNow()}`; + userNameLink.attr('title', originalTitle ? originalTitle + newTitle : newTitle); + } }); + }) + .catch(error => { + // If the API call fails, we just log it and do nothing. The UI will simply not show the extra info. + console.warn("Could not fetch extended user details:", error.message); + }); +} + +// --- Fourth API call: Fetch changeset comments from the OSM API and update the UI. --- +// As of 2025 the OSM API only allows to download changeset comments for one changeset at a time +async function fetchAndDisplayChangesetDiscussions(changesetsToDisplay) { + // 1. Collect all changeset numbers, where discussionCount > 0 + const changesetsWithDiscussions = []; + for (const id in changesetsToDisplay) { + if (changesetsToDisplay[id].discussionCount > 0) { + changesetsWithDiscussions.push(id); + } + } + // console.log(changesetsWithDiscussions); + + //Return early if there are no changesets with discussions + if (changesetsWithDiscussions.length === 0) return; + + // Get the current abort signal + const signal = window.currentAbortController ? window.currentAbortController.signal : null; + + // 2. Iterate over each changeset ID and fetch its discussion + for (const changesetID of changesetsWithDiscussions) { + + // Check if aborted before starting the next request + if (signal && signal.aborted) { + console.log("Fetch discussions aborted."); + break; + } + + const url = debugMode + ? "./examples/exampleDiscussions.json" + : `https://api.openstreetmap.org/api/0.6/changeset/${changesetID}.json?include_discussion=true`; + + try { + // 3. Await the fetch. This ensures requests are done sequentially (1 by 1), + // preventing API rate limiting issues or browser network congestion. + const response = await fetch(url, { signal: signal }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + // Update the main object (the discussion data in the changesets object is used, when the changesets list is filtered and thus rerendered) + const discussion = data.changeset.comments; + + // 4. Update the DOM + + // Select the discussion heading and unhide it + const discussionHeading = d3.select(`[changeset-id="${changesetID}"] .changeset-discussion-heading`); + discussionHeading.classed('hide', false); + + // Select the discussion container for this changeset + const discussionTR = d3.select(`[changeset-id="${changesetID}"] .changeset-discussion`); + + // Loop through each comment and append it to the discussion container + for (let j = 0; j < discussion.length; j++) { + const comment = discussion[j]; + const singleCommentDiv = discussionTR.append('article') + .classed('comment-container', true); + + singleCommentDiv.append('div') + .classed('comment-info', true) + .html(` + Comment from ${comment.user} ${moment(comment.date).fromNow()} + `); + + singleCommentDiv.append('div') + .classed('comment-text', true) + .html(linkify(escapeHtml(comment.text))); // Escape HTML and linkify URLs + } + //Write discussion HTML to changeset object for future use (i.e. when filtering the changesets list) + changesets[changesetID].discussionHTML = discussionTR.html(); + + } catch (error) { + if (error.name === 'AbortError') { + // Silent return/break on abort + return; + } + console.warn(`Could not fetch discussions for changeset ${changesetID}:`, error.message); + } + } +} + +/* Sort GeoJSON features by geometry type and size. + By doing this we can make sure that an element which is completely covered by a larger polygon can still be selected. + Sort order (i.e. order in which they are added to the map): + 1. Polygons (largest to smallest) + 2. Lines + 3. Points +*/ +function sortGeoJsonFeatures(a, b) { // a and b are GeoJSON features + const typeOrder = { 'Polygon': 1, 'LineString': 2, 'Point': 3 }; + const aType = a.geometry.type; + const bType = b.geometry.type; + if (typeOrder[aType] !== typeOrder[bType]) { + return typeOrder[aType] - typeOrder[bType]; + } + if (aType === 'Polygon') { + const aArea = calculateArea(a.geometry); + const bArea = calculateArea(b.geometry); + return bArea - aArea; // Largest first (Reverse the subtraction for descending order) + } + return 0; +}; + +/*Calculate relative area of polygon. +This so called 'Shoelace formula' does not return accurate results in m² because it only works for planar +2D coordinates. Our geo coordinates are on a sphere though (in degrees). Since we only want to compare +the relative size of each polygon for sorting purposes the formula is sufficient.*/ +function calculateArea(geometry) { + let area = 0; + const coords = geometry.coordinates[0]; + for (let i = 0; i < coords.length; i++) { + const j = (i + 1) % coords.length; //j is always x+1 except on the last element of the array where it is 0. + const [xi, yi] = coords[i]; + const [xj, yj] = coords[j]; + area += xi * yj - xj * yi; + } + // console.log(area); + return Math.abs(area) / 2; +}; + +//Render changesets list on the left side +function renderChangesetsList(changesetsToDisplay) { + // console.log(changesetsToDisplay); + //Object.values writes all values of 'changesetsToDisplay' object into an array + //Then the array elements (i.e. changesets) are sorted newest to oldest + const bytime = Object.values(changesetsToDisplay) + // Filter out non-changeset properties like 'allLeafletLayers'. + // Workflow: Is 'cs' NOT 'null' or undefined? And does it have a valid '.id' property? If true: keep this changeset in 'bytime'. + // If false: Filter it out. 'allLeafletLayers' does not have an '.id' property. + .filter(cs => cs && typeof cs.id !== 'undefined') + .sort((a, b) => (+b.time) - (+a.time)); + // console.log(bytime); - }).get(); + //From here onwards the creation of the changesets section starts + const results = d3.select('#results').html("");//Select and Clear the Results Container + const allresults = results + .selectAll('li.result')//Since li.results is nonexistent at this point in time D3 creates an empty selection + .data(bytime, d => d.id);//Bind data + // console.log(allresults); + //Below a single changeset div container 'rl' with all its content (e.g. loupe, traffic light, username, ...) is created. + const rl = allresults.enter() + .append('li') + .attr('class', 'result') + .attr('changeset-id', d => d.id) + .attr('user-id', d => d.userId) + // .attr('title', 'Changeset is highlighted on map') + .style('color', d => changesets[d.id].color) + .on('click', (event, d) => click(null, d))//Highlight changeset on click (desktop/mobile) - Pass null for feature, d for data + .on('mouseover', (event, d) => click(null, d));//Highlight changeset on mouseover (desktop) - Pass null for feature, d for data + // console.log(rl); + allresults.order(); + + //"Zoom to changeset" button + rl.append('div') + .classed('zoom', true) + .attr('title', 'Zoom to changeset') + //.html('🔎 ')//Unicode glyph for a loupe + .on('click', function (event, d) { + //Fit the bounds of the Leaflet layers featureGroup for this changeset + if (changesets[d.id] && changesets[d.id].leafletFeatureGroup) { + const groupToZoom = changesets[d.id].leafletFeatureGroup; + const bounds = groupToZoom.getBounds(); + map.fitBounds(bounds); + } + + //On small screens (screen width < 601px) scroll all the way down, so that map is completely visible on screen + if (screen.width < 601) { + window.scrollTo({ + top: document.body.scrollHeight, // Scroll to the bottom of the page ('document.body.scrollHeight' returns the total height of the entire document body) + behavior: 'smooth' + }); + } + }) + .append('svg') + .classed('loupe', true) + .append('use') + .attr('href', 'img/icons.svg#loupe'); + + //Vandalism Checker traffic light + let trafficLightContainer = rl.append("div") + .classed("traffic-light-container", true) + .attr('title', function (d) { + const changesetData = changesets[d.id]; + if (!changesetData) return "Changeset data not available for title."; + + const possibleVandalism = changesetData.possibleVandalism; + const deltaInNodesWays = changesetData.deltaInNodesWays; + const deltaInTags = changesetData.deltaInTags; + const deltaInIdWarningsAndResolves = changesetData.deltaInIdWarningsAndResolves; + const usedEditorWasId = changesetData.osmEditor && changesetData.osmEditor.toLowerCase().startsWith("id"); + // console.log('OSM Editor: ' + changesetData.osmEditor + ', usedEditorWasId: ' + usedEditorWasId); + let titleText; + //Only show the line "All resolved iD warnings..." if the used editor was actually iD + if (possibleVandalism) { + titleText = `This changeset is potentially destructive!\nAll added nodes/ways - All deleted nodes/ways: ${deltaInNodesWays}. ${deltaInNodesWays < vandalismThreshold ? "This is suspicious!" : ""}\nAll added tags - All deleted tags: ${deltaInTags}. ${deltaInTags < vandalismThreshold ? "This is suspicious!" : ""}\n${usedEditorWasId ? `All resolved iD warnings - New iD warnings: ${deltaInIdWarningsAndResolves}. ${deltaInIdWarningsAndResolves < vandalismThreshold ? "This is suspicious!" : ""}` : ""}\n\nReminder: It is often NOT necessary to delete elements in OSM. For example a closed shop should be tagged as 'disused:shop'. One day a new shop might open at the same exact lot and the tags can be updated. The same is true for demolished buildings ('demolished:building')`; + } else { + titleText = `This changeset looks good!\nAll added nodes/ways - All deleted nodes/ways: ${deltaInNodesWays}. ${deltaInNodesWays < vandalismThreshold ? "This is suspicious!" : ""}\nAll added tags - All deleted tags: ${deltaInTags}. ${deltaInTags < vandalismThreshold ? "This is suspicious!" : ""}\n${usedEditorWasId ? `All resolved iD warnings - New iD warnings: ${deltaInIdWarningsAndResolves}. ${deltaInIdWarningsAndResolves < vandalismThreshold ? "This is suspicious!" : ""}` : ""}`; + } + return titleText; + }); + let trafficLight = trafficLightContainer.append("div") + .classed("traffic-light", true); + trafficLight.append("span") + .attr('class', function (d) { + const possibleVandalism = changesets[d.id].possibleVandalism; + return (possibleVandalism ? "gray" : "green"); + }); + trafficLight.append("span") + .attr('class', function (d) { + const possibleVandalism = changesets[d.id].possibleVandalism; + return (possibleVandalism ? "red" : "gray"); + }); + + //Adds a span element for displaying a text bubble SVG symbol if this OSM changeset has received comments + rl.append('div') + .classed('text-bubble', true) + .filter(d => d.discussionCount > 0) // Filter this span based on the data ('d.discussionCount' from 'bytime') + // If 'discussionCount' is larger 0 the 'span' html tag will pass the filter and the attributes below will be attached to it: + .attr('title', d => `Changeset has ${d.discussionCount} comment${d.discussionCount !== 1 ? "s" : ""}`) + .append('svg') // Append SVG only to filtered spans + .classed('text-bubble-svg', true) + .append('use') + .attr('href', 'img/icons.svg#speech-bubble'); + + //Container for name and age of changeset + let containerNameDate = rl.append('div') + .classed('container-name-date', true); + + //User name. + containerNameDate.append('a') + .classed('user-name', true) + .html(function (d) { + return d.user; // d.user might contain HTML highlights from filtering + }) + .attr('title', function (d) { + // Get unaltered user name from changesets. The value in d.user might have html in it if filtered, + // e.g. rene78. We don't want that in the title and href. + const unalteredUserName = changesets[d.id].user; + let titleText = `Go to OSM user page of ${unalteredUserName}`; + // If we have extended user data, add total changesets and signup date to the title + if (changesets.userData?.[d.userId]) { + return `${titleText}\n\nTotal changesets: ${changesets.userData?.[d.userId]?.changesetCount}\nSigned up: ${moment(changesets.userData?.[d.userId]?.accountCreated).fromNow()}`; + } else return titleText; + }) + .attr('target', '_blank') + .attr('href', function (d) { + const unalteredUserName = changesets[d.id].user;//Get unaltered user name from changesets object. + return '//openstreetmap.org/user/' + unalteredUserName; + }); + + //Timespan since changeset creation + containerNameDate.append('span') + .attr('title', function (d) { + return moment(d.time).format('MMM Do YYYY, h:mm:ss a'); + }) + .attr('class', 'date').text(function (d) { + return moment(d.time).fromNow(); + }); + + //Arrow to expand changeset information + rl.append('div') + .classed('arrow', true) + .attr('title', 'Open details of changeset') + .on('click', function (event, d) { + //Element related to the arrow animation + const arrowDiv = d3.select(this); // 'this' refers to the clicked element + const arrowSvg = arrowDiv.select('svg').node(); // grab the SVG node inside the arrow div + + //Elements related to the expansion of the changeset details + const thisResult = this.closest('.result'); // find parent element + const changesetComment = thisResult.querySelector('.changeset-comment'); //Changeset comment + const tableContainer = thisResult.querySelector('.table-container'); //Changeset details + + const isCollapsed = tableContainer.classList.toggle('hidden'); + + if (isCollapsed) { + //Collapsing + arrowSvg.classList.remove('rotated'); + // Only after collapse transition ends, restore truncation. Else it looks very choppy. + setTimeout(() => { + changesetComment.classList.remove('expanded'); + changesetComment.innerHTML = linkify(d.comment); + }, 300); + // Update tooltip when hovering over arrow + arrowDiv.attr('title', 'Open changeset information'); + // Add back the changeset comment once the details are collapsed + changesetComment.title = `Go to OSM changeset page\n\n${changesets[d.id].comment}`; + } else { + // Expanding + arrowSvg.classList.add('rotated'); + changesetComment.classList.add('expanded'); + changesetComment.innerText = "Information"; + //Remove the changeset comment from the tooltip when details are expanded. + changesetComment.title = "Go to OSM Changeset page"; + // Update tooltip when hovering over arrow + arrowDiv.attr('title', 'Close changeset information'); + } + }) + .append('svg') + .classed('arrow-up-svg', true) + .append('use') + .attr('href', 'img/icons.svg#arrow-up'); + + //Changeset comment + rl.append('a') + .classed('changeset-comment', true) + .attr('href', d => `https://www.openstreetmap.org/changeset/${d.id}`) + .attr('target', '_blank') + .attr('title', d => `Go to OSM changeset page\n\n${changesets[d.id].comment}`) + //Changeset title (was downloaded separately from OSM API) + .html((d) => {// d.comment might contain HTML highlights from filtering + // We apply linkify to make URLs clickable. + // Note: If a URL matches the search term, the highlighting might interfere with the tag creation, but this is a rare edge case. + return linkify(d.comment) || ''; + }) + + //Changeset details. Appears after clicking on the arrow button + let tableContainer = rl.append('div') + .classed('table-container hidden', true) + + //The changeset details table which is hidden by default + tableContainer.append('table') + .classed('changeset-table', true) + .html(function (d) { + let imageryHtml; + const imageryUsed = changesets[d.id].imageryUsed; + if (imageryUsed.length === 0) imageryHtml = '-'; + else if (imageryUsed.length === 1) imageryHtml = imageryUsed[0]; + else { + imageryHtml = '
    '; + imageryUsed.forEach((provider, index) => { + imageryHtml += `
  • ${provider}
  • `; + }) + imageryHtml += '
'; + } + + const usedEditorWasId = changesets[d.id].osmEditor && changesets[d.id].osmEditor.toLowerCase().startsWith("id");//code is written twice. not very clean. + htmlForIdWarningsCheck = ` + + iD Warnings
Resolved-New + ${changesets[d.id].deltaInIdWarningsAndResolves} + `; + + // Infos regarding the "User Experience" data: After executing run() the user data (i.e. total edits & sign-up date) has not been + // downloaded and written into the 'changesets' object yet. When using the filter function the user data is already in 'changesets' though. + // That is why we check if 'changesets.userData?.[d.userId]' is available or not. The same is true for the discussion data further down. + let tableHtml = ` + + + Changeset Details + + + Comment + ${d.comment} + + + Imagery + ${imageryHtml} + + + Editor + ${changesets[d.id].osmEditor || '-'} + + + User Experience + + + Edits count + ${!(changesets.userData?.[d.userId]) ? "..." : changesets.userData[d.userId].changesetCount.toLocaleString()} + + + Joined + ${!(changesets.userData?.[d.userId]) ? "..." : moment(changesets.userData[d.userId].accountCreated).fromNow()} + + + Changeset Integrity + + + Elements
Added-Deleted + ${changesets[d.id].deltaInNodesWays} + + + Tags
Added-Deleted + ${changesets[d.id].deltaInTags} + + ${usedEditorWasId ? htmlForIdWarningsCheck : ""} + + Discussion + + + + ${!(changesets[d.id].discussionHTML) ? "" : changesets[d.id].discussionHTML} + + + `; + return tableHtml; + }) +} + +//Highlight clicked layer on map and in sidebar (happens when selecting element in sidebar or on map) +// eventOrFeature can be a Leaflet GeoJSON feature (from map click) or null (from sidebar click/hover) +// d can be changeset data (from sidebar click/hover) or null (from map click) +function click(eventOrFeature, d) { + // console.log("--- click Function Called ---"); + // console.log("Argument 1 (eventOrFeature):", eventOrFeature); + // console.log("Argument 2 (d):", d); + + let changesetNumber; + if (d && typeof d.id !== 'undefined') { // Click/hover from sidebar (d is changeset data from bytime array) + changesetNumber = d.id; + } else if (eventOrFeature?.properties?.meta?.changeset) { // Click from map (eventOrFeature is a GeoJSON feature) + changesetNumber = eventOrFeature.properties.meta.changeset; + } else { + console.warn("Could not determine changeset number in click handler.", eventOrFeature, d); + return; // Cannot proceed without a changeset number + } + // console.log('changesetNumber: ' + changesetNumber); + + //Reset style of previously highlighted changeset + //The code 'changesets.allLeafletLayers.resetStyle();' is possible, too. But for this setStyle() is called for every element on the map. + //Computationally demanding for no added value since we only need to reset the color of the previously highlighted featureGroup. + highlightedChangeset = changesets.highlightedChangeset; + // console.log(highlightedChangeset); + if (highlightedChangeset) changesets[highlightedChangeset].leafletFeatureGroup.setStyle({ color: changesets[highlightedChangeset].color });//Change color back to shade of red + changesets.highlightedChangeset = changesetNumber;//Change highlighted changeset to the currently selected one. + + const results = d3.select('#results'); + results + .selectAll('li.result') + .classed('active', function (dataItem) { // dataItem here is an element from the 'bytime' array + //assign the class "active" if id of li element equals changeset number + return dataItem.id == changesetNumber; //returns true if dataItem.id equals the changeset number from above. Else it returns false. + }); + + //Highlight featureGroup belonging to the selected changeset number. + if (changesets[changesetNumber] && changesets[changesetNumber].leafletFeatureGroup) { + changesets[changesetNumber].leafletFeatureGroup.setStyle({ color: '#008dff' }); //Highlighting color: blue + } + + //Make sure that sidebar is displayed (if it was hidden) + sidebar.classList.remove("hide"); } -//Error message in case of no results from Overpass -function message(type, error) { - // console.log(error.statusText); +// Helper function to highlight search term in a given text +function highlightSearchTermInText(text, searchTerm) { + const originalText = text || ""; // Ensure we have a string, even if input is null/undefined + let highlightedText = originalText; + let matchFound = false; + + // Only proceed if searchTerm is provided and text is not empty + if (searchTerm && originalText) { + // Check if the original text (case-insensitively) contains the search term + if (originalText.toLowerCase().includes(searchTerm.toLowerCase())) { + matchFound = true; + highlightedText = ''; // Rebuild with highlights + let lastIndex = 0; + // Safely handle any user input, even special regex symbols. Make it case-insensitive. + const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); + let match; + while ((match = regex.exec(originalText)) !== null) { + highlightedText += originalText.substring(lastIndex, match.index); // The original text up until the search term encounter... + highlightedText += `${match[0]}`; // ...plus the highlighted search term... + lastIndex = regex.lastIndex; // index at the end of the search term + } + highlightedText += originalText.substring(lastIndex); // ...plus the part of the text after the highlighted search term + } + } + return { highlightedText, matchFound }; +} + +//Filter changesets and update changesets list and GeoJSON data on map +function filterChangesets() { + let foundChangesets = {}; // This will store data for rendering (potentially with highlighted text) + const searchTerm = document.querySelector(".search-changesets-field").value.toLowerCase(); + const onlyRed = document.querySelector("#filter-red-checkbox").checked; + + // If searchTerm is empty AND "onlyRed" is not checked, show all changesets by writing all the data from 'changesets' object into 'foundChangesets' + if (!searchTerm && !onlyRed) { + for (const changesetId in changesets) { + // Ensure we are only processing actual changeset entries, not 'allLeafletLayers' + if (changesets.hasOwnProperty(changesetId) && changesetId !== 'allLeafletLayers') { + foundChangesets[changesetId] = changesets[changesetId]; // Use original data + } + } + renderChangesetsList(foundChangesets); + displayGeoJson(foundChangesets); // Pass the same object, displayGeoJson will look up groups in changesets + return; + } + + for (const changesetId in changesets) { + if (!changesets.hasOwnProperty(changesetId) || changesetId === 'allLeafletLayers') continue; + const currentChangesetData = changesets[changesetId]; // Original data + + // Apply "onlyRed" filter first. If it's active and current changeset is not marked as 'possible vandalism', skip. + if (onlyRed && !currentChangesetData.possibleVandalism) continue; + + let matchFoundBySearchTerm = false; + let modifiedComment = currentChangesetData.comment || ""; // Start with original or empty string + let modifiedUserName = currentChangesetData.user || ""; // Start with original or empty string + + if (searchTerm) { // Only perform search term matching if searchTerm is present + // 1. Search within COMMENTS for search term and get highlighted version + const commentResult = highlightSearchTermInText(currentChangesetData.comment, searchTerm); + modifiedComment = commentResult.highlightedText; + if (commentResult.matchFound) { + matchFoundBySearchTerm = true; + } + + // 2. Search within USER NAME for search term and get highlighted version + const userNameResult = highlightSearchTermInText(currentChangesetData.user, searchTerm); + modifiedUserName = userNameResult.highlightedText; + if (userNameResult.matchFound) { + matchFoundBySearchTerm = true; + } + } + + // Determine if this changeset should be included in results: + // - If searchTerm is entered AND matchFoundBySearchTerm must be true. + // - If searchTerm is NOT entered BUT onlyRed IS (due to initial check) + if ((searchTerm && matchFoundBySearchTerm) || (!searchTerm && onlyRed)) { + foundChangesets[changesetId] = { + ...currentChangesetData, // Copy all original properties + user: modifiedUserName, // Override with (potentially) highlighted user + comment: modifiedComment // Override with (potentially) highlighted comment + }; + } + } + // console.table(foundChangesets); + renderChangesetsList(foundChangesets); // Pass the object with potentially highlighted text + displayGeoJson(foundChangesets); + + //Filter GeoJSON on map + function displayGeoJson(fSets) { // fSets is the foundChangesets object + if (!changesets.allLeafletLayers) return; + + // Clear layers. Important: The layers are just detached from the map. The reference to those layers + // inside 'changesets[csId].leafletFeatureGroup' is still intact. + changesets.allLeafletLayers.clearLayers(); + + // 1. Collect all Leaflet layer instances that should be visible from the filtered sets + const visibleLayers = []; + for (const csId in fSets) { // Iterate over the fSets (which are the filtered changesets) + if (fSets.hasOwnProperty(csId)) { + // Get the original changeset data from the main object to access its leafletFeatureGroup + const originalChangesetData = changesets[csId]; + if (originalChangesetData && originalChangesetData.leafletFeatureGroup) { + originalChangesetData.leafletFeatureGroup.eachLayer(layer => { + visibleLayers.push(layer); + }); + } + } + } + + // 2. Sort these visible layers using the same logic as the initial sort. + // The Leaflet layer instance (`layer`) has `layer.feature`. + visibleLayers.sort((layerA, layerB) => { + // Use your existing sortGeoJsonFeatures by passing the features + return sortGeoJsonFeatures(layerA.feature, layerB.feature); + }); + + // 3. Add the sorted layers back to the main display group + visibleLayers.forEach(layer => { + if (changesets.allLeafletLayers && typeof changesets.allLeafletLayers.addLayer === 'function') { + changesets.allLeafletLayers.addLayer(layer); + } + }); + } +} + +// --- Helper Functions for URL Linking --- + +// Escapes HTML characters to prevent XSS and broken layout +function escapeHtml(text) { + if (!text) return text; + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Converts text URLs into clickable HTML
tags +function linkify(inputText) { + if (!inputText) return ""; + + // Pattern for http/https/ftp URLs + const protocolPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; + /* Replace the URL within the input text with the URL wrapped in an anchor tag + Example: "Visit http://osm.org" becomes 'Visit http://example.com' */ + let replacedText = inputText.replace(protocolPattern, '$1'); + + // Pattern for "www." URLs (add http:// automatically) + const wwwPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim; + replacedText = replacedText.replace(wwwPattern, '$1$2'); + + return replacedText; +} + +//Show a modal with a message +function message(type, text) { // Get the infobox modal const infobox = document.querySelector(".infobox"); infobox.classList.remove("show", "alarm", "success"); void infobox.offsetWidth; //Found here: https://css-tricks.com/restart-css-animation/#update-another-javascript-method-to-restart-a-css-animation - infobox.innerHTML = "Server error: " + error.statusText; + infobox.innerHTML = text; infobox.classList.add("show", type); } -function abort() { - if (xhr) { - xhr.abort(); - xhr = null; - } - var results = d3.select('#results'); - var allresults = results - .selectAll('div.result') - .data([]) - .exit().remove(); +//Scroll to top when clicking on "Back-to-top" button +function scrollToTop() { + sidebar.scrollTo({ + top: 0, + behavior: 'smooth' + }); } -var timeOutId = 0; -map.on('dragend ', function (event) { - if (event.distance < 12) return; - clearTimeout(timeOutId); - timeOutId = setTimeout(updateMap, 500); -}); -map.on('zoomend', function () { - clearTimeout(timeOutId); - timeOutId = setTimeout(updateMap, 500); +//Only show X to remove content of filter input after at least one char has been entered. Then launch filterChangesets function. +function showHideCrossThenFilter() { + const searchTerm = document.querySelector(".search-changesets-field").value; + const deleteFilterInput = document.querySelector(".delete-filter-input"); + if (searchTerm.length > 0) deleteFilterInput.classList.remove("hide"); + else deleteFilterInput.classList.add("hide"); + + //Filter changesets + filterChangesets(); +} + +//Check current zoom level of map and show info message, if zoomed out too far +map.on('zoom', updateMap); +//Update location in local storage when panning the map +map.on('drag', function (e) { + localStorage.setItem('location-hash', location.hash); }); +//Update variable "daysToShow" after change in time range selector d3.select('#resolution') + .attr('title', 'Select a time range') .on('change', function () { switch (this.selectedIndex) { case 0: // last 24h - days_to_show = 1; + daysToShow = 1; break; case 1: // last 3 days - days_to_show = 3; + daysToShow = 3; break; case 2: // last week - days_to_show = 7; + daysToShow = 7; break; case 3: // last month - days_to_show = 30; + daysToShow = 30; break; } - localStorage.setItem("resolution", days_to_show); - updateMap(); - }); \ No newline at end of file + localStorage.setItem("resolution", daysToShow); + }); + +//Start download on click of button +document.querySelector("#download-changesets-button").addEventListener("click", run); + +//Once text is typed into the filter changeset input --> Start filterChangesets function +document.querySelector(".search-changesets-field").addEventListener("input", showHideCrossThenFilter); + +//Remove content of "filter changeset input" once X is clicked +document.querySelector(".delete-filter-input").addEventListener("click", () => { + document.querySelector(".search-changesets-field").value = ""; + showHideCrossThenFilter(); // Call this to hide cross and re-filter (showing all) +}); + +//Only show changesets with red traffic light when clicking on the "filter red" checkbox +document.querySelector("#filter-red-checkbox").addEventListener("click", (e) => { + document.querySelector(".traffic-light-filter").classList.toggle("filter-red-color"); + filterChangesets(); +}); + +//Display "Back-to-top" button if changesets in sidebar are overflowing and user scrolled down a bit +sidebar.addEventListener("scroll", event => { + let toTop = document.querySelector(".to-top"); + // console.log(sidebar.scrollTop); + if (sidebar.scrollTop > 50) toTop.classList.remove("hide");//display to-top button + else toTop.classList.add("hide");//hide to-top button +}); \ No newline at end of file