audio support
parent
fd3564f1f7
commit
8698ffee6d
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,658 @@
|
||||
/* 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.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;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,767 @@
|
||||
// Packaging/modules magic dance.
|
||||
(function (factory) {
|
||||
var L;
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(['leaflet'], factory);
|
||||
} else if (typeof module !== 'undefined') {
|
||||
// Node/CommonJS
|
||||
L = require('leaflet');
|
||||
module.exports = factory(L);
|
||||
} else {
|
||||
// Browser globals
|
||||
if (typeof window.L === 'undefined')
|
||||
throw 'Leaflet must be loaded first';
|
||||
factory(window.L);
|
||||
}
|
||||
}(function (L) {
|
||||
"use strict";
|
||||
|
||||
L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) {
|
||||
// true if it's a flat array of latlngs; false if nested
|
||||
return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
|
||||
};
|
||||
|
||||
/**
|
||||
* @fileOverview Leaflet Geometry utilities for distances and linear referencing.
|
||||
* @name L.GeometryUtil
|
||||
*/
|
||||
|
||||
L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
|
||||
|
||||
/**
|
||||
Shortcut function for planar distance between two {L.LatLng} at current zoom.
|
||||
|
||||
@tutorial distance-length
|
||||
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {L.LatLng} latlngA geographical point A
|
||||
@param {L.LatLng} latlngB geographical point B
|
||||
@returns {Number} planar distance
|
||||
*/
|
||||
distance: function (map, latlngA, latlngB) {
|
||||
return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
|
||||
},
|
||||
|
||||
/**
|
||||
Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@param {L.LatLng} latlngA geographical point A of the segment
|
||||
@param {L.LatLng} latlngB geographical point B of the segment
|
||||
@returns {Number} planar distance
|
||||
*/
|
||||
distanceSegment: function (map, latlng, latlngA, latlngB) {
|
||||
var p = map.latLngToLayerPoint(latlng),
|
||||
p1 = map.latLngToLayerPoint(latlngA),
|
||||
p2 = map.latLngToLayerPoint(latlngB);
|
||||
return L.LineUtil.pointToSegmentDistance(p, p1, p2);
|
||||
},
|
||||
|
||||
/**
|
||||
Shortcut function for converting distance to readable distance.
|
||||
@param {Number} distance distance to be converted
|
||||
@param {String} unit 'metric' or 'imperial'
|
||||
@returns {String} in yard or miles
|
||||
*/
|
||||
readableDistance: function (distance, unit) {
|
||||
var isMetric = (unit !== 'imperial'),
|
||||
distanceStr;
|
||||
if (isMetric) {
|
||||
// show metres when distance is < 1km, then show km
|
||||
if (distance > 1000) {
|
||||
distanceStr = (distance / 1000).toFixed(2) + ' km';
|
||||
}
|
||||
else {
|
||||
distanceStr = distance.toFixed(1) + ' m';
|
||||
}
|
||||
}
|
||||
else {
|
||||
distance *= 1.09361;
|
||||
if (distance > 1760) {
|
||||
distanceStr = (distance / 1760).toFixed(2) + ' miles';
|
||||
}
|
||||
else {
|
||||
distanceStr = distance.toFixed(1) + ' yd';
|
||||
}
|
||||
}
|
||||
return distanceStr;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns true if the latlng belongs to segment A-B
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@param {L.LatLng} latlngA geographical point A of the segment
|
||||
@param {L.LatLng} latlngB geographical point B of the segment
|
||||
@param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really
|
||||
@returns {boolean}
|
||||
*/
|
||||
belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
|
||||
tolerance = tolerance === undefined ? 0.2 : tolerance;
|
||||
var hypotenuse = latlngA.distanceTo(latlngB),
|
||||
delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
|
||||
return delta/hypotenuse < tolerance;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns total length of line
|
||||
* @tutorial distance-length
|
||||
*
|
||||
* @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
|
||||
* @returns {Number} Total length (pixels for Point, meters for LatLng)
|
||||
*/
|
||||
length: function (coords) {
|
||||
var accumulated = L.GeometryUtil.accumulatedLengths(coords);
|
||||
return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of accumulated length along a line.
|
||||
* @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
|
||||
* @returns {Array<Number>} Array of accumulated lengths (pixels for Point, meters for LatLng)
|
||||
*/
|
||||
accumulatedLengths: function (coords) {
|
||||
if (typeof coords.getLatLngs == 'function') {
|
||||
coords = coords.getLatLngs();
|
||||
}
|
||||
if (coords.length === 0)
|
||||
return [];
|
||||
var total = 0,
|
||||
lengths = [0];
|
||||
for (var i = 0, n = coords.length - 1; i< n; i++) {
|
||||
total += coords[i].distanceTo(coords[i+1]);
|
||||
lengths.push(total);
|
||||
}
|
||||
return lengths;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the closest point of a {L.LatLng} on the segment (A-B)
|
||||
|
||||
@tutorial closest
|
||||
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@param {L.LatLng} latlngA geographical point A of the segment
|
||||
@param {L.LatLng} latlngB geographical point B of the segment
|
||||
@returns {L.LatLng} Closest geographical point
|
||||
*/
|
||||
closestOnSegment: function (map, latlng, latlngA, latlngB) {
|
||||
var maxzoom = map.getMaxZoom();
|
||||
if (maxzoom === Infinity)
|
||||
maxzoom = map.getZoom();
|
||||
var p = map.project(latlng, maxzoom),
|
||||
p1 = map.project(latlngA, maxzoom),
|
||||
p2 = map.project(latlngB, maxzoom),
|
||||
closest = L.LineUtil.closestPointOnSegment(p, p1, p2);
|
||||
return map.unproject(closest, maxzoom);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the closest latlng on layer.
|
||||
|
||||
Accept nested arrays
|
||||
|
||||
@tutorial closest
|
||||
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {Array<L.LatLng>|Array<Array<L.LatLng>>|L.PolyLine|L.Polygon} layer - Layer that contains the result
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@param {?boolean} [vertices=false] - Whether to restrict to path vertices.
|
||||
@returns {L.LatLng} Closest geographical point or null if layer param is incorrect
|
||||
*/
|
||||
closest: function (map, layer, latlng, vertices) {
|
||||
|
||||
var latlngs,
|
||||
mindist = Infinity,
|
||||
result = null,
|
||||
i, n, distance, subResult;
|
||||
|
||||
if (layer instanceof Array) {
|
||||
// if layer is Array<Array<T>>
|
||||
if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') {
|
||||
// if we have nested arrays, we calc the closest for each array
|
||||
// recursive
|
||||
for (i = 0; i < layer.length; i++) {
|
||||
subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices);
|
||||
if (subResult && subResult.distance < mindist) {
|
||||
mindist = subResult.distance;
|
||||
result = subResult;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else if (layer[0] instanceof L.LatLng
|
||||
|| typeof layer[0][0] === 'number'
|
||||
|| typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng}
|
||||
layer = L.polyline(layer);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// if we don't have here a Polyline, that means layer is incorrect
|
||||
// see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23
|
||||
if (! ( layer instanceof L.Polyline ) )
|
||||
return result;
|
||||
|
||||
// deep copy of latlngs
|
||||
latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0)));
|
||||
|
||||
// add the last segment for L.Polygon
|
||||
if (layer instanceof L.Polygon) {
|
||||
// add the last segment for each child that is a nested array
|
||||
var addLastSegment = function(latlngs) {
|
||||
if (L.Polyline._flat(latlngs)) {
|
||||
latlngs.push(latlngs[0]);
|
||||
} else {
|
||||
for (var i = 0; i < latlngs.length; i++) {
|
||||
addLastSegment(latlngs[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
addLastSegment(latlngs);
|
||||
}
|
||||
|
||||
// we have a multi polygon / multi polyline / polygon with holes
|
||||
// use recursive to explore and return the good result
|
||||
if ( ! L.Polyline._flat(latlngs) ) {
|
||||
for (i = 0; i < latlngs.length; i++) {
|
||||
// if we are at the lower level, and if we have a L.Polygon, we add the last segment
|
||||
subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices);
|
||||
if (subResult.distance < mindist) {
|
||||
mindist = subResult.distance;
|
||||
result = subResult;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
} else {
|
||||
|
||||
// Lookup vertices
|
||||
if (vertices) {
|
||||
for(i = 0, n = latlngs.length; i < n; i++) {
|
||||
var ll = latlngs[i];
|
||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
||||
if (distance < mindist) {
|
||||
mindist = distance;
|
||||
result = ll;
|
||||
result.distance = distance;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep the closest point of all segments
|
||||
for (i = 0, n = latlngs.length; i < n-1; i++) {
|
||||
var latlngA = latlngs[i],
|
||||
latlngB = latlngs[i+1];
|
||||
distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
|
||||
if (distance <= mindist) {
|
||||
mindist = distance;
|
||||
result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
|
||||
result.distance = distance;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the closest layer to latlng among a list of layers.
|
||||
|
||||
@tutorial closest
|
||||
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {Array<L.ILayer>} layers Set of layers
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty;
|
||||
*/
|
||||
closestLayer: function (map, layers, latlng) {
|
||||
var mindist = Infinity,
|
||||
result = null,
|
||||
ll = null,
|
||||
distance = Infinity;
|
||||
|
||||
for (var i = 0, n = layers.length; i < n; i++) {
|
||||
var layer = layers[i];
|
||||
if (layer instanceof L.LayerGroup) {
|
||||
// recursive
|
||||
var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
|
||||
if (subResult.distance < mindist) {
|
||||
mindist = subResult.distance;
|
||||
result = subResult;
|
||||
}
|
||||
} else {
|
||||
// Single dimension, snap on points, else snap on closest
|
||||
if (typeof layer.getLatLng == 'function') {
|
||||
ll = layer.getLatLng();
|
||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
||||
}
|
||||
else {
|
||||
ll = L.GeometryUtil.closest(map, layer, latlng);
|
||||
if (ll) distance = ll.distance; // Can return null if layer has no points.
|
||||
}
|
||||
if (distance < mindist) {
|
||||
mindist = distance;
|
||||
result = {layer: layer, latlng: ll, distance: distance};
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the n closest layers to latlng among a list of input layers.
|
||||
|
||||
@param {L.Map} map - Leaflet map to be used for this method
|
||||
@param {Array<L.ILayer>} layers - Set of layers
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@param {?Number} [n=layers.length] - the expected number of output layers.
|
||||
@returns {Array<object>} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n)
|
||||
*/
|
||||
nClosestLayers: function (map, layers, latlng, n) {
|
||||
n = typeof n === 'number' ? n : layers.length;
|
||||
|
||||
if (n < 1 || layers.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var results = [];
|
||||
var distance, ll;
|
||||
|
||||
for (var i = 0, m = layers.length; i < m; i++) {
|
||||
var layer = layers[i];
|
||||
if (layer instanceof L.LayerGroup) {
|
||||
// recursive
|
||||
var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
|
||||
results.push(subResult);
|
||||
} else {
|
||||
// Single dimension, snap on points, else snap on closest
|
||||
if (typeof layer.getLatLng == 'function') {
|
||||
ll = layer.getLatLng();
|
||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
||||
}
|
||||
else {
|
||||
ll = L.GeometryUtil.closest(map, layer, latlng);
|
||||
if (ll) distance = ll.distance; // Can return null if layer has no points.
|
||||
}
|
||||
results.push({layer: layer, latlng: ll, distance: distance});
|
||||
}
|
||||
}
|
||||
|
||||
results.sort(function(a, b) {
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
if (results.length > n) {
|
||||
return results.slice(0, n);
|
||||
} else {
|
||||
return results;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all layers within a radius of the given position, in an ascending order of distance.
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {Array<ILayer>} layers - A list of layers.
|
||||
@param {L.LatLng} latlng - The position to search
|
||||
@param {?Number} [radius=Infinity] - Search radius in pixels
|
||||
@return {object[]} an array of objects including layer within the radius, closest latlng, and distance
|
||||
*/
|
||||
layersWithin: function(map, layers, latlng, radius) {
|
||||
radius = typeof radius == 'number' ? radius : Infinity;
|
||||
|
||||
var results = [];
|
||||
var ll = null;
|
||||
var distance = 0;
|
||||
|
||||
for (var i = 0, n = layers.length; i < n; i++) {
|
||||
var layer = layers[i];
|
||||
|
||||
if (typeof layer.getLatLng == 'function') {
|
||||
ll = layer.getLatLng();
|
||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
||||
}
|
||||
else {
|
||||
ll = L.GeometryUtil.closest(map, layer, latlng);
|
||||
if (ll) distance = ll.distance; // Can return null if layer has no points.
|
||||
}
|
||||
|
||||
if (ll && distance < radius) {
|
||||
results.push({layer: layer, latlng: ll, distance: distance});
|
||||
}
|
||||
}
|
||||
|
||||
var sortedResults = results.sort(function(a, b) {
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
return sortedResults;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the closest position from specified {LatLng} among specified layers,
|
||||
with a maximum tolerance in pixels, providing snapping behaviour.
|
||||
|
||||
@tutorial closest
|
||||
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {Array<ILayer>} layers - A list of layers to snap on.
|
||||
@param {L.LatLng} latlng - The position to snap
|
||||
@param {?Number} [tolerance=Infinity] - Maximum number of pixels.
|
||||
@param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex)
|
||||
@returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
|
||||
*/
|
||||
closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
|
||||
tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
|
||||
withVertices = typeof withVertices == 'boolean' ? withVertices : true;
|
||||
|
||||
var result = L.GeometryUtil.closestLayer(map, layers, latlng);
|
||||
if (!result || result.distance > tolerance)
|
||||
return null;
|
||||
|
||||
// If snapped layer is linear, try to snap on vertices (extremities and middle points)
|
||||
if (withVertices && typeof result.layer.getLatLngs == 'function') {
|
||||
var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true);
|
||||
if (closest.distance < tolerance) {
|
||||
result.latlng = closest;
|
||||
result.distance = L.GeometryUtil.distance(map, closest, latlng);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the Point located on a segment at the specified ratio of the segment length.
|
||||
@param {L.Point} pA coordinates of point A
|
||||
@param {L.Point} pB coordinates of point B
|
||||
@param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
|
||||
@returns {L.Point} the interpolated point.
|
||||
*/
|
||||
interpolateOnPointSegment: function (pA, pB, ratio) {
|
||||
return L.point(
|
||||
(pA.x * (1 - ratio)) + (ratio * pB.x),
|
||||
(pA.y * (1 - ratio)) + (ratio * pB.y)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the coordinate of the point located on a line at the specified ratio of the line length.
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {Array<L.LatLng>|L.PolyLine} latlngs Set of geographical points
|
||||
@param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive
|
||||
@returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
|
||||
(-1 if the interpolated point is the first vertex)
|
||||
*/
|
||||
interpolateOnLine: function (map, latLngs, ratio) {
|
||||
latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs;
|
||||
var n = latLngs.length;
|
||||
if (n < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ensure the ratio is between 0 and 1;
|
||||
ratio = Math.max(Math.min(ratio, 1), 0);
|
||||
|
||||
if (ratio === 0) {
|
||||
return {
|
||||
latLng: latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]),
|
||||
predecessor: -1
|
||||
};
|
||||
}
|
||||
if (ratio == 1) {
|
||||
return {
|
||||
latLng: latLngs[latLngs.length -1] instanceof L.LatLng ? latLngs[latLngs.length -1] : L.latLng(latLngs[latLngs.length -1]),
|
||||
predecessor: latLngs.length - 2
|
||||
};
|
||||
}
|
||||
|
||||
// project the LatLngs as Points,
|
||||
// and compute total planar length of the line at max precision
|
||||
var maxzoom = map.getMaxZoom();
|
||||
if (maxzoom === Infinity)
|
||||
maxzoom = map.getZoom();
|
||||
var pts = [];
|
||||
var lineLength = 0;
|
||||
for(var i = 0; i < n; i++) {
|
||||
pts[i] = map.project(latLngs[i], maxzoom);
|
||||
if(i > 0)
|
||||
lineLength += pts[i-1].distanceTo(pts[i]);
|
||||
}
|
||||
|
||||
var ratioDist = lineLength * ratio;
|
||||
|
||||
// follow the line segments [ab], adding lengths,
|
||||
// until we find the segment where the points should lie on
|
||||
var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0;
|
||||
for (var i = 0; cumulativeDistanceToB < ratioDist; i++) {
|
||||
var pointA = pts[i], pointB = pts[i+1];
|
||||
|
||||
cumulativeDistanceToA = cumulativeDistanceToB;
|
||||
cumulativeDistanceToB += pointA.distanceTo(pointB);
|
||||
}
|
||||
|
||||
if (pointA == undefined && pointB == undefined) { // Happens when line has no length
|
||||
var pointA = pts[0], pointB = pts[1], i = 1;
|
||||
}
|
||||
|
||||
// compute the ratio relative to the segment [ab]
|
||||
var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0;
|
||||
var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio);
|
||||
return {
|
||||
latLng: map.unproject(interpolatedPoint, maxzoom),
|
||||
predecessor: i-1
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a float between 0 and 1 representing the location of the
|
||||
closest point on polyline to the given latlng, as a fraction of total line length.
|
||||
(opposite of L.GeometryUtil.interpolateOnLine())
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {L.PolyLine} polyline Polyline on which the latlng will be search
|
||||
@param {L.LatLng} latlng The position to search
|
||||
@returns {Number} Float between 0 and 1
|
||||
*/
|
||||
locateOnLine: function (map, polyline, latlng) {
|
||||
var latlngs = polyline.getLatLngs();
|
||||
if (latlng.equals(latlngs[0]))
|
||||
return 0.0;
|
||||
if (latlng.equals(latlngs[latlngs.length-1]))
|
||||
return 1.0;
|
||||
|
||||
var point = L.GeometryUtil.closest(map, polyline, latlng, false),
|
||||
lengths = L.GeometryUtil.accumulatedLengths(latlngs),
|
||||
total_length = lengths[lengths.length-1],
|
||||
portion = 0,
|
||||
found = false;
|
||||
for (var i=0, n = latlngs.length-1; i < n; i++) {
|
||||
var l1 = latlngs[i],
|
||||
l2 = latlngs[i+1];
|
||||
portion = lengths[i];
|
||||
if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) {
|
||||
portion += l1.distanceTo(point);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
|
||||
}
|
||||
return portion / total_length;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a clone with reversed coordinates.
|
||||
@param {L.PolyLine} polyline polyline to reverse
|
||||
@returns {L.PolyLine} polyline reversed
|
||||
*/
|
||||
reverse: function (polyline) {
|
||||
return L.polyline(polyline.getLatLngs().slice(0).reverse());
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a sub-part of the polyline, from start to end.
|
||||
If start is superior to end, returns extraction from inverted line.
|
||||
@param {L.Map} map Leaflet map to be used for this method
|
||||
@param {L.PolyLine} polyline Polyline on which will be extracted the sub-part
|
||||
@param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
|
||||
@param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
|
||||
@returns {Array<L.LatLng>} new polyline
|
||||
*/
|
||||
extract: function (map, polyline, start, end) {
|
||||
if (start > end) {
|
||||
return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
|
||||
}
|
||||
|
||||
// Bound start and end to [0-1]
|
||||
start = Math.max(Math.min(start, 1), 0);
|
||||
end = Math.max(Math.min(end, 1), 0);
|
||||
|
||||
var latlngs = polyline.getLatLngs(),
|
||||
startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start),
|
||||
endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end);
|
||||
// Return single point if start == end
|
||||
if (start == end) {
|
||||
var point = L.GeometryUtil.interpolateOnLine(map, polyline, end);
|
||||
return [point.latLng];
|
||||
}
|
||||
// Array.slice() works indexes at 0
|
||||
if (startpoint.predecessor == -1)
|
||||
startpoint.predecessor = 0;
|
||||
if (endpoint.predecessor == -1)
|
||||
endpoint.predecessor = 0;
|
||||
var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
|
||||
result.unshift(startpoint.latLng);
|
||||
result.push(endpoint.latLng);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns true if first polyline ends where other second starts.
|
||||
@param {L.PolyLine} polyline First polyline
|
||||
@param {L.PolyLine} other Second polyline
|
||||
@returns {bool}
|
||||
*/
|
||||
isBefore: function (polyline, other) {
|
||||
if (!other) return false;
|
||||
var lla = polyline.getLatLngs(),
|
||||
llb = other.getLatLngs();
|
||||
return (lla[lla.length-1]).equals(llb[0]);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns true if first polyline starts where second ends.
|
||||
@param {L.PolyLine} polyline First polyline
|
||||
@param {L.PolyLine} other Second polyline
|
||||
@returns {bool}
|
||||
*/
|
||||
isAfter: function (polyline, other) {
|
||||
if (!other) return false;
|
||||
var lla = polyline.getLatLngs(),
|
||||
llb = other.getLatLngs();
|
||||
return (lla[0]).equals(llb[llb.length-1]);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns true if first polyline starts where second ends or start.
|
||||
@param {L.PolyLine} polyline First polyline
|
||||
@param {L.PolyLine} other Second polyline
|
||||
@returns {bool}
|
||||
*/
|
||||
startsAtExtremity: function (polyline, other) {
|
||||
if (!other) return false;
|
||||
var lla = polyline.getLatLngs(),
|
||||
llb = other.getLatLngs(),
|
||||
start = lla[0];
|
||||
return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns horizontal angle in degres between two points.
|
||||
@param {L.Point} a Coordinates of point A
|
||||
@param {L.Point} b Coordinates of point B
|
||||
@returns {Number} horizontal angle
|
||||
*/
|
||||
computeAngle: function(a, b) {
|
||||
return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns slope (Ax+B) between two points.
|
||||
@param {L.Point} a Coordinates of point A
|
||||
@param {L.Point} b Coordinates of point B
|
||||
@returns {Object} with ``a`` and ``b`` properties.
|
||||
*/
|
||||
computeSlope: function(a, b) {
|
||||
var s = (b.y - a.y) / (b.x - a.x),
|
||||
o = a.y - (s * a.x);
|
||||
return {'a': s, 'b': o};
|
||||
},
|
||||
|
||||
/**
|
||||
Returns LatLng of rotated point around specified LatLng center.
|
||||
@param {L.LatLng} latlngPoint: point to rotate
|
||||
@param {double} angleDeg: angle to rotate in degrees
|
||||
@param {L.LatLng} latlngCenter: center of rotation
|
||||
@returns {L.LatLng} rotated point
|
||||
*/
|
||||
rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
|
||||
var maxzoom = map.getMaxZoom();
|
||||
if (maxzoom === Infinity)
|
||||
maxzoom = map.getZoom();
|
||||
var angleRad = angleDeg*Math.PI/180,
|
||||
pPoint = map.project(latlngPoint, maxzoom),
|
||||
pCenter = map.project(latlngCenter, maxzoom),
|
||||
x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
|
||||
y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
|
||||
return map.unproject(new L.Point(x2,y2), maxzoom);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the bearing in degrees clockwise from north (0 degrees)
|
||||
from the first L.LatLng to the second, at the first LatLng
|
||||
@param {L.LatLng} latlng1: origin point of the bearing
|
||||
@param {L.LatLng} latlng2: destination point of the bearing
|
||||
@returns {float} degrees clockwise from north.
|
||||
*/
|
||||
bearing: function(latlng1, latlng2) {
|
||||
var rad = Math.PI / 180,
|
||||
lat1 = latlng1.lat * rad,
|
||||
lat2 = latlng2.lat * rad,
|
||||
lon1 = latlng1.lng * rad,
|
||||
lon2 = latlng2.lng * rad,
|
||||
y = Math.sin(lon2 - lon1) * Math.cos(lat2),
|
||||
x = Math.cos(lat1) * Math.sin(lat2) -
|
||||
Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
|
||||
|
||||
var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360;
|
||||
return bearing >= 180 ? bearing-360 : bearing;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the point that is a distance and heading away from
|
||||
the given origin point.
|
||||
@param {L.LatLng} latlng: origin point
|
||||
@param {float} heading: heading in degrees, clockwise from 0 degrees north.
|
||||
@param {float} distance: distance in meters
|
||||
@returns {L.latLng} the destination point.
|
||||
Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
|
||||
for a great reference and examples.
|
||||
*/
|
||||
destination: function(latlng, heading, distance) {
|
||||
heading = (heading + 360) % 360;
|
||||
var rad = Math.PI / 180,
|
||||
radInv = 180 / Math.PI,
|
||||
R = 6378137, // approximation of Earth's radius
|
||||
lon1 = latlng.lng * rad,
|
||||
lat1 = latlng.lat * rad,
|
||||
rheading = heading * rad,
|
||||
sinLat1 = Math.sin(lat1),
|
||||
cosLat1 = Math.cos(lat1),
|
||||
cosDistR = Math.cos(distance / R),
|
||||
sinDistR = Math.sin(distance / R),
|
||||
lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
|
||||
sinDistR * Math.cos(rheading)),
|
||||
lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
|
||||
cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
|
||||
lon2 = lon2 * radInv;
|
||||
lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
|
||||
return L.latLng([lat2 * radInv, lon2]);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the the angle of the given segment and the Equator in degrees,
|
||||
clockwise from 0 degrees north.
|
||||
@param {L.Map} map: Leaflet map to be used for this method
|
||||
@param {L.LatLng} latlngA: geographical point A of the segment
|
||||
@param {L.LatLng} latlngB: geographical point B of the segment
|
||||
@returns {Float} the angle in degrees.
|
||||
*/
|
||||
angle: function(map, latlngA, latlngB) {
|
||||
var pointA = map.latLngToContainerPoint(latlngA),
|
||||
pointB = map.latLngToContainerPoint(latlngB),
|
||||
angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90;
|
||||
angleDeg += angleDeg < 0 ? 360 : 0;
|
||||
return angleDeg;
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a point snaps on the segment and heading away from the given origin point a distance.
|
||||
@param {L.Map} map: Leaflet map to be used for this method
|
||||
@param {L.LatLng} latlngA: geographical point A of the segment
|
||||
@param {L.LatLng} latlngB: geographical point B of the segment
|
||||
@param {float} distance: distance in meters
|
||||
@returns {L.latLng} the destination point.
|
||||
*/
|
||||
destinationOnSegment: function(map, latlngA, latlngB, distance) {
|
||||
var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB),
|
||||
latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance);
|
||||
return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
|
||||
},
|
||||
});
|
||||
|
||||
return L.GeometryUtil;
|
||||
|
||||
}));
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
||||
importScripts('libmp3lame.min.js');
|
||||
|
||||
var mp3codec;
|
||||
|
||||
this.addEventListener('message', function(e) {
|
||||
switch (e.data.cmd) {
|
||||
case 'init':
|
||||
if (!e.data.config) {
|
||||
e.data.config = { };
|
||||
}
|
||||
mp3codec = Lame.init();
|
||||
|
||||
Lame.set_mode(mp3codec, e.data.config.mode || Lame.JOINT_STEREO);
|
||||
Lame.set_num_channels(mp3codec, e.data.config.channels || 2);
|
||||
Lame.set_num_samples(mp3codec, e.data.config.samples || -1);
|
||||
Lame.set_in_samplerate(mp3codec, e.data.config.samplerate || 44100);
|
||||
Lame.set_out_samplerate(mp3codec, e.data.config.samplerate || 44100);
|
||||
Lame.set_bitrate(mp3codec, e.data.config.bitrate || 128);
|
||||
|
||||
Lame.init_params(mp3codec);
|
||||
// console.log('Version :', Lame.get_version() + ' / ',
|
||||
// 'Mode: '+Lame.get_mode(mp3codec) + ' / ',
|
||||
// 'Samples: '+Lame.get_num_samples(mp3codec) + ' / ',
|
||||
// 'Channels: '+Lame.get_num_channels(mp3codec) + ' / ',
|
||||
// 'Input Samplate: '+ Lame.get_in_samplerate(mp3codec) + ' / ',
|
||||
// 'Output Samplate: '+ Lame.get_in_samplerate(mp3codec) + ' / ',
|
||||
// 'Bitlate :' +Lame.get_bitrate(mp3codec) + ' / ');
|
||||
// 'VBR :' + Lame.get_VBR(mp3codec));
|
||||
break;
|
||||
case 'encode':
|
||||
var mp3data = Lame.encode_buffer_ieee_float(mp3codec, e.data.buf, e.data.buf);
|
||||
self.postMessage({cmd: 'data', buffer: mp3data.data});
|
||||
break;
|
||||
case 'finish':
|
||||
var mp3data = Lame.encode_flush(mp3codec);
|
||||
self.postMessage({cmd: 'end', buffer: mp3data.data});
|
||||
Lame.close(mp3codec);
|
||||
mp3codec = null;
|
||||
break;
|
||||
}
|
||||
});
|
@ -0,0 +1,157 @@
|
||||
(function(window){
|
||||
|
||||
var RECORDER_WORKER_PATH = '/static/js/recorderWorker.js';
|
||||
var ENCODER_WORKER_PATH = '/static/js/mp3Worker.js';
|
||||
|
||||
|
||||
var MP3Recorder = function(context, stream, cfg) {
|
||||
var config = cfg || { statusContainer: null, statusMethod: 'append' }
|
||||
|
||||
var bufferLen = 4096;
|
||||
var recording = false;
|
||||
|
||||
this.source = context.createMediaStreamSource(stream);
|
||||
this.node = (context.createScriptProcessor || context.createJavaScriptNode).call(context, bufferLen, 1, 1);
|
||||
|
||||
var recorderWorker = new Worker(RECORDER_WORKER_PATH);
|
||||
var encoderWorker = new Worker(ENCODER_WORKER_PATH);
|
||||
var exportCallback;
|
||||
|
||||
|
||||
// initialize the Recorder Worker
|
||||
recorderWorker.postMessage({ cmd: 'init', sampleRate: context.sampleRate });
|
||||
|
||||
// the recording loop
|
||||
this.node.onaudioprocess = function(e) {
|
||||
if(!recording) return;
|
||||
recorderWorker.postMessage({ cmd: 'record', buffer: e.inputBuffer.getChannelData(0) });
|
||||
}
|
||||
|
||||
|
||||
this.start = function() {
|
||||
recording = true;
|
||||
this.logStatus('recording...');
|
||||
}
|
||||
this.stop = function() {
|
||||
recording = false;
|
||||
this.logStatus('stopping...');
|
||||
}
|
||||
this.destroy = function() { recorderWorker.postMessage({ cmd: 'destroy' }); }
|
||||
|
||||
this.logStatus = function(status) {
|
||||
if(config.statusContainer) {
|
||||
if(config.statusMethod == 'append') {
|
||||
config.statusContainer.text(config.statusContainer.text + "\n" + status);
|
||||
} else {
|
||||
config.statusContainer.text(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.exportBlob = function(cb) {
|
||||
exportCallback = cb;
|
||||
if (!exportCallback) throw new Error('Callback not set');
|
||||
recorderWorker.postMessage({ cmd: 'exportBlob' });
|
||||
}
|
||||
|
||||
this.exportWAV = function(cb) {
|
||||
// export the blob from the worker
|
||||
this.exportBlob(function(blob) {
|
||||
var fileReader = new FileReader();
|
||||
|
||||
// read the blob as array buffer and convert it
|
||||
// to a base64 encoded WAV buffer
|
||||
fileReader.addEventListener("loadend", function() {
|
||||
var resultBuffer = new Uint8Array(this.result);
|
||||
cb(encode64(resultBuffer));
|
||||
});
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
this.exportMP3 = function(cb) {
|
||||
this.logStatus('converting...');
|
||||
|
||||
// export the blob from the worker
|
||||
this.exportBlob(function(blob) {
|
||||
var fileReader = new FileReader();
|
||||
|
||||
fileReader.addEventListener("loadend", function() {
|
||||
var wavBuffer = new Uint8Array(this.result);
|
||||
var wavData = parseWav(wavBuffer);
|
||||
|
||||
encoderWorker.addEventListener('message', function(e) {
|
||||
if (e.data.cmd == 'data') {
|
||||
cb(encode64(e.data.buffer));
|
||||
}
|
||||
});
|
||||
|
||||
encoderWorker.postMessage({ cmd: 'init', config: { mode: 3, channels: 1, samplerate: wavData.sampleRate, bitrate: wavData.bitsPerSample } });
|
||||
encoderWorker.postMessage({ cmd: 'encode', buf: Uint8ArrayToFloat32Array(wavData.samples) });
|
||||
encoderWorker.postMessage({ cmd: 'finish' });
|
||||
});
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// event listener for return values of the recorderWorker
|
||||
recorderWorker.addEventListener('message', function(e) {
|
||||
switch(e.data.from) {
|
||||
case 'exportBlob':
|
||||
exportCallback(e.data.blob);
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
function encode64(buffer) {
|
||||
var binary = '';
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var len = bytes.byteLength;
|
||||
|
||||
for(var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
function parseWav(wav) {
|
||||
function readInt(i, bytes) {
|
||||
var ret = 0, shft = 0;
|
||||
|
||||
while(bytes) {
|
||||
ret += wav[i] << shft; shft += 8;
|
||||
i++; bytes--;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if(readInt(20, 2) != 1) throw 'Invalid compression code, not PCM';
|
||||
if(readInt(22, 2) != 1) throw 'Invalid number of channels, not 1';
|
||||
|
||||
return { sampleRate: readInt(24, 4), bitsPerSample: readInt(34, 2), samples: wav.subarray(44) };
|
||||
}
|
||||
|
||||
function Uint8ArrayToFloat32Array(u8a){
|
||||
var f32Buffer = new Float32Array(u8a.length);
|
||||
for (var i = 0; i < u8a.length; i++) {
|
||||
var value = u8a[i<<1] + (u8a[(i<<1)+1]<<8);
|
||||
if (value >= 0x8000) value |= ~0x7FFF;
|
||||
f32Buffer[i] = value / 0x8000;
|
||||
}
|
||||
return f32Buffer;
|
||||
}
|
||||
|
||||
|
||||
this.source.connect(this.node);
|
||||
this.node.connect(context.destination); // this should not be necessary
|
||||
}
|
||||
|
||||
window.MP3Recorder = MP3Recorder;
|
||||
|
||||
})(window);
|
@ -0,0 +1,130 @@
|
||||
var recordingLength = 0,
|
||||
recordingBuffer = [],
|
||||
bits = 16,
|
||||
sampleRate = 0;
|
||||
|
||||
|
||||
this.addEventListener('message', function(e) {
|
||||
switch (e.data.cmd) {
|
||||
case 'init':
|
||||
init(e.data.sampleRate);
|
||||
break;
|
||||
case 'start':
|
||||
start();
|
||||
break;
|
||||
case 'stop':
|
||||
stop();
|
||||
break;
|
||||
case 'destroy':
|
||||
destroy();
|
||||
break;
|
||||
|
||||
case 'record':
|
||||
record(e.data.buffer);
|
||||
break;
|
||||
|
||||
case 'exportBlob':
|
||||
exportBlob();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
function init(sr) {
|
||||
sampleRate = sr;
|
||||
}
|
||||
|
||||
function start() {
|
||||
|
||||
}
|
||||
|
||||
function stop() {
|
||||
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
recordingLength = 0;
|
||||
recordingBuffer = [];
|
||||
sampleRate = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function record(buffer) {
|
||||
recordingBuffer.push(buffer);
|
||||
recordingLength += buffer.length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function exportBlob() {
|
||||
var audioBlob = new Blob([encodeWAV(mergeBuffer(recordingBuffer, recordingLength))]);
|
||||
this.postMessage({ from: 'exportBlob', blob: audioBlob });
|
||||
}
|
||||
|
||||
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
function mergeBuffer(buf, len){
|
||||
var result = new Float32Array(len);
|
||||
var offset = 0;
|
||||
for (var i = 0; i < buf.length; i++){
|
||||
result.set(buf[i], offset);
|
||||
offset += buf[i].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function encodeWAV(samples){
|
||||
var buffer = new ArrayBuffer(44 + samples.length * 2);
|
||||
var view = new DataView(buffer);
|
||||
|
||||
/* RIFF identifier */
|
||||
writeString(view, 0, 'RIFF');
|
||||
/* file length */
|
||||
view.setUint32(4, 32 + samples.length * 2, true);
|
||||
/* RIFF type */
|
||||
writeString(view, 8, 'WAVE');
|
||||
/* format chunk identifier */
|
||||
writeString(view, 12, 'fmt ');
|
||||
/* format chunk length */
|
||||
view.setUint32(16, 16, true);
|
||||
/* sample format (raw) */
|
||||
view.setUint16(20, 1, true);
|
||||
/* channel count */
|
||||
//view.setUint16(22, 2, true); /*STEREO*/
|
||||
view.setUint16(22, 1, true); /*MONO*/
|
||||
/* sample rate */
|
||||
view.setUint32(24, sampleRate, true);
|
||||
/* byte rate (sample rate * block align) */
|
||||
//view.setUint32(28, sampleRate * 4, true); /*STEREO*/
|
||||
view.setUint32(28, sampleRate * 2, true); /*MONO*/
|
||||
/* block align (channel count * bytes per sample) */
|
||||
//view.setUint16(32, 4, true); /*STEREO*/
|
||||
view.setUint16(32, 2, true); /*MONO*/
|
||||
/* bits per sample */
|
||||
view.setUint16(34, 16, true);
|
||||
/* data chunk identifier */
|
||||
writeString(view, 36, 'data');
|
||||
/* data chunk length */
|
||||
view.setUint32(40, samples.length * 2, true);
|
||||
|
||||
floatTo16BitPCM(view, 44, samples);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(output, offset, input){
|
||||
for (var i = 0; i < input.length; i++, offset+=2){
|
||||
var s = Math.max(-1, Math.min(1, input[i]));
|
||||
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
function writeString(view, offset, string){
|
||||
for (var i = 0; i < string.length; i++){
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue