1(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ 2const zipCodes = require('./zip-codes.js'); 3var L = require('leaflet'); 4require('leaflet.markercluster'); 5require("leaflet.featuregroup.subgroup"); 6var bodyScrollLock = require('body-scroll-lock'); 7var JsSearch = require('js-search'); 8 9jQuery(document).ready(function($) { 10 11 //Settings page 12 var vp_woo_pont_frontend = { 13 $overlay: $('.vp-woo-pont-modal-bg'), 14 $modal: $('.vp-woo-pont-modal'), 15 $close_button: $('.vp-woo-pont-modal-map-close'), 16 $filters: $('.vp-woo-pont-modal-sidebar-filters'), 17 $list: $('.vp-woo-pont-modal-sidebar-results'), 18 $search_field: $('.vp-woo-pont-modal-sidebar-search-field'), 19 $search_field_clear: $('.vp-woo-pont-modal-sidebar-search-clear'), 20 $map: false, 21 map_loaded: false, 22 markerClusters: false, 23 groups: [], 24 list: false, 25 enabled_filters: [], 26 search: false, 27 selected_point: false, 28 selected_pickup_point: false, 29 providers: false, 30 markerIcons: [], 31 method_changed: false, 32 clickedMarker: false, 33 checkout_needs_reload: false, 34 is_checkoutwc: false, 35 notes: false, 36 billing_country: false, 37 features: [], 38 init: function() { 39 40 //Initialize a map 41 this.$map = L.map('vp-woo-pont-modal-map',{ 42 renderer: L.canvas(), 43 }); 44 45 //this.$map.attributionControl.setPrefix(''); 46 47 //Move zoom controls to bottom right 48 this.$map.zoomControl.setPosition('bottomright'); 49 50 //Get default parameters for markerClusters 51 var markerClusterParams = { 52 spiderfyOnMaxZoom: true, 53 showCoverageOnHover: false, 54 zoomToBoundsOnClick: true, 55 disableClusteringAtZoom: 16, 56 removeOutsideVisibleBounds: true, 57 chunkedLoading: true, 58 chunkProgress: function(processed, total) { 59 // Optional: show loading progress 60 console.log(`Clustering: ${Math.round((processed/total)*100)}%`); 61 }, 62 chunkDelay: 0, // Reduce delay for faster processing 63 maxClusterRadius: 80, // Slightly smaller for better performance 64 animate: true, // Disable animations for large datasets 65 spiderfyDistanceMultiplier: 1.2, 66 // Custom cluster icon for better performance 67 iconCreateFunction: function(cluster) { 68 var count = cluster.getChildCount(); 69 var size = count < 100 ? 'small' : count < 1000 ? 'medium' : 'large'; 70 return new L.DivIcon({ 71 html: '<div><span>' + count + '</span></div>', 72 className: 'marker-cluster marker-cluster-' + size, 73 iconSize: new L.Point(40, 40) 74 }); 75 } 76 }; 77 78 //If custom parameters set 79 if(vp_woo_pont_frontend_params.markerClusterParams) { 80 markerClusterParams = vp_woo_pont_frontend_params.markerClusterParams; 81 } 82 83 //Create cluster layer 84 this.markerClusters = new L.MarkerClusterGroup(markerClusterParams); 85 86 //Initialize search engine, which will search for name, city, zip code and address 87 this.search = new JsSearch.Search('name'); 88 this.search.addIndex('name'); 89 this.search.addIndex('city_nfd'); 90 this.search.addIndex('zip'); 91 this.search.addIndex('addr_nfd');
92 this.search.addIndex('keywords'); 93 94 //When map is moved, update sidebar results 95 this.$map.on("moveend", function (e) { 96 97 //Only, if a point is not selected yet 98 //In that case, clicking on the map will update the sidebar only 99 if(!vp_woo_pont_frontend.selected_point) { 100 vp_woo_pont_frontend.syncSidebar(); 101 } 102 103 }); 104 105 //When map is moved, update sidebar results 106 this.$map.on("movestart", function (e) { 107 vp_woo_pont_frontend.$modal.removeClass('search-focused'); 108 }); 109 110 //Click action to show the modal 111 $(document).on( 'click', '#vp-woo-pont-show-map', this.show_modal ); 112 $(document).on( 'click', '.vp-woo-pont-show-map', this.show_modal ); 113 114 //When filters changed, update stuff 115 $(document).on( 'change', '.vp-woo-pont-modal-sidebar-filters input', this.toggle_layers ); 116 117 //Highlight search box text on click 118 this.$search_field.click(function () { 119 vp_woo_pont_frontend.$modal.addClass('search-focused'); 120 }); 121 122 //Prevent hitting enter from refreshing the page 123 this.$search_field.keypress(function (e) { 124 if (e.which == 13) { 125 e.preventDefault(); 126 vp_woo_pont_frontend.$search_field.blur(); 127 } 128 }); 129 130 //Fix iOS jumping 131 if(this.isiOS()) { 132 var searchFieldElement = this.$search_field[0]; 133 searchFieldElement.addEventListener('touchstart', (event) => { 134 event.stopPropagation(); 135 searchFieldElement.style.transform = 'TranslateY(-10000px)' 136 searchFieldElement.focus(); 137 setTimeout(function () { searchFieldElement.style.transform = 'none' }, 100); 138 }); 139 } 140 141 //When search field typed, start search with a delay 142 var timeout = null; 143 this.$search_field.keyup(function() { 144 var keyword = $(this).val(); 145 146 clearTimeout(timeout); 147 timeout = setTimeout(() => { 148 //Only if at least 2 characters 149 if(keyword.length > 2) { 150 vp_woo_pont_frontend.show_search_results(keyword); 151 } 152 }, 200); 153 154 //Show clear button 155 if(keyword.length > 0) { 156 vp_woo_pont_frontend.$search_field_clear.show(); 157 } else { 158 vp_woo_pont_frontend.$search_field_clear.hide(); 159 } 160 161 }); 162 163 //Hide modal when esc pressed 164 $(document).keyup(function(event){ 165 if(event.which=='27'){ 166 167 //If we are in the search field, just clear that first 168 if(vp_woo_pont_frontend.$search_field.is(":focus")) { 169 vp_woo_pont_frontend.$search_field.blur(); 170 vp_woo_pont_frontend.$modal.removeClass('search-focused'); 171 } else { 172 vp_woo_pont_frontend.hide_modal(); 173 } 174 } 175 }); 176 177 //Select a point in the sidebar 178 this.$list.on( 'click', '.vp-woo-pont-modal-sidebar-result', this.select_in_sidebar ); 179 180 //Select a pickup point for checkout 181 this.$list.on( 'click', '.vp-woo-pont-modal-sidebar-result-select', this.select_pickup_point ); 182 183 //Search clear button function 184 this.$search_field_clear.click(function(){ 185 vp_woo_pont_frontend.$search_field.val(''); 186 vp_woo_pont_frontend.$modal.removeClass('point-selected'); 187 vp_woo_pont_frontend.$modal.removeClass('search-focused'); 188 vp_woo_pont_frontend.$modal.removeClass('no-search-result'); 189 vp_woo_pont_frontend.$modal.removeClass('has-search-result'); 190 vp_woo_pont_frontend.selected_point = false; 191 vp_woo_pont_frontend.$list.find('li.selected').removeClass('selected'); 192 $('.leaflet-marker-icon.selected').removeClass('selected'); 193 $(this).hide(); 194 return false; 195 }); 196 197 //Hide modal 198 this.$close_button.click(function(){ 199 vp_woo_pont_frontend.hide_modal(); 200 return false; 201 }); 202 203 //Check if its a custom CheckoutWC page 204 if($('body').hasClass('checkout-wc')) { 205 vp_woo_pont_frontend.is_checkoutwc = true; 206 } 207 208 //When shipping method changed to vp pont 209 $('form.checkout').on( 'change', 'input[name^="shipping_method"]', this.on_shipping_method_change ); 210 $( document.body ).on( 'updated_checkout', this.on_checkout_updated ); 211 $( document.body ).on( 'updated_shipping_method', this.on_cart_updated ); 212 213 //On page load of shipping fields not visible, we need to reload the checkout page 214 var method = $("#shipping_method input:checked").val(); 215 if(method) { 216 vp_woo_pont_frontend.show_and_hide_shipping_address(method.includes('vp_pont')); 217 } else { 218 var method = $('#shipping_method .shipping_method').val(); 219 if(method) { 220 vp_woo_pont_frontend.show_and_hide_shipping_address(method.includes('vp_pont')); 221 } 222 } 223 224 //Run on page load too 225 this.on_cart_updated(); 226 227 //If theres a payment method condition set for the pricing, update the checkout if payment method changed 228 if(vp_woo_pont_frontend_params.refresh_payment_methods) { 229 $( 'form.checkout' ).on( 'change', 'input[name^="payment_method"]', function() { 230 $('body').trigger('update_checkout'); 231 }); 232 } 233 234 //Toggle open hours in the sidebar 235 this.$list.on( 'click', '.vp-woo-pont-modal-sidebar-result .open-hours', this.toggle_open_hours ); 236 237 }, 238 toggle_open_hours: function() { 239 $(this).toggleClass('open'); 240 }, 241 on_shipping_method_change: function() { 242 var method = $(this).val(); 243 vp_woo_pont_frontend.method_changed = true; 244 vp_woo_pont_frontend.show_and_hide_shipping_address(method.includes('vp_pont')); 245 }, 246 show_and_hide_shipping_address: function(hide) { 247 //Don't do anything for CheckoutWC 248 if(!vp_woo_pont_frontend.is_checkoutwc) { 249 if(hide) { 250 $('.woocommerce-shipping-fields').hide(); 251 $('#ship-to-different-address-checkbox').prop('checked', false); 252 $('#ship-to-different-address-checkbox').trigger( 'change' ) 253 } else { 254 $('.woocommerce-shipping-fields').show(); 255 } 256 } 257 }, 258 on_checkout_updated: function() { 259 if(!$('.vp-woo-pont-review-order-selected').length && vp_woo_pont_frontend.method_changed && vp_woo_pont_frontend_params.show_on_change) { 260 $('#vp-woo-pont-show-map').trigger('click'); 261 } 262 }, 263 show_modal: function() { 264 265 //Show the overlay 266 vp_woo_pont_frontend.$overlay.addClass('show'); 267 268 //Show the modal 269 setTimeout(function() { 270 vp_woo_pont_frontend.$modal.addClass('show'); 271 }, 1); 272 273 //Get available providers 274 var shipping_costs = $(this).data('shipping-costs'); 275 if(!shipping_costs) shipping_costs = $(this).parent().data('shipping-costs'); 276 vp_woo_pont_frontend.providers = shipping_costs; 277 278 //Get notes 279 var notes = $(this).data('notes'); 280 if(!notes) notes = $(this).parent().data('notes'); 281 vp_woo_pont_frontend.notes = notes; 282 283 //Set country based on priority: billing_country > shipping_country > billing-country > default 284 var current_billing_country; 285 if($('#billing_country').length && $('#billing_country').val()) { 286 current_billing_country = $('#billing_country').val(); 287 } else if($('#shipping_country').length && $('#shipping_country').val()) { 288 current_billing_country = $('#shipping_country').val(); 289 } else if($('#billing-country').length && $('#billing-country').val()) { 290 current_billing_country = $('#billing-country').val(); 291 } else { 292 current_billing_country = vp_woo_pont_frontend_params.default_country || 'HU'; 293 } 294 295 //Setup provider filter 296 vp_woo_pont_frontend.setup_provider_filters(vp_woo_pont_frontend.providers, current_billing_country); 297 298 //If we changed the country, we might need to reload the map points 299 if(current_billing_country != vp_woo_pont_frontend.billing_country) { 300 vp_woo_pont_frontend.map_loaded = false;
301 vp_woo_pont_frontend.billing_country = current_billing_country; 302 303 //Clear existing markers and groups 304 if(vp_woo_pont_frontend.markerClusters) { 305 vp_woo_pont_frontend.markerClusters.clearLayers(); 306 } 307 308 //Clear all groups 309 Object.keys(vp_woo_pont_frontend.groups).forEach(function(group){ 310 if(vp_woo_pont_frontend.groups[group]) { 311 vp_woo_pont_frontend.groups[group].clearLayers(); 312 vp_woo_pont_frontend.$map.removeLayer(vp_woo_pont_frontend.groups[group]); 313 } 314 }); 315 316 //Reset groups array 317 vp_woo_pont_frontend.groups = []; 318 319 //Reset search index 320 vp_woo_pont_frontend.search = new JsSearch.Search('name'); 321 vp_woo_pont_frontend.search.addIndex('name'); 322 vp_woo_pont_frontend.search.addIndex('city_nfd'); 323 vp_woo_pont_frontend.search.addIndex('zip'); 324 vp_woo_pont_frontend.search.addIndex('addr_nfd'); 325 vp_woo_pont_frontend.search.addIndex('keywords'); 326 327 //Clear sidebar 328 vp_woo_pont_frontend.$list.html(''); 329 } 330 331 //Init the map, but only once, when the modal is opened 332 if(!vp_woo_pont_frontend.map_loaded) { 333 vp_woo_pont_frontend.map_loaded = true; 334 vp_woo_pont_frontend.init_map(); 335 } else { 336 vp_woo_pont_frontend.geolocate(); 337 } 338 339 //Remove points from map 340 vp_woo_pont_frontend.hide_unwanted_providers(); 341 342 //Disable scrolling 343 bodyScrollLock.disableBodyScroll(vp_woo_pont_frontend.$modal[0], { 344 allowTouchMove: vp_woo_pont_frontend.$list 345 }); 346 347 //For developers to run extra stuff 348 $( document.body ).trigger( 'vp_woo_pont_modal_shown' ); 349 $( document.body ).addClass('vp-woo-pont-modal-visible'); 350 351 return false; 352 }, 353 hide_modal: function() { 354 355 //Hide the modal 356 vp_woo_pont_frontend.$modal.removeClass('show'); 357 358 //Hide the overlay 359 setTimeout(function() { 360 vp_woo_pont_frontend.$overlay.removeClass('show'); 361 }, 100); 362 363 //Enable scrolling 364 bodyScrollLock.clearAllBodyScrollLocks(); 365 366 $( document.body ).removeClass('vp-woo-pont-modal-visible'); 367 368 }, 369 init_map: function() { 370 371 //Center map 372 this.geolocate(); 373 374 //Get default parameters 375 //Get default parameters 376 var mapParameters = { 377 maxZoom: 19, 378 attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', 379 detectRetina: false, 380 tileLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' 381 }; 382 383 //Check if custom params set 384 if(vp_woo_pont_frontend_params.mapParameters) { 385 mapParameters = vp_woo_pont_frontend_params.mapParameters; 386 } 387 388 //Load image layers and set attribution 389 L.tileLayer(mapParameters.tileLayer, mapParameters).addTo(this.$map); 390 391 //Setup markers 392 vp_woo_pont_frontend.markerClusters.addTo(vp_woo_pont_frontend.$map); 393 vp_woo_pont_frontend.markerClusters._getExpandedVisibleBounds = function () { 394 return vp_woo_pont_frontend.markerClusters._map.getBounds(); 395 }; 396 397 //Create groups 398 vp_woo_pont_frontend_params.enabled_providers.forEach(function(provider){ 399 vp_woo_pont_frontend.groups[provider] = L.featureGroup.subGroup(vp_woo_pont_frontend.markerClusters); 400 vp_woo_pont_frontend.markerIcons[provider] = L.divIcon({html: '<div><i class="vp-woo-pont-provider-icon-'+provider+'"></i></div>', className: 'vp-woo-pont-marker '+provider, iconSize: [48, 55], iconAnchor: [24, 52]}); 401 }); 402 403 //Loop through json files 404 Object.keys(vp_woo_pont_frontend_params.files).forEach(function(json){ 405 406 //Check for country specific files 407 vp_woo_pont_frontend_params.files[json].forEach(function(file){ 408 409 //Skip if the country doesn't match 410 if(vp_woo_pont_frontend.billing_country != file.country) { 411 return; //skip this file 412 } 413 414 $.getJSON(vp_woo_pont_frontend_params.db_url+file.file, function (data) { 415 vp_woo_pont_frontend.process_provider(json, data); 416 417 //Refresh sidebar 418 vp_woo_pont_frontend.syncSidebar(); 419 420 }); 421 422 }); 423 424 }); 425 426 //Check for custom points 427 if(vp_woo_pont_frontend_params.custom_points) { 428 429 //Only custom providers 430 var custom_points = vp_woo_pont_frontend_params.custom_points.filter(obj => { 431 return obj.provider === 'custom' && !obj.hidden 432 }); 433 vp_woo_pont_frontend.process_provider('custom', custom_points); 434 435 //Refresh sidebar 436 vp_woo_pont_frontend.syncSidebar(); 437 } 438 439 }, 440 find_custom_point_override: function(data, provider) { 441 var custom_points = vp_woo_pont_frontend_params.custom_points; 442 if(custom_points) {
443 custom_points.forEach((item, i) => { 444 if(item.provider == provider && item.id == data.id) { 445 data = item; 446 447 if(item.hidden) { 448 data = false; 449 } 450 } 451 }); 452 } 453 return data; 454 }, 455 process_provider: function(provider, data) { 456 var allDocuments = []; 457 var markers = []; 458 var batchSize = 4000; // Process 4000 markers at a time 459 var currentIndex = 0; 460 var features = []; 461 462 // Show loading indicator 463 vp_woo_pont_frontend.$modal.addClass('loading-markers'); 464 465 function processBatch() { 466 var endIndex = Math.min(currentIndex + batchSize, data.length); 467 var batchDocuments = []; 468 var batchMarkers = []; 469 470 for (var i = currentIndex; i < endIndex; i++) { 471 var a = data[i]; 472 473 // Process marker data 474 if(a.country && a.country != 'HU' && a.country != 'hu') { 475 a.city_nfd = a.city; 476 a.addr_nfd = a.addr; 477 } else { 478 a.city_nfd = a.city.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 479 a.addr_nfd = a.addr.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 480 } 481 482 // Only create marker if group exists 483 var targetGroup = null; 484 if(provider == 'postapont') { 485 a.provider = provider+'_'+a.group; 486 targetGroup = vp_woo_pont_frontend.groups[provider+'_'+a.group]; 487 } else { 488 a.provider = provider; 489 targetGroup = vp_woo_pont_frontend.groups[provider]; 490 } 491 492 if(targetGroup) { 493 var marker = L.marker(new L.LatLng(a.lat, a.lon), { 494 data: a, 495 riseOnHover: true // Better performance than default 496 }); 497 a.marker_id = L.stamp(marker); 498 marker.on('click', vp_woo_pont_frontend.select_in_map); 499 500 //If the point has a features array, add feature classes to the marker for styling 501 if(a.features && Array.isArray(a.features)) { 502 var classes = 'vp-woo-pont-marker '+a.provider; 503 a.features.forEach(function(feature){ 504 if(features.includes(feature) == false) { 505 features.push(feature); 506 } 507 classes += " feature-"+feature; 508 }); 509 marker.setIcon(L.divIcon({html: '<div><i class="vp-woo-pont-provider-icon-'+a.provider+'"></i></div>', className: classes, iconSize: [48, 55], iconAnchor: [24, 52]})); 510 } else { 511 marker.setIcon(vp_woo_pont_frontend.markerIcons[a.provider]); 512 } 513 514 batchMarkers.push({marker: marker, group: targetGroup}); 515 batchDocuments.push(a); 516 } 517 } 518 519 // Add markers to groups in batch 520 batchMarkers.forEach(function(item) { 521 item.marker.addTo(item.group); 522 }); 523 524 // Add to collections 525 markers = markers.concat(batchMarkers); 526 allDocuments = allDocuments.concat(batchDocuments); 527 528 currentIndex = endIndex; 529 530 // Update progress (optional) 531 var progress = Math.round((currentIndex / data.length) * 100); 532 console.log(`Processing markers: ${progress}%`); 533 534 if (currentIndex < data.length) { 535 // Continue processing in next frame 536 requestAnimationFrame(processBatch); 537 } else { 538 // Finished processing 539 finishProcessing(); 540 } 541 542 //Set features 543 vp_woo_pont_frontend.features[provider] = features; 544 545 console.log(vp_woo_pont_frontend.features); 546 547 } 548 549 function finishProcessing() { 550 // Add groups to map 551 if(provider == 'postapont') { 552 var groups = ['10', '20', '30', '50', '70']; 553 groups.forEach(function(group){ 554 if(vp_woo_pont_frontend.groups[provider+'_'+group]) { 555 vp_woo_pont_frontend.groups[provider+'_'+group].addTo(vp_woo_pont_frontend.$map); 556 } 557 }); 558 } else { 559 if(vp_woo_pont_frontend.groups[provider]) { 560 vp_woo_pont_frontend.groups[provider].addTo(vp_woo_pont_frontend.$map); 561 } 562 } 563 564 // Add to search index 565 vp_woo_pont_frontend.search.addDocuments(allDocuments); 566 vp_woo_pont_frontend.hide_unwanted_providers(); 567 568 // Remove loading indicator 569 vp_woo_pont_frontend.$modal.removeClass('loading-markers'); 570 571 // Refresh sidebar 572 vp_woo_pont_frontend.syncSidebar(); 573 } 574 575 // Start processing 576 requestAnimationFrame(processBatch); 577 }, 578 toggle_layers: function() { 579 var id = $(this).attr('id'); 580 581 // Check if this is a feature filter 582 if(id.startsWith('feature-')) { 583 var featureId = id.replace('feature-', ''); 584 var provider = $(this).parent().data('provider'); 585 var filteredGroupKey = provider + '_' + id; 586 587 if($(this).prop('checked')) { 588 // Create filtered group if it doesn't exist 589 if(!vp_woo_pont_frontend.groups[filteredGroupKey]) { 590 var filteredGroup = L.featureGroup.subGroup(vp_woo_pont_frontend.markerClusters); 591 592 // Copy markers that have the required feature 593 vp_woo_pont_frontend.groups[provider].eachLayer(function(marker) { 594 var pointData = marker.options.data; 595 var hasFeature = pointData.features && Array.isArray(pointData.features) && pointData.features.includes(featureId); 596 597 if(hasFeature) { 598 filteredGroup.addLayer(marker); 599 } 600 }); 601 602 vp_woo_pont_frontend.groups[filteredGroupKey] = filteredGroup; 603 } 604 605 // Hide regular group, show filtered group 606 vp_woo_pont_frontend.$map.removeLayer(vp_woo_pont_frontend.groups[provider]); 607 vp_woo_pont_frontend.$map.addLayer(vp_woo_pont_frontend.groups[filteredGroupKey]); 608 } else { 609 // Hide filtered group, show regular group 610 if(vp_woo_pont_frontend.groups[filteredGroupKey]) { 611 vp_woo_pont_frontend.$map.removeLayer(vp_woo_pont_frontend.groups[filteredGroupKey]); 612 } 613 vp_woo_pont_frontend.$map.addLayer(vp_woo_pont_frontend.groups[provider]); 614 } 615 } else { 616 // Handle regular provider toggle 617 var provider = id.replace('provider-', ''); 618 var $allProviderCheckboxes = vp_woo_pont_frontend.$filters.find('li:not(.filter-feature) input[type="checkbox"]'); 619 var allCheckedBefore = true; 620 $allProviderCheckboxes.each(function() { 621 if($(this).attr('id') === id) { 622 // This one just changed, so its state is already toggled; check the opposite 623 if($(this).prop('checked')) allCheckedBefore = allCheckedBefore && false; 624 } else { 625 if(!$(this).prop('checked')) allCheckedBefore = false;
626 } 627 }); 628 629 if(allCheckedBefore && !$(this).prop('checked')) { 630 // All were checked before this click â solo-select the clicked provider 631 $(this).prop('checked', true); 632 $allProviderCheckboxes.each(function() { 633 var pid = $(this).attr('id').replace('provider-', ''); 634 if($(this).attr('id') !== id) { 635 $(this).prop('checked', false); 636 vp_woo_pont_frontend.$map.removeLayer(vp_woo_pont_frontend.groups[pid]); 637 } else { 638 vp_woo_pont_frontend.$map.addLayer(vp_woo_pont_frontend.groups[pid]); 639 } 640 }); 641 } else { 642 if($(this).prop('checked')) { 643 vp_woo_pont_frontend.$map.addLayer(vp_woo_pont_frontend.groups[provider]); 644 } else { 645 vp_woo_pont_frontend.$map.removeLayer(vp_woo_pont_frontend.groups[provider]); 646 } 647 } 648 } 649 650 //Refresh enabled provider list 651 vp_woo_pont_frontend.enabled_filters = []; 652 vp_woo_pont_frontend.$filters.find('input:checked').each(function(){ 653 var provider = $(this).parent().data('provider'); 654 vp_woo_pont_frontend.enabled_filters.push(provider); 655 }); 656 657 //Refresh sidebar 658 vp_woo_pont_frontend.syncSidebar(); 659 }, 660 setup_provider_filters: function(providers, country) { 661 vp_woo_pont_frontend.$filters.html(''); //reset content 662 vp_woo_pont_frontend.enabled_filters = []; 663 664 //Loop throguh providers and create list items 665 Object.keys(providers).forEach(function(provider_id){ 666 var provider = providers[provider_id]; 667 668 //Set cost 669 var cost = provider.formatted_net; 670 if(vp_woo_pont_frontend_params.prices_including_tax) cost = provider.formatted_gross 671 672 //Render HTML 673 var list_item = $('<li data-provider="'+provider_id+'"><input type="checkbox" checked id="provider-'+provider_id+'"><label for="provider-'+provider_id+'"><i class="vp-woo-pont-provider-icon-'+provider_id+'"></i><strong>'+provider.label+'</strong><em>'+cost+'</em></label></li>'); 674 vp_woo_pont_frontend.enabled_filters.push(provider_id); 675 vp_woo_pont_frontend.$filters.append(list_item); 676 }); 677 678 //If theres only one provider, just hide it, no need for the filters 679 if(Object.keys(providers).length == 1) { 680 vp_woo_pont_frontend.$filters.hide(); 681 } else { 682 vp_woo_pont_frontend.$filters.show(); 683 } 684 685 //Setup features filters too 686 var available_providers = Object.keys(providers); 687 Object.keys(vp_woo_pont_frontend_params.features).forEach(function(feature_id){ 688 var feature = vp_woo_pont_frontend_params.features[feature_id]; 689 690 if(!available_providers.includes(feature_id)) { 691 return; //skip this feature 692 } 693 694 //Check for country support 695 if(feature.countries && Array.isArray(feature.countries)) { 696 if(!feature.countries.includes(country)) { 697 return; //skip this feature 698 } 699 } 700 701 //Render HTML 702 var list_item = $('<li data-provider="'+feature_id+'" class="filter-feature" data-feature="'+feature.id+'"><input type="checkbox" id="feature-'+feature.id+'"><label for="feature-'+feature.id+'"><i class="vp-woo-pont-feature-icon-'+feature.id+'"></i><strong>'+feature.label+'</strong><small>'+feature.description+'</small></label></li>'); 703 vp_woo_pont_frontend.$filters.append(list_item); 704 }); 705 706 707 }, 708 hide_unwanted_providers: function() { 709 Object.keys(vp_woo_pont_frontend.groups).forEach(function(group){ 710 if(!Object.keys(vp_woo_pont_frontend.providers).includes(group)) { 711 vp_woo_pont_frontend.$map.removeLayer(vp_woo_pont_frontend.groups[group]); 712 } else { 713 vp_woo_pont_frontend.$map.addLayer(vp_woo_pont_frontend.groups[group]); 714 } 715 }); 716 }, 717 syncSidebar: function() { 718 719 //Clear the list 720 vp_woo_pont_frontend.$list.html(''); 721 722 //Create new array 723 //TODO: sort ascending 724 725 //Get checked provider filters (not feature filters) 726 vp_woo_pont_frontend.$filters.find('input:checked').not('[id^="feature-"]').each(function(){ 727 var provider = $(this).parent().data('provider'); 728 var provider_limit = 0; 729 730 // Check if there's an active feature filter for this provider 731 var activeGroup = provider; 732 vp_woo_pont_frontend.$filters.find('input[id^="feature-"]:checked').each(function(){ 733 var featureProvider = $(this).parent().data('provider'); 734 if(featureProvider === provider) { 735 var featureId = $(this).attr('id'); 736 activeGroup = provider + '_' + featureId; 737 return false; // break loop 738 } 739 }); 740 741 //Loop through layers(only the ones that are checked in the filter) and in the viewport and append to the list. 742 //Only append 10 / group for performance reasons, i think its still usable this way 743 //Only change the sidebar, if a point is not selected already 744 if(vp_woo_pont_frontend.groups && vp_woo_pont_frontend.groups[activeGroup]) { 745 vp_woo_pont_frontend.groups[activeGroup].eachLayer(function (layer) { 746 if (vp_woo_pont_frontend.$map.getBounds().contains(layer.getLatLng())) {
747 if(provider_limit < 10) { 748 layer.options.data.marker_id = L.stamp(layer); 749 vp_woo_pont_frontend.add_to_sidebar(layer.options.data, provider); 750 } 751 provider_limit++; 752 } 753 }); 754 } 755 756 }); 757 758 }, 759 show_search_results: function(keyword) { 760 761 //Remove accents for more accurate results 762 keyword = keyword.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 763 var search_results = vp_woo_pont_frontend.search.search(keyword); 764 765 //On search, remove selected point 766 vp_woo_pont_frontend.selected_point = false; 767 768 //Set helper classes for styling and mobile 769 vp_woo_pont_frontend.$modal.removeClass('point-selected'); 770 771 //Remove if provider not selected 772 if(vp_woo_pont_frontend.enabled_filters) { 773 search_results = search_results.filter(result => vp_woo_pont_frontend.enabled_filters.includes(result.provider)); 774 } 775 776 if(search_results.length == 0) { 777 778 //Add helper classes 779 vp_woo_pont_frontend.$modal.addClass('no-search-result'); 780 vp_woo_pont_frontend.$modal.removeClass('has-search-result'); 781 782 //Add no results message 783 var list_item = $('#vp-woo-pont-modal-no-result-sample').clone(); 784 vp_woo_pont_frontend.$list.find('.vp-woo-pont-modal-sidebar-no-result').remove(); 785 vp_woo_pont_frontend.$list.prepend(list_item); 786 787 } else { 788 789 //Add helper classes 790 vp_woo_pont_frontend.$modal.removeClass('no-search-result'); 791 vp_woo_pont_frontend.$modal.addClass('has-search-result'); 792 793 //Clear the list 794 vp_woo_pont_frontend.$list.html(''); 795 796 //Limit to max 100 search results 797 if (search_results.length > 100) search_results.length = 100; 798 799 //Add results one by one 800 //// TODO: do this with vanilla js, so its a lit faster 801 search_results.forEach(function(result){ 802 vp_woo_pont_frontend.add_to_sidebar(result, result.provider); 803 }); 804 805 } 806 }, 807 add_to_sidebar(item, provider) { 808 809 //// TODO: do this with vanilla js, so its a little faster 810 var list_item = $('#vp-woo-pont-modal-list-item-sample').clone(); 811 var provider_price = vp_woo_pont_frontend.providers[provider]; 812 813 //If theres no price, skip 814 if(!provider_price) { 815 return; 816 } 817 818 list_item.removeAttr('id'); 819 list_item.find('.name').text(item.name); 820 list_item.find('.addr').text(item.addr+', '+item.zip+' '+item.city); 821 if(vp_woo_pont_frontend_params.prices_including_tax) { 822 list_item.find('.cost').html(provider_price.formatted_gross); 823 } else { 824 list_item.find('.cost').html(provider_price.formatted_net); 825 } 826 827 list_item.find('.comment').text(item.comment); 828 list_item.attr('data-provider', provider); 829 list_item.attr('data-id', item.id); 830 list_item.attr('data-marker-id', item.marker_id); 831 list_item.attr('data-country', item.country || 'HU'); 832 list_item.find('.icon').addClass('vp-woo-pont-provider-icon-'+provider); 833 if(item.hasOwnProperty('cod') && !item.cod) { 834 list_item.find('.cod-notice').addClass("show"); 835 } 836 837 //Show extra note if needed 838 if(vp_woo_pont_frontend.notes && vp_woo_pont_frontend.notes[provider]) { 839 list_item.find('.comment').prepend('<div class="extra-note">'+vp_woo_pont_frontend.notes[provider]+'</div>'); 840 } 841 842 //Show extra features 843 if(item.features && Array.isArray(item.features)) { 844 item.features.forEach(function(feature){ 845 list_item.addClass('feature-'+feature); 846 }); 847 } 848 849 //Setup opening hours 850 if(vp_woo_pont_frontend_params.open_hours && item.hours) { 851 if(typeof item.hours === 'string') { 852 list_item.find('.open-hours .value').text(item.hours); 853 } else { 854 for (var key in item.hours) { 855 var index = key-1; 856 if (item.hours.hasOwnProperty(key)) { 857 if(item.hours[key]) { 858 list_item.find('.open-hours li:eq('+index+') .value').text(item.hours[key]); 859 } 860 } 861 } 862 } 863 list_item.find('.open-hours').addClass('has-hours'); 864 } 865 866 //Check if need to be selected 867 var selected = false; 868 if(vp_woo_pont_frontend.selected_point && vp_woo_pont_frontend.selected_point.id == item.id && vp_woo_pont_frontend.selected_point.provider == provider) { 869 list_item.addClass('selected'); 870 selected = true; 871 } 872 873 //If its the selected one, move that to the top of the list 874 if(selected) { 875 //Check if already on the list 876 if(vp_woo_pont_frontend.$list.find('li.selected').length < 1) { 877 vp_woo_pont_frontend.$list.prepend(list_item); 878 } 879 } else { 880 vp_woo_pont_frontend.$list.append(list_item); 881 } 882 883 }, 884 select_in_sidebar: function() { 885 886 //If filter is not turned on, turn it on so the layer is visible on the map 887 var provider = $(this).data('provider'); 888 if(!vp_woo_pont_frontend.$filters.find('input#provider-'+provider).is(':checked')) { 889 vp_woo_pont_frontend.$filters.find('input#provider-'+provider).prop('checked', true); 890 vp_woo_pont_frontend.$filters.find('input#provider-'+provider).trigger('change'); 891 } 892 893 //Add selected class 894 vp_woo_pont_frontend.$list.find('li.selected').removeClass('selected'); 895 $(this).addClass('selected'); 896 897 //Remove active marker class 898 $('.leaflet-marker-icon.selected').removeClass('selected'); 899 900 //Store selected value 901 vp_woo_pont_frontend.selected_point = { 902 provider: provider, 903 id: $(this).data('id'), 904 marker_id: parseInt($(this).data('marker-id'), 10) 905 } 906 907 //For developers to run extra stuff 908 $( document.body ).trigger( 'vp_woo_pont_modal_point_picked' ); 909 910 //Set helper classes for styling and mobile 911 vp_woo_pont_frontend.$modal.addClass('point-selected'); 912 vp_woo_pont_frontend.$modal.removeClass('no-search-result'); 913 vp_woo_pont_frontend.$modal.removeClass('has-search-result'); 914 915 //Focus on map 916 var layer = vp_woo_pont_frontend.markerClusters.getLayer(vp_woo_pont_frontend.selected_point.marker_id);
917 vp_woo_pont_frontend.$map.setView([layer.getLatLng().lat, layer.getLatLng().lng], 17); 918 $(layer._icon).addClass('selected'); 919 920 setTimeout(function() { 921 $(layer._icon).addClass('selected'); 922 }, 300); 923 924 vp_woo_pont_frontend.clickedMarker = layer; 925 926 return false; 927 }, 928 select_in_map: function(e) { 929 930 //Remove active marker class 931 $('.leaflet-marker-icon.selected').removeClass('selected'); 932 933 //Add selected class 934 var layer = vp_woo_pont_frontend.markerClusters.getLayer(e.target.options.data.marker_id); 935 $(layer._icon).addClass('selected'); 936 937 //Store selected value 938 vp_woo_pont_frontend.selected_point = { 939 provider: e.target.options.data.provider, 940 id: e.target.options.data.id, 941 marker_id: e.target.options.data.marker_id 942 } 943 944 //For developers to run extra stuff 945 $( document.body ).trigger( 'vp_woo_pont_modal_point_picked' ); 946 947 //Set helper classes for styling and mobile 948 vp_woo_pont_frontend.$modal.addClass('point-selected'); 949 vp_woo_pont_frontend.$modal.removeClass('no-search-result'); 950 vp_woo_pont_frontend.$modal.removeClass('has-search-result'); 951 952 //Move map to center 953 vp_woo_pont_frontend.$map.setView(e.latlng); 954 955 //Sync sidebar, which wills how results from the viewport from the map, which includes the selected point anyway 956 vp_woo_pont_frontend.syncSidebar(); 957 958 //Make sure the selected point is always in the sidebar, at the beginning 959 vp_woo_pont_frontend.add_to_sidebar(e.target.options.data, e.target.options.data.provider); 960 961 //Scroll to the top of the list, so the selected point is visible 962 vp_woo_pont_frontend.$list.animate({scrollTop: 0}); 963 964 }, 965 select_pickup_point: function() { 966 967 //Collect data 968 var $item = $(this).parent(); 969 var provider = $item.data('provider'); 970 var id = $item.data('id'); 971 var page = 'checkout'; 972 var security = '' 973 var ajax_url = ''; 974 if($('body').hasClass('woocommerce-cart')) page = 'cart'; 975 976 //Set based on page 977 ajax_url = vp_woo_pont_frontend_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'vp_woo_pont_select' ); 978 security = vp_woo_pont_frontend_params.nonce; 979 980 //Create reuqest 981 var data = { 982 action: 'vp_woo_pont_select', 983 security: security, 984 provider: provider, 985 id: id, 986 country: $item.data('country') 987 }; 988 989 //If its a my account page 990 if($('.vp-woo-pont-show-map-my-account').length) { 991 data.account = true; 992 } 993 994 //Loading indicator animaiton 995 $item.addClass( 'processing' ); 996 if ($.fn.block) { 997 $item.block({ 998 message: null, 999 overlayCSS: { 1000 background: '#fff', 1001 opacity: 0.6 1002 } 1003 }); 1004 } 1005 1006 //Make ajax request 1007 $.post(ajax_url, data, function(response) { 1008 1009 //Remove loading indicator 1010 if ($.fn.block) { 1011 $item.unblock(); 1012 } 1013 $item.removeClass('processing'); 1014 1015 //TODO handle error 1016 if(response.success) { 1017 1018 // Refresh the review order section, so the selected point is visible automatically 1019 $('body').trigger('update_checkout'); 1020 1021 //Update cart 1022 if($('.cart_totals input.shipping_method:checked').length) { 1023 $('.cart_totals input.shipping_method:checked').trigger( 'change' ); 1024 } else { 1025 $('.cart_totals input.shipping_method').trigger( 'change' ); //This is when the only available option is the pont shipping one 1026 } 1027 1028 //Hide the modal window 1029 vp_woo_pont_frontend.hide_modal(); 1030 1031 //Store it locally too 1032 vp_woo_pont_frontend.selected_pickup_point = response.data.point; 1033 1034 //For the checkout block 1035 localStorage.setItem('selectedMapDetails', JSON.stringify(response.data.point)); 1036 1037 //For developers to run extra stuff 1038 $( document.body ).trigger( 'vp_woo_pont_modal_point_selected' ); 1039 1040 //If its a my account page 1041 if($('.vp-woo-pont-show-map-my-account').length) { 1042 $('.vp-woo-pont-review-order-selected').show(); 1043 $('.vp-woo-pont-review-order-selected strong').text(response.data.point.name); 1044 $('.vp-woo-pont-review-order-selected i').addClass('vp-woo-pont-provider-icon-'+response.data.point.provider); 1045 } 1046 1047 } else { 1048 1049 } 1050 1051 }); 1052 1053 return false;
1054 }, 1055 on_cart_updated: function() { 1056 var selected_method = $('.cart_totals .shipping_method:checked').val(); 1057 1058 //Means theres only one shipping method, so its not a checkbox, but check if its the pont one 1059 if(!selected_method) selected_method = $('.cart_totals .shipping_method').val(); 1060 1061 if(selected_method) { 1062 if(selected_method.includes('vp_pont')) { 1063 $('.woocommerce-shipping-destination').hide(); 1064 $('.shipping-calculator-button').hide(); 1065 } else { 1066 $('.woocommerce-shipping-destination').show(); 1067 $('.shipping-calculator-button').show(); 1068 } 1069 } 1070 }, 1071 geolocate: function() { 1072 var selected_postcode = $('#billing_postcode').val(); 1073 var selected_country = vp_woo_pont_frontend.billing_country || 'HU'; 1074 var selected_state = ''; 1075 if(selected_postcode && selected_country == 'HU') { 1076 selected_postcode = parseInt(selected_postcode); 1077 $.each(zipCodes.vp_woo_pont_state_postcodes, function(state, postcodes) { 1078 if(postcodes.includes(selected_postcode)) { 1079 selected_state = state; 1080 return false; 1081 } 1082 }); 1083 } 1084 1085 if(selected_state && zipCodes.vp_woo_pont_state_coordinates[selected_state] !== undefined) { 1086 var selected_state_coordinates = zipCodes.vp_woo_pont_state_coordinates[selected_state]; 1087 vp_woo_pont_frontend.$map.setView(selected_state_coordinates, 10); 1088 } else { 1089 1090 var default_center_position = vp_woo_pont_frontend_params.default_center_position; 1091 1092 //Center billing country 1093 if(selected_country && zipCodes.vp_woo_pont_country_coordinates[selected_country] !== undefined) { 1094 default_center_position = zipCodes.vp_woo_pont_country_coordinates[selected_country]; 1095 } 1096 1097 vp_woo_pont_frontend.$map.setView(default_center_position, 8); 1098 } 1099 }, 1100 isiOS: function() { 1101 return [ 1102 'iPad Simulator', 1103 'iPhone Simulator', 1104 'iPod Simulator', 1105 'iPad', 1106 'iPhone', 1107 'iPod' 1108 ].includes(navigator.platform) 1109 // iPad on iOS 13 detection 1110 || (navigator.userAgent.includes("Mac") && "ontouchend" in document) 1111 } 1112 } 1113 1114 if($('.vp-woo-pont-modal').length) { 1115 vp_woo_pont_frontend.init(); 1116 1117 //Init as a global variable 1118 window.vp_woo_pont_frontend = vp_woo_pont_frontend; 1119 1120 } 1121}); 1122 1123 1124 1125},{"./zip-codes.js":2,"body-scroll-lock":3,"js-search":4,"leaflet":7,"leaflet.featuregroup.subgroup":5,"leaflet.markercluster":6}],2:[function(require,module,exports){ 1126//Simple geolocate function based on ZIP codes, just to zoom in on the state 1127const vp_woo_pont_state_postcodes = {'BU':[1011,1012,1013,1014,1015,1016,1021,1022,1023,1024,1025,1026,1027,1028,1029,1031,1032,1033,1034,1035,1036,1037,1038,1039,1041,1042,1043,1044,1045,1046,1047,1048,1051,1052,1053,1054,1055,1056,1061,1062,1063,1064,1065,1066,1067,1068,1069,1071,1072,1073,1074,1075,1076,1077,1078,1081,1082,1083,1084,1085,1086,1087,1088,1089,1091,1092,1093,1094,1095,1096,1097,1098,1101,1102,1103,1104,1105,1106,1107,1108,1111,1112,1113,1114,1115,1116,1117,1118,1119,1121,1122,1123,1124,1125,1126,1131,1132,1133,1134,1135,1136,1137,1138,1139,1141,1142,1143,1144,1145,1146,1147,1148,1149,1151,1152,1153,1154,1155,1156,1157,1158,1161,1162,1163,1164,1165,1171,1172,1173,1174,1181,1182,1183,1184,1185,1186,1188,1191,1192,1193,1194,1195,1196,1201,1202,1203,1204,1205,1211,1212,1213,1214,1215,1221,1222,1223,1224,1225,1237,1238,1239,1529],'PE':[2000,2009,2011,2014,2015,2016,2017,2021,2022,2023,2024,2025,2026,2030,2035,2036,2040,2049,2051,2053,2071,2072,2074,2080,2081,2084,2085,2086,2087,2089,2092,2093,2094,2095,2097,2098,2099,2100,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2142,2143,2144,2145,2163,2164,2165,2167,2170,2173,2174,2181,2183,2184,2185,2191,2192,2193,2194,2209,2211,2212,2213,2214,2215,2216,2217,2220,2233,2234,2235,2251,2252,2253,2254,2255,2300,2310,2314,2315,2317,2318,2319,2321,2322,2330,2335,2336,2337,2338,2339,2340,2345,2347,2351,2360,2363,2365,2366,2367,2370,2371,2373,2375,2376,2378,2381,2440,2461,2510,2600,2613,2614,2615,2621,2623,2626,2627,2628,2629,2631,2632,2633,2634,2635,2637,2638,2639,2681,2683,2700,2711,2712,2713,2721,2723,2724,2730,2735,2736,2737,2738,2740,2746,2747,2760,2764,2765,2766,2767,2768,2769,2898,3356,3604,3630,3647,3775,3874,3905,3906,4836,6332,6781,7092,7163,7212,7954,7981,8193,8292,8321,8351,8357,8619,8640,8873,8881,8929,9651,9707,9791,9825],'VA':[2013,2027,2038,2039,2182,2532,2745,3212,3422,3462,3622,3623,3672,3757,3762,3795,3933,3992,4274,4761,4821,4942,4971,5062,5065,5091,5324,5622,6422,6784,7300,7385,7511,7517,7678,7763,7833,7847,7935,7957,8122,8428,8863,9325,9354,9500,9511,9514,9515,9516,9517,9521,9522,9523,9531,9541,9544,9545,9547,9548,9549,9551,9552,9553,9554,9555,9556,9561,9600,9608,9609,9611,9622,9623,9624,9625,9631,9632,9633,9634,9635,9641,9643,9652,9653,9654,9661,9662,9663,9664,9665,9671,9672,9673,9674,9683,9684,9685,9721,9722,9724,9725,9726,9733,9734,9735,9737,9739,9741,9742,9743,9744,9745,9746,9747,9748,9751,9752,9754,9756,9757,9761,9762,9763,9764,9766,9771,9772,9774,9775,9776,9777,9781,9782,9783,9784,9789,9792,9793,9794,9795,9796,9797,9798,9799,9800,9811,9812,9813,9814,9821,9823,9826,9831,9832,9833,9834,9835,9836,9841,9842,9900,9909,9912,9915,9917,9918,9919,9921,9922,9923,9931,9932,9934,9936,9937,9938,9941,9942,9944,9945,9946,9951,9952,9953,9955,9961,9962,9970,9981,9982,9983,9985],'KE':[2028,2067,2146,2225,2242,2500,2509,2517,2518,2519,2522,2523,2524,2525,2526,2527,2529,2531,2533,2534,2536,2537,2541,2543,2544,2545,2800,2821,2823,2824,2831,2832,2833,2834,2835,2836,2837,2852,2854,2856,2858,2859,2861,2862,2870,2879,2881,2882,2883,2884,2885,2886,2887,2888,2890,2897,2899,2911,2931,2941,2942,2943,2944,2945,2946,2947,2948,2949,3374,3910,4145,4965,5536,5539,7200,7334,7668,8625,8736],'ZA':[2045,2161,2241,2610,2644,2671,2698,2889,3263,3574,3716,3721,3765,3886,5125,5624,5920,6077,6640,7025,7272,7285,7386,7695,7761,7966,8072,8313,8314,8315,8316,8341,8353,8354,8355,8356,8360,8371,8372,8373,8380,8391,8392,8393,8394,8429,8475,8477,8479,8563,8595,8716,8741,8742,8743,8745,8746,8747,8749,8751,8752,8753,8754,8756,8761,8762,8764,8765,8767,8771,87
112772,8773,8774,8776,8777,8778,8782,8784,8785,8788,8789,8790,8792,8793,8795,8797,8798,8799,8800,8808,8809,8821,8822,8824,8825,8827,8831,8834,8835,8855,8856,8861,8862,8866,8868,8872,8874,8879,8882,8883,8885,8886,8887,8888,8891,8893,8900,8911,8912,8913,8914,8915,8917,8918,8919,8921,8923,8924,8925,8931,8932,8934,8935,8936,8943,8944,8945,8946,8947,8948,8949,8951,8953,8954,8956,8957,8958,8960,8966,8969,8971,8973,8975,8976,8977,8978,8981,8983,8984,8985,8986,8988,8990,8991,8992,8994,8995,8996,8997,8998,8999,9235,9300,9324,9542,9612,9621,9636,9727,9738],'FE':[2060,2063,2064,2065,2066,2091,2400,2407,2421,2422,2423,2424,2425,2426,2427,2428,2431,2432,2433,2434,2435,2451,2453,2454,2455,2456,2457,2459,2462,2465,2471,2472,2473,2475,2476,2477,2481,2483,2484,2485,2490,3024,3787,3864,4241,4763,4954,5725,6755,6785,7000,7003,7011,7012,7013,7014,7015,7016,7017,7018,7019,7041,7672,7751,7854,8000,8019,8043,8044,8045,8051,8052,8054,8055,8056,8065,8071,8073,8081,8082,8083,8086,8087,8088,8089,8092,8095,8096,8097,8111,8112,8121,8124,8125,8126,8127,8128,8130,8131,8132,8133,8135,8136,8137,8138,8139,8141,8143,8144,8145,8146,8151,8153,8154,8155,8156,8286,8558,9681],'BZ':[2073,2132,2162,2316,2463,2535,2612,2624,2699,2822,2840,2921,3154,3243,3327,3328,3400,3413,3416,3417,3418,3421,3423,3424,3425,3431,3432,3433,3434,3441,3442,3443,3444,3450,3458,3459,3461,3463,3464,3465,3466,3467,3500,3501,3508,3510,3515,3516,3517,3518,3519,3521,3524,3525,3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3552,3553,3554,3555,3556,3559,3561,3563,3564,3565,3571,3572,3573,3575,3576,3578,3579,3580,3586,3587,3588,3589,3592,3593,3594,3595,3596,3597,3598,3599,3608,3626,3635,3636,3641,3642,3643,3644,3645,3646,3648,3652,3653,3654,3655,3656,3657,3659,3663,3664,3671,3700,3704,3711,3712,3713,3714,3715,3717,3718,3720,3722,3723,3724,3726,3728,3729,3733,3735,3741,3742,3743,3744,3751,3752,3753,3754,3755,3756,3758,3759,3761,3768,3770,3773,3776,3777,3778,3779,3780,3783,3786,3791,3792,3793,3796,3800,3809,3811,3814,3815,3816,3817,3825,3826,3831,3832,3833,3834,3836,3837,3841,3842,3843,3844,3846,3847,3848,3851,3852,3853,3854,3855,3860,3863,3865,3866,3871,3873,3875,3876,3881,3882,3887,3888,3891,3892,3896,3897,3898,3899,3900,3903,3904,3907,3908,3909,3915,3916,3917,3918,3921,3922,3923,3924,3925,3926,3927,3928,3929,3931,3932,3935,3936,3942,3943,3944,3945,3950,3952,3955,3956,3957,3958,3959,3961,3962,3963,3964,3965,3967,3971,3972,3973,3974,3976,3977,3978,3980,3985,3987,3988,3989,3991,3993,3996,3997,4075,4114,4133,4622,4826,4914,5222,5440,5900,5903,5904,5905,6795,7045,7052,7083,7150,7158,7187,7228,7281,7305,7370,7383,7439,7681,7682,7716,7973,8074,8157,8458,8644,8695,8719,8876,8895,8896,9061,9071,9073,9134,9152,9184,9464,9682,9719,9736,9773],'SO':[2083,2151,2521,2625,2855,2903,3016,3348,3384,3388,3411,3562,3577,3603,3734,3769,3813,3821,3893,3894,4177,4231,4373,4565,5008,5212,5556,6341,7086,7191,7253,7255,7256,7258,7261,7271,7274,7275,7276,7279,7282,7284,7286,7394,7400,7431,7432,7434,7435,7436,7441,7442,7443,7444,7452,7453,7454,7456,7457,7458,7463,7464,7465,7471,7472,7473,7474,7476,7477,7478,7479,7500,7512,7513,7514,7515,7516,7521,7522,7523,7525,7527,7530,7532,7533,7535,7536,7538,7542,7543,7544,7551,7552,7555,7556,7557,7561,7562,7563,7564,7570,7582,7584,7
1127585,7587,7588,7589,7918,7921,7922,7924,7976,7979,7987,7988,8041,8455,8495,8600,8609,8611,8612,8613,8614,8618,8621,8622,8623,8624,8626,8628,8630,8636,8637,8638,8646,8647,8648,8649,8651,8652,8653,8654,8656,8658,8660,8666,8667,8668,8671,8672,8673,8674,8675,8676,8681,8683,8684,8685,8691,8692,8693,8694,8696,8698,8699,8705,8706,8707,8708,8709,8710,8711,8712,8713,8714,8717,8718,8721,8722,8723,8724,8725,8726,8728,8731,8732,8733,8735,8737,8738,8739,8840,8849,8851,8853,8865,9131,9167,9676,9740,9935],'VE':[2096,2131,2243,3022,3066,3176,3345,3600,3621,3662,4275,4623,4755,4941,5000,5200,5449,5743,5919,6230,6445,6648,6912,7227,7541,7728,7756,7960,8085,8100,8103,8104,8105,8109,8142,8163,8164,8171,8172,8174,8175,8181,8182,8183,8184,8192,8194,8195,8196,8200,8220,8225,8226,8227,8228,8229,8230,8233,8236,8237,8241,8242,8243,8245,8247,8248,8251,8252,8253,8254,8255,8256,8257,8258,8261,8262,8263,8264,8265,8271,8272,8275,8281,8282,8283,8284,8291,8294,8295,8296,8297,8300,8308,8311,8312,8318,8319,8344,8345,8347,8348,8349,8352,8400,8411,8412,8413,8415,8416,8417,8418,8419,8420,8422,8423,8424,8427,8430,8431,8432,8433,8435,8438,8439,8440,8441,8442,8443,8444,8445,8446,8447,8448,8449,8451,8452,8454,8456,8457,8460,8469,8471,8473,8474,8476,8478,8481,8482,8483,8484,8485,8491,8492,8493,8494,8496,8497,8500,8511,8512,8513,8514,8515,8516,8517,8518,8521,8522,8523,8531,8532,8533,8541,8542,8543,8551,8552,8554,8555,8556,8557,8561,8562,8564,8565,8571,8572,8581,8582,8591,8592,8593,8594,8596,8597,8598,8697,8700,8858,9147,9451,9533,9534,9535,9913],'JN':[2133,2134,3651,5051,5052,5053,5054,5055,5061,5063,5064,5071,5081,5082,5083,5084,5085,5092,5093,5094,5095,5100,5111,5121,5122,5123,5124,5126,5130,5135,5136,5137,5141,5142,5143,5144,5152,5211,5213,5231,5232,5233,5234,5235,5241,5243,5244,5300,5309,5310,5321,5322,5323,5331,5340,5349,5350,5358,5359,5361,5362,5363,5400,5411,5412,5430,5435,5452,5453,5461,5462,5463,5464,5465,5471,5472,5474,5475,5476,5746,6043,7038,7130,7675,7741,7755,8161,8796,8864,8877],'GS':[2141,2230,2853,3153,3941,3994,4150,4943,4955,6115,6132,6769,6794,7257,7352,7742,7900,7914,8123,8897,9000,9011,9012,9021,9022,9023,9024,9025,9026,9027,9028,9029,9030,9062,9063,9072,9074,9081,9082,9083,9084,9085,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9111,9121,9122,9123,9124,9125,9126,9127,9132,9133,9135,9136,9141,9142,9143,9145,9146,9151,9154,9155,9161,9162,9163,9164,9165,9168,9169,9171,9172,9174,9175,9176,9177,9178,9181,9183,9200,9211,9221,9222,9223,9224,9225,9226,9228,9231,9232,9233,9234,9241,9242,9243,9244,9311,9312,9313,9314,9315,9316,9317,9321,9322,9323,9326,9327,9330,9339,9341,9342,9343,9344,9345,9346,9351,9352,9353,9361,9362,9363,9364,9365,9371,9372,9374,9375,9400,9407,9408,9421,9422,9423,9431,9433,9434,9435,9436,9437,9441,9442,9443,9444,9461,9462,9463,9471,9472,9473,9474,9475,9476,9481,9482,9483,9484,9485,9491,9492,9493,9494,9933],'SZ':[2166,2244,2464,2528,2900,3326,3627,3658,3767,4136,4138,4232,4233,4234,4235,4244,4245,4246,4266,4267,4300,4311,4320,4324,4325,4326,4327,4331,4332,4333,4334,4335,4337,4338,4341,4342,4343,4351,4352,4353,4354,4355,4356,4361,4362,4363,4371,4372,4374,4375,4376,4400,4405,4412,4431,4432,4433,4434,4440,4445,4446,4447,4450,4455,4456,4461,4463,4464,4465,4466,4467,4468,4471,4472,4474,4475,4481,4483,4484,4485,4486,4487,4488,4491,4493,4494,4496,4501,4502,4503,4511,4515,4516,4517,4521,4522,4523,4524,4525,4531,4532,4533,4534,4535,4536,4537,4541,4542,4543,4544,4546,4547,4551,4552,4553,4554,4555,4556,4557,4561,4562,4563,4564,4566,4567,4600,4611,4621,4624,4625,4627,4628,4631,4632,4633,4634,4635,4641,4643,4644,4645,4646,4700,4721,4722,4731,4732,4734,4735,4737,4741,4742,4743,4745,4746,4752,4754,4756,4762,4764,4765,4766,4767,4800,4803,4804,4811,4812,4813,4822,4823,4824,4831,4832,4833,4834,4835,4841,4842,4843,4844,4845,4900,4911,4912,4913,4921,4922,4931,4932,4933,4934,4935,4936,4937,4944,4945,4946,4947,4948,4951,4953,4956,4961,4962,4963,4966,4967,4968,4969,4972,4973,4974,4975,4976,4977,5700,7087,7143,7745,7811,7824,8191,8244,8425,8426,8734,9113,9512,9749,9824,9943,9954],'NO':[2175,2176,2177,2611,2616,2617,2618,2619,2640,2641,2642,2643,2645,2646,2647,2648,2649,2651,2652,2653,2655,2656,2659,2660,2668,2669,2672,2673,2675,2676,2677,2678,2682,2685,2686,2687,2688,2691,2692,2693,2694,2696,2697,3034,3041,3042,3043,3044,3045,3046,3047,3051,3052,3053,3060,3063,3065,3067,3068,3069,3070,3073,3074,3075,3077,3078,3082,3100,3102,3104,3109,3121,3123,3124,3125,3126,3127,3128,3129,3132,3133,3134,3135,3136,3137,3138,3141,3142,3143,3144,3
1127145,3146,3147,3151,3152,3155,3161,3162,3163,3165,3170,3175,3177,3178,3179,3181,3182,3183,3184,3185,3186,3187,3188,3625,3812,3872,3934,3999,4492,7226,7700,7735,8434,9153,9495,9675],'HE':[2200,2377,2658,3000,3011,3013,3015,3023,3031,3032,3035,3036,3200,3213,3214,3231,3232,3233,3234,3235,3240,3242,3244,3245,3246,3247,3248,3250,3252,3253,3254,3255,3256,3257,3258,3259,3261,3262,3264,3265,3271,3272,3273,3274,3275,3281,3282,3283,3284,3291,3292,3293,3294,3295,3296,3300,3304,3321,3322,3323,3324,3325,3331,3332,3334,3336,3337,3341,3343,3344,3346,3347,3349,3350,3351,3352,3353,3354,3355,3357,3358,3359,3360,3369,3371,3373,3375,3377,3378,3379,3381,3382,3383,3385,3386,3387,3390,3394,3395,3396,3397,3398,3399,3414,3557,5516,5745,5836,6200,6758,7064,7132,7142,7539,7545,7720,7972,8553,8878,9723],'BA':[2309,2364,2654,2750,2851,2896,3021,3064,3131,3211,3221,3335,3426,3731,3763,3764,3895,4071,4642,5420,5534,5553,6134,6346,6612,6646,6767,6923,7094,7273,7283,7304,7331,7332,7333,7342,7343,7344,7345,7346,7347,7348,7349,7351,7381,7384,7391,7393,7396,7455,7537,7553,7586,7600,7621,7622,7623,7624,7625,7626,7627,7628,7629,7630,7631,7632,7633,7634,7635,7636,7661,7663,7664,7666,7671,7677,7683,7691,7693,7696,7711,7712,7714,7715,7717,7718,7723,7724,7725,7726,7727,7731,7732,7733,7737,7743,7744,7747,7752,7753,7757,7759,7762,7766,7768,7771,7772,7773,7774,7775,7781,7782,7783,7784,7785,7812,7813,7814,7815,7817,7818,7822,7823,7826,7827,7831,7834,7837,7838,7839,7843,7846,7849,7850,7851,7853,7912,7913,7915,7923,7925,7926,7934,7936,7951,7953,7958,7964,7967,7968,7971,7975,7980,7985,8042,8053,8134,8246,8273,8274,8342,8346,8414,8468,8617,8655,8744,9144,9245,9700],'TO':[2344,2657,2755,3012,3412,3884,3885,3937,3954,4117,4141,4495,4545,6090,7020,7026,7027,7030,7039,7042,7043,7044,7047,7051,7054,7056,7057,7061,7062,7063,7065,7066,7067,7068,7071,7072,7081,7082,7084,7085,7090,7091,7093,7095,7097,7098,7099,7100,7121,7122,7131,7133,7134,7135,7136,7139,7140,7144,7145,7146,7147,7148,7149,7159,7161,7162,7164,7171,7172,7173,7174,7175,7176,7181,7182,7183,7184,7185,7186,7192,7193,7194,7195,7211,7213,7214,7215,7224,7225,7251,7252,7341,7353,7354,7355,7356,7361,7362,7475,7841,7932,7937,7977,8060,8854,9019,9173],'BK':[2458,3368,3551,3591,4135,4336,6000,6008,6031,6032,6033,6034,6035,6041,6044,6045,6050,6055,6060,6062,6064,6065,6066,6067,6070,6075,6076,6078,6080,6085,6086,6087,6088,6096,6097,6098,6100,6111,6112,6113,6114,6116,6120,6131,6133,6136,6211,6221,6222,6223,6224,6236,6237,6238,6239,6300,6320,6323,6325,6326,6327,6328,6331,6333,6334,6336,6337,6342,6343,6344,6345,6347,6348,6351,6352,6353,6400,6411,6412,6413,6414,6421,6423,6424,6425,6430,6435,6440,6444,6446,6447,6448,6449,6451,6452,6453,6454,6455,6456,6500,6503,6511,6512,6513,6521,6522,6523,6524,6525,6527,6528,6782,7524,7526,7754,8162,8330,8409,9086,9112,9532],'HB':[3014,3849,4000,4002,4024,4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4060,4063,4064,4065,4066,4067,4069,4074,4078,4079,4080,4085,4086,4087,4090,4096,4097,4100,4103,4110,4115,4116,4118,4119,4121,4122,4123,4124,4125,4126,4127,4128,4130,4132,4134,4137,4142,4143,4144,4146,4161,4162,4163,4164,4171,4172,4173,4174,4175,4176,4181,4183,4184,4200,4211,4212,4220,4224,4225,4242,4243,4251,4252,4253,4254,4262,4263,4264,4271,4272,4273,4281,4283,4284,4285,4286,4287,4288,4482,4751,4964,6042,6235,6922,7694,7836,7940,8066,8627],'CS':[3033,3732,3794,3995,4952,5451,5665,5940,6135,6600,6621,6622,6623,6624,6625,6630,6635,6636,6645,6647,6700,6710,6720,6721,6722,6723,6724,6725,6726,6727,6728,6729,6750,6753,6754,6756,6757,6760,6762,6763,6764,6765,6768,6771,6772,6773,6774,6775,6783,6786,6787,6791,6792,6793,6800,6806,6821,6900,6903,6911,6913,6914,6915,6916,6917,6921,6931,6932,6933,7165,7800,8989],'BE':[3372,3661,3877,4558,4733,5500,5502,5510,5515,5520,5525,5526,5527,5530,5537,5538,5540,5551,5552,5555,5561,5600,5609,5621,5623,5630,5641,5643,5650,5661,5662,5663,5664,5666,5667,5668,5671,5672,5673,5674,5675,5703,5711,5712,5720,5726,5727,5731,5732,5734,5741,5742,5744,5747,5751,5752,5800,5811,5820,5830,5837,5838,5925,5931,5932,5945,5946,5948,6311,6335,6766,7673,8093,8152,8635,9373,9513,9730,9914]}; 1128 1129const vp_woo_pont_state_coordinates = { 1130 'BU': [47.4894, 19.1041], 1131 'PE': [47.3833, 19.4404], 1132 'VA': [47.1312, 16.8118], 1133 'BK': [46.6076, 19.3895], 1134 'BE': [46.6908, 21.0699], 1135 'BA': [46.0688, 18.2382], 1136 'BZ': [48.2170, 21.0209], 1137 'CS': [46.4470, 20.2191], 1138 'FE': [47.1779, 18.5847],
vendor: 5,070 bytes, lines 1139-1199
1139 'GS': [47.6404, 17.3863], 1140 'HB': [47.4835, 21.6140], 1141 'HE': [47.8131, 20.1920], 1142 'JN': [47.2266, 20.5014], 1143 'KE': [47.6198, 18.3420], 1144 'NO': [48.0130, 19.5863], 1145 'SO': [46.4838, 17.6299], 1146 'SZ': [48.0260, 22.1060], 1147 'TO': [46.5258, 18.5796], 1148 'VE': [47.1683, 17.7088], 1149 'ZA': [46.6823, 16.9070] 1150}; 1151const vp_woo_pont_country_coordinates = { 1152 'CZ': [49.8509, 15.6689], 1153 'SK': [48.8394, 19.8575], 1154 'RO': [45.9280, 24.8392], 1155 'PL': [52.1721, 19.3782], 1156 'DE': [51.1657, 10.4515], 1157 'AT': [47.5162, 14.5501], 1158 'SI': [46.1512, 14.9955], 1159 'HR': [45.1000, 15.2000], 1160 'BA': [44.0000, 18.0000], 1161 'RS': [44.0165, 21.0059], 1162 'UA': [48.3794, 31.1656], 1163 'BG': [42.7339, 25.4858], 1164 'HR': [45.1000, 15.2000], 1165 'IT': [41.8719, 12.5674], 1166 'CH': [46.8182, 8.2275], 1167 'LI': [47.1660, 9.5554], 1168 'MK': [41.6086, 21.7453], 1169 'AL': [41.1533, 20.1683], 1170 'ME': [42.7087, 19.3744], 1171 'XK': [42.6026, 20.9020], 1172 'GR': [39.0742, 21.8243], 1173 'TR': [38.9637, 35.2433], 1174 'MD': [47.4116, 28.3699], 1175 'BY': [53.7098, 27.9534], 1176 'RU': [61.5240, 105.3188], 1177 'LT': [55.1694, 23.8813], 1178 'LV': [56.8796, 24.6032], 1179 'EE': [58.5953, 25.0136], 1180 'FI': [61.9241, 25.7482], 1181 'NO': [60.4720, 8.4689], 1182 'SE': [60.1282, 18.6435], 1183 'DK': [56.2639, 9.5018], 1184 'FR': [46.6034, 1.8883], 1185 'NL': [52.1326, 5.2913], 1186 'LU': [49.8153, 6.1296], 1187 'PT': [39.3999, -8.2245], 1188 'IE': [53.4129, -8.2439], 1189} 1190 1191module.exports = { 1192 vp_woo_pont_state_postcodes, 1193 vp_woo_pont_state_coordinates, 1194 vp_woo_pont_country_coordinates, 1195}; 1196},{}],3:[function(require,module,exports){ 1197!function(e,o){if("function"==typeof define&&define.amd)define(["exports"],o);else if("undefined"!=typeof exports)o(exports);else{var t={};o(t),e.bodyScrollLock=t}}(this,function(exports){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=!1;if("undefined"!=typeof window){var e={get passive(){t=!0}};window.addEventListener("testPassive",null,e),window.removeEventListener("testPassive",null,e)}var n="undefined"!=typeof window&&window.navigator&&window.navigator.platform&&(/iP(ad|hone|od)/.test(window.navigator.platform)||"MacIntel"===window.navigator.platform&&1<window.navigator.maxTouchPoints),i=[],d=!1,l=-1,c=void 0,s=void 0,u=void 0,a=function(o){return i.some(function(e){return!(!e.options.allowTouchMove||!e.options.allowTouchMove(o))})},v=function(e){var o=e||window.event;return!!a(o.target)||(1<o.touches.length||(o.preventDefault&&o.preventDefault(),!1))},r=function(){void 0!==u&&(document.body.style.paddingRight=u,u=void 0),void 0!==c&&(document.body.style.overflow=c,c=void 0)},f=function(){if(void 0!==s){var e=-parseInt(document.body.style.top,10),o=-parseInt(document.body.style.left,10);document.body.style.position=s.position,document.body.style.top=s.top,document.body.style.left=s.left,window.scrollTo(o,e),s=void 0}};exports.disableBodyScroll=function(r,e){if(r){if(!i.some(function(e){return e.targetElement===r})){var o={targetElement:r,options:e||{}};i=[].concat(function(e){if(Array.isArray(e)){for(var o=0,t=Array(e.length);o<e.length;o++)t[o]=e[o];return t}return Array.from(e)}(i),[o]),n?window.requestAnimationFrame(function(){if(void 0===s){s={position:document.body.style.position,top:document.body.style.top,left:document.body.style.left};var e=window,o=e.scrollY,t=e.scrollX,n=e.innerHeight;document.body.style.position="fixed",document.body.style.top=-o,document.body.style.left=-t,setTimeout(function(){return window.requestAnimationFrame(function(){var e=n-window.innerHeight;e&&n<=o&&(document.body.style.top=-(o+e))})},300)}}):function(e){if(void 0===u){var o=!!e&&!0===e.reserveScrollBarGap,t=window.innerWidth-document.documentElement.clientWidth;if(o&&0<t){var n=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"),10);u=document.body.style.paddingRight,document.body.style.paddingRight=n+t+"px"}}void 0===c&&(c=document.body.style.overflow,document.body.style.overflow="hidden")}(e),n&&(r.ontouchstart=function(e){1===e.targetTouches.length&&(l=e.targetTouches[0].clientY)},r.ontouchmove=function(e){var o,t,n,i;1===e.targetTouches.length&&(t=r,i=(o=e).targetTouches[0].clientY-l,!a(o.target)&&(t&&0===t.scrollTop&&0<i?v(o):(n=t)&&n.scrollHeight-n.scrollTop<=n.clientHeight&&i<0?v(o):o.stopPropagation()))},d||(document.addEventListener("touchmove",v,t?{passive:!1}:void 0),d=!0))}}else console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.")},exports.clearAllBodyScrollLocks=function(){n&&(i.forEach(function(e){e.targetElement.ontouchstart=null,e.targetElement.ontouchmove=null}),d&&(document.removeEventListener("touchmove",v,t?{passive:!1}:void 0),d=!1),l=-1),n?f():r(),i=[]},exports.enableBodyScroll=function(o){o?(i=i.filter(function(e){return e.targetElement!==o}),n&&(o.ontouchstart=null,o.ontouchmove=null,d&&0===i.length&&(document.removeEventListener("touchmove",v,t?{passive:!1}:void 0),d=!1)),n?f():r()):console.error("enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.")}}); 1198 1199},{}],4:[function(require,module,exports){
vendor: 25,863 bytes, lines 1200-2107
1200(function (global, factory) { 1201 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 1202 typeof define === 'function' && define.amd ? define(['exports'], factory) : 1203 (global = global || self, factory(global.JsSearch = {})); 1204}(this, (function (exports) { 'use strict'; 1205 1206 /** 1207 * Indexes for all substring searches (e.g. the term "cat" is indexed as "c", "ca", "cat", "a", "at", and "t"). 1208 */ 1209 var AllSubstringsIndexStrategy = /*#__PURE__*/function () { 1210 function AllSubstringsIndexStrategy() {} 1211 1212 var _proto = AllSubstringsIndexStrategy.prototype; 1213 1214 /** 1215 * @inheritDocs 1216 */ 1217 _proto.expandToken = function expandToken(token) { 1218 var expandedTokens = []; 1219 var string; 1220 1221 for (var i = 0, length = token.length; i < length; ++i) { 1222 string = ''; 1223 1224 for (var j = i; j < length; ++j) { 1225 string += token.charAt(j); 1226 expandedTokens.push(string); 1227 } 1228 } 1229 1230 return expandedTokens; 1231 }; 1232 1233 return AllSubstringsIndexStrategy; 1234 }(); 1235 1236 /** 1237 * Indexes for exact word matches. 1238 */ 1239 var ExactWordIndexStrategy = /*#__PURE__*/function () { 1240 function ExactWordIndexStrategy() {} 1241 1242 var _proto = ExactWordIndexStrategy.prototype; 1243 1244 /** 1245 * @inheritDocs 1246 */ 1247 _proto.expandToken = function expandToken(token) { 1248 return token ? [token] : []; 1249 }; 1250 1251 return ExactWordIndexStrategy; 1252 }(); 1253 1254 /** 1255 * Indexes for prefix searches (e.g. the term "cat" is indexed as "c", "ca", and "cat" allowing prefix search lookups). 1256 */ 1257 var PrefixIndexStrategy = /*#__PURE__*/function () { 1258 function PrefixIndexStrategy() {} 1259 1260 var _proto = PrefixIndexStrategy.prototype; 1261 1262 /** 1263 * @inheritDocs 1264 */ 1265 _proto.expandToken = function expandToken(token) { 1266 var expandedTokens = []; 1267 var string = ''; 1268 1269 for (var i = 0, length = token.length; i < length; ++i) { 1270 string += token.charAt(i); 1271 expandedTokens.push(string); 1272 } 1273 1274 return expandedTokens; 1275 }; 1276 1277 return PrefixIndexStrategy; 1278 }(); 1279 1280 /** 1281 * Enforces case-sensitive text matches. 1282 */ 1283 var CaseSensitiveSanitizer = /*#__PURE__*/function () { 1284 function CaseSensitiveSanitizer() {} 1285 1286 var _proto = CaseSensitiveSanitizer.prototype; 1287 1288 /** 1289 * @inheritDocs 1290 */ 1291 _proto.sanitize = function sanitize(text) { 1292 return text ? text.trim() : ''; 1293 }; 1294 1295 return CaseSensitiveSanitizer; 1296 }(); 1297 1298 /** 1299 * Sanitizes text by converting to a locale-friendly lower-case version and triming leading and trailing whitespace. 1300 */ 1301 var LowerCaseSanitizer = /*#__PURE__*/function () { 1302 function LowerCaseSanitizer() {} 1303 1304 var _proto = LowerCaseSanitizer.prototype; 1305 1306 /** 1307 * @inheritDocs 1308 */ 1309 _proto.sanitize = function sanitize(text) { 1310 return text ? text.toLocaleLowerCase().trim() : ''; 1311 }; 1312 1313 return LowerCaseSanitizer; 1314 }(); 1315 1316 /** 1317 * Find and return a nested object value. 1318 * 1319 * @param object to crawl 1320 * @param path Property path 1321 * @returns {any} 1322 */ 1323 function getNestedFieldValue(object, path) { 1324 path = path || []; 1325 object = object || {}; 1326 var value = object; // walk down the property path 1327 1328 for (var i = 0; i < path.length; i++) { 1329 value = value[path[i]]; 1330 1331 if (value == null) { 1332 return null; 1333 } 1334 } 1335 1336 return value; 1337 } 1338 1339 /** 1340 * Search index capable of returning results matching a set of tokens and ranked according to TF-IDF. 1341 */ 1342 var TfIdfSearchIndex = /*#__PURE__*/function () { 1343 function TfIdfSearchIndex(uidFieldName) { 1344 this._uidFieldName = uidFieldName; 1345 this._tokenToIdfCache = {}; 1346 this._tokenMap = {}; 1347 } 1348 /** 1349 * @inheritDocs 1350 */ 1351 1352 1353 var _proto = TfIdfSearchIndex.prototype; 1354 1355 _proto.indexDocument = function indexDocument(token, uid, doc) { 1356 this._tokenToIdfCache = {}; // New index invalidates previous IDF caches 1357 1358 var tokenMap = this._tokenMap; 1359 var tokenDatum; 1360 1361 if (typeof tokenMap[token] !== 'object') { 1362 tokenMap[token] = tokenDatum = { 1363 $numDocumentOccurrences: 0, 1364 $totalNumOccurrences: 1, 1365 $uidMap: {} 1366 }; 1367 } else { 1368 tokenDatum = tokenMap[token]; 1369 tokenDatum.$totalNumOccurrences++; 1370 } 1371 1372 var uidMap = tokenDatum.$uidMap; 1373 1374 if (typeof uidMap[uid] !== 'object') { 1375 tokenDatum.$numDocumentOccurrences++; 1376 uidMap[uid] = { 1377 $document: doc, 1378 $numTokenOccurrences: 1 1379 }; 1380 } else { 1381 uidMap[uid].$numTokenOccurrences++; 1382 } 1383 } 1384 /** 1385 * @inheritDocs 1386 */ 1387 ; 1388 1389 _proto.search = function search(tokens, corpus) { 1390 var uidToDocumentMap = {}; 1391 1392 for (var i = 0, numTokens = tokens.length; i < numTokens; i++) { 1393 var token = tokens[i]; 1394 var tokenMetadata = this._tokenMap[token]; // Short circuit if no matches were found for any given token. 1395 1396 if (!tokenMetadata) { 1397 return []; 1398 } 1399 1400 if (i === 0) { 1401 var keys = Object.keys(tokenMetadata.$uidMap); 1402 1403 for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 1404 var uid = keys[j]; 1405 uidToDocumentMap[uid] = tokenMetadata.$uidMap[uid].$document; 1406 } 1407 } else { 1408 var keys = Object.keys(uidToDocumentMap); 1409 1410 for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 1411 var uid = keys[j]; 1412 1413 if (typeof tokenMetadata.$uidMap[uid] !== 'object') { 1414 delete uidToDocumentMap[uid]; 1415 } 1416 } 1417 } 1418 } 1419 1420 var documents = []; 1421 1422 for (var uid in uidToDocumentMap) { 1423 documents.push(uidToDocumentMap[uid]); 1424 } 1425 1426 var calculateTfIdf = this._createCalculateTfIdf(); // Return documents sorted by TF-IDF 1427 1428 1429 return documents.sort(function (documentA, documentB) { 1430 return calculateTfIdf(tokens, documentB, corpus) - calculateTfIdf(tokens, documentA, corpus); 1431 }); 1432 }; 1433 1434 _proto._createCalculateIdf = function _createCalculateIdf() { 1435 var tokenMap = this._tokenMap; 1436 var tokenToIdfCache = this._tokenToIdfCache; 1437 return function calculateIdf(token, documents) { 1438 if (!tokenToIdfCache[token]) { 1439 var numDocumentsWithToken = typeof tokenMap[token] !== 'undefined' ? tokenMap[token].$numDocumentOccurrences : 0; 1440 tokenToIdfCache[token] = 1 + Math.log(documents.length / (1 + numDocumentsWithToken)); 1441 } 1442 1443 return tokenToIdfCache[token]; 1444 }; 1445 }; 1446 1447 _proto._createCalculateTfIdf = function _createCalculateTfIdf() { 1448 var tokenMap = this._tokenMap; 1449 var uidFieldName = this._uidFieldName; 1450 1451 var calculateIdf = this._createCalculateIdf(); 1452 1453 return function calculateTfIdf(tokens, document, documents) { 1454 var score = 0; 1455 1456 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { 1457 var token = tokens[i]; 1458 var inverseDocumentFrequency = calculateIdf(token, documents); 1459 1460 if (inverseDocumentFrequency === Infinity) { 1461 inverseDocumentFrequency = 0; 1462 } 1463 1464 var uid; 1465 1466 if (uidFieldName instanceof Array) { 1467 uid = document && getNestedFieldValue(document, uidFieldName); 1468 } else { 1469 uid = document && document[uidFieldName]; 1470 } 1471 1472 var termFrequency = typeof tokenMap[token] !== 'undefined' && typeof tokenMap[token].$uidMap[uid] !== 'undefined' ? tokenMap[token].$uidMap[uid].$numTokenOccurrences : 0; 1473 score += termFrequency * inverseDocumentFrequency; 1474 } 1475 1476 return score; 1477 }; 1478 }; 1479 1480 return TfIdfSearchIndex; 1481 }(); 1482 1483 /** 1484 * Search index capable of returning results matching a set of tokens but without any meaningful rank or order. 1485 */ 1486 var UnorderedSearchIndex = /*#__PURE__*/function () { 1487 function UnorderedSearchIndex() { 1488 this._tokenToUidToDocumentMap = {}; 1489 } 1490 /** 1491 * @inheritDocs 1492 */ 1493 1494 1495 var _proto = UnorderedSearchIndex.prototype; 1496 1497 _proto.indexDocument = function indexDocument(token, uid, doc) { 1498 if (typeof this._tokenToUidToDocumentMap[token] !== 'object') { 1499 this._tokenToUidToDocumentMap[token] = {}; 1500 } 1501 1502 this._tokenToUidToDocumentMap[token][uid] = doc; 1503 } 1504 /** 1505 * @inheritDocs 1506 */ 1507 ; 1508 1509 _proto.search = function search(tokens, corpus) { 1510 var intersectingDocumentMap = {}; 1511 var tokenToUidToDocumentMap = this._tokenToUidToDocumentMap; 1512 1513 for (var i = 0, numTokens = tokens.length; i < numTokens; i++) { 1514 var token = tokens[i]; 1515 var documentMap = tokenToUidToDocumentMap[token]; // Short circuit if no matches were found for any given token. 1516 1517 if (!documentMap) { 1518 return []; 1519 } 1520 1521 if (i === 0) { 1522 var keys = Object.keys(documentMap); 1523 1524 for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 1525 var uid = keys[j]; 1526 intersectingDocumentMap[uid] = documentMap[uid]; 1527 } 1528 } else { 1529 var keys = Object.keys(intersectingDocumentMap); 1530 1531 for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 1532 var uid = keys[j]; 1533 1534 if (typeof documentMap[uid] !== 'object') { 1535 delete intersectingDocumentMap[uid]; 1536 } 1537 } 1538 } 1539 } 1540 1541 var keys = Object.keys(intersectingDocumentMap); 1542 var documents = []; 1543 1544 for (var i = 0, numKeys = keys.length; i < numKeys; i++) { 1545 var uid = keys[i]; 1546 documents.push(intersectingDocumentMap[uid]); 1547 } 1548 1549 return documents; 1550 }; 1551 1552 return UnorderedSearchIndex; 1553 }(); 1554 1555 var REGEX = /[^a-zа-ÑÑ0-9\-']+/i; 1556 /** 1557 * Simple tokenizer that splits strings on whitespace characters and returns an array of all non-empty substrings. 1558 */ 1559 1560 var SimpleTokenizer = /*#__PURE__*/function () { 1561 function SimpleTokenizer() {} 1562 1563 var _proto = SimpleTokenizer.prototype; 1564 1565 /** 1566 * @inheritDocs 1567 */ 1568 _proto.tokenize = function tokenize(text) { 1569 return text.split(REGEX).filter(function (text) { 1570 return text; 1571 } // Filter empty tokens 1572 ); 1573 }; 1574 1575 return SimpleTokenizer; 1576 }(); 1577 1578 /** 1579 * Stemming is the process of reducing search tokens to their root (or stem) so that searches for different forms of a 1580 * word will match. For example "search", "searching" and "searched" are all reduced to the stem "search". 1581 * 1582 * <p>This stemming tokenizer converts tokens (words) to their stem forms before returning them. It requires an 1583 * external stemming function to be provided; for this purpose I recommend the NPM 'porter-stemmer' library. 1584 * 1585 * <p>For more information see http : //tartarus.org/~martin/PorterStemmer/ 1586 */ 1587 var StemmingTokenizer = /*#__PURE__*/function () { 1588 /** 1589 * Constructor. 1590 * 1591 * @param stemmingFunction Function capable of accepting a word and returning its stem. 1592 * @param decoratedIndexStrategy Index strategy to be run after all stop words have been removed. 1593 */ 1594 function StemmingTokenizer(stemmingFunction, decoratedTokenizer) { 1595 this._stemmingFunction = stemmingFunction; 1596 this._tokenizer = decoratedTokenizer; 1597 } 1598 /** 1599 * @inheritDocs 1600 */ 1601 1602 1603 var _proto = StemmingTokenizer.prototype; 1604 1605 _proto.tokenize = function tokenize(text) { 1606 return this._tokenizer.tokenize(text).map(this._stemmingFunction); 1607 }; 1608 1609 return StemmingTokenizer; 1610 }(); 1611 1612 /** 1613 * Stop words list copied from Lunr JS. 1614 */ 1615 var StopWordsMap = { 1616 a: true, 1617 able: true, 1618 about: true, 1619 across: true, 1620 after: true, 1621 all: true, 1622 almost: true, 1623 also: true, 1624 am: true, 1625 among: true, 1626 an: true, 1627 and: true, 1628 any: true, 1629 are: true, 1630 as: true, 1631 at: true, 1632 be: true, 1633 because: true, 1634 been: true, 1635 but: true, 1636 by: true, 1637 can: true, 1638 cannot: true, 1639 could: true, 1640 dear: true, 1641 did: true, 1642 'do': true, 1643 does: true, 1644 either: true, 1645 'else': true, 1646 ever: true, 1647 every: true, 1648 'for': true, 1649 from: true, 1650 'get': true, 1651 got: true, 1652 had: true, 1653 has: true, 1654 have: true, 1655 he: true, 1656 her: true, 1657 hers: true, 1658 him: true, 1659 his: true, 1660 how: true, 1661 however: true, 1662 i: true, 1663 'if': true, 1664 'in': true, 1665 into: true, 1666 is: true, 1667 it: true, 1668 its: true, 1669 just: true, 1670 least: true, 1671 "let": true, 1672 like: true, 1673 likely: true, 1674 may: true, 1675 me: true, 1676 might: true, 1677 most: true, 1678 must: true, 1679 my: true, 1680 neither: true, 1681 no: true, 1682 nor: true, 1683 not: true, 1684 of: true, 1685 off: true, 1686 often: true, 1687 on: true, 1688 only: true, 1689 or: true, 1690 other: true, 1691 our: true, 1692 own: true, 1693 rather: true, 1694 said: true, 1695 say: true, 1696 says: true, 1697 she: true, 1698 should: true, 1699 since: true, 1700 so: true, 1701 some: true, 1702 than: true, 1703 that: true, 1704 the: true, 1705 their: true, 1706 them: true, 1707 then: true, 1708 there: true, 1709 these: true, 1710 they: true, 1711 'this': true, 1712 tis: true, 1713 to: true, 1714 too: true, 1715 twas: true, 1716 us: true, 1717 wants: true, 1718 was: true, 1719 we: true, 1720 were: true, 1721 what: true, 1722 when: true, 1723 where: true, 1724 which: true, 1725 'while': true, 1726 who: true, 1727 whom: true, 1728 why: true, 1729 will: true, 1730 'with': true, 1731 would: true, 1732 yet: true, 1733 you: true, 1734 your: true 1735 }; // Prevent false positives for inherited properties 1736 1737 StopWordsMap.constructor = false; 1738 StopWordsMap.hasOwnProperty = false; 1739 StopWordsMap.isPrototypeOf = false; 1740 StopWordsMap.propertyIsEnumerable = false; 1741 StopWordsMap.toLocaleString = false; 1742 StopWordsMap.toString = false; 1743 StopWordsMap.valueOf = false; 1744 1745 /** 1746 * Stop words are very common (e.g. "a", "and", "the") and are often not semantically meaningful in the context of a 1747 * search. This tokenizer removes stop words from a set of tokens before passing the remaining tokens along for 1748 * indexing or searching purposes. 1749 */ 1750 1751 var StopWordsTokenizer = /*#__PURE__*/function () { 1752 /** 1753 * Constructor. 1754 * 1755 * @param decoratedIndexStrategy Index strategy to be run after all stop words have been removed. 1756 */ 1757 function StopWordsTokenizer(decoratedTokenizer) { 1758 this._tokenizer = decoratedTokenizer; 1759 } 1760 /** 1761 * @inheritDocs 1762 */ 1763 1764 1765 var _proto = StopWordsTokenizer.prototype; 1766 1767 _proto.tokenize = function tokenize(text) { 1768 return this._tokenizer.tokenize(text).filter(function (token) { 1769 return !StopWordsMap[token]; 1770 }); 1771 }; 1772 1773 return StopWordsTokenizer; 1774 }(); 1775 1776 function _defineProperties(target, props) { 1777 for (var i = 0; i < props.length; i++) { 1778 var descriptor = props[i]; 1779 descriptor.enumerable = descriptor.enumerable || false; 1780 descriptor.configurable = true; 1781 if ("value" in descriptor) descriptor.writable = true; 1782 Object.defineProperty(target, descriptor.key, descriptor); 1783 } 1784 } 1785 1786 function _createClass(Constructor, protoProps, staticProps) { 1787 if (protoProps) _defineProperties(Constructor.prototype, protoProps); 1788 if (staticProps) _defineProperties(Constructor, staticProps); 1789 return Constructor; 1790 } 1791 1792 /** 1793 * Simple client-side searching within a set of documents. 1794 * 1795 * <p>Documents can be searched by any number of fields. Indexing and search strategies are highly customizable. 1796 */ 1797 var Search = /*#__PURE__*/function () { 1798 /** 1799 * Array containing either a property name or a path (list of property names) to a nested value 1800 */ 1801 1802 /** 1803 * Constructor. 1804 * @param uidFieldName Field containing values that uniquely identify search documents; this field's values are used 1805 * to ensure that a search result set does not contain duplicate objects. 1806 */ 1807 function Search(uidFieldName) { 1808 if (!uidFieldName) { 1809 throw Error('js-search requires a uid field name constructor parameter'); 1810 } 1811 1812 this._uidFieldName = uidFieldName; // Set default/recommended strategies 1813 1814 this._indexStrategy = new PrefixIndexStrategy(); 1815 this._searchIndex = new TfIdfSearchIndex(uidFieldName); 1816 this._sanitizer = new LowerCaseSanitizer(); 1817 this._tokenizer = new SimpleTokenizer(); 1818 this._documents = []; 1819 this._searchableFields = []; 1820 } 1821 /** 1822 * Override the default index strategy. 1823 * @param value Custom index strategy 1824 * @throws Error if documents have already been indexed by this search instance 1825 */ 1826 1827 1828 var _proto = Search.prototype; 1829 1830 /** 1831 * Add a searchable document to the index. Document will automatically be indexed for search. 1832 * @param document 1833 */ 1834 _proto.addDocument = function addDocument(document) { 1835 this.addDocuments([document]); 1836 } 1837 /** 1838 * Adds searchable documents to the index. Documents will automatically be indexed for search. 1839 * @param document 1840 */ 1841 ; 1842 1843 _proto.addDocuments = function addDocuments(documents) { 1844 this._documents = this._documents.concat(documents); 1845 this.indexDocuments_(documents, this._searchableFields); 1846 } 1847 /** 1848 * Add a new searchable field to the index. Existing documents will automatically be indexed using this new field. 1849 * 1850 * @param field Searchable field or field path. Pass a string to index a top-level field and an array of strings for nested fields. 1851 */ 1852 ; 1853 1854 _proto.addIndex = function addIndex(field) { 1855 this._searchableFields.push(field); 1856 1857 this.indexDocuments_(this._documents, [field]); 1858 } 1859 /** 1860 * Search all documents for ones matching the specified query text. 1861 * @param query 1862 * @returns {Array<Object>} 1863 */ 1864 ; 1865 1866 _proto.search = function search(query) { 1867 var tokens = this._tokenizer.tokenize(this._sanitizer.sanitize(query)); 1868 1869 return this._searchIndex.search(tokens, this._documents); 1870 } 1871 /** 1872 * @param documents 1873 * @param _searchableFields Array containing property names and paths (lists of property names) to nested values 1874 * @private 1875 */ 1876 ; 1877 1878 _proto.indexDocuments_ = function indexDocuments_(documents, _searchableFields) { 1879 this._initialized = true; 1880 var indexStrategy = this._indexStrategy; 1881 var sanitizer = this._sanitizer; 1882 var searchIndex = this._searchIndex; 1883 var tokenizer = this._tokenizer; 1884 var uidFieldName = this._uidFieldName; 1885 1886 for (var di = 0, numDocuments = documents.length; di < numDocuments; di++) { 1887 var doc = documents[di]; 1888 var uid; 1889 1890 if (uidFieldName instanceof Array) { 1891 uid = getNestedFieldValue(doc, uidFieldName); 1892 } else { 1893 uid = doc[uidFieldName]; 1894 } 1895 1896 for (var sfi = 0, numSearchableFields = _searchableFields.length; sfi < numSearchableFields; sfi++) { 1897 var fieldValue; 1898 var searchableField = _searchableFields[sfi]; 1899 1900 if (searchableField instanceof Array) { 1901 fieldValue = getNestedFieldValue(doc, searchableField); 1902 } else { 1903 fieldValue = doc[searchableField]; 1904 } 1905 1906 if (fieldValue != null && typeof fieldValue !== 'string' && fieldValue.toString) { 1907 fieldValue = fieldValue.toString(); 1908 } 1909 1910 if (typeof fieldValue === 'string') { 1911 var fieldTokens = tokenizer.tokenize(sanitizer.sanitize(fieldValue)); 1912 1913 for (var fti = 0, numFieldValues = fieldTokens.length; fti < numFieldValues; fti++) { 1914 var fieldToken = fieldTokens[fti]; 1915 var expandedTokens = indexStrategy.expandToken(fieldToken); 1916 1917 for (var eti = 0, nummExpandedTokens = expandedTokens.length; eti < nummExpandedTokens; eti++) { 1918 var expandedToken = expandedTokens[eti]; 1919 searchIndex.indexDocument(expandedToken, uid, doc); 1920 } 1921 } 1922 } 1923 } 1924 } 1925 }; 1926 1927 _createClass(Search, [{ 1928 key: "indexStrategy", 1929 set: function set(value) { 1930 if (this._initialized) { 1931 throw Error('IIndexStrategy cannot be set after initialization'); 1932 } 1933 1934 this._indexStrategy = value; 1935 }, 1936 get: function get() { 1937 return this._indexStrategy; 1938 } 1939 /** 1940 * Override the default text sanitizing strategy. 1941 * @param value Custom text sanitizing strategy 1942 * @throws Error if documents have already been indexed by this search instance 1943 */ 1944 1945 }, { 1946 key: "sanitizer", 1947 set: function set(value) { 1948 if (this._initialized) { 1949 throw Error('ISanitizer cannot be set after initialization'); 1950 } 1951 1952 this._sanitizer = value; 1953 }, 1954 get: function get() { 1955 return this._sanitizer; 1956 } 1957 /** 1958 * Override the default search index strategy. 1959 * @param value Custom search index strategy 1960 * @throws Error if documents have already been indexed 1961 */ 1962 1963 }, { 1964 key: "searchIndex", 1965 set: function set(value) { 1966 if (this._initialized) { 1967 throw Error('ISearchIndex cannot be set after initialization'); 1968 } 1969 1970 this._searchIndex = value; 1971 }, 1972 get: function get() { 1973 return this._searchIndex; 1974 } 1975 /** 1976 * Override the default text tokenizing strategy. 1977 * @param value Custom text tokenizing strategy 1978 * @throws Error if documents have already been indexed by this search instance 1979 */ 1980 1981 }, { 1982 key: "tokenizer", 1983 set: function set(value) { 1984 if (this._initialized) { 1985 throw Error('ITokenizer cannot be set after initialization'); 1986 } 1987 1988 this._tokenizer = value; 1989 }, 1990 get: function get() { 1991 return this._tokenizer; 1992 } 1993 }]); 1994 1995 return Search; 1996 }(); 1997 1998 /** 1999 * This utility highlights the occurrences of tokens within a string of text. It can be used to give visual indicators 2000 * of match criteria within searchable fields. 2001 * 2002 * <p>For performance purposes this highlighter only works with full-word or prefix token indexes. 2003 */ 2004 var TokenHighlighter = /*#__PURE__*/function () { 2005 /** 2006 * Constructor. 2007 * 2008 * @param opt_indexStrategy Index strategy used by Search 2009 * @param opt_sanitizer Sanitizer used by Search 2010 * @param opt_wrapperTagName Optional wrapper tag name; defaults to 'mark' (e.g. <mark>) 2011 */ 2012 function TokenHighlighter(opt_indexStrategy, opt_sanitizer, opt_wrapperTagName) { 2013 this._indexStrategy = opt_indexStrategy || new PrefixIndexStrategy(); 2014 this._sanitizer = opt_sanitizer || new LowerCaseSanitizer(); 2015 this._wrapperTagName = opt_wrapperTagName || 'mark'; 2016 } 2017 /** 2018 * Highlights token occurrences within a string by wrapping them with a DOM element. 2019 * 2020 * @param text e.g. "john wayne" 2021 * @param tokens e.g. ["wa"] 2022 * @returns {string} e.g. "john <mark>wa</mark>yne" 2023 */ 2024 2025 2026 var _proto = TokenHighlighter.prototype; 2027 2028 _proto.highlight = function highlight(text, tokens) { 2029 var tagsLength = this._wrapText('').length; 2030 2031 var tokenDictionary = Object.create(null); // Create a token map for easier lookup below. 2032 2033 for (var i = 0, numTokens = tokens.length; i < numTokens; i++) { 2034 var token = this._sanitizer.sanitize(tokens[i]); 2035 2036 var expandedTokens = this._indexStrategy.expandToken(token); 2037 2038 for (var j = 0, numExpandedTokens = expandedTokens.length; j < numExpandedTokens; j++) { 2039 var expandedToken = expandedTokens[j]; 2040 2041 if (!tokenDictionary[expandedToken]) { 2042 tokenDictionary[expandedToken] = [token]; 2043 } else { 2044 tokenDictionary[expandedToken].push(token); 2045 } 2046 } 2047 } // Track actualCurrentWord and sanitizedCurrentWord separately in case we encounter nested tags. 2048 2049 2050 var actualCurrentWord = ''; 2051 var sanitizedCurrentWord = ''; 2052 var currentWordStartIndex = 0; // Note this assumes either prefix or full word matching. 2053 2054 for (var i = 0, textLength = text.length; i < textLength; i++) { 2055 var character = text.charAt(i); 2056 2057 if (character === ' ') { 2058 actualCurrentWord = ''; 2059 sanitizedCurrentWord = ''; 2060 currentWordStartIndex = i + 1; 2061 } else { 2062 actualCurrentWord += character; 2063 sanitizedCurrentWord += this._sanitizer.sanitize(character); 2064 } 2065 2066 if (tokenDictionary[sanitizedCurrentWord] && tokenDictionary[sanitizedCurrentWord].indexOf(sanitizedCurrentWord) >= 0) { 2067 actualCurrentWord = this._wrapText(actualCurrentWord); 2068 text = text.substring(0, currentWordStartIndex) + actualCurrentWord + text.substring(i + 1); 2069 i += tagsLength; 2070 textLength += tagsLength; 2071 } 2072 } 2073 2074 return text; 2075 } 2076 /** 2077 * @param text to wrap 2078 * @returns Text wrapped by wrapper tag (e.g. "foo" becomes "<mark>foo</mark>") 2079 * @private 2080 */ 2081 ; 2082 2083 _proto._wrapText = function _wrapText(text) { 2084 var tagName = this._wrapperTagName; 2085 return "<" + tagName + ">" + text + "</" + tagName + ">"; 2086 }; 2087 2088 return TokenHighlighter; 2089 }(); 2090 2091 exports.AllSubstringsIndexStrategy = AllSubstringsIndexStrategy; 2092 exports.CaseSensitiveSanitizer = CaseSensitiveSanitizer; 2093 exports.ExactWordIndexStrategy = ExactWordIndexStrategy; 2094 exports.LowerCaseSanitizer = LowerCaseSanitizer; 2095 exports.PrefixIndexStrategy = PrefixIndexStrategy; 2096 exports.Search = Search; 2097 exports.SimpleTokenizer = SimpleTokenizer; 2098 exports.StemmingTokenizer = StemmingTokenizer; 2099 exports.StopWordsMap = StopWordsMap; 2100 exports.StopWordsTokenizer = StopWordsTokenizer; 2101 exports.TfIdfSearchIndex = TfIdfSearchIndex; 2102 exports.TokenHighlighter = TokenHighlighter; 2103 exports.UnorderedSearchIndex = UnorderedSearchIndex; 2104 2105 Object.defineProperty(exports, '__esModule', { value: true }); 2106 2107})));
vendor: 2,293 bytes, lines 2108-2116
2108 2109},{}],5:[function(require,module,exports){ 2110/*! 2111 Leaflet.FeatureGroup.SubGroup 1.0.2+00bb0d4 2112 (c) 2015-2017 Boris Seang 2113 License BSD-2-Clause 2114 */ 2115!function(e,r){"function"==typeof define&&define.amd?define(["leaflet"],r):r("object"==typeof module&&module.exports?require("leaflet"):e.L)}(this,function(e){e.FeatureGroup.SubGroup=e.FeatureGroup.extend({initialize:function(r,t){e.FeatureGroup.prototype.initialize.call(this,t),this.setParentGroup(r)},setParentGroup:function(r){var t=r instanceof e.LayerGroup;return this._parentGroup=r,this.onAdd=t?"function"==typeof r.addLayers?this._onAddToGroupBatch:this._onAddToGroup:this._onAddToMap,this.onRemove=t?"function"==typeof r.removeLayers?this._onRemoveFromGroupBatch:this._onRemoveFromGroup:this._onRemoveFromMap,this.addLayer=t?this._addLayerToGroup:this._addLayerToMap,this.removeLayer=t?this._removeLayerFromGroup:this._removeLayerFromMap,this},setParentGroupSafe:function(e){var r=this._map;return r&&r.removeLayer(this),this.setParentGroup(e),r&&r.addLayer(this),this},getParentGroup:function(){return this._parentGroup},_onAddToGroupBatch:function(e){var r=this.getLayers();this._map=e,this._parentGroup.addLayers(r)},_onRemoveFromGroupBatch:function(){var e=this.getLayers();this._parentGroup.removeLayers(e),this._map=null},_onAddToGroup:function(e){var r=this._parentGroup;this._map=e,this.eachLayer(r.addLayer,r)},_onRemoveFromGroup:function(){var e=this._parentGroup;this.eachLayer(e.removeLayer,e),this._map=null},_onAddToMap:e.FeatureGroup.prototype.onAdd,_onRemoveFromMap:e.FeatureGroup.prototype.onRemove,_addLayerToGroup:function(e){if(this.hasLayer(e))return this;e.addEventParent(this);var r=this.getLayerId(e);return this._layers[r]=e,this._map&&this._parentGroup.addLayer(e),this.fire("layeradd",{layer:e})},_removeLayerFromGroup:function(e){if(!this.hasLayer(e))return this;var r=e in this._layers?e:this.getLayerId(e);return e=this._layers[r],e.removeEventParent(this),this._map&&e&&this._parentGroup.removeLayer(e),delete this._layers[r],this.fire("layerremove",{layer:e})},_addLayerToMap:e.FeatureGroup.prototype.addLayer,_removeLayerFromMap:e.FeatureGroup.prototype.removeLayer}),e.featureGroup.subGroup=function(r,t){return new e.FeatureGroup.SubGroup(r,t)}}); 2116},{"leaflet":7}],6:[function(require,module,exports){
vendor: 81,106 bytes, lines 2117-4833
2117/* 2118 * Leaflet.markercluster 1.5.3+master.e5124b2, 2119 * Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. 2120 * https://github.com/Leaflet/Leaflet.markercluster 2121 * (c) 2012-2017, Dave Leaver, smartrak 2122 */ 2123(function (global, factory) { 2124 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 2125 typeof define === 'function' && define.amd ? define(['exports'], factory) : 2126 (global = global || self, factory((global.Leaflet = global.Leaflet || {}, global.Leaflet.markercluster = {}))); 2127}(this, function (exports) { 'use strict'; 2128 2129 /* 2130 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within 2131 */ 2132 2133 var MarkerClusterGroup = L.MarkerClusterGroup = L.FeatureGroup.extend({ 2134 2135 options: { 2136 maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center 2137 iconCreateFunction: null, 2138 clusterPane: L.Marker.prototype.options.pane, 2139 2140 spiderfyOnEveryZoom: false, 2141 spiderfyOnMaxZoom: true, 2142 showCoverageOnHover: true, 2143 zoomToBoundsOnClick: true, 2144 singleMarkerMode: false, 2145 2146 disableClusteringAtZoom: null, 2147 2148 // Setting this to false prevents the removal of any clusters outside of the viewpoint, which 2149 // is the default behaviour for performance reasons. 2150 removeOutsideVisibleBounds: true, 2151 2152 // Set to false to disable all animations (zoom and spiderfy). 2153 // If false, option animateAddingMarkers below has no effect. 2154 // If L.DomUtil.TRANSITION is falsy, this option has no effect. 2155 animate: true, 2156 2157 //Whether to animate adding markers after adding the MarkerClusterGroup to the map 2158 // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. 2159 animateAddingMarkers: false, 2160 2161 // Make it possible to provide custom function to calculate spiderfy shape positions 2162 spiderfyShapePositions: null, 2163 2164 //Increase to increase the distance away that spiderfied markers appear from the center 2165 spiderfyDistanceMultiplier: 1, 2166 2167 // Make it possible to specify a polyline options on a spider leg 2168 spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 }, 2169 2170 // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts 2171 chunkedLoading: false, 2172 chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) 2173 chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser 2174 chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) 2175 2176 //Options to pass to the L.Polygon constructor 2177 polygonOptions: {} 2178 }, 2179 2180 initialize: function (options) { 2181 L.Util.setOptions(this, options); 2182 if (!this.options.iconCreateFunction) { 2183 this.options.iconCreateFunction = this._defaultIconCreateFunction; 2184 } 2185 2186 this._featureGroup = L.featureGroup(); 2187 this._featureGroup.addEventParent(this); 2188 2189 this._nonPointGroup = L.featureGroup(); 2190 this._nonPointGroup.addEventParent(this); 2191 2192 this._inZoomAnimation = 0; 2193 this._needsClustering = []; 2194 this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of 2195 //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move 2196 this._currentShownBounds = null; 2197 2198 this._queue = []; 2199 2200 this._childMarkerEventHandlers = { 2201 'dragstart': this._childMarkerDragStart, 2202 'move': this._childMarkerMoved, 2203 'dragend': this._childMarkerDragEnd, 2204 }; 2205 2206 // Hook the appropriate animation methods. 2207 var animate = L.DomUtil.TRANSITION && this.options.animate; 2208 L.extend(this, animate ? this._withAnimation : this._noAnimation); 2209 // Remember which MarkerCluster class to instantiate (animated or not). 2210 this._markerCluster = animate ? L.MarkerCluster : L.MarkerClusterNonAnimated; 2211 }, 2212 2213 addLayer: function (layer) { 2214 2215 if (layer instanceof L.LayerGroup) { 2216 return this.addLayers([layer]); 2217 } 2218 2219 //Don't cluster non point data 2220 if (!layer.getLatLng) { 2221 this._nonPointGroup.addLayer(layer); 2222 this.fire('layeradd', { layer: layer }); 2223 return this; 2224 } 2225 2226 if (!this._map) { 2227 this._needsClustering.push(layer); 2228 this.fire('layeradd', { layer: layer }); 2229 return this; 2230 } 2231 2232 if (this.hasLayer(layer)) { 2233 return this; 2234 } 2235 2236 2237 //If we have already clustered we'll need to add this one to a cluster 2238 2239 if (this._unspiderfy) { 2240 this._unspiderfy(); 2241 } 2242 2243 this._addLayer(layer, this._maxZoom); 2244 this.fire('layeradd', { layer: layer }); 2245 2246 // Refresh bounds and weighted positions. 2247 this._topClusterLevel._recalculateBounds(); 2248 2249 this._refreshClustersIcons(); 2250 2251 //Work out what is visible 2252 var visibleLayer = layer, 2253 currentZoom = this._zoom; 2254 if (layer.__parent) { 2255 while (visibleLayer.__parent._zoom >= currentZoom) { 2256 visibleLayer = visibleLayer.__parent; 2257 } 2258 } 2259 2260 if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { 2261 if (this.options.animateAddingMarkers) { 2262 this._animationAddLayer(layer, visibleLayer); 2263 } else { 2264 this._animationAddLayerNonAnimated(layer, visibleLayer); 2265 } 2266 } 2267 return this; 2268 }, 2269 2270 removeLayer: function (layer) { 2271 2272 if (layer instanceof L.LayerGroup) { 2273 return this.removeLayers([layer]); 2274 } 2275 2276 //Non point layers 2277 if (!layer.getLatLng) { 2278 this._nonPointGroup.removeLayer(layer); 2279 this.fire('layerremove', { layer: layer }); 2280 return this; 2281 } 2282 2283 if (!this._map) { 2284 if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { 2285 this._needsRemoving.push({ layer: layer, latlng: layer._latlng }); 2286 } 2287 this.fire('layerremove', { layer: layer }); 2288 return this; 2289 } 2290 2291 if (!layer.__parent) { 2292 return this; 2293 } 2294 2295 if (this._unspiderfy) { 2296 this._unspiderfy(); 2297 this._unspiderfyLayer(layer); 2298 } 2299 2300 //Remove the marker from clusters 2301 this._removeLayer(layer, true); 2302 this.fire('layerremove', { layer: layer }); 2303 2304 // Refresh bounds and weighted positions. 2305 this._topClusterLevel._recalculateBounds(); 2306 2307 this._refreshClustersIcons(); 2308 2309 layer.off(this._childMarkerEventHandlers, this); 2310 2311 if (this._featureGroup.hasLayer(layer)) { 2312 this._featureGroup.removeLayer(layer); 2313 if (layer.clusterShow) { 2314 layer.clusterShow(); 2315 } 2316 } 2317 2318 return this; 2319 }, 2320 2321 //Takes an array of markers and adds them in bulk 2322 addLayers: function (layersArray, skipLayerAddEvent) { 2323 if (!L.Util.isArray(layersArray)) { 2324 return this.addLayer(layersArray); 2325 } 2326 2327 var fg = this._featureGroup, 2328 npg = this._nonPointGroup, 2329 chunked = this.options.chunkedLoading, 2330 chunkInterval = this.options.chunkInterval, 2331 chunkProgress = this.options.chunkProgress, 2332 l = layersArray.length, 2333 offset = 0, 2334 originalArray = true, 2335 m; 2336 2337 if (this._map) { 2338 var started = (new Date()).getTime(); 2339 var process = L.bind(function () { 2340 var start = (new Date()).getTime(); 2341 2342 // Make sure to unspiderfy before starting to add some layers 2343 if (this._map && this._unspiderfy) { 2344 this._unspiderfy(); 2345 } 2346 2347 for (; offset < l; offset++) { 2348 if (chunked && offset % 200 === 0) { 2349 // every couple hundred markers, instrument the time elapsed since processing started: 2350 var elapsed = (new Date()).getTime() - start; 2351 if (elapsed > chunkInterval) { 2352 break; // been working too hard, time to take a break :-) 2353 } 2354 } 2355 2356 m = layersArray[offset]; 2357 2358 // Group of layers, append children to layersArray and skip. 2359 // Side effects: 2360 // - Total increases, so chunkProgress ratio jumps backward. 2361 // - Groups are not included in this group, only their non-group child layers (hasLayer). 2362 // Changing array length while looping does not affect performance in current browsers: 2363 // http://jsperf.com/for-loop-changing-length/6 2364 if (m instanceof L.LayerGroup) { 2365 if (originalArray) { 2366 layersArray = layersArray.slice(); 2367 originalArray = false; 2368 } 2369 this._extractNonGroupLayers(m, layersArray); 2370 l = layersArray.length; 2371 continue; 2372 } 2373 2374 //Not point data, can't be clustered 2375 if (!m.getLatLng) { 2376 npg.addLayer(m); 2377 if (!skipLayerAddEvent) { 2378 this.fire('layeradd', { layer: m }); 2379 } 2380 continue; 2381 } 2382 2383 if (this.hasLayer(m)) { 2384 continue; 2385 } 2386 2387 this._addLayer(m, this._maxZoom); 2388 if (!skipLayerAddEvent) { 2389 this.fire('layeradd', { layer: m }); 2390 } 2391 2392 //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will 2393 if (m.__parent) { 2394 if (m.__parent.getChildCount() === 2) { 2395 var markers = m.__parent.getAllChildMarkers(), 2396 otherMarker = markers[0] === m ? markers[1] : markers[0]; 2397 fg.removeLayer(otherMarker); 2398 } 2399 } 2400 } 2401 2402 if (chunkProgress) { 2403 // report progress and time elapsed: 2404 chunkProgress(offset, l, (new Date()).getTime() - started); 2405 } 2406 2407 // Completed processing all markers. 2408 if (offset === l) { 2409 2410 // Refresh bounds and weighted positions. 2411 this._topClusterLevel._recalculateBounds(); 2412 2413 this._refreshClustersIcons(); 2414 2415 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); 2416 } else { 2417 setTimeout(process, this.options.chunkDelay); 2418 } 2419 }, this); 2420 2421 process(); 2422 } else { 2423 var needsClustering = this._needsClustering; 2424 2425 for (; offset < l; offset++) { 2426 m = layersArray[offset]; 2427 2428 // Group of layers, append children to layersArray and skip. 2429 if (m instanceof L.LayerGroup) { 2430 if (originalArray) { 2431 layersArray = layersArray.slice(); 2432 originalArray = false; 2433 } 2434 this._extractNonGroupLayers(m, layersArray); 2435 l = layersArray.length; 2436 continue; 2437 } 2438 2439 //Not point data, can't be clustered 2440 if (!m.getLatLng) { 2441 npg.addLayer(m); 2442 continue; 2443 } 2444 2445 if (this.hasLayer(m)) { 2446 continue; 2447 } 2448 2449 needsClustering.push(m); 2450 } 2451 } 2452 return this; 2453 }, 2454 2455 //Takes an array of markers and removes them in bulk 2456 removeLayers: function (layersArray) { 2457 var i, m, 2458 l = layersArray.length, 2459 fg = this._featureGroup, 2460 npg = this._nonPointGroup, 2461 originalArray = true; 2462 2463 if (!this._map) { 2464 for (i = 0; i < l; i++) { 2465 m = layersArray[i]; 2466 2467 // Group of layers, append children to layersArray and skip. 2468 if (m instanceof L.LayerGroup) { 2469 if (originalArray) { 2470 layersArray = layersArray.slice(); 2471 originalArray = false; 2472 } 2473 this._extractNonGroupLayers(m, layersArray); 2474 l = layersArray.length; 2475 continue; 2476 } 2477 2478 this._arraySplice(this._needsClustering, m); 2479 npg.removeLayer(m); 2480 if (this.hasLayer(m)) { 2481 this._needsRemoving.push({ layer: m, latlng: m._latlng }); 2482 } 2483 this.fire('layerremove', { layer: m }); 2484 } 2485 return this; 2486 } 2487 2488 if (this._unspiderfy) { 2489 this._unspiderfy(); 2490 2491 // Work on a copy of the array, so that next loop is not affected. 2492 var layersArray2 = layersArray.slice(), 2493 l2 = l; 2494 for (i = 0; i < l2; i++) { 2495 m = layersArray2[i]; 2496 2497 // Group of layers, append children to layersArray and skip. 2498 if (m instanceof L.LayerGroup) { 2499 this._extractNonGroupLayers(m, layersArray2); 2500 l2 = layersArray2.length; 2501 continue; 2502 } 2503 2504 this._unspiderfyLayer(m); 2505 } 2506 } 2507 2508 for (i = 0; i < l; i++) { 2509 m = layersArray[i]; 2510 2511 // Group of layers, append children to layersArray and skip. 2512 if (m instanceof L.LayerGroup) { 2513 if (originalArray) { 2514 layersArray = layersArray.slice(); 2515 originalArray = false; 2516 } 2517 this._extractNonGroupLayers(m, layersArray); 2518 l = layersArray.length; 2519 continue; 2520 } 2521 2522 if (!m.__parent) { 2523 npg.removeLayer(m); 2524 this.fire('layerremove', { layer: m }); 2525 continue; 2526 } 2527 2528 this._removeLayer(m, true, true); 2529 this.fire('layerremove', { layer: m }); 2530 2531 if (fg.hasLayer(m)) { 2532 fg.removeLayer(m); 2533 if (m.clusterShow) { 2534 m.clusterShow(); 2535 } 2536 } 2537 } 2538 2539 // Refresh bounds and weighted positions. 2540 this._topClusterLevel._recalculateBounds(); 2541 2542 this._refreshClustersIcons(); 2543 2544 //Fix up the clusters and markers on the map 2545 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); 2546 2547 return this; 2548 }, 2549 2550 //Removes all layers from the MarkerClusterGroup 2551 clearLayers: function () { 2552 //Need our own special implementation as the LayerGroup one doesn't work for us 2553 2554 //If we aren't on the map (yet), blow away the markers we know of 2555 if (!this._map) { 2556 this._needsClustering = []; 2557 this._needsRemoving = []; 2558 delete this._gridClusters; 2559 delete this._gridUnclustered; 2560 } 2561 2562 if (this._noanimationUnspiderfy) { 2563 this._noanimationUnspiderfy(); 2564 } 2565 2566 //Remove all the visible layers 2567 this._featureGroup.clearLayers(); 2568 this._nonPointGroup.clearLayers(); 2569 2570 this.eachLayer(function (marker) { 2571 marker.off(this._childMarkerEventHandlers, this); 2572 delete marker.__parent; 2573 }, this); 2574 2575 if (this._map) { 2576 //Reset _topClusterLevel and the DistanceGrids 2577 this._generateInitialClusters(); 2578 } 2579 2580 return this; 2581 }, 2582 2583 //Override FeatureGroup.getBounds as it doesn't work 2584 getBounds: function () { 2585 var bounds = new L.LatLngBounds(); 2586 2587 if (this._topClusterLevel) { 2588 bounds.extend(this._topClusterLevel._bounds); 2589 } 2590 2591 for (var i = this._needsClustering.length - 1; i >= 0; i--) { 2592 bounds.extend(this._needsClustering[i].getLatLng()); 2593 } 2594 2595 bounds.extend(this._nonPointGroup.getBounds()); 2596 2597 return bounds; 2598 }, 2599 2600 //Overrides LayerGroup.eachLayer 2601 eachLayer: function (method, context) { 2602 var markers = this._needsClustering.slice(), 2603 needsRemoving = this._needsRemoving, 2604 thisNeedsRemoving, i, j; 2605 2606 if (this._topClusterLevel) { 2607 this._topClusterLevel.getAllChildMarkers(markers); 2608 } 2609 2610 for (i = markers.length - 1; i >= 0; i--) { 2611 thisNeedsRemoving = true; 2612 2613 for (j = needsRemoving.length - 1; j >= 0; j--) { 2614 if (needsRemoving[j].layer === markers[i]) { 2615 thisNeedsRemoving = false; 2616 break; 2617 } 2618 } 2619 2620 if (thisNeedsRemoving) { 2621 method.call(context, markers[i]); 2622 } 2623 } 2624 2625 this._nonPointGroup.eachLayer(method, context); 2626 }, 2627 2628 //Overrides LayerGroup.getLayers 2629 getLayers: function () { 2630 var layers = []; 2631 this.eachLayer(function (l) { 2632 layers.push(l); 2633 }); 2634 return layers; 2635 }, 2636 2637 //Overrides LayerGroup.getLayer, WARNING: Really bad performance 2638 getLayer: function (id) { 2639 var result = null; 2640 2641 id = parseInt(id, 10); 2642 2643 this.eachLayer(function (l) { 2644 if (L.stamp(l) === id) { 2645 result = l; 2646 } 2647 }); 2648 2649 return result; 2650 }, 2651 2652 //Returns true if the given layer is in this MarkerClusterGroup 2653 hasLayer: function (layer) { 2654 if (!layer) { 2655 return false; 2656 } 2657 2658 var i, anArray = this._needsClustering; 2659 2660 for (i = anArray.length - 1; i >= 0; i--) { 2661 if (anArray[i] === layer) { 2662 return true; 2663 } 2664 } 2665 2666 anArray = this._needsRemoving; 2667 for (i = anArray.length - 1; i >= 0; i--) { 2668 if (anArray[i].layer === layer) { 2669 return false; 2670 } 2671 } 2672 2673 return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); 2674 }, 2675 2676 //Zoom down to show the given layer (spiderfying if necessary) then calls the callback 2677 zoomToShowLayer: function (layer, callback) { 2678 2679 var map = this._map; 2680 2681 if (typeof callback !== 'function') { 2682 callback = function () {}; 2683 } 2684 2685 var showMarker = function () { 2686 // Assumes that map.hasLayer checks for direct appearance on map, not recursively calling 2687 // hasLayer on Layer Groups that are on map (typically not calling this MarkerClusterGroup.hasLayer, which would always return true) 2688 if ((map.hasLayer(layer) || map.hasLayer(layer.__parent)) && !this._inZoomAnimation) { 2689 this._map.off('moveend', showMarker, this); 2690 this.off('animationend', showMarker, this); 2691 2692 if (map.hasLayer(layer)) { 2693 callback(); 2694 } else if (layer.__parent._icon) { 2695 this.once('spiderfied', callback, this); 2696 layer.__parent.spiderfy(); 2697 } 2698 } 2699 }; 2700 2701 if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { 2702 //Layer is visible ond on screen, immediate return 2703 callback(); 2704 } else if (layer.__parent._zoom < Math.round(this._map._zoom)) { 2705 //Layer should be visible at this zoom level. It must not be on screen so just pan over to it 2706 this._map.on('moveend', showMarker, this); 2707 this._map.panTo(layer.getLatLng()); 2708 } else { 2709 this._map.on('moveend', showMarker, this); 2710 this.on('animationend', showMarker, this); 2711 layer.__parent.zoomToBounds(); 2712 } 2713 }, 2714 2715 //Overrides FeatureGroup.onAdd 2716 onAdd: function (map) { 2717 this._map = map; 2718 var i, l, layer; 2719 2720 if (!isFinite(this._map.getMaxZoom())) { 2721 throw "Map has no maxZoom specified"; 2722 } 2723 2724 this._featureGroup.addTo(map); 2725 this._nonPointGroup.addTo(map); 2726 2727 if (!this._gridClusters) { 2728 this._generateInitialClusters(); 2729 } 2730 2731 this._maxLat = map.options.crs.projection.MAX_LATITUDE; 2732 2733 //Restore all the positions as they are in the MCG before removing them 2734 for (i = 0, l = this._needsRemoving.length; i < l; i++) { 2735 layer = this._needsRemoving[i]; 2736 layer.newlatlng = layer.layer._latlng; 2737 layer.layer._latlng = layer.latlng; 2738 } 2739 //Remove them, then restore their new positions 2740 for (i = 0, l = this._needsRemoving.length; i < l; i++) { 2741 layer = this._needsRemoving[i]; 2742 this._removeLayer(layer.layer, true); 2743 layer.layer._latlng = layer.newlatlng; 2744 } 2745 this._needsRemoving = []; 2746 2747 //Remember the current zoom level and bounds 2748 this._zoom = Math.round(this._map._zoom); 2749 this._currentShownBounds = this._getExpandedVisibleBounds(); 2750 2751 this._map.on('zoomend', this._zoomEnd, this); 2752 this._map.on('moveend', this._moveEnd, this); 2753 2754 if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely 2755 this._spiderfierOnAdd(); 2756 } 2757 2758 this._bindEvents(); 2759 2760 //Actually add our markers to the map: 2761 l = this._needsClustering; 2762 this._needsClustering = []; 2763 this.addLayers(l, true); 2764 }, 2765 2766 //Overrides FeatureGroup.onRemove 2767 onRemove: function (map) { 2768 map.off('zoomend', this._zoomEnd, this); 2769 map.off('moveend', this._moveEnd, this); 2770 2771 this._unbindEvents(); 2772 2773 //In case we are in a cluster animation 2774 this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); 2775 2776 if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely 2777 this._spiderfierOnRemove(); 2778 } 2779 2780 delete this._maxLat; 2781 2782 //Clean up all the layers we added to the map 2783 this._hideCoverage(); 2784 this._featureGroup.remove(); 2785 this._nonPointGroup.remove(); 2786 2787 this._featureGroup.clearLayers(); 2788 2789 this._map = null; 2790 }, 2791 2792 getVisibleParent: function (marker) { 2793 var vMarker = marker; 2794 while (vMarker && !vMarker._icon) { 2795 vMarker = vMarker.__parent; 2796 } 2797 return vMarker || null; 2798 }, 2799 2800 //Remove the given object from the given array 2801 _arraySplice: function (anArray, obj) { 2802 for (var i = anArray.length - 1; i >= 0; i--) { 2803 if (anArray[i] === obj) { 2804 anArray.splice(i, 1); 2805 return true; 2806 } 2807 } 2808 }, 2809 2810 /** 2811 * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom. 2812 * @param marker to be removed from _gridUnclustered. 2813 * @param z integer bottom start zoom level (included) 2814 * @private 2815 */ 2816 _removeFromGridUnclustered: function (marker, z) { 2817 var map = this._map, 2818 gridUnclustered = this._gridUnclustered, 2819 minZoom = Math.floor(this._map.getMinZoom()); 2820 2821 for (; z >= minZoom; z--) { 2822 if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { 2823 break; 2824 } 2825 } 2826 }, 2827 2828 _childMarkerDragStart: function (e) { 2829 e.target.__dragStart = e.target._latlng; 2830 }, 2831 2832 _childMarkerMoved: function (e) { 2833 if (!this._ignoreMove && !e.target.__dragStart) { 2834 var isPopupOpen = e.target._popup && e.target._popup.isOpen(); 2835 2836 this._moveChild(e.target, e.oldLatLng, e.latlng); 2837 2838 if (isPopupOpen) { 2839 e.target.openPopup(); 2840 } 2841 } 2842 }, 2843 2844 _moveChild: function (layer, from, to) { 2845 layer._latlng = from; 2846 this.removeLayer(layer); 2847 2848 layer._latlng = to; 2849 this.addLayer(layer); 2850 }, 2851 2852 _childMarkerDragEnd: function (e) { 2853 var dragStart = e.target.__dragStart; 2854 delete e.target.__dragStart; 2855 if (dragStart) { 2856 this._moveChild(e.target, dragStart, e.target._latlng); 2857 } 2858 }, 2859 2860 2861 //Internal function for removing a marker from everything. 2862 //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) 2863 _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { 2864 var gridClusters = this._gridClusters, 2865 gridUnclustered = this._gridUnclustered, 2866 fg = this._featureGroup, 2867 map = this._map, 2868 minZoom = Math.floor(this._map.getMinZoom()); 2869 2870 //Remove the marker from distance clusters it might be in 2871 if (removeFromDistanceGrid) { 2872 this._removeFromGridUnclustered(marker, this._maxZoom); 2873 } 2874 2875 //Work our way up the clusters removing them as we go if required 2876 var cluster = marker.__parent, 2877 markers = cluster._markers, 2878 otherMarker; 2879 2880 //Remove the marker from the immediate parents marker list 2881 this._arraySplice(markers, marker); 2882 2883 while (cluster) { 2884 cluster._childCount--; 2885 cluster._boundsNeedUpdate = true; 2886 2887 if (cluster._zoom < minZoom) { 2888 //Top level, do nothing 2889 break; 2890 } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required 2891 //We need to push the other marker up to the parent 2892 otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; 2893 2894 //Update distance grid 2895 gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); 2896 gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); 2897 2898 //Move otherMarker up to parent 2899 this._arraySplice(cluster.__parent._childClusters, cluster); 2900 cluster.__parent._markers.push(otherMarker); 2901 otherMarker.__parent = cluster.__parent; 2902 2903 if (cluster._icon) { 2904 //Cluster is currently on the map, need to put the marker on the map instead 2905 fg.removeLayer(cluster); 2906 if (!dontUpdateMap) { 2907 fg.addLayer(otherMarker); 2908 } 2909 } 2910 } else { 2911 cluster._iconNeedsUpdate = true; 2912 } 2913 2914 cluster = cluster.__parent; 2915 } 2916 2917 delete marker.__parent; 2918 }, 2919 2920 _isOrIsParent: function (el, oel) { 2921 while (oel) { 2922 if (el === oel) { 2923 return true; 2924 } 2925 oel = oel.parentNode; 2926 } 2927 return false; 2928 }, 2929 2930 //Override L.Evented.fire 2931 fire: function (type, data, propagate) { 2932 if (data && data.layer instanceof L.MarkerCluster) { 2933 //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) 2934 if (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) { 2935 return; 2936 } 2937 type = 'cluster' + type; 2938 } 2939 2940 L.FeatureGroup.prototype.fire.call(this, type, data, propagate); 2941 }, 2942 2943 //Override L.Evented.listens 2944 listens: function (type, propagate) { 2945 return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate); 2946 }, 2947 2948 //Default functionality 2949 _defaultIconCreateFunction: function (cluster) { 2950 var childCount = cluster.getChildCount(); 2951 2952 var c = ' marker-cluster-'; 2953 if (childCount < 10) { 2954 c += 'small'; 2955 } else if (childCount < 100) { 2956 c += 'medium'; 2957 } else { 2958 c += 'large'; 2959 } 2960 2961 return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); 2962 }, 2963 2964 _bindEvents: function () { 2965 var map = this._map, 2966 spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, 2967 showCoverageOnHover = this.options.showCoverageOnHover, 2968 zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, 2969 spiderfyOnEveryZoom = this.options.spiderfyOnEveryZoom; 2970 2971 //Zoom on cluster click or spiderfy if we are at the lowest level 2972 if (spiderfyOnMaxZoom || zoomToBoundsOnClick || spiderfyOnEveryZoom) { 2973 this.on('clusterclick clusterkeypress', this._zoomOrSpiderfy, this); 2974 } 2975 2976 //Show convex hull (boundary) polygon on mouse over 2977 if (showCoverageOnHover) { 2978 this.on('clustermouseover', this._showCoverage, this); 2979 this.on('clustermouseout', this._hideCoverage, this); 2980 map.on('zoomend', this._hideCoverage, this); 2981 } 2982 }, 2983 2984 _zoomOrSpiderfy: function (e) { 2985 var cluster = e.layer, 2986 bottomCluster = cluster; 2987 2988 if (e.type === 'clusterkeypress' && e.originalEvent && e.originalEvent.keyCode !== 13) { 2989 return; 2990 } 2991 2992 while (bottomCluster._childClusters.length === 1) { 2993 bottomCluster = bottomCluster._childClusters[0]; 2994 } 2995 2996 if (bottomCluster._zoom === this._maxZoom && 2997 bottomCluster._childCount === cluster._childCount && 2998 this.options.spiderfyOnMaxZoom) { 2999 3000 // All child markers are contained in a single cluster from this._maxZoom to this cluster. 3001 cluster.spiderfy(); 3002 } else if (this.options.zoomToBoundsOnClick) { 3003 cluster.zoomToBounds(); 3004 } 3005 3006 if (this.options.spiderfyOnEveryZoom) { 3007 cluster.spiderfy(); 3008 } 3009 3010 // Focus the map again for keyboard users. 3011 if (e.originalEvent && e.originalEvent.keyCode === 13) { 3012 this._map._container.focus(); 3013 } 3014 }, 3015 3016 _showCoverage: function (e) { 3017 var map = this._map; 3018 if (this._inZoomAnimation) { 3019 return; 3020 } 3021 if (this._shownPolygon) { 3022 map.removeLayer(this._shownPolygon); 3023 } 3024 if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { 3025 this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); 3026 map.addLayer(this._shownPolygon); 3027 } 3028 }, 3029 3030 _hideCoverage: function () { 3031 if (this._shownPolygon) { 3032 this._map.removeLayer(this._shownPolygon); 3033 this._shownPolygon = null; 3034 } 3035 }, 3036 3037 _unbindEvents: function () { 3038 var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, 3039 showCoverageOnHover = this.options.showCoverageOnHover, 3040 zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, 3041 spiderfyOnEveryZoom = this.options.spiderfyOnEveryZoom, 3042 map = this._map; 3043 3044 if (spiderfyOnMaxZoom || zoomToBoundsOnClick || spiderfyOnEveryZoom) { 3045 this.off('clusterclick clusterkeypress', this._zoomOrSpiderfy, this); 3046 } 3047 if (showCoverageOnHover) { 3048 this.off('clustermouseover', this._showCoverage, this); 3049 this.off('clustermouseout', this._hideCoverage, this); 3050 map.off('zoomend', this._hideCoverage, this); 3051 } 3052 }, 3053 3054 _zoomEnd: function () { 3055 if (!this._map) { //May have been removed from the map by a zoomEnd handler 3056 return; 3057 } 3058 this._mergeSplitClusters(); 3059 3060 this._zoom = Math.round(this._map._zoom); 3061 this._currentShownBounds = this._getExpandedVisibleBounds(); 3062 }, 3063 3064 _moveEnd: function () { 3065 if (this._inZoomAnimation) { 3066 return; 3067 } 3068 3069 var newBounds = this._getExpandedVisibleBounds(); 3070 3071 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, newBounds); 3072 this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds); 3073 3074 this._currentShownBounds = newBounds; 3075 return; 3076 }, 3077 3078 _generateInitialClusters: function () { 3079 var maxZoom = Math.ceil(this._map.getMaxZoom()), 3080 minZoom = Math.floor(this._map.getMinZoom()), 3081 radius = this.options.maxClusterRadius, 3082 radiusFn = radius; 3083 3084 //If we just set maxClusterRadius to a single number, we need to create 3085 //a simple function to return that number. Otherwise, we just have to 3086 //use the function we've passed in. 3087 if (typeof radius !== "function") { 3088 radiusFn = function () { return radius; }; 3089 } 3090 3091 if (this.options.disableClusteringAtZoom !== null) { 3092 maxZoom = this.options.disableClusteringAtZoom - 1; 3093 } 3094 this._maxZoom = maxZoom; 3095 this._gridClusters = {}; 3096 this._gridUnclustered = {}; 3097 3098 //Set up DistanceGrids for each zoom 3099 for (var zoom = maxZoom; zoom >= minZoom; zoom--) { 3100 this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); 3101 this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); 3102 } 3103 3104 // Instantiate the appropriate L.MarkerCluster class (animated or not). 3105 this._topClusterLevel = new this._markerCluster(this, minZoom - 1); 3106 }, 3107 3108 //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) 3109 _addLayer: function (layer, zoom) { 3110 var gridClusters = this._gridClusters, 3111 gridUnclustered = this._gridUnclustered, 3112 minZoom = Math.floor(this._map.getMinZoom()), 3113 markerPoint, z; 3114 3115 if (this.options.singleMarkerMode) { 3116 this._overrideMarkerIcon(layer); 3117 } 3118 3119 layer.on(this._childMarkerEventHandlers, this); 3120 3121 //Find the lowest zoom level to slot this one in 3122 for (; zoom >= minZoom; zoom--) { 3123 markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position 3124 3125 //Try find a cluster close by 3126 var closest = gridClusters[zoom].getNearObject(markerPoint); 3127 if (closest) { 3128 closest._addChild(layer); 3129 layer.__parent = closest; 3130 return; 3131 } 3132 3133 //Try find a marker close by to form a new cluster with 3134 closest = gridUnclustered[zoom].getNearObject(markerPoint); 3135 if (closest) { 3136 var parent = closest.__parent; 3137 if (parent) { 3138 this._removeLayer(closest, false); 3139 } 3140 3141 //Create new cluster with these 2 in it 3142 3143 var newCluster = new this._markerCluster(this, zoom, closest, layer); 3144 gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); 3145 closest.__parent = newCluster; 3146 layer.__parent = newCluster; 3147 3148 //First create any new intermediate parent clusters that don't exist 3149 var lastParent = newCluster; 3150 for (z = zoom - 1; z > parent._zoom; z--) { 3151 lastParent = new this._markerCluster(this, z, lastParent); 3152 gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); 3153 } 3154 parent._addChild(lastParent); 3155 3156 //Remove closest from this zoom level and any above that it is in, replace with newCluster 3157 this._removeFromGridUnclustered(closest, zoom); 3158 3159 return; 3160 } 3161 3162 //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards 3163 gridUnclustered[zoom].addObject(layer, markerPoint); 3164 } 3165 3166 //Didn't get in anything, add us to the top 3167 this._topClusterLevel._addChild(layer); 3168 layer.__parent = this._topClusterLevel; 3169 return; 3170 }, 3171 3172 /** 3173 * Refreshes the icon of all "dirty" visible clusters. 3174 * Non-visible "dirty" clusters will be updated when they are added to the map. 3175 * @private 3176 */ 3177 _refreshClustersIcons: function () { 3178 this._featureGroup.eachLayer(function (c) { 3179 if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { 3180 c._updateIcon(); 3181 } 3182 }); 3183 }, 3184 3185 //Enqueue code to fire after the marker expand/contract has happened 3186 _enqueue: function (fn) { 3187 this._queue.push(fn); 3188 if (!this._queueTimeout) { 3189 this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); 3190 } 3191 }, 3192 _processQueue: function () { 3193 for (var i = 0; i < this._queue.length; i++) { 3194 this._queue[i].call(this); 3195 } 3196 this._queue.length = 0; 3197 clearTimeout(this._queueTimeout); 3198 this._queueTimeout = null; 3199 }, 3200 3201 //Merge and split any existing clusters that are too big or small 3202 _mergeSplitClusters: function () { 3203 var mapZoom = Math.round(this._map._zoom); 3204 3205 //In case we are starting to split before the animation finished 3206 this._processQueue(); 3207 3208 if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split 3209 this._animationStart(); 3210 //Remove clusters now off screen 3211 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, this._getExpandedVisibleBounds()); 3212 3213 this._animationZoomIn(this._zoom, mapZoom); 3214 3215 } else if (this._zoom > mapZoom) { //Zoom out, merge 3216 this._animationStart(); 3217 3218 this._animationZoomOut(this._zoom, mapZoom); 3219 } else { 3220 this._moveEnd(); 3221 } 3222 }, 3223 3224 //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan) 3225 _getExpandedVisibleBounds: function () { 3226 if (!this.options.removeOutsideVisibleBounds) { 3227 return this._mapBoundsInfinite; 3228 } else if (L.Browser.mobile) { 3229 return this._checkBoundsMaxLat(this._map.getBounds()); 3230 } 3231 3232 return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor. 3233 }, 3234 3235 /** 3236 * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude 3237 * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas). 3238 * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without 3239 * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit, 3240 * making the user think that MCG "eats" them and never displays them again. 3241 * @param bounds L.LatLngBounds 3242 * @returns {L.LatLngBounds} 3243 * @private 3244 */ 3245 _checkBoundsMaxLat: function (bounds) { 3246 var maxLat = this._maxLat; 3247 3248 if (maxLat !== undefined) { 3249 if (bounds.getNorth() >= maxLat) { 3250 bounds._northEast.lat = Infinity; 3251 } 3252 if (bounds.getSouth() <= -maxLat) { 3253 bounds._southWest.lat = -Infinity; 3254 } 3255 } 3256 3257 return bounds; 3258 }, 3259 3260 //Shared animation code 3261 _animationAddLayerNonAnimated: function (layer, newCluster) { 3262 if (newCluster === layer) { 3263 this._featureGroup.addLayer(layer); 3264 } else if (newCluster._childCount === 2) { 3265 newCluster._addToMap(); 3266 3267 var markers = newCluster.getAllChildMarkers(); 3268 this._featureGroup.removeLayer(markers[0]); 3269 this._featureGroup.removeLayer(markers[1]); 3270 } else { 3271 newCluster._updateIcon(); 3272 } 3273 }, 3274 3275 /** 3276 * Extracts individual (i.e. non-group) layers from a Layer Group. 3277 * @param group to extract layers from. 3278 * @param output {Array} in which to store the extracted layers. 3279 * @returns {*|Array} 3280 * @private 3281 */ 3282 _extractNonGroupLayers: function (group, output) { 3283 var layers = group.getLayers(), 3284 i = 0, 3285 layer; 3286 3287 output = output || []; 3288 3289 for (; i < layers.length; i++) { 3290 layer = layers[i]; 3291 3292 if (layer instanceof L.LayerGroup) { 3293 this._extractNonGroupLayers(layer, output); 3294 continue; 3295 } 3296 3297 output.push(layer); 3298 } 3299 3300 return output; 3301 }, 3302 3303 /** 3304 * Implements the singleMarkerMode option. 3305 * @param layer Marker to re-style using the Clusters iconCreateFunction. 3306 * @returns {L.Icon} The newly created icon. 3307 * @private 3308 */ 3309 _overrideMarkerIcon: function (layer) { 3310 var icon = layer.options.icon = this.options.iconCreateFunction({ 3311 getChildCount: function () { 3312 return 1; 3313 }, 3314 getAllChildMarkers: function () { 3315 return [layer]; 3316 } 3317 }); 3318 3319 return icon; 3320 } 3321 }); 3322 3323 // Constant bounds used in case option "removeOutsideVisibleBounds" is set to false. 3324 L.MarkerClusterGroup.include({ 3325 _mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity)) 3326 }); 3327 3328 L.MarkerClusterGroup.include({ 3329 _noAnimation: { 3330 //Non Animated versions of everything 3331 _animationStart: function () { 3332 //Do nothing... 3333 }, 3334 _animationZoomIn: function (previousZoomLevel, newZoomLevel) { 3335 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel); 3336 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 3337 3338 //We didn't actually animate, but we use this event to mean "clustering animations have finished" 3339 this.fire('animationend'); 3340 }, 3341 _animationZoomOut: function (previousZoomLevel, newZoomLevel) { 3342 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel); 3343 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 3344 3345 //We didn't actually animate, but we use this event to mean "clustering animations have finished" 3346 this.fire('animationend'); 3347 }, 3348 _animationAddLayer: function (layer, newCluster) { 3349 this._animationAddLayerNonAnimated(layer, newCluster); 3350 } 3351 }, 3352 3353 _withAnimation: { 3354 //Animated versions here 3355 _animationStart: function () { 3356 this._map._mapPane.className += ' leaflet-cluster-anim'; 3357 this._inZoomAnimation++; 3358 }, 3359 3360 _animationZoomIn: function (previousZoomLevel, newZoomLevel) { 3361 var bounds = this._getExpandedVisibleBounds(), 3362 fg = this._featureGroup, 3363 minZoom = Math.floor(this._map.getMinZoom()), 3364 i; 3365 3366 this._ignoreMove = true; 3367 3368 //Add all children of current clusters to map and remove those clusters from map 3369 this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { 3370 var startPos = c._latlng, 3371 markers = c._markers, 3372 m; 3373 3374 if (!bounds.contains(startPos)) { 3375 startPos = null; 3376 } 3377 3378 if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us 3379 fg.removeLayer(c); 3380 c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); 3381 } else { 3382 //Fade out old cluster 3383 c.clusterHide(); 3384 c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); 3385 } 3386 3387 //Remove all markers that aren't visible any more 3388 //TODO: Do we actually need to do this on the higher levels too? 3389 for (i = markers.length - 1; i >= 0; i--) { 3390 m = markers[i]; 3391 if (!bounds.contains(m._latlng)) { 3392 fg.removeLayer(m); 3393 } 3394 } 3395 3396 }); 3397 3398 this._forceLayout(); 3399 3400 //Update opacities 3401 this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); 3402 //TODO Maybe? Update markers in _recursivelyBecomeVisible 3403 fg.eachLayer(function (n) { 3404 if (!(n instanceof L.MarkerCluster) && n._icon) { 3405 n.clusterShow(); 3406 } 3407 }); 3408 3409 //update the positions of the just added clusters/markers 3410 this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { 3411 c._recursivelyRestoreChildPositions(newZoomLevel); 3412 }); 3413 3414 this._ignoreMove = false; 3415 3416 //Remove the old clusters and close the zoom animation 3417 this._enqueue(function () { 3418 //update the positions of the just added clusters/markers 3419 this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { 3420 fg.removeLayer(c); 3421 c.clusterShow(); 3422 }); 3423 3424 this._animationEnd(); 3425 }); 3426 }, 3427 3428 _animationZoomOut: function (previousZoomLevel, newZoomLevel) { 3429 this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel); 3430 3431 //Need to add markers for those that weren't on the map before but are now 3432 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 3433 //Remove markers that were on the map before but won't be now 3434 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel, this._getExpandedVisibleBounds()); 3435 }, 3436 3437 _animationAddLayer: function (layer, newCluster) { 3438 var me = this, 3439 fg = this._featureGroup; 3440 3441 fg.addLayer(layer); 3442 if (newCluster !== layer) { 3443 if (newCluster._childCount > 2) { //Was already a cluster 3444 3445 newCluster._updateIcon(); 3446 this._forceLayout(); 3447 this._animationStart(); 3448 3449 layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); 3450 layer.clusterHide(); 3451 3452 this._enqueue(function () { 3453 fg.removeLayer(layer); 3454 layer.clusterShow(); 3455 3456 me._animationEnd(); 3457 }); 3458 3459 } else { //Just became a cluster 3460 this._forceLayout(); 3461 3462 me._animationStart(); 3463 me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._zoom); 3464 } 3465 } 3466 } 3467 }, 3468 3469 // Private methods for animated versions. 3470 _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { 3471 var bounds = this._getExpandedVisibleBounds(), 3472 minZoom = Math.floor(this._map.getMinZoom()); 3473 3474 //Animate all of the markers in the clusters to move to their cluster center point 3475 cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, previousZoomLevel + 1, newZoomLevel); 3476 3477 var me = this; 3478 3479 //Update the opacity (If we immediately set it they won't animate) 3480 this._forceLayout(); 3481 cluster._recursivelyBecomeVisible(bounds, newZoomLevel); 3482 3483 //TODO: Maybe use the transition timing stuff to make this more reliable 3484 //When the animations are done, tidy up 3485 this._enqueue(function () { 3486 3487 //This cluster stopped being a cluster before the timeout fired 3488 if (cluster._childCount === 1) { 3489 var m = cluster._markers[0]; 3490 //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it 3491 this._ignoreMove = true; 3492 m.setLatLng(m.getLatLng()); 3493 this._ignoreMove = false; 3494 if (m.clusterShow) { 3495 m.clusterShow(); 3496 } 3497 } else { 3498 cluster._recursively(bounds, newZoomLevel, minZoom, function (c) { 3499 c._recursivelyRemoveChildrenFromMap(bounds, minZoom, previousZoomLevel + 1); 3500 }); 3501 } 3502 me._animationEnd(); 3503 }); 3504 }, 3505 3506 _animationEnd: function () { 3507 if (this._map) { 3508 this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); 3509 } 3510 this._inZoomAnimation--; 3511 this.fire('animationend'); 3512 }, 3513 3514 //Force a browser layout of stuff in the map 3515 // Should apply the current opacity and location to all elements so we can update them again for an animation 3516 _forceLayout: function () { 3517 //In my testing this works, infact offsetWidth of any element seems to work. 3518 //Could loop all this._layers and do this for each _icon if it stops working 3519 3520 L.Util.falseFn(document.body.offsetWidth); 3521 } 3522 }); 3523 3524 L.markerClusterGroup = function (options) { 3525 return new L.MarkerClusterGroup(options); 3526 }; 3527 3528 var MarkerCluster = L.MarkerCluster = L.Marker.extend({ 3529 options: L.Icon.prototype.options, 3530 3531 initialize: function (group, zoom, a, b) { 3532 3533 L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), 3534 { icon: this, pane: group.options.clusterPane }); 3535 3536 this._group = group; 3537 this._zoom = zoom; 3538 3539 this._markers = []; 3540 this._childClusters = []; 3541 this._childCount = 0; 3542 this._iconNeedsUpdate = true; 3543 this._boundsNeedUpdate = true; 3544 3545 this._bounds = new L.LatLngBounds(); 3546 3547 if (a) { 3548 this._addChild(a); 3549 } 3550 if (b) { 3551 this._addChild(b); 3552 } 3553 }, 3554 3555 //Recursively retrieve all child markers of this cluster 3556 getAllChildMarkers: function (storageArray, ignoreDraggedMarker) { 3557 storageArray = storageArray || []; 3558 3559 for (var i = this._childClusters.length - 1; i >= 0; i--) { 3560 this._childClusters[i].getAllChildMarkers(storageArray, ignoreDraggedMarker); 3561 } 3562 3563 for (var j = this._markers.length - 1; j >= 0; j--) { 3564 if (ignoreDraggedMarker && this._markers[j].__dragStart) { 3565 continue; 3566 } 3567 storageArray.push(this._markers[j]); 3568 } 3569 3570 return storageArray; 3571 }, 3572 3573 //Returns the count of how many child markers we have 3574 getChildCount: function () { 3575 return this._childCount; 3576 }, 3577 3578 //Zoom to the minimum of showing all of the child markers, or the extents of this cluster 3579 zoomToBounds: function (fitBoundsOptions) { 3580 var childClusters = this._childClusters.slice(), 3581 map = this._group._map, 3582 boundsZoom = map.getBoundsZoom(this._bounds), 3583 zoom = this._zoom + 1, 3584 mapZoom = map.getZoom(), 3585 i; 3586 3587 //calculate how far we need to zoom down to see all of the markers 3588 while (childClusters.length > 0 && boundsZoom > zoom) { 3589 zoom++; 3590 var newClusters = []; 3591 for (i = 0; i < childClusters.length; i++) { 3592 newClusters = newClusters.concat(childClusters[i]._childClusters); 3593 } 3594 childClusters = newClusters; 3595 } 3596 3597 if (boundsZoom > zoom) { 3598 this._group._map.setView(this._latlng, zoom); 3599 } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead 3600 this._group._map.setView(this._latlng, mapZoom + 1); 3601 } else { 3602 this._group._map.fitBounds(this._bounds, fitBoundsOptions); 3603 } 3604 }, 3605 3606 getBounds: function () { 3607 var bounds = new L.LatLngBounds(); 3608 bounds.extend(this._bounds); 3609 return bounds; 3610 }, 3611 3612 _updateIcon: function () { 3613 this._iconNeedsUpdate = true; 3614 if (this._icon) { 3615 this.setIcon(this); 3616 } 3617 }, 3618 3619 //Cludge for Icon, we pretend to be an icon for performance 3620 createIcon: function () { 3621 if (this._iconNeedsUpdate) { 3622 this._iconObj = this._group.options.iconCreateFunction(this); 3623 this._iconNeedsUpdate = false; 3624 } 3625 return this._iconObj.createIcon(); 3626 }, 3627 createShadow: function () { 3628 return this._iconObj.createShadow(); 3629 }, 3630 3631 3632 _addChild: function (new1, isNotificationFromChild) { 3633 3634 this._iconNeedsUpdate = true; 3635 3636 this._boundsNeedUpdate = true; 3637 this._setClusterCenter(new1); 3638 3639 if (new1 instanceof L.MarkerCluster) { 3640 if (!isNotificationFromChild) { 3641 this._childClusters.push(new1); 3642 new1.__parent = this; 3643 } 3644 this._childCount += new1._childCount; 3645 } else { 3646 if (!isNotificationFromChild) { 3647 this._markers.push(new1); 3648 } 3649 this._childCount++; 3650 } 3651 3652 if (this.__parent) { 3653 this.__parent._addChild(new1, true); 3654 } 3655 }, 3656 3657 /** 3658 * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position. 3659 * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet. 3660 * @private 3661 */ 3662 _setClusterCenter: function (child) { 3663 if (!this._cLatLng) { 3664 // when clustering, take position of the first point as the cluster center 3665 this._cLatLng = child._cLatLng || child._latlng; 3666 } 3667 }, 3668 3669 /** 3670 * Assigns impossible bounding values so that the next extend entirely determines the new bounds. 3671 * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class. 3672 * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended. 3673 * @private 3674 */ 3675 _resetBounds: function () { 3676 var bounds = this._bounds; 3677 3678 if (bounds._southWest) { 3679 bounds._southWest.lat = Infinity; 3680 bounds._southWest.lng = Infinity; 3681 } 3682 if (bounds._northEast) { 3683 bounds._northEast.lat = -Infinity; 3684 bounds._northEast.lng = -Infinity; 3685 } 3686 }, 3687 3688 _recalculateBounds: function () { 3689 var markers = this._markers, 3690 childClusters = this._childClusters, 3691 latSum = 0, 3692 lngSum = 0, 3693 totalCount = this._childCount, 3694 i, child, childLatLng, childCount; 3695 3696 // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel. 3697 if (totalCount === 0) { 3698 return; 3699 } 3700 3701 // Reset rather than creating a new object, for performance. 3702 this._resetBounds(); 3703 3704 // Child markers. 3705 for (i = 0; i < markers.length; i++) { 3706 childLatLng = markers[i]._latlng; 3707 3708 this._bounds.extend(childLatLng); 3709 3710 latSum += childLatLng.lat; 3711 lngSum += childLatLng.lng; 3712 } 3713 3714 // Child clusters. 3715 for (i = 0; i < childClusters.length; i++) { 3716 child = childClusters[i]; 3717 3718 // Re-compute child bounds and weighted position first if necessary. 3719 if (child._boundsNeedUpdate) { 3720 child._recalculateBounds(); 3721 } 3722 3723 this._bounds.extend(child._bounds); 3724 3725 childLatLng = child._wLatLng; 3726 childCount = child._childCount; 3727 3728 latSum += childLatLng.lat * childCount; 3729 lngSum += childLatLng.lng * childCount; 3730 } 3731 3732 this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount); 3733 3734 // Reset dirty flag. 3735 this._boundsNeedUpdate = false; 3736 }, 3737 3738 //Set our markers position as given and add it to the map 3739 _addToMap: function (startPos) { 3740 if (startPos) { 3741 this._backupLatlng = this._latlng; 3742 this.setLatLng(startPos); 3743 } 3744 this._group._featureGroup.addLayer(this); 3745 }, 3746 3747 _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { 3748 this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1, 3749 function (c) { 3750 var markers = c._markers, 3751 i, m; 3752 for (i = markers.length - 1; i >= 0; i--) { 3753 m = markers[i]; 3754 3755 //Only do it if the icon is still on the map 3756 if (m._icon) { 3757 m._setPos(center); 3758 m.clusterHide(); 3759 } 3760 } 3761 }, 3762 function (c) { 3763 var childClusters = c._childClusters, 3764 j, cm; 3765 for (j = childClusters.length - 1; j >= 0; j--) { 3766 cm = childClusters[j]; 3767 if (cm._icon) { 3768 cm._setPos(center); 3769 cm.clusterHide(); 3770 } 3771 } 3772 } 3773 ); 3774 }, 3775 3776 _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) { 3777 this._recursively(bounds, newZoomLevel, mapMinZoom, 3778 function (c) { 3779 c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); 3780 3781 //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. 3782 //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate 3783 if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { 3784 c.clusterShow(); 3785 c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds 3786 } else { 3787 c.clusterHide(); 3788 } 3789 3790 c._addToMap(); 3791 } 3792 ); 3793 }, 3794 3795 _recursivelyBecomeVisible: function (bounds, zoomLevel) { 3796 this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) { 3797 c.clusterShow(); 3798 }); 3799 }, 3800 3801 _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { 3802 this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel, 3803 function (c) { 3804 if (zoomLevel === c._zoom) { 3805 return; 3806 } 3807 3808 //Add our child markers at startPos (so they can be animated out) 3809 for (var i = c._markers.length - 1; i >= 0; i--) { 3810 var nm = c._markers[i]; 3811 3812 if (!bounds.contains(nm._latlng)) { 3813 continue; 3814 } 3815 3816 if (startPos) { 3817 nm._backupLatlng = nm.getLatLng(); 3818 3819 nm.setLatLng(startPos); 3820 if (nm.clusterHide) { 3821 nm.clusterHide(); 3822 } 3823 } 3824 3825 c._group._featureGroup.addLayer(nm); 3826 } 3827 }, 3828 function (c) { 3829 c._addToMap(startPos); 3830 } 3831 ); 3832 }, 3833 3834 _recursivelyRestoreChildPositions: function (zoomLevel) { 3835 //Fix positions of child markers 3836 for (var i = this._markers.length - 1; i >= 0; i--) { 3837 var nm = this._markers[i]; 3838 if (nm._backupLatlng) { 3839 nm.setLatLng(nm._backupLatlng); 3840 delete nm._backupLatlng; 3841 } 3842 } 3843 3844 if (zoomLevel - 1 === this._zoom) { 3845 //Reposition child clusters 3846 for (var j = this._childClusters.length - 1; j >= 0; j--) { 3847 this._childClusters[j]._restorePosition(); 3848 } 3849 } else { 3850 for (var k = this._childClusters.length - 1; k >= 0; k--) { 3851 this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); 3852 } 3853 } 3854 }, 3855 3856 _restorePosition: function () { 3857 if (this._backupLatlng) { 3858 this.setLatLng(this._backupLatlng); 3859 delete this._backupLatlng; 3860 } 3861 }, 3862 3863 //exceptBounds: If set, don't remove any markers/clusters in it 3864 _recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) { 3865 var m, i; 3866 this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1, 3867 function (c) { 3868 //Remove markers at every level 3869 for (i = c._markers.length - 1; i >= 0; i--) { 3870 m = c._markers[i]; 3871 if (!exceptBounds || !exceptBounds.contains(m._latlng)) { 3872 c._group._featureGroup.removeLayer(m); 3873 if (m.clusterShow) { 3874 m.clusterShow(); 3875 } 3876 } 3877 } 3878 }, 3879 function (c) { 3880 //Remove child clusters at just the bottom level 3881 for (i = c._childClusters.length - 1; i >= 0; i--) { 3882 m = c._childClusters[i]; 3883 if (!exceptBounds || !exceptBounds.contains(m._latlng)) { 3884 c._group._featureGroup.removeLayer(m); 3885 if (m.clusterShow) { 3886 m.clusterShow(); 3887 } 3888 } 3889 } 3890 } 3891 ); 3892 }, 3893 3894 //Run the given functions recursively to this and child clusters 3895 // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to 3896 // zoomLevelToStart: zoom level to start running functions (inclusive) 3897 // zoomLevelToStop: zoom level to stop running functions (inclusive) 3898 // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level 3899 // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level 3900 _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { 3901 var childClusters = this._childClusters, 3902 zoom = this._zoom, 3903 i, c; 3904 3905 if (zoomLevelToStart <= zoom) { 3906 if (runAtEveryLevel) { 3907 runAtEveryLevel(this); 3908 } 3909 if (runAtBottomLevel && zoom === zoomLevelToStop) { 3910 runAtBottomLevel(this); 3911 } 3912 } 3913 3914 if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) { 3915 for (i = childClusters.length - 1; i >= 0; i--) { 3916 c = childClusters[i]; 3917 if (c._boundsNeedUpdate) { 3918 c._recalculateBounds(); 3919 } 3920 if (boundsToApplyTo.intersects(c._bounds)) { 3921 c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); 3922 } 3923 } 3924 } 3925 }, 3926 3927 //Returns true if we are the parent of only one cluster and that cluster is the same as us 3928 _isSingleParent: function () { 3929 //Don't need to check this._markers as the rest won't work if there are any 3930 return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; 3931 } 3932 }); 3933 3934 /* 3935 * Extends L.Marker to include two extra methods: clusterHide and clusterShow. 3936 * 3937 * They work as setOpacity(0) and setOpacity(1) respectively, but 3938 * don't overwrite the options.opacity 3939 * 3940 */ 3941 3942 L.Marker.include({ 3943 clusterHide: function () { 3944 var backup = this.options.opacity; 3945 this.setOpacity(0); 3946 this.options.opacity = backup; 3947 return this; 3948 }, 3949 3950 clusterShow: function () { 3951 return this.setOpacity(this.options.opacity); 3952 } 3953 }); 3954 3955 L.DistanceGrid = function (cellSize) { 3956 this._cellSize = cellSize; 3957 this._sqCellSize = cellSize * cellSize; 3958 this._grid = {}; 3959 this._objectPoint = { }; 3960 }; 3961 3962 L.DistanceGrid.prototype = { 3963 3964 addObject: function (obj, point) { 3965 var x = this._getCoord(point.x), 3966 y = this._getCoord(point.y), 3967 grid = this._grid, 3968 row = grid[y] = grid[y] || {}, 3969 cell = row[x] = row[x] || [], 3970 stamp = L.Util.stamp(obj); 3971 3972 this._objectPoint[stamp] = point; 3973 3974 cell.push(obj); 3975 }, 3976 3977 updateObject: function (obj, point) { 3978 this.removeObject(obj); 3979 this.addObject(obj, point); 3980 }, 3981 3982 //Returns true if the object was found 3983 removeObject: function (obj, point) { 3984 var x = this._getCoord(point.x), 3985 y = this._getCoord(point.y), 3986 grid = this._grid, 3987 row = grid[y] = grid[y] || {}, 3988 cell = row[x] = row[x] || [], 3989 i, len; 3990 3991 delete this._objectPoint[L.Util.stamp(obj)]; 3992 3993 for (i = 0, len = cell.length; i < len; i++) { 3994 if (cell[i] === obj) { 3995 3996 cell.splice(i, 1); 3997 3998 if (len === 1) { 3999 delete row[x]; 4000 } 4001 4002 return true; 4003 } 4004 } 4005 4006 }, 4007 4008 eachObject: function (fn, context) { 4009 var i, j, k, len, row, cell, removed, 4010 grid = this._grid; 4011 4012 for (i in grid) { 4013 row = grid[i]; 4014 4015 for (j in row) { 4016 cell = row[j]; 4017 4018 for (k = 0, len = cell.length; k < len; k++) { 4019 removed = fn.call(context, cell[k]); 4020 if (removed) { 4021 k--; 4022 len--; 4023 } 4024 } 4025 } 4026 } 4027 }, 4028 4029 getNearObject: function (point) { 4030 var x = this._getCoord(point.x), 4031 y = this._getCoord(point.y), 4032 i, j, k, row, cell, len, obj, dist, 4033 objectPoint = this._objectPoint, 4034 closestDistSq = this._sqCellSize, 4035 closest = null; 4036 4037 for (i = y - 1; i <= y + 1; i++) { 4038 row = this._grid[i]; 4039 if (row) { 4040 4041 for (j = x - 1; j <= x + 1; j++) { 4042 cell = row[j]; 4043 if (cell) { 4044 4045 for (k = 0, len = cell.length; k < len; k++) { 4046 obj = cell[k]; 4047 dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); 4048 if (dist < closestDistSq || 4049 dist <= closestDistSq && closest === null) { 4050 closestDistSq = dist; 4051 closest = obj; 4052 } 4053 } 4054 } 4055 } 4056 } 4057 } 4058 return closest; 4059 }, 4060 4061 _getCoord: function (x) { 4062 var coord = Math.floor(x / this._cellSize); 4063 return isFinite(coord) ? coord : x; 4064 }, 4065 4066 _sqDist: function (p, p2) { 4067 var dx = p2.x - p.x, 4068 dy = p2.y - p.y; 4069 return dx * dx + dy * dy; 4070 } 4071 }; 4072 4073 /* Copyright (c) 2012 the authors listed at the following URL, and/or 4074 the authors of referenced articles or incorporated external code: 4075 http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 4076 4077 Permission is hereby granted, free of charge, to any person obtaining 4078 a copy of this software and associated documentation files (the 4079 "Software"), to deal in the Software without restriction, including 4080 without limitation the rights to use, copy, modify, merge, publish, 4081 distribute, sublicense, and/or sell copies of the Software, and to 4082 permit persons to whom the Software is furnished to do so, subject to 4083 the following conditions: 4084 4085 The above copyright notice and this permission notice shall be 4086 included in all copies or substantial portions of the Software. 4087 4088 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 4089 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 4090 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 4091 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 4092 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 4093 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 4094 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 4095 4096 Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 4097 */ 4098 4099 (function () { 4100 L.QuickHull = { 4101 4102 /* 4103 * @param {Object} cpt a point to be measured from the baseline 4104 * @param {Array} bl the baseline, as represented by a two-element 4105 * array of latlng objects. 4106 * @returns {Number} an approximate distance measure 4107 */ 4108 getDistant: function (cpt, bl) { 4109 var vY = bl[1].lat - bl[0].lat, 4110 vX = bl[0].lng - bl[1].lng; 4111 return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); 4112 }, 4113 4114 /* 4115 * @param {Array} baseLine a two-element array of latlng objects 4116 * representing the baseline to project from 4117 * @param {Array} latLngs an array of latlng objects 4118 * @returns {Object} the maximum point and all new points to stay 4119 * in consideration for the hull. 4120 */ 4121 findMostDistantPointFromBaseLine: function (baseLine, latLngs) { 4122 var maxD = 0, 4123 maxPt = null, 4124 newPoints = [], 4125 i, pt, d; 4126 4127 for (i = latLngs.length - 1; i >= 0; i--) { 4128 pt = latLngs[i]; 4129 d = this.getDistant(pt, baseLine); 4130 4131 if (d > 0) { 4132 newPoints.push(pt); 4133 } else { 4134 continue; 4135 } 4136 4137 if (d > maxD) { 4138 maxD = d; 4139 maxPt = pt; 4140 } 4141 } 4142 4143 return { maxPoint: maxPt, newPoints: newPoints }; 4144 }, 4145 4146 4147 /* 4148 * Given a baseline, compute the convex hull of latLngs as an array 4149 * of latLngs. 4150 * 4151 * @param {Array} latLngs 4152 * @returns {Array} 4153 */ 4154 buildConvexHull: function (baseLine, latLngs) { 4155 var convexHullBaseLines = [], 4156 t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); 4157 4158 if (t.maxPoint) { // if there is still a point "outside" the base line 4159 convexHullBaseLines = 4160 convexHullBaseLines.concat( 4161 this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) 4162 ); 4163 convexHullBaseLines = 4164 convexHullBaseLines.concat( 4165 this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) 4166 ); 4167 return convexHullBaseLines; 4168 } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull 4169 return [baseLine[0]]; 4170 } 4171 }, 4172 4173 /* 4174 * Given an array of latlngs, compute a convex hull as an array 4175 * of latlngs 4176 * 4177 * @param {Array} latLngs 4178 * @returns {Array} 4179 */ 4180 getConvexHull: function (latLngs) { 4181 // find first baseline 4182 var maxLat = false, minLat = false, 4183 maxLng = false, minLng = false, 4184 maxLatPt = null, minLatPt = null, 4185 maxLngPt = null, minLngPt = null, 4186 maxPt = null, minPt = null, 4187 i; 4188 4189 for (i = latLngs.length - 1; i >= 0; i--) { 4190 var pt = latLngs[i]; 4191 if (maxLat === false || pt.lat > maxLat) { 4192 maxLatPt = pt; 4193 maxLat = pt.lat; 4194 } 4195 if (minLat === false || pt.lat < minLat) { 4196 minLatPt = pt; 4197 minLat = pt.lat; 4198 } 4199 if (maxLng === false || pt.lng > maxLng) { 4200 maxLngPt = pt; 4201 maxLng = pt.lng; 4202 } 4203 if (minLng === false || pt.lng < minLng) { 4204 minLngPt = pt; 4205 minLng = pt.lng; 4206 } 4207 } 4208 4209 if (minLat !== maxLat) { 4210 minPt = minLatPt; 4211 maxPt = maxLatPt; 4212 } else { 4213 minPt = minLngPt; 4214 maxPt = maxLngPt; 4215 } 4216 4217 var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), 4218 this.buildConvexHull([maxPt, minPt], latLngs)); 4219 return ch; 4220 } 4221 }; 4222 }()); 4223 4224 L.MarkerCluster.include({ 4225 getConvexHull: function () { 4226 var childMarkers = this.getAllChildMarkers(), 4227 points = [], 4228 p, i; 4229 4230 for (i = childMarkers.length - 1; i >= 0; i--) { 4231 p = childMarkers[i].getLatLng(); 4232 points.push(p); 4233 } 4234 4235 return L.QuickHull.getConvexHull(points); 4236 } 4237 }); 4238 4239 //This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet 4240 //Huge thanks to jawj for implementing it first to make my job easy :-) 4241 4242 L.MarkerCluster.include({ 4243 4244 _2PI: Math.PI * 2, 4245 _circleFootSeparation: 25, //related to circumference of circle 4246 _circleStartAngle: 0, 4247 4248 _spiralFootSeparation: 28, //related to size of spiral (experiment!) 4249 _spiralLengthStart: 11, 4250 _spiralLengthFactor: 5, 4251 4252 _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. 4253 // 0 -> always spiral; Infinity -> always circle 4254 4255 spiderfy: function () { 4256 if (this._group._spiderfied === this || this._group._inZoomAnimation) { 4257 return; 4258 } 4259 4260 var childMarkers = this.getAllChildMarkers(null, true), 4261 group = this._group, 4262 map = group._map, 4263 center = map.latLngToLayerPoint(this._latlng), 4264 positions; 4265 4266 this._group._unspiderfy(); 4267 this._group._spiderfied = this; 4268 4269 //TODO Maybe: childMarkers order by distance to center 4270 4271 if (this._group.options.spiderfyShapePositions) { 4272 positions = this._group.options.spiderfyShapePositions(childMarkers.length, center); 4273 } else if (childMarkers.length >= this._circleSpiralSwitchover) { 4274 positions = this._generatePointsSpiral(childMarkers.length, center); 4275 } else { 4276 center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons. 4277 positions = this._generatePointsCircle(childMarkers.length, center); 4278 } 4279 4280 this._animationSpiderfy(childMarkers, positions); 4281 }, 4282 4283 unspiderfy: function (zoomDetails) { 4284 /// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param> 4285 if (this._group._inZoomAnimation) { 4286 return; 4287 } 4288 this._animationUnspiderfy(zoomDetails); 4289 4290 this._group._spiderfied = null; 4291 }, 4292 4293 _generatePointsCircle: function (count, centerPt) { 4294 var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), 4295 legLength = circumference / this._2PI, //radius from circumference 4296 angleStep = this._2PI / count, 4297 res = [], 4298 i, angle; 4299 4300 legLength = Math.max(legLength, 35); // Minimum distance to get outside the cluster icon. 4301 4302 res.length = count; 4303 4304 for (i = 0; i < count; i++) { // Clockwise, like spiral. 4305 angle = this._circleStartAngle + i * angleStep; 4306 res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 4307 } 4308 4309 return res; 4310 }, 4311 4312 _generatePointsSpiral: function (count, centerPt) { 4313 var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, 4314 legLength = spiderfyDistanceMultiplier * this._spiralLengthStart, 4315 separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, 4316 lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, 4317 angle = 0, 4318 res = [], 4319 i; 4320 4321 res.length = count; 4322 4323 // Higher index, closer position to cluster center. 4324 for (i = count; i >= 0; i--) { 4325 // Skip the first position, so that we are already farther from center and we avoid 4326 // being under the default cluster icon (especially important for Circle Markers). 4327 if (i < count) { 4328 res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 4329 } 4330 angle += separation / legLength + i * 0.0005; 4331 legLength += lengthFactor / angle; 4332 } 4333 return res; 4334 }, 4335 4336 _noanimationUnspiderfy: function () { 4337 var group = this._group, 4338 map = group._map, 4339 fg = group._featureGroup, 4340 childMarkers = this.getAllChildMarkers(null, true), 4341 m, i; 4342 4343 group._ignoreMove = true; 4344 4345 this.setOpacity(1); 4346 for (i = childMarkers.length - 1; i >= 0; i--) { 4347 m = childMarkers[i]; 4348 4349 fg.removeLayer(m); 4350 4351 if (m._preSpiderfyLatlng) { 4352 m.setLatLng(m._preSpiderfyLatlng); 4353 delete m._preSpiderfyLatlng; 4354 } 4355 if (m.setZIndexOffset) { 4356 m.setZIndexOffset(0); 4357 } 4358 4359 if (m._spiderLeg) { 4360 map.removeLayer(m._spiderLeg); 4361 delete m._spiderLeg; 4362 } 4363 } 4364 4365 group.fire('unspiderfied', { 4366 cluster: this, 4367 markers: childMarkers 4368 }); 4369 group._ignoreMove = false; 4370 group._spiderfied = null; 4371 } 4372 }); 4373 4374 //Non Animated versions of everything 4375 L.MarkerClusterNonAnimated = L.MarkerCluster.extend({ 4376 _animationSpiderfy: function (childMarkers, positions) { 4377 var group = this._group, 4378 map = group._map, 4379 fg = group._featureGroup, 4380 legOptions = this._group.options.spiderLegPolylineOptions, 4381 i, m, leg, newPos; 4382 4383 group._ignoreMove = true; 4384 4385 // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. 4386 // The reverse order trick no longer improves performance on modern browsers. 4387 for (i = 0; i < childMarkers.length; i++) { 4388 newPos = map.layerPointToLatLng(positions[i]); 4389 m = childMarkers[i]; 4390 4391 // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. 4392 leg = new L.Polyline([this._latlng, newPos], legOptions); 4393 map.addLayer(leg); 4394 m._spiderLeg = leg; 4395 4396 // Now add the marker. 4397 m._preSpiderfyLatlng = m._latlng; 4398 m.setLatLng(newPos); 4399 if (m.setZIndexOffset) { 4400 m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING 4401 } 4402 4403 fg.addLayer(m); 4404 } 4405 this.setOpacity(0.3); 4406 4407 group._ignoreMove = false; 4408 group.fire('spiderfied', { 4409 cluster: this, 4410 markers: childMarkers 4411 }); 4412 }, 4413 4414 _animationUnspiderfy: function () { 4415 this._noanimationUnspiderfy(); 4416 } 4417 }); 4418 4419 //Animated versions here 4420 L.MarkerCluster.include({ 4421 4422 _animationSpiderfy: function (childMarkers, positions) { 4423 var me = this, 4424 group = this._group, 4425 map = group._map, 4426 fg = group._featureGroup, 4427 thisLayerLatLng = this._latlng, 4428 thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng), 4429 svg = L.Path.SVG, 4430 legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation. 4431 finalLegOpacity = legOptions.opacity, 4432 i, m, leg, legPath, legLength, newPos; 4433 4434 if (finalLegOpacity === undefined) { 4435 finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity; 4436 } 4437 4438 if (svg) { 4439 // If the initial opacity of the spider leg is not 0 then it appears before the animation starts. 4440 legOptions.opacity = 0; 4441 4442 // Add the class for CSS transitions. 4443 legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg'; 4444 } else { 4445 // Make sure we have a defined opacity. 4446 legOptions.opacity = finalLegOpacity; 4447 } 4448 4449 group._ignoreMove = true; 4450 4451 // Add markers and spider legs to map, hidden at our center point. 4452 // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. 4453 // The reverse order trick no longer improves performance on modern browsers. 4454 for (i = 0; i < childMarkers.length; i++) { 4455 m = childMarkers[i]; 4456 4457 newPos = map.layerPointToLatLng(positions[i]); 4458 4459 // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. 4460 leg = new L.Polyline([thisLayerLatLng, newPos], legOptions); 4461 map.addLayer(leg); 4462 m._spiderLeg = leg; 4463 4464 // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/ 4465 // In our case the transition property is declared in the CSS file. 4466 if (svg) { 4467 legPath = leg._path; 4468 legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox. 4469 legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated. 4470 legPath.style.strokeDashoffset = legLength; 4471 } 4472 4473 // If it is a marker, add it now and we'll animate it out 4474 if (m.setZIndexOffset) { 4475 m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING 4476 } 4477 if (m.clusterHide) { 4478 m.clusterHide(); 4479 } 4480 4481 // Vectors just get immediately added 4482 fg.addLayer(m); 4483 4484 if (m._setPos) { 4485 m._setPos(thisLayerPos); 4486 } 4487 } 4488 4489 group._forceLayout(); 4490 group._animationStart(); 4491 4492 // Reveal markers and spider legs. 4493 for (i = childMarkers.length - 1; i >= 0; i--) { 4494 newPos = map.layerPointToLatLng(positions[i]); 4495 m = childMarkers[i]; 4496 4497 //Move marker to new position 4498 m._preSpiderfyLatlng = m._latlng; 4499 m.setLatLng(newPos); 4500 4501 if (m.clusterShow) { 4502 m.clusterShow(); 4503 } 4504 4505 // Animate leg (animation is actually delegated to CSS transition). 4506 if (svg) { 4507 leg = m._spiderLeg; 4508 legPath = leg._path; 4509 legPath.style.strokeDashoffset = 0; 4510 //legPath.style.strokeOpacity = finalLegOpacity; 4511 leg.setStyle({opacity: finalLegOpacity}); 4512 } 4513 } 4514 this.setOpacity(0.3); 4515 4516 group._ignoreMove = false; 4517 4518 setTimeout(function () { 4519 group._animationEnd(); 4520 group.fire('spiderfied', { 4521 cluster: me, 4522 markers: childMarkers 4523 }); 4524 }, 200); 4525 }, 4526 4527 _animationUnspiderfy: function (zoomDetails) { 4528 var me = this, 4529 group = this._group, 4530 map = group._map, 4531 fg = group._featureGroup, 4532 thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), 4533 childMarkers = this.getAllChildMarkers(null, true), 4534 svg = L.Path.SVG, 4535 m, i, leg, legPath, legLength, nonAnimatable; 4536 4537 group._ignoreMove = true; 4538 group._animationStart(); 4539 4540 //Make us visible and bring the child markers back in 4541 this.setOpacity(1); 4542 for (i = childMarkers.length - 1; i >= 0; i--) { 4543 m = childMarkers[i]; 4544 4545 //Marker was added to us after we were spiderfied 4546 if (!m._preSpiderfyLatlng) { 4547 continue; 4548 } 4549 4550 //Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll 4551 m.closePopup(); 4552 4553 //Fix up the location to the real one 4554 m.setLatLng(m._preSpiderfyLatlng); 4555 delete m._preSpiderfyLatlng; 4556 4557 //Hack override the location to be our center 4558 nonAnimatable = true; 4559 if (m._setPos) { 4560 m._setPos(thisLayerPos); 4561 nonAnimatable = false; 4562 } 4563 if (m.clusterHide) { 4564 m.clusterHide(); 4565 nonAnimatable = false; 4566 } 4567 if (nonAnimatable) { 4568 fg.removeLayer(m); 4569 } 4570 4571 // Animate the spider leg back in (animation is actually delegated to CSS transition). 4572 if (svg) { 4573 leg = m._spiderLeg; 4574 legPath = leg._path; 4575 legLength = legPath.getTotalLength() + 0.1; 4576 legPath.style.strokeDashoffset = legLength; 4577 leg.setStyle({opacity: 0}); 4578 } 4579 } 4580 4581 group._ignoreMove = false; 4582 4583 setTimeout(function () { 4584 //If we have only <= one child left then that marker will be shown on the map so don't remove it! 4585 var stillThereChildCount = 0; 4586 for (i = childMarkers.length - 1; i >= 0; i--) { 4587 m = childMarkers[i]; 4588 if (m._spiderLeg) { 4589 stillThereChildCount++; 4590 } 4591 } 4592 4593 4594 for (i = childMarkers.length - 1; i >= 0; i--) { 4595 m = childMarkers[i]; 4596 4597 if (!m._spiderLeg) { //Has already been unspiderfied 4598 continue; 4599 } 4600 4601 if (m.clusterShow) { 4602 m.clusterShow(); 4603 } 4604 if (m.setZIndexOffset) { 4605 m.setZIndexOffset(0); 4606 } 4607 4608 if (stillThereChildCount > 1) { 4609 fg.removeLayer(m); 4610 } 4611 4612 map.removeLayer(m._spiderLeg); 4613 delete m._spiderLeg; 4614 } 4615 group._animationEnd(); 4616 group.fire('unspiderfied', { 4617 cluster: me, 4618 markers: childMarkers 4619 }); 4620 }, 200); 4621 } 4622 }); 4623 4624 4625 L.MarkerClusterGroup.include({ 4626 //The MarkerCluster currently spiderfied (if any) 4627 _spiderfied: null, 4628 4629 unspiderfy: function () { 4630 this._unspiderfy.apply(this, arguments); 4631 }, 4632 4633 _spiderfierOnAdd: function () { 4634 this._map.on('click', this._unspiderfyWrapper, this); 4635 4636 if (this._map.options.zoomAnimation) { 4637 this._map.on('zoomstart', this._unspiderfyZoomStart, this); 4638 } 4639 //Browsers without zoomAnimation or a big zoom don't fire zoomstart 4640 this._map.on('zoomend', this._noanimationUnspiderfy, this); 4641 4642 if (!L.Browser.touch) { 4643 this._map.getRenderer(this); 4644 //Needs to happen in the pageload, not after, or animations don't work in webkit 4645 // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements 4646 //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable 4647 } 4648 }, 4649 4650 _spiderfierOnRemove: function () { 4651 this._map.off('click', this._unspiderfyWrapper, this); 4652 this._map.off('zoomstart', this._unspiderfyZoomStart, this); 4653 this._map.off('zoomanim', this._unspiderfyZoomAnim, this); 4654 this._map.off('zoomend', this._noanimationUnspiderfy, this); 4655 4656 //Ensure that markers are back where they should be 4657 // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane 4658 this._noanimationUnspiderfy(); 4659 }, 4660 4661 //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) 4662 //This means we can define the animation they do rather than Markers doing an animation to their actual location 4663 _unspiderfyZoomStart: function () { 4664 if (!this._map) { //May have been removed from the map by a zoomEnd handler 4665 return; 4666 } 4667 4668 this._map.on('zoomanim', this._unspiderfyZoomAnim, this); 4669 }, 4670 4671 _unspiderfyZoomAnim: function (zoomDetails) { 4672 //Wait until the first zoomanim after the user has finished touch-zooming before running the animation 4673 if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { 4674 return; 4675 } 4676 4677 this._map.off('zoomanim', this._unspiderfyZoomAnim, this); 4678 this._unspiderfy(zoomDetails); 4679 }, 4680 4681 _unspiderfyWrapper: function () { 4682 /// <summary>_unspiderfy but passes no arguments</summary> 4683 this._unspiderfy(); 4684 }, 4685 4686 _unspiderfy: function (zoomDetails) { 4687 if (this._spiderfied) { 4688 this._spiderfied.unspiderfy(zoomDetails); 4689 } 4690 }, 4691 4692 _noanimationUnspiderfy: function () { 4693 if (this._spiderfied) { 4694 this._spiderfied._noanimationUnspiderfy(); 4695 } 4696 }, 4697 4698 //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc 4699 _unspiderfyLayer: function (layer) { 4700 if (layer._spiderLeg) { 4701 this._featureGroup.removeLayer(layer); 4702 4703 if (layer.clusterShow) { 4704 layer.clusterShow(); 4705 } 4706 //Position will be fixed up immediately in _animationUnspiderfy 4707 if (layer.setZIndexOffset) { 4708 layer.setZIndexOffset(0); 4709 } 4710 4711 this._map.removeLayer(layer._spiderLeg); 4712 delete layer._spiderLeg; 4713 } 4714 } 4715 }); 4716 4717 /** 4718 * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing 4719 * markers' icon options and refreshing their icon and their parent clusters 4720 * accordingly (case where their iconCreateFunction uses data of childMarkers 4721 * to make up the cluster icon). 4722 */ 4723 4724 4725 L.MarkerClusterGroup.include({ 4726 /** 4727 * Updates the icon of all clusters which are parents of the given marker(s). 4728 * In singleMarkerMode, also updates the given marker(s) icon. 4729 * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)| 4730 * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent 4731 * clusters need to be updated. If not provided, retrieves all child markers of this. 4732 * @returns {L.MarkerClusterGroup} 4733 */ 4734 refreshClusters: function (layers) { 4735 if (!layers) { 4736 layers = this._topClusterLevel.getAllChildMarkers(); 4737 } else if (layers instanceof L.MarkerClusterGroup) { 4738 layers = layers._topClusterLevel.getAllChildMarkers(); 4739 } else if (layers instanceof L.LayerGroup) { 4740 layers = layers._layers; 4741 } else if (layers instanceof L.MarkerCluster) { 4742 layers = layers.getAllChildMarkers(); 4743 } else if (layers instanceof L.Marker) { 4744 layers = [layers]; 4745 } // else: must be an Array(L.Marker)|Map(L.Marker) 4746 this._flagParentsIconsNeedUpdate(layers); 4747 this._refreshClustersIcons(); 4748 4749 // In case of singleMarkerMode, also re-draw the markers. 4750 if (this.options.singleMarkerMode) { 4751 this._refreshSingleMarkerModeMarkers(layers); 4752 } 4753 4754 return this; 4755 }, 4756 4757 /** 4758 * Simply flags all parent clusters of the given markers as having a "dirty" icon. 4759 * @param layers Array(L.Marker)|Map(L.Marker) list of markers. 4760 * @private 4761 */ 4762 _flagParentsIconsNeedUpdate: function (layers) { 4763 var id, parent; 4764 4765 // Assumes layers is an Array or an Object whose prototype is non-enumerable. 4766 for (id in layers) { 4767 // Flag parent clusters' icon as "dirty", all the way up. 4768 // Dumb process that flags multiple times upper parents, but still 4769 // much more efficient than trying to be smart and make short lists, 4770 // at least in the case of a hierarchy following a power law: 4771 // http://jsperf.com/flag-nodes-in-power-hierarchy/2 4772 parent = layers[id].__parent; 4773 while (parent) { 4774 parent._iconNeedsUpdate = true; 4775 parent = parent.__parent; 4776 } 4777 } 4778 }, 4779 4780 /** 4781 * Re-draws the icon of the supplied markers. 4782 * To be used in singleMarkerMode only. 4783 * @param layers Array(L.Marker)|Map(L.Marker) list of markers. 4784 * @private 4785 */ 4786 _refreshSingleMarkerModeMarkers: function (layers) { 4787 var id, layer; 4788 4789 for (id in layers) { 4790 layer = layers[id]; 4791 4792 // Make sure we do not override markers that do not belong to THIS group. 4793 if (this.hasLayer(layer)) { 4794 // Need to re-create the icon first, then re-draw the marker. 4795 layer.setIcon(this._overrideMarkerIcon(layer)); 4796 } 4797 } 4798 } 4799 }); 4800 4801 L.Marker.include({ 4802 /** 4803 * Updates the given options in the marker's icon and refreshes the marker. 4804 * @param options map object of icon options. 4805 * @param directlyRefreshClusters boolean (optional) true to trigger 4806 * MCG.refreshClustersOf() right away with this single marker. 4807 * @returns {L.Marker} 4808 */ 4809 refreshIconOptions: function (options, directlyRefreshClusters) { 4810 var icon = this.options.icon; 4811 4812 L.setOptions(icon, options); 4813 4814 this.setIcon(icon); 4815 4816 // Shortcut to refresh the associated MCG clusters right away. 4817 // To be used when refreshing a single marker. 4818 // Otherwise, better use MCG.refreshClusters() once at the end with 4819 // the list of modified markers. 4820 if (directlyRefreshClusters && this.__parent) { 4821 this.__parent._group.refreshClusters(this); 4822 } 4823 4824 return this; 4825 } 4826 }); 4827 4828 exports.MarkerClusterGroup = MarkerClusterGroup; 4829 exports.MarkerCluster = MarkerCluster; 4830 4831 Object.defineProperty(exports, '__esModule', { value: true }); 4832 4833}));
4834 4835 4836},{}],7:[function(require,module,exports){
vendor: 450,189 bytes, lines 4837-19347
4837/* @preserve 4838 * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com 4839 * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade 4840 */ 4841 4842(function (global, factory) { 4843 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 4844 typeof define === 'function' && define.amd ? define(['exports'], factory) : 4845 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.leaflet = {})); 4846})(this, (function (exports) { 'use strict'; 4847 4848 var version = "1.9.4"; 4849 4850 /* 4851 * @namespace Util 4852 * 4853 * Various utility functions, used by Leaflet internally. 4854 */ 4855 4856 // @function extend(dest: Object, src?: Object): Object 4857 // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut. 4858 function extend(dest) { 4859 var i, j, len, src; 4860 4861 for (j = 1, len = arguments.length; j < len; j++) { 4862 src = arguments[j]; 4863 for (i in src) { 4864 dest[i] = src[i]; 4865 } 4866 } 4867 return dest; 4868 } 4869 4870 // @function create(proto: Object, properties?: Object): Object 4871 // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create) 4872 var create$2 = Object.create || (function () { 4873 function F() {} 4874 return function (proto) { 4875 F.prototype = proto; 4876 return new F(); 4877 }; 4878 })(); 4879 4880 // @function bind(fn: Function, â¦): Function 4881 // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). 4882 // Has a `L.bind()` shortcut. 4883 function bind(fn, obj) { 4884 var slice = Array.prototype.slice; 4885 4886 if (fn.bind) { 4887 return fn.bind.apply(fn, slice.call(arguments, 1)); 4888 } 4889 4890 var args = slice.call(arguments, 2); 4891 4892 return function () { 4893 return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); 4894 }; 4895 } 4896 4897 // @property lastId: Number 4898 // Last unique ID used by [`stamp()`](#util-stamp) 4899 var lastId = 0; 4900 4901 // @function stamp(obj: Object): Number 4902 // Returns the unique ID of an object, assigning it one if it doesn't have it. 4903 function stamp(obj) { 4904 if (!('_leaflet_id' in obj)) { 4905 obj['_leaflet_id'] = ++lastId; 4906 } 4907 return obj._leaflet_id; 4908 } 4909 4910 // @function throttle(fn: Function, time: Number, context: Object): Function 4911 // Returns a function which executes function `fn` with the given scope `context` 4912 // (so that the `this` keyword refers to `context` inside `fn`'s code). The function 4913 // `fn` will be called no more than one time per given amount of `time`. The arguments 4914 // received by the bound function will be any arguments passed when binding the 4915 // function, followed by any arguments passed when invoking the bound function. 4916 // Has an `L.throttle` shortcut. 4917 function throttle(fn, time, context) { 4918 var lock, args, wrapperFn, later; 4919 4920 later = function () { 4921 // reset lock and call if queued 4922 lock = false; 4923 if (args) { 4924 wrapperFn.apply(context, args); 4925 args = false; 4926 } 4927 }; 4928 4929 wrapperFn = function () { 4930 if (lock) { 4931 // called too soon, queue to call later 4932 args = arguments; 4933 4934 } else { 4935 // call and lock until later 4936 fn.apply(context, arguments); 4937 setTimeout(later, time); 4938 lock = true; 4939 } 4940 }; 4941 4942 return wrapperFn; 4943 } 4944 4945 // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number 4946 // Returns the number `num` modulo `range` in such a way so it lies within 4947 // `range[0]` and `range[1]`. The returned value will be always smaller than 4948 // `range[1]` unless `includeMax` is set to `true`. 4949 function wrapNum(x, range, includeMax) { 4950 var max = range[1], 4951 min = range[0], 4952 d = max - min; 4953 return x === max && includeMax ? x : ((x - min) % d + d) % d + min; 4954 } 4955 4956 // @function falseFn(): Function 4957 // Returns a function which always returns `false`. 4958 function falseFn() { return false; } 4959 4960 // @function formatNum(num: Number, precision?: Number|false): Number 4961 // Returns the number `num` rounded with specified `precision`. 4962 // The default `precision` value is 6 decimal places. 4963 // `false` can be passed to skip any processing (can be useful to avoid round-off errors). 4964 function formatNum(num, precision) { 4965 if (precision === false) { return num; } 4966 var pow = Math.pow(10, precision === undefined ? 6 : precision); 4967 return Math.round(num * pow) / pow; 4968 } 4969 4970 // @function trim(str: String): String 4971 // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) 4972 function trim(str) { 4973 return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); 4974 } 4975 4976 // @function splitWords(str: String): String[] 4977 // Trims and splits the string on whitespace and returns the array of parts. 4978 function splitWords(str) { 4979 return trim(str).split(/\s+/); 4980 } 4981 4982 // @function setOptions(obj: Object, options: Object): Object 4983 // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut. 4984 function setOptions(obj, options) { 4985 if (!Object.prototype.hasOwnProperty.call(obj, 'options')) { 4986 obj.options = obj.options ? create$2(obj.options) : {}; 4987 } 4988 for (var i in options) { 4989 obj.options[i] = options[i]; 4990 } 4991 return obj.options; 4992 } 4993 4994 // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String 4995 // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}` 4996 // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will 4997 // be appended at the end. If `uppercase` is `true`, the parameter names will 4998 // be uppercased (e.g. `'?A=foo&B=bar'`) 4999 function getParamString(obj, existingUrl, uppercase) { 5000 var params = []; 5001 for (var i in obj) { 5002 params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); 5003 } 5004 return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); 5005 } 5006 5007 var templateRe = /\{ *([\w_ -]+) *\}/g; 5008 5009 // @function template(str: String, data: Object): String 5010 // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` 5011 // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string 5012 // `('Hello foo, bar')`. You can also specify functions instead of strings for 5013 // data values â they will be evaluated passing `data` as an argument. 5014 function template(str, data) { 5015 return str.replace(templateRe, function (str, key) { 5016 var value = data[key]; 5017 5018 if (value === undefined) { 5019 throw new Error('No value provided for variable ' + str); 5020 5021 } else if (typeof value === 'function') { 5022 value = value(data); 5023 } 5024 return value; 5025 }); 5026 } 5027 5028 // @function isArray(obj): Boolean 5029 // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) 5030 var isArray = Array.isArray || function (obj) { 5031 return (Object.prototype.toString.call(obj) === '[object Array]'); 5032 }; 5033 5034 // @function indexOf(array: Array, el: Object): Number 5035 // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) 5036 function indexOf(array, el) { 5037 for (var i = 0; i < array.length; i++) { 5038 if (array[i] === el) { return i; } 5039 } 5040 return -1; 5041 } 5042 5043 // @property emptyImageUrl: String 5044 // Data URI string containing a base64-encoded empty GIF image. 5045 // Used as a hack to free memory from unused images on WebKit-powered 5046 // mobile devices (by setting image `src` to this string). 5047 var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; 5048 5049 // inspired by https://paulirish.com/2011/requestanimationframe-for-smart-animating/ 5050 5051 function getPrefixed(name) { 5052 return window['webkit' + name] || window['moz' + name] || window['ms' + name]; 5053 } 5054 5055 var lastTime = 0; 5056 5057 // fallback for IE 7-8 5058 function timeoutDefer(fn) { 5059 var time = +new Date(), 5060 timeToCall = Math.max(0, 16 - (time - lastTime)); 5061 5062 lastTime = time + timeToCall; 5063 return window.setTimeout(fn, timeToCall); 5064 } 5065 5066 var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer; 5067 var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || 5068 getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; 5069 5070 // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number 5071 // Schedules `fn` to be executed when the browser repaints. `fn` is bound to 5072 // `context` if given. When `immediate` is set, `fn` is called immediately if 5073 // the browser doesn't have native support for 5074 // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame), 5075 // otherwise it's delayed. Returns a request ID that can be used to cancel the request. 5076 function requestAnimFrame(fn, context, immediate) { 5077 if (immediate && requestFn === timeoutDefer) { 5078 fn.call(context); 5079 } else { 5080 return requestFn.call(window, bind(fn, context)); 5081 } 5082 } 5083 5084 // @function cancelAnimFrame(id: Number): undefined 5085 // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame). 5086 function cancelAnimFrame(id) { 5087 if (id) { 5088 cancelFn.call(window, id); 5089 } 5090 } 5091 5092 var Util = { 5093 __proto__: null, 5094 extend: extend, 5095 create: create$2, 5096 bind: bind, 5097 get lastId () { return lastId; }, 5098 stamp: stamp, 5099 throttle: throttle, 5100 wrapNum: wrapNum, 5101 falseFn: falseFn, 5102 formatNum: formatNum, 5103 trim: trim, 5104 splitWords: splitWords, 5105 setOptions: setOptions, 5106 getParamString: getParamString, 5107 template: template, 5108 isArray: isArray, 5109 indexOf: indexOf, 5110 emptyImageUrl: emptyImageUrl, 5111 requestFn: requestFn, 5112 cancelFn: cancelFn, 5113 requestAnimFrame: requestAnimFrame, 5114 cancelAnimFrame: cancelAnimFrame 5115 }; 5116 5117 // @class Class 5118 // @aka L.Class 5119 5120 // @section 5121 // @uninheritable 5122 5123 // Thanks to John Resig and Dean Edwards for inspiration! 5124 5125 function Class() {} 5126 5127 Class.extend = function (props) { 5128 5129 // @function extend(props: Object): Function 5130 // [Extends the current class](#class-inheritance) given the properties to be included. 5131 // Returns a Javascript function that is a class constructor (to be called with `new`). 5132 var NewClass = function () { 5133 5134 setOptions(this); 5135 5136 // call the constructor 5137 if (this.initialize) { 5138 this.initialize.apply(this, arguments); 5139 } 5140 5141 // call all constructor hooks 5142 this.callInitHooks(); 5143 }; 5144 5145 var parentProto = NewClass.__super__ = this.prototype; 5146 5147 var proto = create$2(parentProto); 5148 proto.constructor = NewClass; 5149 5150 NewClass.prototype = proto; 5151 5152 // inherit parent's statics 5153 for (var i in this) { 5154 if (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') { 5155 NewClass[i] = this[i]; 5156 } 5157 } 5158 5159 // mix static properties into the class 5160 if (props.statics) { 5161 extend(NewClass, props.statics); 5162 } 5163 5164 // mix includes into the prototype 5165 if (props.includes) { 5166 checkDeprecatedMixinEvents(props.includes); 5167 extend.apply(null, [proto].concat(props.includes)); 5168 } 5169 5170 // mix given properties into the prototype 5171 extend(proto, props); 5172 delete proto.statics; 5173 delete proto.includes; 5174 5175 // merge options 5176 if (proto.options) { 5177 proto.options = parentProto.options ? create$2(parentProto.options) : {}; 5178 extend(proto.options, props.options); 5179 } 5180 5181 proto._initHooks = []; 5182 5183 // add method for calling all hooks 5184 proto.callInitHooks = function () { 5185 5186 if (this._initHooksCalled) { return; } 5187 5188 if (parentProto.callInitHooks) { 5189 parentProto.callInitHooks.call(this); 5190 } 5191 5192 this._initHooksCalled = true; 5193 5194 for (var i = 0, len = proto._initHooks.length; i < len; i++) { 5195 proto._initHooks[i].call(this); 5196 } 5197 }; 5198 5199 return NewClass; 5200 }; 5201 5202 5203 // @function include(properties: Object): this 5204 // [Includes a mixin](#class-includes) into the current class. 5205 Class.include = function (props) { 5206 var parentOptions = this.prototype.options; 5207 extend(this.prototype, props); 5208 if (props.options) { 5209 this.prototype.options = parentOptions; 5210 this.mergeOptions(props.options); 5211 } 5212 return this; 5213 }; 5214 5215 // @function mergeOptions(options: Object): this 5216 // [Merges `options`](#class-options) into the defaults of the class. 5217 Class.mergeOptions = function (options) { 5218 extend(this.prototype.options, options); 5219 return this; 5220 }; 5221 5222 // @function addInitHook(fn: Function): this 5223 // Adds a [constructor hook](#class-constructor-hooks) to the class. 5224 Class.addInitHook = function (fn) { // (Function) || (String, args...) 5225 var args = Array.prototype.slice.call(arguments, 1); 5226 5227 var init = typeof fn === 'function' ? fn : function () { 5228 this[fn].apply(this, args); 5229 }; 5230 5231 this.prototype._initHooks = this.prototype._initHooks || []; 5232 this.prototype._initHooks.push(init); 5233 return this; 5234 }; 5235 5236 function checkDeprecatedMixinEvents(includes) { 5237 /* global L: true */ 5238 if (typeof L === 'undefined' || !L || !L.Mixin) { return; } 5239 5240 includes = isArray(includes) ? includes : [includes]; 5241 5242 for (var i = 0; i < includes.length; i++) { 5243 if (includes[i] === L.Mixin.Events) { 5244 console.warn('Deprecated include of L.Mixin.Events: ' + 5245 'this property will be removed in future releases, ' + 5246 'please inherit from L.Evented instead.', new Error().stack); 5247 } 5248 } 5249 } 5250 5251 /* 5252 * @class Evented 5253 * @aka L.Evented 5254 * @inherits Class 5255 * 5256 * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). 5257 * 5258 * @example 5259 * 5260 * ```js 5261 * map.on('click', function(e) { 5262 * alert(e.latlng); 5263 * } ); 5264 * ``` 5265 * 5266 * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: 5267 * 5268 * ```js 5269 * function onClick(e) { ... } 5270 * 5271 * map.on('click', onClick); 5272 * map.off('click', onClick); 5273 * ``` 5274 */ 5275 5276 var Events = { 5277 /* @method on(type: String, fn: Function, context?: Object): this 5278 * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). 5279 * 5280 * @alternative 5281 * @method on(eventMap: Object): this 5282 * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` 5283 */ 5284 on: function (types, fn, context) { 5285 5286 // types can be a map of types/handlers 5287 if (typeof types === 'object') { 5288 for (var type in types) { 5289 // we don't process space-separated events here for performance; 5290 // it's a hot path since Layer uses the on(obj) syntax 5291 this._on(type, types[type], fn); 5292 } 5293 5294 } else { 5295 // types can be a string of space-separated words 5296 types = splitWords(types); 5297 5298 for (var i = 0, len = types.length; i < len; i++) { 5299 this._on(types[i], fn, context); 5300 } 5301 } 5302 5303 return this; 5304 }, 5305 5306 /* @method off(type: String, fn?: Function, context?: Object): this 5307 * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. 5308 * 5309 * @alternative 5310 * @method off(eventMap: Object): this 5311 * Removes a set of type/listener pairs. 5312 * 5313 * @alternative 5314 * @method off: this 5315 * Removes all listeners to all events on the object. This includes implicitly attached events. 5316 */ 5317 off: function (types, fn, context) { 5318 5319 if (!arguments.length) { 5320 // clear all listeners if called without arguments 5321 delete this._events; 5322 5323 } else if (typeof types === 'object') { 5324 for (var type in types) { 5325 this._off(type, types[type], fn); 5326 } 5327 5328 } else { 5329 types = splitWords(types); 5330 5331 var removeAll = arguments.length === 1; 5332 for (var i = 0, len = types.length; i < len; i++) { 5333 if (removeAll) { 5334 this._off(types[i]); 5335 } else { 5336 this._off(types[i], fn, context); 5337 } 5338 } 5339 } 5340 5341 return this; 5342 }, 5343 5344 // attach listener (without syntactic sugar now) 5345 _on: function (type, fn, context, _once) { 5346 if (typeof fn !== 'function') { 5347 console.warn('wrong listener type: ' + typeof fn); 5348 return; 5349 } 5350 5351 // check if fn already there 5352 if (this._listens(type, fn, context) !== false) { 5353 return; 5354 } 5355 5356 if (context === this) { 5357 // Less memory footprint. 5358 context = undefined; 5359 } 5360 5361 var newListener = {fn: fn, ctx: context}; 5362 if (_once) { 5363 newListener.once = true; 5364 } 5365 5366 this._events = this._events || {}; 5367 this._events[type] = this._events[type] || []; 5368 this._events[type].push(newListener); 5369 }, 5370 5371 _off: function (type, fn, context) { 5372 var listeners, 5373 i, 5374 len; 5375 5376 if (!this._events) { 5377 return; 5378 } 5379 5380 listeners = this._events[type]; 5381 if (!listeners) { 5382 return; 5383 } 5384 5385 if (arguments.length === 1) { // remove all 5386 if (this._firingCount) { 5387 // Set all removed listeners to noop 5388 // so they are not called if remove happens in fire 5389 for (i = 0, len = listeners.length; i < len; i++) { 5390 listeners[i].fn = falseFn; 5391 } 5392 } 5393 // clear all listeners for a type if function isn't specified 5394 delete this._events[type]; 5395 return; 5396 } 5397 5398 if (typeof fn !== 'function') { 5399 console.warn('wrong listener type: ' + typeof fn); 5400 return; 5401 } 5402 5403 // find fn and remove it 5404 var index = this._listens(type, fn, context); 5405 if (index !== false) { 5406 var listener = listeners[index]; 5407 if (this._firingCount) { 5408 // set the removed listener to noop so that's not called if remove happens in fire 5409 listener.fn = falseFn; 5410 5411 /* copy array in case events are being fired */ 5412 this._events[type] = listeners = listeners.slice(); 5413 } 5414 listeners.splice(index, 1); 5415 } 5416 }, 5417 5418 // @method fire(type: String, data?: Object, propagate?: Boolean): this 5419 // Fires an event of the specified type. You can optionally provide a data 5420 // object â the first argument of the listener function will contain its 5421 // properties. The event can optionally be propagated to event parents. 5422 fire: function (type, data, propagate) { 5423 if (!this.listens(type, propagate)) { return this; } 5424 5425 var event = extend({}, data, { 5426 type: type, 5427 target: this, 5428 sourceTarget: data && data.sourceTarget || this 5429 }); 5430 5431 if (this._events) { 5432 var listeners = this._events[type]; 5433 if (listeners) { 5434 this._firingCount = (this._firingCount + 1) || 1; 5435 for (var i = 0, len = listeners.length; i < len; i++) { 5436 var l = listeners[i]; 5437 // off overwrites l.fn, so we need to copy fn to a var 5438 var fn = l.fn; 5439 if (l.once) { 5440 this.off(type, fn, l.ctx); 5441 } 5442 fn.call(l.ctx || this, event); 5443 } 5444 5445 this._firingCount--; 5446 } 5447 } 5448 5449 if (propagate) { 5450 // propagate the event to parents (set with addEventParent) 5451 this._propagateEvent(event); 5452 } 5453 5454 return this; 5455 }, 5456 5457 // @method listens(type: String, propagate?: Boolean): Boolean 5458 // @method listens(type: String, fn: Function, context?: Object, propagate?: Boolean): Boolean 5459 // Returns `true` if a particular event type has any listeners attached to it. 5460 // The verification can optionally be propagated, it will return `true` if parents have the listener attached to it. 5461 listens: function (type, fn, context, propagate) { 5462 if (typeof type !== 'string') { 5463 console.warn('"string" type argument expected'); 5464 } 5465 5466 // we don't overwrite the input `fn` value, because we need to use it for propagation 5467 var _fn = fn; 5468 if (typeof fn !== 'function') { 5469 propagate = !!fn; 5470 _fn = undefined; 5471 context = undefined; 5472 } 5473 5474 var listeners = this._events && this._events[type]; 5475 if (listeners && listeners.length) { 5476 if (this._listens(type, _fn, context) !== false) { 5477 return true; 5478 } 5479 } 5480 5481 if (propagate) { 5482 // also check parents for listeners if event propagates 5483 for (var id in this._eventParents) { 5484 if (this._eventParents[id].listens(type, fn, context, propagate)) { return true; } 5485 } 5486 } 5487 return false; 5488 }, 5489 5490 // returns the index (number) or false 5491 _listens: function (type, fn, context) { 5492 if (!this._events) { 5493 return false; 5494 } 5495 5496 var listeners = this._events[type] || []; 5497 if (!fn) { 5498 return !!listeners.length; 5499 } 5500 5501 if (context === this) { 5502 // Less memory footprint. 5503 context = undefined; 5504 } 5505 5506 for (var i = 0, len = listeners.length; i < len; i++) { 5507 if (listeners[i].fn === fn && listeners[i].ctx === context) { 5508 return i; 5509 } 5510 } 5511 return false; 5512 5513 }, 5514 5515 // @method once(â¦): this 5516 // Behaves as [`on(â¦)`](#evented-on), except the listener will only get fired once and then removed. 5517 once: function (types, fn, context) { 5518 5519 // types can be a map of types/handlers 5520 if (typeof types === 'object') { 5521 for (var type in types) { 5522 // we don't process space-separated events here for performance; 5523 // it's a hot path since Layer uses the on(obj) syntax 5524 this._on(type, types[type], fn, true); 5525 } 5526 5527 } else { 5528 // types can be a string of space-separated words 5529 types = splitWords(types); 5530 5531 for (var i = 0, len = types.length; i < len; i++) { 5532 this._on(types[i], fn, context, true); 5533 } 5534 } 5535 5536 return this; 5537 }, 5538 5539 // @method addEventParent(obj: Evented): this 5540 // Adds an event parent - an `Evented` that will receive propagated events 5541 addEventParent: function (obj) { 5542 this._eventParents = this._eventParents || {}; 5543 this._eventParents[stamp(obj)] = obj; 5544 return this; 5545 }, 5546 5547 // @method removeEventParent(obj: Evented): this 5548 // Removes an event parent, so it will stop receiving propagated events 5549 removeEventParent: function (obj) { 5550 if (this._eventParents) { 5551 delete this._eventParents[stamp(obj)]; 5552 } 5553 return this; 5554 }, 5555 5556 _propagateEvent: function (e) { 5557 for (var id in this._eventParents) { 5558 this._eventParents[id].fire(e.type, extend({ 5559 layer: e.target, 5560 propagatedFrom: e.target 5561 }, e), true); 5562 } 5563 } 5564 }; 5565 5566 // aliases; we should ditch those eventually 5567 5568 // @method addEventListener(â¦): this 5569 // Alias to [`on(â¦)`](#evented-on) 5570 Events.addEventListener = Events.on; 5571 5572 // @method removeEventListener(â¦): this 5573 // Alias to [`off(â¦)`](#evented-off) 5574 5575 // @method clearAllEventListeners(â¦): this 5576 // Alias to [`off()`](#evented-off) 5577 Events.removeEventListener = Events.clearAllEventListeners = Events.off; 5578 5579 // @method addOneTimeEventListener(â¦): this 5580 // Alias to [`once(â¦)`](#evented-once) 5581 Events.addOneTimeEventListener = Events.once; 5582 5583 // @method fireEvent(â¦): this 5584 // Alias to [`fire(â¦)`](#evented-fire) 5585 Events.fireEvent = Events.fire; 5586 5587 // @method hasEventListeners(â¦): Boolean 5588 // Alias to [`listens(â¦)`](#evented-listens) 5589 Events.hasEventListeners = Events.listens; 5590 5591 var Evented = Class.extend(Events); 5592 5593 /* 5594 * @class Point 5595 * @aka L.Point 5596 * 5597 * Represents a point with `x` and `y` coordinates in pixels. 5598 * 5599 * @example 5600 * 5601 * ```js 5602 * var point = L.point(200, 300); 5603 * ``` 5604 * 5605 * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: 5606 * 5607 * ```js 5608 * map.panBy([200, 300]); 5609 * map.panBy(L.point(200, 300)); 5610 * ``` 5611 * 5612 * Note that `Point` does not inherit from Leaflet's `Class` object, 5613 * which means new classes can't inherit from it, and new methods 5614 * can't be added to it with the `include` function. 5615 */ 5616 5617 function Point(x, y, round) { 5618 // @property x: Number; The `x` coordinate of the point 5619 this.x = (round ? Math.round(x) : x); 5620 // @property y: Number; The `y` coordinate of the point 5621 this.y = (round ? Math.round(y) : y); 5622 } 5623 5624 var trunc = Math.trunc || function (v) { 5625 return v > 0 ? Math.floor(v) : Math.ceil(v); 5626 }; 5627 5628 Point.prototype = { 5629 5630 // @method clone(): Point 5631 // Returns a copy of the current point. 5632 clone: function () { 5633 return new Point(this.x, this.y); 5634 }, 5635 5636 // @method add(otherPoint: Point): Point 5637 // Returns the result of addition of the current and the given points. 5638 add: function (point) { 5639 // non-destructive, returns a new point 5640 return this.clone()._add(toPoint(point)); 5641 }, 5642 5643 _add: function (point) { 5644 // destructive, used directly for performance in situations where it's safe to modify existing point 5645 this.x += point.x; 5646 this.y += point.y; 5647 return this; 5648 }, 5649 5650 // @method subtract(otherPoint: Point): Point 5651 // Returns the result of subtraction of the given point from the current. 5652 subtract: function (point) { 5653 return this.clone()._subtract(toPoint(point)); 5654 }, 5655 5656 _subtract: function (point) { 5657 this.x -= point.x; 5658 this.y -= point.y; 5659 return this; 5660 }, 5661 5662 // @method divideBy(num: Number): Point 5663 // Returns the result of division of the current point by the given number. 5664 divideBy: function (num) { 5665 return this.clone()._divideBy(num); 5666 }, 5667 5668 _divideBy: function (num) { 5669 this.x /= num; 5670 this.y /= num; 5671 return this; 5672 }, 5673 5674 // @method multiplyBy(num: Number): Point 5675 // Returns the result of multiplication of the current point by the given number. 5676 multiplyBy: function (num) { 5677 return this.clone()._multiplyBy(num); 5678 }, 5679 5680 _multiplyBy: function (num) { 5681 this.x *= num; 5682 this.y *= num; 5683 return this; 5684 }, 5685 5686 // @method scaleBy(scale: Point): Point 5687 // Multiply each coordinate of the current point by each coordinate of 5688 // `scale`. In linear algebra terms, multiply the point by the 5689 // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) 5690 // defined by `scale`. 5691 scaleBy: function (point) { 5692 return new Point(this.x * point.x, this.y * point.y); 5693 }, 5694 5695 // @method unscaleBy(scale: Point): Point 5696 // Inverse of `scaleBy`. Divide each coordinate of the current point by 5697 // each coordinate of `scale`. 5698 unscaleBy: function (point) { 5699 return new Point(this.x / point.x, this.y / point.y); 5700 }, 5701 5702 // @method round(): Point 5703 // Returns a copy of the current point with rounded coordinates. 5704 round: function () { 5705 return this.clone()._round(); 5706 }, 5707 5708 _round: function () { 5709 this.x = Math.round(this.x); 5710 this.y = Math.round(this.y); 5711 return this; 5712 }, 5713 5714 // @method floor(): Point 5715 // Returns a copy of the current point with floored coordinates (rounded down). 5716 floor: function () { 5717 return this.clone()._floor(); 5718 }, 5719 5720 _floor: function () { 5721 this.x = Math.floor(this.x); 5722 this.y = Math.floor(this.y); 5723 return this; 5724 }, 5725 5726 // @method ceil(): Point 5727 // Returns a copy of the current point with ceiled coordinates (rounded up). 5728 ceil: function () { 5729 return this.clone()._ceil(); 5730 }, 5731 5732 _ceil: function () { 5733 this.x = Math.ceil(this.x); 5734 this.y = Math.ceil(this.y); 5735 return this; 5736 }, 5737 5738 // @method trunc(): Point 5739 // Returns a copy of the current point with truncated coordinates (rounded towards zero). 5740 trunc: function () { 5741 return this.clone()._trunc(); 5742 }, 5743 5744 _trunc: function () { 5745 this.x = trunc(this.x); 5746 this.y = trunc(this.y); 5747 return this; 5748 }, 5749 5750 // @method distanceTo(otherPoint: Point): Number 5751 // Returns the cartesian distance between the current and the given points. 5752 distanceTo: function (point) { 5753 point = toPoint(point); 5754 5755 var x = point.x - this.x, 5756 y = point.y - this.y; 5757 5758 return Math.sqrt(x * x + y * y); 5759 }, 5760 5761 // @method equals(otherPoint: Point): Boolean 5762 // Returns `true` if the given point has the same coordinates. 5763 equals: function (point) { 5764 point = toPoint(point); 5765 5766 return point.x === this.x && 5767 point.y === this.y; 5768 }, 5769 5770 // @method contains(otherPoint: Point): Boolean 5771 // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). 5772 contains: function (point) { 5773 point = toPoint(point); 5774 5775 return Math.abs(point.x) <= Math.abs(this.x) && 5776 Math.abs(point.y) <= Math.abs(this.y); 5777 }, 5778 5779 // @method toString(): String 5780 // Returns a string representation of the point for debugging purposes. 5781 toString: function () { 5782 return 'Point(' + 5783 formatNum(this.x) + ', ' + 5784 formatNum(this.y) + ')'; 5785 } 5786 }; 5787 5788 // @factory L.point(x: Number, y: Number, round?: Boolean) 5789 // Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. 5790 5791 // @alternative 5792 // @factory L.point(coords: Number[]) 5793 // Expects an array of the form `[x, y]` instead. 5794 5795 // @alternative 5796 // @factory L.point(coords: Object) 5797 // Expects a plain object of the form `{x: Number, y: Number}` instead. 5798 function toPoint(x, y, round) { 5799 if (x instanceof Point) { 5800 return x; 5801 } 5802 if (isArray(x)) { 5803 return new Point(x[0], x[1]); 5804 } 5805 if (x === undefined || x === null) { 5806 return x; 5807 } 5808 if (typeof x === 'object' && 'x' in x && 'y' in x) { 5809 return new Point(x.x, x.y); 5810 } 5811 return new Point(x, y, round); 5812 } 5813 5814 /* 5815 * @class Bounds 5816 * @aka L.Bounds 5817 * 5818 * Represents a rectangular area in pixel coordinates. 5819 * 5820 * @example 5821 * 5822 * ```js 5823 * var p1 = L.point(10, 10), 5824 * p2 = L.point(40, 60), 5825 * bounds = L.bounds(p1, p2); 5826 * ``` 5827 * 5828 * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: 5829 * 5830 * ```js 5831 * otherBounds.intersects([[10, 10], [40, 60]]); 5832 * ``` 5833 * 5834 * Note that `Bounds` does not inherit from Leaflet's `Class` object, 5835 * which means new classes can't inherit from it, and new methods 5836 * can't be added to it with the `include` function. 5837 */ 5838 5839 function Bounds(a, b) { 5840 if (!a) { return; } 5841 5842 var points = b ? [a, b] : a; 5843 5844 for (var i = 0, len = points.length; i < len; i++) { 5845 this.extend(points[i]); 5846 } 5847 } 5848 5849 Bounds.prototype = { 5850 // @method extend(point: Point): this 5851 // Extends the bounds to contain the given point. 5852 5853 // @alternative 5854 // @method extend(otherBounds: Bounds): this 5855 // Extend the bounds to contain the given bounds 5856 extend: function (obj) { 5857 var min2, max2; 5858 if (!obj) { return this; } 5859 5860 if (obj instanceof Point || typeof obj[0] === 'number' || 'x' in obj) { 5861 min2 = max2 = toPoint(obj); 5862 } else { 5863 obj = toBounds(obj); 5864 min2 = obj.min; 5865 max2 = obj.max; 5866 5867 if (!min2 || !max2) { return this; } 5868 } 5869 5870 // @property min: Point 5871 // The top left corner of the rectangle. 5872 // @property max: Point 5873 // The bottom right corner of the rectangle. 5874 if (!this.min && !this.max) { 5875 this.min = min2.clone(); 5876 this.max = max2.clone(); 5877 } else { 5878 this.min.x = Math.min(min2.x, this.min.x); 5879 this.max.x = Math.max(max2.x, this.max.x); 5880 this.min.y = Math.min(min2.y, this.min.y); 5881 this.max.y = Math.max(max2.y, this.max.y); 5882 } 5883 return this; 5884 }, 5885 5886 // @method getCenter(round?: Boolean): Point 5887 // Returns the center point of the bounds. 5888 getCenter: function (round) { 5889 return toPoint( 5890 (this.min.x + this.max.x) / 2, 5891 (this.min.y + this.max.y) / 2, round); 5892 }, 5893 5894 // @method getBottomLeft(): Point 5895 // Returns the bottom-left point of the bounds. 5896 getBottomLeft: function () { 5897 return toPoint(this.min.x, this.max.y); 5898 }, 5899 5900 // @method getTopRight(): Point 5901 // Returns the top-right point of the bounds. 5902 getTopRight: function () { // -> Point 5903 return toPoint(this.max.x, this.min.y); 5904 }, 5905 5906 // @method getTopLeft(): Point 5907 // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)). 5908 getTopLeft: function () { 5909 return this.min; // left, top 5910 }, 5911 5912 // @method getBottomRight(): Point 5913 // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)). 5914 getBottomRight: function () { 5915 return this.max; // right, bottom 5916 }, 5917 5918 // @method getSize(): Point 5919 // Returns the size of the given bounds 5920 getSize: function () { 5921 return this.max.subtract(this.min); 5922 }, 5923 5924 // @method contains(otherBounds: Bounds): Boolean 5925 // Returns `true` if the rectangle contains the given one. 5926 // @alternative 5927 // @method contains(point: Point): Boolean 5928 // Returns `true` if the rectangle contains the given point. 5929 contains: function (obj) { 5930 var min, max; 5931 5932 if (typeof obj[0] === 'number' || obj instanceof Point) { 5933 obj = toPoint(obj); 5934 } else { 5935 obj = toBounds(obj); 5936 } 5937 5938 if (obj instanceof Bounds) { 5939 min = obj.min; 5940 max = obj.max; 5941 } else { 5942 min = max = obj; 5943 } 5944 5945 return (min.x >= this.min.x) && 5946 (max.x <= this.max.x) && 5947 (min.y >= this.min.y) && 5948 (max.y <= this.max.y); 5949 }, 5950 5951 // @method intersects(otherBounds: Bounds): Boolean 5952 // Returns `true` if the rectangle intersects the given bounds. Two bounds 5953 // intersect if they have at least one point in common. 5954 intersects: function (bounds) { // (Bounds) -> Boolean 5955 bounds = toBounds(bounds); 5956 5957 var min = this.min, 5958 max = this.max, 5959 min2 = bounds.min, 5960 max2 = bounds.max, 5961 xIntersects = (max2.x >= min.x) && (min2.x <= max.x), 5962 yIntersects = (max2.y >= min.y) && (min2.y <= max.y); 5963 5964 return xIntersects && yIntersects; 5965 }, 5966 5967 // @method overlaps(otherBounds: Bounds): Boolean 5968 // Returns `true` if the rectangle overlaps the given bounds. Two bounds 5969 // overlap if their intersection is an area. 5970 overlaps: function (bounds) { // (Bounds) -> Boolean 5971 bounds = toBounds(bounds); 5972 5973 var min = this.min, 5974 max = this.max, 5975 min2 = bounds.min, 5976 max2 = bounds.max, 5977 xOverlaps = (max2.x > min.x) && (min2.x < max.x), 5978 yOverlaps = (max2.y > min.y) && (min2.y < max.y); 5979 5980 return xOverlaps && yOverlaps; 5981 }, 5982 5983 // @method isValid(): Boolean 5984 // Returns `true` if the bounds are properly initialized. 5985 isValid: function () { 5986 return !!(this.min && this.max); 5987 }, 5988 5989 5990 // @method pad(bufferRatio: Number): Bounds 5991 // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. 5992 // For example, a ratio of 0.5 extends the bounds by 50% in each direction. 5993 // Negative values will retract the bounds. 5994 pad: function (bufferRatio) { 5995 var min = this.min, 5996 max = this.max, 5997 heightBuffer = Math.abs(min.x - max.x) * bufferRatio, 5998 widthBuffer = Math.abs(min.y - max.y) * bufferRatio; 5999 6000 6001 return toBounds( 6002 toPoint(min.x - heightBuffer, min.y - widthBuffer), 6003 toPoint(max.x + heightBuffer, max.y + widthBuffer)); 6004 }, 6005 6006 6007 // @method equals(otherBounds: Bounds): Boolean 6008 // Returns `true` if the rectangle is equivalent to the given bounds. 6009 equals: function (bounds) { 6010 if (!bounds) { return false; } 6011 6012 bounds = toBounds(bounds); 6013 6014 return this.min.equals(bounds.getTopLeft()) && 6015 this.max.equals(bounds.getBottomRight()); 6016 }, 6017 }; 6018 6019 6020 // @factory L.bounds(corner1: Point, corner2: Point) 6021 // Creates a Bounds object from two corners coordinate pairs. 6022 // @alternative 6023 // @factory L.bounds(points: Point[]) 6024 // Creates a Bounds object from the given array of points. 6025 function toBounds(a, b) { 6026 if (!a || a instanceof Bounds) { 6027 return a; 6028 } 6029 return new Bounds(a, b); 6030 } 6031 6032 /* 6033 * @class LatLngBounds 6034 * @aka L.LatLngBounds 6035 * 6036 * Represents a rectangular geographical area on a map. 6037 * 6038 * @example 6039 * 6040 * ```js 6041 * var corner1 = L.latLng(40.712, -74.227), 6042 * corner2 = L.latLng(40.774, -74.125), 6043 * bounds = L.latLngBounds(corner1, corner2); 6044 * ``` 6045 * 6046 * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: 6047 * 6048 * ```js 6049 * map.fitBounds([ 6050 * [40.712, -74.227], 6051 * [40.774, -74.125] 6052 * ]); 6053 * ``` 6054 * 6055 * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. 6056 * 6057 * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object, 6058 * which means new classes can't inherit from it, and new methods 6059 * can't be added to it with the `include` function. 6060 */ 6061 6062 function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) 6063 if (!corner1) { return; } 6064 6065 var latlngs = corner2 ? [corner1, corner2] : corner1; 6066 6067 for (var i = 0, len = latlngs.length; i < len; i++) { 6068 this.extend(latlngs[i]); 6069 } 6070 } 6071 6072 LatLngBounds.prototype = { 6073 6074 // @method extend(latlng: LatLng): this 6075 // Extend the bounds to contain the given point 6076 6077 // @alternative 6078 // @method extend(otherBounds: LatLngBounds): this 6079 // Extend the bounds to contain the given bounds 6080 extend: function (obj) { 6081 var sw = this._southWest, 6082 ne = this._northEast, 6083 sw2, ne2; 6084 6085 if (obj instanceof LatLng) { 6086 sw2 = obj; 6087 ne2 = obj; 6088 6089 } else if (obj instanceof LatLngBounds) { 6090 sw2 = obj._southWest; 6091 ne2 = obj._northEast; 6092 6093 if (!sw2 || !ne2) { return this; } 6094 6095 } else { 6096 return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this; 6097 } 6098 6099 if (!sw && !ne) { 6100 this._southWest = new LatLng(sw2.lat, sw2.lng); 6101 this._northEast = new LatLng(ne2.lat, ne2.lng); 6102 } else { 6103 sw.lat = Math.min(sw2.lat, sw.lat); 6104 sw.lng = Math.min(sw2.lng, sw.lng); 6105 ne.lat = Math.max(ne2.lat, ne.lat); 6106 ne.lng = Math.max(ne2.lng, ne.lng); 6107 } 6108 6109 return this; 6110 }, 6111 6112 // @method pad(bufferRatio: Number): LatLngBounds 6113 // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. 6114 // For example, a ratio of 0.5 extends the bounds by 50% in each direction. 6115 // Negative values will retract the bounds. 6116 pad: function (bufferRatio) { 6117 var sw = this._southWest, 6118 ne = this._northEast, 6119 heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, 6120 widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; 6121 6122 return new LatLngBounds( 6123 new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), 6124 new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); 6125 }, 6126 6127 // @method getCenter(): LatLng 6128 // Returns the center point of the bounds. 6129 getCenter: function () { 6130 return new LatLng( 6131 (this._southWest.lat + this._northEast.lat) / 2, 6132 (this._southWest.lng + this._northEast.lng) / 2); 6133 }, 6134 6135 // @method getSouthWest(): LatLng 6136 // Returns the south-west point of the bounds. 6137 getSouthWest: function () { 6138 return this._southWest; 6139 }, 6140 6141 // @method getNorthEast(): LatLng 6142 // Returns the north-east point of the bounds. 6143 getNorthEast: function () { 6144 return this._northEast; 6145 }, 6146 6147 // @method getNorthWest(): LatLng 6148 // Returns the north-west point of the bounds. 6149 getNorthWest: function () { 6150 return new LatLng(this.getNorth(), this.getWest()); 6151 }, 6152 6153 // @method getSouthEast(): LatLng 6154 // Returns the south-east point of the bounds. 6155 getSouthEast: function () { 6156 return new LatLng(this.getSouth(), this.getEast()); 6157 }, 6158 6159 // @method getWest(): Number 6160 // Returns the west longitude of the bounds 6161 getWest: function () { 6162 return this._southWest.lng; 6163 }, 6164 6165 // @method getSouth(): Number 6166 // Returns the south latitude of the bounds 6167 getSouth: function () { 6168 return this._southWest.lat; 6169 }, 6170 6171 // @method getEast(): Number 6172 // Returns the east longitude of the bounds 6173 getEast: function () { 6174 return this._northEast.lng; 6175 }, 6176 6177 // @method getNorth(): Number 6178 // Returns the north latitude of the bounds 6179 getNorth: function () { 6180 return this._northEast.lat; 6181 }, 6182 6183 // @method contains(otherBounds: LatLngBounds): Boolean 6184 // Returns `true` if the rectangle contains the given one. 6185 6186 // @alternative 6187 // @method contains (latlng: LatLng): Boolean 6188 // Returns `true` if the rectangle contains the given point. 6189 contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean 6190 if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) { 6191 obj = toLatLng(obj); 6192 } else { 6193 obj = toLatLngBounds(obj); 6194 } 6195 6196 var sw = this._southWest, 6197 ne = this._northEast, 6198 sw2, ne2; 6199 6200 if (obj instanceof LatLngBounds) { 6201 sw2 = obj.getSouthWest(); 6202 ne2 = obj.getNorthEast(); 6203 } else { 6204 sw2 = ne2 = obj; 6205 } 6206 6207 return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && 6208 (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); 6209 }, 6210 6211 // @method intersects(otherBounds: LatLngBounds): Boolean 6212 // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. 6213 intersects: function (bounds) { 6214 bounds = toLatLngBounds(bounds); 6215 6216 var sw = this._southWest, 6217 ne = this._northEast, 6218 sw2 = bounds.getSouthWest(), 6219 ne2 = bounds.getNorthEast(), 6220 6221 latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), 6222 lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); 6223 6224 return latIntersects && lngIntersects; 6225 }, 6226 6227 // @method overlaps(otherBounds: LatLngBounds): Boolean 6228 // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. 6229 overlaps: function (bounds) { 6230 bounds = toLatLngBounds(bounds); 6231 6232 var sw = this._southWest, 6233 ne = this._northEast, 6234 sw2 = bounds.getSouthWest(), 6235 ne2 = bounds.getNorthEast(), 6236 6237 latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), 6238 lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); 6239 6240 return latOverlaps && lngOverlaps; 6241 }, 6242 6243 // @method toBBoxString(): String 6244 // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. 6245 toBBoxString: function () { 6246 return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); 6247 }, 6248 6249 // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean 6250 // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number. 6251 equals: function (bounds, maxMargin) { 6252 if (!bounds) { return false; } 6253 6254 bounds = toLatLngBounds(bounds); 6255 6256 return this._southWest.equals(bounds.getSouthWest(), maxMargin) && 6257 this._northEast.equals(bounds.getNorthEast(), maxMargin); 6258 }, 6259 6260 // @method isValid(): Boolean 6261 // Returns `true` if the bounds are properly initialized. 6262 isValid: function () { 6263 return !!(this._southWest && this._northEast); 6264 } 6265 }; 6266 6267 // TODO International date line? 6268 6269 // @factory L.latLngBounds(corner1: LatLng, corner2: LatLng) 6270 // Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. 6271 6272 // @alternative 6273 // @factory L.latLngBounds(latlngs: LatLng[]) 6274 // Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). 6275 function toLatLngBounds(a, b) { 6276 if (a instanceof LatLngBounds) { 6277 return a; 6278 } 6279 return new LatLngBounds(a, b); 6280 } 6281 6282 /* @class LatLng 6283 * @aka L.LatLng 6284 * 6285 * Represents a geographical point with a certain latitude and longitude. 6286 * 6287 * @example 6288 * 6289 * ``` 6290 * var latlng = L.latLng(50.5, 30.5); 6291 * ``` 6292 * 6293 * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: 6294 * 6295 * ``` 6296 * map.panTo([50, 30]); 6297 * map.panTo({lon: 30, lat: 50}); 6298 * map.panTo({lat: 50, lng: 30}); 6299 * map.panTo(L.latLng(50, 30)); 6300 * ``` 6301 * 6302 * Note that `LatLng` does not inherit from Leaflet's `Class` object, 6303 * which means new classes can't inherit from it, and new methods 6304 * can't be added to it with the `include` function. 6305 */ 6306 6307 function LatLng(lat, lng, alt) { 6308 if (isNaN(lat) || isNaN(lng)) { 6309 throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); 6310 } 6311 6312 // @property lat: Number 6313 // Latitude in degrees 6314 this.lat = +lat; 6315 6316 // @property lng: Number 6317 // Longitude in degrees 6318 this.lng = +lng; 6319 6320 // @property alt: Number 6321 // Altitude in meters (optional) 6322 if (alt !== undefined) { 6323 this.alt = +alt; 6324 } 6325 } 6326 6327 LatLng.prototype = { 6328 // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean 6329 // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number. 6330 equals: function (obj, maxMargin) { 6331 if (!obj) { return false; } 6332 6333 obj = toLatLng(obj); 6334 6335 var margin = Math.max( 6336 Math.abs(this.lat - obj.lat), 6337 Math.abs(this.lng - obj.lng)); 6338 6339 return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); 6340 }, 6341 6342 // @method toString(): String 6343 // Returns a string representation of the point (for debugging purposes). 6344 toString: function (precision) { 6345 return 'LatLng(' + 6346 formatNum(this.lat, precision) + ', ' + 6347 formatNum(this.lng, precision) + ')'; 6348 }, 6349 6350 // @method distanceTo(otherLatLng: LatLng): Number 6351 // Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines). 6352 distanceTo: function (other) { 6353 return Earth.distance(this, toLatLng(other)); 6354 }, 6355 6356 // @method wrap(): LatLng 6357 // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. 6358 wrap: function () { 6359 return Earth.wrapLatLng(this); 6360 }, 6361 6362 // @method toBounds(sizeInMeters: Number): LatLngBounds 6363 // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. 6364 toBounds: function (sizeInMeters) { 6365 var latAccuracy = 180 * sizeInMeters / 40075017, 6366 lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); 6367 6368 return toLatLngBounds( 6369 [this.lat - latAccuracy, this.lng - lngAccuracy], 6370 [this.lat + latAccuracy, this.lng + lngAccuracy]); 6371 }, 6372 6373 clone: function () { 6374 return new LatLng(this.lat, this.lng, this.alt); 6375 } 6376 }; 6377 6378 6379 6380 // @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng 6381 // Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). 6382 6383 // @alternative 6384 // @factory L.latLng(coords: Array): LatLng 6385 // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. 6386 6387 // @alternative 6388 // @factory L.latLng(coords: Object): LatLng 6389 // Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. 6390 6391 function toLatLng(a, b, c) { 6392 if (a instanceof LatLng) { 6393 return a; 6394 } 6395 if (isArray(a) && typeof a[0] !== 'object') { 6396 if (a.length === 3) { 6397 return new LatLng(a[0], a[1], a[2]); 6398 } 6399 if (a.length === 2) { 6400 return new LatLng(a[0], a[1]); 6401 } 6402 return null; 6403 } 6404 if (a === undefined || a === null) { 6405 return a; 6406 } 6407 if (typeof a === 'object' && 'lat' in a) { 6408 return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); 6409 } 6410 if (b === undefined) { 6411 return null; 6412 } 6413 return new LatLng(a, b, c); 6414 } 6415 6416 /* 6417 * @namespace CRS 6418 * @crs L.CRS.Base 6419 * Object that defines coordinate reference systems for projecting 6420 * geographical points into pixel (screen) coordinates and back (and to 6421 * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See 6422 * [spatial reference system](https://en.wikipedia.org/wiki/Spatial_reference_system). 6423 * 6424 * Leaflet defines the most usual CRSs by default. If you want to use a 6425 * CRS not defined by default, take a look at the 6426 * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. 6427 * 6428 * Note that the CRS instances do not inherit from Leaflet's `Class` object, 6429 * and can't be instantiated. Also, new classes can't inherit from them, 6430 * and methods can't be added to them with the `include` function. 6431 */ 6432 6433 var CRS = { 6434 // @method latLngToPoint(latlng: LatLng, zoom: Number): Point 6435 // Projects geographical coordinates into pixel coordinates for a given zoom. 6436 latLngToPoint: function (latlng, zoom) { 6437 var projectedPoint = this.projection.project(latlng), 6438 scale = this.scale(zoom); 6439 6440 return this.transformation._transform(projectedPoint, scale); 6441 }, 6442 6443 // @method pointToLatLng(point: Point, zoom: Number): LatLng 6444 // The inverse of `latLngToPoint`. Projects pixel coordinates on a given 6445 // zoom into geographical coordinates. 6446 pointToLatLng: function (point, zoom) { 6447 var scale = this.scale(zoom), 6448 untransformedPoint = this.transformation.untransform(point, scale); 6449 6450 return this.projection.unproject(untransformedPoint); 6451 }, 6452 6453 // @method project(latlng: LatLng): Point 6454 // Projects geographical coordinates into coordinates in units accepted for 6455 // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). 6456 project: function (latlng) { 6457 return this.projection.project(latlng); 6458 }, 6459 6460 // @method unproject(point: Point): LatLng 6461 // Given a projected coordinate returns the corresponding LatLng. 6462 // The inverse of `project`. 6463 unproject: function (point) { 6464 return this.projection.unproject(point); 6465 }, 6466 6467 // @method scale(zoom: Number): Number 6468 // Returns the scale used when transforming projected coordinates into 6469 // pixel coordinates for a particular zoom. For example, it returns 6470 // `256 * 2^zoom` for Mercator-based CRS. 6471 scale: function (zoom) { 6472 return 256 * Math.pow(2, zoom); 6473 }, 6474 6475 // @method zoom(scale: Number): Number 6476 // Inverse of `scale()`, returns the zoom level corresponding to a scale 6477 // factor of `scale`. 6478 zoom: function (scale) { 6479 return Math.log(scale / 256) / Math.LN2; 6480 }, 6481 6482 // @method getProjectedBounds(zoom: Number): Bounds 6483 // Returns the projection's bounds scaled and transformed for the provided `zoom`. 6484 getProjectedBounds: function (zoom) { 6485 if (this.infinite) { return null; } 6486 6487 var b = this.projection.bounds, 6488 s = this.scale(zoom), 6489 min = this.transformation.transform(b.min, s), 6490 max = this.transformation.transform(b.max, s); 6491 6492 return new Bounds(min, max); 6493 }, 6494 6495 // @method distance(latlng1: LatLng, latlng2: LatLng): Number 6496 // Returns the distance between two geographical coordinates. 6497 6498 // @property code: String 6499 // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) 6500 // 6501 // @property wrapLng: Number[] 6502 // An array of two numbers defining whether the longitude (horizontal) coordinate 6503 // axis wraps around a given range and how. Defaults to `[-180, 180]` in most 6504 // geographical CRSs. If `undefined`, the longitude axis does not wrap around. 6505 // 6506 // @property wrapLat: Number[] 6507 // Like `wrapLng`, but for the latitude (vertical) axis. 6508 6509 // wrapLng: [min, max], 6510 // wrapLat: [min, max], 6511 6512 // @property infinite: Boolean 6513 // If true, the coordinate space will be unbounded (infinite in both axes) 6514 infinite: false, 6515 6516 // @method wrapLatLng(latlng: LatLng): LatLng 6517 // Returns a `LatLng` where lat and lng has been wrapped according to the 6518 // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. 6519 wrapLatLng: function (latlng) { 6520 var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, 6521 lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, 6522 alt = latlng.alt; 6523 6524 return new LatLng(lat, lng, alt); 6525 }, 6526 6527 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds 6528 // Returns a `LatLngBounds` with the same size as the given one, ensuring 6529 // that its center is within the CRS's bounds. 6530 // Only accepts actual `L.LatLngBounds` instances, not arrays. 6531 wrapLatLngBounds: function (bounds) { 6532 var center = bounds.getCenter(), 6533 newCenter = this.wrapLatLng(center), 6534 latShift = center.lat - newCenter.lat, 6535 lngShift = center.lng - newCenter.lng; 6536 6537 if (latShift === 0 && lngShift === 0) { 6538 return bounds; 6539 } 6540 6541 var sw = bounds.getSouthWest(), 6542 ne = bounds.getNorthEast(), 6543 newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift), 6544 newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift); 6545 6546 return new LatLngBounds(newSw, newNe); 6547 } 6548 }; 6549 6550 /* 6551 * @namespace CRS 6552 * @crs L.CRS.Earth 6553 * 6554 * Serves as the base for CRS that are global such that they cover the earth. 6555 * Can only be used as the base for other CRS and cannot be used directly, 6556 * since it does not have a `code`, `projection` or `transformation`. `distance()` returns 6557 * meters. 6558 */ 6559 6560 var Earth = extend({}, CRS, { 6561 wrapLng: [-180, 180], 6562 6563 // Mean Earth Radius, as recommended for use by 6564 // the International Union of Geodesy and Geophysics, 6565 // see https://rosettacode.org/wiki/Haversine_formula 6566 R: 6371000, 6567 6568 // distance between two geographical points using spherical law of cosines approximation 6569 distance: function (latlng1, latlng2) { 6570 var rad = Math.PI / 180, 6571 lat1 = latlng1.lat * rad, 6572 lat2 = latlng2.lat * rad, 6573 sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2), 6574 sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2), 6575 a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon, 6576 c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 6577 return this.R * c; 6578 } 6579 }); 6580 6581 /* 6582 * @namespace Projection 6583 * @projection L.Projection.SphericalMercator 6584 * 6585 * Spherical Mercator projection â the most common projection for online maps, 6586 * used by almost all free and commercial tile providers. Assumes that Earth is 6587 * a sphere. Used by the `EPSG:3857` CRS. 6588 */ 6589 6590 var earthRadius = 6378137; 6591 6592 var SphericalMercator = { 6593 6594 R: earthRadius, 6595 MAX_LATITUDE: 85.0511287798, 6596 6597 project: function (latlng) { 6598 var d = Math.PI / 180, 6599 max = this.MAX_LATITUDE, 6600 lat = Math.max(Math.min(max, latlng.lat), -max), 6601 sin = Math.sin(lat * d); 6602 6603 return new Point( 6604 this.R * latlng.lng * d, 6605 this.R * Math.log((1 + sin) / (1 - sin)) / 2); 6606 }, 6607 6608 unproject: function (point) { 6609 var d = 180 / Math.PI; 6610 6611 return new LatLng( 6612 (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, 6613 point.x * d / this.R); 6614 }, 6615 6616 bounds: (function () { 6617 var d = earthRadius * Math.PI; 6618 return new Bounds([-d, -d], [d, d]); 6619 })() 6620 }; 6621 6622 /* 6623 * @class Transformation 6624 * @aka L.Transformation 6625 * 6626 * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d` 6627 * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing 6628 * the reverse. Used by Leaflet in its projections code. 6629 * 6630 * @example 6631 * 6632 * ```js 6633 * var transformation = L.transformation(2, 5, -1, 10), 6634 * p = L.point(1, 2), 6635 * p2 = transformation.transform(p), // L.point(7, 8) 6636 * p3 = transformation.untransform(p2); // L.point(1, 2) 6637 * ``` 6638 */ 6639 6640 6641 // factory new L.Transformation(a: Number, b: Number, c: Number, d: Number) 6642 // Creates a `Transformation` object with the given coefficients. 6643 function Transformation(a, b, c, d) { 6644 if (isArray(a)) { 6645 // use array properties 6646 this._a = a[0]; 6647 this._b = a[1]; 6648 this._c = a[2]; 6649 this._d = a[3]; 6650 return; 6651 } 6652 this._a = a; 6653 this._b = b; 6654 this._c = c; 6655 this._d = d; 6656 } 6657 6658 Transformation.prototype = { 6659 // @method transform(point: Point, scale?: Number): Point 6660 // Returns a transformed point, optionally multiplied by the given scale. 6661 // Only accepts actual `L.Point` instances, not arrays. 6662 transform: function (point, scale) { // (Point, Number) -> Point 6663 return this._transform(point.clone(), scale); 6664 }, 6665 6666 // destructive transform (faster) 6667 _transform: function (point, scale) { 6668 scale = scale || 1; 6669 point.x = scale * (this._a * point.x + this._b); 6670 point.y = scale * (this._c * point.y + this._d); 6671 return point; 6672 }, 6673 6674 // @method untransform(point: Point, scale?: Number): Point 6675 // Returns the reverse transformation of the given point, optionally divided 6676 // by the given scale. Only accepts actual `L.Point` instances, not arrays. 6677 untransform: function (point, scale) { 6678 scale = scale || 1; 6679 return new Point( 6680 (point.x / scale - this._b) / this._a, 6681 (point.y / scale - this._d) / this._c); 6682 } 6683 }; 6684 6685 // factory L.transformation(a: Number, b: Number, c: Number, d: Number) 6686 6687 // @factory L.transformation(a: Number, b: Number, c: Number, d: Number) 6688 // Instantiates a Transformation object with the given coefficients. 6689 6690 // @alternative 6691 // @factory L.transformation(coefficients: Array): Transformation 6692 // Expects an coefficients array of the form 6693 // `[a: Number, b: Number, c: Number, d: Number]`. 6694 6695 function toTransformation(a, b, c, d) { 6696 return new Transformation(a, b, c, d); 6697 } 6698 6699 /* 6700 * @namespace CRS 6701 * @crs L.CRS.EPSG3857 6702 * 6703 * The most common CRS for online maps, used by almost all free and commercial 6704 * tile providers. Uses Spherical Mercator projection. Set in by default in 6705 * Map's `crs` option. 6706 */ 6707 6708 var EPSG3857 = extend({}, Earth, { 6709 code: 'EPSG:3857', 6710 projection: SphericalMercator, 6711 6712 transformation: (function () { 6713 var scale = 0.5 / (Math.PI * SphericalMercator.R); 6714 return toTransformation(scale, 0.5, -scale, 0.5); 6715 }()) 6716 }); 6717 6718 var EPSG900913 = extend({}, EPSG3857, { 6719 code: 'EPSG:900913' 6720 }); 6721 6722 // @namespace SVG; @section 6723 // There are several static functions which can be called without instantiating L.SVG: 6724 6725 // @function create(name: String): SVGElement 6726 // Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement), 6727 // corresponding to the class name passed. For example, using 'line' will return 6728 // an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement). 6729 function svgCreate(name) { 6730 return document.createElementNS('http://www.w3.org/2000/svg', name); 6731 } 6732 6733 // @function pointsToPath(rings: Point[], closed: Boolean): String 6734 // Generates a SVG path string for multiple rings, with each ring turning 6735 // into "M..L..L.." instructions 6736 function pointsToPath(rings, closed) { 6737 var str = '', 6738 i, j, len, len2, points, p; 6739 6740 for (i = 0, len = rings.length; i < len; i++) { 6741 points = rings[i]; 6742 6743 for (j = 0, len2 = points.length; j < len2; j++) { 6744 p = points[j]; 6745 str += (j ? 'L' : 'M') + p.x + ' ' + p.y; 6746 } 6747 6748 // closes the ring for polygons; "x" is VML syntax 6749 str += closed ? (Browser.svg ? 'z' : 'x') : ''; 6750 } 6751 6752 // SVG complains about empty path strings 6753 return str || 'M0 0'; 6754 } 6755 6756 /* 6757 * @namespace Browser 6758 * @aka L.Browser 6759 * 6760 * A namespace with static properties for browser/feature detection used by Leaflet internally. 6761 * 6762 * @example 6763 * 6764 * ```js 6765 * if (L.Browser.ielt9) { 6766 * alert('Upgrade your browser, dude!'); 6767 * } 6768 * ``` 6769 */ 6770 6771 var style = document.documentElement.style; 6772 6773 // @property ie: Boolean; `true` for all Internet Explorer versions (not Edge). 6774 var ie = 'ActiveXObject' in window; 6775 6776 // @property ielt9: Boolean; `true` for Internet Explorer versions less than 9. 6777 var ielt9 = ie && !document.addEventListener; 6778 6779 // @property edge: Boolean; `true` for the Edge web browser. 6780 var edge = 'msLaunchUri' in navigator && !('documentMode' in document); 6781 6782 // @property webkit: Boolean; 6783 // `true` for webkit-based browsers like Chrome and Safari (including mobile versions). 6784 var webkit = userAgentContains('webkit'); 6785 6786 // @property android: Boolean 6787 // **Deprecated.** `true` for any browser running on an Android platform. 6788 var android = userAgentContains('android'); 6789 6790 // @property android23: Boolean; **Deprecated.** `true` for browsers running on Android 2 or Android 3. 6791 var android23 = userAgentContains('android 2') || userAgentContains('android 3'); 6792 6793 /* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */ 6794 var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit 6795 // @property androidStock: Boolean; **Deprecated.** `true` for the Android stock browser (i.e. not Chrome) 6796 var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window); 6797 6798 // @property opera: Boolean; `true` for the Opera browser 6799 var opera = !!window.opera; 6800 6801 // @property chrome: Boolean; `true` for the Chrome browser. 6802 var chrome = !edge && userAgentContains('chrome'); 6803 6804 // @property gecko: Boolean; `true` for gecko-based browsers like Firefox. 6805 var gecko = userAgentContains('gecko') && !webkit && !opera && !ie; 6806 6807 // @property safari: Boolean; `true` for the Safari browser. 6808 var safari = !chrome && userAgentContains('safari'); 6809 6810 var phantom = userAgentContains('phantom'); 6811 6812 // @property opera12: Boolean 6813 // `true` for the Opera browser supporting CSS transforms (version 12 or later). 6814 var opera12 = 'OTransition' in style; 6815 6816 // @property win: Boolean; `true` when the browser is running in a Windows platform 6817 var win = navigator.platform.indexOf('Win') === 0; 6818 6819 // @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms. 6820 var ie3d = ie && ('transition' in style); 6821 6822 // @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms. 6823 var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23; 6824 6825 // @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms. 6826 var gecko3d = 'MozPerspective' in style; 6827 6828 // @property any3d: Boolean 6829 // `true` for all browsers supporting CSS transforms. 6830 var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom; 6831 6832 // @property mobile: Boolean; `true` for all browsers running in a mobile device. 6833 var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile'); 6834 6835 // @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device. 6836 var mobileWebkit = mobile && webkit; 6837 6838 // @property mobileWebkit3d: Boolean 6839 // `true` for all webkit-based browsers in a mobile device supporting CSS transforms. 6840 var mobileWebkit3d = mobile && webkit3d; 6841 6842 // @property msPointer: Boolean 6843 // `true` for browsers implementing the Microsoft touch events model (notably IE10). 6844 var msPointer = !window.PointerEvent && window.MSPointerEvent; 6845 6846 // @property pointer: Boolean 6847 // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx). 6848 var pointer = !!(window.PointerEvent || msPointer); 6849 6850 // @property touchNative: Boolean 6851 // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events). 6852 // **This does not necessarily mean** that the browser is running in a computer with 6853 // a touchscreen, it only means that the browser is capable of understanding 6854 // touch events. 6855 var touchNative = 'ontouchstart' in window || !!window.TouchEvent; 6856 6857 // @property touch: Boolean 6858 // `true` for all browsers supporting either [touch](#browser-touch) or [pointer](#browser-pointer) events. 6859 // Note: pointer events will be preferred (if available), and processed for all `touch*` listeners. 6860 var touch = !window.L_NO_TOUCH && (touchNative || pointer); 6861 6862 // @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device. 6863 var mobileOpera = mobile && opera; 6864 6865 // @property mobileGecko: Boolean 6866 // `true` for gecko-based browsers running in a mobile device. 6867 var mobileGecko = mobile && gecko; 6868 6869 // @property retina: Boolean 6870 // `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%. 6871 var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1; 6872 6873 // @property passiveEvents: Boolean 6874 // `true` for browsers that support passive events. 6875 var passiveEvents = (function () { 6876 var supportsPassiveOption = false; 6877 try { 6878 var opts = Object.defineProperty({}, 'passive', { 6879 get: function () { // eslint-disable-line getter-return 6880 supportsPassiveOption = true; 6881 } 6882 }); 6883 window.addEventListener('testPassiveEventSupport', falseFn, opts); 6884 window.removeEventListener('testPassiveEventSupport', falseFn, opts); 6885 } catch (e) { 6886 // Errors can safely be ignored since this is only a browser support test. 6887 } 6888 return supportsPassiveOption; 6889 }()); 6890 6891 // @property canvas: Boolean 6892 // `true` when the browser supports [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API). 6893 var canvas$1 = (function () { 6894 return !!document.createElement('canvas').getContext; 6895 }()); 6896 6897 // @property svg: Boolean 6898 // `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG). 6899 var svg$1 = !!(document.createElementNS && svgCreate('svg').createSVGRect); 6900 6901 var inlineSvg = !!svg$1 && (function () { 6902 var div = document.createElement('div'); 6903 div.innerHTML = '<svg/>'; 6904 return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg'; 6905 })(); 6906 6907 // @property vml: Boolean 6908 // `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language). 6909 var vml = !svg$1 && (function () { 6910 try { 6911 var div = document.createElement('div'); 6912 div.innerHTML = '<v:shape adj="1"/>'; 6913 6914 var shape = div.firstChild; 6915 shape.style.behavior = 'url(#default#VML)'; 6916 6917 return shape && (typeof shape.adj === 'object'); 6918 6919 } catch (e) { 6920 return false; 6921 } 6922 }()); 6923 6924 6925 // @property mac: Boolean; `true` when the browser is running in a Mac platform 6926 var mac = navigator.platform.indexOf('Mac') === 0; 6927 6928 // @property mac: Boolean; `true` when the browser is running in a Linux platform 6929 var linux = navigator.platform.indexOf('Linux') === 0; 6930 6931 function userAgentContains(str) { 6932 return navigator.userAgent.toLowerCase().indexOf(str) >= 0; 6933 } 6934 6935 6936 var Browser = { 6937 ie: ie, 6938 ielt9: ielt9, 6939 edge: edge, 6940 webkit: webkit, 6941 android: android, 6942 android23: android23, 6943 androidStock: androidStock, 6944 opera: opera, 6945 chrome: chrome, 6946 gecko: gecko, 6947 safari: safari, 6948 phantom: phantom, 6949 opera12: opera12, 6950 win: win, 6951 ie3d: ie3d, 6952 webkit3d: webkit3d, 6953 gecko3d: gecko3d, 6954 any3d: any3d, 6955 mobile: mobile, 6956 mobileWebkit: mobileWebkit, 6957 mobileWebkit3d: mobileWebkit3d, 6958 msPointer: msPointer, 6959 pointer: pointer, 6960 touch: touch, 6961 touchNative: touchNative, 6962 mobileOpera: mobileOpera, 6963 mobileGecko: mobileGecko, 6964 retina: retina, 6965 passiveEvents: passiveEvents, 6966 canvas: canvas$1, 6967 svg: svg$1, 6968 vml: vml, 6969 inlineSvg: inlineSvg, 6970 mac: mac, 6971 linux: linux 6972 }; 6973 6974 /* 6975 * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. 6976 */ 6977 6978 var POINTER_DOWN = Browser.msPointer ? 'MSPointerDown' : 'pointerdown'; 6979 var POINTER_MOVE = Browser.msPointer ? 'MSPointerMove' : 'pointermove'; 6980 var POINTER_UP = Browser.msPointer ? 'MSPointerUp' : 'pointerup'; 6981 var POINTER_CANCEL = Browser.msPointer ? 'MSPointerCancel' : 'pointercancel'; 6982 var pEvent = { 6983 touchstart : POINTER_DOWN, 6984 touchmove : POINTER_MOVE, 6985 touchend : POINTER_UP, 6986 touchcancel : POINTER_CANCEL 6987 }; 6988 var handle = { 6989 touchstart : _onPointerStart, 6990 touchmove : _handlePointer, 6991 touchend : _handlePointer, 6992 touchcancel : _handlePointer 6993 }; 6994 var _pointers = {}; 6995 var _pointerDocListener = false; 6996 6997 // Provides a touch events wrapper for (ms)pointer events. 6998 // ref https://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 6999 7000 function addPointerListener(obj, type, handler) { 7001 if (type === 'touchstart') { 7002 _addPointerDocListener(); 7003 } 7004 if (!handle[type]) { 7005 console.warn('wrong event specified:', type); 7006 return falseFn; 7007 } 7008 handler = handle[type].bind(this, handler); 7009 obj.addEventListener(pEvent[type], handler, false); 7010 return handler; 7011 } 7012 7013 function removePointerListener(obj, type, handler) { 7014 if (!pEvent[type]) { 7015 console.warn('wrong event specified:', type); 7016 return; 7017 } 7018 obj.removeEventListener(pEvent[type], handler, false); 7019 } 7020 7021 function _globalPointerDown(e) { 7022 _pointers[e.pointerId] = e; 7023 } 7024 7025 function _globalPointerMove(e) { 7026 if (_pointers[e.pointerId]) { 7027 _pointers[e.pointerId] = e; 7028 } 7029 } 7030 7031 function _globalPointerUp(e) { 7032 delete _pointers[e.pointerId]; 7033 } 7034 7035 function _addPointerDocListener() { 7036 // need to keep track of what pointers and how many are active to provide e.touches emulation 7037 if (!_pointerDocListener) { 7038 // we listen document as any drags that end by moving the touch off the screen get fired there 7039 document.addEventListener(POINTER_DOWN, _globalPointerDown, true); 7040 document.addEventListener(POINTER_MOVE, _globalPointerMove, true); 7041 document.addEventListener(POINTER_UP, _globalPointerUp, true); 7042 document.addEventListener(POINTER_CANCEL, _globalPointerUp, true); 7043 7044 _pointerDocListener = true; 7045 } 7046 } 7047 7048 function _handlePointer(handler, e) { 7049 if (e.pointerType === (e.MSPOINTER_TYPE_MOUSE || 'mouse')) { return; } 7050 7051 e.touches = []; 7052 for (var i in _pointers) { 7053 e.touches.push(_pointers[i]); 7054 } 7055 e.changedTouches = [e]; 7056 7057 handler(e); 7058 } 7059 7060 function _onPointerStart(handler, e) { 7061 // IE10 specific: MsTouch needs preventDefault. See #2000 7062 if (e.MSPOINTER_TYPE_TOUCH && e.pointerType === e.MSPOINTER_TYPE_TOUCH) { 7063 preventDefault(e); 7064 } 7065 _handlePointer(handler, e); 7066 } 7067 7068 /* 7069 * Extends the event handling code with double tap support for mobile browsers. 7070 * 7071 * Note: currently most browsers fire native dblclick, with only a few exceptions 7072 * (see https://github.com/Leaflet/Leaflet/issues/7012#issuecomment-595087386) 7073 */ 7074 7075 function makeDblclick(event) { 7076 // in modern browsers `type` cannot be just overridden: 7077 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only 7078 var newEvent = {}, 7079 prop, i; 7080 for (i in event) { 7081 prop = event[i]; 7082 newEvent[i] = prop && prop.bind ? prop.bind(event) : prop; 7083 } 7084 event = newEvent; 7085 newEvent.type = 'dblclick'; 7086 newEvent.detail = 2; 7087 newEvent.isTrusted = false; 7088 newEvent._simulated = true; // for debug purposes 7089 return newEvent; 7090 } 7091 7092 var delay = 200; 7093 function addDoubleTapListener(obj, handler) { 7094 // Most browsers handle double tap natively 7095 obj.addEventListener('dblclick', handler); 7096 7097 // On some platforms the browser doesn't fire native dblclicks for touch events. 7098 // It seems that in all such cases `detail` property of `click` event is always `1`. 7099 // So here we rely on that fact to avoid excessive 'dblclick' simulation when not needed. 7100 var last = 0, 7101 detail; 7102 function simDblclick(e) { 7103 if (e.detail !== 1) { 7104 detail = e.detail; // keep in sync to avoid false dblclick in some cases 7105 return; 7106 } 7107 7108 if (e.pointerType === 'mouse' || 7109 (e.sourceCapabilities && !e.sourceCapabilities.firesTouchEvents)) { 7110 7111 return; 7112 } 7113 7114 // When clicking on an <input>, the browser generates a click on its 7115 // <label> (and vice versa) triggering two clicks in quick succession. 7116 // This ignores clicks on elements which are a label with a 'for' 7117 // attribute (or children of such a label), but not children of 7118 // a <input>. 7119 var path = getPropagationPath(e); 7120 if (path.some(function (el) { 7121 return el instanceof HTMLLabelElement && el.attributes.for; 7122 }) && 7123 !path.some(function (el) { 7124 return ( 7125 el instanceof HTMLInputElement || 7126 el instanceof HTMLSelectElement 7127 ); 7128 }) 7129 ) { 7130 return; 7131 } 7132 7133 var now = Date.now(); 7134 if (now - last <= delay) { 7135 detail++; 7136 if (detail === 2) { 7137 handler(makeDblclick(e)); 7138 } 7139 } else { 7140 detail = 1; 7141 } 7142 last = now; 7143 } 7144 7145 obj.addEventListener('click', simDblclick); 7146 7147 return { 7148 dblclick: handler, 7149 simDblclick: simDblclick 7150 }; 7151 } 7152 7153 function removeDoubleTapListener(obj, handlers) { 7154 obj.removeEventListener('dblclick', handlers.dblclick); 7155 obj.removeEventListener('click', handlers.simDblclick); 7156 } 7157 7158 /* 7159 * @namespace DomUtil 7160 * 7161 * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model) 7162 * tree, used by Leaflet internally. 7163 * 7164 * Most functions expecting or returning a `HTMLElement` also work for 7165 * SVG elements. The only difference is that classes refer to CSS classes 7166 * in HTML and SVG classes in SVG. 7167 */ 7168 7169 7170 // @property TRANSFORM: String 7171 // Vendor-prefixed transform style name (e.g. `'webkitTransform'` for WebKit). 7172 var TRANSFORM = testProp( 7173 ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']); 7174 7175 // webkitTransition comes first because some browser versions that drop vendor prefix don't do 7176 // the same for the transitionend event, in particular the Android 4.1 stock browser 7177 7178 // @property TRANSITION: String 7179 // Vendor-prefixed transition style name. 7180 var TRANSITION = testProp( 7181 ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); 7182 7183 // @property TRANSITION_END: String 7184 // Vendor-prefixed transitionend event name. 7185 var TRANSITION_END = 7186 TRANSITION === 'webkitTransition' || TRANSITION === 'OTransition' ? TRANSITION + 'End' : 'transitionend'; 7187 7188 7189 // @function get(id: String|HTMLElement): HTMLElement 7190 // Returns an element given its DOM id, or returns the element itself 7191 // if it was passed directly. 7192 function get(id) { 7193 return typeof id === 'string' ? document.getElementById(id) : id; 7194 } 7195 7196 // @function getStyle(el: HTMLElement, styleAttrib: String): String 7197 // Returns the value for a certain style attribute on an element, 7198 // including computed values or values set through CSS. 7199 function getStyle(el, style) { 7200 var value = el.style[style] || (el.currentStyle && el.currentStyle[style]); 7201 7202 if ((!value || value === 'auto') && document.defaultView) { 7203 var css = document.defaultView.getComputedStyle(el, null); 7204 value = css ? css[style] : null; 7205 } 7206 return value === 'auto' ? null : value; 7207 } 7208 7209 // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement 7210 // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element. 7211 function create$1(tagName, className, container) { 7212 var el = document.createElement(tagName); 7213 el.className = className || ''; 7214 7215 if (container) { 7216 container.appendChild(el); 7217 } 7218 return el; 7219 } 7220 7221 // @function remove(el: HTMLElement) 7222 // Removes `el` from its parent element 7223 function remove(el) { 7224 var parent = el.parentNode; 7225 if (parent) { 7226 parent.removeChild(el); 7227 } 7228 } 7229 7230 // @function empty(el: HTMLElement) 7231 // Removes all of `el`'s children elements from `el` 7232 function empty(el) { 7233 while (el.firstChild) { 7234 el.removeChild(el.firstChild); 7235 } 7236 } 7237 7238 // @function toFront(el: HTMLElement) 7239 // Makes `el` the last child of its parent, so it renders in front of the other children. 7240 function toFront(el) { 7241 var parent = el.parentNode; 7242 if (parent && parent.lastChild !== el) { 7243 parent.appendChild(el); 7244 } 7245 } 7246 7247 // @function toBack(el: HTMLElement) 7248 // Makes `el` the first child of its parent, so it renders behind the other children. 7249 function toBack(el) { 7250 var parent = el.parentNode; 7251 if (parent && parent.firstChild !== el) { 7252 parent.insertBefore(el, parent.firstChild); 7253 } 7254 } 7255 7256 // @function hasClass(el: HTMLElement, name: String): Boolean 7257 // Returns `true` if the element's class attribute contains `name`. 7258 function hasClass(el, name) { 7259 if (el.classList !== undefined) { 7260 return el.classList.contains(name); 7261 } 7262 var className = getClass(el); 7263 return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); 7264 } 7265 7266 // @function addClass(el: HTMLElement, name: String) 7267 // Adds `name` to the element's class attribute. 7268 function addClass(el, name) { 7269 if (el.classList !== undefined) { 7270 var classes = splitWords(name); 7271 for (var i = 0, len = classes.length; i < len; i++) { 7272 el.classList.add(classes[i]); 7273 } 7274 } else if (!hasClass(el, name)) { 7275 var className = getClass(el); 7276 setClass(el, (className ? className + ' ' : '') + name); 7277 } 7278 } 7279 7280 // @function removeClass(el: HTMLElement, name: String) 7281 // Removes `name` from the element's class attribute. 7282 function removeClass(el, name) { 7283 if (el.classList !== undefined) { 7284 el.classList.remove(name); 7285 } else { 7286 setClass(el, trim((' ' + getClass(el) + ' ').replace(' ' + name + ' ', ' '))); 7287 } 7288 } 7289 7290 // @function setClass(el: HTMLElement, name: String) 7291 // Sets the element's class. 7292 function setClass(el, name) { 7293 if (el.className.baseVal === undefined) { 7294 el.className = name; 7295 } else { 7296 // in case of SVG element 7297 el.className.baseVal = name; 7298 } 7299 } 7300 7301 // @function getClass(el: HTMLElement): String 7302 // Returns the element's class. 7303 function getClass(el) { 7304 // Check if the element is an SVGElementInstance and use the correspondingElement instead 7305 // (Required for linked SVG elements in IE11.) 7306 if (el.correspondingElement) { 7307 el = el.correspondingElement; 7308 } 7309 return el.className.baseVal === undefined ? el.className : el.className.baseVal; 7310 } 7311 7312 // @function setOpacity(el: HTMLElement, opacity: Number) 7313 // Set the opacity of an element (including old IE support). 7314 // `opacity` must be a number from `0` to `1`. 7315 function setOpacity(el, value) { 7316 if ('opacity' in el.style) { 7317 el.style.opacity = value; 7318 } else if ('filter' in el.style) { 7319 _setOpacityIE(el, value); 7320 } 7321 } 7322 7323 function _setOpacityIE(el, value) { 7324 var filter = false, 7325 filterName = 'DXImageTransform.Microsoft.Alpha'; 7326 7327 // filters collection throws an error if we try to retrieve a filter that doesn't exist 7328 try { 7329 filter = el.filters.item(filterName); 7330 } catch (e) { 7331 // don't set opacity to 1 if we haven't already set an opacity, 7332 // it isn't needed and breaks transparent pngs. 7333 if (value === 1) { return; } 7334 } 7335 7336 value = Math.round(value * 100); 7337 7338 if (filter) { 7339 filter.Enabled = (value !== 100); 7340 filter.Opacity = value; 7341 } else { 7342 el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; 7343 } 7344 } 7345 7346 // @function testProp(props: String[]): String|false 7347 // Goes through the array of style names and returns the first name 7348 // that is a valid style name for an element. If no such name is found, 7349 // it returns false. Useful for vendor-prefixed styles like `transform`. 7350 function testProp(props) { 7351 var style = document.documentElement.style; 7352 7353 for (var i = 0; i < props.length; i++) { 7354 if (props[i] in style) { 7355 return props[i]; 7356 } 7357 } 7358 return false; 7359 } 7360 7361 // @function setTransform(el: HTMLElement, offset: Point, scale?: Number) 7362 // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels 7363 // and optionally scaled by `scale`. Does not have an effect if the 7364 // browser doesn't support 3D CSS transforms. 7365 function setTransform(el, offset, scale) { 7366 var pos = offset || new Point(0, 0); 7367 7368 el.style[TRANSFORM] = 7369 (Browser.ie3d ? 7370 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 7371 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + 7372 (scale ? ' scale(' + scale + ')' : ''); 7373 } 7374 7375 // @function setPosition(el: HTMLElement, position: Point) 7376 // Sets the position of `el` to coordinates specified by `position`, 7377 // using CSS translate or top/left positioning depending on the browser 7378 // (used by Leaflet internally to position its layers). 7379 function setPosition(el, point) { 7380 7381 /*eslint-disable */ 7382 el._leaflet_pos = point; 7383 /* eslint-enable */ 7384 7385 if (Browser.any3d) { 7386 setTransform(el, point); 7387 } else { 7388 el.style.left = point.x + 'px'; 7389 el.style.top = point.y + 'px'; 7390 } 7391 } 7392 7393 // @function getPosition(el: HTMLElement): Point 7394 // Returns the coordinates of an element previously positioned with setPosition. 7395 function getPosition(el) { 7396 // this method is only used for elements previously positioned using setPosition, 7397 // so it's safe to cache the position for performance 7398 7399 return el._leaflet_pos || new Point(0, 0); 7400 } 7401 7402 // @function disableTextSelection() 7403 // Prevents the user from generating `selectstart` DOM events, usually generated 7404 // when the user drags the mouse through a page with text. Used internally 7405 // by Leaflet to override the behaviour of any click-and-drag interaction on 7406 // the map. Affects drag interactions on the whole document. 7407 7408 // @function enableTextSelection() 7409 // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection). 7410 var disableTextSelection; 7411 var enableTextSelection; 7412 var _userSelect; 7413 if ('onselectstart' in document) { 7414 disableTextSelection = function () { 7415 on(window, 'selectstart', preventDefault); 7416 }; 7417 enableTextSelection = function () { 7418 off(window, 'selectstart', preventDefault); 7419 }; 7420 } else { 7421 var userSelectProperty = testProp( 7422 ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); 7423 7424 disableTextSelection = function () { 7425 if (userSelectProperty) { 7426 var style = document.documentElement.style; 7427 _userSelect = style[userSelectProperty]; 7428 style[userSelectProperty] = 'none'; 7429 } 7430 }; 7431 enableTextSelection = function () { 7432 if (userSelectProperty) { 7433 document.documentElement.style[userSelectProperty] = _userSelect; 7434 _userSelect = undefined; 7435 } 7436 }; 7437 } 7438 7439 // @function disableImageDrag() 7440 // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but 7441 // for `dragstart` DOM events, usually generated when the user drags an image. 7442 function disableImageDrag() { 7443 on(window, 'dragstart', preventDefault); 7444 } 7445 7446 // @function enableImageDrag() 7447 // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection). 7448 function enableImageDrag() { 7449 off(window, 'dragstart', preventDefault); 7450 } 7451 7452 var _outlineElement, _outlineStyle; 7453 // @function preventOutline(el: HTMLElement) 7454 // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline) 7455 // of the element `el` invisible. Used internally by Leaflet to prevent 7456 // focusable elements from displaying an outline when the user performs a 7457 // drag interaction on them. 7458 function preventOutline(element) { 7459 while (element.tabIndex === -1) { 7460 element = element.parentNode; 7461 } 7462 if (!element.style) { return; } 7463 restoreOutline(); 7464 _outlineElement = element; 7465 _outlineStyle = element.style.outlineStyle; 7466 element.style.outlineStyle = 'none'; 7467 on(window, 'keydown', restoreOutline); 7468 } 7469 7470 // @function restoreOutline() 7471 // Cancels the effects of a previous [`L.DomUtil.preventOutline`](). 7472 function restoreOutline() { 7473 if (!_outlineElement) { return; } 7474 _outlineElement.style.outlineStyle = _outlineStyle; 7475 _outlineElement = undefined; 7476 _outlineStyle = undefined; 7477 off(window, 'keydown', restoreOutline); 7478 } 7479 7480 // @function getSizedParentNode(el: HTMLElement): HTMLElement 7481 // Finds the closest parent node which size (width and height) is not null. 7482 function getSizedParentNode(element) { 7483 do { 7484 element = element.parentNode; 7485 } while ((!element.offsetWidth || !element.offsetHeight) && element !== document.body); 7486 return element; 7487 } 7488 7489 // @function getScale(el: HTMLElement): Object 7490 // Computes the CSS scale currently applied on the element. 7491 // Returns an object with `x` and `y` members as horizontal and vertical scales respectively, 7492 // and `boundingClientRect` as the result of [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). 7493 function getScale(element) { 7494 var rect = element.getBoundingClientRect(); // Read-only in old browsers. 7495 7496 return { 7497 x: rect.width / element.offsetWidth || 1, 7498 y: rect.height / element.offsetHeight || 1, 7499 boundingClientRect: rect 7500 }; 7501 } 7502 7503 var DomUtil = { 7504 __proto__: null, 7505 TRANSFORM: TRANSFORM, 7506 TRANSITION: TRANSITION, 7507 TRANSITION_END: TRANSITION_END, 7508 get: get, 7509 getStyle: getStyle, 7510 create: create$1, 7511 remove: remove, 7512 empty: empty, 7513 toFront: toFront, 7514 toBack: toBack, 7515 hasClass: hasClass, 7516 addClass: addClass, 7517 removeClass: removeClass, 7518 setClass: setClass, 7519 getClass: getClass, 7520 setOpacity: setOpacity, 7521 testProp: testProp, 7522 setTransform: setTransform, 7523 setPosition: setPosition, 7524 getPosition: getPosition, 7525 get disableTextSelection () { return disableTextSelection; }, 7526 get enableTextSelection () { return enableTextSelection; }, 7527 disableImageDrag: disableImageDrag, 7528 enableImageDrag: enableImageDrag, 7529 preventOutline: preventOutline, 7530 restoreOutline: restoreOutline, 7531 getSizedParentNode: getSizedParentNode, 7532 getScale: getScale 7533 }; 7534 7535 /* 7536 * @namespace DomEvent 7537 * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally. 7538 */ 7539 7540 // Inspired by John Resig, Dean Edwards and YUI addEvent implementations. 7541 7542 // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this 7543 // Adds a listener function (`fn`) to a particular DOM event type of the 7544 // element `el`. You can optionally specify the context of the listener 7545 // (object the `this` keyword will point to). You can also pass several 7546 // space-separated types (e.g. `'click dblclick'`). 7547 7548 // @alternative 7549 // @function on(el: HTMLElement, eventMap: Object, context?: Object): this 7550 // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` 7551 function on(obj, types, fn, context) { 7552 7553 if (types && typeof types === 'object') { 7554 for (var type in types) { 7555 addOne(obj, type, types[type], fn); 7556 } 7557 } else { 7558 types = splitWords(types); 7559 7560 for (var i = 0, len = types.length; i < len; i++) { 7561 addOne(obj, types[i], fn, context); 7562 } 7563 } 7564 7565 return this; 7566 } 7567 7568 var eventsKey = '_leaflet_events'; 7569 7570 // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this 7571 // Removes a previously added listener function. 7572 // Note that if you passed a custom context to on, you must pass the same 7573 // context to `off` in order to remove the listener. 7574 7575 // @alternative 7576 // @function off(el: HTMLElement, eventMap: Object, context?: Object): this 7577 // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` 7578 7579 // @alternative 7580 // @function off(el: HTMLElement, types: String): this 7581 // Removes all previously added listeners of given types. 7582 7583 // @alternative 7584 // @function off(el: HTMLElement): this 7585 // Removes all previously added listeners from given HTMLElement 7586 function off(obj, types, fn, context) { 7587 7588 if (arguments.length === 1) { 7589 batchRemove(obj); 7590 delete obj[eventsKey]; 7591 7592 } else if (types && typeof types === 'object') { 7593 for (var type in types) { 7594 removeOne(obj, type, types[type], fn); 7595 } 7596 7597 } else { 7598 types = splitWords(types); 7599 7600 if (arguments.length === 2) { 7601 batchRemove(obj, function (type) { 7602 return indexOf(types, type) !== -1; 7603 }); 7604 } else { 7605 for (var i = 0, len = types.length; i < len; i++) { 7606 removeOne(obj, types[i], fn, context); 7607 } 7608 } 7609 } 7610 7611 return this; 7612 } 7613 7614 function batchRemove(obj, filterFn) { 7615 for (var id in obj[eventsKey]) { 7616 var type = id.split(/\d/)[0]; 7617 if (!filterFn || filterFn(type)) { 7618 removeOne(obj, type, null, null, id); 7619 } 7620 } 7621 } 7622 7623 var mouseSubst = { 7624 mouseenter: 'mouseover', 7625 mouseleave: 'mouseout', 7626 wheel: !('onwheel' in window) && 'mousewheel' 7627 }; 7628 7629 function addOne(obj, type, fn, context) { 7630 var id = type + stamp(fn) + (context ? '_' + stamp(context) : ''); 7631 7632 if (obj[eventsKey] && obj[eventsKey][id]) { return this; } 7633 7634 var handler = function (e) { 7635 return fn.call(context || obj, e || window.event); 7636 }; 7637 7638 var originalHandler = handler; 7639 7640 if (!Browser.touchNative && Browser.pointer && type.indexOf('touch') === 0) { 7641 // Needs DomEvent.Pointer.js 7642 handler = addPointerListener(obj, type, handler); 7643 7644 } else if (Browser.touch && (type === 'dblclick')) { 7645 handler = addDoubleTapListener(obj, handler); 7646 7647 } else if ('addEventListener' in obj) { 7648 7649 if (type === 'touchstart' || type === 'touchmove' || type === 'wheel' || type === 'mousewheel') { 7650 obj.addEventListener(mouseSubst[type] || type, handler, Browser.passiveEvents ? {passive: false} : false); 7651 7652 } else if (type === 'mouseenter' || type === 'mouseleave') { 7653 handler = function (e) { 7654 e = e || window.event; 7655 if (isExternalTarget(obj, e)) { 7656 originalHandler(e); 7657 } 7658 }; 7659 obj.addEventListener(mouseSubst[type], handler, false); 7660 7661 } else { 7662 obj.addEventListener(type, originalHandler, false); 7663 } 7664 7665 } else { 7666 obj.attachEvent('on' + type, handler); 7667 } 7668 7669 obj[eventsKey] = obj[eventsKey] || {}; 7670 obj[eventsKey][id] = handler; 7671 } 7672 7673 function removeOne(obj, type, fn, context, id) { 7674 id = id || type + stamp(fn) + (context ? '_' + stamp(context) : ''); 7675 var handler = obj[eventsKey] && obj[eventsKey][id]; 7676 7677 if (!handler) { return this; } 7678 7679 if (!Browser.touchNative && Browser.pointer && type.indexOf('touch') === 0) { 7680 removePointerListener(obj, type, handler); 7681 7682 } else if (Browser.touch && (type === 'dblclick')) { 7683 removeDoubleTapListener(obj, handler); 7684 7685 } else if ('removeEventListener' in obj) { 7686 7687 obj.removeEventListener(mouseSubst[type] || type, handler, false); 7688 7689 } else { 7690 obj.detachEvent('on' + type, handler); 7691 } 7692 7693 obj[eventsKey][id] = null; 7694 } 7695 7696 // @function stopPropagation(ev: DOMEvent): this 7697 // Stop the given event from propagation to parent elements. Used inside the listener functions: 7698 // ```js 7699 // L.DomEvent.on(div, 'click', function (ev) { 7700 // L.DomEvent.stopPropagation(ev); 7701 // }); 7702 // ``` 7703 function stopPropagation(e) { 7704 7705 if (e.stopPropagation) { 7706 e.stopPropagation(); 7707 } else if (e.originalEvent) { // In case of Leaflet event. 7708 e.originalEvent._stopped = true; 7709 } else { 7710 e.cancelBubble = true; 7711 } 7712 7713 return this; 7714 } 7715 7716 // @function disableScrollPropagation(el: HTMLElement): this 7717 // Adds `stopPropagation` to the element's `'wheel'` events (plus browser variants). 7718 function disableScrollPropagation(el) { 7719 addOne(el, 'wheel', stopPropagation); 7720 return this; 7721 } 7722 7723 // @function disableClickPropagation(el: HTMLElement): this 7724 // Adds `stopPropagation` to the element's `'click'`, `'dblclick'`, `'contextmenu'`, 7725 // `'mousedown'` and `'touchstart'` events (plus browser variants). 7726 function disableClickPropagation(el) { 7727 on(el, 'mousedown touchstart dblclick contextmenu', stopPropagation); 7728 el['_leaflet_disable_click'] = true; 7729 return this; 7730 } 7731 7732 // @function preventDefault(ev: DOMEvent): this 7733 // Prevents the default action of the DOM Event `ev` from happening (such as 7734 // following a link in the href of the a element, or doing a POST request 7735 // with page reload when a `<form>` is submitted). 7736 // Use it inside listener functions. 7737 function preventDefault(e) { 7738 if (e.preventDefault) { 7739 e.preventDefault(); 7740 } else { 7741 e.returnValue = false; 7742 } 7743 return this; 7744 } 7745 7746 // @function stop(ev: DOMEvent): this 7747 // Does `stopPropagation` and `preventDefault` at the same time. 7748 function stop(e) { 7749 preventDefault(e); 7750 stopPropagation(e); 7751 return this; 7752 } 7753 7754 // @function getPropagationPath(ev: DOMEvent): Array 7755 // Compatibility polyfill for [`Event.composedPath()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath). 7756 // Returns an array containing the `HTMLElement`s that the given DOM event 7757 // should propagate to (if not stopped). 7758 function getPropagationPath(ev) { 7759 if (ev.composedPath) { 7760 return ev.composedPath(); 7761 } 7762 7763 var path = []; 7764 var el = ev.target; 7765 7766 while (el) { 7767 path.push(el); 7768 el = el.parentNode; 7769 } 7770 return path; 7771 } 7772 7773 7774 // @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point 7775 // Gets normalized mouse position from a DOM event relative to the 7776 // `container` (border excluded) or to the whole page if not specified. 7777 function getMousePosition(e, container) { 7778 if (!container) { 7779 return new Point(e.clientX, e.clientY); 7780 } 7781 7782 var scale = getScale(container), 7783 offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y) 7784 7785 return new Point( 7786 // offset.left/top values are in page scale (like clientX/Y), 7787 // whereas clientLeft/Top (border width) values are the original values (before CSS scale applies). 7788 (e.clientX - offset.left) / scale.x - container.clientLeft, 7789 (e.clientY - offset.top) / scale.y - container.clientTop 7790 ); 7791 } 7792 7793 7794 // except , Safari and 7795 // We need double the scroll pixels (see #7403 and #4538) for all Browsers 7796 // except OSX (Mac) -> 3x, Chrome running on Linux 1x 7797 7798 var wheelPxFactor = 7799 (Browser.linux && Browser.chrome) ? window.devicePixelRatio : 7800 Browser.mac ? window.devicePixelRatio * 3 : 7801 window.devicePixelRatio > 0 ? 2 * window.devicePixelRatio : 1; 7802 // @function getWheelDelta(ev: DOMEvent): Number 7803 // Gets normalized wheel delta from a wheel DOM event, in vertical 7804 // pixels scrolled (negative if scrolling down). 7805 // Events from pointing devices without precise scrolling are mapped to 7806 // a best guess of 60 pixels. 7807 function getWheelDelta(e) { 7808 return (Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta 7809 (e.deltaY && e.deltaMode === 0) ? -e.deltaY / wheelPxFactor : // Pixels 7810 (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines 7811 (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages 7812 (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events 7813 e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels 7814 (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines 7815 e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages 7816 0; 7817 } 7818 7819 // check if element really left/entered the event target (for mouseenter/mouseleave) 7820 function isExternalTarget(el, e) { 7821 7822 var related = e.relatedTarget; 7823 7824 if (!related) { return true; } 7825 7826 try { 7827 while (related && (related !== el)) { 7828 related = related.parentNode; 7829 } 7830 } catch (err) { 7831 return false; 7832 } 7833 return (related !== el); 7834 } 7835 7836 var DomEvent = { 7837 __proto__: null, 7838 on: on, 7839 off: off, 7840 stopPropagation: stopPropagation, 7841 disableScrollPropagation: disableScrollPropagation, 7842 disableClickPropagation: disableClickPropagation, 7843 preventDefault: preventDefault, 7844 stop: stop, 7845 getPropagationPath: getPropagationPath, 7846 getMousePosition: getMousePosition, 7847 getWheelDelta: getWheelDelta, 7848 isExternalTarget: isExternalTarget, 7849 addListener: on, 7850 removeListener: off 7851 }; 7852 7853 /* 7854 * @class PosAnimation 7855 * @aka L.PosAnimation 7856 * @inherits Evented 7857 * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9. 7858 * 7859 * @example 7860 * ```js 7861 * var myPositionMarker = L.marker([48.864716, 2.294694]).addTo(map); 7862 * 7863 * myPositionMarker.on("click", function() { 7864 * var pos = map.latLngToLayerPoint(myPositionMarker.getLatLng()); 7865 * pos.y -= 25; 7866 * var fx = new L.PosAnimation(); 7867 * 7868 * fx.once('end',function() { 7869 * pos.y += 25; 7870 * fx.run(myPositionMarker._icon, pos, 0.8); 7871 * }); 7872 * 7873 * fx.run(myPositionMarker._icon, pos, 0.3); 7874 * }); 7875 * 7876 * ``` 7877 * 7878 * @constructor L.PosAnimation() 7879 * Creates a `PosAnimation` object. 7880 * 7881 */ 7882 7883 var PosAnimation = Evented.extend({ 7884 7885 // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number) 7886 // Run an animation of a given element to a new position, optionally setting 7887 // duration in seconds (`0.25` by default) and easing linearity factor (3rd 7888 // argument of the [cubic bezier curve](https://cubic-bezier.com/#0,0,.5,1), 7889 // `0.5` by default). 7890 run: function (el, newPos, duration, easeLinearity) { 7891 this.stop(); 7892 7893 this._el = el; 7894 this._inProgress = true; 7895 this._duration = duration || 0.25; 7896 this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2); 7897 7898 this._startPos = getPosition(el); 7899 this._offset = newPos.subtract(this._startPos); 7900 this._startTime = +new Date(); 7901 7902 // @event start: Event 7903 // Fired when the animation starts 7904 this.fire('start'); 7905 7906 this._animate(); 7907 }, 7908 7909 // @method stop() 7910 // Stops the animation (if currently running). 7911 stop: function () { 7912 if (!this._inProgress) { return; } 7913 7914 this._step(true); 7915 this._complete(); 7916 }, 7917 7918 _animate: function () { 7919 // animation loop 7920 this._animId = requestAnimFrame(this._animate, this); 7921 this._step(); 7922 }, 7923 7924 _step: function (round) { 7925 var elapsed = (+new Date()) - this._startTime, 7926 duration = this._duration * 1000; 7927 7928 if (elapsed < duration) { 7929 this._runFrame(this._easeOut(elapsed / duration), round); 7930 } else { 7931 this._runFrame(1); 7932 this._complete(); 7933 } 7934 }, 7935 7936 _runFrame: function (progress, round) { 7937 var pos = this._startPos.add(this._offset.multiplyBy(progress)); 7938 if (round) { 7939 pos._round(); 7940 } 7941 setPosition(this._el, pos); 7942 7943 // @event step: Event 7944 // Fired continuously during the animation. 7945 this.fire('step'); 7946 }, 7947 7948 _complete: function () { 7949 cancelAnimFrame(this._animId); 7950 7951 this._inProgress = false; 7952 // @event end: Event 7953 // Fired when the animation ends. 7954 this.fire('end'); 7955 }, 7956 7957 _easeOut: function (t) { 7958 return 1 - Math.pow(1 - t, this._easeOutPower); 7959 } 7960 }); 7961 7962 /* 7963 * @class Map 7964 * @aka L.Map 7965 * @inherits Evented 7966 * 7967 * The central class of the API â it is used to create a map on a page and manipulate it. 7968 * 7969 * @example 7970 * 7971 * ```js 7972 * // initialize the map on the "map" div with a given center and zoom 7973 * var map = L.map('map', { 7974 * center: [51.505, -0.09], 7975 * zoom: 13 7976 * }); 7977 * ``` 7978 * 7979 */ 7980 7981 var Map = Evented.extend({ 7982 7983 options: { 7984 // @section Map State Options 7985 // @option crs: CRS = L.CRS.EPSG3857 7986 // The [Coordinate Reference System](#crs) to use. Don't change this if you're not 7987 // sure what it means. 7988 crs: EPSG3857, 7989 7990 // @option center: LatLng = undefined 7991 // Initial geographic center of the map 7992 center: undefined, 7993 7994 // @option zoom: Number = undefined 7995 // Initial map zoom level 7996 zoom: undefined, 7997 7998 // @option minZoom: Number = * 7999 // Minimum zoom level of the map. 8000 // If not specified and at least one `GridLayer` or `TileLayer` is in the map, 8001 // the lowest of their `minZoom` options will be used instead. 8002 minZoom: undefined, 8003 8004 // @option maxZoom: Number = * 8005 // Maximum zoom level of the map. 8006 // If not specified and at least one `GridLayer` or `TileLayer` is in the map, 8007 // the highest of their `maxZoom` options will be used instead. 8008 maxZoom: undefined, 8009 8010 // @option layers: Layer[] = [] 8011 // Array of layers that will be added to the map initially 8012 layers: [], 8013 8014 // @option maxBounds: LatLngBounds = null 8015 // When this option is set, the map restricts the view to the given 8016 // geographical bounds, bouncing the user back if the user tries to pan 8017 // outside the view. To set the restriction dynamically, use 8018 // [`setMaxBounds`](#map-setmaxbounds) method. 8019 maxBounds: undefined, 8020 8021 // @option renderer: Renderer = * 8022 // The default method for drawing vector layers on the map. `L.SVG` 8023 // or `L.Canvas` by default depending on browser support. 8024 renderer: undefined, 8025 8026 8027 // @section Animation Options 8028 // @option zoomAnimation: Boolean = true 8029 // Whether the map zoom animation is enabled. By default it's enabled 8030 // in all browsers that support CSS3 Transitions except Android. 8031 zoomAnimation: true, 8032 8033 // @option zoomAnimationThreshold: Number = 4 8034 // Won't animate zoom if the zoom difference exceeds this value. 8035 zoomAnimationThreshold: 4, 8036 8037 // @option fadeAnimation: Boolean = true 8038 // Whether the tile fade animation is enabled. By default it's enabled 8039 // in all browsers that support CSS3 Transitions except Android. 8040 fadeAnimation: true, 8041 8042 // @option markerZoomAnimation: Boolean = true 8043 // Whether markers animate their zoom with the zoom animation, if disabled 8044 // they will disappear for the length of the animation. By default it's 8045 // enabled in all browsers that support CSS3 Transitions except Android. 8046 markerZoomAnimation: true, 8047 8048 // @option transform3DLimit: Number = 2^23 8049 // Defines the maximum size of a CSS translation transform. The default 8050 // value should not be changed unless a web browser positions layers in 8051 // the wrong place after doing a large `panBy`. 8052 transform3DLimit: 8388608, // Precision limit of a 32-bit float 8053 8054 // @section Interaction Options 8055 // @option zoomSnap: Number = 1 8056 // Forces the map's zoom level to always be a multiple of this, particularly 8057 // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom. 8058 // By default, the zoom level snaps to the nearest integer; lower values 8059 // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0` 8060 // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom. 8061 zoomSnap: 1, 8062 8063 // @option zoomDelta: Number = 1 8064 // Controls how much the map's zoom level will change after a 8065 // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+` 8066 // or `-` on the keyboard, or using the [zoom controls](#control-zoom). 8067 // Values smaller than `1` (e.g. `0.5`) allow for greater granularity. 8068 zoomDelta: 1, 8069 8070 // @option trackResize: Boolean = true 8071 // Whether the map automatically handles browser window resize to update itself. 8072 trackResize: true 8073 }, 8074 8075 initialize: function (id, options) { // (HTMLElement or String, Object) 8076 options = setOptions(this, options); 8077 8078 // Make sure to assign internal flags at the beginning, 8079 // to avoid inconsistent state in some edge cases. 8080 this._handlers = []; 8081 this._layers = {}; 8082 this._zoomBoundLayers = {}; 8083 this._sizeChanged = true; 8084 8085 this._initContainer(id); 8086 this._initLayout(); 8087 8088 // hack for https://github.com/Leaflet/Leaflet/issues/1980 8089 this._onResize = bind(this._onResize, this); 8090 8091 this._initEvents(); 8092 8093 if (options.maxBounds) { 8094 this.setMaxBounds(options.maxBounds); 8095 } 8096 8097 if (options.zoom !== undefined) { 8098 this._zoom = this._limitZoom(options.zoom); 8099 } 8100 8101 if (options.center && options.zoom !== undefined) { 8102 this.setView(toLatLng(options.center), options.zoom, {reset: true}); 8103 } 8104 8105 this.callInitHooks(); 8106 8107 // don't animate on browsers without hardware-accelerated transitions or old Android/Opera 8108 this._zoomAnimated = TRANSITION && Browser.any3d && !Browser.mobileOpera && 8109 this.options.zoomAnimation; 8110 8111 // zoom transitions run with the same duration for all layers, so if one of transitionend events 8112 // happens after starting zoom animation (propagating to the map pane), we know that it ended globally 8113 if (this._zoomAnimated) { 8114 this._createAnimProxy(); 8115 on(this._proxy, TRANSITION_END, this._catchTransitionEnd, this); 8116 } 8117 8118 this._addLayers(this.options.layers); 8119 }, 8120 8121 8122 // @section Methods for modifying map state 8123 8124 // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this 8125 // Sets the view of the map (geographical center and zoom) with the given 8126 // animation options. 8127 setView: function (center, zoom, options) { 8128 8129 zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); 8130 center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds); 8131 options = options || {}; 8132 8133 this._stop(); 8134 8135 if (this._loaded && !options.reset && options !== true) { 8136 8137 if (options.animate !== undefined) { 8138 options.zoom = extend({animate: options.animate}, options.zoom); 8139 options.pan = extend({animate: options.animate, duration: options.duration}, options.pan); 8140 } 8141 8142 // try animating pan or zoom 8143 var moved = (this._zoom !== zoom) ? 8144 this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : 8145 this._tryAnimatedPan(center, options.pan); 8146 8147 if (moved) { 8148 // prevent resize handler call, the view will refresh after animation anyway 8149 clearTimeout(this._sizeTimer); 8150 return this; 8151 } 8152 } 8153 8154 // animation didn't start, just reset the map view 8155 this._resetView(center, zoom, options.pan && options.pan.noMoveStart); 8156 8157 return this; 8158 }, 8159 8160 // @method setZoom(zoom: Number, options?: Zoom/pan options): this 8161 // Sets the zoom of the map. 8162 setZoom: function (zoom, options) { 8163 if (!this._loaded) { 8164 this._zoom = zoom; 8165 return this; 8166 } 8167 return this.setView(this.getCenter(), zoom, {zoom: options}); 8168 }, 8169 8170 // @method zoomIn(delta?: Number, options?: Zoom options): this 8171 // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). 8172 zoomIn: function (delta, options) { 8173 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1); 8174 return this.setZoom(this._zoom + delta, options); 8175 }, 8176 8177 // @method zoomOut(delta?: Number, options?: Zoom options): this 8178 // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). 8179 zoomOut: function (delta, options) { 8180 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1); 8181 return this.setZoom(this._zoom - delta, options); 8182 }, 8183 8184 // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this 8185 // Zooms the map while keeping a specified geographical point on the map 8186 // stationary (e.g. used internally for scroll zoom and double-click zoom). 8187 // @alternative 8188 // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this 8189 // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary. 8190 setZoomAround: function (latlng, zoom, options) { 8191 var scale = this.getZoomScale(zoom), 8192 viewHalf = this.getSize().divideBy(2), 8193 containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng), 8194 8195 centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), 8196 newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); 8197 8198 return this.setView(newCenter, zoom, {zoom: options}); 8199 }, 8200 8201 _getBoundsCenterZoom: function (bounds, options) { 8202 8203 options = options || {}; 8204 bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds); 8205 8206 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]), 8207 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]), 8208 8209 zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); 8210 8211 zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom; 8212 8213 if (zoom === Infinity) { 8214 return { 8215 center: bounds.getCenter(), 8216 zoom: zoom 8217 }; 8218 } 8219 8220 var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), 8221 8222 swPoint = this.project(bounds.getSouthWest(), zoom), 8223 nePoint = this.project(bounds.getNorthEast(), zoom), 8224 center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); 8225 8226 return { 8227 center: center, 8228 zoom: zoom 8229 }; 8230 }, 8231 8232 // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this 8233 // Sets a map view that contains the given geographical bounds with the 8234 // maximum zoom level possible. 8235 fitBounds: function (bounds, options) { 8236 8237 bounds = toLatLngBounds(bounds); 8238 8239 if (!bounds.isValid()) { 8240 throw new Error('Bounds are not valid.'); 8241 } 8242 8243 var target = this._getBoundsCenterZoom(bounds, options); 8244 return this.setView(target.center, target.zoom, options); 8245 }, 8246 8247 // @method fitWorld(options?: fitBounds options): this 8248 // Sets a map view that mostly contains the whole world with the maximum 8249 // zoom level possible. 8250 fitWorld: function (options) { 8251 return this.fitBounds([[-90, -180], [90, 180]], options); 8252 }, 8253 8254 // @method panTo(latlng: LatLng, options?: Pan options): this 8255 // Pans the map to a given center. 8256 panTo: function (center, options) { // (LatLng) 8257 return this.setView(center, this._zoom, {pan: options}); 8258 }, 8259 8260 // @method panBy(offset: Point, options?: Pan options): this 8261 // Pans the map by a given number of pixels (animated). 8262 panBy: function (offset, options) { 8263 offset = toPoint(offset).round(); 8264 options = options || {}; 8265 8266 if (!offset.x && !offset.y) { 8267 return this.fire('moveend'); 8268 } 8269 // If we pan too far, Chrome gets issues with tiles 8270 // and makes them disappear or appear in the wrong place (slightly offset) #2602 8271 if (options.animate !== true && !this.getSize().contains(offset)) { 8272 this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); 8273 return this; 8274 } 8275 8276 if (!this._panAnim) { 8277 this._panAnim = new PosAnimation(); 8278 8279 this._panAnim.on({ 8280 'step': this._onPanTransitionStep, 8281 'end': this._onPanTransitionEnd 8282 }, this); 8283 } 8284 8285 // don't fire movestart if animating inertia 8286 if (!options.noMoveStart) { 8287 this.fire('movestart'); 8288 } 8289 8290 // animate pan unless animate: false specified 8291 if (options.animate !== false) { 8292 addClass(this._mapPane, 'leaflet-pan-anim'); 8293 8294 var newPos = this._getMapPanePos().subtract(offset).round(); 8295 this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); 8296 } else { 8297 this._rawPanBy(offset); 8298 this.fire('move').fire('moveend'); 8299 } 8300 8301 return this; 8302 }, 8303 8304 // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this 8305 // Sets the view of the map (geographical center and zoom) performing a smooth 8306 // pan-zoom animation. 8307 flyTo: function (targetCenter, targetZoom, options) { 8308 8309 options = options || {}; 8310 if (options.animate === false || !Browser.any3d) { 8311 return this.setView(targetCenter, targetZoom, options); 8312 } 8313 8314 this._stop(); 8315 8316 var from = this.project(this.getCenter()), 8317 to = this.project(targetCenter), 8318 size = this.getSize(), 8319 startZoom = this._zoom; 8320 8321 targetCenter = toLatLng(targetCenter); 8322 targetZoom = targetZoom === undefined ? startZoom : targetZoom; 8323 8324 var w0 = Math.max(size.x, size.y), 8325 w1 = w0 * this.getZoomScale(startZoom, targetZoom), 8326 u1 = (to.distanceTo(from)) || 1, 8327 rho = 1.42, 8328 rho2 = rho * rho; 8329 8330 function r(i) { 8331 var s1 = i ? -1 : 1, 8332 s2 = i ? w1 : w0, 8333 t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1, 8334 b1 = 2 * s2 * rho2 * u1, 8335 b = t1 / b1, 8336 sq = Math.sqrt(b * b + 1) - b; 8337 8338 // workaround for floating point precision bug when sq = 0, log = -Infinite, 8339 // thus triggering an infinite loop in flyTo 8340 var log = sq < 0.000000001 ? -18 : Math.log(sq); 8341 8342 return log; 8343 } 8344 8345 function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } 8346 function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } 8347 function tanh(n) { return sinh(n) / cosh(n); } 8348 8349 var r0 = r(0); 8350 8351 function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); } 8352 function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; } 8353 8354 function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); } 8355 8356 var start = Date.now(), 8357 S = (r(1) - r0) / rho, 8358 duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8; 8359 8360 function frame() { 8361 var t = (Date.now() - start) / duration, 8362 s = easeOut(t) * S; 8363 8364 if (t <= 1) { 8365 this._flyToFrame = requestAnimFrame(frame, this); 8366 8367 this._move( 8368 this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), 8369 this.getScaleZoom(w0 / w(s), startZoom), 8370 {flyTo: true}); 8371 8372 } else { 8373 this 8374 ._move(targetCenter, targetZoom) 8375 ._moveEnd(true); 8376 } 8377 } 8378 8379 this._moveStart(true, options.noMoveStart); 8380 8381 frame.call(this); 8382 return this; 8383 }, 8384 8385 // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this 8386 // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto), 8387 // but takes a bounds parameter like [`fitBounds`](#map-fitbounds). 8388 flyToBounds: function (bounds, options) { 8389 var target = this._getBoundsCenterZoom(bounds, options); 8390 return this.flyTo(target.center, target.zoom, options); 8391 }, 8392 8393 // @method setMaxBounds(bounds: LatLngBounds): this 8394 // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option). 8395 setMaxBounds: function (bounds) { 8396 bounds = toLatLngBounds(bounds); 8397 8398 if (this.listens('moveend', this._panInsideMaxBounds)) { 8399 this.off('moveend', this._panInsideMaxBounds); 8400 } 8401 8402 if (!bounds.isValid()) { 8403 this.options.maxBounds = null; 8404 return this; 8405 } 8406 8407 this.options.maxBounds = bounds; 8408 8409 if (this._loaded) { 8410 this._panInsideMaxBounds(); 8411 } 8412 8413 return this.on('moveend', this._panInsideMaxBounds); 8414 }, 8415 8416 // @method setMinZoom(zoom: Number): this 8417 // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option). 8418 setMinZoom: function (zoom) { 8419 var oldZoom = this.options.minZoom; 8420 this.options.minZoom = zoom; 8421 8422 if (this._loaded && oldZoom !== zoom) { 8423 this.fire('zoomlevelschange'); 8424 8425 if (this.getZoom() < this.options.minZoom) { 8426 return this.setZoom(zoom); 8427 } 8428 } 8429 8430 return this; 8431 }, 8432 8433 // @method setMaxZoom(zoom: Number): this 8434 // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option). 8435 setMaxZoom: function (zoom) { 8436 var oldZoom = this.options.maxZoom; 8437 this.options.maxZoom = zoom; 8438 8439 if (this._loaded && oldZoom !== zoom) { 8440 this.fire('zoomlevelschange'); 8441 8442 if (this.getZoom() > this.options.maxZoom) { 8443 return this.setZoom(zoom); 8444 } 8445 } 8446 8447 return this; 8448 }, 8449 8450 // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this 8451 // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any. 8452 panInsideBounds: function (bounds, options) { 8453 this._enforcingBounds = true; 8454 var center = this.getCenter(), 8455 newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds)); 8456 8457 if (!center.equals(newCenter)) { 8458 this.panTo(newCenter, options); 8459 } 8460 8461 this._enforcingBounds = false; 8462 return this; 8463 }, 8464 8465 // @method panInside(latlng: LatLng, options?: padding options): this 8466 // Pans the map the minimum amount to make the `latlng` visible. Use 8467 // padding options to fit the display to more restricted bounds. 8468 // If `latlng` is already within the (optionally padded) display bounds, 8469 // the map will not be panned. 8470 panInside: function (latlng, options) { 8471 options = options || {}; 8472 8473 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]), 8474 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]), 8475 pixelCenter = this.project(this.getCenter()), 8476 pixelPoint = this.project(latlng), 8477 pixelBounds = this.getPixelBounds(), 8478 paddedBounds = toBounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]), 8479 paddedSize = paddedBounds.getSize(); 8480 8481 if (!paddedBounds.contains(pixelPoint)) { 8482 this._enforcingBounds = true; 8483 var centerOffset = pixelPoint.subtract(paddedBounds.getCenter()); 8484 var offset = paddedBounds.extend(pixelPoint).getSize().subtract(paddedSize); 8485 pixelCenter.x += centerOffset.x < 0 ? -offset.x : offset.x; 8486 pixelCenter.y += centerOffset.y < 0 ? -offset.y : offset.y; 8487 this.panTo(this.unproject(pixelCenter), options); 8488 this._enforcingBounds = false; 8489 } 8490 return this; 8491 }, 8492 8493 // @method invalidateSize(options: Zoom/pan options): this 8494 // Checks if the map container size changed and updates the map if so â 8495 // call it after you've changed the map size dynamically, also animating 8496 // pan by default. If `options.pan` is `false`, panning will not occur. 8497 // If `options.debounceMoveend` is `true`, it will delay `moveend` event so 8498 // that it doesn't happen often even if the method is called many 8499 // times in a row. 8500 8501 // @alternative 8502 // @method invalidateSize(animate: Boolean): this 8503 // Checks if the map container size changed and updates the map if so â 8504 // call it after you've changed the map size dynamically, also animating 8505 // pan by default. 8506 invalidateSize: function (options) { 8507 if (!this._loaded) { return this; } 8508 8509 options = extend({ 8510 animate: false, 8511 pan: true 8512 }, options === true ? {animate: true} : options); 8513 8514 var oldSize = this.getSize(); 8515 this._sizeChanged = true; 8516 this._lastCenter = null; 8517 8518 var newSize = this.getSize(), 8519 oldCenter = oldSize.divideBy(2).round(), 8520 newCenter = newSize.divideBy(2).round(), 8521 offset = oldCenter.subtract(newCenter); 8522 8523 if (!offset.x && !offset.y) { return this; } 8524 8525 if (options.animate && options.pan) { 8526 this.panBy(offset); 8527 8528 } else { 8529 if (options.pan) { 8530 this._rawPanBy(offset); 8531 } 8532 8533 this.fire('move'); 8534 8535 if (options.debounceMoveend) { 8536 clearTimeout(this._sizeTimer); 8537 this._sizeTimer = setTimeout(bind(this.fire, this, 'moveend'), 200); 8538 } else { 8539 this.fire('moveend'); 8540 } 8541 } 8542 8543 // @section Map state change events 8544 // @event resize: ResizeEvent 8545 // Fired when the map is resized. 8546 return this.fire('resize', { 8547 oldSize: oldSize, 8548 newSize: newSize 8549 }); 8550 }, 8551 8552 // @section Methods for modifying map state 8553 // @method stop(): this 8554 // Stops the currently running `panTo` or `flyTo` animation, if any. 8555 stop: function () { 8556 this.setZoom(this._limitZoom(this._zoom)); 8557 if (!this.options.zoomSnap) { 8558 this.fire('viewreset'); 8559 } 8560 return this._stop(); 8561 }, 8562 8563 // @section Geolocation methods 8564 // @method locate(options?: Locate options): this 8565 // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound) 8566 // event with location data on success or a [`locationerror`](#map-locationerror) event on failure, 8567 // and optionally sets the map view to the user's location with respect to 8568 // detection accuracy (or to the world view if geolocation failed). 8569 // Note that, if your page doesn't use HTTPS, this method will fail in 8570 // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins)) 8571 // See `Locate options` for more details. 8572 locate: function (options) { 8573 8574 options = this._locateOptions = extend({ 8575 timeout: 10000, 8576 watch: false 8577 // setView: false 8578 // maxZoom: <Number> 8579 // maximumAge: 0 8580 // enableHighAccuracy: false 8581 }, options); 8582 8583 if (!('geolocation' in navigator)) { 8584 this._handleGeolocationError({ 8585 code: 0, 8586 message: 'Geolocation not supported.' 8587 }); 8588 return this; 8589 } 8590 8591 var onResponse = bind(this._handleGeolocationResponse, this), 8592 onError = bind(this._handleGeolocationError, this); 8593 8594 if (options.watch) { 8595 this._locationWatchId = 8596 navigator.geolocation.watchPosition(onResponse, onError, options); 8597 } else { 8598 navigator.geolocation.getCurrentPosition(onResponse, onError, options); 8599 } 8600 return this; 8601 }, 8602 8603 // @method stopLocate(): this 8604 // Stops watching location previously initiated by `map.locate({watch: true})` 8605 // and aborts resetting the map view if map.locate was called with 8606 // `{setView: true}`. 8607 stopLocate: function () { 8608 if (navigator.geolocation && navigator.geolocation.clearWatch) { 8609 navigator.geolocation.clearWatch(this._locationWatchId); 8610 } 8611 if (this._locateOptions) { 8612 this._locateOptions.setView = false; 8613 } 8614 return this; 8615 }, 8616 8617 _handleGeolocationError: function (error) { 8618 if (!this._container._leaflet_id) { return; } 8619 8620 var c = error.code, 8621 message = error.message || 8622 (c === 1 ? 'permission denied' : 8623 (c === 2 ? 'position unavailable' : 'timeout')); 8624 8625 if (this._locateOptions.setView && !this._loaded) { 8626 this.fitWorld(); 8627 } 8628 8629 // @section Location events 8630 // @event locationerror: ErrorEvent 8631 // Fired when geolocation (using the [`locate`](#map-locate) method) failed. 8632 this.fire('locationerror', { 8633 code: c, 8634 message: 'Geolocation error: ' + message + '.' 8635 }); 8636 }, 8637 8638 _handleGeolocationResponse: function (pos) { 8639 if (!this._container._leaflet_id) { return; } 8640 8641 var lat = pos.coords.latitude, 8642 lng = pos.coords.longitude, 8643 latlng = new LatLng(lat, lng), 8644 bounds = latlng.toBounds(pos.coords.accuracy * 2), 8645 options = this._locateOptions; 8646 8647 if (options.setView) { 8648 var zoom = this.getBoundsZoom(bounds); 8649 this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); 8650 } 8651 8652 var data = { 8653 latlng: latlng, 8654 bounds: bounds, 8655 timestamp: pos.timestamp 8656 }; 8657 8658 for (var i in pos.coords) { 8659 if (typeof pos.coords[i] === 'number') { 8660 data[i] = pos.coords[i]; 8661 } 8662 } 8663 8664 // @event locationfound: LocationEvent 8665 // Fired when geolocation (using the [`locate`](#map-locate) method) 8666 // went successfully. 8667 this.fire('locationfound', data); 8668 }, 8669 8670 // TODO Appropriate docs section? 8671 // @section Other Methods 8672 // @method addHandler(name: String, HandlerClass: Function): this 8673 // Adds a new `Handler` to the map, given its name and constructor function. 8674 addHandler: function (name, HandlerClass) { 8675 if (!HandlerClass) { return this; } 8676 8677 var handler = this[name] = new HandlerClass(this); 8678 8679 this._handlers.push(handler); 8680 8681 if (this.options[name]) { 8682 handler.enable(); 8683 } 8684 8685 return this; 8686 }, 8687 8688 // @method remove(): this 8689 // Destroys the map and clears all related event listeners. 8690 remove: function () { 8691 8692 this._initEvents(true); 8693 if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); } 8694 8695 if (this._containerId !== this._container._leaflet_id) { 8696 throw new Error('Map container is being reused by another instance'); 8697 } 8698 8699 try { 8700 // throws error in IE6-8 8701 delete this._container._leaflet_id; 8702 delete this._containerId; 8703 } catch (e) { 8704 /*eslint-disable */ 8705 this._container._leaflet_id = undefined; 8706 /* eslint-enable */ 8707 this._containerId = undefined; 8708 } 8709 8710 if (this._locationWatchId !== undefined) { 8711 this.stopLocate(); 8712 } 8713 8714 this._stop(); 8715 8716 remove(this._mapPane); 8717 8718 if (this._clearControlPos) { 8719 this._clearControlPos(); 8720 } 8721 if (this._resizeRequest) { 8722 cancelAnimFrame(this._resizeRequest); 8723 this._resizeRequest = null; 8724 } 8725 8726 this._clearHandlers(); 8727 8728 if (this._loaded) { 8729 // @section Map state change events 8730 // @event unload: Event 8731 // Fired when the map is destroyed with [remove](#map-remove) method. 8732 this.fire('unload'); 8733 } 8734 8735 var i; 8736 for (i in this._layers) { 8737 this._layers[i].remove(); 8738 } 8739 for (i in this._panes) { 8740 remove(this._panes[i]); 8741 } 8742 8743 this._layers = []; 8744 this._panes = []; 8745 delete this._mapPane; 8746 delete this._renderer; 8747 8748 return this; 8749 }, 8750 8751 // @section Other Methods 8752 // @method createPane(name: String, container?: HTMLElement): HTMLElement 8753 // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already, 8754 // then returns it. The pane is created as a child of `container`, or 8755 // as a child of the main map pane if not set. 8756 createPane: function (name, container) { 8757 var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), 8758 pane = create$1('div', className, container || this._mapPane); 8759 8760 if (name) { 8761 this._panes[name] = pane; 8762 } 8763 return pane; 8764 }, 8765 8766 // @section Methods for Getting Map State 8767 8768 // @method getCenter(): LatLng 8769 // Returns the geographical center of the map view 8770 getCenter: function () { 8771 this._checkIfLoaded(); 8772 8773 if (this._lastCenter && !this._moved()) { 8774 return this._lastCenter.clone(); 8775 } 8776 return this.layerPointToLatLng(this._getCenterLayerPoint()); 8777 }, 8778 8779 // @method getZoom(): Number 8780 // Returns the current zoom level of the map view 8781 getZoom: function () { 8782 return this._zoom; 8783 }, 8784 8785 // @method getBounds(): LatLngBounds 8786 // Returns the geographical bounds visible in the current map view 8787 getBounds: function () { 8788 var bounds = this.getPixelBounds(), 8789 sw = this.unproject(bounds.getBottomLeft()), 8790 ne = this.unproject(bounds.getTopRight()); 8791 8792 return new LatLngBounds(sw, ne); 8793 }, 8794 8795 // @method getMinZoom(): Number 8796 // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default. 8797 getMinZoom: function () { 8798 return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; 8799 }, 8800 8801 // @method getMaxZoom(): Number 8802 // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers). 8803 getMaxZoom: function () { 8804 return this.options.maxZoom === undefined ? 8805 (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : 8806 this.options.maxZoom; 8807 }, 8808 8809 // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean, padding?: Point): Number 8810 // Returns the maximum zoom level on which the given bounds fit to the map 8811 // view in its entirety. If `inside` (optional) is set to `true`, the method 8812 // instead returns the minimum zoom level on which the map view fits into 8813 // the given bounds in its entirety. 8814 getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number 8815 bounds = toLatLngBounds(bounds); 8816 padding = toPoint(padding || [0, 0]); 8817 8818 var zoom = this.getZoom() || 0, 8819 min = this.getMinZoom(), 8820 max = this.getMaxZoom(), 8821 nw = bounds.getNorthWest(), 8822 se = bounds.getSouthEast(), 8823 size = this.getSize().subtract(padding), 8824 boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(), 8825 snap = Browser.any3d ? this.options.zoomSnap : 1, 8826 scalex = size.x / boundsSize.x, 8827 scaley = size.y / boundsSize.y, 8828 scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley); 8829 8830 zoom = this.getScaleZoom(scale, zoom); 8831 8832 if (snap) { 8833 zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level 8834 zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; 8835 } 8836 8837 return Math.max(min, Math.min(max, zoom)); 8838 }, 8839 8840 // @method getSize(): Point 8841 // Returns the current size of the map container (in pixels). 8842 getSize: function () { 8843 if (!this._size || this._sizeChanged) { 8844 this._size = new Point( 8845 this._container.clientWidth || 0, 8846 this._container.clientHeight || 0); 8847 8848 this._sizeChanged = false; 8849 } 8850 return this._size.clone(); 8851 }, 8852 8853 // @method getPixelBounds(): Bounds 8854 // Returns the bounds of the current map view in projected pixel 8855 // coordinates (sometimes useful in layer and overlay implementations). 8856 getPixelBounds: function (center, zoom) { 8857 var topLeftPoint = this._getTopLeftPoint(center, zoom); 8858 return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); 8859 }, 8860 8861 // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to 8862 // the map pane? "left point of the map layer" can be confusing, specially 8863 // since there can be negative offsets. 8864 // @method getPixelOrigin(): Point 8865 // Returns the projected pixel coordinates of the top left point of 8866 // the map layer (useful in custom layer and overlay implementations). 8867 getPixelOrigin: function () { 8868 this._checkIfLoaded(); 8869 return this._pixelOrigin; 8870 }, 8871 8872 // @method getPixelWorldBounds(zoom?: Number): Bounds 8873 // Returns the world's bounds in pixel coordinates for zoom level `zoom`. 8874 // If `zoom` is omitted, the map's current zoom level is used. 8875 getPixelWorldBounds: function (zoom) { 8876 return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom); 8877 }, 8878 8879 // @section Other Methods 8880 8881 // @method getPane(pane: String|HTMLElement): HTMLElement 8882 // Returns a [map pane](#map-pane), given its name or its HTML element (its identity). 8883 getPane: function (pane) { 8884 return typeof pane === 'string' ? this._panes[pane] : pane; 8885 }, 8886 8887 // @method getPanes(): Object 8888 // Returns a plain object containing the names of all [panes](#map-pane) as keys and 8889 // the panes as values. 8890 getPanes: function () { 8891 return this._panes; 8892 }, 8893 8894 // @method getContainer: HTMLElement 8895 // Returns the HTML element that contains the map. 8896 getContainer: function () { 8897 return this._container; 8898 }, 8899 8900 8901 // @section Conversion Methods 8902 8903 // @method getZoomScale(toZoom: Number, fromZoom: Number): Number 8904 // Returns the scale factor to be applied to a map transition from zoom level 8905 // `fromZoom` to `toZoom`. Used internally to help with zoom animations. 8906 getZoomScale: function (toZoom, fromZoom) { 8907 // TODO replace with universal implementation after refactoring projections 8908 var crs = this.options.crs; 8909 fromZoom = fromZoom === undefined ? this._zoom : fromZoom; 8910 return crs.scale(toZoom) / crs.scale(fromZoom); 8911 }, 8912 8913 // @method getScaleZoom(scale: Number, fromZoom: Number): Number 8914 // Returns the zoom level that the map would end up at, if it is at `fromZoom` 8915 // level and everything is scaled by a factor of `scale`. Inverse of 8916 // [`getZoomScale`](#map-getZoomScale). 8917 getScaleZoom: function (scale, fromZoom) { 8918 var crs = this.options.crs; 8919 fromZoom = fromZoom === undefined ? this._zoom : fromZoom; 8920 var zoom = crs.zoom(scale * crs.scale(fromZoom)); 8921 return isNaN(zoom) ? Infinity : zoom; 8922 }, 8923 8924 // @method project(latlng: LatLng, zoom: Number): Point 8925 // Projects a geographical coordinate `LatLng` according to the projection 8926 // of the map's CRS, then scales it according to `zoom` and the CRS's 8927 // `Transformation`. The result is pixel coordinate relative to 8928 // the CRS origin. 8929 project: function (latlng, zoom) { 8930 zoom = zoom === undefined ? this._zoom : zoom; 8931 return this.options.crs.latLngToPoint(toLatLng(latlng), zoom); 8932 }, 8933 8934 // @method unproject(point: Point, zoom: Number): LatLng 8935 // Inverse of [`project`](#map-project). 8936 unproject: function (point, zoom) { 8937 zoom = zoom === undefined ? this._zoom : zoom; 8938 return this.options.crs.pointToLatLng(toPoint(point), zoom); 8939 }, 8940 8941 // @method layerPointToLatLng(point: Point): LatLng 8942 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), 8943 // returns the corresponding geographical coordinate (for the current zoom level). 8944 layerPointToLatLng: function (point) { 8945 var projectedPoint = toPoint(point).add(this.getPixelOrigin()); 8946 return this.unproject(projectedPoint); 8947 }, 8948 8949 // @method latLngToLayerPoint(latlng: LatLng): Point 8950 // Given a geographical coordinate, returns the corresponding pixel coordinate 8951 // relative to the [origin pixel](#map-getpixelorigin). 8952 latLngToLayerPoint: function (latlng) { 8953 var projectedPoint = this.project(toLatLng(latlng))._round(); 8954 return projectedPoint._subtract(this.getPixelOrigin()); 8955 }, 8956 8957 // @method wrapLatLng(latlng: LatLng): LatLng 8958 // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the 8959 // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the 8960 // CRS's bounds. 8961 // By default this means longitude is wrapped around the dateline so its 8962 // value is between -180 and +180 degrees. 8963 wrapLatLng: function (latlng) { 8964 return this.options.crs.wrapLatLng(toLatLng(latlng)); 8965 }, 8966 8967 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds 8968 // Returns a `LatLngBounds` with the same size as the given one, ensuring that 8969 // its center is within the CRS's bounds. 8970 // By default this means the center longitude is wrapped around the dateline so its 8971 // value is between -180 and +180 degrees, and the majority of the bounds 8972 // overlaps the CRS's bounds. 8973 wrapLatLngBounds: function (latlng) { 8974 return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng)); 8975 }, 8976 8977 // @method distance(latlng1: LatLng, latlng2: LatLng): Number 8978 // Returns the distance between two geographical coordinates according to 8979 // the map's CRS. By default this measures distance in meters. 8980 distance: function (latlng1, latlng2) { 8981 return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2)); 8982 }, 8983 8984 // @method containerPointToLayerPoint(point: Point): Point 8985 // Given a pixel coordinate relative to the map container, returns the corresponding 8986 // pixel coordinate relative to the [origin pixel](#map-getpixelorigin). 8987 containerPointToLayerPoint: function (point) { // (Point) 8988 return toPoint(point).subtract(this._getMapPanePos()); 8989 }, 8990 8991 // @method layerPointToContainerPoint(point: Point): Point 8992 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), 8993 // returns the corresponding pixel coordinate relative to the map container. 8994 layerPointToContainerPoint: function (point) { // (Point) 8995 return toPoint(point).add(this._getMapPanePos()); 8996 }, 8997 8998 // @method containerPointToLatLng(point: Point): LatLng 8999 // Given a pixel coordinate relative to the map container, returns 9000 // the corresponding geographical coordinate (for the current zoom level). 9001 containerPointToLatLng: function (point) { 9002 var layerPoint = this.containerPointToLayerPoint(toPoint(point)); 9003 return this.layerPointToLatLng(layerPoint); 9004 }, 9005 9006 // @method latLngToContainerPoint(latlng: LatLng): Point 9007 // Given a geographical coordinate, returns the corresponding pixel coordinate 9008 // relative to the map container. 9009 latLngToContainerPoint: function (latlng) { 9010 return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng))); 9011 }, 9012 9013 // @method mouseEventToContainerPoint(ev: MouseEvent): Point 9014 // Given a MouseEvent object, returns the pixel coordinate relative to the 9015 // map container where the event took place. 9016 mouseEventToContainerPoint: function (e) { 9017 return getMousePosition(e, this._container); 9018 }, 9019 9020 // @method mouseEventToLayerPoint(ev: MouseEvent): Point 9021 // Given a MouseEvent object, returns the pixel coordinate relative to 9022 // the [origin pixel](#map-getpixelorigin) where the event took place. 9023 mouseEventToLayerPoint: function (e) { 9024 return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); 9025 }, 9026 9027 // @method mouseEventToLatLng(ev: MouseEvent): LatLng 9028 // Given a MouseEvent object, returns geographical coordinate where the 9029 // event took place. 9030 mouseEventToLatLng: function (e) { // (MouseEvent) 9031 return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); 9032 }, 9033 9034 9035 // map initialization methods 9036 9037 _initContainer: function (id) { 9038 var container = this._container = get(id); 9039 9040 if (!container) { 9041 throw new Error('Map container not found.'); 9042 } else if (container._leaflet_id) { 9043 throw new Error('Map container is already initialized.'); 9044 } 9045 9046 on(container, 'scroll', this._onScroll, this); 9047 this._containerId = stamp(container); 9048 }, 9049 9050 _initLayout: function () { 9051 var container = this._container; 9052 9053 this._fadeAnimated = this.options.fadeAnimation && Browser.any3d; 9054 9055 addClass(container, 'leaflet-container' + 9056 (Browser.touch ? ' leaflet-touch' : '') + 9057 (Browser.retina ? ' leaflet-retina' : '') + 9058 (Browser.ielt9 ? ' leaflet-oldie' : '') + 9059 (Browser.safari ? ' leaflet-safari' : '') + 9060 (this._fadeAnimated ? ' leaflet-fade-anim' : '')); 9061 9062 var position = getStyle(container, 'position'); 9063 9064 if (position !== 'absolute' && position !== 'relative' && position !== 'fixed' && position !== 'sticky') { 9065 container.style.position = 'relative'; 9066 } 9067 9068 this._initPanes(); 9069 9070 if (this._initControlPos) { 9071 this._initControlPos(); 9072 } 9073 }, 9074 9075 _initPanes: function () { 9076 var panes = this._panes = {}; 9077 this._paneRenderers = {}; 9078 9079 // @section 9080 // 9081 // Panes are DOM elements used to control the ordering of layers on the map. You 9082 // can access panes with [`map.getPane`](#map-getpane) or 9083 // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the 9084 // [`map.createPane`](#map-createpane) method. 9085 // 9086 // Every map has the following default panes that differ only in zIndex. 9087 // 9088 // @pane mapPane: HTMLElement = 'auto' 9089 // Pane that contains all other map panes 9090 9091 this._mapPane = this.createPane('mapPane', this._container); 9092 setPosition(this._mapPane, new Point(0, 0)); 9093 9094 // @pane tilePane: HTMLElement = 200 9095 // Pane for `GridLayer`s and `TileLayer`s 9096 this.createPane('tilePane'); 9097 // @pane overlayPane: HTMLElement = 400 9098 // Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s 9099 this.createPane('overlayPane'); 9100 // @pane shadowPane: HTMLElement = 500 9101 // Pane for overlay shadows (e.g. `Marker` shadows) 9102 this.createPane('shadowPane'); 9103 // @pane markerPane: HTMLElement = 600 9104 // Pane for `Icon`s of `Marker`s 9105 this.createPane('markerPane'); 9106 // @pane tooltipPane: HTMLElement = 650 9107 // Pane for `Tooltip`s. 9108 this.createPane('tooltipPane'); 9109 // @pane popupPane: HTMLElement = 700 9110 // Pane for `Popup`s. 9111 this.createPane('popupPane'); 9112 9113 if (!this.options.markerZoomAnimation) { 9114 addClass(panes.markerPane, 'leaflet-zoom-hide'); 9115 addClass(panes.shadowPane, 'leaflet-zoom-hide'); 9116 } 9117 }, 9118 9119 9120 // private methods that modify map state 9121 9122 // @section Map state change events 9123 _resetView: function (center, zoom, noMoveStart) { 9124 setPosition(this._mapPane, new Point(0, 0)); 9125 9126 var loading = !this._loaded; 9127 this._loaded = true; 9128 zoom = this._limitZoom(zoom); 9129 9130 this.fire('viewprereset'); 9131 9132 var zoomChanged = this._zoom !== zoom; 9133 this 9134 ._moveStart(zoomChanged, noMoveStart) 9135 ._move(center, zoom) 9136 ._moveEnd(zoomChanged); 9137 9138 // @event viewreset: Event 9139 // Fired when the map needs to redraw its content (this usually happens 9140 // on map zoom or load). Very useful for creating custom overlays. 9141 this.fire('viewreset'); 9142 9143 // @event load: Event 9144 // Fired when the map is initialized (when its center and zoom are set 9145 // for the first time). 9146 if (loading) { 9147 this.fire('load'); 9148 } 9149 }, 9150 9151 _moveStart: function (zoomChanged, noMoveStart) { 9152 // @event zoomstart: Event 9153 // Fired when the map zoom is about to change (e.g. before zoom animation). 9154 // @event movestart: Event 9155 // Fired when the view of the map starts changing (e.g. user starts dragging the map). 9156 if (zoomChanged) { 9157 this.fire('zoomstart'); 9158 } 9159 if (!noMoveStart) { 9160 this.fire('movestart'); 9161 } 9162 return this; 9163 }, 9164 9165 _move: function (center, zoom, data, supressEvent) { 9166 if (zoom === undefined) { 9167 zoom = this._zoom; 9168 } 9169 var zoomChanged = this._zoom !== zoom; 9170 9171 this._zoom = zoom; 9172 this._lastCenter = center; 9173 this._pixelOrigin = this._getNewPixelOrigin(center); 9174 9175 if (!supressEvent) { 9176 // @event zoom: Event 9177 // Fired repeatedly during any change in zoom level, 9178 // including zoom and fly animations. 9179 if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530 9180 this.fire('zoom', data); 9181 } 9182 9183 // @event move: Event 9184 // Fired repeatedly during any movement of the map, 9185 // including pan and fly animations. 9186 this.fire('move', data); 9187 } else if (data && data.pinch) { // Always fire 'zoom' if pinching because #3530 9188 this.fire('zoom', data); 9189 } 9190 return this; 9191 }, 9192 9193 _moveEnd: function (zoomChanged) { 9194 // @event zoomend: Event 9195 // Fired when the map zoom changed, after any animations. 9196 if (zoomChanged) { 9197 this.fire('zoomend'); 9198 } 9199 9200 // @event moveend: Event 9201 // Fired when the center of the map stops changing 9202 // (e.g. user stopped dragging the map or after non-centered zoom). 9203 return this.fire('moveend'); 9204 }, 9205 9206 _stop: function () { 9207 cancelAnimFrame(this._flyToFrame); 9208 if (this._panAnim) { 9209 this._panAnim.stop(); 9210 } 9211 return this; 9212 }, 9213 9214 _rawPanBy: function (offset) { 9215 setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); 9216 }, 9217 9218 _getZoomSpan: function () { 9219 return this.getMaxZoom() - this.getMinZoom(); 9220 }, 9221 9222 _panInsideMaxBounds: function () { 9223 if (!this._enforcingBounds) { 9224 this.panInsideBounds(this.options.maxBounds); 9225 } 9226 }, 9227 9228 _checkIfLoaded: function () { 9229 if (!this._loaded) { 9230 throw new Error('Set map center and zoom first.'); 9231 } 9232 }, 9233 9234 // DOM event handling 9235 9236 // @section Interaction events 9237 _initEvents: function (remove) { 9238 this._targets = {}; 9239 this._targets[stamp(this._container)] = this; 9240 9241 var onOff = remove ? off : on; 9242 9243 // @event click: MouseEvent 9244 // Fired when the user clicks (or taps) the map. 9245 // @event dblclick: MouseEvent 9246 // Fired when the user double-clicks (or double-taps) the map. 9247 // @event mousedown: MouseEvent 9248 // Fired when the user pushes the mouse button on the map. 9249 // @event mouseup: MouseEvent 9250 // Fired when the user releases the mouse button on the map. 9251 // @event mouseover: MouseEvent 9252 // Fired when the mouse enters the map. 9253 // @event mouseout: MouseEvent 9254 // Fired when the mouse leaves the map. 9255 // @event mousemove: MouseEvent 9256 // Fired while the mouse moves over the map. 9257 // @event contextmenu: MouseEvent 9258 // Fired when the user pushes the right mouse button on the map, prevents 9259 // default browser context menu from showing if there are listeners on 9260 // this event. Also fired on mobile when the user holds a single touch 9261 // for a second (also called long press). 9262 // @event keypress: KeyboardEvent 9263 // Fired when the user presses a key from the keyboard that produces a character value while the map is focused. 9264 // @event keydown: KeyboardEvent 9265 // Fired when the user presses a key from the keyboard while the map is focused. Unlike the `keypress` event, 9266 // the `keydown` event is fired for keys that produce a character value and for keys 9267 // that do not produce a character value. 9268 // @event keyup: KeyboardEvent 9269 // Fired when the user releases a key from the keyboard while the map is focused. 9270 onOff(this._container, 'click dblclick mousedown mouseup ' + 9271 'mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this); 9272 9273 if (this.options.trackResize) { 9274 onOff(window, 'resize', this._onResize, this); 9275 } 9276 9277 if (Browser.any3d && this.options.transform3DLimit) { 9278 (remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd); 9279 } 9280 }, 9281 9282 _onResize: function () { 9283 cancelAnimFrame(this._resizeRequest); 9284 this._resizeRequest = requestAnimFrame( 9285 function () { this.invalidateSize({debounceMoveend: true}); }, this); 9286 }, 9287 9288 _onScroll: function () { 9289 this._container.scrollTop = 0; 9290 this._container.scrollLeft = 0; 9291 }, 9292 9293 _onMoveEnd: function () { 9294 var pos = this._getMapPanePos(); 9295 if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) { 9296 // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have 9297 // a pixel offset on very high values, see: https://jsfiddle.net/dg6r5hhb/ 9298 this._resetView(this.getCenter(), this.getZoom()); 9299 } 9300 }, 9301 9302 _findEventTargets: function (e, type) { 9303 var targets = [], 9304 target, 9305 isHover = type === 'mouseout' || type === 'mouseover', 9306 src = e.target || e.srcElement, 9307 dragging = false; 9308 9309 while (src) { 9310 target = this._targets[stamp(src)]; 9311 if (target && (type === 'click' || type === 'preclick') && this._draggableMoved(target)) { 9312 // Prevent firing click after you just dragged an object. 9313 dragging = true; 9314 break; 9315 } 9316 if (target && target.listens(type, true)) { 9317 if (isHover && !isExternalTarget(src, e)) { break; } 9318 targets.push(target); 9319 if (isHover) { break; } 9320 } 9321 if (src === this._container) { break; } 9322 src = src.parentNode; 9323 } 9324 if (!targets.length && !dragging && !isHover && this.listens(type, true)) { 9325 targets = [this]; 9326 } 9327 return targets; 9328 }, 9329 9330 _isClickDisabled: function (el) { 9331 while (el && el !== this._container) { 9332 if (el['_leaflet_disable_click']) { return true; } 9333 el = el.parentNode; 9334 } 9335 }, 9336 9337 _handleDOMEvent: function (e) { 9338 var el = (e.target || e.srcElement); 9339 if (!this._loaded || el['_leaflet_disable_events'] || e.type === 'click' && this._isClickDisabled(el)) { 9340 return; 9341 } 9342 9343 var type = e.type; 9344 9345 if (type === 'mousedown') { 9346 // prevents outline when clicking on keyboard-focusable element 9347 preventOutline(el); 9348 } 9349 9350 this._fireDOMEvent(e, type); 9351 }, 9352 9353 _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'], 9354 9355 _fireDOMEvent: function (e, type, canvasTargets) { 9356 9357 if (e.type === 'click') { 9358 // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). 9359 // @event preclick: MouseEvent 9360 // Fired before mouse click on the map (sometimes useful when you 9361 // want something to happen on click before any existing click 9362 // handlers start running). 9363 var synth = extend({}, e); 9364 synth.type = 'preclick'; 9365 this._fireDOMEvent(synth, synth.type, canvasTargets); 9366 } 9367 9368 // Find the layer the event is propagating from and its parents. 9369 var targets = this._findEventTargets(e, type); 9370 9371 if (canvasTargets) { 9372 var filtered = []; // pick only targets with listeners 9373 for (var i = 0; i < canvasTargets.length; i++) { 9374 if (canvasTargets[i].listens(type, true)) { 9375 filtered.push(canvasTargets[i]); 9376 } 9377 } 9378 targets = filtered.concat(targets); 9379 } 9380 9381 if (!targets.length) { return; } 9382 9383 if (type === 'contextmenu') { 9384 preventDefault(e); 9385 } 9386 9387 var target = targets[0]; 9388 var data = { 9389 originalEvent: e 9390 }; 9391 9392 if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') { 9393 var isMarker = target.getLatLng && (!target._radius || target._radius <= 10); 9394 data.containerPoint = isMarker ? 9395 this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); 9396 data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); 9397 data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint); 9398 } 9399 9400 for (i = 0; i < targets.length; i++) { 9401 targets[i].fire(type, data, true); 9402 if (data.originalEvent._stopped || 9403 (targets[i].options.bubblingMouseEvents === false && indexOf(this._mouseEvents, type) !== -1)) { return; } 9404 } 9405 }, 9406 9407 _draggableMoved: function (obj) { 9408 obj = obj.dragging && obj.dragging.enabled() ? obj : this; 9409 return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()); 9410 }, 9411 9412 _clearHandlers: function () { 9413 for (var i = 0, len = this._handlers.length; i < len; i++) { 9414 this._handlers[i].disable(); 9415 } 9416 }, 9417 9418 // @section Other Methods 9419 9420 // @method whenReady(fn: Function, context?: Object): this 9421 // Runs the given function `fn` when the map gets initialized with 9422 // a view (center and zoom) and at least one layer, or immediately 9423 // if it's already initialized, optionally passing a function context. 9424 whenReady: function (callback, context) { 9425 if (this._loaded) { 9426 callback.call(context || this, {target: this}); 9427 } else { 9428 this.on('load', callback, context); 9429 } 9430 return this; 9431 }, 9432 9433 9434 // private methods for getting map state 9435 9436 _getMapPanePos: function () { 9437 return getPosition(this._mapPane) || new Point(0, 0); 9438 }, 9439 9440 _moved: function () { 9441 var pos = this._getMapPanePos(); 9442 return pos && !pos.equals([0, 0]); 9443 }, 9444 9445 _getTopLeftPoint: function (center, zoom) { 9446 var pixelOrigin = center && zoom !== undefined ? 9447 this._getNewPixelOrigin(center, zoom) : 9448 this.getPixelOrigin(); 9449 return pixelOrigin.subtract(this._getMapPanePos()); 9450 }, 9451 9452 _getNewPixelOrigin: function (center, zoom) { 9453 var viewHalf = this.getSize()._divideBy(2); 9454 return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round(); 9455 }, 9456 9457 _latLngToNewLayerPoint: function (latlng, zoom, center) { 9458 var topLeft = this._getNewPixelOrigin(center, zoom); 9459 return this.project(latlng, zoom)._subtract(topLeft); 9460 }, 9461 9462 _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) { 9463 var topLeft = this._getNewPixelOrigin(center, zoom); 9464 return toBounds([ 9465 this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft), 9466 this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft), 9467 this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft), 9468 this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft) 9469 ]); 9470 }, 9471 9472 // layer point of the current center 9473 _getCenterLayerPoint: function () { 9474 return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); 9475 }, 9476 9477 // offset of the specified place to the current center in pixels 9478 _getCenterOffset: function (latlng) { 9479 return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); 9480 }, 9481 9482 // adjust center for view to get inside bounds 9483 _limitCenter: function (center, zoom, bounds) { 9484 9485 if (!bounds) { return center; } 9486 9487 var centerPoint = this.project(center, zoom), 9488 viewHalf = this.getSize().divideBy(2), 9489 viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), 9490 offset = this._getBoundsOffset(viewBounds, bounds, zoom); 9491 9492 // If offset is less than a pixel, ignore. 9493 // This prevents unstable projections from getting into 9494 // an infinite loop of tiny offsets. 9495 if (Math.abs(offset.x) <= 1 && Math.abs(offset.y) <= 1) { 9496 return center; 9497 } 9498 9499 return this.unproject(centerPoint.add(offset), zoom); 9500 }, 9501 9502 // adjust offset for view to get inside bounds 9503 _limitOffset: function (offset, bounds) { 9504 if (!bounds) { return offset; } 9505 9506 var viewBounds = this.getPixelBounds(), 9507 newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); 9508 9509 return offset.add(this._getBoundsOffset(newBounds, bounds)); 9510 }, 9511 9512 // returns offset needed for pxBounds to get inside maxBounds at a specified zoom 9513 _getBoundsOffset: function (pxBounds, maxBounds, zoom) { 9514 var projectedMaxBounds = toBounds( 9515 this.project(maxBounds.getNorthEast(), zoom), 9516 this.project(maxBounds.getSouthWest(), zoom) 9517 ), 9518 minOffset = projectedMaxBounds.min.subtract(pxBounds.min), 9519 maxOffset = projectedMaxBounds.max.subtract(pxBounds.max), 9520 9521 dx = this._rebound(minOffset.x, -maxOffset.x), 9522 dy = this._rebound(minOffset.y, -maxOffset.y); 9523 9524 return new Point(dx, dy); 9525 }, 9526 9527 _rebound: function (left, right) { 9528 return left + right > 0 ? 9529 Math.round(left - right) / 2 : 9530 Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); 9531 }, 9532 9533 _limitZoom: function (zoom) { 9534 var min = this.getMinZoom(), 9535 max = this.getMaxZoom(), 9536 snap = Browser.any3d ? this.options.zoomSnap : 1; 9537 if (snap) { 9538 zoom = Math.round(zoom / snap) * snap; 9539 } 9540 return Math.max(min, Math.min(max, zoom)); 9541 }, 9542 9543 _onPanTransitionStep: function () { 9544 this.fire('move'); 9545 }, 9546 9547 _onPanTransitionEnd: function () { 9548 removeClass(this._mapPane, 'leaflet-pan-anim'); 9549 this.fire('moveend'); 9550 }, 9551 9552 _tryAnimatedPan: function (center, options) { 9553 // difference between the new and current centers in pixels 9554 var offset = this._getCenterOffset(center)._trunc(); 9555 9556 // don't animate too far unless animate: true specified in options 9557 if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; } 9558 9559 this.panBy(offset, options); 9560 9561 return true; 9562 }, 9563 9564 _createAnimProxy: function () { 9565 9566 var proxy = this._proxy = create$1('div', 'leaflet-proxy leaflet-zoom-animated'); 9567 this._panes.mapPane.appendChild(proxy); 9568 9569 this.on('zoomanim', function (e) { 9570 var prop = TRANSFORM, 9571 transform = this._proxy.style[prop]; 9572 9573 setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1)); 9574 9575 // workaround for case when transform is the same and so transitionend event is not fired 9576 if (transform === this._proxy.style[prop] && this._animatingZoom) { 9577 this._onZoomTransitionEnd(); 9578 } 9579 }, this); 9580 9581 this.on('load moveend', this._animMoveEnd, this); 9582 9583 this._on('unload', this._destroyAnimProxy, this); 9584 }, 9585 9586 _destroyAnimProxy: function () { 9587 remove(this._proxy); 9588 this.off('load moveend', this._animMoveEnd, this); 9589 delete this._proxy; 9590 }, 9591 9592 _animMoveEnd: function () { 9593 var c = this.getCenter(), 9594 z = this.getZoom(); 9595 setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1)); 9596 }, 9597 9598 _catchTransitionEnd: function (e) { 9599 if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) { 9600 this._onZoomTransitionEnd(); 9601 } 9602 }, 9603 9604 _nothingToAnimate: function () { 9605 return !this._container.getElementsByClassName('leaflet-zoom-animated').length; 9606 }, 9607 9608 _tryAnimatedZoom: function (center, zoom, options) { 9609 9610 if (this._animatingZoom) { return true; } 9611 9612 options = options || {}; 9613 9614 // don't animate if disabled, not supported or zoom difference is too large 9615 if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() || 9616 Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; } 9617 9618 // offset is the pixel coords of the zoom origin relative to the current center 9619 var scale = this.getZoomScale(zoom), 9620 offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale); 9621 9622 // don't animate if the zoom origin isn't within one screen from the current center, unless forced 9623 if (options.animate !== true && !this.getSize().contains(offset)) { return false; } 9624 9625 requestAnimFrame(function () { 9626 this 9627 ._moveStart(true, options.noMoveStart || false) 9628 ._animateZoom(center, zoom, true); 9629 }, this); 9630 9631 return true; 9632 }, 9633 9634 _animateZoom: function (center, zoom, startAnim, noUpdate) { 9635 if (!this._mapPane) { return; } 9636 9637 if (startAnim) { 9638 this._animatingZoom = true; 9639 9640 // remember what center/zoom to set after animation 9641 this._animateToCenter = center; 9642 this._animateToZoom = zoom; 9643 9644 addClass(this._mapPane, 'leaflet-zoom-anim'); 9645 } 9646 9647 // @section Other Events 9648 // @event zoomanim: ZoomAnimEvent 9649 // Fired at least once per zoom animation. For continuous zoom, like pinch zooming, fired once per frame during zoom. 9650 this.fire('zoomanim', { 9651 center: center, 9652 zoom: zoom, 9653 noUpdate: noUpdate 9654 }); 9655 9656 if (!this._tempFireZoomEvent) { 9657 this._tempFireZoomEvent = this._zoom !== this._animateToZoom; 9658 } 9659 9660 this._move(this._animateToCenter, this._animateToZoom, undefined, true); 9661 9662 // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693 9663 setTimeout(bind(this._onZoomTransitionEnd, this), 250); 9664 }, 9665 9666 _onZoomTransitionEnd: function () { 9667 if (!this._animatingZoom) { return; } 9668 9669 if (this._mapPane) { 9670 removeClass(this._mapPane, 'leaflet-zoom-anim'); 9671 } 9672 9673 this._animatingZoom = false; 9674 9675 this._move(this._animateToCenter, this._animateToZoom, undefined, true); 9676 9677 if (this._tempFireZoomEvent) { 9678 this.fire('zoom'); 9679 } 9680 delete this._tempFireZoomEvent; 9681 9682 this.fire('move'); 9683 9684 this._moveEnd(true); 9685 } 9686 }); 9687 9688 // @section 9689 9690 // @factory L.map(id: String, options?: Map options) 9691 // Instantiates a map object given the DOM ID of a `<div>` element 9692 // and optionally an object literal with `Map options`. 9693 // 9694 // @alternative 9695 // @factory L.map(el: HTMLElement, options?: Map options) 9696 // Instantiates a map object given an instance of a `<div>` HTML element 9697 // and optionally an object literal with `Map options`. 9698 function createMap(id, options) { 9699 return new Map(id, options); 9700 } 9701 9702 /* 9703 * @class Control 9704 * @aka L.Control 9705 * @inherits Class 9706 * 9707 * L.Control is a base class for implementing map controls. Handles positioning. 9708 * All other controls extend from this class. 9709 */ 9710 9711 var Control = Class.extend({ 9712 // @section 9713 // @aka Control Options 9714 options: { 9715 // @option position: String = 'topright' 9716 // The position of the control (one of the map corners). Possible values are `'topleft'`, 9717 // `'topright'`, `'bottomleft'` or `'bottomright'` 9718 position: 'topright' 9719 }, 9720 9721 initialize: function (options) { 9722 setOptions(this, options); 9723 }, 9724 9725 /* @section 9726 * Classes extending L.Control will inherit the following methods: 9727 * 9728 * @method getPosition: string 9729 * Returns the position of the control. 9730 */ 9731 getPosition: function () { 9732 return this.options.position; 9733 }, 9734 9735 // @method setPosition(position: string): this 9736 // Sets the position of the control. 9737 setPosition: function (position) { 9738 var map = this._map; 9739 9740 if (map) { 9741 map.removeControl(this); 9742 } 9743 9744 this.options.position = position; 9745 9746 if (map) { 9747 map.addControl(this); 9748 } 9749 9750 return this; 9751 }, 9752 9753 // @method getContainer: HTMLElement 9754 // Returns the HTMLElement that contains the control. 9755 getContainer: function () { 9756 return this._container; 9757 }, 9758 9759 // @method addTo(map: Map): this 9760 // Adds the control to the given map. 9761 addTo: function (map) { 9762 this.remove(); 9763 this._map = map; 9764 9765 var container = this._container = this.onAdd(map), 9766 pos = this.getPosition(), 9767 corner = map._controlCorners[pos]; 9768 9769 addClass(container, 'leaflet-control'); 9770 9771 if (pos.indexOf('bottom') !== -1) { 9772 corner.insertBefore(container, corner.firstChild); 9773 } else { 9774 corner.appendChild(container); 9775 } 9776 9777 this._map.on('unload', this.remove, this); 9778 9779 return this; 9780 }, 9781 9782 // @method remove: this 9783 // Removes the control from the map it is currently active on. 9784 remove: function () { 9785 if (!this._map) { 9786 return this; 9787 } 9788 9789 remove(this._container); 9790 9791 if (this.onRemove) { 9792 this.onRemove(this._map); 9793 } 9794 9795 this._map.off('unload', this.remove, this); 9796 this._map = null; 9797 9798 return this; 9799 }, 9800 9801 _refocusOnMap: function (e) { 9802 // if map exists and event is not a keyboard event 9803 if (this._map && e && e.screenX > 0 && e.screenY > 0) { 9804 this._map.getContainer().focus(); 9805 } 9806 } 9807 }); 9808 9809 var control = function (options) { 9810 return new Control(options); 9811 }; 9812 9813 /* @section Extension methods 9814 * @uninheritable 9815 * 9816 * Every control should extend from `L.Control` and (re-)implement the following methods. 9817 * 9818 * @method onAdd(map: Map): HTMLElement 9819 * Should return the container DOM element for the control and add listeners on relevant map events. Called on [`control.addTo(map)`](#control-addTo). 9820 * 9821 * @method onRemove(map: Map) 9822 * Optional method. Should contain all clean up code that removes the listeners previously added in [`onAdd`](#control-onadd). Called on [`control.remove()`](#control-remove). 9823 */ 9824 9825 /* @namespace Map 9826 * @section Methods for Layers and Controls 9827 */ 9828 Map.include({ 9829 // @method addControl(control: Control): this 9830 // Adds the given control to the map 9831 addControl: function (control) { 9832 control.addTo(this); 9833 return this; 9834 }, 9835 9836 // @method removeControl(control: Control): this 9837 // Removes the given control from the map 9838 removeControl: function (control) { 9839 control.remove(); 9840 return this; 9841 }, 9842 9843 _initControlPos: function () { 9844 var corners = this._controlCorners = {}, 9845 l = 'leaflet-', 9846 container = this._controlContainer = 9847 create$1('div', l + 'control-container', this._container); 9848 9849 function createCorner(vSide, hSide) { 9850 var className = l + vSide + ' ' + l + hSide; 9851 9852 corners[vSide + hSide] = create$1('div', className, container); 9853 } 9854 9855 createCorner('top', 'left'); 9856 createCorner('top', 'right'); 9857 createCorner('bottom', 'left'); 9858 createCorner('bottom', 'right'); 9859 }, 9860 9861 _clearControlPos: function () { 9862 for (var i in this._controlCorners) { 9863 remove(this._controlCorners[i]); 9864 } 9865 remove(this._controlContainer); 9866 delete this._controlCorners; 9867 delete this._controlContainer; 9868 } 9869 }); 9870 9871 /* 9872 * @class Control.Layers 9873 * @aka L.Control.Layers 9874 * @inherits Control 9875 * 9876 * The layers control gives users the ability to switch between different base layers and switch overlays on/off (check out the [detailed example](https://leafletjs.com/examples/layers-control/)). Extends `Control`. 9877 * 9878 * @example 9879 * 9880 * ```js 9881 * var baseLayers = { 9882 * "Mapbox": mapbox, 9883 * "OpenStreetMap": osm 9884 * }; 9885 * 9886 * var overlays = { 9887 * "Marker": marker, 9888 * "Roads": roadsLayer 9889 * }; 9890 * 9891 * L.control.layers(baseLayers, overlays).addTo(map); 9892 * ``` 9893 * 9894 * The `baseLayers` and `overlays` parameters are object literals with layer names as keys and `Layer` objects as values: 9895 * 9896 * ```js 9897 * { 9898 * "<someName1>": layer1, 9899 * "<someName2>": layer2 9900 * } 9901 * ``` 9902 * 9903 * The layer names can contain HTML, which allows you to add additional styling to the items: 9904 * 9905 * ```js 9906 * {"<img src='my-layer-icon' /> <span class='my-layer-item'>My Layer</span>": myLayer} 9907 * ``` 9908 */ 9909 9910 var Layers = Control.extend({ 9911 // @section 9912 // @aka Control.Layers options 9913 options: { 9914 // @option collapsed: Boolean = true 9915 // If `true`, the control will be collapsed into an icon and expanded on mouse hover, touch, or keyboard activation. 9916 collapsed: true, 9917 position: 'topright', 9918 9919 // @option autoZIndex: Boolean = true 9920 // If `true`, the control will assign zIndexes in increasing order to all of its layers so that the order is preserved when switching them on/off. 9921 autoZIndex: true, 9922 9923 // @option hideSingleBase: Boolean = false 9924 // If `true`, the base layers in the control will be hidden when there is only one. 9925 hideSingleBase: false, 9926 9927 // @option sortLayers: Boolean = false 9928 // Whether to sort the layers. When `false`, layers will keep the order 9929 // in which they were added to the control. 9930 sortLayers: false, 9931 9932 // @option sortFunction: Function = * 9933 // A [compare function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) 9934 // that will be used for sorting the layers, when `sortLayers` is `true`. 9935 // The function receives both the `L.Layer` instances and their names, as in 9936 // `sortFunction(layerA, layerB, nameA, nameB)`. 9937 // By default, it sorts layers alphabetically by their name. 9938 sortFunction: function (layerA, layerB, nameA, nameB) { 9939 return nameA < nameB ? -1 : (nameB < nameA ? 1 : 0); 9940 } 9941 }, 9942 9943 initialize: function (baseLayers, overlays, options) { 9944 setOptions(this, options); 9945 9946 this._layerControlInputs = []; 9947 this._layers = []; 9948 this._lastZIndex = 0; 9949 this._handlingClick = false; 9950 this._preventClick = false; 9951 9952 for (var i in baseLayers) { 9953 this._addLayer(baseLayers[i], i); 9954 } 9955 9956 for (i in overlays) { 9957 this._addLayer(overlays[i], i, true); 9958 } 9959 }, 9960 9961 onAdd: function (map) { 9962 this._initLayout(); 9963 this._update(); 9964 9965 this._map = map; 9966 map.on('zoomend', this._checkDisabledLayers, this); 9967 9968 for (var i = 0; i < this._layers.length; i++) { 9969 this._layers[i].layer.on('add remove', this._onLayerChange, this); 9970 } 9971 9972 return this._container; 9973 }, 9974 9975 addTo: function (map) { 9976 Control.prototype.addTo.call(this, map); 9977 // Trigger expand after Layers Control has been inserted into DOM so that is now has an actual height. 9978 return this._expandIfNotCollapsed(); 9979 }, 9980 9981 onRemove: function () { 9982 this._map.off('zoomend', this._checkDisabledLayers, this); 9983 9984 for (var i = 0; i < this._layers.length; i++) { 9985 this._layers[i].layer.off('add remove', this._onLayerChange, this); 9986 } 9987 }, 9988 9989 // @method addBaseLayer(layer: Layer, name: String): this 9990 // Adds a base layer (radio button entry) with the given name to the control. 9991 addBaseLayer: function (layer, name) { 9992 this._addLayer(layer, name); 9993 return (this._map) ? this._update() : this; 9994 }, 9995 9996 // @method addOverlay(layer: Layer, name: String): this 9997 // Adds an overlay (checkbox entry) with the given name to the control. 9998 addOverlay: function (layer, name) { 9999 this._addLayer(layer, name, true); 10000 return (this._map) ? this._update() : this; 10001 }, 10002 10003 // @method removeLayer(layer: Layer): this 10004 // Remove the given layer from the control. 10005 removeLayer: function (layer) { 10006 layer.off('add remove', this._onLayerChange, this); 10007 10008 var obj = this._getLayer(stamp(layer)); 10009 if (obj) { 10010 this._layers.splice(this._layers.indexOf(obj), 1); 10011 } 10012 return (this._map) ? this._update() : this; 10013 }, 10014 10015 // @method expand(): this 10016 // Expand the control container if collapsed. 10017 expand: function () { 10018 addClass(this._container, 'leaflet-control-layers-expanded'); 10019 this._section.style.height = null; 10020 var acceptableHeight = this._map.getSize().y - (this._container.offsetTop + 50); 10021 if (acceptableHeight < this._section.clientHeight) { 10022 addClass(this._section, 'leaflet-control-layers-scrollbar'); 10023 this._section.style.height = acceptableHeight + 'px'; 10024 } else { 10025 removeClass(this._section, 'leaflet-control-layers-scrollbar'); 10026 } 10027 this._checkDisabledLayers(); 10028 return this; 10029 }, 10030 10031 // @method collapse(): this 10032 // Collapse the control container if expanded. 10033 collapse: function () { 10034 removeClass(this._container, 'leaflet-control-layers-expanded'); 10035 return this; 10036 }, 10037 10038 _initLayout: function () { 10039 var className = 'leaflet-control-layers', 10040 container = this._container = create$1('div', className), 10041 collapsed = this.options.collapsed; 10042 10043 // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released 10044 container.setAttribute('aria-haspopup', true); 10045 10046 disableClickPropagation(container); 10047 disableScrollPropagation(container); 10048 10049 var section = this._section = create$1('section', className + '-list'); 10050 10051 if (collapsed) { 10052 this._map.on('click', this.collapse, this); 10053 10054 on(container, { 10055 mouseenter: this._expandSafely, 10056 mouseleave: this.collapse 10057 }, this); 10058 } 10059 10060 var link = this._layersLink = create$1('a', className + '-toggle', container); 10061 link.href = '#'; 10062 link.title = 'Layers'; 10063 link.setAttribute('role', 'button'); 10064 10065 on(link, { 10066 keydown: function (e) { 10067 if (e.keyCode === 13) { 10068 this._expandSafely(); 10069 } 10070 }, 10071 // Certain screen readers intercept the key event and instead send a click event 10072 click: function (e) { 10073 preventDefault(e); 10074 this._expandSafely(); 10075 } 10076 }, this); 10077 10078 if (!collapsed) { 10079 this.expand(); 10080 } 10081 10082 this._baseLayersList = create$1('div', className + '-base', section); 10083 this._separator = create$1('div', className + '-separator', section); 10084 this._overlaysList = create$1('div', className + '-overlays', section); 10085 10086 container.appendChild(section); 10087 }, 10088 10089 _getLayer: function (id) { 10090 for (var i = 0; i < this._layers.length; i++) { 10091 10092 if (this._layers[i] && stamp(this._layers[i].layer) === id) { 10093 return this._layers[i]; 10094 } 10095 } 10096 }, 10097 10098 _addLayer: function (layer, name, overlay) { 10099 if (this._map) { 10100 layer.on('add remove', this._onLayerChange, this); 10101 } 10102 10103 this._layers.push({ 10104 layer: layer, 10105 name: name, 10106 overlay: overlay 10107 }); 10108 10109 if (this.options.sortLayers) { 10110 this._layers.sort(bind(function (a, b) { 10111 return this.options.sortFunction(a.layer, b.layer, a.name, b.name); 10112 }, this)); 10113 } 10114 10115 if (this.options.autoZIndex && layer.setZIndex) { 10116 this._lastZIndex++; 10117 layer.setZIndex(this._lastZIndex); 10118 } 10119 10120 this._expandIfNotCollapsed(); 10121 }, 10122 10123 _update: function () { 10124 if (!this._container) { return this; } 10125 10126 empty(this._baseLayersList); 10127 empty(this._overlaysList); 10128 10129 this._layerControlInputs = []; 10130 var baseLayersPresent, overlaysPresent, i, obj, baseLayersCount = 0; 10131 10132 for (i = 0; i < this._layers.length; i++) { 10133 obj = this._layers[i]; 10134 this._addItem(obj); 10135 overlaysPresent = overlaysPresent || obj.overlay; 10136 baseLayersPresent = baseLayersPresent || !obj.overlay; 10137 baseLayersCount += !obj.overlay ? 1 : 0; 10138 } 10139 10140 // Hide base layers section if there's only one layer. 10141 if (this.options.hideSingleBase) { 10142 baseLayersPresent = baseLayersPresent && baseLayersCount > 1; 10143 this._baseLayersList.style.display = baseLayersPresent ? '' : 'none'; 10144 } 10145 10146 this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; 10147 10148 return this; 10149 }, 10150 10151 _onLayerChange: function (e) { 10152 if (!this._handlingClick) { 10153 this._update(); 10154 } 10155 10156 var obj = this._getLayer(stamp(e.target)); 10157 10158 // @namespace Map 10159 // @section Layer events 10160 // @event baselayerchange: LayersControlEvent 10161 // Fired when the base layer is changed through the [layers control](#control-layers). 10162 // @event overlayadd: LayersControlEvent 10163 // Fired when an overlay is selected through the [layers control](#control-layers). 10164 // @event overlayremove: LayersControlEvent 10165 // Fired when an overlay is deselected through the [layers control](#control-layers). 10166 // @namespace Control.Layers 10167 var type = obj.overlay ? 10168 (e.type === 'add' ? 'overlayadd' : 'overlayremove') : 10169 (e.type === 'add' ? 'baselayerchange' : null); 10170 10171 if (type) { 10172 this._map.fire(type, obj); 10173 } 10174 }, 10175 10176 // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see https://stackoverflow.com/a/119079) 10177 _createRadioElement: function (name, checked) { 10178 10179 var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' + 10180 name + '"' + (checked ? ' checked="checked"' : '') + '/>'; 10181 10182 var radioFragment = document.createElement('div'); 10183 radioFragment.innerHTML = radioHtml; 10184 10185 return radioFragment.firstChild; 10186 }, 10187 10188 _addItem: function (obj) { 10189 var label = document.createElement('label'), 10190 checked = this._map.hasLayer(obj.layer), 10191 input; 10192 10193 if (obj.overlay) { 10194 input = document.createElement('input'); 10195 input.type = 'checkbox'; 10196 input.className = 'leaflet-control-layers-selector'; 10197 input.defaultChecked = checked; 10198 } else { 10199 input = this._createRadioElement('leaflet-base-layers_' + stamp(this), checked); 10200 } 10201 10202 this._layerControlInputs.push(input); 10203 input.layerId = stamp(obj.layer); 10204 10205 on(input, 'click', this._onInputClick, this); 10206 10207 var name = document.createElement('span'); 10208 name.innerHTML = ' ' + obj.name; 10209 10210 // Helps from preventing layer control flicker when checkboxes are disabled 10211 // https://github.com/Leaflet/Leaflet/issues/2771 10212 var holder = document.createElement('span'); 10213 10214 label.appendChild(holder); 10215 holder.appendChild(input); 10216 holder.appendChild(name); 10217 10218 var container = obj.overlay ? this._overlaysList : this._baseLayersList; 10219 container.appendChild(label); 10220 10221 this._checkDisabledLayers(); 10222 return label; 10223 }, 10224 10225 _onInputClick: function () { 10226 // expanding the control on mobile with a click can cause adding a layer - we don't want this 10227 if (this._preventClick) { 10228 return; 10229 } 10230 10231 var inputs = this._layerControlInputs, 10232 input, layer; 10233 var addedLayers = [], 10234 removedLayers = []; 10235 10236 this._handlingClick = true; 10237 10238 for (var i = inputs.length - 1; i >= 0; i--) { 10239 input = inputs[i]; 10240 layer = this._getLayer(input.layerId).layer; 10241 10242 if (input.checked) { 10243 addedLayers.push(layer); 10244 } else if (!input.checked) { 10245 removedLayers.push(layer); 10246 } 10247 } 10248 10249 // Bugfix issue 2318: Should remove all old layers before readding new ones 10250 for (i = 0; i < removedLayers.length; i++) { 10251 if (this._map.hasLayer(removedLayers[i])) { 10252 this._map.removeLayer(removedLayers[i]); 10253 } 10254 } 10255 for (i = 0; i < addedLayers.length; i++) { 10256 if (!this._map.hasLayer(addedLayers[i])) { 10257 this._map.addLayer(addedLayers[i]); 10258 } 10259 } 10260 10261 this._handlingClick = false; 10262 10263 this._refocusOnMap(); 10264 }, 10265 10266 _checkDisabledLayers: function () { 10267 var inputs = this._layerControlInputs, 10268 input, 10269 layer, 10270 zoom = this._map.getZoom(); 10271 10272 for (var i = inputs.length - 1; i >= 0; i--) { 10273 input = inputs[i]; 10274 layer = this._getLayer(input.layerId).layer; 10275 input.disabled = (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) || 10276 (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom); 10277 10278 } 10279 }, 10280 10281 _expandIfNotCollapsed: function () { 10282 if (this._map && !this.options.collapsed) { 10283 this.expand(); 10284 } 10285 return this; 10286 }, 10287 10288 _expandSafely: function () { 10289 var section = this._section; 10290 this._preventClick = true; 10291 on(section, 'click', preventDefault); 10292 this.expand(); 10293 var that = this; 10294 setTimeout(function () { 10295 off(section, 'click', preventDefault); 10296 that._preventClick = false; 10297 }); 10298 } 10299 10300 }); 10301 10302 10303 // @factory L.control.layers(baselayers?: Object, overlays?: Object, options?: Control.Layers options) 10304 // Creates a layers control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. Note that all base layers should be passed in the base layers object, but only one should be added to the map during map instantiation. 10305 var layers = function (baseLayers, overlays, options) { 10306 return new Layers(baseLayers, overlays, options); 10307 }; 10308 10309 /* 10310 * @class Control.Zoom 10311 * @aka L.Control.Zoom 10312 * @inherits Control 10313 * 10314 * A basic zoom control with two buttons (zoom in and zoom out). It is put on the map by default unless you set its [`zoomControl` option](#map-zoomcontrol) to `false`. Extends `Control`. 10315 */ 10316 10317 var Zoom = Control.extend({ 10318 // @section 10319 // @aka Control.Zoom options 10320 options: { 10321 position: 'topleft', 10322 10323 // @option zoomInText: String = '<span aria-hidden="true">+</span>' 10324 // The text set on the 'zoom in' button. 10325 zoomInText: '<span aria-hidden="true">+</span>', 10326 10327 // @option zoomInTitle: String = 'Zoom in' 10328 // The title set on the 'zoom in' button. 10329 zoomInTitle: 'Zoom in', 10330 10331 // @option zoomOutText: String = '<span aria-hidden="true">−</span>' 10332 // The text set on the 'zoom out' button. 10333 zoomOutText: '<span aria-hidden="true">−</span>', 10334 10335 // @option zoomOutTitle: String = 'Zoom out' 10336 // The title set on the 'zoom out' button. 10337 zoomOutTitle: 'Zoom out' 10338 }, 10339 10340 onAdd: function (map) { 10341 var zoomName = 'leaflet-control-zoom', 10342 container = create$1('div', zoomName + ' leaflet-bar'), 10343 options = this.options; 10344 10345 this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, 10346 zoomName + '-in', container, this._zoomIn); 10347 this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, 10348 zoomName + '-out', container, this._zoomOut); 10349 10350 this._updateDisabled(); 10351 map.on('zoomend zoomlevelschange', this._updateDisabled, this); 10352 10353 return container; 10354 }, 10355 10356 onRemove: function (map) { 10357 map.off('zoomend zoomlevelschange', this._updateDisabled, this); 10358 }, 10359 10360 disable: function () { 10361 this._disabled = true; 10362 this._updateDisabled(); 10363 return this; 10364 }, 10365 10366 enable: function () { 10367 this._disabled = false; 10368 this._updateDisabled(); 10369 return this; 10370 }, 10371 10372 _zoomIn: function (e) { 10373 if (!this._disabled && this._map._zoom < this._map.getMaxZoom()) { 10374 this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); 10375 } 10376 }, 10377 10378 _zoomOut: function (e) { 10379 if (!this._disabled && this._map._zoom > this._map.getMinZoom()) { 10380 this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); 10381 } 10382 }, 10383 10384 _createButton: function (html, title, className, container, fn) { 10385 var link = create$1('a', className, container); 10386 link.innerHTML = html; 10387 link.href = '#'; 10388 link.title = title; 10389 10390 /* 10391 * Will force screen readers like VoiceOver to read this as "Zoom in - button" 10392 */ 10393 link.setAttribute('role', 'button'); 10394 link.setAttribute('aria-label', title); 10395 10396 disableClickPropagation(link); 10397 on(link, 'click', stop); 10398 on(link, 'click', fn, this); 10399 on(link, 'click', this._refocusOnMap, this); 10400 10401 return link; 10402 }, 10403 10404 _updateDisabled: function () { 10405 var map = this._map, 10406 className = 'leaflet-disabled'; 10407 10408 removeClass(this._zoomInButton, className); 10409 removeClass(this._zoomOutButton, className); 10410 this._zoomInButton.setAttribute('aria-disabled', 'false'); 10411 this._zoomOutButton.setAttribute('aria-disabled', 'false'); 10412 10413 if (this._disabled || map._zoom === map.getMinZoom()) { 10414 addClass(this._zoomOutButton, className); 10415 this._zoomOutButton.setAttribute('aria-disabled', 'true'); 10416 } 10417 if (this._disabled || map._zoom === map.getMaxZoom()) { 10418 addClass(this._zoomInButton, className); 10419 this._zoomInButton.setAttribute('aria-disabled', 'true'); 10420 } 10421 } 10422 }); 10423 10424 // @namespace Map 10425 // @section Control options 10426 // @option zoomControl: Boolean = true 10427 // Whether a [zoom control](#control-zoom) is added to the map by default. 10428 Map.mergeOptions({ 10429 zoomControl: true 10430 }); 10431 10432 Map.addInitHook(function () { 10433 if (this.options.zoomControl) { 10434 // @section Controls 10435 // @property zoomControl: Control.Zoom 10436 // The default zoom control (only available if the 10437 // [`zoomControl` option](#map-zoomcontrol) was `true` when creating the map). 10438 this.zoomControl = new Zoom(); 10439 this.addControl(this.zoomControl); 10440 } 10441 }); 10442 10443 // @namespace Control.Zoom 10444 // @factory L.control.zoom(options: Control.Zoom options) 10445 // Creates a zoom control 10446 var zoom = function (options) { 10447 return new Zoom(options); 10448 }; 10449 10450 /* 10451 * @class Control.Scale 10452 * @aka L.Control.Scale 10453 * @inherits Control 10454 * 10455 * A simple scale control that shows the scale of the current center of screen in metric (m/km) and imperial (mi/ft) systems. Extends `Control`. 10456 * 10457 * @example 10458 * 10459 * ```js 10460 * L.control.scale().addTo(map); 10461 * ``` 10462 */ 10463 10464 var Scale = Control.extend({ 10465 // @section 10466 // @aka Control.Scale options 10467 options: { 10468 position: 'bottomleft', 10469 10470 // @option maxWidth: Number = 100 10471 // Maximum width of the control in pixels. The width is set dynamically to show round values (e.g. 100, 200, 500). 10472 maxWidth: 100, 10473 10474 // @option metric: Boolean = True 10475 // Whether to show the metric scale line (m/km). 10476 metric: true, 10477 10478 // @option imperial: Boolean = True 10479 // Whether to show the imperial scale line (mi/ft). 10480 imperial: true 10481 10482 // @option updateWhenIdle: Boolean = false 10483 // If `true`, the control is updated on [`moveend`](#map-moveend), otherwise it's always up-to-date (updated on [`move`](#map-move)). 10484 }, 10485 10486 onAdd: function (map) { 10487 var className = 'leaflet-control-scale', 10488 container = create$1('div', className), 10489 options = this.options; 10490 10491 this._addScales(options, className + '-line', container); 10492 10493 map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this); 10494 map.whenReady(this._update, this); 10495 10496 return container; 10497 }, 10498 10499 onRemove: function (map) { 10500 map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this); 10501 }, 10502 10503 _addScales: function (options, className, container) { 10504 if (options.metric) { 10505 this._mScale = create$1('div', className, container); 10506 } 10507 if (options.imperial) { 10508 this._iScale = create$1('div', className, container); 10509 } 10510 }, 10511 10512 _update: function () { 10513 var map = this._map, 10514 y = map.getSize().y / 2; 10515 10516 var maxMeters = map.distance( 10517 map.containerPointToLatLng([0, y]), 10518 map.containerPointToLatLng([this.options.maxWidth, y])); 10519 10520 this._updateScales(maxMeters); 10521 }, 10522 10523 _updateScales: function (maxMeters) { 10524 if (this.options.metric && maxMeters) { 10525 this._updateMetric(maxMeters); 10526 } 10527 if (this.options.imperial && maxMeters) { 10528 this._updateImperial(maxMeters); 10529 } 10530 }, 10531 10532 _updateMetric: function (maxMeters) { 10533 var meters = this._getRoundNum(maxMeters), 10534 label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; 10535 10536 this._updateScale(this._mScale, label, meters / maxMeters); 10537 }, 10538 10539 _updateImperial: function (maxMeters) { 10540 var maxFeet = maxMeters * 3.2808399, 10541 maxMiles, miles, feet; 10542 10543 if (maxFeet > 5280) { 10544 maxMiles = maxFeet / 5280; 10545 miles = this._getRoundNum(maxMiles); 10546 this._updateScale(this._iScale, miles + ' mi', miles / maxMiles); 10547 10548 } else { 10549 feet = this._getRoundNum(maxFeet); 10550 this._updateScale(this._iScale, feet + ' ft', feet / maxFeet); 10551 } 10552 }, 10553 10554 _updateScale: function (scale, text, ratio) { 10555 scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px'; 10556 scale.innerHTML = text; 10557 }, 10558 10559 _getRoundNum: function (num) { 10560 var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1), 10561 d = num / pow10; 10562 10563 d = d >= 10 ? 10 : 10564 d >= 5 ? 5 : 10565 d >= 3 ? 3 : 10566 d >= 2 ? 2 : 1; 10567 10568 return pow10 * d; 10569 } 10570 }); 10571 10572 10573 // @factory L.control.scale(options?: Control.Scale options) 10574 // Creates an scale control with the given options. 10575 var scale = function (options) { 10576 return new Scale(options); 10577 }; 10578 10579 var ukrainianFlag = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8" class="leaflet-attribution-flag"><path fill="#4C7BE1" d="M0 0h12v4H0z"/><path fill="#FFD500" d="M0 4h12v3H0z"/><path fill="#E0BC00" d="M0 7h12v1H0z"/></svg>'; 10580 10581 10582 /* 10583 * @class Control.Attribution 10584 * @aka L.Control.Attribution 10585 * @inherits Control 10586 * 10587 * The attribution control allows you to display attribution data in a small text box on a map. It is put on the map by default unless you set its [`attributionControl` option](#map-attributioncontrol) to `false`, and it fetches attribution texts from layers with the [`getAttribution` method](#layer-getattribution) automatically. Extends Control. 10588 */ 10589 10590 var Attribution = Control.extend({ 10591 // @section 10592 // @aka Control.Attribution options 10593 options: { 10594 position: 'bottomright', 10595 10596 // @option prefix: String|false = 'Leaflet' 10597 // The HTML text shown before the attributions. Pass `false` to disable. 10598 prefix: '<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">' + (Browser.inlineSvg ? ukrainianFlag + ' ' : '') + 'Leaflet</a>' 10599 }, 10600 10601 initialize: function (options) { 10602 setOptions(this, options); 10603 10604 this._attributions = {}; 10605 }, 10606 10607 onAdd: function (map) { 10608 map.attributionControl = this; 10609 this._container = create$1('div', 'leaflet-control-attribution'); 10610 disableClickPropagation(this._container); 10611 10612 // TODO ugly, refactor 10613 for (var i in map._layers) { 10614 if (map._layers[i].getAttribution) { 10615 this.addAttribution(map._layers[i].getAttribution()); 10616 } 10617 } 10618 10619 this._update(); 10620 10621 map.on('layeradd', this._addAttribution, this); 10622 10623 return this._container; 10624 }, 10625 10626 onRemove: function (map) { 10627 map.off('layeradd', this._addAttribution, this); 10628 }, 10629 10630 _addAttribution: function (ev) { 10631 if (ev.layer.getAttribution) { 10632 this.addAttribution(ev.layer.getAttribution()); 10633 ev.layer.once('remove', function () { 10634 this.removeAttribution(ev.layer.getAttribution()); 10635 }, this); 10636 } 10637 }, 10638 10639 // @method setPrefix(prefix: String|false): this 10640 // The HTML text shown before the attributions. Pass `false` to disable. 10641 setPrefix: function (prefix) { 10642 this.options.prefix = prefix; 10643 this._update(); 10644 return this; 10645 }, 10646 10647 // @method addAttribution(text: String): this 10648 // Adds an attribution text (e.g. `'© OpenStreetMap contributors'`). 10649 addAttribution: function (text) { 10650 if (!text) { return this; } 10651 10652 if (!this._attributions[text]) { 10653 this._attributions[text] = 0; 10654 } 10655 this._attributions[text]++; 10656 10657 this._update(); 10658 10659 return this; 10660 }, 10661 10662 // @method removeAttribution(text: String): this 10663 // Removes an attribution text. 10664 removeAttribution: function (text) { 10665 if (!text) { return this; } 10666 10667 if (this._attributions[text]) { 10668 this._attributions[text]--; 10669 this._update(); 10670 } 10671 10672 return this; 10673 }, 10674 10675 _update: function () { 10676 if (!this._map) { return; } 10677 10678 var attribs = []; 10679 10680 for (var i in this._attributions) { 10681 if (this._attributions[i]) { 10682 attribs.push(i); 10683 } 10684 } 10685 10686 var prefixAndAttribs = []; 10687 10688 if (this.options.prefix) { 10689 prefixAndAttribs.push(this.options.prefix); 10690 } 10691 if (attribs.length) { 10692 prefixAndAttribs.push(attribs.join(', ')); 10693 } 10694 10695 this._container.innerHTML = prefixAndAttribs.join(' <span aria-hidden="true">|</span> '); 10696 } 10697 }); 10698 10699 // @namespace Map 10700 // @section Control options 10701 // @option attributionControl: Boolean = true 10702 // Whether a [attribution control](#control-attribution) is added to the map by default. 10703 Map.mergeOptions({ 10704 attributionControl: true 10705 }); 10706 10707 Map.addInitHook(function () { 10708 if (this.options.attributionControl) { 10709 new Attribution().addTo(this); 10710 } 10711 }); 10712 10713 // @namespace Control.Attribution 10714 // @factory L.control.attribution(options: Control.Attribution options) 10715 // Creates an attribution control. 10716 var attribution = function (options) { 10717 return new Attribution(options); 10718 }; 10719 10720 Control.Layers = Layers; 10721 Control.Zoom = Zoom; 10722 Control.Scale = Scale; 10723 Control.Attribution = Attribution; 10724 10725 control.layers = layers; 10726 control.zoom = zoom; 10727 control.scale = scale; 10728 control.attribution = attribution; 10729 10730 /* 10731 L.Handler is a base class for handler classes that are used internally to inject 10732 interaction features like dragging to classes like Map and Marker. 10733 */ 10734 10735 // @class Handler 10736 // @aka L.Handler 10737 // Abstract class for map interaction handlers 10738 10739 var Handler = Class.extend({ 10740 initialize: function (map) { 10741 this._map = map; 10742 }, 10743 10744 // @method enable(): this 10745 // Enables the handler 10746 enable: function () { 10747 if (this._enabled) { return this; } 10748 10749 this._enabled = true; 10750 this.addHooks(); 10751 return this; 10752 }, 10753 10754 // @method disable(): this 10755 // Disables the handler 10756 disable: function () { 10757 if (!this._enabled) { return this; } 10758 10759 this._enabled = false; 10760 this.removeHooks(); 10761 return this; 10762 }, 10763 10764 // @method enabled(): Boolean 10765 // Returns `true` if the handler is enabled 10766 enabled: function () { 10767 return !!this._enabled; 10768 } 10769 10770 // @section Extension methods 10771 // Classes inheriting from `Handler` must implement the two following methods: 10772 // @method addHooks() 10773 // Called when the handler is enabled, should add event hooks. 10774 // @method removeHooks() 10775 // Called when the handler is disabled, should remove the event hooks added previously. 10776 }); 10777 10778 // @section There is static function which can be called without instantiating L.Handler: 10779 // @function addTo(map: Map, name: String): this 10780 // Adds a new Handler to the given map with the given name. 10781 Handler.addTo = function (map, name) { 10782 map.addHandler(name, this); 10783 return this; 10784 }; 10785 10786 var Mixin = {Events: Events}; 10787 10788 /* 10789 * @class Draggable 10790 * @aka L.Draggable 10791 * @inherits Evented 10792 * 10793 * A class for making DOM elements draggable (including touch support). 10794 * Used internally for map and marker dragging. Only works for elements 10795 * that were positioned with [`L.DomUtil.setPosition`](#domutil-setposition). 10796 * 10797 * @example 10798 * ```js 10799 * var draggable = new L.Draggable(elementToDrag); 10800 * draggable.enable(); 10801 * ``` 10802 */ 10803 10804 var START = Browser.touch ? 'touchstart mousedown' : 'mousedown'; 10805 10806 var Draggable = Evented.extend({ 10807 10808 options: { 10809 // @section 10810 // @aka Draggable options 10811 // @option clickTolerance: Number = 3 10812 // The max number of pixels a user can shift the mouse pointer during a click 10813 // for it to be considered a valid click (as opposed to a mouse drag). 10814 clickTolerance: 3 10815 }, 10816 10817 // @constructor L.Draggable(el: HTMLElement, dragHandle?: HTMLElement, preventOutline?: Boolean, options?: Draggable options) 10818 // Creates a `Draggable` object for moving `el` when you start dragging the `dragHandle` element (equals `el` itself by default). 10819 initialize: function (element, dragStartTarget, preventOutline, options) { 10820 setOptions(this, options); 10821 10822 this._element = element; 10823 this._dragStartTarget = dragStartTarget || element; 10824 this._preventOutline = preventOutline; 10825 }, 10826 10827 // @method enable() 10828 // Enables the dragging ability 10829 enable: function () { 10830 if (this._enabled) { return; } 10831 10832 on(this._dragStartTarget, START, this._onDown, this); 10833 10834 this._enabled = true; 10835 }, 10836 10837 // @method disable() 10838 // Disables the dragging ability 10839 disable: function () { 10840 if (!this._enabled) { return; } 10841 10842 // If we're currently dragging this draggable, 10843 // disabling it counts as first ending the drag. 10844 if (Draggable._dragging === this) { 10845 this.finishDrag(true); 10846 } 10847 10848 off(this._dragStartTarget, START, this._onDown, this); 10849 10850 this._enabled = false; 10851 this._moved = false; 10852 }, 10853 10854 _onDown: function (e) { 10855 // Ignore the event if disabled; this happens in IE11 10856 // under some circumstances, see #3666. 10857 if (!this._enabled) { return; } 10858 10859 this._moved = false; 10860 10861 if (hasClass(this._element, 'leaflet-zoom-anim')) { return; } 10862 10863 if (e.touches && e.touches.length !== 1) { 10864 // Finish dragging to avoid conflict with touchZoom 10865 if (Draggable._dragging === this) { 10866 this.finishDrag(); 10867 } 10868 return; 10869 } 10870 10871 if (Draggable._dragging || e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } 10872 Draggable._dragging = this; // Prevent dragging multiple objects at once. 10873 10874 if (this._preventOutline) { 10875 preventOutline(this._element); 10876 } 10877 10878 disableImageDrag(); 10879 disableTextSelection(); 10880 10881 if (this._moving) { return; } 10882 10883 // @event down: Event 10884 // Fired when a drag is about to start. 10885 this.fire('down'); 10886 10887 var first = e.touches ? e.touches[0] : e, 10888 sizedParent = getSizedParentNode(this._element); 10889 10890 this._startPoint = new Point(first.clientX, first.clientY); 10891 this._startPos = getPosition(this._element); 10892 10893 // Cache the scale, so that we can continuously compensate for it during drag (_onMove). 10894 this._parentScale = getScale(sizedParent); 10895 10896 var mouseevent = e.type === 'mousedown'; 10897 on(document, mouseevent ? 'mousemove' : 'touchmove', this._onMove, this); 10898 on(document, mouseevent ? 'mouseup' : 'touchend touchcancel', this._onUp, this); 10899 }, 10900 10901 _onMove: function (e) { 10902 // Ignore the event if disabled; this happens in IE11 10903 // under some circumstances, see #3666. 10904 if (!this._enabled) { return; } 10905 10906 if (e.touches && e.touches.length > 1) { 10907 this._moved = true; 10908 return; 10909 } 10910 10911 var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), 10912 offset = new Point(first.clientX, first.clientY)._subtract(this._startPoint); 10913 10914 if (!offset.x && !offset.y) { return; } 10915 if (Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) { return; } 10916 10917 // We assume that the parent container's position, border and scale do not change for the duration of the drag. 10918 // Therefore there is no need to account for the position and border (they are eliminated by the subtraction) 10919 // and we can use the cached value for the scale. 10920 offset.x /= this._parentScale.x; 10921 offset.y /= this._parentScale.y; 10922 10923 preventDefault(e); 10924 10925 if (!this._moved) { 10926 // @event dragstart: Event 10927 // Fired when a drag starts 10928 this.fire('dragstart'); 10929 10930 this._moved = true; 10931 10932 addClass(document.body, 'leaflet-dragging'); 10933 10934 this._lastTarget = e.target || e.srcElement; 10935 // IE and Edge do not give the <use> element, so fetch it 10936 // if necessary 10937 if (window.SVGElementInstance && this._lastTarget instanceof window.SVGElementInstance) { 10938 this._lastTarget = this._lastTarget.correspondingUseElement; 10939 } 10940 addClass(this._lastTarget, 'leaflet-drag-target'); 10941 } 10942 10943 this._newPos = this._startPos.add(offset); 10944 this._moving = true; 10945 10946 this._lastEvent = e; 10947 this._updatePosition(); 10948 }, 10949 10950 _updatePosition: function () { 10951 var e = {originalEvent: this._lastEvent}; 10952 10953 // @event predrag: Event 10954 // Fired continuously during dragging *before* each corresponding 10955 // update of the element's position. 10956 this.fire('predrag', e); 10957 setPosition(this._element, this._newPos); 10958 10959 // @event drag: Event 10960 // Fired continuously during dragging. 10961 this.fire('drag', e); 10962 }, 10963 10964 _onUp: function () { 10965 // Ignore the event if disabled; this happens in IE11 10966 // under some circumstances, see #3666. 10967 if (!this._enabled) { return; } 10968 this.finishDrag(); 10969 }, 10970 10971 finishDrag: function (noInertia) { 10972 removeClass(document.body, 'leaflet-dragging'); 10973 10974 if (this._lastTarget) { 10975 removeClass(this._lastTarget, 'leaflet-drag-target'); 10976 this._lastTarget = null; 10977 } 10978 10979 off(document, 'mousemove touchmove', this._onMove, this); 10980 off(document, 'mouseup touchend touchcancel', this._onUp, this); 10981 10982 enableImageDrag(); 10983 enableTextSelection(); 10984 10985 var fireDragend = this._moved && this._moving; 10986 10987 this._moving = false; 10988 Draggable._dragging = false; 10989 10990 if (fireDragend) { 10991 // @event dragend: DragEndEvent 10992 // Fired when the drag ends. 10993 this.fire('dragend', { 10994 noInertia: noInertia, 10995 distance: this._newPos.distanceTo(this._startPos) 10996 }); 10997 } 10998 } 10999 11000 }); 11001 11002 /* 11003 * @namespace PolyUtil 11004 * Various utility functions for polygon geometries. 11005 */ 11006 11007 /* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[] 11008 * Clips the polygon geometry defined by the given `points` by the given bounds (using the [Sutherland-Hodgman algorithm](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm)). 11009 * Used by Leaflet to only show polygon points that are on the screen or near, increasing 11010 * performance. Note that polygon points needs different algorithm for clipping 11011 * than polyline, so there's a separate method for it. 11012 */ 11013 function clipPolygon(points, bounds, round) { 11014 var clippedPoints, 11015 edges = [1, 4, 2, 8], 11016 i, j, k, 11017 a, b, 11018 len, edge, p; 11019 11020 for (i = 0, len = points.length; i < len; i++) { 11021 points[i]._code = _getBitCode(points[i], bounds); 11022 } 11023 11024 // for each edge (left, bottom, right, top) 11025 for (k = 0; k < 4; k++) { 11026 edge = edges[k]; 11027 clippedPoints = []; 11028 11029 for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { 11030 a = points[i]; 11031 b = points[j]; 11032 11033 // if a is inside the clip window 11034 if (!(a._code & edge)) { 11035 // if b is outside the clip window (a->b goes out of screen) 11036 if (b._code & edge) { 11037 p = _getEdgeIntersection(b, a, edge, bounds, round); 11038 p._code = _getBitCode(p, bounds); 11039 clippedPoints.push(p); 11040 } 11041 clippedPoints.push(a); 11042 11043 // else if b is inside the clip window (a->b enters the screen) 11044 } else if (!(b._code & edge)) { 11045 p = _getEdgeIntersection(b, a, edge, bounds, round); 11046 p._code = _getBitCode(p, bounds); 11047 clippedPoints.push(p); 11048 } 11049 } 11050 points = clippedPoints; 11051 } 11052 11053 return points; 11054 } 11055 11056 /* @function polygonCenter(latlngs: LatLng[], crs: CRS): LatLng 11057 * Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the passed LatLngs (first ring) from a polygon. 11058 */ 11059 function polygonCenter(latlngs, crs) { 11060 var i, j, p1, p2, f, area, x, y, center; 11061 11062 if (!latlngs || latlngs.length === 0) { 11063 throw new Error('latlngs not passed'); 11064 } 11065 11066 if (!isFlat(latlngs)) { 11067 console.warn('latlngs are not flat! Only the first ring will be used'); 11068 latlngs = latlngs[0]; 11069 } 11070 11071 var centroidLatLng = toLatLng([0, 0]); 11072 11073 var bounds = toLatLngBounds(latlngs); 11074 var areaBounds = bounds.getNorthWest().distanceTo(bounds.getSouthWest()) * bounds.getNorthEast().distanceTo(bounds.getNorthWest()); 11075 // tests showed that below 1700 rounding errors are happening 11076 if (areaBounds < 1700) { 11077 // getting a inexact center, to move the latlngs near to [0, 0] to prevent rounding errors 11078 centroidLatLng = centroid(latlngs); 11079 } 11080 11081 var len = latlngs.length; 11082 var points = []; 11083 for (i = 0; i < len; i++) { 11084 var latlng = toLatLng(latlngs[i]); 11085 points.push(crs.project(toLatLng([latlng.lat - centroidLatLng.lat, latlng.lng - centroidLatLng.lng]))); 11086 } 11087 11088 area = x = y = 0; 11089 11090 // polygon centroid algorithm; 11091 for (i = 0, j = len - 1; i < len; j = i++) { 11092 p1 = points[i]; 11093 p2 = points[j]; 11094 11095 f = p1.y * p2.x - p2.y * p1.x; 11096 x += (p1.x + p2.x) * f; 11097 y += (p1.y + p2.y) * f; 11098 area += f * 3; 11099 } 11100 11101 if (area === 0) { 11102 // Polygon is so small that all points are on same pixel. 11103 center = points[0]; 11104 } else { 11105 center = [x / area, y / area]; 11106 } 11107 11108 var latlngCenter = crs.unproject(toPoint(center)); 11109 return toLatLng([latlngCenter.lat + centroidLatLng.lat, latlngCenter.lng + centroidLatLng.lng]); 11110 } 11111 11112 /* @function centroid(latlngs: LatLng[]): LatLng 11113 * Returns the 'center of mass' of the passed LatLngs. 11114 */ 11115 function centroid(coords) { 11116 var latSum = 0; 11117 var lngSum = 0; 11118 var len = 0; 11119 for (var i = 0; i < coords.length; i++) { 11120 var latlng = toLatLng(coords[i]); 11121 latSum += latlng.lat; 11122 lngSum += latlng.lng; 11123 len++; 11124 } 11125 return toLatLng([latSum / len, lngSum / len]); 11126 } 11127 11128 var PolyUtil = { 11129 __proto__: null, 11130 clipPolygon: clipPolygon, 11131 polygonCenter: polygonCenter, 11132 centroid: centroid 11133 }; 11134 11135 /* 11136 * @namespace LineUtil 11137 * 11138 * Various utility functions for polyline points processing, used by Leaflet internally to make polylines lightning-fast. 11139 */ 11140 11141 // Simplify polyline with vertex reduction and Douglas-Peucker simplification. 11142 // Improves rendering performance dramatically by lessening the number of points to draw. 11143 11144 // @function simplify(points: Point[], tolerance: Number): Point[] 11145 // Dramatically reduces the number of points in a polyline while retaining 11146 // its shape and returns a new array of simplified points, using the 11147 // [Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm). 11148 // Used for a huge performance boost when processing/displaying Leaflet polylines for 11149 // each zoom level and also reducing visual noise. tolerance affects the amount of 11150 // simplification (lesser value means higher quality but slower and with more points). 11151 // Also released as a separated micro-library [Simplify.js](https://mourner.github.io/simplify-js/). 11152 function simplify(points, tolerance) { 11153 if (!tolerance || !points.length) { 11154 return points.slice(); 11155 } 11156 11157 var sqTolerance = tolerance * tolerance; 11158 11159 // stage 1: vertex reduction 11160 points = _reducePoints(points, sqTolerance); 11161 11162 // stage 2: Douglas-Peucker simplification 11163 points = _simplifyDP(points, sqTolerance); 11164 11165 return points; 11166 } 11167 11168 // @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number 11169 // Returns the distance between point `p` and segment `p1` to `p2`. 11170 function pointToSegmentDistance(p, p1, p2) { 11171 return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true)); 11172 } 11173 11174 // @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number 11175 // Returns the closest point from a point `p` on a segment `p1` to `p2`. 11176 function closestPointOnSegment(p, p1, p2) { 11177 return _sqClosestPointOnSegment(p, p1, p2); 11178 } 11179 11180 // Ramer-Douglas-Peucker simplification, see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm 11181 function _simplifyDP(points, sqTolerance) { 11182 11183 var len = points.length, 11184 ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, 11185 markers = new ArrayConstructor(len); 11186 11187 markers[0] = markers[len - 1] = 1; 11188 11189 _simplifyDPStep(points, markers, sqTolerance, 0, len - 1); 11190 11191 var i, 11192 newPoints = []; 11193 11194 for (i = 0; i < len; i++) { 11195 if (markers[i]) { 11196 newPoints.push(points[i]); 11197 } 11198 } 11199 11200 return newPoints; 11201 } 11202 11203 function _simplifyDPStep(points, markers, sqTolerance, first, last) { 11204 11205 var maxSqDist = 0, 11206 index, i, sqDist; 11207 11208 for (i = first + 1; i <= last - 1; i++) { 11209 sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true); 11210 11211 if (sqDist > maxSqDist) { 11212 index = i; 11213 maxSqDist = sqDist; 11214 } 11215 } 11216 11217 if (maxSqDist > sqTolerance) { 11218 markers[index] = 1; 11219 11220 _simplifyDPStep(points, markers, sqTolerance, first, index); 11221 _simplifyDPStep(points, markers, sqTolerance, index, last); 11222 } 11223 } 11224 11225 // reduce points that are too close to each other to a single point 11226 function _reducePoints(points, sqTolerance) { 11227 var reducedPoints = [points[0]]; 11228 11229 for (var i = 1, prev = 0, len = points.length; i < len; i++) { 11230 if (_sqDist(points[i], points[prev]) > sqTolerance) { 11231 reducedPoints.push(points[i]); 11232 prev = i; 11233 } 11234 } 11235 if (prev < len - 1) { 11236 reducedPoints.push(points[len - 1]); 11237 } 11238 return reducedPoints; 11239 } 11240 11241 var _lastCode; 11242 11243 // @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean 11244 // Clips the segment a to b by rectangular bounds with the 11245 // [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm) 11246 // (modifying the segment points directly!). Used by Leaflet to only show polyline 11247 // points that are on the screen or near, increasing performance. 11248 function clipSegment(a, b, bounds, useLastCode, round) { 11249 var codeA = useLastCode ? _lastCode : _getBitCode(a, bounds), 11250 codeB = _getBitCode(b, bounds), 11251 11252 codeOut, p, newCode; 11253 11254 // save 2nd code to avoid calculating it on the next segment 11255 _lastCode = codeB; 11256 11257 while (true) { 11258 // if a,b is inside the clip window (trivial accept) 11259 if (!(codeA | codeB)) { 11260 return [a, b]; 11261 } 11262 11263 // if a,b is outside the clip window (trivial reject) 11264 if (codeA & codeB) { 11265 return false; 11266 } 11267 11268 // other cases 11269 codeOut = codeA || codeB; 11270 p = _getEdgeIntersection(a, b, codeOut, bounds, round); 11271 newCode = _getBitCode(p, bounds); 11272 11273 if (codeOut === codeA) { 11274 a = p; 11275 codeA = newCode; 11276 } else { 11277 b = p; 11278 codeB = newCode; 11279 } 11280 } 11281 } 11282 11283 function _getEdgeIntersection(a, b, code, bounds, round) { 11284 var dx = b.x - a.x, 11285 dy = b.y - a.y, 11286 min = bounds.min, 11287 max = bounds.max, 11288 x, y; 11289 11290 if (code & 8) { // top 11291 x = a.x + dx * (max.y - a.y) / dy; 11292 y = max.y; 11293 11294 } else if (code & 4) { // bottom 11295 x = a.x + dx * (min.y - a.y) / dy; 11296 y = min.y; 11297 11298 } else if (code & 2) { // right 11299 x = max.x; 11300 y = a.y + dy * (max.x - a.x) / dx; 11301 11302 } else if (code & 1) { // left 11303 x = min.x; 11304 y = a.y + dy * (min.x - a.x) / dx; 11305 } 11306 11307 return new Point(x, y, round); 11308 } 11309 11310 function _getBitCode(p, bounds) { 11311 var code = 0; 11312 11313 if (p.x < bounds.min.x) { // left 11314 code |= 1; 11315 } else if (p.x > bounds.max.x) { // right 11316 code |= 2; 11317 } 11318 11319 if (p.y < bounds.min.y) { // bottom 11320 code |= 4; 11321 } else if (p.y > bounds.max.y) { // top 11322 code |= 8; 11323 } 11324 11325 return code; 11326 } 11327 11328 // square distance (to avoid unnecessary Math.sqrt calls) 11329 function _sqDist(p1, p2) { 11330 var dx = p2.x - p1.x, 11331 dy = p2.y - p1.y; 11332 return dx * dx + dy * dy; 11333 } 11334 11335 // return closest point on segment or distance to that point 11336 function _sqClosestPointOnSegment(p, p1, p2, sqDist) { 11337 var x = p1.x, 11338 y = p1.y, 11339 dx = p2.x - x, 11340 dy = p2.y - y, 11341 dot = dx * dx + dy * dy, 11342 t; 11343 11344 if (dot > 0) { 11345 t = ((p.x - x) * dx + (p.y - y) * dy) / dot; 11346 11347 if (t > 1) { 11348 x = p2.x; 11349 y = p2.y; 11350 } else if (t > 0) { 11351 x += dx * t; 11352 y += dy * t; 11353 } 11354 } 11355 11356 dx = p.x - x; 11357 dy = p.y - y; 11358 11359 return sqDist ? dx * dx + dy * dy : new Point(x, y); 11360 } 11361 11362 11363 // @function isFlat(latlngs: LatLng[]): Boolean 11364 // Returns true if `latlngs` is a flat array, false is nested. 11365 function isFlat(latlngs) { 11366 return !isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined'); 11367 } 11368 11369 function _flat(latlngs) { 11370 console.warn('Deprecated use of _flat, please use L.LineUtil.isFlat instead.'); 11371 return isFlat(latlngs); 11372 } 11373 11374 /* @function polylineCenter(latlngs: LatLng[], crs: CRS): LatLng 11375 * Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the passed LatLngs (first ring) from a polyline. 11376 */ 11377 function polylineCenter(latlngs, crs) { 11378 var i, halfDist, segDist, dist, p1, p2, ratio, center; 11379 11380 if (!latlngs || latlngs.length === 0) { 11381 throw new Error('latlngs not passed'); 11382 } 11383 11384 if (!isFlat(latlngs)) { 11385 console.warn('latlngs are not flat! Only the first ring will be used'); 11386 latlngs = latlngs[0]; 11387 } 11388 11389 var centroidLatLng = toLatLng([0, 0]); 11390 11391 var bounds = toLatLngBounds(latlngs); 11392 var areaBounds = bounds.getNorthWest().distanceTo(bounds.getSouthWest()) * bounds.getNorthEast().distanceTo(bounds.getNorthWest()); 11393 // tests showed that below 1700 rounding errors are happening 11394 if (areaBounds < 1700) { 11395 // getting a inexact center, to move the latlngs near to [0, 0] to prevent rounding errors 11396 centroidLatLng = centroid(latlngs); 11397 } 11398 11399 var len = latlngs.length; 11400 var points = []; 11401 for (i = 0; i < len; i++) { 11402 var latlng = toLatLng(latlngs[i]); 11403 points.push(crs.project(toLatLng([latlng.lat - centroidLatLng.lat, latlng.lng - centroidLatLng.lng]))); 11404 } 11405 11406 for (i = 0, halfDist = 0; i < len - 1; i++) { 11407 halfDist += points[i].distanceTo(points[i + 1]) / 2; 11408 } 11409 11410 // The line is so small in the current view that all points are on the same pixel. 11411 if (halfDist === 0) { 11412 center = points[0]; 11413 } else { 11414 for (i = 0, dist = 0; i < len - 1; i++) { 11415 p1 = points[i]; 11416 p2 = points[i + 1]; 11417 segDist = p1.distanceTo(p2); 11418 dist += segDist; 11419 11420 if (dist > halfDist) { 11421 ratio = (dist - halfDist) / segDist; 11422 center = [ 11423 p2.x - ratio * (p2.x - p1.x), 11424 p2.y - ratio * (p2.y - p1.y) 11425 ]; 11426 break; 11427 } 11428 } 11429 } 11430 11431 var latlngCenter = crs.unproject(toPoint(center)); 11432 return toLatLng([latlngCenter.lat + centroidLatLng.lat, latlngCenter.lng + centroidLatLng.lng]); 11433 } 11434 11435 var LineUtil = { 11436 __proto__: null, 11437 simplify: simplify, 11438 pointToSegmentDistance: pointToSegmentDistance, 11439 closestPointOnSegment: closestPointOnSegment, 11440 clipSegment: clipSegment, 11441 _getEdgeIntersection: _getEdgeIntersection, 11442 _getBitCode: _getBitCode, 11443 _sqClosestPointOnSegment: _sqClosestPointOnSegment, 11444 isFlat: isFlat, 11445 _flat: _flat, 11446 polylineCenter: polylineCenter 11447 }; 11448 11449 /* 11450 * @namespace Projection 11451 * @section 11452 * Leaflet comes with a set of already defined Projections out of the box: 11453 * 11454 * @projection L.Projection.LonLat 11455 * 11456 * Equirectangular, or Plate Carree projection â the most simple projection, 11457 * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as 11458 * latitude. Also suitable for flat worlds, e.g. game maps. Used by the 11459 * `EPSG:4326` and `Simple` CRS. 11460 */ 11461 11462 var LonLat = { 11463 project: function (latlng) { 11464 return new Point(latlng.lng, latlng.lat); 11465 }, 11466 11467 unproject: function (point) { 11468 return new LatLng(point.y, point.x); 11469 }, 11470 11471 bounds: new Bounds([-180, -90], [180, 90]) 11472 }; 11473 11474 /* 11475 * @namespace Projection 11476 * @projection L.Projection.Mercator 11477 * 11478 * Elliptical Mercator projection â more complex than Spherical Mercator. Assumes that Earth is an ellipsoid. Used by the EPSG:3395 CRS. 11479 */ 11480 11481 var Mercator = { 11482 R: 6378137, 11483 R_MINOR: 6356752.314245179, 11484 11485 bounds: new Bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), 11486 11487 project: function (latlng) { 11488 var d = Math.PI / 180, 11489 r = this.R, 11490 y = latlng.lat * d, 11491 tmp = this.R_MINOR / r, 11492 e = Math.sqrt(1 - tmp * tmp), 11493 con = e * Math.sin(y); 11494 11495 var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2); 11496 y = -r * Math.log(Math.max(ts, 1E-10)); 11497 11498 return new Point(latlng.lng * d * r, y); 11499 }, 11500 11501 unproject: function (point) { 11502 var d = 180 / Math.PI, 11503 r = this.R, 11504 tmp = this.R_MINOR / r, 11505 e = Math.sqrt(1 - tmp * tmp), 11506 ts = Math.exp(-point.y / r), 11507 phi = Math.PI / 2 - 2 * Math.atan(ts); 11508 11509 for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) { 11510 con = e * Math.sin(phi); 11511 con = Math.pow((1 - con) / (1 + con), e / 2); 11512 dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi; 11513 phi += dphi; 11514 } 11515 11516 return new LatLng(phi * d, point.x * d / r); 11517 } 11518 }; 11519 11520 /* 11521 * @class Projection 11522 11523 * An object with methods for projecting geographical coordinates of the world onto 11524 * a flat surface (and back). See [Map projection](https://en.wikipedia.org/wiki/Map_projection). 11525 11526 * @property bounds: Bounds 11527 * The bounds (specified in CRS units) where the projection is valid 11528 11529 * @method project(latlng: LatLng): Point 11530 * Projects geographical coordinates into a 2D point. 11531 * Only accepts actual `L.LatLng` instances, not arrays. 11532 11533 * @method unproject(point: Point): LatLng 11534 * The inverse of `project`. Projects a 2D point into a geographical location. 11535 * Only accepts actual `L.Point` instances, not arrays. 11536 11537 * Note that the projection instances do not inherit from Leaflet's `Class` object, 11538 * and can't be instantiated. Also, new classes can't inherit from them, 11539 * and methods can't be added to them with the `include` function. 11540 11541 */ 11542 11543 var index = { 11544 __proto__: null, 11545 LonLat: LonLat, 11546 Mercator: Mercator, 11547 SphericalMercator: SphericalMercator 11548 }; 11549 11550 /* 11551 * @namespace CRS 11552 * @crs L.CRS.EPSG3395 11553 * 11554 * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection. 11555 */ 11556 var EPSG3395 = extend({}, Earth, { 11557 code: 'EPSG:3395', 11558 projection: Mercator, 11559 11560 transformation: (function () { 11561 var scale = 0.5 / (Math.PI * Mercator.R); 11562 return toTransformation(scale, 0.5, -scale, 0.5); 11563 }()) 11564 }); 11565 11566 /* 11567 * @namespace CRS 11568 * @crs L.CRS.EPSG4326 11569 * 11570 * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. 11571 * 11572 * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic), 11573 * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer` 11574 * with this CRS, ensure that there are two 256x256 pixel tiles covering the 11575 * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90), 11576 * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set. 11577 */ 11578 11579 var EPSG4326 = extend({}, Earth, { 11580 code: 'EPSG:4326', 11581 projection: LonLat, 11582 transformation: toTransformation(1 / 180, 1, -1 / 180, 0.5) 11583 }); 11584 11585 /* 11586 * @namespace CRS 11587 * @crs L.CRS.Simple 11588 * 11589 * A simple CRS that maps longitude and latitude into `x` and `y` directly. 11590 * May be used for maps of flat surfaces (e.g. game maps). Note that the `y` 11591 * axis should still be inverted (going from bottom to top). `distance()` returns 11592 * simple euclidean distance. 11593 */ 11594 11595 var Simple = extend({}, CRS, { 11596 projection: LonLat, 11597 transformation: toTransformation(1, 0, -1, 0), 11598 11599 scale: function (zoom) { 11600 return Math.pow(2, zoom); 11601 }, 11602 11603 zoom: function (scale) { 11604 return Math.log(scale) / Math.LN2; 11605 }, 11606 11607 distance: function (latlng1, latlng2) { 11608 var dx = latlng2.lng - latlng1.lng, 11609 dy = latlng2.lat - latlng1.lat; 11610 11611 return Math.sqrt(dx * dx + dy * dy); 11612 }, 11613 11614 infinite: true 11615 }); 11616 11617 CRS.Earth = Earth; 11618 CRS.EPSG3395 = EPSG3395; 11619 CRS.EPSG3857 = EPSG3857; 11620 CRS.EPSG900913 = EPSG900913; 11621 CRS.EPSG4326 = EPSG4326; 11622 CRS.Simple = Simple; 11623 11624 /* 11625 * @class Layer 11626 * @inherits Evented 11627 * @aka L.Layer 11628 * @aka ILayer 11629 * 11630 * A set of methods from the Layer base class that all Leaflet layers use. 11631 * Inherits all methods, options and events from `L.Evented`. 11632 * 11633 * @example 11634 * 11635 * ```js 11636 * var layer = L.marker(latlng).addTo(map); 11637 * layer.addTo(map); 11638 * layer.remove(); 11639 * ``` 11640 * 11641 * @event add: Event 11642 * Fired after the layer is added to a map 11643 * 11644 * @event remove: Event 11645 * Fired after the layer is removed from a map 11646 */ 11647 11648 11649 var Layer = Evented.extend({ 11650 11651 // Classes extending `L.Layer` will inherit the following options: 11652 options: { 11653 // @option pane: String = 'overlayPane' 11654 // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default. 11655 pane: 'overlayPane', 11656 11657 // @option attribution: String = null 11658 // String to be shown in the attribution control, e.g. "© OpenStreetMap contributors". It describes the layer data and is often a legal obligation towards copyright holders and tile providers. 11659 attribution: null, 11660 11661 bubblingMouseEvents: true 11662 }, 11663 11664 /* @section 11665 * Classes extending `L.Layer` will inherit the following methods: 11666 * 11667 * @method addTo(map: Map|LayerGroup): this 11668 * Adds the layer to the given map or layer group. 11669 */ 11670 addTo: function (map) { 11671 map.addLayer(this); 11672 return this; 11673 }, 11674 11675 // @method remove: this 11676 // Removes the layer from the map it is currently active on. 11677 remove: function () { 11678 return this.removeFrom(this._map || this._mapToAdd); 11679 }, 11680 11681 // @method removeFrom(map: Map): this 11682 // Removes the layer from the given map 11683 // 11684 // @alternative 11685 // @method removeFrom(group: LayerGroup): this 11686 // Removes the layer from the given `LayerGroup` 11687 removeFrom: function (obj) { 11688 if (obj) { 11689 obj.removeLayer(this); 11690 } 11691 return this; 11692 }, 11693 11694 // @method getPane(name? : String): HTMLElement 11695 // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer. 11696 getPane: function (name) { 11697 return this._map.getPane(name ? (this.options[name] || name) : this.options.pane); 11698 }, 11699 11700 addInteractiveTarget: function (targetEl) { 11701 this._map._targets[stamp(targetEl)] = this; 11702 return this; 11703 }, 11704 11705 removeInteractiveTarget: function (targetEl) { 11706 delete this._map._targets[stamp(targetEl)]; 11707 return this; 11708 }, 11709 11710 // @method getAttribution: String 11711 // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution). 11712 getAttribution: function () { 11713 return this.options.attribution; 11714 }, 11715 11716 _layerAdd: function (e) { 11717 var map = e.target; 11718 11719 // check in case layer gets added and then removed before the map is ready 11720 if (!map.hasLayer(this)) { return; } 11721 11722 this._map = map; 11723 this._zoomAnimated = map._zoomAnimated; 11724 11725 if (this.getEvents) { 11726 var events = this.getEvents(); 11727 map.on(events, this); 11728 this.once('remove', function () { 11729 map.off(events, this); 11730 }, this); 11731 } 11732 11733 this.onAdd(map); 11734 11735 this.fire('add'); 11736 map.fire('layeradd', {layer: this}); 11737 } 11738 }); 11739 11740 /* @section Extension methods 11741 * @uninheritable 11742 * 11743 * Every layer should extend from `L.Layer` and (re-)implement the following methods. 11744 * 11745 * @method onAdd(map: Map): this 11746 * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer). 11747 * 11748 * @method onRemove(map: Map): this 11749 * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer). 11750 * 11751 * @method getEvents(): Object 11752 * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer. 11753 * 11754 * @method getAttribution(): String 11755 * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible. 11756 * 11757 * @method beforeAdd(map: Map): this 11758 * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only. 11759 */ 11760 11761 11762 /* @namespace Map 11763 * @section Layer events 11764 * 11765 * @event layeradd: LayerEvent 11766 * Fired when a new layer is added to the map. 11767 * 11768 * @event layerremove: LayerEvent 11769 * Fired when some layer is removed from the map 11770 * 11771 * @section Methods for Layers and Controls 11772 */ 11773 Map.include({ 11774 // @method addLayer(layer: Layer): this 11775 // Adds the given layer to the map 11776 addLayer: function (layer) { 11777 if (!layer._layerAdd) { 11778 throw new Error('The provided object is not a Layer.'); 11779 } 11780 11781 var id = stamp(layer); 11782 if (this._layers[id]) { return this; } 11783 this._layers[id] = layer; 11784 11785 layer._mapToAdd = this; 11786 11787 if (layer.beforeAdd) { 11788 layer.beforeAdd(this); 11789 } 11790 11791 this.whenReady(layer._layerAdd, layer); 11792 11793 return this; 11794 }, 11795 11796 // @method removeLayer(layer: Layer): this 11797 // Removes the given layer from the map. 11798 removeLayer: function (layer) { 11799 var id = stamp(layer); 11800 11801 if (!this._layers[id]) { return this; } 11802 11803 if (this._loaded) { 11804 layer.onRemove(this); 11805 } 11806 11807 delete this._layers[id]; 11808 11809 if (this._loaded) { 11810 this.fire('layerremove', {layer: layer}); 11811 layer.fire('remove'); 11812 } 11813 11814 layer._map = layer._mapToAdd = null; 11815 11816 return this; 11817 }, 11818 11819 // @method hasLayer(layer: Layer): Boolean 11820 // Returns `true` if the given layer is currently added to the map 11821 hasLayer: function (layer) { 11822 return stamp(layer) in this._layers; 11823 }, 11824 11825 /* @method eachLayer(fn: Function, context?: Object): this 11826 * Iterates over the layers of the map, optionally specifying context of the iterator function. 11827 * ``` 11828 * map.eachLayer(function(layer){ 11829 * layer.bindPopup('Hello'); 11830 * }); 11831 * ``` 11832 */ 11833 eachLayer: function (method, context) { 11834 for (var i in this._layers) { 11835 method.call(context, this._layers[i]); 11836 } 11837 return this; 11838 }, 11839 11840 _addLayers: function (layers) { 11841 layers = layers ? (isArray(layers) ? layers : [layers]) : []; 11842 11843 for (var i = 0, len = layers.length; i < len; i++) { 11844 this.addLayer(layers[i]); 11845 } 11846 }, 11847 11848 _addZoomLimit: function (layer) { 11849 if (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) { 11850 this._zoomBoundLayers[stamp(layer)] = layer; 11851 this._updateZoomLevels(); 11852 } 11853 }, 11854 11855 _removeZoomLimit: function (layer) { 11856 var id = stamp(layer); 11857 11858 if (this._zoomBoundLayers[id]) { 11859 delete this._zoomBoundLayers[id]; 11860 this._updateZoomLevels(); 11861 } 11862 }, 11863 11864 _updateZoomLevels: function () { 11865 var minZoom = Infinity, 11866 maxZoom = -Infinity, 11867 oldZoomSpan = this._getZoomSpan(); 11868 11869 for (var i in this._zoomBoundLayers) { 11870 var options = this._zoomBoundLayers[i].options; 11871 11872 minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom); 11873 maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom); 11874 } 11875 11876 this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom; 11877 this._layersMinZoom = minZoom === Infinity ? undefined : minZoom; 11878 11879 // @section Map state change events 11880 // @event zoomlevelschange: Event 11881 // Fired when the number of zoomlevels on the map is changed due 11882 // to adding or removing a layer. 11883 if (oldZoomSpan !== this._getZoomSpan()) { 11884 this.fire('zoomlevelschange'); 11885 } 11886 11887 if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) { 11888 this.setZoom(this._layersMaxZoom); 11889 } 11890 if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) { 11891 this.setZoom(this._layersMinZoom); 11892 } 11893 } 11894 }); 11895 11896 /* 11897 * @class LayerGroup 11898 * @aka L.LayerGroup 11899 * @inherits Interactive layer 11900 * 11901 * Used to group several layers and handle them as one. If you add it to the map, 11902 * any layers added or removed from the group will be added/removed on the map as 11903 * well. Extends `Layer`. 11904 * 11905 * @example 11906 * 11907 * ```js 11908 * L.layerGroup([marker1, marker2]) 11909 * .addLayer(polyline) 11910 * .addTo(map); 11911 * ``` 11912 */ 11913 11914 var LayerGroup = Layer.extend({ 11915 11916 initialize: function (layers, options) { 11917 setOptions(this, options); 11918 11919 this._layers = {}; 11920 11921 var i, len; 11922 11923 if (layers) { 11924 for (i = 0, len = layers.length; i < len; i++) { 11925 this.addLayer(layers[i]); 11926 } 11927 } 11928 }, 11929 11930 // @method addLayer(layer: Layer): this 11931 // Adds the given layer to the group. 11932 addLayer: function (layer) { 11933 var id = this.getLayerId(layer); 11934 11935 this._layers[id] = layer; 11936 11937 if (this._map) { 11938 this._map.addLayer(layer); 11939 } 11940 11941 return this; 11942 }, 11943 11944 // @method removeLayer(layer: Layer): this 11945 // Removes the given layer from the group. 11946 // @alternative 11947 // @method removeLayer(id: Number): this 11948 // Removes the layer with the given internal ID from the group. 11949 removeLayer: function (layer) { 11950 var id = layer in this._layers ? layer : this.getLayerId(layer); 11951 11952 if (this._map && this._layers[id]) { 11953 this._map.removeLayer(this._layers[id]); 11954 } 11955 11956 delete this._layers[id]; 11957 11958 return this; 11959 }, 11960 11961 // @method hasLayer(layer: Layer): Boolean 11962 // Returns `true` if the given layer is currently added to the group. 11963 // @alternative 11964 // @method hasLayer(id: Number): Boolean 11965 // Returns `true` if the given internal ID is currently added to the group. 11966 hasLayer: function (layer) { 11967 var layerId = typeof layer === 'number' ? layer : this.getLayerId(layer); 11968 return layerId in this._layers; 11969 }, 11970 11971 // @method clearLayers(): this 11972 // Removes all the layers from the group. 11973 clearLayers: function () { 11974 return this.eachLayer(this.removeLayer, this); 11975 }, 11976 11977 // @method invoke(methodName: String, â¦): this 11978 // Calls `methodName` on every layer contained in this group, passing any 11979 // additional parameters. Has no effect if the layers contained do not 11980 // implement `methodName`. 11981 invoke: function (methodName) { 11982 var args = Array.prototype.slice.call(arguments, 1), 11983 i, layer; 11984 11985 for (i in this._layers) { 11986 layer = this._layers[i]; 11987 11988 if (layer[methodName]) { 11989 layer[methodName].apply(layer, args); 11990 } 11991 } 11992 11993 return this; 11994 }, 11995 11996 onAdd: function (map) { 11997 this.eachLayer(map.addLayer, map); 11998 }, 11999 12000 onRemove: function (map) { 12001 this.eachLayer(map.removeLayer, map); 12002 }, 12003 12004 // @method eachLayer(fn: Function, context?: Object): this 12005 // Iterates over the layers of the group, optionally specifying context of the iterator function. 12006 // ```js 12007 // group.eachLayer(function (layer) { 12008 // layer.bindPopup('Hello'); 12009 // }); 12010 // ``` 12011 eachLayer: function (method, context) { 12012 for (var i in this._layers) { 12013 method.call(context, this._layers[i]); 12014 } 12015 return this; 12016 }, 12017 12018 // @method getLayer(id: Number): Layer 12019 // Returns the layer with the given internal ID. 12020 getLayer: function (id) { 12021 return this._layers[id]; 12022 }, 12023 12024 // @method getLayers(): Layer[] 12025 // Returns an array of all the layers added to the group. 12026 getLayers: function () { 12027 var layers = []; 12028 this.eachLayer(layers.push, layers); 12029 return layers; 12030 }, 12031 12032 // @method setZIndex(zIndex: Number): this 12033 // Calls `setZIndex` on every layer contained in this group, passing the z-index. 12034 setZIndex: function (zIndex) { 12035 return this.invoke('setZIndex', zIndex); 12036 }, 12037 12038 // @method getLayerId(layer: Layer): Number 12039 // Returns the internal ID for a layer 12040 getLayerId: function (layer) { 12041 return stamp(layer); 12042 } 12043 }); 12044 12045 12046 // @factory L.layerGroup(layers?: Layer[], options?: Object) 12047 // Create a layer group, optionally given an initial set of layers and an `options` object. 12048 var layerGroup = function (layers, options) { 12049 return new LayerGroup(layers, options); 12050 }; 12051 12052 /* 12053 * @class FeatureGroup 12054 * @aka L.FeatureGroup 12055 * @inherits LayerGroup 12056 * 12057 * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers: 12058 * * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip)) 12059 * * Events are propagated to the `FeatureGroup`, so if the group has an event 12060 * handler, it will handle events from any of the layers. This includes mouse events 12061 * and custom events. 12062 * * Has `layeradd` and `layerremove` events 12063 * 12064 * @example 12065 * 12066 * ```js 12067 * L.featureGroup([marker1, marker2, polyline]) 12068 * .bindPopup('Hello world!') 12069 * .on('click', function() { alert('Clicked on a member of the group!'); }) 12070 * .addTo(map); 12071 * ``` 12072 */ 12073 12074 var FeatureGroup = LayerGroup.extend({ 12075 12076 addLayer: function (layer) { 12077 if (this.hasLayer(layer)) { 12078 return this; 12079 } 12080 12081 layer.addEventParent(this); 12082 12083 LayerGroup.prototype.addLayer.call(this, layer); 12084 12085 // @event layeradd: LayerEvent 12086 // Fired when a layer is added to this `FeatureGroup` 12087 return this.fire('layeradd', {layer: layer}); 12088 }, 12089 12090 removeLayer: function (layer) { 12091 if (!this.hasLayer(layer)) { 12092 return this; 12093 } 12094 if (layer in this._layers) { 12095 layer = this._layers[layer]; 12096 } 12097 12098 layer.removeEventParent(this); 12099 12100 LayerGroup.prototype.removeLayer.call(this, layer); 12101 12102 // @event layerremove: LayerEvent 12103 // Fired when a layer is removed from this `FeatureGroup` 12104 return this.fire('layerremove', {layer: layer}); 12105 }, 12106 12107 // @method setStyle(style: Path options): this 12108 // Sets the given path options to each layer of the group that has a `setStyle` method. 12109 setStyle: function (style) { 12110 return this.invoke('setStyle', style); 12111 }, 12112 12113 // @method bringToFront(): this 12114 // Brings the layer group to the top of all other layers 12115 bringToFront: function () { 12116 return this.invoke('bringToFront'); 12117 }, 12118 12119 // @method bringToBack(): this 12120 // Brings the layer group to the back of all other layers 12121 bringToBack: function () { 12122 return this.invoke('bringToBack'); 12123 }, 12124 12125 // @method getBounds(): LatLngBounds 12126 // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children). 12127 getBounds: function () { 12128 var bounds = new LatLngBounds(); 12129 12130 for (var id in this._layers) { 12131 var layer = this._layers[id]; 12132 bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng()); 12133 } 12134 return bounds; 12135 } 12136 }); 12137 12138 // @factory L.featureGroup(layers?: Layer[], options?: Object) 12139 // Create a feature group, optionally given an initial set of layers and an `options` object. 12140 var featureGroup = function (layers, options) { 12141 return new FeatureGroup(layers, options); 12142 }; 12143 12144 /* 12145 * @class Icon 12146 * @aka L.Icon 12147 * 12148 * Represents an icon to provide when creating a marker. 12149 * 12150 * @example 12151 * 12152 * ```js 12153 * var myIcon = L.icon({ 12154 * iconUrl: 'my-icon.png', 12155 * iconRetinaUrl: '[email protected]', 12156 * iconSize: [38, 95], 12157 * iconAnchor: [22, 94], 12158 * popupAnchor: [-3, -76], 12159 * shadowUrl: 'my-icon-shadow.png', 12160 * shadowRetinaUrl: '[email protected]', 12161 * shadowSize: [68, 95], 12162 * shadowAnchor: [22, 94] 12163 * }); 12164 * 12165 * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map); 12166 * ``` 12167 * 12168 * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default. 12169 * 12170 */ 12171 12172 var Icon = Class.extend({ 12173 12174 /* @section 12175 * @aka Icon options 12176 * 12177 * @option iconUrl: String = null 12178 * **(required)** The URL to the icon image (absolute or relative to your script path). 12179 * 12180 * @option iconRetinaUrl: String = null 12181 * The URL to a retina sized version of the icon image (absolute or relative to your 12182 * script path). Used for Retina screen devices. 12183 * 12184 * @option iconSize: Point = null 12185 * Size of the icon image in pixels. 12186 * 12187 * @option iconAnchor: Point = null 12188 * The coordinates of the "tip" of the icon (relative to its top left corner). The icon 12189 * will be aligned so that this point is at the marker's geographical location. Centered 12190 * by default if size is specified, also can be set in CSS with negative margins. 12191 * 12192 * @option popupAnchor: Point = [0, 0] 12193 * The coordinates of the point from which popups will "open", relative to the icon anchor. 12194 * 12195 * @option tooltipAnchor: Point = [0, 0] 12196 * The coordinates of the point from which tooltips will "open", relative to the icon anchor. 12197 * 12198 * @option shadowUrl: String = null 12199 * The URL to the icon shadow image. If not specified, no shadow image will be created. 12200 * 12201 * @option shadowRetinaUrl: String = null 12202 * 12203 * @option shadowSize: Point = null 12204 * Size of the shadow image in pixels. 12205 * 12206 * @option shadowAnchor: Point = null 12207 * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same 12208 * as iconAnchor if not specified). 12209 * 12210 * @option className: String = '' 12211 * A custom class name to assign to both icon and shadow images. Empty by default. 12212 */ 12213 12214 options: { 12215 popupAnchor: [0, 0], 12216 tooltipAnchor: [0, 0], 12217 12218 // @option crossOrigin: Boolean|String = false 12219 // Whether the crossOrigin attribute will be added to the tiles. 12220 // If a String is provided, all tiles will have their crossOrigin attribute set to the String provided. This is needed if you want to access tile pixel data. 12221 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values. 12222 crossOrigin: false 12223 }, 12224 12225 initialize: function (options) { 12226 setOptions(this, options); 12227 }, 12228 12229 // @method createIcon(oldIcon?: HTMLElement): HTMLElement 12230 // Called internally when the icon has to be shown, returns a `<img>` HTML element 12231 // styled according to the options. 12232 createIcon: function (oldIcon) { 12233 return this._createIcon('icon', oldIcon); 12234 }, 12235 12236 // @method createShadow(oldIcon?: HTMLElement): HTMLElement 12237 // As `createIcon`, but for the shadow beneath it. 12238 createShadow: function (oldIcon) { 12239 return this._createIcon('shadow', oldIcon); 12240 }, 12241 12242 _createIcon: function (name, oldIcon) { 12243 var src = this._getIconUrl(name); 12244 12245 if (!src) { 12246 if (name === 'icon') { 12247 throw new Error('iconUrl not set in Icon options (see the docs).'); 12248 } 12249 return null; 12250 } 12251 12252 var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null); 12253 this._setIconStyles(img, name); 12254 12255 if (this.options.crossOrigin || this.options.crossOrigin === '') { 12256 img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin; 12257 } 12258 12259 return img; 12260 }, 12261 12262 _setIconStyles: function (img, name) { 12263 var options = this.options; 12264 var sizeOption = options[name + 'Size']; 12265 12266 if (typeof sizeOption === 'number') { 12267 sizeOption = [sizeOption, sizeOption]; 12268 } 12269 12270 var size = toPoint(sizeOption), 12271 anchor = toPoint(name === 'shadow' && options.shadowAnchor || options.iconAnchor || 12272 size && size.divideBy(2, true)); 12273 12274 img.className = 'leaflet-marker-' + name + ' ' + (options.className || ''); 12275 12276 if (anchor) { 12277 img.style.marginLeft = (-anchor.x) + 'px'; 12278 img.style.marginTop = (-anchor.y) + 'px'; 12279 } 12280 12281 if (size) { 12282 img.style.width = size.x + 'px'; 12283 img.style.height = size.y + 'px'; 12284 } 12285 }, 12286 12287 _createImg: function (src, el) { 12288 el = el || document.createElement('img'); 12289 el.src = src; 12290 return el; 12291 }, 12292 12293 _getIconUrl: function (name) { 12294 return Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url']; 12295 } 12296 }); 12297 12298 12299 // @factory L.icon(options: Icon options) 12300 // Creates an icon instance with the given options. 12301 function icon(options) { 12302 return new Icon(options); 12303 } 12304 12305 /* 12306 * @miniclass Icon.Default (Icon) 12307 * @aka L.Icon.Default 12308 * @section 12309 * 12310 * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when 12311 * no icon is specified. Points to the blue marker image distributed with Leaflet 12312 * releases. 12313 * 12314 * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options` 12315 * (which is a set of `Icon options`). 12316 * 12317 * If you want to _completely_ replace the default icon, override the 12318 * `L.Marker.prototype.options.icon` with your own icon instead. 12319 */ 12320 12321 var IconDefault = Icon.extend({ 12322 12323 options: { 12324 iconUrl: 'marker-icon.png', 12325 iconRetinaUrl: 'marker-icon-2x.png', 12326 shadowUrl: 'marker-shadow.png', 12327 iconSize: [25, 41], 12328 iconAnchor: [12, 41], 12329 popupAnchor: [1, -34], 12330 tooltipAnchor: [16, -28], 12331 shadowSize: [41, 41] 12332 }, 12333 12334 _getIconUrl: function (name) { 12335 if (typeof IconDefault.imagePath !== 'string') { // Deprecated, backwards-compatibility only 12336 IconDefault.imagePath = this._detectIconPath(); 12337 } 12338 12339 // @option imagePath: String 12340 // `Icon.Default` will try to auto-detect the location of the 12341 // blue icon images. If you are placing these images in a non-standard 12342 // way, set this option to point to the right path. 12343 return (this.options.imagePath || IconDefault.imagePath) + Icon.prototype._getIconUrl.call(this, name); 12344 }, 12345 12346 _stripUrl: function (path) { // separate function to use in tests 12347 var strip = function (str, re, idx) { 12348 var match = re.exec(str); 12349 return match && match[idx]; 12350 }; 12351 path = strip(path, /^url\((['"])?(.+)\1\)$/, 2); 12352 return path && strip(path, /^(.*)marker-icon\.png$/, 1); 12353 }, 12354 12355 _detectIconPath: function () { 12356 var el = create$1('div', 'leaflet-default-icon-path', document.body); 12357 var path = getStyle(el, 'background-image') || 12358 getStyle(el, 'backgroundImage'); // IE8 12359 12360 document.body.removeChild(el); 12361 path = this._stripUrl(path); 12362 if (path) { return path; } 12363 var link = document.querySelector('link[href$="leaflet.css"]'); 12364 if (!link) { return ''; } 12365 return link.href.substring(0, link.href.length - 'leaflet.css'.length - 1); 12366 } 12367 }); 12368 12369 /* 12370 * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable. 12371 */ 12372 12373 12374 /* @namespace Marker 12375 * @section Interaction handlers 12376 * 12377 * Interaction handlers are properties of a marker instance that allow you to control interaction behavior in runtime, enabling or disabling certain features such as dragging (see `Handler` methods). Example: 12378 * 12379 * ```js 12380 * marker.dragging.disable(); 12381 * ``` 12382 * 12383 * @property dragging: Handler 12384 * Marker dragging handler (by both mouse and touch). Only valid when the marker is on the map (Otherwise set [`marker.options.draggable`](#marker-draggable)). 12385 */ 12386 12387 var MarkerDrag = Handler.extend({ 12388 initialize: function (marker) { 12389 this._marker = marker; 12390 }, 12391 12392 addHooks: function () { 12393 var icon = this._marker._icon; 12394 12395 if (!this._draggable) { 12396 this._draggable = new Draggable(icon, icon, true); 12397 } 12398 12399 this._draggable.on({ 12400 dragstart: this._onDragStart, 12401 predrag: this._onPreDrag, 12402 drag: this._onDrag, 12403 dragend: this._onDragEnd 12404 }, this).enable(); 12405 12406 addClass(icon, 'leaflet-marker-draggable'); 12407 }, 12408 12409 removeHooks: function () { 12410 this._draggable.off({ 12411 dragstart: this._onDragStart, 12412 predrag: this._onPreDrag, 12413 drag: this._onDrag, 12414 dragend: this._onDragEnd 12415 }, this).disable(); 12416 12417 if (this._marker._icon) { 12418 removeClass(this._marker._icon, 'leaflet-marker-draggable'); 12419 } 12420 }, 12421 12422 moved: function () { 12423 return this._draggable && this._draggable._moved; 12424 }, 12425 12426 _adjustPan: function (e) { 12427 var marker = this._marker, 12428 map = marker._map, 12429 speed = this._marker.options.autoPanSpeed, 12430 padding = this._marker.options.autoPanPadding, 12431 iconPos = getPosition(marker._icon), 12432 bounds = map.getPixelBounds(), 12433 origin = map.getPixelOrigin(); 12434 12435 var panBounds = toBounds( 12436 bounds.min._subtract(origin).add(padding), 12437 bounds.max._subtract(origin).subtract(padding) 12438 ); 12439 12440 if (!panBounds.contains(iconPos)) { 12441 // Compute incremental movement 12442 var movement = toPoint( 12443 (Math.max(panBounds.max.x, iconPos.x) - panBounds.max.x) / (bounds.max.x - panBounds.max.x) - 12444 (Math.min(panBounds.min.x, iconPos.x) - panBounds.min.x) / (bounds.min.x - panBounds.min.x), 12445 12446 (Math.max(panBounds.max.y, iconPos.y) - panBounds.max.y) / (bounds.max.y - panBounds.max.y) - 12447 (Math.min(panBounds.min.y, iconPos.y) - panBounds.min.y) / (bounds.min.y - panBounds.min.y) 12448 ).multiplyBy(speed); 12449 12450 map.panBy(movement, {animate: false}); 12451 12452 this._draggable._newPos._add(movement); 12453 this._draggable._startPos._add(movement); 12454 12455 setPosition(marker._icon, this._draggable._newPos); 12456 this._onDrag(e); 12457 12458 this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e)); 12459 } 12460 }, 12461 12462 _onDragStart: function () { 12463 // @section Dragging events 12464 // @event dragstart: Event 12465 // Fired when the user starts dragging the marker. 12466 12467 // @event movestart: Event 12468 // Fired when the marker starts moving (because of dragging). 12469 12470 this._oldLatLng = this._marker.getLatLng(); 12471 12472 // When using ES6 imports it could not be set when `Popup` was not imported as well 12473 this._marker.closePopup && this._marker.closePopup(); 12474 12475 this._marker 12476 .fire('movestart') 12477 .fire('dragstart'); 12478 }, 12479 12480 _onPreDrag: function (e) { 12481 if (this._marker.options.autoPan) { 12482 cancelAnimFrame(this._panRequest); 12483 this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e)); 12484 } 12485 }, 12486 12487 _onDrag: function (e) { 12488 var marker = this._marker, 12489 shadow = marker._shadow, 12490 iconPos = getPosition(marker._icon), 12491 latlng = marker._map.layerPointToLatLng(iconPos); 12492 12493 // update shadow position 12494 if (shadow) { 12495 setPosition(shadow, iconPos); 12496 } 12497 12498 marker._latlng = latlng; 12499 e.latlng = latlng; 12500 e.oldLatLng = this._oldLatLng; 12501 12502 // @event drag: Event 12503 // Fired repeatedly while the user drags the marker. 12504 marker 12505 .fire('move', e) 12506 .fire('drag', e); 12507 }, 12508 12509 _onDragEnd: function (e) { 12510 // @event dragend: DragEndEvent 12511 // Fired when the user stops dragging the marker. 12512 12513 cancelAnimFrame(this._panRequest); 12514 12515 // @event moveend: Event 12516 // Fired when the marker stops moving (because of dragging). 12517 delete this._oldLatLng; 12518 this._marker 12519 .fire('moveend') 12520 .fire('dragend', e); 12521 } 12522 }); 12523 12524 /* 12525 * @class Marker 12526 * @inherits Interactive layer 12527 * @aka L.Marker 12528 * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`. 12529 * 12530 * @example 12531 * 12532 * ```js 12533 * L.marker([50.5, 30.5]).addTo(map); 12534 * ``` 12535 */ 12536 12537 var Marker = Layer.extend({ 12538 12539 // @section 12540 // @aka Marker options 12541 options: { 12542 // @option icon: Icon = * 12543 // Icon instance to use for rendering the marker. 12544 // See [Icon documentation](#L.Icon) for details on how to customize the marker icon. 12545 // If not specified, a common instance of `L.Icon.Default` is used. 12546 icon: new IconDefault(), 12547 12548 // Option inherited from "Interactive layer" abstract class 12549 interactive: true, 12550 12551 // @option keyboard: Boolean = true 12552 // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter. 12553 keyboard: true, 12554 12555 // @option title: String = '' 12556 // Text for the browser tooltip that appear on marker hover (no tooltip by default). 12557 // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled). 12558 title: '', 12559 12560 // @option alt: String = 'Marker' 12561 // Text for the `alt` attribute of the icon image. 12562 // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled). 12563 alt: 'Marker', 12564 12565 // @option zIndexOffset: Number = 0 12566 // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively). 12567 zIndexOffset: 0, 12568 12569 // @option opacity: Number = 1.0 12570 // The opacity of the marker. 12571 opacity: 1, 12572 12573 // @option riseOnHover: Boolean = false 12574 // If `true`, the marker will get on top of others when you hover the mouse over it. 12575 riseOnHover: false, 12576 12577 // @option riseOffset: Number = 250 12578 // The z-index offset used for the `riseOnHover` feature. 12579 riseOffset: 250, 12580 12581 // @option pane: String = 'markerPane' 12582 // `Map pane` where the markers icon will be added. 12583 pane: 'markerPane', 12584 12585 // @option shadowPane: String = 'shadowPane' 12586 // `Map pane` where the markers shadow will be added. 12587 shadowPane: 'shadowPane', 12588 12589 // @option bubblingMouseEvents: Boolean = false 12590 // When `true`, a mouse event on this marker will trigger the same event on the map 12591 // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used). 12592 bubblingMouseEvents: false, 12593 12594 // @option autoPanOnFocus: Boolean = true 12595 // When `true`, the map will pan whenever the marker is focused (via 12596 // e.g. pressing `tab` on the keyboard) to ensure the marker is 12597 // visible within the map's bounds 12598 autoPanOnFocus: true, 12599 12600 // @section Draggable marker options 12601 // @option draggable: Boolean = false 12602 // Whether the marker is draggable with mouse/touch or not. 12603 draggable: false, 12604 12605 // @option autoPan: Boolean = false 12606 // Whether to pan the map when dragging this marker near its edge or not. 12607 autoPan: false, 12608 12609 // @option autoPanPadding: Point = Point(50, 50) 12610 // Distance (in pixels to the left/right and to the top/bottom) of the 12611 // map edge to start panning the map. 12612 autoPanPadding: [50, 50], 12613 12614 // @option autoPanSpeed: Number = 10 12615 // Number of pixels the map should pan by. 12616 autoPanSpeed: 10 12617 }, 12618 12619 /* @section 12620 * 12621 * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods: 12622 */ 12623 12624 initialize: function (latlng, options) { 12625 setOptions(this, options); 12626 this._latlng = toLatLng(latlng); 12627 }, 12628 12629 onAdd: function (map) { 12630 this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation; 12631 12632 if (this._zoomAnimated) { 12633 map.on('zoomanim', this._animateZoom, this); 12634 } 12635 12636 this._initIcon(); 12637 this.update(); 12638 }, 12639 12640 onRemove: function (map) { 12641 if (this.dragging && this.dragging.enabled()) { 12642 this.options.draggable = true; 12643 this.dragging.removeHooks(); 12644 } 12645 delete this.dragging; 12646 12647 if (this._zoomAnimated) { 12648 map.off('zoomanim', this._animateZoom, this); 12649 } 12650 12651 this._removeIcon(); 12652 this._removeShadow(); 12653 }, 12654 12655 getEvents: function () { 12656 return { 12657 zoom: this.update, 12658 viewreset: this.update 12659 }; 12660 }, 12661 12662 // @method getLatLng: LatLng 12663 // Returns the current geographical position of the marker. 12664 getLatLng: function () { 12665 return this._latlng; 12666 }, 12667 12668 // @method setLatLng(latlng: LatLng): this 12669 // Changes the marker position to the given point. 12670 setLatLng: function (latlng) { 12671 var oldLatLng = this._latlng; 12672 this._latlng = toLatLng(latlng); 12673 this.update(); 12674 12675 // @event move: Event 12676 // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`. 12677 return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng}); 12678 }, 12679 12680 // @method setZIndexOffset(offset: Number): this 12681 // Changes the [zIndex offset](#marker-zindexoffset) of the marker. 12682 setZIndexOffset: function (offset) { 12683 this.options.zIndexOffset = offset; 12684 return this.update(); 12685 }, 12686 12687 // @method getIcon: Icon 12688 // Returns the current icon used by the marker 12689 getIcon: function () { 12690 return this.options.icon; 12691 }, 12692 12693 // @method setIcon(icon: Icon): this 12694 // Changes the marker icon. 12695 setIcon: function (icon) { 12696 12697 this.options.icon = icon; 12698 12699 if (this._map) { 12700 this._initIcon(); 12701 this.update(); 12702 } 12703 12704 if (this._popup) { 12705 this.bindPopup(this._popup, this._popup.options); 12706 } 12707 12708 return this; 12709 }, 12710 12711 getElement: function () { 12712 return this._icon; 12713 }, 12714 12715 update: function () { 12716 12717 if (this._icon && this._map) { 12718 var pos = this._map.latLngToLayerPoint(this._latlng).round(); 12719 this._setPos(pos); 12720 } 12721 12722 return this; 12723 }, 12724 12725 _initIcon: function () { 12726 var options = this.options, 12727 classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); 12728 12729 var icon = options.icon.createIcon(this._icon), 12730 addIcon = false; 12731 12732 // if we're not reusing the icon, remove the old one and init new one 12733 if (icon !== this._icon) { 12734 if (this._icon) { 12735 this._removeIcon(); 12736 } 12737 addIcon = true; 12738 12739 if (options.title) { 12740 icon.title = options.title; 12741 } 12742 12743 if (icon.tagName === 'IMG') { 12744 icon.alt = options.alt || ''; 12745 } 12746 } 12747 12748 addClass(icon, classToAdd); 12749 12750 if (options.keyboard) { 12751 icon.tabIndex = '0'; 12752 icon.setAttribute('role', 'button'); 12753 } 12754 12755 this._icon = icon; 12756 12757 if (options.riseOnHover) { 12758 this.on({ 12759 mouseover: this._bringToFront, 12760 mouseout: this._resetZIndex 12761 }); 12762 } 12763 12764 if (this.options.autoPanOnFocus) { 12765 on(icon, 'focus', this._panOnFocus, this); 12766 } 12767 12768 var newShadow = options.icon.createShadow(this._shadow), 12769 addShadow = false; 12770 12771 if (newShadow !== this._shadow) { 12772 this._removeShadow(); 12773 addShadow = true; 12774 } 12775 12776 if (newShadow) { 12777 addClass(newShadow, classToAdd); 12778 newShadow.alt = ''; 12779 } 12780 this._shadow = newShadow; 12781 12782 12783 if (options.opacity < 1) { 12784 this._updateOpacity(); 12785 } 12786 12787 12788 if (addIcon) { 12789 this.getPane().appendChild(this._icon); 12790 } 12791 this._initInteraction(); 12792 if (newShadow && addShadow) { 12793 this.getPane(options.shadowPane).appendChild(this._shadow); 12794 } 12795 }, 12796 12797 _removeIcon: function () { 12798 if (this.options.riseOnHover) { 12799 this.off({ 12800 mouseover: this._bringToFront, 12801 mouseout: this._resetZIndex 12802 }); 12803 } 12804 12805 if (this.options.autoPanOnFocus) { 12806 off(this._icon, 'focus', this._panOnFocus, this); 12807 } 12808 12809 remove(this._icon); 12810 this.removeInteractiveTarget(this._icon); 12811 12812 this._icon = null; 12813 }, 12814 12815 _removeShadow: function () { 12816 if (this._shadow) { 12817 remove(this._shadow); 12818 } 12819 this._shadow = null; 12820 }, 12821 12822 _setPos: function (pos) { 12823 12824 if (this._icon) { 12825 setPosition(this._icon, pos); 12826 } 12827 12828 if (this._shadow) { 12829 setPosition(this._shadow, pos); 12830 } 12831 12832 this._zIndex = pos.y + this.options.zIndexOffset; 12833 12834 this._resetZIndex(); 12835 }, 12836 12837 _updateZIndex: function (offset) { 12838 if (this._icon) { 12839 this._icon.style.zIndex = this._zIndex + offset; 12840 } 12841 }, 12842 12843 _animateZoom: function (opt) { 12844 var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round(); 12845 12846 this._setPos(pos); 12847 }, 12848 12849 _initInteraction: function () { 12850 12851 if (!this.options.interactive) { return; } 12852 12853 addClass(this._icon, 'leaflet-interactive'); 12854 12855 this.addInteractiveTarget(this._icon); 12856 12857 if (MarkerDrag) { 12858 var draggable = this.options.draggable; 12859 if (this.dragging) { 12860 draggable = this.dragging.enabled(); 12861 this.dragging.disable(); 12862 } 12863 12864 this.dragging = new MarkerDrag(this); 12865 12866 if (draggable) { 12867 this.dragging.enable(); 12868 } 12869 } 12870 }, 12871 12872 // @method setOpacity(opacity: Number): this 12873 // Changes the opacity of the marker. 12874 setOpacity: function (opacity) { 12875 this.options.opacity = opacity; 12876 if (this._map) { 12877 this._updateOpacity(); 12878 } 12879 12880 return this; 12881 }, 12882 12883 _updateOpacity: function () { 12884 var opacity = this.options.opacity; 12885 12886 if (this._icon) { 12887 setOpacity(this._icon, opacity); 12888 } 12889 12890 if (this._shadow) { 12891 setOpacity(this._shadow, opacity); 12892 } 12893 }, 12894 12895 _bringToFront: function () { 12896 this._updateZIndex(this.options.riseOffset); 12897 }, 12898 12899 _resetZIndex: function () { 12900 this._updateZIndex(0); 12901 }, 12902 12903 _panOnFocus: function () { 12904 var map = this._map; 12905 if (!map) { return; } 12906 12907 var iconOpts = this.options.icon.options; 12908 var size = iconOpts.iconSize ? toPoint(iconOpts.iconSize) : toPoint(0, 0); 12909 var anchor = iconOpts.iconAnchor ? toPoint(iconOpts.iconAnchor) : toPoint(0, 0); 12910 12911 map.panInside(this._latlng, { 12912 paddingTopLeft: anchor, 12913 paddingBottomRight: size.subtract(anchor) 12914 }); 12915 }, 12916 12917 _getPopupAnchor: function () { 12918 return this.options.icon.options.popupAnchor; 12919 }, 12920 12921 _getTooltipAnchor: function () { 12922 return this.options.icon.options.tooltipAnchor; 12923 } 12924 }); 12925 12926 12927 // factory L.marker(latlng: LatLng, options? : Marker options) 12928 12929 // @factory L.marker(latlng: LatLng, options? : Marker options) 12930 // Instantiates a Marker object given a geographical point and optionally an options object. 12931 function marker(latlng, options) { 12932 return new Marker(latlng, options); 12933 } 12934 12935 /* 12936 * @class Path 12937 * @aka L.Path 12938 * @inherits Interactive layer 12939 * 12940 * An abstract class that contains options and constants shared between vector 12941 * overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`. 12942 */ 12943 12944 var Path = Layer.extend({ 12945 12946 // @section 12947 // @aka Path options 12948 options: { 12949 // @option stroke: Boolean = true 12950 // Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles. 12951 stroke: true, 12952 12953 // @option color: String = '#3388ff' 12954 // Stroke color 12955 color: '#3388ff', 12956 12957 // @option weight: Number = 3 12958 // Stroke width in pixels 12959 weight: 3, 12960 12961 // @option opacity: Number = 1.0 12962 // Stroke opacity 12963 opacity: 1, 12964 12965 // @option lineCap: String= 'round' 12966 // A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke. 12967 lineCap: 'round', 12968 12969 // @option lineJoin: String = 'round' 12970 // A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke. 12971 lineJoin: 'round', 12972 12973 // @option dashArray: String = null 12974 // A string that defines the stroke [dash pattern](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dasharray). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility). 12975 dashArray: null, 12976 12977 // @option dashOffset: String = null 12978 // A string that defines the [distance into the dash pattern to start the dash](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dashoffset). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility). 12979 dashOffset: null, 12980 12981 // @option fill: Boolean = depends 12982 // Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles. 12983 fill: false, 12984 12985 // @option fillColor: String = * 12986 // Fill color. Defaults to the value of the [`color`](#path-color) option 12987 fillColor: null, 12988 12989 // @option fillOpacity: Number = 0.2 12990 // Fill opacity. 12991 fillOpacity: 0.2, 12992 12993 // @option fillRule: String = 'evenodd' 12994 // A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined. 12995 fillRule: 'evenodd', 12996 12997 // className: '', 12998 12999 // Option inherited from "Interactive layer" abstract class 13000 interactive: true, 13001 13002 // @option bubblingMouseEvents: Boolean = true 13003 // When `true`, a mouse event on this path will trigger the same event on the map 13004 // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used). 13005 bubblingMouseEvents: true 13006 }, 13007 13008 beforeAdd: function (map) { 13009 // Renderer is set here because we need to call renderer.getEvents 13010 // before this.getEvents. 13011 this._renderer = map.getRenderer(this); 13012 }, 13013 13014 onAdd: function () { 13015 this._renderer._initPath(this); 13016 this._reset(); 13017 this._renderer._addPath(this); 13018 }, 13019 13020 onRemove: function () { 13021 this._renderer._removePath(this); 13022 }, 13023 13024 // @method redraw(): this 13025 // Redraws the layer. Sometimes useful after you changed the coordinates that the path uses. 13026 redraw: function () { 13027 if (this._map) { 13028 this._renderer._updatePath(this); 13029 } 13030 return this; 13031 }, 13032 13033 // @method setStyle(style: Path options): this 13034 // Changes the appearance of a Path based on the options in the `Path options` object. 13035 setStyle: function (style) { 13036 setOptions(this, style); 13037 if (this._renderer) { 13038 this._renderer._updateStyle(this); 13039 if (this.options.stroke && style && Object.prototype.hasOwnProperty.call(style, 'weight')) { 13040 this._updateBounds(); 13041 } 13042 } 13043 return this; 13044 }, 13045 13046 // @method bringToFront(): this 13047 // Brings the layer to the top of all path layers. 13048 bringToFront: function () { 13049 if (this._renderer) { 13050 this._renderer._bringToFront(this); 13051 } 13052 return this; 13053 }, 13054 13055 // @method bringToBack(): this 13056 // Brings the layer to the bottom of all path layers. 13057 bringToBack: function () { 13058 if (this._renderer) { 13059 this._renderer._bringToBack(this); 13060 } 13061 return this; 13062 }, 13063 13064 getElement: function () { 13065 return this._path; 13066 }, 13067 13068 _reset: function () { 13069 // defined in child classes 13070 this._project(); 13071 this._update(); 13072 }, 13073 13074 _clickTolerance: function () { 13075 // used when doing hit detection for Canvas layers 13076 return (this.options.stroke ? this.options.weight / 2 : 0) + 13077 (this._renderer.options.tolerance || 0); 13078 } 13079 }); 13080 13081 /* 13082 * @class CircleMarker 13083 * @aka L.CircleMarker 13084 * @inherits Path 13085 * 13086 * A circle of a fixed size with radius specified in pixels. Extends `Path`. 13087 */ 13088 13089 var CircleMarker = Path.extend({ 13090 13091 // @section 13092 // @aka CircleMarker options 13093 options: { 13094 fill: true, 13095 13096 // @option radius: Number = 10 13097 // Radius of the circle marker, in pixels 13098 radius: 10 13099 }, 13100 13101 initialize: function (latlng, options) { 13102 setOptions(this, options); 13103 this._latlng = toLatLng(latlng); 13104 this._radius = this.options.radius; 13105 }, 13106 13107 // @method setLatLng(latLng: LatLng): this 13108 // Sets the position of a circle marker to a new location. 13109 setLatLng: function (latlng) { 13110 var oldLatLng = this._latlng; 13111 this._latlng = toLatLng(latlng); 13112 this.redraw(); 13113 13114 // @event move: Event 13115 // Fired when the marker is moved via [`setLatLng`](#circlemarker-setlatlng). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`. 13116 return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng}); 13117 }, 13118 13119 // @method getLatLng(): LatLng 13120 // Returns the current geographical position of the circle marker 13121 getLatLng: function () { 13122 return this._latlng; 13123 }, 13124 13125 // @method setRadius(radius: Number): this 13126 // Sets the radius of a circle marker. Units are in pixels. 13127 setRadius: function (radius) { 13128 this.options.radius = this._radius = radius; 13129 return this.redraw(); 13130 }, 13131 13132 // @method getRadius(): Number 13133 // Returns the current radius of the circle 13134 getRadius: function () { 13135 return this._radius; 13136 }, 13137 13138 setStyle : function (options) { 13139 var radius = options && options.radius || this._radius; 13140 Path.prototype.setStyle.call(this, options); 13141 this.setRadius(radius); 13142 return this; 13143 }, 13144 13145 _project: function () { 13146 this._point = this._map.latLngToLayerPoint(this._latlng); 13147 this._updateBounds(); 13148 }, 13149 13150 _updateBounds: function () { 13151 var r = this._radius, 13152 r2 = this._radiusY || r, 13153 w = this._clickTolerance(), 13154 p = [r + w, r2 + w]; 13155 this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p)); 13156 }, 13157 13158 _update: function () { 13159 if (this._map) { 13160 this._updatePath(); 13161 } 13162 }, 13163 13164 _updatePath: function () { 13165 this._renderer._updateCircle(this); 13166 }, 13167 13168 _empty: function () { 13169 return this._radius && !this._renderer._bounds.intersects(this._pxBounds); 13170 }, 13171 13172 // Needed by the `Canvas` renderer for interactivity 13173 _containsPoint: function (p) { 13174 return p.distanceTo(this._point) <= this._radius + this._clickTolerance(); 13175 } 13176 }); 13177 13178 13179 // @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options) 13180 // Instantiates a circle marker object given a geographical point, and an optional options object. 13181 function circleMarker(latlng, options) { 13182 return new CircleMarker(latlng, options); 13183 } 13184 13185 /* 13186 * @class Circle 13187 * @aka L.Circle 13188 * @inherits CircleMarker 13189 * 13190 * A class for drawing circle overlays on a map. Extends `CircleMarker`. 13191 * 13192 * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion). 13193 * 13194 * @example 13195 * 13196 * ```js 13197 * L.circle([50.5, 30.5], {radius: 200}).addTo(map); 13198 * ``` 13199 */ 13200 13201 var Circle = CircleMarker.extend({ 13202 13203 initialize: function (latlng, options, legacyOptions) { 13204 if (typeof options === 'number') { 13205 // Backwards compatibility with 0.7.x factory (latlng, radius, options?) 13206 options = extend({}, legacyOptions, {radius: options}); 13207 } 13208 setOptions(this, options); 13209 this._latlng = toLatLng(latlng); 13210 13211 if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); } 13212 13213 // @section 13214 // @aka Circle options 13215 // @option radius: Number; Radius of the circle, in meters. 13216 this._mRadius = this.options.radius; 13217 }, 13218 13219 // @method setRadius(radius: Number): this 13220 // Sets the radius of a circle. Units are in meters. 13221 setRadius: function (radius) { 13222 this._mRadius = radius; 13223 return this.redraw(); 13224 }, 13225 13226 // @method getRadius(): Number 13227 // Returns the current radius of a circle. Units are in meters. 13228 getRadius: function () { 13229 return this._mRadius; 13230 }, 13231 13232 // @method getBounds(): LatLngBounds 13233 // Returns the `LatLngBounds` of the path. 13234 getBounds: function () { 13235 var half = [this._radius, this._radiusY || this._radius]; 13236 13237 return new LatLngBounds( 13238 this._map.layerPointToLatLng(this._point.subtract(half)), 13239 this._map.layerPointToLatLng(this._point.add(half))); 13240 }, 13241 13242 setStyle: Path.prototype.setStyle, 13243 13244 _project: function () { 13245 13246 var lng = this._latlng.lng, 13247 lat = this._latlng.lat, 13248 map = this._map, 13249 crs = map.options.crs; 13250 13251 if (crs.distance === Earth.distance) { 13252 var d = Math.PI / 180, 13253 latR = (this._mRadius / Earth.R) / d, 13254 top = map.project([lat + latR, lng]), 13255 bottom = map.project([lat - latR, lng]), 13256 p = top.add(bottom).divideBy(2), 13257 lat2 = map.unproject(p).lat, 13258 lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) / 13259 (Math.cos(lat * d) * Math.cos(lat2 * d))) / d; 13260 13261 if (isNaN(lngR) || lngR === 0) { 13262 lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425 13263 } 13264 13265 this._point = p.subtract(map.getPixelOrigin()); 13266 this._radius = isNaN(lngR) ? 0 : p.x - map.project([lat2, lng - lngR]).x; 13267 this._radiusY = p.y - top.y; 13268 13269 } else { 13270 var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0])); 13271 13272 this._point = map.latLngToLayerPoint(this._latlng); 13273 this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x; 13274 } 13275 13276 this._updateBounds(); 13277 } 13278 }); 13279 13280 // @factory L.circle(latlng: LatLng, options?: Circle options) 13281 // Instantiates a circle object given a geographical point, and an options object 13282 // which contains the circle radius. 13283 // @alternative 13284 // @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options) 13285 // Obsolete way of instantiating a circle, for compatibility with 0.7.x code. 13286 // Do not use in new applications or plugins. 13287 function circle(latlng, options, legacyOptions) { 13288 return new Circle(latlng, options, legacyOptions); 13289 } 13290 13291 /* 13292 * @class Polyline 13293 * @aka L.Polyline 13294 * @inherits Path 13295 * 13296 * A class for drawing polyline overlays on a map. Extends `Path`. 13297 * 13298 * @example 13299 * 13300 * ```js 13301 * // create a red polyline from an array of LatLng points 13302 * var latlngs = [ 13303 * [45.51, -122.68], 13304 * [37.77, -122.43], 13305 * [34.04, -118.2] 13306 * ]; 13307 * 13308 * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map); 13309 * 13310 * // zoom the map to the polyline 13311 * map.fitBounds(polyline.getBounds()); 13312 * ``` 13313 * 13314 * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape: 13315 * 13316 * ```js 13317 * // create a red polyline from an array of arrays of LatLng points 13318 * var latlngs = [ 13319 * [[45.51, -122.68], 13320 * [37.77, -122.43], 13321 * [34.04, -118.2]], 13322 * [[40.78, -73.91], 13323 * [41.83, -87.62], 13324 * [32.76, -96.72]] 13325 * ]; 13326 * ``` 13327 */ 13328 13329 13330 var Polyline = Path.extend({ 13331 13332 // @section 13333 // @aka Polyline options 13334 options: { 13335 // @option smoothFactor: Number = 1.0 13336 // How much to simplify the polyline on each zoom level. More means 13337 // better performance and smoother look, and less means more accurate representation. 13338 smoothFactor: 1.0, 13339 13340 // @option noClip: Boolean = false 13341 // Disable polyline clipping. 13342 noClip: false 13343 }, 13344 13345 initialize: function (latlngs, options) { 13346 setOptions(this, options); 13347 this._setLatLngs(latlngs); 13348 }, 13349 13350 // @method getLatLngs(): LatLng[] 13351 // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline. 13352 getLatLngs: function () { 13353 return this._latlngs; 13354 }, 13355 13356 // @method setLatLngs(latlngs: LatLng[]): this 13357 // Replaces all the points in the polyline with the given array of geographical points. 13358 setLatLngs: function (latlngs) { 13359 this._setLatLngs(latlngs); 13360 return this.redraw(); 13361 }, 13362 13363 // @method isEmpty(): Boolean 13364 // Returns `true` if the Polyline has no LatLngs. 13365 isEmpty: function () { 13366 return !this._latlngs.length; 13367 }, 13368 13369 // @method closestLayerPoint(p: Point): Point 13370 // Returns the point closest to `p` on the Polyline. 13371 closestLayerPoint: function (p) { 13372 var minDistance = Infinity, 13373 minPoint = null, 13374 closest = _sqClosestPointOnSegment, 13375 p1, p2; 13376 13377 for (var j = 0, jLen = this._parts.length; j < jLen; j++) { 13378 var points = this._parts[j]; 13379 13380 for (var i = 1, len = points.length; i < len; i++) { 13381 p1 = points[i - 1]; 13382 p2 = points[i]; 13383 13384 var sqDist = closest(p, p1, p2, true); 13385 13386 if (sqDist < minDistance) { 13387 minDistance = sqDist; 13388 minPoint = closest(p, p1, p2); 13389 } 13390 } 13391 } 13392 if (minPoint) { 13393 minPoint.distance = Math.sqrt(minDistance); 13394 } 13395 return minPoint; 13396 }, 13397 13398 // @method getCenter(): LatLng 13399 // Returns the center ([centroid](https://en.wikipedia.org/wiki/Centroid)) of the polyline. 13400 getCenter: function () { 13401 // throws error when not yet added to map as this center calculation requires projected coordinates 13402 if (!this._map) { 13403 throw new Error('Must add layer to map before using getCenter()'); 13404 } 13405 return polylineCenter(this._defaultShape(), this._map.options.crs); 13406 }, 13407 13408 // @method getBounds(): LatLngBounds 13409 // Returns the `LatLngBounds` of the path. 13410 getBounds: function () { 13411 return this._bounds; 13412 }, 13413 13414 // @method addLatLng(latlng: LatLng, latlngs?: LatLng[]): this 13415 // Adds a given point to the polyline. By default, adds to the first ring of 13416 // the polyline in case of a multi-polyline, but can be overridden by passing 13417 // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)). 13418 addLatLng: function (latlng, latlngs) { 13419 latlngs = latlngs || this._defaultShape(); 13420 latlng = toLatLng(latlng); 13421 latlngs.push(latlng); 13422 this._bounds.extend(latlng); 13423 return this.redraw(); 13424 }, 13425 13426 _setLatLngs: function (latlngs) { 13427 this._bounds = new LatLngBounds(); 13428 this._latlngs = this._convertLatLngs(latlngs); 13429 }, 13430 13431 _defaultShape: function () { 13432 return isFlat(this._latlngs) ? this._latlngs : this._latlngs[0]; 13433 }, 13434 13435 // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way 13436 _convertLatLngs: function (latlngs) { 13437 var result = [], 13438 flat = isFlat(latlngs); 13439 13440 for (var i = 0, len = latlngs.length; i < len; i++) { 13441 if (flat) { 13442 result[i] = toLatLng(latlngs[i]); 13443 this._bounds.extend(result[i]); 13444 } else { 13445 result[i] = this._convertLatLngs(latlngs[i]); 13446 } 13447 } 13448 13449 return result; 13450 }, 13451 13452 _project: function () { 13453 var pxBounds = new Bounds(); 13454 this._rings = []; 13455 this._projectLatlngs(this._latlngs, this._rings, pxBounds); 13456 13457 if (this._bounds.isValid() && pxBounds.isValid()) { 13458 this._rawPxBounds = pxBounds; 13459 this._updateBounds(); 13460 } 13461 }, 13462 13463 _updateBounds: function () { 13464 var w = this._clickTolerance(), 13465 p = new Point(w, w); 13466 13467 if (!this._rawPxBounds) { 13468 return; 13469 } 13470 13471 this._pxBounds = new Bounds([ 13472 this._rawPxBounds.min.subtract(p), 13473 this._rawPxBounds.max.add(p) 13474 ]); 13475 }, 13476 13477 // recursively turns latlngs into a set of rings with projected coordinates 13478 _projectLatlngs: function (latlngs, result, projectedBounds) { 13479 var flat = latlngs[0] instanceof LatLng, 13480 len = latlngs.length, 13481 i, ring; 13482 13483 if (flat) { 13484 ring = []; 13485 for (i = 0; i < len; i++) { 13486 ring[i] = this._map.latLngToLayerPoint(latlngs[i]); 13487 projectedBounds.extend(ring[i]); 13488 } 13489 result.push(ring); 13490 } else { 13491 for (i = 0; i < len; i++) { 13492 this._projectLatlngs(latlngs[i], result, projectedBounds); 13493 } 13494 } 13495 }, 13496 13497 // clip polyline by renderer bounds so that we have less to render for performance 13498 _clipPoints: function () { 13499 var bounds = this._renderer._bounds; 13500 13501 this._parts = []; 13502 if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { 13503 return; 13504 } 13505 13506 if (this.options.noClip) { 13507 this._parts = this._rings; 13508 return; 13509 } 13510 13511 var parts = this._parts, 13512 i, j, k, len, len2, segment, points; 13513 13514 for (i = 0, k = 0, len = this._rings.length; i < len; i++) { 13515 points = this._rings[i]; 13516 13517 for (j = 0, len2 = points.length; j < len2 - 1; j++) { 13518 segment = clipSegment(points[j], points[j + 1], bounds, j, true); 13519 13520 if (!segment) { continue; } 13521 13522 parts[k] = parts[k] || []; 13523 parts[k].push(segment[0]); 13524 13525 // if segment goes out of screen, or it's the last one, it's the end of the line part 13526 if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) { 13527 parts[k].push(segment[1]); 13528 k++; 13529 } 13530 } 13531 } 13532 }, 13533 13534 // simplify each clipped part of the polyline for performance 13535 _simplifyPoints: function () { 13536 var parts = this._parts, 13537 tolerance = this.options.smoothFactor; 13538 13539 for (var i = 0, len = parts.length; i < len; i++) { 13540 parts[i] = simplify(parts[i], tolerance); 13541 } 13542 }, 13543 13544 _update: function () { 13545 if (!this._map) { return; } 13546 13547 this._clipPoints(); 13548 this._simplifyPoints(); 13549 this._updatePath(); 13550 }, 13551 13552 _updatePath: function () { 13553 this._renderer._updatePoly(this); 13554 }, 13555 13556 // Needed by the `Canvas` renderer for interactivity 13557 _containsPoint: function (p, closed) { 13558 var i, j, k, len, len2, part, 13559 w = this._clickTolerance(); 13560 13561 if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; } 13562 13563 // hit detection for polylines 13564 for (i = 0, len = this._parts.length; i < len; i++) { 13565 part = this._parts[i]; 13566 13567 for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { 13568 if (!closed && (j === 0)) { continue; } 13569 13570 if (pointToSegmentDistance(p, part[k], part[j]) <= w) { 13571 return true; 13572 } 13573 } 13574 } 13575 return false; 13576 } 13577 }); 13578 13579 // @factory L.polyline(latlngs: LatLng[], options?: Polyline options) 13580 // Instantiates a polyline object given an array of geographical points and 13581 // optionally an options object. You can create a `Polyline` object with 13582 // multiple separate lines (`MultiPolyline`) by passing an array of arrays 13583 // of geographic points. 13584 function polyline(latlngs, options) { 13585 return new Polyline(latlngs, options); 13586 } 13587 13588 // Retrocompat. Allow plugins to support Leaflet versions before and after 1.1. 13589 Polyline._flat = _flat; 13590 13591 /* 13592 * @class Polygon 13593 * @aka L.Polygon 13594 * @inherits Polyline 13595 * 13596 * A class for drawing polygon overlays on a map. Extends `Polyline`. 13597 * 13598 * Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one â it's better to filter out such points. 13599 * 13600 * 13601 * @example 13602 * 13603 * ```js 13604 * // create a red polygon from an array of LatLng points 13605 * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]]; 13606 * 13607 * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map); 13608 * 13609 * // zoom the map to the polygon 13610 * map.fitBounds(polygon.getBounds()); 13611 * ``` 13612 * 13613 * You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape: 13614 * 13615 * ```js 13616 * var latlngs = [ 13617 * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring 13618 * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole 13619 * ]; 13620 * ``` 13621 * 13622 * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape. 13623 * 13624 * ```js 13625 * var latlngs = [ 13626 * [ // first polygon 13627 * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring 13628 * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole 13629 * ], 13630 * [ // second polygon 13631 * [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]] 13632 * ] 13633 * ]; 13634 * ``` 13635 */ 13636 13637 var Polygon = Polyline.extend({ 13638 13639 options: { 13640 fill: true 13641 }, 13642 13643 isEmpty: function () { 13644 return !this._latlngs.length || !this._latlngs[0].length; 13645 }, 13646 13647 // @method getCenter(): LatLng 13648 // Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the Polygon. 13649 getCenter: function () { 13650 // throws error when not yet added to map as this center calculation requires projected coordinates 13651 if (!this._map) { 13652 throw new Error('Must add layer to map before using getCenter()'); 13653 } 13654 return polygonCenter(this._defaultShape(), this._map.options.crs); 13655 }, 13656 13657 _convertLatLngs: function (latlngs) { 13658 var result = Polyline.prototype._convertLatLngs.call(this, latlngs), 13659 len = result.length; 13660 13661 // remove last point if it equals first one 13662 if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) { 13663 result.pop(); 13664 } 13665 return result; 13666 }, 13667 13668 _setLatLngs: function (latlngs) { 13669 Polyline.prototype._setLatLngs.call(this, latlngs); 13670 if (isFlat(this._latlngs)) { 13671 this._latlngs = [this._latlngs]; 13672 } 13673 }, 13674 13675 _defaultShape: function () { 13676 return isFlat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0]; 13677 }, 13678 13679 _clipPoints: function () { 13680 // polygons need a different clipping algorithm so we redefine that 13681 13682 var bounds = this._renderer._bounds, 13683 w = this.options.weight, 13684 p = new Point(w, w); 13685 13686 // increase clip padding by stroke width to avoid stroke on clip edges 13687 bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p)); 13688 13689 this._parts = []; 13690 if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { 13691 return; 13692 } 13693 13694 if (this.options.noClip) { 13695 this._parts = this._rings; 13696 return; 13697 } 13698 13699 for (var i = 0, len = this._rings.length, clipped; i < len; i++) { 13700 clipped = clipPolygon(this._rings[i], bounds, true); 13701 if (clipped.length) { 13702 this._parts.push(clipped); 13703 } 13704 } 13705 }, 13706 13707 _updatePath: function () { 13708 this._renderer._updatePoly(this, true); 13709 }, 13710 13711 // Needed by the `Canvas` renderer for interactivity 13712 _containsPoint: function (p) { 13713 var inside = false, 13714 part, p1, p2, i, j, k, len, len2; 13715 13716 if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; } 13717 13718 // ray casting algorithm for detecting if point is in polygon 13719 for (i = 0, len = this._parts.length; i < len; i++) { 13720 part = this._parts[i]; 13721 13722 for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { 13723 p1 = part[j]; 13724 p2 = part[k]; 13725 13726 if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { 13727 inside = !inside; 13728 } 13729 } 13730 } 13731 13732 // also check if it's on polygon stroke 13733 return inside || Polyline.prototype._containsPoint.call(this, p, true); 13734 } 13735 13736 }); 13737 13738 13739 // @factory L.polygon(latlngs: LatLng[], options?: Polyline options) 13740 function polygon(latlngs, options) { 13741 return new Polygon(latlngs, options); 13742 } 13743 13744 /* 13745 * @class GeoJSON 13746 * @aka L.GeoJSON 13747 * @inherits FeatureGroup 13748 * 13749 * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse 13750 * GeoJSON data and display it on the map. Extends `FeatureGroup`. 13751 * 13752 * @example 13753 * 13754 * ```js 13755 * L.geoJSON(data, { 13756 * style: function (feature) { 13757 * return {color: feature.properties.color}; 13758 * } 13759 * }).bindPopup(function (layer) { 13760 * return layer.feature.properties.description; 13761 * }).addTo(map); 13762 * ``` 13763 */ 13764 13765 var GeoJSON = FeatureGroup.extend({ 13766 13767 /* @section 13768 * @aka GeoJSON options 13769 * 13770 * @option pointToLayer: Function = * 13771 * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally 13772 * called when data is added, passing the GeoJSON point feature and its `LatLng`. 13773 * The default is to spawn a default `Marker`: 13774 * ```js 13775 * function(geoJsonPoint, latlng) { 13776 * return L.marker(latlng); 13777 * } 13778 * ``` 13779 * 13780 * @option style: Function = * 13781 * A `Function` defining the `Path options` for styling GeoJSON lines and polygons, 13782 * called internally when data is added. 13783 * The default value is to not override any defaults: 13784 * ```js 13785 * function (geoJsonFeature) { 13786 * return {} 13787 * } 13788 * ``` 13789 * 13790 * @option onEachFeature: Function = * 13791 * A `Function` that will be called once for each created `Feature`, after it has 13792 * been created and styled. Useful for attaching events and popups to features. 13793 * The default is to do nothing with the newly created layers: 13794 * ```js 13795 * function (feature, layer) {} 13796 * ``` 13797 * 13798 * @option filter: Function = * 13799 * A `Function` that will be used to decide whether to include a feature or not. 13800 * The default is to include all features: 13801 * ```js 13802 * function (geoJsonFeature) { 13803 * return true; 13804 * } 13805 * ``` 13806 * Note: dynamically changing the `filter` option will have effect only on newly 13807 * added data. It will _not_ re-evaluate already included features. 13808 * 13809 * @option coordsToLatLng: Function = * 13810 * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s. 13811 * The default is the `coordsToLatLng` static method. 13812 * 13813 * @option markersInheritOptions: Boolean = false 13814 * Whether default Markers for "Point" type Features inherit from group options. 13815 */ 13816 13817 initialize: function (geojson, options) { 13818 setOptions(this, options); 13819 13820 this._layers = {}; 13821 13822 if (geojson) { 13823 this.addData(geojson); 13824 } 13825 }, 13826 13827 // @method addData( <GeoJSON> data ): this 13828 // Adds a GeoJSON object to the layer. 13829 addData: function (geojson) { 13830 var features = isArray(geojson) ? geojson : geojson.features, 13831 i, len, feature; 13832 13833 if (features) { 13834 for (i = 0, len = features.length; i < len; i++) { 13835 // only add this if geometry or geometries are set and not null 13836 feature = features[i]; 13837 if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { 13838 this.addData(feature); 13839 } 13840 } 13841 return this; 13842 } 13843 13844 var options = this.options; 13845 13846 if (options.filter && !options.filter(geojson)) { return this; } 13847 13848 var layer = geometryToLayer(geojson, options); 13849 if (!layer) { 13850 return this; 13851 } 13852 layer.feature = asFeature(geojson); 13853 13854 layer.defaultOptions = layer.options; 13855 this.resetStyle(layer); 13856 13857 if (options.onEachFeature) { 13858 options.onEachFeature(geojson, layer); 13859 } 13860 13861 return this.addLayer(layer); 13862 }, 13863 13864 // @method resetStyle( <Path> layer? ): this 13865 // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events. 13866 // If `layer` is omitted, the style of all features in the current layer is reset. 13867 resetStyle: function (layer) { 13868 if (layer === undefined) { 13869 return this.eachLayer(this.resetStyle, this); 13870 } 13871 // reset any custom styles 13872 layer.options = extend({}, layer.defaultOptions); 13873 this._setLayerStyle(layer, this.options.style); 13874 return this; 13875 }, 13876 13877 // @method setStyle( <Function> style ): this 13878 // Changes styles of GeoJSON vector layers with the given style function. 13879 setStyle: function (style) { 13880 return this.eachLayer(function (layer) { 13881 this._setLayerStyle(layer, style); 13882 }, this); 13883 }, 13884 13885 _setLayerStyle: function (layer, style) { 13886 if (layer.setStyle) { 13887 if (typeof style === 'function') { 13888 style = style(layer.feature); 13889 } 13890 layer.setStyle(style); 13891 } 13892 } 13893 }); 13894 13895 // @section 13896 // There are several static functions which can be called without instantiating L.GeoJSON: 13897 13898 // @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer 13899 // Creates a `Layer` from a given GeoJSON feature. Can use a custom 13900 // [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng) 13901 // functions if provided as options. 13902 function geometryToLayer(geojson, options) { 13903 13904 var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, 13905 coords = geometry ? geometry.coordinates : null, 13906 layers = [], 13907 pointToLayer = options && options.pointToLayer, 13908 _coordsToLatLng = options && options.coordsToLatLng || coordsToLatLng, 13909 latlng, latlngs, i, len; 13910 13911 if (!coords && !geometry) { 13912 return null; 13913 } 13914 13915 switch (geometry.type) { 13916 case 'Point': 13917 latlng = _coordsToLatLng(coords); 13918 return _pointToLayer(pointToLayer, geojson, latlng, options); 13919 13920 case 'MultiPoint': 13921 for (i = 0, len = coords.length; i < len; i++) { 13922 latlng = _coordsToLatLng(coords[i]); 13923 layers.push(_pointToLayer(pointToLayer, geojson, latlng, options)); 13924 } 13925 return new FeatureGroup(layers); 13926 13927 case 'LineString': 13928 case 'MultiLineString': 13929 latlngs = coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng); 13930 return new Polyline(latlngs, options); 13931 13932 case 'Polygon': 13933 case 'MultiPolygon': 13934 latlngs = coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng); 13935 return new Polygon(latlngs, options); 13936 13937 case 'GeometryCollection': 13938 for (i = 0, len = geometry.geometries.length; i < len; i++) { 13939 var geoLayer = geometryToLayer({ 13940 geometry: geometry.geometries[i], 13941 type: 'Feature', 13942 properties: geojson.properties 13943 }, options); 13944 13945 if (geoLayer) { 13946 layers.push(geoLayer); 13947 } 13948 } 13949 return new FeatureGroup(layers); 13950 13951 case 'FeatureCollection': 13952 for (i = 0, len = geometry.features.length; i < len; i++) { 13953 var featureLayer = geometryToLayer(geometry.features[i], options); 13954 13955 if (featureLayer) { 13956 layers.push(featureLayer); 13957 } 13958 } 13959 return new FeatureGroup(layers); 13960 13961 default: 13962 throw new Error('Invalid GeoJSON object.'); 13963 } 13964 } 13965 13966 function _pointToLayer(pointToLayerFn, geojson, latlng, options) { 13967 return pointToLayerFn ? 13968 pointToLayerFn(geojson, latlng) : 13969 new Marker(latlng, options && options.markersInheritOptions && options); 13970 } 13971 13972 // @function coordsToLatLng(coords: Array): LatLng 13973 // Creates a `LatLng` object from an array of 2 numbers (longitude, latitude) 13974 // or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points. 13975 function coordsToLatLng(coords) { 13976 return new LatLng(coords[1], coords[0], coords[2]); 13977 } 13978 13979 // @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array 13980 // Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array. 13981 // `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default). 13982 // Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function. 13983 function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) { 13984 var latlngs = []; 13985 13986 for (var i = 0, len = coords.length, latlng; i < len; i++) { 13987 latlng = levelsDeep ? 13988 coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng) : 13989 (_coordsToLatLng || coordsToLatLng)(coords[i]); 13990 13991 latlngs.push(latlng); 13992 } 13993 13994 return latlngs; 13995 } 13996 13997 // @function latLngToCoords(latlng: LatLng, precision?: Number|false): Array 13998 // Reverse of [`coordsToLatLng`](#geojson-coordstolatlng) 13999 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function. 14000 function latLngToCoords(latlng, precision) { 14001 latlng = toLatLng(latlng); 14002 return latlng.alt !== undefined ? 14003 [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision), formatNum(latlng.alt, precision)] : 14004 [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision)]; 14005 } 14006 14007 // @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean, precision?: Number|false): Array 14008 // Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs) 14009 // `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default. 14010 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function. 14011 function latLngsToCoords(latlngs, levelsDeep, closed, precision) { 14012 var coords = []; 14013 14014 for (var i = 0, len = latlngs.length; i < len; i++) { 14015 // Check for flat arrays required to ensure unbalanced arrays are correctly converted in recursion 14016 coords.push(levelsDeep ? 14017 latLngsToCoords(latlngs[i], isFlat(latlngs[i]) ? 0 : levelsDeep - 1, closed, precision) : 14018 latLngToCoords(latlngs[i], precision)); 14019 } 14020 14021 if (!levelsDeep && closed && coords.length > 0) { 14022 coords.push(coords[0].slice()); 14023 } 14024 14025 return coords; 14026 } 14027 14028 function getFeature(layer, newGeometry) { 14029 return layer.feature ? 14030 extend({}, layer.feature, {geometry: newGeometry}) : 14031 asFeature(newGeometry); 14032 } 14033 14034 // @function asFeature(geojson: Object): Object 14035 // Normalize GeoJSON geometries/features into GeoJSON features. 14036 function asFeature(geojson) { 14037 if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') { 14038 return geojson; 14039 } 14040 14041 return { 14042 type: 'Feature', 14043 properties: {}, 14044 geometry: geojson 14045 }; 14046 } 14047 14048 var PointToGeoJSON = { 14049 toGeoJSON: function (precision) { 14050 return getFeature(this, { 14051 type: 'Point', 14052 coordinates: latLngToCoords(this.getLatLng(), precision) 14053 }); 14054 } 14055 }; 14056 14057 // @namespace Marker 14058 // @section Other methods 14059 // @method toGeoJSON(precision?: Number|false): Object 14060 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`. 14061 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature). 14062 Marker.include(PointToGeoJSON); 14063 14064 // @namespace CircleMarker 14065 // @method toGeoJSON(precision?: Number|false): Object 14066 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`. 14067 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature). 14068 Circle.include(PointToGeoJSON); 14069 CircleMarker.include(PointToGeoJSON); 14070 14071 14072 // @namespace Polyline 14073 // @method toGeoJSON(precision?: Number|false): Object 14074 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`. 14075 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature). 14076 Polyline.include({ 14077 toGeoJSON: function (precision) { 14078 var multi = !isFlat(this._latlngs); 14079 14080 var coords = latLngsToCoords(this._latlngs, multi ? 1 : 0, false, precision); 14081 14082 return getFeature(this, { 14083 type: (multi ? 'Multi' : '') + 'LineString', 14084 coordinates: coords 14085 }); 14086 } 14087 }); 14088 14089 // @namespace Polygon 14090 // @method toGeoJSON(precision?: Number|false): Object 14091 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`. 14092 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature). 14093 Polygon.include({ 14094 toGeoJSON: function (precision) { 14095 var holes = !isFlat(this._latlngs), 14096 multi = holes && !isFlat(this._latlngs[0]); 14097 14098 var coords = latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true, precision); 14099 14100 if (!holes) { 14101 coords = [coords]; 14102 } 14103 14104 return getFeature(this, { 14105 type: (multi ? 'Multi' : '') + 'Polygon', 14106 coordinates: coords 14107 }); 14108 } 14109 }); 14110 14111 14112 // @namespace LayerGroup 14113 LayerGroup.include({ 14114 toMultiPoint: function (precision) { 14115 var coords = []; 14116 14117 this.eachLayer(function (layer) { 14118 coords.push(layer.toGeoJSON(precision).geometry.coordinates); 14119 }); 14120 14121 return getFeature(this, { 14122 type: 'MultiPoint', 14123 coordinates: coords 14124 }); 14125 }, 14126 14127 // @method toGeoJSON(precision?: Number|false): Object 14128 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`. 14129 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`). 14130 toGeoJSON: function (precision) { 14131 14132 var type = this.feature && this.feature.geometry && this.feature.geometry.type; 14133 14134 if (type === 'MultiPoint') { 14135 return this.toMultiPoint(precision); 14136 } 14137 14138 var isGeometryCollection = type === 'GeometryCollection', 14139 jsons = []; 14140 14141 this.eachLayer(function (layer) { 14142 if (layer.toGeoJSON) { 14143 var json = layer.toGeoJSON(precision); 14144 if (isGeometryCollection) { 14145 jsons.push(json.geometry); 14146 } else { 14147 var feature = asFeature(json); 14148 // Squash nested feature collections 14149 if (feature.type === 'FeatureCollection') { 14150 jsons.push.apply(jsons, feature.features); 14151 } else { 14152 jsons.push(feature); 14153 } 14154 } 14155 } 14156 }); 14157 14158 if (isGeometryCollection) { 14159 return getFeature(this, { 14160 geometries: jsons, 14161 type: 'GeometryCollection' 14162 }); 14163 } 14164 14165 return { 14166 type: 'FeatureCollection', 14167 features: jsons 14168 }; 14169 } 14170 }); 14171 14172 // @namespace GeoJSON 14173 // @factory L.geoJSON(geojson?: Object, options?: GeoJSON options) 14174 // Creates a GeoJSON layer. Optionally accepts an object in 14175 // [GeoJSON format](https://tools.ietf.org/html/rfc7946) to display on the map 14176 // (you can alternatively add it later with `addData` method) and an `options` object. 14177 function geoJSON(geojson, options) { 14178 return new GeoJSON(geojson, options); 14179 } 14180 14181 // Backward compatibility. 14182 var geoJson = geoJSON; 14183 14184 /* 14185 * @class ImageOverlay 14186 * @aka L.ImageOverlay 14187 * @inherits Interactive layer 14188 * 14189 * Used to load and display a single image over specific bounds of the map. Extends `Layer`. 14190 * 14191 * @example 14192 * 14193 * ```js 14194 * var imageUrl = 'https://maps.lib.utexas.edu/maps/historical/newark_nj_1922.jpg', 14195 * imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]]; 14196 * L.imageOverlay(imageUrl, imageBounds).addTo(map); 14197 * ``` 14198 */ 14199 14200 var ImageOverlay = Layer.extend({ 14201 14202 // @section 14203 // @aka ImageOverlay options 14204 options: { 14205 // @option opacity: Number = 1.0 14206 // The opacity of the image overlay. 14207 opacity: 1, 14208 14209 // @option alt: String = '' 14210 // Text for the `alt` attribute of the image (useful for accessibility). 14211 alt: '', 14212 14213 // @option interactive: Boolean = false 14214 // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered. 14215 interactive: false, 14216 14217 // @option crossOrigin: Boolean|String = false 14218 // Whether the crossOrigin attribute will be added to the image. 14219 // If a String is provided, the image will have its crossOrigin attribute set to the String provided. This is needed if you want to access image pixel data. 14220 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values. 14221 crossOrigin: false, 14222 14223 // @option errorOverlayUrl: String = '' 14224 // URL to the overlay image to show in place of the overlay that failed to load. 14225 errorOverlayUrl: '', 14226 14227 // @option zIndex: Number = 1 14228 // The explicit [zIndex](https://developer.mozilla.org/docs/Web/CSS/CSS_Positioning/Understanding_z_index) of the overlay layer. 14229 zIndex: 1, 14230 14231 // @option className: String = '' 14232 // A custom class name to assign to the image. Empty by default. 14233 className: '' 14234 }, 14235 14236 initialize: function (url, bounds, options) { // (String, LatLngBounds, Object) 14237 this._url = url; 14238 this._bounds = toLatLngBounds(bounds); 14239 14240 setOptions(this, options); 14241 }, 14242 14243 onAdd: function () { 14244 if (!this._image) { 14245 this._initImage(); 14246 14247 if (this.options.opacity < 1) { 14248 this._updateOpacity(); 14249 } 14250 } 14251 14252 if (this.options.interactive) { 14253 addClass(this._image, 'leaflet-interactive'); 14254 this.addInteractiveTarget(this._image); 14255 } 14256 14257 this.getPane().appendChild(this._image); 14258 this._reset(); 14259 }, 14260 14261 onRemove: function () { 14262 remove(this._image); 14263 if (this.options.interactive) { 14264 this.removeInteractiveTarget(this._image); 14265 } 14266 }, 14267 14268 // @method setOpacity(opacity: Number): this 14269 // Sets the opacity of the overlay. 14270 setOpacity: function (opacity) { 14271 this.options.opacity = opacity; 14272 14273 if (this._image) { 14274 this._updateOpacity(); 14275 } 14276 return this; 14277 }, 14278 14279 setStyle: function (styleOpts) { 14280 if (styleOpts.opacity) { 14281 this.setOpacity(styleOpts.opacity); 14282 } 14283 return this; 14284 }, 14285 14286 // @method bringToFront(): this 14287 // Brings the layer to the top of all overlays. 14288 bringToFront: function () { 14289 if (this._map) { 14290 toFront(this._image); 14291 } 14292 return this; 14293 }, 14294 14295 // @method bringToBack(): this 14296 // Brings the layer to the bottom of all overlays. 14297 bringToBack: function () { 14298 if (this._map) { 14299 toBack(this._image); 14300 } 14301 return this; 14302 }, 14303 14304 // @method setUrl(url: String): this 14305 // Changes the URL of the image. 14306 setUrl: function (url) { 14307 this._url = url; 14308 14309 if (this._image) { 14310 this._image.src = url; 14311 } 14312 return this; 14313 }, 14314 14315 // @method setBounds(bounds: LatLngBounds): this 14316 // Update the bounds that this ImageOverlay covers 14317 setBounds: function (bounds) { 14318 this._bounds = toLatLngBounds(bounds); 14319 14320 if (this._map) { 14321 this._reset(); 14322 } 14323 return this; 14324 }, 14325 14326 getEvents: function () { 14327 var events = { 14328 zoom: this._reset, 14329 viewreset: this._reset 14330 }; 14331 14332 if (this._zoomAnimated) { 14333 events.zoomanim = this._animateZoom; 14334 } 14335 14336 return events; 14337 }, 14338 14339 // @method setZIndex(value: Number): this 14340 // Changes the [zIndex](#imageoverlay-zindex) of the image overlay. 14341 setZIndex: function (value) { 14342 this.options.zIndex = value; 14343 this._updateZIndex(); 14344 return this; 14345 }, 14346 14347 // @method getBounds(): LatLngBounds 14348 // Get the bounds that this ImageOverlay covers 14349 getBounds: function () { 14350 return this._bounds; 14351 }, 14352 14353 // @method getElement(): HTMLElement 14354 // Returns the instance of [`HTMLImageElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement) 14355 // used by this overlay. 14356 getElement: function () { 14357 return this._image; 14358 }, 14359 14360 _initImage: function () { 14361 var wasElementSupplied = this._url.tagName === 'IMG'; 14362 var img = this._image = wasElementSupplied ? this._url : create$1('img'); 14363 14364 addClass(img, 'leaflet-image-layer'); 14365 if (this._zoomAnimated) { addClass(img, 'leaflet-zoom-animated'); } 14366 if (this.options.className) { addClass(img, this.options.className); } 14367 14368 img.onselectstart = falseFn; 14369 img.onmousemove = falseFn; 14370 14371 // @event load: Event 14372 // Fired when the ImageOverlay layer has loaded its image 14373 img.onload = bind(this.fire, this, 'load'); 14374 img.onerror = bind(this._overlayOnError, this, 'error'); 14375 14376 if (this.options.crossOrigin || this.options.crossOrigin === '') { 14377 img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin; 14378 } 14379 14380 if (this.options.zIndex) { 14381 this._updateZIndex(); 14382 } 14383 14384 if (wasElementSupplied) { 14385 this._url = img.src; 14386 return; 14387 } 14388 14389 img.src = this._url; 14390 img.alt = this.options.alt; 14391 }, 14392 14393 _animateZoom: function (e) { 14394 var scale = this._map.getZoomScale(e.zoom), 14395 offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min; 14396 14397 setTransform(this._image, offset, scale); 14398 }, 14399 14400 _reset: function () { 14401 var image = this._image, 14402 bounds = new Bounds( 14403 this._map.latLngToLayerPoint(this._bounds.getNorthWest()), 14404 this._map.latLngToLayerPoint(this._bounds.getSouthEast())), 14405 size = bounds.getSize(); 14406 14407 setPosition(image, bounds.min); 14408 14409 image.style.width = size.x + 'px'; 14410 image.style.height = size.y + 'px'; 14411 }, 14412 14413 _updateOpacity: function () { 14414 setOpacity(this._image, this.options.opacity); 14415 }, 14416 14417 _updateZIndex: function () { 14418 if (this._image && this.options.zIndex !== undefined && this.options.zIndex !== null) { 14419 this._image.style.zIndex = this.options.zIndex; 14420 } 14421 }, 14422 14423 _overlayOnError: function () { 14424 // @event error: Event 14425 // Fired when the ImageOverlay layer fails to load its image 14426 this.fire('error'); 14427 14428 var errorUrl = this.options.errorOverlayUrl; 14429 if (errorUrl && this._url !== errorUrl) { 14430 this._url = errorUrl; 14431 this._image.src = errorUrl; 14432 } 14433 }, 14434 14435 // @method getCenter(): LatLng 14436 // Returns the center of the ImageOverlay. 14437 getCenter: function () { 14438 return this._bounds.getCenter(); 14439 } 14440 }); 14441 14442 // @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options) 14443 // Instantiates an image overlay object given the URL of the image and the 14444 // geographical bounds it is tied to. 14445 var imageOverlay = function (url, bounds, options) { 14446 return new ImageOverlay(url, bounds, options); 14447 }; 14448 14449 /* 14450 * @class VideoOverlay 14451 * @aka L.VideoOverlay 14452 * @inherits ImageOverlay 14453 * 14454 * Used to load and display a video player over specific bounds of the map. Extends `ImageOverlay`. 14455 * 14456 * A video overlay uses the [`<video>`](https://developer.mozilla.org/docs/Web/HTML/Element/video) 14457 * HTML5 element. 14458 * 14459 * @example 14460 * 14461 * ```js 14462 * var videoUrl = 'https://www.mapbox.com/bites/00188/patricia_nasa.webm', 14463 * videoBounds = [[ 32, -130], [ 13, -100]]; 14464 * L.videoOverlay(videoUrl, videoBounds ).addTo(map); 14465 * ``` 14466 */ 14467 14468 var VideoOverlay = ImageOverlay.extend({ 14469 14470 // @section 14471 // @aka VideoOverlay options 14472 options: { 14473 // @option autoplay: Boolean = true 14474 // Whether the video starts playing automatically when loaded. 14475 // On some browsers autoplay will only work with `muted: true` 14476 autoplay: true, 14477 14478 // @option loop: Boolean = true 14479 // Whether the video will loop back to the beginning when played. 14480 loop: true, 14481 14482 // @option keepAspectRatio: Boolean = true 14483 // Whether the video will save aspect ratio after the projection. 14484 // Relevant for supported browsers. See [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) 14485 keepAspectRatio: true, 14486 14487 // @option muted: Boolean = false 14488 // Whether the video starts on mute when loaded. 14489 muted: false, 14490 14491 // @option playsInline: Boolean = true 14492 // Mobile browsers will play the video right where it is instead of open it up in fullscreen mode. 14493 playsInline: true 14494 }, 14495 14496 _initImage: function () { 14497 var wasElementSupplied = this._url.tagName === 'VIDEO'; 14498 var vid = this._image = wasElementSupplied ? this._url : create$1('video'); 14499 14500 addClass(vid, 'leaflet-image-layer'); 14501 if (this._zoomAnimated) { addClass(vid, 'leaflet-zoom-animated'); } 14502 if (this.options.className) { addClass(vid, this.options.className); } 14503 14504 vid.onselectstart = falseFn; 14505 vid.onmousemove = falseFn; 14506 14507 // @event load: Event 14508 // Fired when the video has finished loading the first frame 14509 vid.onloadeddata = bind(this.fire, this, 'load'); 14510 14511 if (wasElementSupplied) { 14512 var sourceElements = vid.getElementsByTagName('source'); 14513 var sources = []; 14514 for (var j = 0; j < sourceElements.length; j++) { 14515 sources.push(sourceElements[j].src); 14516 } 14517 14518 this._url = (sourceElements.length > 0) ? sources : [vid.src]; 14519 return; 14520 } 14521 14522 if (!isArray(this._url)) { this._url = [this._url]; } 14523 14524 if (!this.options.keepAspectRatio && Object.prototype.hasOwnProperty.call(vid.style, 'objectFit')) { 14525 vid.style['objectFit'] = 'fill'; 14526 } 14527 vid.autoplay = !!this.options.autoplay; 14528 vid.loop = !!this.options.loop; 14529 vid.muted = !!this.options.muted; 14530 vid.playsInline = !!this.options.playsInline; 14531 for (var i = 0; i < this._url.length; i++) { 14532 var source = create$1('source'); 14533 source.src = this._url[i]; 14534 vid.appendChild(source); 14535 } 14536 } 14537 14538 // @method getElement(): HTMLVideoElement 14539 // Returns the instance of [`HTMLVideoElement`](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement) 14540 // used by this overlay. 14541 }); 14542 14543 14544 // @factory L.videoOverlay(video: String|Array|HTMLVideoElement, bounds: LatLngBounds, options?: VideoOverlay options) 14545 // Instantiates an image overlay object given the URL of the video (or array of URLs, or even a video element) and the 14546 // geographical bounds it is tied to. 14547 14548 function videoOverlay(video, bounds, options) { 14549 return new VideoOverlay(video, bounds, options); 14550 } 14551 14552 /* 14553 * @class SVGOverlay 14554 * @aka L.SVGOverlay 14555 * @inherits ImageOverlay 14556 * 14557 * Used to load, display and provide DOM access to an SVG file over specific bounds of the map. Extends `ImageOverlay`. 14558 * 14559 * An SVG overlay uses the [`<svg>`](https://developer.mozilla.org/docs/Web/SVG/Element/svg) element. 14560 * 14561 * @example 14562 * 14563 * ```js 14564 * var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 14565 * svgElement.setAttribute('xmlns', "http://www.w3.org/2000/svg"); 14566 * svgElement.setAttribute('viewBox', "0 0 200 200"); 14567 * svgElement.innerHTML = '<rect width="200" height="200"/><rect x="75" y="23" width="50" height="50" style="fill:red"/><rect x="75" y="123" width="50" height="50" style="fill:#0013ff"/>'; 14568 * var svgElementBounds = [ [ 32, -130 ], [ 13, -100 ] ]; 14569 * L.svgOverlay(svgElement, svgElementBounds).addTo(map); 14570 * ``` 14571 */ 14572 14573 var SVGOverlay = ImageOverlay.extend({ 14574 _initImage: function () { 14575 var el = this._image = this._url; 14576 14577 addClass(el, 'leaflet-image-layer'); 14578 if (this._zoomAnimated) { addClass(el, 'leaflet-zoom-animated'); } 14579 if (this.options.className) { addClass(el, this.options.className); } 14580 14581 el.onselectstart = falseFn; 14582 el.onmousemove = falseFn; 14583 } 14584 14585 // @method getElement(): SVGElement 14586 // Returns the instance of [`SVGElement`](https://developer.mozilla.org/docs/Web/API/SVGElement) 14587 // used by this overlay. 14588 }); 14589 14590 14591 // @factory L.svgOverlay(svg: String|SVGElement, bounds: LatLngBounds, options?: SVGOverlay options) 14592 // Instantiates an image overlay object given an SVG element and the geographical bounds it is tied to. 14593 // A viewBox attribute is required on the SVG element to zoom in and out properly. 14594 14595 function svgOverlay(el, bounds, options) { 14596 return new SVGOverlay(el, bounds, options); 14597 } 14598 14599 /* 14600 * @class DivOverlay 14601 * @inherits Interactive layer 14602 * @aka L.DivOverlay 14603 * Base model for L.Popup and L.Tooltip. Inherit from it for custom overlays like plugins. 14604 */ 14605 14606 // @namespace DivOverlay 14607 var DivOverlay = Layer.extend({ 14608 14609 // @section 14610 // @aka DivOverlay options 14611 options: { 14612 // @option interactive: Boolean = false 14613 // If true, the popup/tooltip will listen to the mouse events. 14614 interactive: false, 14615 14616 // @option offset: Point = Point(0, 0) 14617 // The offset of the overlay position. 14618 offset: [0, 0], 14619 14620 // @option className: String = '' 14621 // A custom CSS class name to assign to the overlay. 14622 className: '', 14623 14624 // @option pane: String = undefined 14625 // `Map pane` where the overlay will be added. 14626 pane: undefined, 14627 14628 // @option content: String|HTMLElement|Function = '' 14629 // Sets the HTML content of the overlay while initializing. If a function is passed the source layer will be 14630 // passed to the function. The function should return a `String` or `HTMLElement` to be used in the overlay. 14631 content: '' 14632 }, 14633 14634 initialize: function (options, source) { 14635 if (options && (options instanceof LatLng || isArray(options))) { 14636 this._latlng = toLatLng(options); 14637 setOptions(this, source); 14638 } else { 14639 setOptions(this, options); 14640 this._source = source; 14641 } 14642 if (this.options.content) { 14643 this._content = this.options.content; 14644 } 14645 }, 14646 14647 // @method openOn(map: Map): this 14648 // Adds the overlay to the map. 14649 // Alternative to `map.openPopup(popup)`/`.openTooltip(tooltip)`. 14650 openOn: function (map) { 14651 map = arguments.length ? map : this._source._map; // experimental, not the part of public api 14652 if (!map.hasLayer(this)) { 14653 map.addLayer(this); 14654 } 14655 return this; 14656 }, 14657 14658 // @method close(): this 14659 // Closes the overlay. 14660 // Alternative to `map.closePopup(popup)`/`.closeTooltip(tooltip)` 14661 // and `layer.closePopup()`/`.closeTooltip()`. 14662 close: function () { 14663 if (this._map) { 14664 this._map.removeLayer(this); 14665 } 14666 return this; 14667 }, 14668 14669 // @method toggle(layer?: Layer): this 14670 // Opens or closes the overlay bound to layer depending on its current state. 14671 // Argument may be omitted only for overlay bound to layer. 14672 // Alternative to `layer.togglePopup()`/`.toggleTooltip()`. 14673 toggle: function (layer) { 14674 if (this._map) { 14675 this.close(); 14676 } else { 14677 if (arguments.length) { 14678 this._source = layer; 14679 } else { 14680 layer = this._source; 14681 } 14682 this._prepareOpen(); 14683 14684 // open the overlay on the map 14685 this.openOn(layer._map); 14686 } 14687 return this; 14688 }, 14689 14690 onAdd: function (map) { 14691 this._zoomAnimated = map._zoomAnimated; 14692 14693 if (!this._container) { 14694 this._initLayout(); 14695 } 14696 14697 if (map._fadeAnimated) { 14698 setOpacity(this._container, 0); 14699 } 14700 14701 clearTimeout(this._removeTimeout); 14702 this.getPane().appendChild(this._container); 14703 this.update(); 14704 14705 if (map._fadeAnimated) { 14706 setOpacity(this._container, 1); 14707 } 14708 14709 this.bringToFront(); 14710 14711 if (this.options.interactive) { 14712 addClass(this._container, 'leaflet-interactive'); 14713 this.addInteractiveTarget(this._container); 14714 } 14715 }, 14716 14717 onRemove: function (map) { 14718 if (map._fadeAnimated) { 14719 setOpacity(this._container, 0); 14720 this._removeTimeout = setTimeout(bind(remove, undefined, this._container), 200); 14721 } else { 14722 remove(this._container); 14723 } 14724 14725 if (this.options.interactive) { 14726 removeClass(this._container, 'leaflet-interactive'); 14727 this.removeInteractiveTarget(this._container); 14728 } 14729 }, 14730 14731 // @namespace DivOverlay 14732 // @method getLatLng: LatLng 14733 // Returns the geographical point of the overlay. 14734 getLatLng: function () { 14735 return this._latlng; 14736 }, 14737 14738 // @method setLatLng(latlng: LatLng): this 14739 // Sets the geographical point where the overlay will open. 14740 setLatLng: function (latlng) { 14741 this._latlng = toLatLng(latlng); 14742 if (this._map) { 14743 this._updatePosition(); 14744 this._adjustPan(); 14745 } 14746 return this; 14747 }, 14748 14749 // @method getContent: String|HTMLElement 14750 // Returns the content of the overlay. 14751 getContent: function () { 14752 return this._content; 14753 }, 14754 14755 // @method setContent(htmlContent: String|HTMLElement|Function): this 14756 // Sets the HTML content of the overlay. If a function is passed the source layer will be passed to the function. 14757 // The function should return a `String` or `HTMLElement` to be used in the overlay. 14758 setContent: function (content) { 14759 this._content = content; 14760 this.update(); 14761 return this; 14762 }, 14763 14764 // @method getElement: String|HTMLElement 14765 // Returns the HTML container of the overlay. 14766 getElement: function () { 14767 return this._container; 14768 }, 14769 14770 // @method update: null 14771 // Updates the overlay content, layout and position. Useful for updating the overlay after something inside changed, e.g. image loaded. 14772 update: function () { 14773 if (!this._map) { return; } 14774 14775 this._container.style.visibility = 'hidden'; 14776 14777 this._updateContent(); 14778 this._updateLayout(); 14779 this._updatePosition(); 14780 14781 this._container.style.visibility = ''; 14782 14783 this._adjustPan(); 14784 }, 14785 14786 getEvents: function () { 14787 var events = { 14788 zoom: this._updatePosition, 14789 viewreset: this._updatePosition 14790 }; 14791 14792 if (this._zoomAnimated) { 14793 events.zoomanim = this._animateZoom; 14794 } 14795 return events; 14796 }, 14797 14798 // @method isOpen: Boolean 14799 // Returns `true` when the overlay is visible on the map. 14800 isOpen: function () { 14801 return !!this._map && this._map.hasLayer(this); 14802 }, 14803 14804 // @method bringToFront: this 14805 // Brings this overlay in front of other overlays (in the same map pane). 14806 bringToFront: function () { 14807 if (this._map) { 14808 toFront(this._container); 14809 } 14810 return this; 14811 }, 14812 14813 // @method bringToBack: this 14814 // Brings this overlay to the back of other overlays (in the same map pane). 14815 bringToBack: function () { 14816 if (this._map) { 14817 toBack(this._container); 14818 } 14819 return this; 14820 }, 14821 14822 // prepare bound overlay to open: update latlng pos / content source (for FeatureGroup) 14823 _prepareOpen: function (latlng) { 14824 var source = this._source; 14825 if (!source._map) { return false; } 14826 14827 if (source instanceof FeatureGroup) { 14828 source = null; 14829 var layers = this._source._layers; 14830 for (var id in layers) { 14831 if (layers[id]._map) { 14832 source = layers[id]; 14833 break; 14834 } 14835 } 14836 if (!source) { return false; } // Unable to get source layer. 14837 14838 // set overlay source to this layer 14839 this._source = source; 14840 } 14841 14842 if (!latlng) { 14843 if (source.getCenter) { 14844 latlng = source.getCenter(); 14845 } else if (source.getLatLng) { 14846 latlng = source.getLatLng(); 14847 } else if (source.getBounds) { 14848 latlng = source.getBounds().getCenter(); 14849 } else { 14850 throw new Error('Unable to get source layer LatLng.'); 14851 } 14852 } 14853 this.setLatLng(latlng); 14854 14855 if (this._map) { 14856 // update the overlay (content, layout, etc...) 14857 this.update(); 14858 } 14859 14860 return true; 14861 }, 14862 14863 _updateContent: function () { 14864 if (!this._content) { return; } 14865 14866 var node = this._contentNode; 14867 var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content; 14868 14869 if (typeof content === 'string') { 14870 node.innerHTML = content; 14871 } else { 14872 while (node.hasChildNodes()) { 14873 node.removeChild(node.firstChild); 14874 } 14875 node.appendChild(content); 14876 } 14877 14878 // @namespace DivOverlay 14879 // @section DivOverlay events 14880 // @event contentupdate: Event 14881 // Fired when the content of the overlay is updated 14882 this.fire('contentupdate'); 14883 }, 14884 14885 _updatePosition: function () { 14886 if (!this._map) { return; } 14887 14888 var pos = this._map.latLngToLayerPoint(this._latlng), 14889 offset = toPoint(this.options.offset), 14890 anchor = this._getAnchor(); 14891 14892 if (this._zoomAnimated) { 14893 setPosition(this._container, pos.add(anchor)); 14894 } else { 14895 offset = offset.add(pos).add(anchor); 14896 } 14897 14898 var bottom = this._containerBottom = -offset.y, 14899 left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x; 14900 14901 // bottom position the overlay in case the height of the overlay changes (images loading etc) 14902 this._container.style.bottom = bottom + 'px'; 14903 this._container.style.left = left + 'px'; 14904 }, 14905 14906 _getAnchor: function () { 14907 return [0, 0]; 14908 } 14909 14910 }); 14911 14912 Map.include({ 14913 _initOverlay: function (OverlayClass, content, latlng, options) { 14914 var overlay = content; 14915 if (!(overlay instanceof OverlayClass)) { 14916 overlay = new OverlayClass(options).setContent(content); 14917 } 14918 if (latlng) { 14919 overlay.setLatLng(latlng); 14920 } 14921 return overlay; 14922 } 14923 }); 14924 14925 14926 Layer.include({ 14927 _initOverlay: function (OverlayClass, old, content, options) { 14928 var overlay = content; 14929 if (overlay instanceof OverlayClass) { 14930 setOptions(overlay, options); 14931 overlay._source = this; 14932 } else { 14933 overlay = (old && !options) ? old : new OverlayClass(options, this); 14934 overlay.setContent(content); 14935 } 14936 return overlay; 14937 } 14938 }); 14939 14940 /* 14941 * @class Popup 14942 * @inherits DivOverlay 14943 * @aka L.Popup 14944 * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to 14945 * open popups while making sure that only one popup is open at one time 14946 * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want. 14947 * 14948 * @example 14949 * 14950 * If you want to just bind a popup to marker click and then open it, it's really easy: 14951 * 14952 * ```js 14953 * marker.bindPopup(popupContent).openPopup(); 14954 * ``` 14955 * Path overlays like polylines also have a `bindPopup` method. 14956 * 14957 * A popup can be also standalone: 14958 * 14959 * ```js 14960 * var popup = L.popup() 14961 * .setLatLng(latlng) 14962 * .setContent('<p>Hello world!<br />This is a nice popup.</p>') 14963 * .openOn(map); 14964 * ``` 14965 * or 14966 * ```js 14967 * var popup = L.popup(latlng, {content: '<p>Hello world!<br />This is a nice popup.</p>') 14968 * .openOn(map); 14969 * ``` 14970 */ 14971 14972 14973 // @namespace Popup 14974 var Popup = DivOverlay.extend({ 14975 14976 // @section 14977 // @aka Popup options 14978 options: { 14979 // @option pane: String = 'popupPane' 14980 // `Map pane` where the popup will be added. 14981 pane: 'popupPane', 14982 14983 // @option offset: Point = Point(0, 7) 14984 // The offset of the popup position. 14985 offset: [0, 7], 14986 14987 // @option maxWidth: Number = 300 14988 // Max width of the popup, in pixels. 14989 maxWidth: 300, 14990 14991 // @option minWidth: Number = 50 14992 // Min width of the popup, in pixels. 14993 minWidth: 50, 14994 14995 // @option maxHeight: Number = null 14996 // If set, creates a scrollable container of the given height 14997 // inside a popup if its content exceeds it. 14998 // The scrollable container can be styled using the 14999 // `leaflet-popup-scrolled` CSS class selector. 15000 maxHeight: null, 15001 15002 // @option autoPan: Boolean = true 15003 // Set it to `false` if you don't want the map to do panning animation 15004 // to fit the opened popup. 15005 autoPan: true, 15006 15007 // @option autoPanPaddingTopLeft: Point = null 15008 // The margin between the popup and the top left corner of the map 15009 // view after autopanning was performed. 15010 autoPanPaddingTopLeft: null, 15011 15012 // @option autoPanPaddingBottomRight: Point = null 15013 // The margin between the popup and the bottom right corner of the map 15014 // view after autopanning was performed. 15015 autoPanPaddingBottomRight: null, 15016 15017 // @option autoPanPadding: Point = Point(5, 5) 15018 // Equivalent of setting both top left and bottom right autopan padding to the same value. 15019 autoPanPadding: [5, 5], 15020 15021 // @option keepInView: Boolean = false 15022 // Set it to `true` if you want to prevent users from panning the popup 15023 // off of the screen while it is open. 15024 keepInView: false, 15025 15026 // @option closeButton: Boolean = true 15027 // Controls the presence of a close button in the popup. 15028 closeButton: true, 15029 15030 // @option autoClose: Boolean = true 15031 // Set it to `false` if you want to override the default behavior of 15032 // the popup closing when another popup is opened. 15033 autoClose: true, 15034 15035 // @option closeOnEscapeKey: Boolean = true 15036 // Set it to `false` if you want to override the default behavior of 15037 // the ESC key for closing of the popup. 15038 closeOnEscapeKey: true, 15039 15040 // @option closeOnClick: Boolean = * 15041 // Set it if you want to override the default behavior of the popup closing when user clicks 15042 // on the map. Defaults to the map's [`closePopupOnClick`](#map-closepopuponclick) option. 15043 15044 // @option className: String = '' 15045 // A custom CSS class name to assign to the popup. 15046 className: '' 15047 }, 15048 15049 // @namespace Popup 15050 // @method openOn(map: Map): this 15051 // Alternative to `map.openPopup(popup)`. 15052 // Adds the popup to the map and closes the previous one. 15053 openOn: function (map) { 15054 map = arguments.length ? map : this._source._map; // experimental, not the part of public api 15055 15056 if (!map.hasLayer(this) && map._popup && map._popup.options.autoClose) { 15057 map.removeLayer(map._popup); 15058 } 15059 map._popup = this; 15060 15061 return DivOverlay.prototype.openOn.call(this, map); 15062 }, 15063 15064 onAdd: function (map) { 15065 DivOverlay.prototype.onAdd.call(this, map); 15066 15067 // @namespace Map 15068 // @section Popup events 15069 // @event popupopen: PopupEvent 15070 // Fired when a popup is opened in the map 15071 map.fire('popupopen', {popup: this}); 15072 15073 if (this._source) { 15074 // @namespace Layer 15075 // @section Popup events 15076 // @event popupopen: PopupEvent 15077 // Fired when a popup bound to this layer is opened 15078 this._source.fire('popupopen', {popup: this}, true); 15079 // For non-path layers, we toggle the popup when clicking 15080 // again the layer, so prevent the map to reopen it. 15081 if (!(this._source instanceof Path)) { 15082 this._source.on('preclick', stopPropagation); 15083 } 15084 } 15085 }, 15086 15087 onRemove: function (map) { 15088 DivOverlay.prototype.onRemove.call(this, map); 15089 15090 // @namespace Map 15091 // @section Popup events 15092 // @event popupclose: PopupEvent 15093 // Fired when a popup in the map is closed 15094 map.fire('popupclose', {popup: this}); 15095 15096 if (this._source) { 15097 // @namespace Layer 15098 // @section Popup events 15099 // @event popupclose: PopupEvent 15100 // Fired when a popup bound to this layer is closed 15101 this._source.fire('popupclose', {popup: this}, true); 15102 if (!(this._source instanceof Path)) { 15103 this._source.off('preclick', stopPropagation); 15104 } 15105 } 15106 }, 15107 15108 getEvents: function () { 15109 var events = DivOverlay.prototype.getEvents.call(this); 15110 15111 if (this.options.closeOnClick !== undefined ? this.options.closeOnClick : this._map.options.closePopupOnClick) { 15112 events.preclick = this.close; 15113 } 15114 15115 if (this.options.keepInView) { 15116 events.moveend = this._adjustPan; 15117 } 15118 15119 return events; 15120 }, 15121 15122 _initLayout: function () { 15123 var prefix = 'leaflet-popup', 15124 container = this._container = create$1('div', 15125 prefix + ' ' + (this.options.className || '') + 15126 ' leaflet-zoom-animated'); 15127 15128 var wrapper = this._wrapper = create$1('div', prefix + '-content-wrapper', container); 15129 this._contentNode = create$1('div', prefix + '-content', wrapper); 15130 15131 disableClickPropagation(container); 15132 disableScrollPropagation(this._contentNode); 15133 on(container, 'contextmenu', stopPropagation); 15134 15135 this._tipContainer = create$1('div', prefix + '-tip-container', container); 15136 this._tip = create$1('div', prefix + '-tip', this._tipContainer); 15137 15138 if (this.options.closeButton) { 15139 var closeButton = this._closeButton = create$1('a', prefix + '-close-button', container); 15140 closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399 15141 closeButton.setAttribute('aria-label', 'Close popup'); 15142 closeButton.href = '#close'; 15143 closeButton.innerHTML = '<span aria-hidden="true">×</span>'; 15144 15145 on(closeButton, 'click', function (ev) { 15146 preventDefault(ev); 15147 this.close(); 15148 }, this); 15149 } 15150 }, 15151 15152 _updateLayout: function () { 15153 var container = this._contentNode, 15154 style = container.style; 15155 15156 style.width = ''; 15157 style.whiteSpace = 'nowrap'; 15158 15159 var width = container.offsetWidth; 15160 width = Math.min(width, this.options.maxWidth); 15161 width = Math.max(width, this.options.minWidth); 15162 15163 style.width = (width + 1) + 'px'; 15164 style.whiteSpace = ''; 15165 15166 style.height = ''; 15167 15168 var height = container.offsetHeight, 15169 maxHeight = this.options.maxHeight, 15170 scrolledClass = 'leaflet-popup-scrolled'; 15171 15172 if (maxHeight && height > maxHeight) { 15173 style.height = maxHeight + 'px'; 15174 addClass(container, scrolledClass); 15175 } else { 15176 removeClass(container, scrolledClass); 15177 } 15178 15179 this._containerWidth = this._container.offsetWidth; 15180 }, 15181 15182 _animateZoom: function (e) { 15183 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center), 15184 anchor = this._getAnchor(); 15185 setPosition(this._container, pos.add(anchor)); 15186 }, 15187 15188 _adjustPan: function () { 15189 if (!this.options.autoPan) { return; } 15190 if (this._map._panAnim) { this._map._panAnim.stop(); } 15191 15192 // We can endlessly recurse if keepInView is set and the view resets. 15193 // Let's guard against that by exiting early if we're responding to our own autopan. 15194 if (this._autopanning) { 15195 this._autopanning = false; 15196 return; 15197 } 15198 15199 var map = this._map, 15200 marginBottom = parseInt(getStyle(this._container, 'marginBottom'), 10) || 0, 15201 containerHeight = this._container.offsetHeight + marginBottom, 15202 containerWidth = this._containerWidth, 15203 layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom); 15204 15205 layerPos._add(getPosition(this._container)); 15206 15207 var containerPos = map.layerPointToContainerPoint(layerPos), 15208 padding = toPoint(this.options.autoPanPadding), 15209 paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding), 15210 paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding), 15211 size = map.getSize(), 15212 dx = 0, 15213 dy = 0; 15214 15215 if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right 15216 dx = containerPos.x + containerWidth - size.x + paddingBR.x; 15217 } 15218 if (containerPos.x - dx - paddingTL.x < 0) { // left 15219 dx = containerPos.x - paddingTL.x; 15220 } 15221 if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom 15222 dy = containerPos.y + containerHeight - size.y + paddingBR.y; 15223 } 15224 if (containerPos.y - dy - paddingTL.y < 0) { // top 15225 dy = containerPos.y - paddingTL.y; 15226 } 15227 15228 // @namespace Map 15229 // @section Popup events 15230 // @event autopanstart: Event 15231 // Fired when the map starts autopanning when opening a popup. 15232 if (dx || dy) { 15233 // Track that we're autopanning, as this function will be re-ran on moveend 15234 if (this.options.keepInView) { 15235 this._autopanning = true; 15236 } 15237 15238 map 15239 .fire('autopanstart') 15240 .panBy([dx, dy]); 15241 } 15242 }, 15243 15244 _getAnchor: function () { 15245 // Where should we anchor the popup on the source layer? 15246 return toPoint(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]); 15247 } 15248 15249 }); 15250 15251 // @namespace Popup 15252 // @factory L.popup(options?: Popup options, source?: Layer) 15253 // Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers. 15254 // @alternative 15255 // @factory L.popup(latlng: LatLng, options?: Popup options) 15256 // Instantiates a `Popup` object given `latlng` where the popup will open and an optional `options` object that describes its appearance and location. 15257 var popup = function (options, source) { 15258 return new Popup(options, source); 15259 }; 15260 15261 15262 /* @namespace Map 15263 * @section Interaction Options 15264 * @option closePopupOnClick: Boolean = true 15265 * Set it to `false` if you don't want popups to close when user clicks the map. 15266 */ 15267 Map.mergeOptions({ 15268 closePopupOnClick: true 15269 }); 15270 15271 15272 // @namespace Map 15273 // @section Methods for Layers and Controls 15274 Map.include({ 15275 // @method openPopup(popup: Popup): this 15276 // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability). 15277 // @alternative 15278 // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this 15279 // Creates a popup with the specified content and options and opens it in the given point on a map. 15280 openPopup: function (popup, latlng, options) { 15281 this._initOverlay(Popup, popup, latlng, options) 15282 .openOn(this); 15283 15284 return this; 15285 }, 15286 15287 // @method closePopup(popup?: Popup): this 15288 // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one). 15289 closePopup: function (popup) { 15290 popup = arguments.length ? popup : this._popup; 15291 if (popup) { 15292 popup.close(); 15293 } 15294 return this; 15295 } 15296 }); 15297 15298 /* 15299 * @namespace Layer 15300 * @section Popup methods example 15301 * 15302 * All layers share a set of methods convenient for binding popups to it. 15303 * 15304 * ```js 15305 * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map); 15306 * layer.openPopup(); 15307 * layer.closePopup(); 15308 * ``` 15309 * 15310 * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened. 15311 */ 15312 15313 // @section Popup methods 15314 Layer.include({ 15315 15316 // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this 15317 // Binds a popup to the layer with the passed `content` and sets up the 15318 // necessary event listeners. If a `Function` is passed it will receive 15319 // the layer as the first argument and should return a `String` or `HTMLElement`. 15320 bindPopup: function (content, options) { 15321 this._popup = this._initOverlay(Popup, this._popup, content, options); 15322 if (!this._popupHandlersAdded) { 15323 this.on({ 15324 click: this._openPopup, 15325 keypress: this._onKeyPress, 15326 remove: this.closePopup, 15327 move: this._movePopup 15328 }); 15329 this._popupHandlersAdded = true; 15330 } 15331 15332 return this; 15333 }, 15334 15335 // @method unbindPopup(): this 15336 // Removes the popup previously bound with `bindPopup`. 15337 unbindPopup: function () { 15338 if (this._popup) { 15339 this.off({ 15340 click: this._openPopup, 15341 keypress: this._onKeyPress, 15342 remove: this.closePopup, 15343 move: this._movePopup 15344 }); 15345 this._popupHandlersAdded = false; 15346 this._popup = null; 15347 } 15348 return this; 15349 }, 15350 15351 // @method openPopup(latlng?: LatLng): this 15352 // Opens the bound popup at the specified `latlng` or at the default popup anchor if no `latlng` is passed. 15353 openPopup: function (latlng) { 15354 if (this._popup) { 15355 if (!(this instanceof FeatureGroup)) { 15356 this._popup._source = this; 15357 } 15358 if (this._popup._prepareOpen(latlng || this._latlng)) { 15359 // open the popup on the map 15360 this._popup.openOn(this._map); 15361 } 15362 } 15363 return this; 15364 }, 15365 15366 // @method closePopup(): this 15367 // Closes the popup bound to this layer if it is open. 15368 closePopup: function () { 15369 if (this._popup) { 15370 this._popup.close(); 15371 } 15372 return this; 15373 }, 15374 15375 // @method togglePopup(): this 15376 // Opens or closes the popup bound to this layer depending on its current state. 15377 togglePopup: function () { 15378 if (this._popup) { 15379 this._popup.toggle(this); 15380 } 15381 return this; 15382 }, 15383 15384 // @method isPopupOpen(): boolean 15385 // Returns `true` if the popup bound to this layer is currently open. 15386 isPopupOpen: function () { 15387 return (this._popup ? this._popup.isOpen() : false); 15388 }, 15389 15390 // @method setPopupContent(content: String|HTMLElement|Popup): this 15391 // Sets the content of the popup bound to this layer. 15392 setPopupContent: function (content) { 15393 if (this._popup) { 15394 this._popup.setContent(content); 15395 } 15396 return this; 15397 }, 15398 15399 // @method getPopup(): Popup 15400 // Returns the popup bound to this layer. 15401 getPopup: function () { 15402 return this._popup; 15403 }, 15404 15405 _openPopup: function (e) { 15406 if (!this._popup || !this._map) { 15407 return; 15408 } 15409 // prevent map click 15410 stop(e); 15411 15412 var target = e.layer || e.target; 15413 if (this._popup._source === target && !(target instanceof Path)) { 15414 // treat it like a marker and figure out 15415 // if we should toggle it open/closed 15416 if (this._map.hasLayer(this._popup)) { 15417 this.closePopup(); 15418 } else { 15419 this.openPopup(e.latlng); 15420 } 15421 return; 15422 } 15423 this._popup._source = target; 15424 this.openPopup(e.latlng); 15425 }, 15426 15427 _movePopup: function (e) { 15428 this._popup.setLatLng(e.latlng); 15429 }, 15430 15431 _onKeyPress: function (e) { 15432 if (e.originalEvent.keyCode === 13) { 15433 this._openPopup(e); 15434 } 15435 } 15436 }); 15437 15438 /* 15439 * @class Tooltip 15440 * @inherits DivOverlay 15441 * @aka L.Tooltip 15442 * Used to display small texts on top of map layers. 15443 * 15444 * @example 15445 * If you want to just bind a tooltip to marker: 15446 * 15447 * ```js 15448 * marker.bindTooltip("my tooltip text").openTooltip(); 15449 * ``` 15450 * Path overlays like polylines also have a `bindTooltip` method. 15451 * 15452 * A tooltip can be also standalone: 15453 * 15454 * ```js 15455 * var tooltip = L.tooltip() 15456 * .setLatLng(latlng) 15457 * .setContent('Hello world!<br />This is a nice tooltip.') 15458 * .addTo(map); 15459 * ``` 15460 * or 15461 * ```js 15462 * var tooltip = L.tooltip(latlng, {content: 'Hello world!<br />This is a nice tooltip.'}) 15463 * .addTo(map); 15464 * ``` 15465 * 15466 * 15467 * Note about tooltip offset. Leaflet takes two options in consideration 15468 * for computing tooltip offsetting: 15469 * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip. 15470 * Add a positive x offset to move the tooltip to the right, and a positive y offset to 15471 * move it to the bottom. Negatives will move to the left and top. 15472 * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You 15473 * should adapt this value if you use a custom icon. 15474 */ 15475 15476 15477 // @namespace Tooltip 15478 var Tooltip = DivOverlay.extend({ 15479 15480 // @section 15481 // @aka Tooltip options 15482 options: { 15483 // @option pane: String = 'tooltipPane' 15484 // `Map pane` where the tooltip will be added. 15485 pane: 'tooltipPane', 15486 15487 // @option offset: Point = Point(0, 0) 15488 // Optional offset of the tooltip position. 15489 offset: [0, 0], 15490 15491 // @option direction: String = 'auto' 15492 // Direction where to open the tooltip. Possible values are: `right`, `left`, 15493 // `top`, `bottom`, `center`, `auto`. 15494 // `auto` will dynamically switch between `right` and `left` according to the tooltip 15495 // position on the map. 15496 direction: 'auto', 15497 15498 // @option permanent: Boolean = false 15499 // Whether to open the tooltip permanently or only on mouseover. 15500 permanent: false, 15501 15502 // @option sticky: Boolean = false 15503 // If true, the tooltip will follow the mouse instead of being fixed at the feature center. 15504 sticky: false, 15505 15506 // @option opacity: Number = 0.9 15507 // Tooltip container opacity. 15508 opacity: 0.9 15509 }, 15510 15511 onAdd: function (map) { 15512 DivOverlay.prototype.onAdd.call(this, map); 15513 this.setOpacity(this.options.opacity); 15514 15515 // @namespace Map 15516 // @section Tooltip events 15517 // @event tooltipopen: TooltipEvent 15518 // Fired when a tooltip is opened in the map. 15519 map.fire('tooltipopen', {tooltip: this}); 15520 15521 if (this._source) { 15522 this.addEventParent(this._source); 15523 15524 // @namespace Layer 15525 // @section Tooltip events 15526 // @event tooltipopen: TooltipEvent 15527 // Fired when a tooltip bound to this layer is opened. 15528 this._source.fire('tooltipopen', {tooltip: this}, true); 15529 } 15530 }, 15531 15532 onRemove: function (map) { 15533 DivOverlay.prototype.onRemove.call(this, map); 15534 15535 // @namespace Map 15536 // @section Tooltip events 15537 // @event tooltipclose: TooltipEvent 15538 // Fired when a tooltip in the map is closed. 15539 map.fire('tooltipclose', {tooltip: this}); 15540 15541 if (this._source) { 15542 this.removeEventParent(this._source); 15543 15544 // @namespace Layer 15545 // @section Tooltip events 15546 // @event tooltipclose: TooltipEvent 15547 // Fired when a tooltip bound to this layer is closed. 15548 this._source.fire('tooltipclose', {tooltip: this}, true); 15549 } 15550 }, 15551 15552 getEvents: function () { 15553 var events = DivOverlay.prototype.getEvents.call(this); 15554 15555 if (!this.options.permanent) { 15556 events.preclick = this.close; 15557 } 15558 15559 return events; 15560 }, 15561 15562 _initLayout: function () { 15563 var prefix = 'leaflet-tooltip', 15564 className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); 15565 15566 this._contentNode = this._container = create$1('div', className); 15567 15568 this._container.setAttribute('role', 'tooltip'); 15569 this._container.setAttribute('id', 'leaflet-tooltip-' + stamp(this)); 15570 }, 15571 15572 _updateLayout: function () {}, 15573 15574 _adjustPan: function () {}, 15575 15576 _setPosition: function (pos) { 15577 var subX, subY, 15578 map = this._map, 15579 container = this._container, 15580 centerPoint = map.latLngToContainerPoint(map.getCenter()), 15581 tooltipPoint = map.layerPointToContainerPoint(pos), 15582 direction = this.options.direction, 15583 tooltipWidth = container.offsetWidth, 15584 tooltipHeight = container.offsetHeight, 15585 offset = toPoint(this.options.offset), 15586 anchor = this._getAnchor(); 15587 15588 if (direction === 'top') { 15589 subX = tooltipWidth / 2; 15590 subY = tooltipHeight; 15591 } else if (direction === 'bottom') { 15592 subX = tooltipWidth / 2; 15593 subY = 0; 15594 } else if (direction === 'center') { 15595 subX = tooltipWidth / 2; 15596 subY = tooltipHeight / 2; 15597 } else if (direction === 'right') { 15598 subX = 0; 15599 subY = tooltipHeight / 2; 15600 } else if (direction === 'left') { 15601 subX = tooltipWidth; 15602 subY = tooltipHeight / 2; 15603 } else if (tooltipPoint.x < centerPoint.x) { 15604 direction = 'right'; 15605 subX = 0; 15606 subY = tooltipHeight / 2; 15607 } else { 15608 direction = 'left'; 15609 subX = tooltipWidth + (offset.x + anchor.x) * 2; 15610 subY = tooltipHeight / 2; 15611 } 15612 15613 pos = pos.subtract(toPoint(subX, subY, true)).add(offset).add(anchor); 15614 15615 removeClass(container, 'leaflet-tooltip-right'); 15616 removeClass(container, 'leaflet-tooltip-left'); 15617 removeClass(container, 'leaflet-tooltip-top'); 15618 removeClass(container, 'leaflet-tooltip-bottom'); 15619 addClass(container, 'leaflet-tooltip-' + direction); 15620 setPosition(container, pos); 15621 }, 15622 15623 _updatePosition: function () { 15624 var pos = this._map.latLngToLayerPoint(this._latlng); 15625 this._setPosition(pos); 15626 }, 15627 15628 setOpacity: function (opacity) { 15629 this.options.opacity = opacity; 15630 15631 if (this._container) { 15632 setOpacity(this._container, opacity); 15633 } 15634 }, 15635 15636 _animateZoom: function (e) { 15637 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); 15638 this._setPosition(pos); 15639 }, 15640 15641 _getAnchor: function () { 15642 // Where should we anchor the tooltip on the source layer? 15643 return toPoint(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]); 15644 } 15645 15646 }); 15647 15648 // @namespace Tooltip 15649 // @factory L.tooltip(options?: Tooltip options, source?: Layer) 15650 // Instantiates a `Tooltip` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers. 15651 // @alternative 15652 // @factory L.tooltip(latlng: LatLng, options?: Tooltip options) 15653 // Instantiates a `Tooltip` object given `latlng` where the tooltip will open and an optional `options` object that describes its appearance and location. 15654 var tooltip = function (options, source) { 15655 return new Tooltip(options, source); 15656 }; 15657 15658 // @namespace Map 15659 // @section Methods for Layers and Controls 15660 Map.include({ 15661 15662 // @method openTooltip(tooltip: Tooltip): this 15663 // Opens the specified tooltip. 15664 // @alternative 15665 // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this 15666 // Creates a tooltip with the specified content and options and open it. 15667 openTooltip: function (tooltip, latlng, options) { 15668 this._initOverlay(Tooltip, tooltip, latlng, options) 15669 .openOn(this); 15670 15671 return this; 15672 }, 15673 15674 // @method closeTooltip(tooltip: Tooltip): this 15675 // Closes the tooltip given as parameter. 15676 closeTooltip: function (tooltip) { 15677 tooltip.close(); 15678 return this; 15679 } 15680 15681 }); 15682 15683 /* 15684 * @namespace Layer 15685 * @section Tooltip methods example 15686 * 15687 * All layers share a set of methods convenient for binding tooltips to it. 15688 * 15689 * ```js 15690 * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map); 15691 * layer.openTooltip(); 15692 * layer.closeTooltip(); 15693 * ``` 15694 */ 15695 15696 // @section Tooltip methods 15697 Layer.include({ 15698 15699 // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this 15700 // Binds a tooltip to the layer with the passed `content` and sets up the 15701 // necessary event listeners. If a `Function` is passed it will receive 15702 // the layer as the first argument and should return a `String` or `HTMLElement`. 15703 bindTooltip: function (content, options) { 15704 15705 if (this._tooltip && this.isTooltipOpen()) { 15706 this.unbindTooltip(); 15707 } 15708 15709 this._tooltip = this._initOverlay(Tooltip, this._tooltip, content, options); 15710 this._initTooltipInteractions(); 15711 15712 if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) { 15713 this.openTooltip(); 15714 } 15715 15716 return this; 15717 }, 15718 15719 // @method unbindTooltip(): this 15720 // Removes the tooltip previously bound with `bindTooltip`. 15721 unbindTooltip: function () { 15722 if (this._tooltip) { 15723 this._initTooltipInteractions(true); 15724 this.closeTooltip(); 15725 this._tooltip = null; 15726 } 15727 return this; 15728 }, 15729 15730 _initTooltipInteractions: function (remove) { 15731 if (!remove && this._tooltipHandlersAdded) { return; } 15732 var onOff = remove ? 'off' : 'on', 15733 events = { 15734 remove: this.closeTooltip, 15735 move: this._moveTooltip 15736 }; 15737 if (!this._tooltip.options.permanent) { 15738 events.mouseover = this._openTooltip; 15739 events.mouseout = this.closeTooltip; 15740 events.click = this._openTooltip; 15741 if (this._map) { 15742 this._addFocusListeners(); 15743 } else { 15744 events.add = this._addFocusListeners; 15745 } 15746 } else { 15747 events.add = this._openTooltip; 15748 } 15749 if (this._tooltip.options.sticky) { 15750 events.mousemove = this._moveTooltip; 15751 } 15752 this[onOff](events); 15753 this._tooltipHandlersAdded = !remove; 15754 }, 15755 15756 // @method openTooltip(latlng?: LatLng): this 15757 // Opens the bound tooltip at the specified `latlng` or at the default tooltip anchor if no `latlng` is passed. 15758 openTooltip: function (latlng) { 15759 if (this._tooltip) { 15760 if (!(this instanceof FeatureGroup)) { 15761 this._tooltip._source = this; 15762 } 15763 if (this._tooltip._prepareOpen(latlng)) { 15764 // open the tooltip on the map 15765 this._tooltip.openOn(this._map); 15766 15767 if (this.getElement) { 15768 this._setAriaDescribedByOnLayer(this); 15769 } else if (this.eachLayer) { 15770 this.eachLayer(this._setAriaDescribedByOnLayer, this); 15771 } 15772 } 15773 } 15774 return this; 15775 }, 15776 15777 // @method closeTooltip(): this 15778 // Closes the tooltip bound to this layer if it is open. 15779 closeTooltip: function () { 15780 if (this._tooltip) { 15781 return this._tooltip.close(); 15782 } 15783 }, 15784 15785 // @method toggleTooltip(): this 15786 // Opens or closes the tooltip bound to this layer depending on its current state. 15787 toggleTooltip: function () { 15788 if (this._tooltip) { 15789 this._tooltip.toggle(this); 15790 } 15791 return this; 15792 }, 15793 15794 // @method isTooltipOpen(): boolean 15795 // Returns `true` if the tooltip bound to this layer is currently open. 15796 isTooltipOpen: function () { 15797 return this._tooltip.isOpen(); 15798 }, 15799 15800 // @method setTooltipContent(content: String|HTMLElement|Tooltip): this 15801 // Sets the content of the tooltip bound to this layer. 15802 setTooltipContent: function (content) { 15803 if (this._tooltip) { 15804 this._tooltip.setContent(content); 15805 } 15806 return this; 15807 }, 15808 15809 // @method getTooltip(): Tooltip 15810 // Returns the tooltip bound to this layer. 15811 getTooltip: function () { 15812 return this._tooltip; 15813 }, 15814 15815 _addFocusListeners: function () { 15816 if (this.getElement) { 15817 this._addFocusListenersOnLayer(this); 15818 } else if (this.eachLayer) { 15819 this.eachLayer(this._addFocusListenersOnLayer, this); 15820 } 15821 }, 15822 15823 _addFocusListenersOnLayer: function (layer) { 15824 var el = typeof layer.getElement === 'function' && layer.getElement(); 15825 if (el) { 15826 on(el, 'focus', function () { 15827 this._tooltip._source = layer; 15828 this.openTooltip(); 15829 }, this); 15830 on(el, 'blur', this.closeTooltip, this); 15831 } 15832 }, 15833 15834 _setAriaDescribedByOnLayer: function (layer) { 15835 var el = typeof layer.getElement === 'function' && layer.getElement(); 15836 if (el) { 15837 el.setAttribute('aria-describedby', this._tooltip._container.id); 15838 } 15839 }, 15840 15841 15842 _openTooltip: function (e) { 15843 if (!this._tooltip || !this._map) { 15844 return; 15845 } 15846 15847 // If the map is moving, we will show the tooltip after it's done. 15848 if (this._map.dragging && this._map.dragging.moving() && !this._openOnceFlag) { 15849 this._openOnceFlag = true; 15850 var that = this; 15851 this._map.once('moveend', function () { 15852 that._openOnceFlag = false; 15853 that._openTooltip(e); 15854 }); 15855 return; 15856 } 15857 15858 this._tooltip._source = e.layer || e.target; 15859 15860 this.openTooltip(this._tooltip.options.sticky ? e.latlng : undefined); 15861 }, 15862 15863 _moveTooltip: function (e) { 15864 var latlng = e.latlng, containerPoint, layerPoint; 15865 if (this._tooltip.options.sticky && e.originalEvent) { 15866 containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent); 15867 layerPoint = this._map.containerPointToLayerPoint(containerPoint); 15868 latlng = this._map.layerPointToLatLng(layerPoint); 15869 } 15870 this._tooltip.setLatLng(latlng); 15871 } 15872 }); 15873 15874 /* 15875 * @class DivIcon 15876 * @aka L.DivIcon 15877 * @inherits Icon 15878 * 15879 * Represents a lightweight icon for markers that uses a simple `<div>` 15880 * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options. 15881 * 15882 * @example 15883 * ```js 15884 * var myIcon = L.divIcon({className: 'my-div-icon'}); 15885 * // you can set .my-div-icon styles in CSS 15886 * 15887 * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map); 15888 * ``` 15889 * 15890 * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow. 15891 */ 15892 15893 var DivIcon = Icon.extend({ 15894 options: { 15895 // @section 15896 // @aka DivIcon options 15897 iconSize: [12, 12], // also can be set through CSS 15898 15899 // iconAnchor: (Point), 15900 // popupAnchor: (Point), 15901 15902 // @option html: String|HTMLElement = '' 15903 // Custom HTML code to put inside the div element, empty by default. Alternatively, 15904 // an instance of `HTMLElement`. 15905 html: false, 15906 15907 // @option bgPos: Point = [0, 0] 15908 // Optional relative position of the background, in pixels 15909 bgPos: null, 15910 15911 className: 'leaflet-div-icon' 15912 }, 15913 15914 createIcon: function (oldIcon) { 15915 var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'), 15916 options = this.options; 15917 15918 if (options.html instanceof Element) { 15919 empty(div); 15920 div.appendChild(options.html); 15921 } else { 15922 div.innerHTML = options.html !== false ? options.html : ''; 15923 } 15924 15925 if (options.bgPos) { 15926 var bgPos = toPoint(options.bgPos); 15927 div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px'; 15928 } 15929 this._setIconStyles(div, 'icon'); 15930 15931 return div; 15932 }, 15933 15934 createShadow: function () { 15935 return null; 15936 } 15937 }); 15938 15939 // @factory L.divIcon(options: DivIcon options) 15940 // Creates a `DivIcon` instance with the given options. 15941 function divIcon(options) { 15942 return new DivIcon(options); 15943 } 15944 15945 Icon.Default = IconDefault; 15946 15947 /* 15948 * @class GridLayer 15949 * @inherits Layer 15950 * @aka L.GridLayer 15951 * 15952 * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`. 15953 * GridLayer can be extended to create a tiled grid of HTML elements like `<canvas>`, `<img>` or `<div>`. GridLayer will handle creating and animating these DOM elements for you. 15954 * 15955 * 15956 * @section Synchronous usage 15957 * @example 15958 * 15959 * To create a custom layer, extend GridLayer and implement the `createTile()` method, which will be passed a `Point` object with the `x`, `y`, and `z` (zoom level) coordinates to draw your tile. 15960 * 15961 * ```js 15962 * var CanvasLayer = L.GridLayer.extend({ 15963 * createTile: function(coords){ 15964 * // create a <canvas> element for drawing 15965 * var tile = L.DomUtil.create('canvas', 'leaflet-tile'); 15966 * 15967 * // setup tile width and height according to the options 15968 * var size = this.getTileSize(); 15969 * tile.width = size.x; 15970 * tile.height = size.y; 15971 * 15972 * // get a canvas context and draw something on it using coords.x, coords.y and coords.z 15973 * var ctx = tile.getContext('2d'); 15974 * 15975 * // return the tile so it can be rendered on screen 15976 * return tile; 15977 * } 15978 * }); 15979 * ``` 15980 * 15981 * @section Asynchronous usage 15982 * @example 15983 * 15984 * Tile creation can also be asynchronous, this is useful when using a third-party drawing library. Once the tile is finished drawing it can be passed to the `done()` callback. 15985 * 15986 * ```js 15987 * var CanvasLayer = L.GridLayer.extend({ 15988 * createTile: function(coords, done){ 15989 * var error; 15990 * 15991 * // create a <canvas> element for drawing 15992 * var tile = L.DomUtil.create('canvas', 'leaflet-tile'); 15993 * 15994 * // setup tile width and height according to the options 15995 * var size = this.getTileSize(); 15996 * tile.width = size.x; 15997 * tile.height = size.y; 15998 * 15999 * // draw something asynchronously and pass the tile to the done() callback 16000 * setTimeout(function() { 16001 * done(error, tile); 16002 * }, 1000); 16003 * 16004 * return tile; 16005 * } 16006 * }); 16007 * ``` 16008 * 16009 * @section 16010 */ 16011 16012 16013 var GridLayer = Layer.extend({ 16014 16015 // @section 16016 // @aka GridLayer options 16017 options: { 16018 // @option tileSize: Number|Point = 256 16019 // Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise. 16020 tileSize: 256, 16021 16022 // @option opacity: Number = 1.0 16023 // Opacity of the tiles. Can be used in the `createTile()` function. 16024 opacity: 1, 16025 16026 // @option updateWhenIdle: Boolean = (depends) 16027 // Load new tiles only when panning ends. 16028 // `true` by default on mobile browsers, in order to avoid too many requests and keep smooth navigation. 16029 // `false` otherwise in order to display new tiles _during_ panning, since it is easy to pan outside the 16030 // [`keepBuffer`](#gridlayer-keepbuffer) option in desktop browsers. 16031 updateWhenIdle: Browser.mobile, 16032 16033 // @option updateWhenZooming: Boolean = true 16034 // By default, a smooth zoom animation (during a [touch zoom](#map-touchzoom) or a [`flyTo()`](#map-flyto)) will update grid layers every integer zoom level. Setting this option to `false` will update the grid layer only when the smooth animation ends. 16035 updateWhenZooming: true, 16036 16037 // @option updateInterval: Number = 200 16038 // Tiles will not update more than once every `updateInterval` milliseconds when panning. 16039 updateInterval: 200, 16040 16041 // @option zIndex: Number = 1 16042 // The explicit zIndex of the tile layer. 16043 zIndex: 1, 16044 16045 // @option bounds: LatLngBounds = undefined 16046 // If set, tiles will only be loaded inside the set `LatLngBounds`. 16047 bounds: null, 16048 16049 // @option minZoom: Number = 0 16050 // The minimum zoom level down to which this layer will be displayed (inclusive). 16051 minZoom: 0, 16052 16053 // @option maxZoom: Number = undefined 16054 // The maximum zoom level up to which this layer will be displayed (inclusive). 16055 maxZoom: undefined, 16056 16057 // @option maxNativeZoom: Number = undefined 16058 // Maximum zoom number the tile source has available. If it is specified, 16059 // the tiles on all zoom levels higher than `maxNativeZoom` will be loaded 16060 // from `maxNativeZoom` level and auto-scaled. 16061 maxNativeZoom: undefined, 16062 16063 // @option minNativeZoom: Number = undefined 16064 // Minimum zoom number the tile source has available. If it is specified, 16065 // the tiles on all zoom levels lower than `minNativeZoom` will be loaded 16066 // from `minNativeZoom` level and auto-scaled. 16067 minNativeZoom: undefined, 16068 16069 // @option noWrap: Boolean = false 16070 // Whether the layer is wrapped around the antimeridian. If `true`, the 16071 // GridLayer will only be displayed once at low zoom levels. Has no 16072 // effect when the [map CRS](#map-crs) doesn't wrap around. Can be used 16073 // in combination with [`bounds`](#gridlayer-bounds) to prevent requesting 16074 // tiles outside the CRS limits. 16075 noWrap: false, 16076 16077 // @option pane: String = 'tilePane' 16078 // `Map pane` where the grid layer will be added. 16079 pane: 'tilePane', 16080 16081 // @option className: String = '' 16082 // A custom class name to assign to the tile layer. Empty by default. 16083 className: '', 16084 16085 // @option keepBuffer: Number = 2 16086 // When panning the map, keep this many rows and columns of tiles before unloading them. 16087 keepBuffer: 2 16088 }, 16089 16090 initialize: function (options) { 16091 setOptions(this, options); 16092 }, 16093 16094 onAdd: function () { 16095 this._initContainer(); 16096 16097 this._levels = {}; 16098 this._tiles = {}; 16099 16100 this._resetView(); // implicit _update() call 16101 }, 16102 16103 beforeAdd: function (map) { 16104 map._addZoomLimit(this); 16105 }, 16106 16107 onRemove: function (map) { 16108 this._removeAllTiles(); 16109 remove(this._container); 16110 map._removeZoomLimit(this); 16111 this._container = null; 16112 this._tileZoom = undefined; 16113 }, 16114 16115 // @method bringToFront: this 16116 // Brings the tile layer to the top of all tile layers. 16117 bringToFront: function () { 16118 if (this._map) { 16119 toFront(this._container); 16120 this._setAutoZIndex(Math.max); 16121 } 16122 return this; 16123 }, 16124 16125 // @method bringToBack: this 16126 // Brings the tile layer to the bottom of all tile layers. 16127 bringToBack: function () { 16128 if (this._map) { 16129 toBack(this._container); 16130 this._setAutoZIndex(Math.min); 16131 } 16132 return this; 16133 }, 16134 16135 // @method getContainer: HTMLElement 16136 // Returns the HTML element that contains the tiles for this layer. 16137 getContainer: function () { 16138 return this._container; 16139 }, 16140 16141 // @method setOpacity(opacity: Number): this 16142 // Changes the [opacity](#gridlayer-opacity) of the grid layer. 16143 setOpacity: function (opacity) { 16144 this.options.opacity = opacity; 16145 this._updateOpacity(); 16146 return this; 16147 }, 16148 16149 // @method setZIndex(zIndex: Number): this 16150 // Changes the [zIndex](#gridlayer-zindex) of the grid layer. 16151 setZIndex: function (zIndex) { 16152 this.options.zIndex = zIndex; 16153 this._updateZIndex(); 16154 16155 return this; 16156 }, 16157 16158 // @method isLoading: Boolean 16159 // Returns `true` if any tile in the grid layer has not finished loading. 16160 isLoading: function () { 16161 return this._loading; 16162 }, 16163 16164 // @method redraw: this 16165 // Causes the layer to clear all the tiles and request them again. 16166 redraw: function () { 16167 if (this._map) { 16168 this._removeAllTiles(); 16169 var tileZoom = this._clampZoom(this._map.getZoom()); 16170 if (tileZoom !== this._tileZoom) { 16171 this._tileZoom = tileZoom; 16172 this._updateLevels(); 16173 } 16174 this._update(); 16175 } 16176 return this; 16177 }, 16178 16179 getEvents: function () { 16180 var events = { 16181 viewprereset: this._invalidateAll, 16182 viewreset: this._resetView, 16183 zoom: this._resetView, 16184 moveend: this._onMoveEnd 16185 }; 16186 16187 if (!this.options.updateWhenIdle) { 16188 // update tiles on move, but not more often than once per given interval 16189 if (!this._onMove) { 16190 this._onMove = throttle(this._onMoveEnd, this.options.updateInterval, this); 16191 } 16192 16193 events.move = this._onMove; 16194 } 16195 16196 if (this._zoomAnimated) { 16197 events.zoomanim = this._animateZoom; 16198 } 16199 16200 return events; 16201 }, 16202 16203 // @section Extension methods 16204 // Layers extending `GridLayer` shall reimplement the following method. 16205 // @method createTile(coords: Object, done?: Function): HTMLElement 16206 // Called only internally, must be overridden by classes extending `GridLayer`. 16207 // Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback 16208 // is specified, it must be called when the tile has finished loading and drawing. 16209 createTile: function () { 16210 return document.createElement('div'); 16211 }, 16212 16213 // @section 16214 // @method getTileSize: Point 16215 // Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method. 16216 getTileSize: function () { 16217 var s = this.options.tileSize; 16218 return s instanceof Point ? s : new Point(s, s); 16219 }, 16220 16221 _updateZIndex: function () { 16222 if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) { 16223 this._container.style.zIndex = this.options.zIndex; 16224 } 16225 }, 16226 16227 _setAutoZIndex: function (compare) { 16228 // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back) 16229 16230 var layers = this.getPane().children, 16231 edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min 16232 16233 for (var i = 0, len = layers.length, zIndex; i < len; i++) { 16234 16235 zIndex = layers[i].style.zIndex; 16236 16237 if (layers[i] !== this._container && zIndex) { 16238 edgeZIndex = compare(edgeZIndex, +zIndex); 16239 } 16240 } 16241 16242 if (isFinite(edgeZIndex)) { 16243 this.options.zIndex = edgeZIndex + compare(-1, 1); 16244 this._updateZIndex(); 16245 } 16246 }, 16247 16248 _updateOpacity: function () { 16249 if (!this._map) { return; } 16250 16251 // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles 16252 if (Browser.ielt9) { return; } 16253 16254 setOpacity(this._container, this.options.opacity); 16255 16256 var now = +new Date(), 16257 nextFrame = false, 16258 willPrune = false; 16259 16260 for (var key in this._tiles) { 16261 var tile = this._tiles[key]; 16262 if (!tile.current || !tile.loaded) { continue; } 16263 16264 var fade = Math.min(1, (now - tile.loaded) / 200); 16265 16266 setOpacity(tile.el, fade); 16267 if (fade < 1) { 16268 nextFrame = true; 16269 } else { 16270 if (tile.active) { 16271 willPrune = true; 16272 } else { 16273 this._onOpaqueTile(tile); 16274 } 16275 tile.active = true; 16276 } 16277 } 16278 16279 if (willPrune && !this._noPrune) { this._pruneTiles(); } 16280 16281 if (nextFrame) { 16282 cancelAnimFrame(this._fadeFrame); 16283 this._fadeFrame = requestAnimFrame(this._updateOpacity, this); 16284 } 16285 }, 16286 16287 _onOpaqueTile: falseFn, 16288 16289 _initContainer: function () { 16290 if (this._container) { return; } 16291 16292 this._container = create$1('div', 'leaflet-layer ' + (this.options.className || '')); 16293 this._updateZIndex(); 16294 16295 if (this.options.opacity < 1) { 16296 this._updateOpacity(); 16297 } 16298 16299 this.getPane().appendChild(this._container); 16300 }, 16301 16302 _updateLevels: function () { 16303 16304 var zoom = this._tileZoom, 16305 maxZoom = this.options.maxZoom; 16306 16307 if (zoom === undefined) { return undefined; } 16308 16309 for (var z in this._levels) { 16310 z = Number(z); 16311 if (this._levels[z].el.children.length || z === zoom) { 16312 this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z); 16313 this._onUpdateLevel(z); 16314 } else { 16315 remove(this._levels[z].el); 16316 this._removeTilesAtZoom(z); 16317 this._onRemoveLevel(z); 16318 delete this._levels[z]; 16319 } 16320 } 16321 16322 var level = this._levels[zoom], 16323 map = this._map; 16324 16325 if (!level) { 16326 level = this._levels[zoom] = {}; 16327 16328 level.el = create$1('div', 'leaflet-tile-container leaflet-zoom-animated', this._container); 16329 level.el.style.zIndex = maxZoom; 16330 16331 level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round(); 16332 level.zoom = zoom; 16333 16334 this._setZoomTransform(level, map.getCenter(), map.getZoom()); 16335 16336 // force the browser to consider the newly added element for transition 16337 falseFn(level.el.offsetWidth); 16338 16339 this._onCreateLevel(level); 16340 } 16341 16342 this._level = level; 16343 16344 return level; 16345 }, 16346 16347 _onUpdateLevel: falseFn, 16348 16349 _onRemoveLevel: falseFn, 16350 16351 _onCreateLevel: falseFn, 16352 16353 _pruneTiles: function () { 16354 if (!this._map) { 16355 return; 16356 } 16357 16358 var key, tile; 16359 16360 var zoom = this._map.getZoom(); 16361 if (zoom > this.options.maxZoom || 16362 zoom < this.options.minZoom) { 16363 this._removeAllTiles(); 16364 return; 16365 } 16366 16367 for (key in this._tiles) { 16368 tile = this._tiles[key]; 16369 tile.retain = tile.current; 16370 } 16371 16372 for (key in this._tiles) { 16373 tile = this._tiles[key]; 16374 if (tile.current && !tile.active) { 16375 var coords = tile.coords; 16376 if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) { 16377 this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2); 16378 } 16379 } 16380 } 16381 16382 for (key in this._tiles) { 16383 if (!this._tiles[key].retain) { 16384 this._removeTile(key); 16385 } 16386 } 16387 }, 16388 16389 _removeTilesAtZoom: function (zoom) { 16390 for (var key in this._tiles) { 16391 if (this._tiles[key].coords.z !== zoom) { 16392 continue; 16393 } 16394 this._removeTile(key); 16395 } 16396 }, 16397 16398 _removeAllTiles: function () { 16399 for (var key in this._tiles) { 16400 this._removeTile(key); 16401 } 16402 }, 16403 16404 _invalidateAll: function () { 16405 for (var z in this._levels) { 16406 remove(this._levels[z].el); 16407 this._onRemoveLevel(Number(z)); 16408 delete this._levels[z]; 16409 } 16410 this._removeAllTiles(); 16411 16412 this._tileZoom = undefined; 16413 }, 16414 16415 _retainParent: function (x, y, z, minZoom) { 16416 var x2 = Math.floor(x / 2), 16417 y2 = Math.floor(y / 2), 16418 z2 = z - 1, 16419 coords2 = new Point(+x2, +y2); 16420 coords2.z = +z2; 16421 16422 var key = this._tileCoordsToKey(coords2), 16423 tile = this._tiles[key]; 16424 16425 if (tile && tile.active) { 16426 tile.retain = true; 16427 return true; 16428 16429 } else if (tile && tile.loaded) { 16430 tile.retain = true; 16431 } 16432 16433 if (z2 > minZoom) { 16434 return this._retainParent(x2, y2, z2, minZoom); 16435 } 16436 16437 return false; 16438 }, 16439 16440 _retainChildren: function (x, y, z, maxZoom) { 16441 16442 for (var i = 2 * x; i < 2 * x + 2; i++) { 16443 for (var j = 2 * y; j < 2 * y + 2; j++) { 16444 16445 var coords = new Point(i, j); 16446 coords.z = z + 1; 16447 16448 var key = this._tileCoordsToKey(coords), 16449 tile = this._tiles[key]; 16450 16451 if (tile && tile.active) { 16452 tile.retain = true; 16453 continue; 16454 16455 } else if (tile && tile.loaded) { 16456 tile.retain = true; 16457 } 16458 16459 if (z + 1 < maxZoom) { 16460 this._retainChildren(i, j, z + 1, maxZoom); 16461 } 16462 } 16463 } 16464 }, 16465 16466 _resetView: function (e) { 16467 var animating = e && (e.pinch || e.flyTo); 16468 this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating); 16469 }, 16470 16471 _animateZoom: function (e) { 16472 this._setView(e.center, e.zoom, true, e.noUpdate); 16473 }, 16474 16475 _clampZoom: function (zoom) { 16476 var options = this.options; 16477 16478 if (undefined !== options.minNativeZoom && zoom < options.minNativeZoom) { 16479 return options.minNativeZoom; 16480 } 16481 16482 if (undefined !== options.maxNativeZoom && options.maxNativeZoom < zoom) { 16483 return options.maxNativeZoom; 16484 } 16485 16486 return zoom; 16487 }, 16488 16489 _setView: function (center, zoom, noPrune, noUpdate) { 16490 var tileZoom = Math.round(zoom); 16491 if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) || 16492 (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) { 16493 tileZoom = undefined; 16494 } else { 16495 tileZoom = this._clampZoom(tileZoom); 16496 } 16497 16498 var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom); 16499 16500 if (!noUpdate || tileZoomChanged) { 16501 16502 this._tileZoom = tileZoom; 16503 16504 if (this._abortLoading) { 16505 this._abortLoading(); 16506 } 16507 16508 this._updateLevels(); 16509 this._resetGrid(); 16510 16511 if (tileZoom !== undefined) { 16512 this._update(center); 16513 } 16514 16515 if (!noPrune) { 16516 this._pruneTiles(); 16517 } 16518 16519 // Flag to prevent _updateOpacity from pruning tiles during 16520 // a zoom anim or a pinch gesture 16521 this._noPrune = !!noPrune; 16522 } 16523 16524 this._setZoomTransforms(center, zoom); 16525 }, 16526 16527 _setZoomTransforms: function (center, zoom) { 16528 for (var i in this._levels) { 16529 this._setZoomTransform(this._levels[i], center, zoom); 16530 } 16531 }, 16532 16533 _setZoomTransform: function (level, center, zoom) { 16534 var scale = this._map.getZoomScale(zoom, level.zoom), 16535 translate = level.origin.multiplyBy(scale) 16536 .subtract(this._map._getNewPixelOrigin(center, zoom)).round(); 16537 16538 if (Browser.any3d) { 16539 setTransform(level.el, translate, scale); 16540 } else { 16541 setPosition(level.el, translate); 16542 } 16543 }, 16544 16545 _resetGrid: function () { 16546 var map = this._map, 16547 crs = map.options.crs, 16548 tileSize = this._tileSize = this.getTileSize(), 16549 tileZoom = this._tileZoom; 16550 16551 var bounds = this._map.getPixelWorldBounds(this._tileZoom); 16552 if (bounds) { 16553 this._globalTileRange = this._pxBoundsToTileRange(bounds); 16554 } 16555 16556 this._wrapX = crs.wrapLng && !this.options.noWrap && [ 16557 Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x), 16558 Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y) 16559 ]; 16560 this._wrapY = crs.wrapLat && !this.options.noWrap && [ 16561 Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x), 16562 Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y) 16563 ]; 16564 }, 16565 16566 _onMoveEnd: function () { 16567 if (!this._map || this._map._animatingZoom) { return; } 16568 16569 this._update(); 16570 }, 16571 16572 _getTiledPixelBounds: function (center) { 16573 var map = this._map, 16574 mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(), 16575 scale = map.getZoomScale(mapZoom, this._tileZoom), 16576 pixelCenter = map.project(center, this._tileZoom).floor(), 16577 halfSize = map.getSize().divideBy(scale * 2); 16578 16579 return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize)); 16580 }, 16581 16582 // Private method to load tiles in the grid's active zoom level according to map bounds 16583 _update: function (center) { 16584 var map = this._map; 16585 if (!map) { return; } 16586 var zoom = this._clampZoom(map.getZoom()); 16587 16588 if (center === undefined) { center = map.getCenter(); } 16589 if (this._tileZoom === undefined) { return; } // if out of minzoom/maxzoom 16590 16591 var pixelBounds = this._getTiledPixelBounds(center), 16592 tileRange = this._pxBoundsToTileRange(pixelBounds), 16593 tileCenter = tileRange.getCenter(), 16594 queue = [], 16595 margin = this.options.keepBuffer, 16596 noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]), 16597 tileRange.getTopRight().add([margin, -margin])); 16598 16599 // Sanity check: panic if the tile range contains Infinity somewhere. 16600 if (!(isFinite(tileRange.min.x) && 16601 isFinite(tileRange.min.y) && 16602 isFinite(tileRange.max.x) && 16603 isFinite(tileRange.max.y))) { throw new Error('Attempted to load an infinite number of tiles'); } 16604 16605 for (var key in this._tiles) { 16606 var c = this._tiles[key].coords; 16607 if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) { 16608 this._tiles[key].current = false; 16609 } 16610 } 16611 16612 // _update just loads more tiles. If the tile zoom level differs too much 16613 // from the map's, let _setView reset levels and prune old tiles. 16614 if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; } 16615 16616 // create a queue of coordinates to load tiles from 16617 for (var j = tileRange.min.y; j <= tileRange.max.y; j++) { 16618 for (var i = tileRange.min.x; i <= tileRange.max.x; i++) { 16619 var coords = new Point(i, j); 16620 coords.z = this._tileZoom; 16621 16622 if (!this._isValidTile(coords)) { continue; } 16623 16624 var tile = this._tiles[this._tileCoordsToKey(coords)]; 16625 if (tile) { 16626 tile.current = true; 16627 } else { 16628 queue.push(coords); 16629 } 16630 } 16631 } 16632 16633 // sort tile queue to load tiles in order of their distance to center 16634 queue.sort(function (a, b) { 16635 return a.distanceTo(tileCenter) - b.distanceTo(tileCenter); 16636 }); 16637 16638 if (queue.length !== 0) { 16639 // if it's the first batch of tiles to load 16640 if (!this._loading) { 16641 this._loading = true; 16642 // @event loading: Event 16643 // Fired when the grid layer starts loading tiles. 16644 this.fire('loading'); 16645 } 16646 16647 // create DOM fragment to append tiles in one batch 16648 var fragment = document.createDocumentFragment(); 16649 16650 for (i = 0; i < queue.length; i++) { 16651 this._addTile(queue[i], fragment); 16652 } 16653 16654 this._level.el.appendChild(fragment); 16655 } 16656 }, 16657 16658 _isValidTile: function (coords) { 16659 var crs = this._map.options.crs; 16660 16661 if (!crs.infinite) { 16662 // don't load tile if it's out of bounds and not wrapped 16663 var bounds = this._globalTileRange; 16664 if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) || 16665 (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; } 16666 } 16667 16668 if (!this.options.bounds) { return true; } 16669 16670 // don't load tile if it doesn't intersect the bounds in options 16671 var tileBounds = this._tileCoordsToBounds(coords); 16672 return toLatLngBounds(this.options.bounds).overlaps(tileBounds); 16673 }, 16674 16675 _keyToBounds: function (key) { 16676 return this._tileCoordsToBounds(this._keyToTileCoords(key)); 16677 }, 16678 16679 _tileCoordsToNwSe: function (coords) { 16680 var map = this._map, 16681 tileSize = this.getTileSize(), 16682 nwPoint = coords.scaleBy(tileSize), 16683 sePoint = nwPoint.add(tileSize), 16684 nw = map.unproject(nwPoint, coords.z), 16685 se = map.unproject(sePoint, coords.z); 16686 return [nw, se]; 16687 }, 16688 16689 // converts tile coordinates to its geographical bounds 16690 _tileCoordsToBounds: function (coords) { 16691 var bp = this._tileCoordsToNwSe(coords), 16692 bounds = new LatLngBounds(bp[0], bp[1]); 16693 16694 if (!this.options.noWrap) { 16695 bounds = this._map.wrapLatLngBounds(bounds); 16696 } 16697 return bounds; 16698 }, 16699 // converts tile coordinates to key for the tile cache 16700 _tileCoordsToKey: function (coords) { 16701 return coords.x + ':' + coords.y + ':' + coords.z; 16702 }, 16703 16704 // converts tile cache key to coordinates 16705 _keyToTileCoords: function (key) { 16706 var k = key.split(':'), 16707 coords = new Point(+k[0], +k[1]); 16708 coords.z = +k[2]; 16709 return coords; 16710 }, 16711 16712 _removeTile: function (key) { 16713 var tile = this._tiles[key]; 16714 if (!tile) { return; } 16715 16716 remove(tile.el); 16717 16718 delete this._tiles[key]; 16719 16720 // @event tileunload: TileEvent 16721 // Fired when a tile is removed (e.g. when a tile goes off the screen). 16722 this.fire('tileunload', { 16723 tile: tile.el, 16724 coords: this._keyToTileCoords(key) 16725 }); 16726 }, 16727 16728 _initTile: function (tile) { 16729 addClass(tile, 'leaflet-tile'); 16730 16731 var tileSize = this.getTileSize(); 16732 tile.style.width = tileSize.x + 'px'; 16733 tile.style.height = tileSize.y + 'px'; 16734 16735 tile.onselectstart = falseFn; 16736 tile.onmousemove = falseFn; 16737 16738 // update opacity on tiles in IE7-8 because of filter inheritance problems 16739 if (Browser.ielt9 && this.options.opacity < 1) { 16740 setOpacity(tile, this.options.opacity); 16741 } 16742 }, 16743 16744 _addTile: function (coords, container) { 16745 var tilePos = this._getTilePos(coords), 16746 key = this._tileCoordsToKey(coords); 16747 16748 var tile = this.createTile(this._wrapCoords(coords), bind(this._tileReady, this, coords)); 16749 16750 this._initTile(tile); 16751 16752 // if createTile is defined with a second argument ("done" callback), 16753 // we know that tile is async and will be ready later; otherwise 16754 if (this.createTile.length < 2) { 16755 // mark tile as ready, but delay one frame for opacity animation to happen 16756 requestAnimFrame(bind(this._tileReady, this, coords, null, tile)); 16757 } 16758 16759 setPosition(tile, tilePos); 16760 16761 // save tile in cache 16762 this._tiles[key] = { 16763 el: tile, 16764 coords: coords, 16765 current: true 16766 }; 16767 16768 container.appendChild(tile); 16769 // @event tileloadstart: TileEvent 16770 // Fired when a tile is requested and starts loading. 16771 this.fire('tileloadstart', { 16772 tile: tile, 16773 coords: coords 16774 }); 16775 }, 16776 16777 _tileReady: function (coords, err, tile) { 16778 if (err) { 16779 // @event tileerror: TileErrorEvent 16780 // Fired when there is an error loading a tile. 16781 this.fire('tileerror', { 16782 error: err, 16783 tile: tile, 16784 coords: coords 16785 }); 16786 } 16787 16788 var key = this._tileCoordsToKey(coords); 16789 16790 tile = this._tiles[key]; 16791 if (!tile) { return; } 16792 16793 tile.loaded = +new Date(); 16794 if (this._map._fadeAnimated) { 16795 setOpacity(tile.el, 0); 16796 cancelAnimFrame(this._fadeFrame); 16797 this._fadeFrame = requestAnimFrame(this._updateOpacity, this); 16798 } else { 16799 tile.active = true; 16800 this._pruneTiles(); 16801 } 16802 16803 if (!err) { 16804 addClass(tile.el, 'leaflet-tile-loaded'); 16805 16806 // @event tileload: TileEvent 16807 // Fired when a tile loads. 16808 this.fire('tileload', { 16809 tile: tile.el, 16810 coords: coords 16811 }); 16812 } 16813 16814 if (this._noTilesToLoad()) { 16815 this._loading = false; 16816 // @event load: Event 16817 // Fired when the grid layer loaded all visible tiles. 16818 this.fire('load'); 16819 16820 if (Browser.ielt9 || !this._map._fadeAnimated) { 16821 requestAnimFrame(this._pruneTiles, this); 16822 } else { 16823 // Wait a bit more than 0.2 secs (the duration of the tile fade-in) 16824 // to trigger a pruning. 16825 setTimeout(bind(this._pruneTiles, this), 250); 16826 } 16827 } 16828 }, 16829 16830 _getTilePos: function (coords) { 16831 return coords.scaleBy(this.getTileSize()).subtract(this._level.origin); 16832 }, 16833 16834 _wrapCoords: function (coords) { 16835 var newCoords = new Point( 16836 this._wrapX ? wrapNum(coords.x, this._wrapX) : coords.x, 16837 this._wrapY ? wrapNum(coords.y, this._wrapY) : coords.y); 16838 newCoords.z = coords.z; 16839 return newCoords; 16840 }, 16841 16842 _pxBoundsToTileRange: function (bounds) { 16843 var tileSize = this.getTileSize(); 16844 return new Bounds( 16845 bounds.min.unscaleBy(tileSize).floor(), 16846 bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1])); 16847 }, 16848 16849 _noTilesToLoad: function () { 16850 for (var key in this._tiles) { 16851 if (!this._tiles[key].loaded) { return false; } 16852 } 16853 return true; 16854 } 16855 }); 16856 16857 // @factory L.gridLayer(options?: GridLayer options) 16858 // Creates a new instance of GridLayer with the supplied options. 16859 function gridLayer(options) { 16860 return new GridLayer(options); 16861 } 16862 16863 /* 16864 * @class TileLayer 16865 * @inherits GridLayer 16866 * @aka L.TileLayer 16867 * Used to load and display tile layers on the map. Note that most tile servers require attribution, which you can set under `Layer`. Extends `GridLayer`. 16868 * 16869 * @example 16870 * 16871 * ```js 16872 * L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map); 16873 * ``` 16874 * 16875 * @section URL template 16876 * @example 16877 * 16878 * A string of the following form: 16879 * 16880 * ``` 16881 * 'https://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png' 16882 * ``` 16883 * 16884 * `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; `a`, `b` or `c` by default, can be omitted), `{z}` â zoom level, `{x}` and `{y}` â tile coordinates. `{r}` can be used to add "@2x" to the URL to load retina tiles. 16885 * 16886 * You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this: 16887 * 16888 * ``` 16889 * L.tileLayer('https://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'}); 16890 * ``` 16891 */ 16892 16893 16894 var TileLayer = GridLayer.extend({ 16895 16896 // @section 16897 // @aka TileLayer options 16898 options: { 16899 // @option minZoom: Number = 0 16900 // The minimum zoom level down to which this layer will be displayed (inclusive). 16901 minZoom: 0, 16902 16903 // @option maxZoom: Number = 18 16904 // The maximum zoom level up to which this layer will be displayed (inclusive). 16905 maxZoom: 18, 16906 16907 // @option subdomains: String|String[] = 'abc' 16908 // Subdomains of the tile service. Can be passed in the form of one string (where each letter is a subdomain name) or an array of strings. 16909 subdomains: 'abc', 16910 16911 // @option errorTileUrl: String = '' 16912 // URL to the tile image to show in place of the tile that failed to load. 16913 errorTileUrl: '', 16914 16915 // @option zoomOffset: Number = 0 16916 // The zoom number used in tile URLs will be offset with this value. 16917 zoomOffset: 0, 16918 16919 // @option tms: Boolean = false 16920 // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services). 16921 tms: false, 16922 16923 // @option zoomReverse: Boolean = false 16924 // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`) 16925 zoomReverse: false, 16926 16927 // @option detectRetina: Boolean = false 16928 // If `true` and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution. 16929 detectRetina: false, 16930 16931 // @option crossOrigin: Boolean|String = false 16932 // Whether the crossOrigin attribute will be added to the tiles. 16933 // If a String is provided, all tiles will have their crossOrigin attribute set to the String provided. This is needed if you want to access tile pixel data. 16934 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values. 16935 crossOrigin: false, 16936 16937 // @option referrerPolicy: Boolean|String = false 16938 // Whether the referrerPolicy attribute will be added to the tiles. 16939 // If a String is provided, all tiles will have their referrerPolicy attribute set to the String provided. 16940 // This may be needed if your map's rendering context has a strict default but your tile provider expects a valid referrer 16941 // (e.g. to validate an API token). 16942 // Refer to [HTMLImageElement.referrerPolicy](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/referrerPolicy) for valid String values. 16943 referrerPolicy: false 16944 }, 16945 16946 initialize: function (url, options) { 16947 16948 this._url = url; 16949 16950 options = setOptions(this, options); 16951 16952 // detecting retina displays, adjusting tileSize and zoom levels 16953 if (options.detectRetina && Browser.retina && options.maxZoom > 0) { 16954 16955 options.tileSize = Math.floor(options.tileSize / 2); 16956 16957 if (!options.zoomReverse) { 16958 options.zoomOffset++; 16959 options.maxZoom = Math.max(options.minZoom, options.maxZoom - 1); 16960 } else { 16961 options.zoomOffset--; 16962 options.minZoom = Math.min(options.maxZoom, options.minZoom + 1); 16963 } 16964 16965 options.minZoom = Math.max(0, options.minZoom); 16966 } else if (!options.zoomReverse) { 16967 // make sure maxZoom is gte minZoom 16968 options.maxZoom = Math.max(options.minZoom, options.maxZoom); 16969 } else { 16970 // make sure minZoom is lte maxZoom 16971 options.minZoom = Math.min(options.maxZoom, options.minZoom); 16972 } 16973 16974 if (typeof options.subdomains === 'string') { 16975 options.subdomains = options.subdomains.split(''); 16976 } 16977 16978 this.on('tileunload', this._onTileRemove); 16979 }, 16980 16981 // @method setUrl(url: String, noRedraw?: Boolean): this 16982 // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`). 16983 // If the URL does not change, the layer will not be redrawn unless 16984 // the noRedraw parameter is set to false. 16985 setUrl: function (url, noRedraw) { 16986 if (this._url === url && noRedraw === undefined) { 16987 noRedraw = true; 16988 } 16989 16990 this._url = url; 16991 16992 if (!noRedraw) { 16993 this.redraw(); 16994 } 16995 return this; 16996 }, 16997 16998 // @method createTile(coords: Object, done?: Function): HTMLElement 16999 // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile) 17000 // to return an `<img>` HTML element with the appropriate image URL given `coords`. The `done` 17001 // callback is called when the tile has been loaded. 17002 createTile: function (coords, done) { 17003 var tile = document.createElement('img'); 17004 17005 on(tile, 'load', bind(this._tileOnLoad, this, done, tile)); 17006 on(tile, 'error', bind(this._tileOnError, this, done, tile)); 17007 17008 if (this.options.crossOrigin || this.options.crossOrigin === '') { 17009 tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin; 17010 } 17011 17012 // for this new option we follow the documented behavior 17013 // more closely by only setting the property when string 17014 if (typeof this.options.referrerPolicy === 'string') { 17015 tile.referrerPolicy = this.options.referrerPolicy; 17016 } 17017 17018 // The alt attribute is set to the empty string, 17019 // allowing screen readers to ignore the decorative image tiles. 17020 // https://www.w3.org/WAI/tutorials/images/decorative/ 17021 // https://www.w3.org/TR/html-aria/#el-img-empty-alt 17022 tile.alt = ''; 17023 17024 tile.src = this.getTileUrl(coords); 17025 17026 return tile; 17027 }, 17028 17029 // @section Extension methods 17030 // @uninheritable 17031 // Layers extending `TileLayer` might reimplement the following method. 17032 // @method getTileUrl(coords: Object): String 17033 // Called only internally, returns the URL for a tile given its coordinates. 17034 // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes. 17035 getTileUrl: function (coords) { 17036 var data = { 17037 r: Browser.retina ? '@2x' : '', 17038 s: this._getSubdomain(coords), 17039 x: coords.x, 17040 y: coords.y, 17041 z: this._getZoomForUrl() 17042 }; 17043 if (this._map && !this._map.options.crs.infinite) { 17044 var invertedY = this._globalTileRange.max.y - coords.y; 17045 if (this.options.tms) { 17046 data['y'] = invertedY; 17047 } 17048 data['-y'] = invertedY; 17049 } 17050 17051 return template(this._url, extend(data, this.options)); 17052 }, 17053 17054 _tileOnLoad: function (done, tile) { 17055 // For https://github.com/Leaflet/Leaflet/issues/3332 17056 if (Browser.ielt9) { 17057 setTimeout(bind(done, this, null, tile), 0); 17058 } else { 17059 done(null, tile); 17060 } 17061 }, 17062 17063 _tileOnError: function (done, tile, e) { 17064 var errorUrl = this.options.errorTileUrl; 17065 if (errorUrl && tile.getAttribute('src') !== errorUrl) { 17066 tile.src = errorUrl; 17067 } 17068 done(e, tile); 17069 }, 17070 17071 _onTileRemove: function (e) { 17072 e.tile.onload = null; 17073 }, 17074 17075 _getZoomForUrl: function () { 17076 var zoom = this._tileZoom, 17077 maxZoom = this.options.maxZoom, 17078 zoomReverse = this.options.zoomReverse, 17079 zoomOffset = this.options.zoomOffset; 17080 17081 if (zoomReverse) { 17082 zoom = maxZoom - zoom; 17083 } 17084 17085 return zoom + zoomOffset; 17086 }, 17087 17088 _getSubdomain: function (tilePoint) { 17089 var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length; 17090 return this.options.subdomains[index]; 17091 }, 17092 17093 // stops loading all tiles in the background layer 17094 _abortLoading: function () { 17095 var i, tile; 17096 for (i in this._tiles) { 17097 if (this._tiles[i].coords.z !== this._tileZoom) { 17098 tile = this._tiles[i].el; 17099 17100 tile.onload = falseFn; 17101 tile.onerror = falseFn; 17102 17103 if (!tile.complete) { 17104 tile.src = emptyImageUrl; 17105 var coords = this._tiles[i].coords; 17106 remove(tile); 17107 delete this._tiles[i]; 17108 // @event tileabort: TileEvent 17109 // Fired when a tile was loading but is now not wanted. 17110 this.fire('tileabort', { 17111 tile: tile, 17112 coords: coords 17113 }); 17114 } 17115 } 17116 } 17117 }, 17118 17119 _removeTile: function (key) { 17120 var tile = this._tiles[key]; 17121 if (!tile) { return; } 17122 17123 // Cancels any pending http requests associated with the tile 17124 tile.el.setAttribute('src', emptyImageUrl); 17125 17126 return GridLayer.prototype._removeTile.call(this, key); 17127 }, 17128 17129 _tileReady: function (coords, err, tile) { 17130 if (!this._map || (tile && tile.getAttribute('src') === emptyImageUrl)) { 17131 return; 17132 } 17133 17134 return GridLayer.prototype._tileReady.call(this, coords, err, tile); 17135 } 17136 }); 17137 17138 17139 // @factory L.tilelayer(urlTemplate: String, options?: TileLayer options) 17140 // Instantiates a tile layer object given a `URL template` and optionally an options object. 17141 17142 function tileLayer(url, options) { 17143 return new TileLayer(url, options); 17144 } 17145 17146 /* 17147 * @class TileLayer.WMS 17148 * @inherits TileLayer 17149 * @aka L.TileLayer.WMS 17150 * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`. 17151 * 17152 * @example 17153 * 17154 * ```js 17155 * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", { 17156 * layers: 'nexrad-n0r-900913', 17157 * format: 'image/png', 17158 * transparent: true, 17159 * attribution: "Weather data © 2012 IEM Nexrad" 17160 * }); 17161 * ``` 17162 */ 17163 17164 var TileLayerWMS = TileLayer.extend({ 17165 17166 // @section 17167 // @aka TileLayer.WMS options 17168 // If any custom options not documented here are used, they will be sent to the 17169 // WMS server as extra parameters in each request URL. This can be useful for 17170 // [non-standard vendor WMS parameters](https://docs.geoserver.org/stable/en/user/services/wms/vendor.html). 17171 defaultWmsParams: { 17172 service: 'WMS', 17173 request: 'GetMap', 17174 17175 // @option layers: String = '' 17176 // **(required)** Comma-separated list of WMS layers to show. 17177 layers: '', 17178 17179 // @option styles: String = '' 17180 // Comma-separated list of WMS styles. 17181 styles: '', 17182 17183 // @option format: String = 'image/jpeg' 17184 // WMS image format (use `'image/png'` for layers with transparency). 17185 format: 'image/jpeg', 17186 17187 // @option transparent: Boolean = false 17188 // If `true`, the WMS service will return images with transparency. 17189 transparent: false, 17190 17191 // @option version: String = '1.1.1' 17192 // Version of the WMS service to use 17193 version: '1.1.1' 17194 }, 17195 17196 options: { 17197 // @option crs: CRS = null 17198 // Coordinate Reference System to use for the WMS requests, defaults to 17199 // map CRS. Don't change this if you're not sure what it means. 17200 crs: null, 17201 17202 // @option uppercase: Boolean = false 17203 // If `true`, WMS request parameter keys will be uppercase. 17204 uppercase: false 17205 }, 17206 17207 initialize: function (url, options) { 17208 17209 this._url = url; 17210 17211 var wmsParams = extend({}, this.defaultWmsParams); 17212 17213 // all keys that are not TileLayer options go to WMS params 17214 for (var i in options) { 17215 if (!(i in this.options)) { 17216 wmsParams[i] = options[i]; 17217 } 17218 } 17219 17220 options = setOptions(this, options); 17221 17222 var realRetina = options.detectRetina && Browser.retina ? 2 : 1; 17223 var tileSize = this.getTileSize(); 17224 wmsParams.width = tileSize.x * realRetina; 17225 wmsParams.height = tileSize.y * realRetina; 17226 17227 this.wmsParams = wmsParams; 17228 }, 17229 17230 onAdd: function (map) { 17231 17232 this._crs = this.options.crs || map.options.crs; 17233 this._wmsVersion = parseFloat(this.wmsParams.version); 17234 17235 var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs'; 17236 this.wmsParams[projectionKey] = this._crs.code; 17237 17238 TileLayer.prototype.onAdd.call(this, map); 17239 }, 17240 17241 getTileUrl: function (coords) { 17242 17243 var tileBounds = this._tileCoordsToNwSe(coords), 17244 crs = this._crs, 17245 bounds = toBounds(crs.project(tileBounds[0]), crs.project(tileBounds[1])), 17246 min = bounds.min, 17247 max = bounds.max, 17248 bbox = (this._wmsVersion >= 1.3 && this._crs === EPSG4326 ? 17249 [min.y, min.x, max.y, max.x] : 17250 [min.x, min.y, max.x, max.y]).join(','), 17251 url = TileLayer.prototype.getTileUrl.call(this, coords); 17252 return url + 17253 getParamString(this.wmsParams, url, this.options.uppercase) + 17254 (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox; 17255 }, 17256 17257 // @method setParams(params: Object, noRedraw?: Boolean): this 17258 // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true). 17259 setParams: function (params, noRedraw) { 17260 17261 extend(this.wmsParams, params); 17262 17263 if (!noRedraw) { 17264 this.redraw(); 17265 } 17266 17267 return this; 17268 } 17269 }); 17270 17271 17272 // @factory L.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options) 17273 // Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object. 17274 function tileLayerWMS(url, options) { 17275 return new TileLayerWMS(url, options); 17276 } 17277 17278 TileLayer.WMS = TileLayerWMS; 17279 tileLayer.wms = tileLayerWMS; 17280 17281 /* 17282 * @class Renderer 17283 * @inherits Layer 17284 * @aka L.Renderer 17285 * 17286 * Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the 17287 * DOM container of the renderer, its bounds, and its zoom animation. 17288 * 17289 * A `Renderer` works as an implicit layer group for all `Path`s - the renderer 17290 * itself can be added or removed to the map. All paths use a renderer, which can 17291 * be implicit (the map will decide the type of renderer and use it automatically) 17292 * or explicit (using the [`renderer`](#path-renderer) option of the path). 17293 * 17294 * Do not use this class directly, use `SVG` and `Canvas` instead. 17295 * 17296 * @event update: Event 17297 * Fired when the renderer updates its bounds, center and zoom, for example when 17298 * its map has moved 17299 */ 17300 17301 var Renderer = Layer.extend({ 17302 17303 // @section 17304 // @aka Renderer options 17305 options: { 17306 // @option padding: Number = 0.1 17307 // How much to extend the clip area around the map view (relative to its size) 17308 // e.g. 0.1 would be 10% of map view in each direction 17309 padding: 0.1 17310 }, 17311 17312 initialize: function (options) { 17313 setOptions(this, options); 17314 stamp(this); 17315 this._layers = this._layers || {}; 17316 }, 17317 17318 onAdd: function () { 17319 if (!this._container) { 17320 this._initContainer(); // defined by renderer implementations 17321 17322 // always keep transform-origin as 0 0 17323 addClass(this._container, 'leaflet-zoom-animated'); 17324 } 17325 17326 this.getPane().appendChild(this._container); 17327 this._update(); 17328 this.on('update', this._updatePaths, this); 17329 }, 17330 17331 onRemove: function () { 17332 this.off('update', this._updatePaths, this); 17333 this._destroyContainer(); 17334 }, 17335 17336 getEvents: function () { 17337 var events = { 17338 viewreset: this._reset, 17339 zoom: this._onZoom, 17340 moveend: this._update, 17341 zoomend: this._onZoomEnd 17342 }; 17343 if (this._zoomAnimated) { 17344 events.zoomanim = this._onAnimZoom; 17345 } 17346 return events; 17347 }, 17348 17349 _onAnimZoom: function (ev) { 17350 this._updateTransform(ev.center, ev.zoom); 17351 }, 17352 17353 _onZoom: function () { 17354 this._updateTransform(this._map.getCenter(), this._map.getZoom()); 17355 }, 17356 17357 _updateTransform: function (center, zoom) { 17358 var scale = this._map.getZoomScale(zoom, this._zoom), 17359 viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding), 17360 currentCenterPoint = this._map.project(this._center, zoom), 17361 17362 topLeftOffset = viewHalf.multiplyBy(-scale).add(currentCenterPoint) 17363 .subtract(this._map._getNewPixelOrigin(center, zoom)); 17364 17365 if (Browser.any3d) { 17366 setTransform(this._container, topLeftOffset, scale); 17367 } else { 17368 setPosition(this._container, topLeftOffset); 17369 } 17370 }, 17371 17372 _reset: function () { 17373 this._update(); 17374 this._updateTransform(this._center, this._zoom); 17375 17376 for (var id in this._layers) { 17377 this._layers[id]._reset(); 17378 } 17379 }, 17380 17381 _onZoomEnd: function () { 17382 for (var id in this._layers) { 17383 this._layers[id]._project(); 17384 } 17385 }, 17386 17387 _updatePaths: function () { 17388 for (var id in this._layers) { 17389 this._layers[id]._update(); 17390 } 17391 }, 17392 17393 _update: function () { 17394 // Update pixel bounds of renderer container (for positioning/sizing/clipping later) 17395 // Subclasses are responsible of firing the 'update' event. 17396 var p = this.options.padding, 17397 size = this._map.getSize(), 17398 min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round(); 17399 17400 this._bounds = new Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round()); 17401 17402 this._center = this._map.getCenter(); 17403 this._zoom = this._map.getZoom(); 17404 } 17405 }); 17406 17407 /* 17408 * @class Canvas 17409 * @inherits Renderer 17410 * @aka L.Canvas 17411 * 17412 * Allows vector layers to be displayed with [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API). 17413 * Inherits `Renderer`. 17414 * 17415 * Due to [technical limitations](https://caniuse.com/canvas), Canvas is not 17416 * available in all web browsers, notably IE8, and overlapping geometries might 17417 * not display properly in some edge cases. 17418 * 17419 * @example 17420 * 17421 * Use Canvas by default for all paths in the map: 17422 * 17423 * ```js 17424 * var map = L.map('map', { 17425 * renderer: L.canvas() 17426 * }); 17427 * ``` 17428 * 17429 * Use a Canvas renderer with extra padding for specific vector geometries: 17430 * 17431 * ```js 17432 * var map = L.map('map'); 17433 * var myRenderer = L.canvas({ padding: 0.5 }); 17434 * var line = L.polyline( coordinates, { renderer: myRenderer } ); 17435 * var circle = L.circle( center, { renderer: myRenderer } ); 17436 * ``` 17437 */ 17438 17439 var Canvas = Renderer.extend({ 17440 17441 // @section 17442 // @aka Canvas options 17443 options: { 17444 // @option tolerance: Number = 0 17445 // How much to extend the click tolerance around a path/object on the map. 17446 tolerance: 0 17447 }, 17448 17449 getEvents: function () { 17450 var events = Renderer.prototype.getEvents.call(this); 17451 events.viewprereset = this._onViewPreReset; 17452 return events; 17453 }, 17454 17455 _onViewPreReset: function () { 17456 // Set a flag so that a viewprereset+moveend+viewreset only updates&redraws once 17457 this._postponeUpdatePaths = true; 17458 }, 17459 17460 onAdd: function () { 17461 Renderer.prototype.onAdd.call(this); 17462 17463 // Redraw vectors since canvas is cleared upon removal, 17464 // in case of removing the renderer itself from the map. 17465 this._draw(); 17466 }, 17467 17468 _initContainer: function () { 17469 var container = this._container = document.createElement('canvas'); 17470 17471 on(container, 'mousemove', this._onMouseMove, this); 17472 on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this); 17473 on(container, 'mouseout', this._handleMouseOut, this); 17474 container['_leaflet_disable_events'] = true; 17475 17476 this._ctx = container.getContext('2d'); 17477 }, 17478 17479 _destroyContainer: function () { 17480 cancelAnimFrame(this._redrawRequest); 17481 delete this._ctx; 17482 remove(this._container); 17483 off(this._container); 17484 delete this._container; 17485 }, 17486 17487 _updatePaths: function () { 17488 if (this._postponeUpdatePaths) { return; } 17489 17490 var layer; 17491 this._redrawBounds = null; 17492 for (var id in this._layers) { 17493 layer = this._layers[id]; 17494 layer._update(); 17495 } 17496 this._redraw(); 17497 }, 17498 17499 _update: function () { 17500 if (this._map._animatingZoom && this._bounds) { return; } 17501 17502 Renderer.prototype._update.call(this); 17503 17504 var b = this._bounds, 17505 container = this._container, 17506 size = b.getSize(), 17507 m = Browser.retina ? 2 : 1; 17508 17509 setPosition(container, b.min); 17510 17511 // set canvas size (also clearing it); use double size on retina 17512 container.width = m * size.x; 17513 container.height = m * size.y; 17514 container.style.width = size.x + 'px'; 17515 container.style.height = size.y + 'px'; 17516 17517 if (Browser.retina) { 17518 this._ctx.scale(2, 2); 17519 } 17520 17521 // translate so we use the same path coordinates after canvas element moves 17522 this._ctx.translate(-b.min.x, -b.min.y); 17523 17524 // Tell paths to redraw themselves 17525 this.fire('update'); 17526 }, 17527 17528 _reset: function () { 17529 Renderer.prototype._reset.call(this); 17530 17531 if (this._postponeUpdatePaths) { 17532 this._postponeUpdatePaths = false; 17533 this._updatePaths(); 17534 } 17535 }, 17536 17537 _initPath: function (layer) { 17538 this._updateDashArray(layer); 17539 this._layers[stamp(layer)] = layer; 17540 17541 var order = layer._order = { 17542 layer: layer, 17543 prev: this._drawLast, 17544 next: null 17545 }; 17546 if (this._drawLast) { this._drawLast.next = order; } 17547 this._drawLast = order; 17548 this._drawFirst = this._drawFirst || this._drawLast; 17549 }, 17550 17551 _addPath: function (layer) { 17552 this._requestRedraw(layer); 17553 }, 17554 17555 _removePath: function (layer) { 17556 var order = layer._order; 17557 var next = order.next; 17558 var prev = order.prev; 17559 17560 if (next) { 17561 next.prev = prev; 17562 } else { 17563 this._drawLast = prev; 17564 } 17565 if (prev) { 17566 prev.next = next; 17567 } else { 17568 this._drawFirst = next; 17569 } 17570 17571 delete layer._order; 17572 17573 delete this._layers[stamp(layer)]; 17574 17575 this._requestRedraw(layer); 17576 }, 17577 17578 _updatePath: function (layer) { 17579 // Redraw the union of the layer's old pixel 17580 // bounds and the new pixel bounds. 17581 this._extendRedrawBounds(layer); 17582 layer._project(); 17583 layer._update(); 17584 // The redraw will extend the redraw bounds 17585 // with the new pixel bounds. 17586 this._requestRedraw(layer); 17587 }, 17588 17589 _updateStyle: function (layer) { 17590 this._updateDashArray(layer); 17591 this._requestRedraw(layer); 17592 }, 17593 17594 _updateDashArray: function (layer) { 17595 if (typeof layer.options.dashArray === 'string') { 17596 var parts = layer.options.dashArray.split(/[, ]+/), 17597 dashArray = [], 17598 dashValue, 17599 i; 17600 for (i = 0; i < parts.length; i++) { 17601 dashValue = Number(parts[i]); 17602 // Ignore dash array containing invalid lengths 17603 if (isNaN(dashValue)) { return; } 17604 dashArray.push(dashValue); 17605 } 17606 layer.options._dashArray = dashArray; 17607 } else { 17608 layer.options._dashArray = layer.options.dashArray; 17609 } 17610 }, 17611 17612 _requestRedraw: function (layer) { 17613 if (!this._map) { return; } 17614 17615 this._extendRedrawBounds(layer); 17616 this._redrawRequest = this._redrawRequest || requestAnimFrame(this._redraw, this); 17617 }, 17618 17619 _extendRedrawBounds: function (layer) { 17620 if (layer._pxBounds) { 17621 var padding = (layer.options.weight || 0) + 1; 17622 this._redrawBounds = this._redrawBounds || new Bounds(); 17623 this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding])); 17624 this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding])); 17625 } 17626 }, 17627 17628 _redraw: function () { 17629 this._redrawRequest = null; 17630 17631 if (this._redrawBounds) { 17632 this._redrawBounds.min._floor(); 17633 this._redrawBounds.max._ceil(); 17634 } 17635 17636 this._clear(); // clear layers in redraw bounds 17637 this._draw(); // draw layers 17638 17639 this._redrawBounds = null; 17640 }, 17641 17642 _clear: function () { 17643 var bounds = this._redrawBounds; 17644 if (bounds) { 17645 var size = bounds.getSize(); 17646 this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y); 17647 } else { 17648 this._ctx.save(); 17649 this._ctx.setTransform(1, 0, 0, 1, 0, 0); 17650 this._ctx.clearRect(0, 0, this._container.width, this._container.height); 17651 this._ctx.restore(); 17652 } 17653 }, 17654 17655 _draw: function () { 17656 var layer, bounds = this._redrawBounds; 17657 this._ctx.save(); 17658 if (bounds) { 17659 var size = bounds.getSize(); 17660 this._ctx.beginPath(); 17661 this._ctx.rect(bounds.min.x, bounds.min.y, size.x, size.y); 17662 this._ctx.clip(); 17663 } 17664 17665 this._drawing = true; 17666 17667 for (var order = this._drawFirst; order; order = order.next) { 17668 layer = order.layer; 17669 if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) { 17670 layer._updatePath(); 17671 } 17672 } 17673 17674 this._drawing = false; 17675 17676 this._ctx.restore(); // Restore state before clipping. 17677 }, 17678 17679 _updatePoly: function (layer, closed) { 17680 if (!this._drawing) { return; } 17681 17682 var i, j, len2, p, 17683 parts = layer._parts, 17684 len = parts.length, 17685 ctx = this._ctx; 17686 17687 if (!len) { return; } 17688 17689 ctx.beginPath(); 17690 17691 for (i = 0; i < len; i++) { 17692 for (j = 0, len2 = parts[i].length; j < len2; j++) { 17693 p = parts[i][j]; 17694 ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y); 17695 } 17696 if (closed) { 17697 ctx.closePath(); 17698 } 17699 } 17700 17701 this._fillStroke(ctx, layer); 17702 17703 // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature 17704 }, 17705 17706 _updateCircle: function (layer) { 17707 17708 if (!this._drawing || layer._empty()) { return; } 17709 17710 var p = layer._point, 17711 ctx = this._ctx, 17712 r = Math.max(Math.round(layer._radius), 1), 17713 s = (Math.max(Math.round(layer._radiusY), 1) || r) / r; 17714 17715 if (s !== 1) { 17716 ctx.save(); 17717 ctx.scale(1, s); 17718 } 17719 17720 ctx.beginPath(); 17721 ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false); 17722 17723 if (s !== 1) { 17724 ctx.restore(); 17725 } 17726 17727 this._fillStroke(ctx, layer); 17728 }, 17729 17730 _fillStroke: function (ctx, layer) { 17731 var options = layer.options; 17732 17733 if (options.fill) { 17734 ctx.globalAlpha = options.fillOpacity; 17735 ctx.fillStyle = options.fillColor || options.color; 17736 ctx.fill(options.fillRule || 'evenodd'); 17737 } 17738 17739 if (options.stroke && options.weight !== 0) { 17740 if (ctx.setLineDash) { 17741 ctx.setLineDash(layer.options && layer.options._dashArray || []); 17742 } 17743 ctx.globalAlpha = options.opacity; 17744 ctx.lineWidth = options.weight; 17745 ctx.strokeStyle = options.color; 17746 ctx.lineCap = options.lineCap; 17747 ctx.lineJoin = options.lineJoin; 17748 ctx.stroke(); 17749 } 17750 }, 17751 17752 // Canvas obviously doesn't have mouse events for individual drawn objects, 17753 // so we emulate that by calculating what's under the mouse on mousemove/click manually 17754 17755 _onClick: function (e) { 17756 var point = this._map.mouseEventToLayerPoint(e), layer, clickedLayer; 17757 17758 for (var order = this._drawFirst; order; order = order.next) { 17759 layer = order.layer; 17760 if (layer.options.interactive && layer._containsPoint(point)) { 17761 if (!(e.type === 'click' || e.type === 'preclick') || !this._map._draggableMoved(layer)) { 17762 clickedLayer = layer; 17763 } 17764 } 17765 } 17766 this._fireEvent(clickedLayer ? [clickedLayer] : false, e); 17767 }, 17768 17769 _onMouseMove: function (e) { 17770 if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; } 17771 17772 var point = this._map.mouseEventToLayerPoint(e); 17773 this._handleMouseHover(e, point); 17774 }, 17775 17776 17777 _handleMouseOut: function (e) { 17778 var layer = this._hoveredLayer; 17779 if (layer) { 17780 // if we're leaving the layer, fire mouseout 17781 removeClass(this._container, 'leaflet-interactive'); 17782 this._fireEvent([layer], e, 'mouseout'); 17783 this._hoveredLayer = null; 17784 this._mouseHoverThrottled = false; 17785 } 17786 }, 17787 17788 _handleMouseHover: function (e, point) { 17789 if (this._mouseHoverThrottled) { 17790 return; 17791 } 17792 17793 var layer, candidateHoveredLayer; 17794 17795 for (var order = this._drawFirst; order; order = order.next) { 17796 layer = order.layer; 17797 if (layer.options.interactive && layer._containsPoint(point)) { 17798 candidateHoveredLayer = layer; 17799 } 17800 } 17801 17802 if (candidateHoveredLayer !== this._hoveredLayer) { 17803 this._handleMouseOut(e); 17804 17805 if (candidateHoveredLayer) { 17806 addClass(this._container, 'leaflet-interactive'); // change cursor 17807 this._fireEvent([candidateHoveredLayer], e, 'mouseover'); 17808 this._hoveredLayer = candidateHoveredLayer; 17809 } 17810 } 17811 17812 this._fireEvent(this._hoveredLayer ? [this._hoveredLayer] : false, e); 17813 17814 this._mouseHoverThrottled = true; 17815 setTimeout(bind(function () { 17816 this._mouseHoverThrottled = false; 17817 }, this), 32); 17818 }, 17819 17820 _fireEvent: function (layers, e, type) { 17821 this._map._fireDOMEvent(e, type || e.type, layers); 17822 }, 17823 17824 _bringToFront: function (layer) { 17825 var order = layer._order; 17826 17827 if (!order) { return; } 17828 17829 var next = order.next; 17830 var prev = order.prev; 17831 17832 if (next) { 17833 next.prev = prev; 17834 } else { 17835 // Already last 17836 return; 17837 } 17838 if (prev) { 17839 prev.next = next; 17840 } else if (next) { 17841 // Update first entry unless this is the 17842 // single entry 17843 this._drawFirst = next; 17844 } 17845 17846 order.prev = this._drawLast; 17847 this._drawLast.next = order; 17848 17849 order.next = null; 17850 this._drawLast = order; 17851 17852 this._requestRedraw(layer); 17853 }, 17854 17855 _bringToBack: function (layer) { 17856 var order = layer._order; 17857 17858 if (!order) { return; } 17859 17860 var next = order.next; 17861 var prev = order.prev; 17862 17863 if (prev) { 17864 prev.next = next; 17865 } else { 17866 // Already first 17867 return; 17868 } 17869 if (next) { 17870 next.prev = prev; 17871 } else if (prev) { 17872 // Update last entry unless this is the 17873 // single entry 17874 this._drawLast = prev; 17875 } 17876 17877 order.prev = null; 17878 17879 order.next = this._drawFirst; 17880 this._drawFirst.prev = order; 17881 this._drawFirst = order; 17882 17883 this._requestRedraw(layer); 17884 } 17885 }); 17886 17887 // @factory L.canvas(options?: Renderer options) 17888 // Creates a Canvas renderer with the given options. 17889 function canvas(options) { 17890 return Browser.canvas ? new Canvas(options) : null; 17891 } 17892 17893 /* 17894 * Thanks to Dmitry Baranovsky and his Raphael library for inspiration! 17895 */ 17896 17897 17898 var vmlCreate = (function () { 17899 try { 17900 document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml'); 17901 return function (name) { 17902 return document.createElement('<lvml:' + name + ' class="lvml">'); 17903 }; 17904 } catch (e) { 17905 // Do not return fn from catch block so `e` can be garbage collected 17906 // See https://github.com/Leaflet/Leaflet/pull/7279 17907 } 17908 return function (name) { 17909 return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">'); 17910 }; 17911 })(); 17912 17913 17914 /* 17915 * @class SVG 17916 * 17917 * 17918 * VML was deprecated in 2012, which means VML functionality exists only for backwards compatibility 17919 * with old versions of Internet Explorer. 17920 */ 17921 17922 // mixin to redefine some SVG methods to handle VML syntax which is similar but with some differences 17923 var vmlMixin = { 17924 17925 _initContainer: function () { 17926 this._container = create$1('div', 'leaflet-vml-container'); 17927 }, 17928 17929 _update: function () { 17930 if (this._map._animatingZoom) { return; } 17931 Renderer.prototype._update.call(this); 17932 this.fire('update'); 17933 }, 17934 17935 _initPath: function (layer) { 17936 var container = layer._container = vmlCreate('shape'); 17937 17938 addClass(container, 'leaflet-vml-shape ' + (this.options.className || '')); 17939 17940 container.coordsize = '1 1'; 17941 17942 layer._path = vmlCreate('path'); 17943 container.appendChild(layer._path); 17944 17945 this._updateStyle(layer); 17946 this._layers[stamp(layer)] = layer; 17947 }, 17948 17949 _addPath: function (layer) { 17950 var container = layer._container; 17951 this._container.appendChild(container); 17952 17953 if (layer.options.interactive) { 17954 layer.addInteractiveTarget(container); 17955 } 17956 }, 17957 17958 _removePath: function (layer) { 17959 var container = layer._container; 17960 remove(container); 17961 layer.removeInteractiveTarget(container); 17962 delete this._layers[stamp(layer)]; 17963 }, 17964 17965 _updateStyle: function (layer) { 17966 var stroke = layer._stroke, 17967 fill = layer._fill, 17968 options = layer.options, 17969 container = layer._container; 17970 17971 container.stroked = !!options.stroke; 17972 container.filled = !!options.fill; 17973 17974 if (options.stroke) { 17975 if (!stroke) { 17976 stroke = layer._stroke = vmlCreate('stroke'); 17977 } 17978 container.appendChild(stroke); 17979 stroke.weight = options.weight + 'px'; 17980 stroke.color = options.color; 17981 stroke.opacity = options.opacity; 17982 17983 if (options.dashArray) { 17984 stroke.dashStyle = isArray(options.dashArray) ? 17985 options.dashArray.join(' ') : 17986 options.dashArray.replace(/( *, *)/g, ' '); 17987 } else { 17988 stroke.dashStyle = ''; 17989 } 17990 stroke.endcap = options.lineCap.replace('butt', 'flat'); 17991 stroke.joinstyle = options.lineJoin; 17992 17993 } else if (stroke) { 17994 container.removeChild(stroke); 17995 layer._stroke = null; 17996 } 17997 17998 if (options.fill) { 17999 if (!fill) { 18000 fill = layer._fill = vmlCreate('fill'); 18001 } 18002 container.appendChild(fill); 18003 fill.color = options.fillColor || options.color; 18004 fill.opacity = options.fillOpacity; 18005 18006 } else if (fill) { 18007 container.removeChild(fill); 18008 layer._fill = null; 18009 } 18010 }, 18011 18012 _updateCircle: function (layer) { 18013 var p = layer._point.round(), 18014 r = Math.round(layer._radius), 18015 r2 = Math.round(layer._radiusY || r); 18016 18017 this._setPath(layer, layer._empty() ? 'M0 0' : 18018 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r2 + ' 0,' + (65535 * 360)); 18019 }, 18020 18021 _setPath: function (layer, path) { 18022 layer._path.v = path; 18023 }, 18024 18025 _bringToFront: function (layer) { 18026 toFront(layer._container); 18027 }, 18028 18029 _bringToBack: function (layer) { 18030 toBack(layer._container); 18031 } 18032 }; 18033 18034 var create = Browser.vml ? vmlCreate : svgCreate; 18035 18036 /* 18037 * @class SVG 18038 * @inherits Renderer 18039 * @aka L.SVG 18040 * 18041 * Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG). 18042 * Inherits `Renderer`. 18043 * 18044 * Due to [technical limitations](https://caniuse.com/svg), SVG is not 18045 * available in all web browsers, notably Android 2.x and 3.x. 18046 * 18047 * Although SVG is not available on IE7 and IE8, these browsers support 18048 * [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language) 18049 * (a now deprecated technology), and the SVG renderer will fall back to VML in 18050 * this case. 18051 * 18052 * @example 18053 * 18054 * Use SVG by default for all paths in the map: 18055 * 18056 * ```js 18057 * var map = L.map('map', { 18058 * renderer: L.svg() 18059 * }); 18060 * ``` 18061 * 18062 * Use a SVG renderer with extra padding for specific vector geometries: 18063 * 18064 * ```js 18065 * var map = L.map('map'); 18066 * var myRenderer = L.svg({ padding: 0.5 }); 18067 * var line = L.polyline( coordinates, { renderer: myRenderer } ); 18068 * var circle = L.circle( center, { renderer: myRenderer } ); 18069 * ``` 18070 */ 18071 18072 var SVG = Renderer.extend({ 18073 18074 _initContainer: function () { 18075 this._container = create('svg'); 18076 18077 // makes it possible to click through svg root; we'll reset it back in individual paths 18078 this._container.setAttribute('pointer-events', 'none'); 18079 18080 this._rootGroup = create('g'); 18081 this._container.appendChild(this._rootGroup); 18082 }, 18083 18084 _destroyContainer: function () { 18085 remove(this._container); 18086 off(this._container); 18087 delete this._container; 18088 delete this._rootGroup; 18089 delete this._svgSize; 18090 }, 18091 18092 _update: function () { 18093 if (this._map._animatingZoom && this._bounds) { return; } 18094 18095 Renderer.prototype._update.call(this); 18096 18097 var b = this._bounds, 18098 size = b.getSize(), 18099 container = this._container; 18100 18101 // set size of svg-container if changed 18102 if (!this._svgSize || !this._svgSize.equals(size)) { 18103 this._svgSize = size; 18104 container.setAttribute('width', size.x); 18105 container.setAttribute('height', size.y); 18106 } 18107 18108 // movement: update container viewBox so that we don't have to change coordinates of individual layers 18109 setPosition(container, b.min); 18110 container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' ')); 18111 18112 this.fire('update'); 18113 }, 18114 18115 // methods below are called by vector layers implementations 18116 18117 _initPath: function (layer) { 18118 var path = layer._path = create('path'); 18119 18120 // @namespace Path 18121 // @option className: String = null 18122 // Custom class name set on an element. Only for SVG renderer. 18123 if (layer.options.className) { 18124 addClass(path, layer.options.className); 18125 } 18126 18127 if (layer.options.interactive) { 18128 addClass(path, 'leaflet-interactive'); 18129 } 18130 18131 this._updateStyle(layer); 18132 this._layers[stamp(layer)] = layer; 18133 }, 18134 18135 _addPath: function (layer) { 18136 if (!this._rootGroup) { this._initContainer(); } 18137 this._rootGroup.appendChild(layer._path); 18138 layer.addInteractiveTarget(layer._path); 18139 }, 18140 18141 _removePath: function (layer) { 18142 remove(layer._path); 18143 layer.removeInteractiveTarget(layer._path); 18144 delete this._layers[stamp(layer)]; 18145 }, 18146 18147 _updatePath: function (layer) { 18148 layer._project(); 18149 layer._update(); 18150 }, 18151 18152 _updateStyle: function (layer) { 18153 var path = layer._path, 18154 options = layer.options; 18155 18156 if (!path) { return; } 18157 18158 if (options.stroke) { 18159 path.setAttribute('stroke', options.color); 18160 path.setAttribute('stroke-opacity', options.opacity); 18161 path.setAttribute('stroke-width', options.weight); 18162 path.setAttribute('stroke-linecap', options.lineCap); 18163 path.setAttribute('stroke-linejoin', options.lineJoin); 18164 18165 if (options.dashArray) { 18166 path.setAttribute('stroke-dasharray', options.dashArray); 18167 } else { 18168 path.removeAttribute('stroke-dasharray'); 18169 } 18170 18171 if (options.dashOffset) { 18172 path.setAttribute('stroke-dashoffset', options.dashOffset); 18173 } else { 18174 path.removeAttribute('stroke-dashoffset'); 18175 } 18176 } else { 18177 path.setAttribute('stroke', 'none'); 18178 } 18179 18180 if (options.fill) { 18181 path.setAttribute('fill', options.fillColor || options.color); 18182 path.setAttribute('fill-opacity', options.fillOpacity); 18183 path.setAttribute('fill-rule', options.fillRule || 'evenodd'); 18184 } else { 18185 path.setAttribute('fill', 'none'); 18186 } 18187 }, 18188 18189 _updatePoly: function (layer, closed) { 18190 this._setPath(layer, pointsToPath(layer._parts, closed)); 18191 }, 18192 18193 _updateCircle: function (layer) { 18194 var p = layer._point, 18195 r = Math.max(Math.round(layer._radius), 1), 18196 r2 = Math.max(Math.round(layer._radiusY), 1) || r, 18197 arc = 'a' + r + ',' + r2 + ' 0 1,0 '; 18198 18199 // drawing a circle with two half-arcs 18200 var d = layer._empty() ? 'M0 0' : 18201 'M' + (p.x - r) + ',' + p.y + 18202 arc + (r * 2) + ',0 ' + 18203 arc + (-r * 2) + ',0 '; 18204 18205 this._setPath(layer, d); 18206 }, 18207 18208 _setPath: function (layer, path) { 18209 layer._path.setAttribute('d', path); 18210 }, 18211 18212 // SVG does not have the concept of zIndex so we resort to changing the DOM order of elements 18213 _bringToFront: function (layer) { 18214 toFront(layer._path); 18215 }, 18216 18217 _bringToBack: function (layer) { 18218 toBack(layer._path); 18219 } 18220 }); 18221 18222 if (Browser.vml) { 18223 SVG.include(vmlMixin); 18224 } 18225 18226 // @namespace SVG 18227 // @factory L.svg(options?: Renderer options) 18228 // Creates a SVG renderer with the given options. 18229 function svg(options) { 18230 return Browser.svg || Browser.vml ? new SVG(options) : null; 18231 } 18232 18233 Map.include({ 18234 // @namespace Map; @method getRenderer(layer: Path): Renderer 18235 // Returns the instance of `Renderer` that should be used to render the given 18236 // `Path`. It will ensure that the `renderer` options of the map and paths 18237 // are respected, and that the renderers do exist on the map. 18238 getRenderer: function (layer) { 18239 // @namespace Path; @option renderer: Renderer 18240 // Use this specific instance of `Renderer` for this path. Takes 18241 // precedence over the map's [default renderer](#map-renderer). 18242 var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer; 18243 18244 if (!renderer) { 18245 renderer = this._renderer = this._createRenderer(); 18246 } 18247 18248 if (!this.hasLayer(renderer)) { 18249 this.addLayer(renderer); 18250 } 18251 return renderer; 18252 }, 18253 18254 _getPaneRenderer: function (name) { 18255 if (name === 'overlayPane' || name === undefined) { 18256 return false; 18257 } 18258 18259 var renderer = this._paneRenderers[name]; 18260 if (renderer === undefined) { 18261 renderer = this._createRenderer({pane: name}); 18262 this._paneRenderers[name] = renderer; 18263 } 18264 return renderer; 18265 }, 18266 18267 _createRenderer: function (options) { 18268 // @namespace Map; @option preferCanvas: Boolean = false 18269 // Whether `Path`s should be rendered on a `Canvas` renderer. 18270 // By default, all `Path`s are rendered in a `SVG` renderer. 18271 return (this.options.preferCanvas && canvas(options)) || svg(options); 18272 } 18273 }); 18274 18275 /* 18276 * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object. 18277 */ 18278 18279 /* 18280 * @class Rectangle 18281 * @aka L.Rectangle 18282 * @inherits Polygon 18283 * 18284 * A class for drawing rectangle overlays on a map. Extends `Polygon`. 18285 * 18286 * @example 18287 * 18288 * ```js 18289 * // define rectangle geographical bounds 18290 * var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]]; 18291 * 18292 * // create an orange rectangle 18293 * L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(map); 18294 * 18295 * // zoom the map to the rectangle bounds 18296 * map.fitBounds(bounds); 18297 * ``` 18298 * 18299 */ 18300 18301 18302 var Rectangle = Polygon.extend({ 18303 initialize: function (latLngBounds, options) { 18304 Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options); 18305 }, 18306 18307 // @method setBounds(latLngBounds: LatLngBounds): this 18308 // Redraws the rectangle with the passed bounds. 18309 setBounds: function (latLngBounds) { 18310 return this.setLatLngs(this._boundsToLatLngs(latLngBounds)); 18311 }, 18312 18313 _boundsToLatLngs: function (latLngBounds) { 18314 latLngBounds = toLatLngBounds(latLngBounds); 18315 return [ 18316 latLngBounds.getSouthWest(), 18317 latLngBounds.getNorthWest(), 18318 latLngBounds.getNorthEast(), 18319 latLngBounds.getSouthEast() 18320 ]; 18321 } 18322 }); 18323 18324 18325 // @factory L.rectangle(latLngBounds: LatLngBounds, options?: Polyline options) 18326 function rectangle(latLngBounds, options) { 18327 return new Rectangle(latLngBounds, options); 18328 } 18329 18330 SVG.create = create; 18331 SVG.pointsToPath = pointsToPath; 18332 18333 GeoJSON.geometryToLayer = geometryToLayer; 18334 GeoJSON.coordsToLatLng = coordsToLatLng; 18335 GeoJSON.coordsToLatLngs = coordsToLatLngs; 18336 GeoJSON.latLngToCoords = latLngToCoords; 18337 GeoJSON.latLngsToCoords = latLngsToCoords; 18338 GeoJSON.getFeature = getFeature; 18339 GeoJSON.asFeature = asFeature; 18340 18341 /* 18342 * L.Handler.BoxZoom is used to add shift-drag zoom interaction to the map 18343 * (zoom to a selected bounding box), enabled by default. 18344 */ 18345 18346 // @namespace Map 18347 // @section Interaction Options 18348 Map.mergeOptions({ 18349 // @option boxZoom: Boolean = true 18350 // Whether the map can be zoomed to a rectangular area specified by 18351 // dragging the mouse while pressing the shift key. 18352 boxZoom: true 18353 }); 18354 18355 var BoxZoom = Handler.extend({ 18356 initialize: function (map) { 18357 this._map = map; 18358 this._container = map._container; 18359 this._pane = map._panes.overlayPane; 18360 this._resetStateTimeout = 0; 18361 map.on('unload', this._destroy, this); 18362 }, 18363 18364 addHooks: function () { 18365 on(this._container, 'mousedown', this._onMouseDown, this); 18366 }, 18367 18368 removeHooks: function () { 18369 off(this._container, 'mousedown', this._onMouseDown, this); 18370 }, 18371 18372 moved: function () { 18373 return this._moved; 18374 }, 18375 18376 _destroy: function () { 18377 remove(this._pane); 18378 delete this._pane; 18379 }, 18380 18381 _resetState: function () { 18382 this._resetStateTimeout = 0; 18383 this._moved = false; 18384 }, 18385 18386 _clearDeferredResetState: function () { 18387 if (this._resetStateTimeout !== 0) { 18388 clearTimeout(this._resetStateTimeout); 18389 this._resetStateTimeout = 0; 18390 } 18391 }, 18392 18393 _onMouseDown: function (e) { 18394 if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } 18395 18396 // Clear the deferred resetState if it hasn't executed yet, otherwise it 18397 // will interrupt the interaction and orphan a box element in the container. 18398 this._clearDeferredResetState(); 18399 this._resetState(); 18400 18401 disableTextSelection(); 18402 disableImageDrag(); 18403 18404 this._startPoint = this._map.mouseEventToContainerPoint(e); 18405 18406 on(document, { 18407 contextmenu: stop, 18408 mousemove: this._onMouseMove, 18409 mouseup: this._onMouseUp, 18410 keydown: this._onKeyDown 18411 }, this); 18412 }, 18413 18414 _onMouseMove: function (e) { 18415 if (!this._moved) { 18416 this._moved = true; 18417 18418 this._box = create$1('div', 'leaflet-zoom-box', this._container); 18419 addClass(this._container, 'leaflet-crosshair'); 18420 18421 this._map.fire('boxzoomstart'); 18422 } 18423 18424 this._point = this._map.mouseEventToContainerPoint(e); 18425 18426 var bounds = new Bounds(this._point, this._startPoint), 18427 size = bounds.getSize(); 18428 18429 setPosition(this._box, bounds.min); 18430 18431 this._box.style.width = size.x + 'px'; 18432 this._box.style.height = size.y + 'px'; 18433 }, 18434 18435 _finish: function () { 18436 if (this._moved) { 18437 remove(this._box); 18438 removeClass(this._container, 'leaflet-crosshair'); 18439 } 18440 18441 enableTextSelection(); 18442 enableImageDrag(); 18443 18444 off(document, { 18445 contextmenu: stop, 18446 mousemove: this._onMouseMove, 18447 mouseup: this._onMouseUp, 18448 keydown: this._onKeyDown 18449 }, this); 18450 }, 18451 18452 _onMouseUp: function (e) { 18453 if ((e.which !== 1) && (e.button !== 1)) { return; } 18454 18455 this._finish(); 18456 18457 if (!this._moved) { return; } 18458 // Postpone to next JS tick so internal click event handling 18459 // still see it as "moved". 18460 this._clearDeferredResetState(); 18461 this._resetStateTimeout = setTimeout(bind(this._resetState, this), 0); 18462 18463 var bounds = new LatLngBounds( 18464 this._map.containerPointToLatLng(this._startPoint), 18465 this._map.containerPointToLatLng(this._point)); 18466 18467 this._map 18468 .fitBounds(bounds) 18469 .fire('boxzoomend', {boxZoomBounds: bounds}); 18470 }, 18471 18472 _onKeyDown: function (e) { 18473 if (e.keyCode === 27) { 18474 this._finish(); 18475 this._clearDeferredResetState(); 18476 this._resetState(); 18477 } 18478 } 18479 }); 18480 18481 // @section Handlers 18482 // @property boxZoom: Handler 18483 // Box (shift-drag with mouse) zoom handler. 18484 Map.addInitHook('addHandler', 'boxZoom', BoxZoom); 18485 18486 /* 18487 * L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default. 18488 */ 18489 18490 // @namespace Map 18491 // @section Interaction Options 18492 18493 Map.mergeOptions({ 18494 // @option doubleClickZoom: Boolean|String = true 18495 // Whether the map can be zoomed in by double clicking on it and 18496 // zoomed out by double clicking while holding shift. If passed 18497 // `'center'`, double-click zoom will zoom to the center of the 18498 // view regardless of where the mouse was. 18499 doubleClickZoom: true 18500 }); 18501 18502 var DoubleClickZoom = Handler.extend({ 18503 addHooks: function () { 18504 this._map.on('dblclick', this._onDoubleClick, this); 18505 }, 18506 18507 removeHooks: function () { 18508 this._map.off('dblclick', this._onDoubleClick, this); 18509 }, 18510 18511 _onDoubleClick: function (e) { 18512 var map = this._map, 18513 oldZoom = map.getZoom(), 18514 delta = map.options.zoomDelta, 18515 zoom = e.originalEvent.shiftKey ? oldZoom - delta : oldZoom + delta; 18516 18517 if (map.options.doubleClickZoom === 'center') { 18518 map.setZoom(zoom); 18519 } else { 18520 map.setZoomAround(e.containerPoint, zoom); 18521 } 18522 } 18523 }); 18524 18525 // @section Handlers 18526 // 18527 // Map properties include interaction handlers that allow you to control 18528 // interaction behavior in runtime, enabling or disabling certain features such 18529 // as dragging or touch zoom (see `Handler` methods). For example: 18530 // 18531 // ```js 18532 // map.doubleClickZoom.disable(); 18533 // ``` 18534 // 18535 // @property doubleClickZoom: Handler 18536 // Double click zoom handler. 18537 Map.addInitHook('addHandler', 'doubleClickZoom', DoubleClickZoom); 18538 18539 /* 18540 * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default. 18541 */ 18542 18543 // @namespace Map 18544 // @section Interaction Options 18545 Map.mergeOptions({ 18546 // @option dragging: Boolean = true 18547 // Whether the map is draggable with mouse/touch or not. 18548 dragging: true, 18549 18550 // @section Panning Inertia Options 18551 // @option inertia: Boolean = * 18552 // If enabled, panning of the map will have an inertia effect where 18553 // the map builds momentum while dragging and continues moving in 18554 // the same direction for some time. Feels especially nice on touch 18555 // devices. Enabled by default. 18556 inertia: true, 18557 18558 // @option inertiaDeceleration: Number = 3000 18559 // The rate with which the inertial movement slows down, in pixels/second². 18560 inertiaDeceleration: 3400, // px/s^2 18561 18562 // @option inertiaMaxSpeed: Number = Infinity 18563 // Max speed of the inertial movement, in pixels/second. 18564 inertiaMaxSpeed: Infinity, // px/s 18565 18566 // @option easeLinearity: Number = 0.2 18567 easeLinearity: 0.2, 18568 18569 // TODO refactor, move to CRS 18570 // @option worldCopyJump: Boolean = false 18571 // With this option enabled, the map tracks when you pan to another "copy" 18572 // of the world and seamlessly jumps to the original one so that all overlays 18573 // like markers and vector layers are still visible. 18574 worldCopyJump: false, 18575 18576 // @option maxBoundsViscosity: Number = 0.0 18577 // If `maxBounds` is set, this option will control how solid the bounds 18578 // are when dragging the map around. The default value of `0.0` allows the 18579 // user to drag outside the bounds at normal speed, higher values will 18580 // slow down map dragging outside bounds, and `1.0` makes the bounds fully 18581 // solid, preventing the user from dragging outside the bounds. 18582 maxBoundsViscosity: 0.0 18583 }); 18584 18585 var Drag = Handler.extend({ 18586 addHooks: function () { 18587 if (!this._draggable) { 18588 var map = this._map; 18589 18590 this._draggable = new Draggable(map._mapPane, map._container); 18591 18592 this._draggable.on({ 18593 dragstart: this._onDragStart, 18594 drag: this._onDrag, 18595 dragend: this._onDragEnd 18596 }, this); 18597 18598 this._draggable.on('predrag', this._onPreDragLimit, this); 18599 if (map.options.worldCopyJump) { 18600 this._draggable.on('predrag', this._onPreDragWrap, this); 18601 map.on('zoomend', this._onZoomEnd, this); 18602 18603 map.whenReady(this._onZoomEnd, this); 18604 } 18605 } 18606 addClass(this._map._container, 'leaflet-grab leaflet-touch-drag'); 18607 this._draggable.enable(); 18608 this._positions = []; 18609 this._times = []; 18610 }, 18611 18612 removeHooks: function () { 18613 removeClass(this._map._container, 'leaflet-grab'); 18614 removeClass(this._map._container, 'leaflet-touch-drag'); 18615 this._draggable.disable(); 18616 }, 18617 18618 moved: function () { 18619 return this._draggable && this._draggable._moved; 18620 }, 18621 18622 moving: function () { 18623 return this._draggable && this._draggable._moving; 18624 }, 18625 18626 _onDragStart: function () { 18627 var map = this._map; 18628 18629 map._stop(); 18630 if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) { 18631 var bounds = toLatLngBounds(this._map.options.maxBounds); 18632 18633 this._offsetLimit = toBounds( 18634 this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1), 18635 this._map.latLngToContainerPoint(bounds.getSouthEast()).multiplyBy(-1) 18636 .add(this._map.getSize())); 18637 18638 this._viscosity = Math.min(1.0, Math.max(0.0, this._map.options.maxBoundsViscosity)); 18639 } else { 18640 this._offsetLimit = null; 18641 } 18642 18643 map 18644 .fire('movestart') 18645 .fire('dragstart'); 18646 18647 if (map.options.inertia) { 18648 this._positions = []; 18649 this._times = []; 18650 } 18651 }, 18652 18653 _onDrag: function (e) { 18654 if (this._map.options.inertia) { 18655 var time = this._lastTime = +new Date(), 18656 pos = this._lastPos = this._draggable._absPos || this._draggable._newPos; 18657 18658 this._positions.push(pos); 18659 this._times.push(time); 18660 18661 this._prunePositions(time); 18662 } 18663 18664 this._map 18665 .fire('move', e) 18666 .fire('drag', e); 18667 }, 18668 18669 _prunePositions: function (time) { 18670 while (this._positions.length > 1 && time - this._times[0] > 50) { 18671 this._positions.shift(); 18672 this._times.shift(); 18673 } 18674 }, 18675 18676 _onZoomEnd: function () { 18677 var pxCenter = this._map.getSize().divideBy(2), 18678 pxWorldCenter = this._map.latLngToLayerPoint([0, 0]); 18679 18680 this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x; 18681 this._worldWidth = this._map.getPixelWorldBounds().getSize().x; 18682 }, 18683 18684 _viscousLimit: function (value, threshold) { 18685 return value - (value - threshold) * this._viscosity; 18686 }, 18687 18688 _onPreDragLimit: function () { 18689 if (!this._viscosity || !this._offsetLimit) { return; } 18690 18691 var offset = this._draggable._newPos.subtract(this._draggable._startPos); 18692 18693 var limit = this._offsetLimit; 18694 if (offset.x < limit.min.x) { offset.x = this._viscousLimit(offset.x, limit.min.x); } 18695 if (offset.y < limit.min.y) { offset.y = this._viscousLimit(offset.y, limit.min.y); } 18696 if (offset.x > limit.max.x) { offset.x = this._viscousLimit(offset.x, limit.max.x); } 18697 if (offset.y > limit.max.y) { offset.y = this._viscousLimit(offset.y, limit.max.y); } 18698 18699 this._draggable._newPos = this._draggable._startPos.add(offset); 18700 }, 18701 18702 _onPreDragWrap: function () { 18703 // TODO refactor to be able to adjust map pane position after zoom 18704 var worldWidth = this._worldWidth, 18705 halfWidth = Math.round(worldWidth / 2), 18706 dx = this._initialWorldOffset, 18707 x = this._draggable._newPos.x, 18708 newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx, 18709 newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx, 18710 newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2; 18711 18712 this._draggable._absPos = this._draggable._newPos.clone(); 18713 this._draggable._newPos.x = newX; 18714 }, 18715 18716 _onDragEnd: function (e) { 18717 var map = this._map, 18718 options = map.options, 18719 18720 noInertia = !options.inertia || e.noInertia || this._times.length < 2; 18721 18722 map.fire('dragend', e); 18723 18724 if (noInertia) { 18725 map.fire('moveend'); 18726 18727 } else { 18728 this._prunePositions(+new Date()); 18729 18730 var direction = this._lastPos.subtract(this._positions[0]), 18731 duration = (this._lastTime - this._times[0]) / 1000, 18732 ease = options.easeLinearity, 18733 18734 speedVector = direction.multiplyBy(ease / duration), 18735 speed = speedVector.distanceTo([0, 0]), 18736 18737 limitedSpeed = Math.min(options.inertiaMaxSpeed, speed), 18738 limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed), 18739 18740 decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease), 18741 offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round(); 18742 18743 if (!offset.x && !offset.y) { 18744 map.fire('moveend'); 18745 18746 } else { 18747 offset = map._limitOffset(offset, map.options.maxBounds); 18748 18749 requestAnimFrame(function () { 18750 map.panBy(offset, { 18751 duration: decelerationDuration, 18752 easeLinearity: ease, 18753 noMoveStart: true, 18754 animate: true 18755 }); 18756 }); 18757 } 18758 } 18759 } 18760 }); 18761 18762 // @section Handlers 18763 // @property dragging: Handler 18764 // Map dragging handler (by both mouse and touch). 18765 Map.addInitHook('addHandler', 'dragging', Drag); 18766 18767 /* 18768 * L.Map.Keyboard is handling keyboard interaction with the map, enabled by default. 18769 */ 18770 18771 // @namespace Map 18772 // @section Keyboard Navigation Options 18773 Map.mergeOptions({ 18774 // @option keyboard: Boolean = true 18775 // Makes the map focusable and allows users to navigate the map with keyboard 18776 // arrows and `+`/`-` keys. 18777 keyboard: true, 18778 18779 // @option keyboardPanDelta: Number = 80 18780 // Amount of pixels to pan when pressing an arrow key. 18781 keyboardPanDelta: 80 18782 }); 18783 18784 var Keyboard = Handler.extend({ 18785 18786 keyCodes: { 18787 left: [37], 18788 right: [39], 18789 down: [40], 18790 up: [38], 18791 zoomIn: [187, 107, 61, 171], 18792 zoomOut: [189, 109, 54, 173] 18793 }, 18794 18795 initialize: function (map) { 18796 this._map = map; 18797 18798 this._setPanDelta(map.options.keyboardPanDelta); 18799 this._setZoomDelta(map.options.zoomDelta); 18800 }, 18801 18802 addHooks: function () { 18803 var container = this._map._container; 18804 18805 // make the container focusable by tabbing 18806 if (container.tabIndex <= 0) { 18807 container.tabIndex = '0'; 18808 } 18809 18810 on(container, { 18811 focus: this._onFocus, 18812 blur: this._onBlur, 18813 mousedown: this._onMouseDown 18814 }, this); 18815 18816 this._map.on({ 18817 focus: this._addHooks, 18818 blur: this._removeHooks 18819 }, this); 18820 }, 18821 18822 removeHooks: function () { 18823 this._removeHooks(); 18824 18825 off(this._map._container, { 18826 focus: this._onFocus, 18827 blur: this._onBlur, 18828 mousedown: this._onMouseDown 18829 }, this); 18830 18831 this._map.off({ 18832 focus: this._addHooks, 18833 blur: this._removeHooks 18834 }, this); 18835 }, 18836 18837 _onMouseDown: function () { 18838 if (this._focused) { return; } 18839 18840 var body = document.body, 18841 docEl = document.documentElement, 18842 top = body.scrollTop || docEl.scrollTop, 18843 left = body.scrollLeft || docEl.scrollLeft; 18844 18845 this._map._container.focus(); 18846 18847 window.scrollTo(left, top); 18848 }, 18849 18850 _onFocus: function () { 18851 this._focused = true; 18852 this._map.fire('focus'); 18853 }, 18854 18855 _onBlur: function () { 18856 this._focused = false; 18857 this._map.fire('blur'); 18858 }, 18859 18860 _setPanDelta: function (panDelta) { 18861 var keys = this._panKeys = {}, 18862 codes = this.keyCodes, 18863 i, len; 18864 18865 for (i = 0, len = codes.left.length; i < len; i++) { 18866 keys[codes.left[i]] = [-1 * panDelta, 0]; 18867 } 18868 for (i = 0, len = codes.right.length; i < len; i++) { 18869 keys[codes.right[i]] = [panDelta, 0]; 18870 } 18871 for (i = 0, len = codes.down.length; i < len; i++) { 18872 keys[codes.down[i]] = [0, panDelta]; 18873 } 18874 for (i = 0, len = codes.up.length; i < len; i++) { 18875 keys[codes.up[i]] = [0, -1 * panDelta]; 18876 } 18877 }, 18878 18879 _setZoomDelta: function (zoomDelta) { 18880 var keys = this._zoomKeys = {}, 18881 codes = this.keyCodes, 18882 i, len; 18883 18884 for (i = 0, len = codes.zoomIn.length; i < len; i++) { 18885 keys[codes.zoomIn[i]] = zoomDelta; 18886 } 18887 for (i = 0, len = codes.zoomOut.length; i < len; i++) { 18888 keys[codes.zoomOut[i]] = -zoomDelta; 18889 } 18890 }, 18891 18892 _addHooks: function () { 18893 on(document, 'keydown', this._onKeyDown, this); 18894 }, 18895 18896 _removeHooks: function () { 18897 off(document, 'keydown', this._onKeyDown, this); 18898 }, 18899 18900 _onKeyDown: function (e) { 18901 if (e.altKey || e.ctrlKey || e.metaKey) { return; } 18902 18903 var key = e.keyCode, 18904 map = this._map, 18905 offset; 18906 18907 if (key in this._panKeys) { 18908 if (!map._panAnim || !map._panAnim._inProgress) { 18909 offset = this._panKeys[key]; 18910 if (e.shiftKey) { 18911 offset = toPoint(offset).multiplyBy(3); 18912 } 18913 18914 if (map.options.maxBounds) { 18915 offset = map._limitOffset(toPoint(offset), map.options.maxBounds); 18916 } 18917 18918 if (map.options.worldCopyJump) { 18919 var newLatLng = map.wrapLatLng(map.unproject(map.project(map.getCenter()).add(offset))); 18920 map.panTo(newLatLng); 18921 } else { 18922 map.panBy(offset); 18923 } 18924 } 18925 } else if (key in this._zoomKeys) { 18926 map.setZoom(map.getZoom() + (e.shiftKey ? 3 : 1) * this._zoomKeys[key]); 18927 18928 } else if (key === 27 && map._popup && map._popup.options.closeOnEscapeKey) { 18929 map.closePopup(); 18930 18931 } else { 18932 return; 18933 } 18934 18935 stop(e); 18936 } 18937 }); 18938 18939 // @section Handlers 18940 // @section Handlers 18941 // @property keyboard: Handler 18942 // Keyboard navigation handler. 18943 Map.addInitHook('addHandler', 'keyboard', Keyboard); 18944 18945 /* 18946 * L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map. 18947 */ 18948 18949 // @namespace Map 18950 // @section Interaction Options 18951 Map.mergeOptions({ 18952 // @section Mouse wheel options 18953 // @option scrollWheelZoom: Boolean|String = true 18954 // Whether the map can be zoomed by using the mouse wheel. If passed `'center'`, 18955 // it will zoom to the center of the view regardless of where the mouse was. 18956 scrollWheelZoom: true, 18957 18958 // @option wheelDebounceTime: Number = 40 18959 // Limits the rate at which a wheel can fire (in milliseconds). By default 18960 // user can't zoom via wheel more often than once per 40 ms. 18961 wheelDebounceTime: 40, 18962 18963 // @option wheelPxPerZoomLevel: Number = 60 18964 // How many scroll pixels (as reported by [L.DomEvent.getWheelDelta](#domevent-getwheeldelta)) 18965 // mean a change of one full zoom level. Smaller values will make wheel-zooming 18966 // faster (and vice versa). 18967 wheelPxPerZoomLevel: 60 18968 }); 18969 18970 var ScrollWheelZoom = Handler.extend({ 18971 addHooks: function () { 18972 on(this._map._container, 'wheel', this._onWheelScroll, this); 18973 18974 this._delta = 0; 18975 }, 18976 18977 removeHooks: function () { 18978 off(this._map._container, 'wheel', this._onWheelScroll, this); 18979 }, 18980 18981 _onWheelScroll: function (e) { 18982 var delta = getWheelDelta(e); 18983 18984 var debounce = this._map.options.wheelDebounceTime; 18985 18986 this._delta += delta; 18987 this._lastMousePos = this._map.mouseEventToContainerPoint(e); 18988 18989 if (!this._startTime) { 18990 this._startTime = +new Date(); 18991 } 18992 18993 var left = Math.max(debounce - (+new Date() - this._startTime), 0); 18994 18995 clearTimeout(this._timer); 18996 this._timer = setTimeout(bind(this._performZoom, this), left); 18997 18998 stop(e); 18999 }, 19000 19001 _performZoom: function () { 19002 var map = this._map, 19003 zoom = map.getZoom(), 19004 snap = this._map.options.zoomSnap || 0; 19005 19006 map._stop(); // stop panning and fly animations if any 19007 19008 // map the delta with a sigmoid function to -4..4 range leaning on -1..1 19009 var d2 = this._delta / (this._map.options.wheelPxPerZoomLevel * 4), 19010 d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2, 19011 d4 = snap ? Math.ceil(d3 / snap) * snap : d3, 19012 delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom; 19013 19014 this._delta = 0; 19015 this._startTime = null; 19016 19017 if (!delta) { return; } 19018 19019 if (map.options.scrollWheelZoom === 'center') { 19020 map.setZoom(zoom + delta); 19021 } else { 19022 map.setZoomAround(this._lastMousePos, zoom + delta); 19023 } 19024 } 19025 }); 19026 19027 // @section Handlers 19028 // @property scrollWheelZoom: Handler 19029 // Scroll wheel zoom handler. 19030 Map.addInitHook('addHandler', 'scrollWheelZoom', ScrollWheelZoom); 19031 19032 /* 19033 * L.Map.TapHold is used to simulate `contextmenu` event on long hold, 19034 * which otherwise is not fired by mobile Safari. 19035 */ 19036 19037 var tapHoldDelay = 600; 19038 19039 // @namespace Map 19040 // @section Interaction Options 19041 Map.mergeOptions({ 19042 // @section Touch interaction options 19043 // @option tapHold: Boolean 19044 // Enables simulation of `contextmenu` event, default is `true` for mobile Safari. 19045 tapHold: Browser.touchNative && Browser.safari && Browser.mobile, 19046 19047 // @option tapTolerance: Number = 15 19048 // The max number of pixels a user can shift his finger during touch 19049 // for it to be considered a valid tap. 19050 tapTolerance: 15 19051 }); 19052 19053 var TapHold = Handler.extend({ 19054 addHooks: function () { 19055 on(this._map._container, 'touchstart', this._onDown, this); 19056 }, 19057 19058 removeHooks: function () { 19059 off(this._map._container, 'touchstart', this._onDown, this); 19060 }, 19061 19062 _onDown: function (e) { 19063 clearTimeout(this._holdTimeout); 19064 if (e.touches.length !== 1) { return; } 19065 19066 var first = e.touches[0]; 19067 this._startPos = this._newPos = new Point(first.clientX, first.clientY); 19068 19069 this._holdTimeout = setTimeout(bind(function () { 19070 this._cancel(); 19071 if (!this._isTapValid()) { return; } 19072 19073 // prevent simulated mouse events https://w3c.github.io/touch-events/#mouse-events 19074 on(document, 'touchend', preventDefault); 19075 on(document, 'touchend touchcancel', this._cancelClickPrevent); 19076 this._simulateEvent('contextmenu', first); 19077 }, this), tapHoldDelay); 19078 19079 on(document, 'touchend touchcancel contextmenu', this._cancel, this); 19080 on(document, 'touchmove', this._onMove, this); 19081 }, 19082 19083 _cancelClickPrevent: function cancelClickPrevent() { 19084 off(document, 'touchend', preventDefault); 19085 off(document, 'touchend touchcancel', cancelClickPrevent); 19086 }, 19087 19088 _cancel: function () { 19089 clearTimeout(this._holdTimeout); 19090 off(document, 'touchend touchcancel contextmenu', this._cancel, this); 19091 off(document, 'touchmove', this._onMove, this); 19092 }, 19093 19094 _onMove: function (e) { 19095 var first = e.touches[0]; 19096 this._newPos = new Point(first.clientX, first.clientY); 19097 }, 19098 19099 _isTapValid: function () { 19100 return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance; 19101 }, 19102 19103 _simulateEvent: function (type, e) { 19104 var simulatedEvent = new MouseEvent(type, { 19105 bubbles: true, 19106 cancelable: true, 19107 view: window, 19108 // detail: 1, 19109 screenX: e.screenX, 19110 screenY: e.screenY, 19111 clientX: e.clientX, 19112 clientY: e.clientY, 19113 // button: 2, 19114 // buttons: 2 19115 }); 19116 19117 simulatedEvent._simulated = true; 19118 19119 e.target.dispatchEvent(simulatedEvent); 19120 } 19121 }); 19122 19123 // @section Handlers 19124 // @property tapHold: Handler 19125 // Long tap handler to simulate `contextmenu` event (useful in mobile Safari). 19126 Map.addInitHook('addHandler', 'tapHold', TapHold); 19127 19128 /* 19129 * L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers. 19130 */ 19131 19132 // @namespace Map 19133 // @section Interaction Options 19134 Map.mergeOptions({ 19135 // @section Touch interaction options 19136 // @option touchZoom: Boolean|String = * 19137 // Whether the map can be zoomed by touch-dragging with two fingers. If 19138 // passed `'center'`, it will zoom to the center of the view regardless of 19139 // where the touch events (fingers) were. Enabled for touch-capable web 19140 // browsers. 19141 touchZoom: Browser.touch, 19142 19143 // @option bounceAtZoomLimits: Boolean = true 19144 // Set it to false if you don't want the map to zoom beyond min/max zoom 19145 // and then bounce back when pinch-zooming. 19146 bounceAtZoomLimits: true 19147 }); 19148 19149 var TouchZoom = Handler.extend({ 19150 addHooks: function () { 19151 addClass(this._map._container, 'leaflet-touch-zoom'); 19152 on(this._map._container, 'touchstart', this._onTouchStart, this); 19153 }, 19154 19155 removeHooks: function () { 19156 removeClass(this._map._container, 'leaflet-touch-zoom'); 19157 off(this._map._container, 'touchstart', this._onTouchStart, this); 19158 }, 19159 19160 _onTouchStart: function (e) { 19161 var map = this._map; 19162 if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; } 19163 19164 var p1 = map.mouseEventToContainerPoint(e.touches[0]), 19165 p2 = map.mouseEventToContainerPoint(e.touches[1]); 19166 19167 this._centerPoint = map.getSize()._divideBy(2); 19168 this._startLatLng = map.containerPointToLatLng(this._centerPoint); 19169 if (map.options.touchZoom !== 'center') { 19170 this._pinchStartLatLng = map.containerPointToLatLng(p1.add(p2)._divideBy(2)); 19171 } 19172 19173 this._startDist = p1.distanceTo(p2); 19174 this._startZoom = map.getZoom(); 19175 19176 this._moved = false; 19177 this._zooming = true; 19178 19179 map._stop(); 19180 19181 on(document, 'touchmove', this._onTouchMove, this); 19182 on(document, 'touchend touchcancel', this._onTouchEnd, this); 19183 19184 preventDefault(e); 19185 }, 19186 19187 _onTouchMove: function (e) { 19188 if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; } 19189 19190 var map = this._map, 19191 p1 = map.mouseEventToContainerPoint(e.touches[0]), 19192 p2 = map.mouseEventToContainerPoint(e.touches[1]), 19193 scale = p1.distanceTo(p2) / this._startDist; 19194 19195 this._zoom = map.getScaleZoom(scale, this._startZoom); 19196 19197 if (!map.options.bounceAtZoomLimits && ( 19198 (this._zoom < map.getMinZoom() && scale < 1) || 19199 (this._zoom > map.getMaxZoom() && scale > 1))) { 19200 this._zoom = map._limitZoom(this._zoom); 19201 } 19202 19203 if (map.options.touchZoom === 'center') { 19204 this._center = this._startLatLng; 19205 if (scale === 1) { return; } 19206 } else { 19207 // Get delta from pinch to center, so centerLatLng is delta applied to initial pinchLatLng 19208 var delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint); 19209 if (scale === 1 && delta.x === 0 && delta.y === 0) { return; } 19210 this._center = map.unproject(map.project(this._pinchStartLatLng, this._zoom).subtract(delta), this._zoom); 19211 } 19212 19213 if (!this._moved) { 19214 map._moveStart(true, false); 19215 this._moved = true; 19216 } 19217 19218 cancelAnimFrame(this._animRequest); 19219 19220 var moveFn = bind(map._move, map, this._center, this._zoom, {pinch: true, round: false}, undefined); 19221 this._animRequest = requestAnimFrame(moveFn, this, true); 19222 19223 preventDefault(e); 19224 }, 19225 19226 _onTouchEnd: function () { 19227 if (!this._moved || !this._zooming) { 19228 this._zooming = false; 19229 return; 19230 } 19231 19232 this._zooming = false; 19233 cancelAnimFrame(this._animRequest); 19234 19235 off(document, 'touchmove', this._onTouchMove, this); 19236 off(document, 'touchend touchcancel', this._onTouchEnd, this); 19237 19238 // Pinch updates GridLayers' levels only when zoomSnap is off, so zoomSnap becomes noUpdate. 19239 if (this._map.options.zoomAnimation) { 19240 this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.zoomSnap); 19241 } else { 19242 this._map._resetView(this._center, this._map._limitZoom(this._zoom)); 19243 } 19244 } 19245 }); 19246 19247 // @section Handlers 19248 // @property touchZoom: Handler 19249 // Touch zoom handler. 19250 Map.addInitHook('addHandler', 'touchZoom', TouchZoom); 19251 19252 Map.BoxZoom = BoxZoom; 19253 Map.DoubleClickZoom = DoubleClickZoom; 19254 Map.Drag = Drag; 19255 Map.Keyboard = Keyboard; 19256 Map.ScrollWheelZoom = ScrollWheelZoom; 19257 Map.TapHold = TapHold; 19258 Map.TouchZoom = TouchZoom; 19259 19260 exports.Bounds = Bounds; 19261 exports.Browser = Browser; 19262 exports.CRS = CRS; 19263 exports.Canvas = Canvas; 19264 exports.Circle = Circle; 19265 exports.CircleMarker = CircleMarker; 19266 exports.Class = Class; 19267 exports.Control = Control; 19268 exports.DivIcon = DivIcon; 19269 exports.DivOverlay = DivOverlay; 19270 exports.DomEvent = DomEvent; 19271 exports.DomUtil = DomUtil; 19272 exports.Draggable = Draggable; 19273 exports.Evented = Evented; 19274 exports.FeatureGroup = FeatureGroup; 19275 exports.GeoJSON = GeoJSON; 19276 exports.GridLayer = GridLayer; 19277 exports.Handler = Handler; 19278 exports.Icon = Icon; 19279 exports.ImageOverlay = ImageOverlay; 19280 exports.LatLng = LatLng; 19281 exports.LatLngBounds = LatLngBounds; 19282 exports.Layer = Layer; 19283 exports.LayerGroup = LayerGroup; 19284 exports.LineUtil = LineUtil; 19285 exports.Map = Map; 19286 exports.Marker = Marker; 19287 exports.Mixin = Mixin; 19288 exports.Path = Path; 19289 exports.Point = Point; 19290 exports.PolyUtil = PolyUtil; 19291 exports.Polygon = Polygon; 19292 exports.Polyline = Polyline; 19293 exports.Popup = Popup; 19294 exports.PosAnimation = PosAnimation; 19295 exports.Projection = index; 19296 exports.Rectangle = Rectangle; 19297 exports.Renderer = Renderer; 19298 exports.SVG = SVG; 19299 exports.SVGOverlay = SVGOverlay; 19300 exports.TileLayer = TileLayer; 19301 exports.Tooltip = Tooltip; 19302 exports.Transformation = Transformation; 19303 exports.Util = Util; 19304 exports.VideoOverlay = VideoOverlay; 19305 exports.bind = bind; 19306 exports.bounds = toBounds; 19307 exports.canvas = canvas; 19308 exports.circle = circle; 19309 exports.circleMarker = circleMarker; 19310 exports.control = control; 19311 exports.divIcon = divIcon; 19312 exports.extend = extend; 19313 exports.featureGroup = featureGroup; 19314 exports.geoJSON = geoJSON; 19315 exports.geoJson = geoJson; 19316 exports.gridLayer = gridLayer; 19317 exports.icon = icon; 19318 exports.imageOverlay = imageOverlay; 19319 exports.latLng = toLatLng; 19320 exports.latLngBounds = toLatLngBounds; 19321 exports.layerGroup = layerGroup; 19322 exports.map = createMap; 19323 exports.marker = marker; 19324 exports.point = toPoint; 19325 exports.polygon = polygon; 19326 exports.polyline = polyline; 19327 exports.popup = popup; 19328 exports.rectangle = rectangle; 19329 exports.setOptions = setOptions; 19330 exports.stamp = stamp; 19331 exports.svg = svg; 19332 exports.svgOverlay = svgOverlay; 19333 exports.tileLayer = tileLayer; 19334 exports.tooltip = tooltip; 19335 exports.transformation = toTransformation; 19336 exports.version = version; 19337 exports.videoOverlay = videoOverlay; 19338 19339 var oldL = window.L; 19340 exports.noConflict = function() { 19341 window.L = oldL; 19342 return this; 19343 } 19344 // Always export us to window global (see #2364) 19345 window.L = exports; 19346 19347}));
19348 19349 19350},{}]},{},[1]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.