PageSourceSearch

http://www.charygriffin.com/include/js/mapsearch/BingMapv8Wrapper.js

js charygriffin.com collected 2026-09-30 19:43:25 UTC 176,455 bytes, 4,591 lines download raw bytes

1if (typeof window.dmpLoaded === "undefined") {
2  window.dmpLoaded = window.dmpLoadStarted = false;
3}
4
5function BingMapv8Wrapper(options, parent) {
6  "use strict";
7
8  var _self = this,
9    _map = null,
10    _boundaryCount = 1,
11    _boundaryShapeLayerName = null,
12    _boundaryShapeLayerCount = 0,
13    _currentShapeName = null,
14    _mapContainer,
15    _loaded = false,
16    _mapMoveOrigin = null,
17    _mapState,
18    _mapH,
19    _mapW,
20    _layerCollections = {},
21    _pinIdLayers = {},
22    _layers = {},
23    _shapes = {},
24    _mapMoveCallback = null,
25    _infoBox,
26    _visibleBubblePosition = 0,
27    _infoboxTimer = null,
28    _infoboxLoadCount = 0,
29    _activeDialog = null,
30    _poiEnabled = true,
31    _trafficManager,
32    _tryingToZoomOut = false,
33    _currentDrawType = false,
34    _drawing = false,
35    _drawMouseDownHandler,
36    _drawMouseMoveHandler,
37    _drawMouseUpHandler,
38    _enableMouseMoveTimer = null,
39    _enableMouseMoveHandler = true,
40    _initialShapes = {},
41    _initialShapesTimer = null,
42    _loadingInitialShapes = true,
43    _addingShapes = {},
44    _addingShapesTimer = null,
45    _addingShapesTryCount = 0,
46    _bubbleLoading = false,
47    _firstViewChangeEnd = true,
48    _currentView = null,
49    _currentPinId = 0,
50    _dmpInitState = 1,
51    _publicRecords = {},
52    _parcelPropertyIds = {},
53    _parcelHover = {
54      lastMouseLocation: { x: 0, y: 0 },
55      mousePauseThreshold: 250,
56      pauseThread: null,
57      lastLocation: null,
58      lastPropertyId: null
59    };
60
61  // Zoom Level offsets for the Html based pushpins
62  // Because they are dynamic and we are trying to place them
63  // over the real property, we have to apply diffent anchors 
64  // for different zoom levels, otherwise the map places them incorrectly
65  var _zoomLevelOffsets = [
66    { x: 23, y: 25 },   // zoom level 0   
67    { x: 23, y: 25 },   // zoom level 1  
68    { x: 23, y: 25 },   // zoom level 2  
69    { x: 23, y: 25 },   // zoom level 3  
70    { x: 23, y: 25 },   // zoom level 4   
71    { x: 23, y: 25 },   // zoom level 5
72    { x: 23, y: 25 },   // zoom level 6
73    { x: 23, y: 25 },   // zoom level 7
74    { x: 23, y: 25 },   // zoom level 8
75    { x: 23, y: 25 },   // zoom level 9
76    { x: 23, y: 25 },   // zoom level 10
77    { x: 23, y: 25 },   // zoom level 11
78    { x: 23, y: 25 },   // zoom level 12
79    { x: 23, y: 27 },   // zoom level 13
80    { x: 23, y: 27 },   // zoom level 14
81    { x: 23, y: 25 },   // zoom level 15
82    { x: 23, y: 25 },   // zoom level 16
83    { x: 25, y: 20 },   // zoom level 17
84    { x: 30, y: 18 },   // zoom level 18
85    { x: 30, y: 15 },   // zoom level 19
86    { x: 30, y: 10 }    // zoom level 20
87  ];
88  var cachedInfo = false;
89  var _maxPricePinsToRender = 200,
90    _enableClusteringForPricePins = false,
91    _totalPropertiesPinCount = 0,
92    _layerCharacteristics = [],
93    _hullPolygon = null;
94
95  var MM = window.Microsoft.Maps;
96  var defaultOptions = {
97    disableStreetside: true,
98    pinMouseOverDelay: 1000,
99    pinMouseOutDelay: 1000,
100    shapeLineColor: new MM.Color(255, 186, 0, 255),
101    shapeFillColor: new MM.Color(25, 131, 185, 187),
102    shapeFillOpacity: 25,
103    shapeLineThickness: 2,
104    strokeDashArray: "5 2",
105    clusterPinColor: "rgb(110, 122, 141)",
106    maxItemsPerBubble: 30,
107    showClusterCount: false,
108    dmpLayers: [
109      { id: "ParcelLine", label: Utils.translate("Parcel lines", "Parcel lines"), resource: "SS.Base.Parcels/Parcels", sld: "RED.Public.Styles.Parcels/Default.sld.xml", lsld: "RED.Public.SLD/ReInsight_style_labels.sld.xml", zIndex: 3, zoomRange: { min: 16, max: 20 }, isVisible: true, labelIsVisible: false },
110      { id: "FloodZone", label: Utils.translate("Flood zones", "Flood zones"), resource: "SS.Hazards.DFIRM/DFIRMTiles", sld: "", lsld: "", zIndex: 3, zoomRange: { min: 11, max: 20 }, isVisible: false, labelIsVisible: false }
111    ],
112    mapText: {
113      "menuBar": {
114        "draw": Utils.translate("Draw", "Draw"),
115        "layer": Utils.translate("Layers", "Layers"),
116        "poi": Utils.translate("POIs", "POIs"),
117        "pin": Utils.translate("Pins", "Pins")
118      },
119      "navBar": {
120        "zoomIn": Utils.translate("Zoom in", ""),
121        "zoomOut": Utils.translate("Zoom out", ""),
122        "rotateRight": Utils.translate("Rotate right", ""),
123        "rotateLeft": Utils.translate("Rotate left", ""),
124        "reset": Utils.translate("Reset orientation", ""),
125      },
126      "mapTypeName": {
127        "a": Utils.translate("Aerial", "Aerial"),
128        "r": Utils.translate("Road", "Road"),
129        "be": Utils.translate("Birds eye", "Birds eye"),
130        "label": Utils.translate("Labels", "Labels"),
131      },
132      "mapTypeDescription": {
133        "a": Utils.translate("A detailed look from above", "A detailed look from above"),
134        "r": Utils.translate("A standard road map", "A standard road map"),
135        "be": Utils.translate("A better angle of aerial photography", "A better angle of aerial photography"),
136        "label": Utils.translate("for Aerial and Bird's eye", "for Aerial and Bird's eye"),
137      },
138      "clusterBubble": {
139        "propertyCount": Utils.translate("View {PropertyCount} properties", "View {PropertyCount} properties"),
140        "singlePropertyCount": Utils.translate("View {PropertyCount} property", "View {PropertyCount} property"),
141        "priceRange": Utils.translate("ranging in price from<br/>{MinPrice} to {MaxPrice}", "ranging in price from<br/>{MinPrice} to {MaxPrice}"),
142        "singlePrice": Utils.translate("priced at {ListPrice}", "priced at {ListPrice}"),
143        "averagePrice": Utils.translate("with an average list price of<br/>{AveragePrice}", "with an average list price of<br/>{AveragePrice}")
144      },
145      "dialogs": {
146        "draw": {
147          "drawTab": Utils.translate("Draw Shape", "Draw Shape"),
148          "radiusTab": Utils.translate("Draw Radius", "Draw Radius"),
149          "rectangleTab": Utils.translate("Draw Rectangle", "Draw Rectangle"),
150          "drawHeader": Utils.translate("Click Draw to begin drawing your shape", "Click Draw to begin drawing your shape. Hold down on the mouse button or c
150ontinuously press down with your finger to draw out your shape."),
151          "radiusHeader": Utils.translate("Click Draw to begin drawing out your radius", "Click Draw to begin drawing out your radius after selecting your starting location. Click and hold on the mouse and move it to expand the radius."),
152          "radiusFree": Utils.translate("Draw out a radius manually", "Draw out a radius manually"),
153          "radiusMyLocation": Utils.translate("Enter radius from my current location in miles:", "Enter radius from my current location in miles"),
154          "radiusMyLocationNote": Utils.translate("Note: You must allow the map the use of your location.", "Note: You must allow the map the use of your location."),
155          "radiusCenter": Utils.translate("Enter radius from the map center in miles:", "Enter radius from the map center in miles:"),
156          "rectangleHeader": Utils.translate("Click Draw to begin drawing out your rectangle or square on the map.", "Click Draw to begin drawing out your rectangle or square on the map."),
157          "footer": Utils.translate("Name your shape (Optional):", "Name your shape (Optional):"),
158          "buttonText": Utils.translate("Draw", "Draw"),
159        },
160        "layers": {
161          "headerMapView": Utils.translate("Select your map view:", "Select your map view"),
162          "header": Utils.translate("Select layer(s) to be added to the map:", "Select layer(s) to be added to the map:"),
163          "note": Utils.translate("Note: layers will only appear at a certain zoom level.", "Note: layers will only appear at a certain zoom level."),
164          "boundaryHeader": Utils.translate("Show boundary", "Show boundary"),
165          "labelHeader": Utils.translate("Show label", "Show label"),
166          "disclaimer": "",
167          "clear": Utils.translate("Clear all", "Clear all"),
168        },
169        "poi": {
170          "header": Utils.translate("Select points of interest to display on the map:", "Select points of interest to display on the map:"),
171          "note": Utils.translate("Note: POIs will only appear at a certain zoom level.", "Note: POIs will only appear at a certain zoom level."),
172          "clear": Utils.translate("Clear all", "Clear all"),
173        },
174        "pins": {
175          "header": Utils.translate("Select pin(s) to display on the map:", "Select pin(s) to display on the map:"),
176          "none": Utils.translate("No pins have been added to the map.", "No pins have been added to the map."),
177        }
178      },
179      "overlayText": {
180        "shapeRemover": Utils.translate("Remove", "Remove"),
181      }
182    },
183
184    // default values set in setDefaultPinTypeLayerNames() below
185    pinTypeLayerNames: {},
186    minZoom: 1
187  };
188  var LayerType = {
189    "Pin": 1,
190    "Poi": 2,
191    "Shape": 3,
192    "Cluster": 4
193  };
194
195  // define the type of BingMap v8 module that's used for the layer
196  var LayerModuleType = {
197    HtmlPushpinModule: "HtmlPushpinModule",
198    ClusteredModule: "ClusteredModule",
199    StandardModule: "StandardModule",
200  };
201
202  var customMapStyles = {
203    "Faded": {
204      "version": "1.0",
205      "elements": {
206        "vegetation": {
207          "fillColor": "#c5dea2"
208        },
209        "naturalPoint": {
210          "visible": false,
211          "labelVisible": false
212        },
213        "transportation": {
214          "labelOutlineColor": "#ffffff",
215          "fillColor": "#ffffff",
216          "strokeColor": "#d7d6d5"
217        },
218        "water": {
219          "fillColor": "#b1bdd6",
220          "labelColor": "#ffffff",
221          "labelOutlineColor": "#9aa9ca"
222        },
223        "structure": {
224          "fillColor": "#d7d6d5"
225        },
226        "indigenousPeoplesReserve": {
227          "visible": false
228        },
229        "military": {
230          "visible": false
231        }
232      },
233      "settings": {
234        "landColor": "#e7e6e5",
235        "shadedReliefVisible": false
236      }
237    }
238  };
239
240  // PinType enum is defined in /include/js/mapsearch/bingmap.js
241  (function setDefaultPinTypeLayerNames() {
242    function set(label, pinTypeKeys) {
243      if (typeof pinTypeKeys === "string")
244        pinTypeKeys = [pinTypeKeys];
245
246      pinTypeKeys.forEach(function (key) {
247        defaultOptions.pinTypeLayerNames[BingMap.PinType[key]] = label;
248      });
249    }
250    set(Utils.translate("Property"), ["ListingActive", "ListingOther", "ListingMultiple"]);
251    set(Utils.translate("Company Office"), "Office");
252    set(Utils.translate("New Property"), ["ListingNew", "ListingNewNoDoor"]);
253    set(Utils.translate("Open House Available"), ["ListingOpenHouse", "ListingOpenHouseMultiple"]);
254    set(Utils.translate("Sale Pending"), ["ListingPending", "ListingPendingMultiple", "ListingPendingNoDoor"]);
255    set(Utils.translate("Sold Property"), ["ListingSold", "ListingSoldMultiple"]);
256    set(Utils.translate("Commercial Property"), ["ListingCommercialActive", "ListingCommercialMultiple", "ListingCommercialNew", "ListingCommercialOther"]);
257    set(Utils.translate("Commercial Sale Pending"), ["ListingCommercialPending", "ListingCommercialPendingMultiple"]);
258    set(Utils.translate("Sold Commercial"), ["ListingCommercialSold", "ListingCommercialSoldMultiple"]);
259    set(Utils.translate("Valuation", "Valuation"), "Valuation");
260    set(Utils.translate("Coming Soon"), "ListingComingSoon");
261    set(Utils.translate("Auction"), "ListingAuction");
262    set(Utils.translate("Rent"), "ListingRent");
263  })();
264
265  // Collection functions
266
267  function translatePinTypeToLayerName(pinType, fallbackLayerName) {
268    return typeof options.pinTypeLayerNames[pinType] !== "undefined" ?
269      options.pinTypeLayerNames[pinType] : (fallbackLayerName || "Properties");
270  }
271
272  function getLayerCollection(layerName) {
273    if (typeof _layerCollections[layerName] === "undefined") {
274      _layerCollections[layerName] = {
275        name: layerName,
276        pins: [],
277        pinIds: {},
278        layers: {},
279        cluster: null
280      };
281    }
282
283    return _layerCollections[layerName];
284  }
285
286  function clearCollectionLayer(collection) {
287    collection.pins = [];
288    collection.pinIds = {};
289
290    Object.keys(collection.layers).forEach(function (layerName) {
291      clearLayer(collection.layers[layerName]);
292    });
293  }
294
295  // Layer functions
296  function getLayer(layerName, cluster, layerType, isVisible) {
297    var layer = _layers[layerName];
298    if (typeof (layer) === "undefined") {
299      if (typeof cluster === "undefined" || cluster === null) {
300        cluster = true;
301      }
302      if (typeof isVisible === "undefined" || isVisible === null) {
303        isVisible = true;
304      }
305      if (typeof layerType === "undefined" || layerType === null) {
306        layerType = LayerType.Pin;
307      }
308
309      var mapLayer = createMapLayer(layerName);
310
311      layer = {
312        name: layerName,
313        pins: [],
314        pinIds: {},
315        layer: mapLayer.mapLayerObject,
316        cluster: cluster,
317        isVisible: isVisible,
318        layerType: layerType,
319        layerModule: mapLayer.layerModule
320      };
321
322      if (layer.layerModule === LayerModuleType.StandardModule) {
323        if (layerType === LayerType.Shape) {
324          layer.layer.setZIndex(10002);
325        } else {
326          layer.layer.setZIndex(10003);
327        }
328      }
329      _layers[layerName] = layer;
330    }
331    return layer;
332  }
333
334  function clearLayer(layer) {
335    layer.pins = [];
336    var msLayer = layer.layer;
337    if (typeof msLayer !== "undefined" && msLayer != null) {
338      _map.layers.remove(msLayer);
339    }
340    layer.layer = createMapLayer(layer.name).mapLayerObject;
341  }
342
343  function createMapLayer(layerName) {
344    var layerModule, mapLayerObject;
345
346    if (isSupportedHtmlPushpin(layerName)) {
347      if (_enableClusteringForPricePins) {
348        mapLayerObject = new MM.ClusterLayer();
349        layerModule = LayerModuleType.ClusteredModule;
350      } else {
351        mapLayerObject = new HtmlPushpinLayer();
352        layerModule = LayerModuleType.HtmlPushpinModule;
353      }
354    } else {
355      mapLayerObject = new MM.Layer();
356      layerModule = LayerModuleType.StandardModule;
357    }
358
359    return { mapLayerObject: mapLayerObject, layerModule: layerModule };
360  }
361
362  //pass an address and return a promise
363  function getLatLongFromAddress(address) {
364    var deferred = $.Deferred();
365    $.getJSON('https://dev.virtualearth.net/REST/v1/Locations?query=' + address + '&key=' + Utils.BingMapKey + '&jsonp=?').done(function (data) {
366      if (data && data.resourceSets.length && data.resourceSets[0].resources.length && data.resourceSets[0].resources[0].geocodePoints.length) {
367        var lat = data.resourceSets[0].resources[0].geocodePoints[0].coordinates[0];
368        var long = data.resourceSets[0].resources[0].geocodePoints[0].coordinates[1];
369        if (lat && long) {
370          deferred.resolve([lat, long]);
371        }
372      }
373    });
374    return deferred;
375  }
376
377  this.directionsManager;
378
379  this.getDirections = function (location) {
380    MM.loadModule('Microsoft.Maps.Directions', (function () {
381      this.directionsManager = this.directionsManager || new MM.Directions.DirectionsManager(_map);
382      // Set the element in which the itinerary will be rendered
383      this.directionsManager.setRenderOptions({ itineraryContainer: document.getElementById('dd-route') });
384      //this.directionsManager.showInputPanel('directionsInputContainer');
385      //clear any existing directions
386      this.directionsManager.clearAll();
387      // Set Route Mode to driving
388      this.directionsManager.setRequestOptions({ routeMode: MM.Directions.RouteMode.driving });
389      $.when(getLatLongFromAddress(location[0]), getLatLongFromAddress(location[1])).done((function (start, finish) {
390        var waypoint1 = new MM.Directions.Waypoint({ address: location[0], location: new MM.Location(start[0], start[1]) });
391        var waypoint2 = new MM.Directions.Waypoint({ address: location[1], location: new MM.Location(finish[0], finish[1]) });
392        this.directionsManager.addWaypoint(waypoint1);
393        this.directionsManager.addWaypoint(waypoint2);
394        this.directionsManager.calculateDirections();
395      }).bind(this));
396    }).bind(this));
397  };
398
399  this.setupDrivingDirections = function (fromLocation, toLocation, routeID, directionsID) {
400    MM.loadModule('Microsoft.Maps.Directions', (function () {
401      this.directionsManager = this.directionsManager || new MM.Directions.DirectionsManager(_map);
402      // Set the element in which the itinerary will be rendered
403      this.directionsManager.setRenderOptions({ itineraryContainer: document.getElementById(routeID) });
404      this.directionsManager.showInputPanel(directionsID);
405
406      // Set Route Mode to driving
407      this.directionsManager.setRequestOptions({ routeMode: MM.Directions.RouteMode.driving });
408
409      /*$.when(getLatLongFromAddress(fromLocation), getLatLongFromAddress(toLocation)).done((function (start, finish) {
410          var waypoint1 = new MM.Directions.Waypoint({ address: fromLocation, location: new MM.Location(start[0], start[1]) });
411          var waypoint2 = new MM.Directions.Waypoint({ address: toLocation, location: new MM.Location(finish[0], finish[1]) });
412          this.directionsManager.addWaypoint(waypoint1);
413          this.directionsManager.addWaypoint(waypoint2);
414      }).bind(this));*/
415    }).bind(this));
416  };
417
418  function removeCustomShape(e) {
419    var $div = $(e.target);
420    $div.hide();
421    var shapeName = $div.attr("data-shapename");
422    _self.clearPolygon(shapeName);
423    $(document).trigger("removecriteria", [
424      {
425        name: "Criteria/Location",
426        val: shapeName
427      }
428    ]);
429    _mapMoveOrigin = "RemoveCustomShape";
430    _self.onChangeView();
431  }
432
433  function hideShapeRemover() {
434    var $div = $("div.map-shape-remover", _mapContainer);
435    if ($div.length && $div.is(":visible")) {
436      var shapeName = $div.attr("data-shapename");
437      var shape = _shapes[shapeName];
438      if (shape && shape.length) {
439        var shapeOpts = {
440          strokeColor: options.shapeLineColor,
441          fillColor: options.shapeFillColor,
442          strokeThickness: options.shapeLineThickness
443        };
444        shape[0].setOptions(shapeOpts);
445      }
446      $div.hide();
447    }
448  }
449
450  function showShapeRemover(e) {
451    var shape = e.target;
452    var locs = shape.getLocations();
453    var minLat = 90, maxLat = 0, minLon = 0, maxLon = -180;
454    var midLat, midLon;
455    var topRightLoc, offset;
456    for (var i = 0; i < locs.length; i++) {
457      var loc = locs[i];
458      minLat = Math.min(minLat, loc.latitude);
459      maxLat = Math.max(maxLat, loc.latitude);
460      minLon = Math.min(minLon, loc.longitude);
461      maxLon = Math.max(maxLon, loc.longitude);
462    }
463    if (shape.center) { // Radius
464      midLat = (maxLat + (shape.center.latitude + maxLat) / 2) / 2;
465      midLon = (maxLon + (shape.center.longitude + maxLon) / 2) / 2;
466      topRightLoc = new MM.Location(midLat, midLon);
467      offset = _map.tryLocationToPixel(topRightLoc, MM.PixelReference.control);
468    } else if (shape.start) { // Rectangle
469      topRightLoc = new MM.Location(maxLat, maxLon);
470      offset = _map.tryLocationToPixel(topRightLoc, MM.PixelReference.control);
471    } else {
472      midLat = (maxLat + ((maxLat + minLat) / 2)) / 2;
473      midLon = (maxLon + ((maxLon + minLon) / 2)) / 2;
474      topRightLoc = locs.reduce(function (x, y) {
475        var xLatDist = x.latitude - midLat;
476        var xLonDist = x.longitude - midLon;
477        var yLatDist = y.latitude - midLat;
478        var yLonDist = y.longitude - midLon;
479        return (xLatDist + xLonDist > yLatDist + yLonDist) ? x : y;
480      });
481      offset = _map.tryLocationToPixel(topRightLoc, MM.PixelReference.control);
482    }
483    // Redraw the shape with a different color
484    var shapeOpts = {
485      strokeColor: new MM.Color(255, 235, 40, 40),
486      fillColor: new MM.Color(25, 235, 40, 40),
487      strokeThickness: options.shapeLineThickness
488    };
489    shape.setOptions(shapeOpts);
490
491    $("div.map-shape-remover", _mapContainer)
492      .attr("data-shapename", shape.name)
493      .css({ top: offset.y, left: offset.x })
494      .show();
495  }
496
497  function addShapesToLayer(shapes, layer, cb) {
498    // Clear out any existing shapes and layers
499    clearLayer(layer);
500    shapes = typeof shapes === "undefined" || shapes === null
501      ? []
502      : shapes;
503
504    // Add the shapes to the layer
505    layer.layer.add(shapes);
506    // If this is a custom shape, add the click event to remove
507    if (shapes && shapes.length && shapes[0].isCustom) {
508      MM.Events.addHandler(layer.layer, "click", showShapeRemover);
509    }
510
511    _map.layers.insert(layer.layer);
512
513    _shapes[layer.name] = shapes;
514    // Call the callback
515    if (typeof cb === "function") {
516      cb(shapes);
517    }
518  }
519
520  // Bubble/infobox functions
521
522  function setInfoBoxOffset(pin, afterOffset) {
523    // Try to get the body from the popup.  Retry on delay
524    if (_infoboxTimer !== null) {
525      clearTimeout(_infoboxTimer);
526      _infoboxTimer = null;
527    }
528    var bodyHtml = $(".pushpinPopupBody", _mapContainer).html();
529    if (!bodyHtml || !bodyHtml.length) {
530      // Only try 40 times (8 seconds), in case there's something wrong
531      if (_infoboxLoadCount++ < 40) {
532        _infoboxTimer = setTimeout(function () {
533          setInfoBoxOffset(pin, afterOffset);
534        }, 200);
535      }
536      return;
537    }
538    // Reset the retry count
539    _infoboxLoadCount = 0;
540    // Get the current dimensions
541    _mapW = $(_mapContainer).outerWidth();
542    _mapH = $(_mapContainer).outerHeight();
543
544    var html, top, left;
545    // Get the initial location of the infobox as set by Bing
546    var loc = _map.tryLocationToPixel(_infoBox.getLocation(), MM.PixelReference.control);
547    // Get the current height and width of the body, this will be used to determine the offset
548    var boxH = $(".pushpinPopupBody").height();
549    var boxW = $(".pushpinPopupBody").width();
550
551    //Incase the popup element not loaded properly
552    if (pin.iconTemplates !== undefined && pin.iconTemplates.length > 1) {
553      if (boxH < 260) {
554        boxH = 260;
555      }
556    } else {
557      if (boxH < 210) {
558        boxH = 210;
559      }
560    }
561
562    var pinType = getPinType(pin);
563    var arrowTop, arrowDir;
564
565    if (pinType === BingMap.PinType.Valuation) {
566      top = 30;
567      left = -25;
568      var propertyId = pin.id.replace('val_1_', '');
569      var forceTop = $(".pushpinPopupBody").find(".bubble-loading").length ? -40 : -25;
570      html = "<div class='pushpinPopup valuationPopup' style='top: " +
571        forceTop +
572        "px;' data-property-id='" +
573        propertyId +
574        "'>" +
575        "<div class='pushpinPopupArrowBottomSmall'></div>" +
576        "<div class='pushpinPopupBody'>" +
577        bodyHtml +
578        "</div>" +
579        "</div>";
580    } else if (pin.tag === 'ClusterPin') {
581      top = 30;
582      var pinRadius = (pin.image.width / 2) || 18;
583      if (loc.x > _mapW / 2) { // Right half of map
584        // The box will be on the left side of the pin, so offset the arrow likewise
585        left = -1 * (pinRadius + 25 + boxW);
586        arrowDir = "Right";
587      } else { // Left half
588        // On the right side of the pin, offset 45 px to the left.
589        // Limitation here, depends on the arrow always being styled to be the same size
590        left = pinRadius + 15;
591        arrowDir = "Left";
592      }
593      // Override the min-height if this is a cluster pin
594      html = "<div class='pushpinPopup' style='min-height: 45px !important; top: 0 !important'>" +
595        "<div class='pushpinPopupArrow" + arrowDir + "' style='margin-top: 3px !important'></div>" +
596        "<div class='pushpinPopupBody' style='min-height: 45px !important;'>" + bodyHtml + "</div>" +
597        "</div>";
598
599    } else {
600      if (loc.y > _mapH / 2) { // Bottom half of map
601        // Get the midway point between the pin and the bottom
602        // of the map, and position the top the height of the
603        // infobox above it
604        top = boxH - ((_mapH - loc.y) / 2);
605      } else { // Top half
606        // Get the midway point between the pin and the top of the map, and
607        // make that the top
608        top = (loc.y / 2);
609      }
610      // If the top is too close to the pin location, the
611      // arrow won't look good, so adjust
612      if (top < 60) top = 60;
613      // If the top is beyond the top of the map, move it down to the top of the map
614      if (top > loc.y) {
615        top = loc.y;
616      }
617      // if the bottom of the box is too high, bring it down next to 
618      // the pin so the arrow looks good
619      if (boxH - top < 40) {
620        top = boxH / 2;
621      }
622
623      var scroll =
624        window.scrollY //Modern Way (Chrome, Firefox)
625        || window.pageYOffset
626        || document.documentElement.scrollTop;
627
628
629      var verticalHidden = $(_mapContainer).offset().top + _mapH - $(window).height() - scroll;
630      verticalHidden = verticalHidden > 0 ? verticalHidden : 0;
631
632      // If the bottom of the infobox is lower than the bottom of
633      // the map, move it up a little
634      // (If bubbles are taller than the map, not currently supported, 
635      // but maybe we can add something to put a scrollbar in the pushPinBody)
636      if (top !== loc.y && loc.y - top + boxH + verticalHidden > _mapH) {
637        top = loc.y - (_mapH - boxH - verticalHidden);
638        arrowTop = top - 35;
639      } else if (loc.y < 83) {
640        // Boundary case for adjusting display when pin too close to top edge
641        top = -25 + loc.y;
642        arrowTop = top - 35;
643      } else {
644        // The default arrow offset.  Honestly don't know how this 35 works, but this
645        // always puts it on the pin no matter where the top is
646        arrowTop = top - 35;
647      }
648
649      var leftOffset = 40;
650      if (loc.x > _mapW / 2) { // Right half of map
651        // The box will be on the left side of the pin, so offset the arrow likewise
652        left = -1 * (leftOffset + boxW);
653        arrowDir = "Right";
654      } else { // Left half
655        // On the right side of the pin, offset 20 px to the left.
656        // Limitation here, depends on the arrow always being styled to be the same size
657        left = leftOffset;
658        arrowDir = "Left";
659      }
660
661      html = "<div class='pushpinPopup'>" +
662        "<div class='pushpinPopupArrow" + arrowDir + "' style='top: " + arrowTop + "px;'></div>" +
663        "<div class='pushpinPopupBody'>" + bodyHtml + "</div>" +
664        "</div>";
665    }
666
667    // Update the infobox with the new offset.  We have to reset the html in the options
668    // or it goes back to the original html
669    var offset = new MM.Point(left, top);
670    _infoBox.setOptions({
671      visible: true,
672      showCloseButton: false,
673      offset: offset,
674      htmlContent: html,
675      zIndex: 100
676    });
677    // Sometimes after the image loads the offset is wrong
678    if (!afterOffset) {
679      $("#listingPhoto", $(".pushpinPopup")).one("load", function () {
680        var newBoxH = $(".pushpinPopupBody").height();
681        if (boxH !== newBoxH) {
682          setInfoBoxOffset(pin, true);
683        }
684      });
685    }
686    // If the pin is near the top of the map, hide the menu bar so the infobox
687    // won't be hidden
688    if (cachedInfo) {
689      setTimeout(function () {
690        var mapTop = $(_mapContainer).offset().top;
691        var popupTop = $(".pushpinPopup").offset().top;
692        if (popupTop - mapTop < 80) {
693          $("div.map-menubar", _mapContainer).slideUp(250, function () { $("div.map-menubar", _mapContainer).css("overflow", "visible") });
694        }
695      }, 1000);
696
697    }
698
699    if (pinType === BingMap.PinType.Valuation) {
700      // If needed, hide the zoom buttons. Pick the correct #MapNavBar (i.e., not the one from Public Records overlay).
701      var $zoomButtons = $("#MapNavBar", "#mapsearch-mainmap");
702      if (isOverlap($zoomButtons.get(0), $(".pushpinPopup").get(0))) {
703        $zoomButtons.hide();
704      }
705    }
706  }
707
708  function isOverlap(elem1, elem2) {
709    if (!elem1 || !elem2) return false;
710
711    var rect1 = elem1.getBoundingClientRect();
712    var rect2 = elem2.getBoundingClientRect();
713
714    return !(rect1.right < rect2.left ||
715      rect1.left > rect2.right ||
716      rect1.bottom < rect2.top ||
717      rect1.top > rect2.bottom);
718  }
719
720  function showProperBubbleDiv(pin) {
721    // The _visibleBubblePosition is set when mousing over a listing in 
722    // the grid below the map.  It tells us which listing to display
723    // in a multi-listing bubble.
724    if (_visibleBubblePosition > 0 && !_bubbleLoading) {
725      $("#pin_" + _visibleBubblePosition, _mapContainer).show();
726      $("#pin_0", _mapContainer).hide();
727      _visibleBubblePosition = 0;
728    }
729    setInfoBoxOffset(pin);
730  }
731
732  function getToggleBubbleHtml(count, curPosition, displayName) {
733    var html = new StringBuilder();
734
735    var displayNames;
736    if (displayName.substring(displayName.length - 1, 1) === "y") {
737      displayNames = displayName.substring(0, displayName.length - 2) + "ies";
738    } else {
739      displayNames = displayName + "s";
740    }
741
742    html.append("<tr><td valign='top'><table cellspacing='0' cellpadding='0' border='0' class='txt_blk_graybg' width='100%'>");
743    html.append("<tr><td colspan='3'>Multiple " + displayNames + " found here</td></tr>");
744    html.append("<tr><td align='left' width='30%'>");
745    if (curPosition > 0) {
746      html.append("<a href='javascript:;' onclick='javascript:Search.togglePinBubble(" + (curPosition - 1) + "," + curPosition + " )'>&lt;" + Utils.translate("Prev", "Prev") + "</a>");
747    }
748    html.append("</td><td align='center' width='40%'>");
749    html.append("<b>" + (curPosition + 1) + " of " + count + "</b>");
750    html.append("</td><td align='right' width='30%'>");
751    if (curPosition < count - 1) {
752      html.append("<a href='javascript:;' onclick='javascript:Search.togglePinBubble(" + (curPosition + 1) + "," + curPosition + ")'> &gt;" + Utils.translate("Next", "Next") + "</a>");
753    }
754    html.append("</td></tr></table></td></tr>");
755
756    return html.toString();
757  }
758
759  function getToggleBubbleHtml2(count, curPosition, displayName) {
760    var html = "";
761    var cleanedDisplayName;
762    if (displayName.substring(displayName.length - 1, 1) === "y") {
763      cleanedDisplayName = displayName.substring(0, displayName.length - 2) + "ies";
764      cleanedDisplayName = Utils.translate(cleanedDisplayName, cleanedDisplayName);
765    } else {
766      cleanedDisplayName = displayName + "s";
767      cleanedDisplayName = Utils.translate(cleanedDisplayName, cleanedDisplayName);
768    }
769
770    html += "<tr><td valign='top'><table cellspacing='0' cellpadding='0' border='0' width='100%'>";
771    html += "<tr><td class='r-bubble-navigation-left'>";
772    if (curPosition > 0) {
773      html += "<a href='javascript:;' onclick='javascript:Search.togglePinBubble(" + (curPosition - 1) + "," + curPosition + " )'><i class='fa fa-angle-left'></i> " + Utils.translate('Prev', 'Prev') + "</a>";
774    }
775    html += "</td><td class='r-bubble-navigation-center'>";
776    html += "<b>" + Utils.translate('{[0]} of {[1]}', '{[0]} of {[1]}').replace('{[0]}', curPosition + 1).replace('{[1]}', count) + " " + cleanedDisplayName + "</b>";
777    html += "</td><td class='r-bubble-navigation-right'>";
778    if (curPosition < count - 1) {
779      html += "<a href='javascript:;' onclick='javascript:Search.togglePinBubble(" + (curPosition + 1) + "," + curPosition + ")'>" + Utils.translate('Next', 'Next') + " <i class='fa fa-angle-right'></i></a>";
780    }
781    html += "</td></tr></table></td></tr>";
782
783    return html;
784  }
785
786  function getPinType(pin) {
787    if (!pin || !pin.id) return 0;
788
789    var joinedPinIds = pin.id;
790    var firstPinId = joinedPinIds.split('-').shift();
791    var pinLayer = _pinIdLayers[firstPinId];
792    var pinInfo = pinLayer.pinIds[joinedPinIds];
793    return pinInfo.pinType;
794  }
795
796  function getBubbleHtmlAsync(pin) {
797    if (getPinType(pin) === BingMap.PinType.Valuation) {
798      getBubbleHtmlAsyncValuation(pin);
799    } else {
800      getBubbleHtmlAsyncListing(pin);
801    }
802  }
803
804  function getPinClusterLabel(pinsInCluster) {
805    return pinsInCluster != null ? pinsInCluster.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : null;
806  }
807
808  function toMapFriendlyPrice(price) {
809    // Ensure number has max 3 significant digits (no rounding up can happen)
810    var i = Math.pow(10, Math.max(0, Math.log10(price) - 2));
811    price = price / i * i;
812
813    if (price >= 1000000000) {
814      return parseFloat((price / 1000000000.0).toFixed(2)) + "B";
815    }
816
817    if (price >= 1000000) {
818      return parseFloat((price / 1000000.0).toFixed(2)) + "M";
819    }
820
821    if (price >= 1000) {
822      return Math.floor(price / 1000.0) + "K";
823    }
824
825    return Math.floor(price);
826  }
827
828  function getBubbleHtmlCluster(pin) {
829    var propertyCountText = pin.pinsInCluster === 1
830      ? options.mapText.clusterBubble.singlePropertyCount
831      : options.mapText.clusterBubble.propertyCount;
832    var bubbleHtml = '<div id="clusterBubble"><div id="countBubble" style="text-align:center;">' +
833      propertyCountText.replace('{PropertyCount}', '<strong>' + getPinClusterLabel(pin.pinsInCluster) + '</strong>') +
834      '</div>';
835    var minPrice = toMapFriendlyPrice(pin.minPrice);
836    var maxPrice = toMapFriendlyPrice(pin.maxPrice);
837    if (minPrice !== maxPrice) {
838      if (options.mapText.clusterBubble.priceRange.length > 0) {
839        bubbleHtml += '<div id="priceBubble" style="text-align:center">' +
840          options.mapText.clusterBubble.priceRange
841            .replace('{MinPrice}', '<strong>$' + minPrice + '</strong>')
842            .replace('{MaxPrice}', '<strong>$' + maxPrice + '</strong>') +
843          '</div>';
844      }
845      if (pin.avgPrice > 0 && options.mapText.clusterBubble.averagePrice.length > 0) {
846        bubbleHtml += '<div id="avgPriceBubble" style="text-align:center">' +
847          options.mapText.clusterBubble.averagePrice
848            .replace('{AveragePrice}', '<strong>$' + toMapFriendlyPrice(pin.avgPrice) + '</strong>') +
849          '</div>';
850      }
851    } else if (pin.maxPrice > 0 && options.mapText.clusterBubble.singlePrice.length > 0) {
852      bubbleHtml += '<div id="priceBubble" style="text-align:center">' +
853        options.mapText.clusterBubble.singlePrice
854          .replace('{ListPrice}', '<strong>$' + maxPrice + '</strong>') +
855        '</div>';
856    }
857    bubbleHtml += '</div>';
858    return bubbleHtml;
859  }
860
861  function getBubbleHtmlAsyncListing(pin) {
862    _bubbleLoading = true;
863
864    var joinedPinIds = pin.id;
865
866    var firstPinId = joinedPinIds.split("-").shift();
867    var pinLayer = _pinIdLayers[firstPinId];
868    var pinInfo = pinLayer.pinIds[joinedPinIds];
869    var pinType = pinInfo.pinType;
870
871    // The ListingIDs are embedded in the PinIDs
872    var url = Utils.AppPath + "/Include/AJAX/MapSearch/GetListingBubble.aspx?searchoverride=" + options.cookieGuid;
873
874    if (pinType === 3) {
875      url += "&" +
876        "Office" +
877        "ID=" +
878        joinedPinIds.replace(/3_/g, "");
879    } else {
880      url += "&ListingID=" + joinedPinIds.replace(/1_/g, "");
881    }
882
883    // Special layout overrides for preview functionality
884    var qs = new Querystring();
885    if (qs.contains("LayoutID")) {
886      url += "&LayoutID=" + qs.get("LayoutID") + "&LayoutVersion=" + qs.get("LayoutVersion");
887    }
888    if (qs.contains("embed")) {
889      url += "&embed=" + qs.get("embed");
890    }
891    if (qs.contains("collab") || qs.contains("mozaic")) {
892      url += "&mozaic=1";
893    }
894
895    $.ajax({
896      mode: "abort",
897      port: "bubble",
898      type: "GET",
899      url: url,
900      cache: false,
901      success: function (resp) {
902        // Do we have a multi-pin here?
903        if (resp.indexOf("~~") > 0) {
904          var desc = new StringBuilder();
905          var arrDesc = resp.split("~~");
906          for (var i = 0, len = arrDesc.length; i < len; i++) {
907            desc.append("<table width='100%' valign='top' cellspacing='1' cellpadding='1' class='txt_blk' ");
908            if (i > 0) {
909              desc.append("style='display:none;'");
910            }
911            desc.append(" id='pin_" + i + "''>");
912            if (options != null && options.toggleBubbleVersion === 2) {
913              desc.append(getToggleBubbleHtml2(len, i, BingMap.PinInfo[pinType].DisplayName));
914            } else {
915              desc.append(getToggleBubbleHtml(len, i, BingMap.PinInfo[pinType].DisplayName));
916            }
917            desc.append("<tr><td>" + arrDesc[i] + "</td></tr>");
918            desc.append("</table>");
919          }
920          resp = desc.toString();
921        }
922
923        // This fills the bubble immediately
924        $("div.pushpinPopupBody", _mapContainer).html(resp);
925        // After the bubble is shown, set it to the right div in a multi-listing bubble
926        _bubbleLoading = false;
927        showProperBubbleDiv(pin);
928        // This fills it for the next open
929        pin.pinDesc = resp;
930        // Raise an event to say the bubble is filled and displaying
931        $(document).trigger("mapbubbledisplay", [joinedPinIds]);
932      }
933    });
934  }
935
936  function getBubbleHtmlAsyncValuation(pin) {
937    _bubbleLoading = true;
938
939    // The PropertyId is embedded in the Pin ID
940    var propertyId = pin.id.replace('val_1_', '');
941    var url = Utils.AppPath + "/Include/AJAX/MapSearch/GetValuationBubble.aspx?propertyId=" + propertyId;
942    $.ajax({
943      mode: "abort",
944      port: "bubble",
945      type: "GET",
946      url: url,
947      cache: false,
948      success: function (resp) {
949        // This fills the bubble immediately
950        if (resp) {
951          $("div.pushpinPopupBody", _mapContainer).html(resp);
952          _bubbleLoading = false;
953          setInfoBoxOffset(pin);
954          // This fills it for the next open
955          pin.pinDesc = resp;
956          // Raise an event to say the bubble is filled and displaying
957          $(document).trigger("mapbubbledisplay");
958        } else {
959          _self.hideListingBubble();
960        }
961      }
962    });
963  }
964
965  function getBubbleHtml(pin) {
966    var pinid = pin.id;
967    if (typeof pinid === 'undefined') {
968      return "";
969    }
970
971    // If we've retrieved the bubble html via ajax already, just display that
972    if (pin.pinDesc && pin.pinDesc.length > 0) {
973      cachedInfo = true;
974      return pin.pinDesc;
975    } else {
976      // Get the bubble html
977      cachedInfo = false;
978      getBubbleHtmlAsync(pin);
979      // Set the 'loading' text until our ajax request returns
980      return "<div style='width:100%; text-align:center;' class='bubble-loading'>" +
981        "<span style='height:50px;'><br/><br/><img src='" + options.imageBase + "/search/Images/spinner.gif' border='0' /> " + Utils.translate("Loading", "Loading") + "...</span>" +
982        "</div>";
983    }
984  }
985
986  function showBubble(pin) {
987    if (!options.showBubble) return;
988    if (_activeDialog !== null) return;
989
990    if (typeof _infoBox === "undefined" || _infoBox == null) {
991      _infoBox = new MM.Infobox(new MM.Location(0, 0), { visible: false, offset: new MM.Point(0, 20) });
992      MM.Events.addHandler(_infoBox, 'mouseleave', onPinMouseOut);
993      MM.Events.addHandler(_infoBox, 'click', onInfoBoxClick);
994      _infoBox.setMap(_map);
995    }
996    var pinid = pin.id;
997    if (pinid && pinid != "1_0") {
998      // Set the initial visibilty hidden so it doesn't jump around
999      var bubbleHtml = getBubbleHtml(pin);
1000      var pinType = getPinType(pin);
1001      var desc = pinType === BingMap.PinType.Valuation
1002        ? '<div class="pushpinPopup" style="visibility: hidden;"><div class="pushpinPopupBody">' + bubbleHtml + '</div></div>'
1003        : '<div class="pushpinPopup valuationPopup" style="visibility: hidden;"><div class="pushpinPopupBody">' + bubbleHtml + '</div></div>';
1004      _infoBox.setLocation(pin.getLocation());
1005      _infoBox.setOptions({
1006        visible: true,
1007        showCloseButton: false,
1008        htmlContent: desc,
1009        zIndex: 100
1010      });
1011
1012      // After the bubble is shown, set it to the right div in a multi-listing bubble
1013      showProperBubbleDiv(pin);
1014
1015      // Raise an event to say the bubble is filled and displaying
1016      $(document).trigger("mapbubbledisplay", [pinid]);
1017    }
1018  }
1019
1020  // Parcel event handlers
1021
1022  function parcelHoverHandler(event) {
1023    if (!event) return;
1024
1025    window.clearTimeout(_parcelHover.pauseThread);
1026    if (event.eventName === 'mousemove') {
1027      _parcelHover.pauseThread = window.setTimeout(
1028        function () {
1029          if (Math.abs(event.getX() - _parcelHover.lastMouseLocation.x) > 1 ||
1030            Math.abs(event.getY() - _parcelHover.lastMouseLocation.y) > 1) {
1031            _parcelHover.lastMouseLocation.x = event.getX();
1032            _parcelHover.lastMouseLocation.y = event.getY();
1033            hoverParcel(event);
1034          }
1035        }, _parcelHover.mousePauseThreshold);
1036    }
1037  }
1038
1039  function hoverParcel(event) {
1040    var layer = null;
1041    var highlightLayerIndex = getLayerIndexById('Highlight');;
1042
1043    var checkHoveringClass = function (propertyId, add) {
1044      if (propertyId) {
1045        $('#pin-val_1_' + propertyId).find('.pin-valuation')[add ? 'addClass' : 'removeClass']('hovering');
1046      }
1047      _parcelHover.lastPropertyId = add ? propertyId : null;
1048    };
1049
1050    var clearParcelLayer = function () {
1051      removeDmpLayer(highlightLayerIndex, false);
1052      checkHoveringClass(_parcelHover.lastPropertyId, false);
1053    };
1054
1055    var processParcelResult = function (json) {
1056      if (!json || !json.Response || json.Response.Results.totalRecords <= 0) {
1057        clearParcelLayer();
1058        _parcelHover.lastLocation = null;
1059        return;
1060      }
1061
1062      var recordSet = json.Response.Results;
1063      var parcelRec = null;
1064      if (recordSet.Data && recordSet.Data.Row.length > 1) {
1065        parcelRec = recordSet.Data.Row[0];
1066      } else if (recordSet.Data) {
1067        parcelRec = recordSet.Data.Row;
1068      }
1069
1070      // Use the LOCID (LOCATION_ID) to identify the record that hovered.
1071      var keyField = "LOCID";
1072      var keyValue = parcelRec[keyField];
1073
1074      if (_parcelHover.lastLocation === keyValue) {
1075        // Same location so no need to re-draw.
1076        return;
1077      }
1078      clearParcelLayer();
1079
1080      var parcelNumber = parcelRec["APN"];
1081      if (parcelNumber) {
1082        checkHoveringClass(_parcelPropertyIds[parcelNumber.replace(new RegExp('-', 'g'), '')], true);
1083      }
1084
1085
1086      // add the server side highlight layer to highlight the hovered parcel
1087      addDmpLayer(highlightLayerIndex, false, {
1088        showField: keyField,
1089        showValues: keyValue,
1090        antiAlias: true,
1091        ignoreHoles: true
1092      });
1093
1094      _parcelHover.lastLocation = keyValue;
1095    };
1096
1097    if (!event) return;
1098
1099    if (!highlightLayerIndex) return;
1100
1101    layer = options.dmpLayers[highlightLayerIndex];
1102    if (layer === undefined || layer === null ||
1103      layer.zoomRange.min > Math.round(_mapState.Zoom) ||
1104      layer.zoomRange.max < Math.round(_mapState.Zoom)) {
1105      clearParcelLayer();
1106      _parcelHover.lastLocation = null;
1107      return;
1108    }
1109
1110    // Querystring parameters:
1111    //   nested: 
1112    //      "true" will nest the related records with the base records. This makes the result easier to work with.
1113    //   showSchema: 
1114    //      "false" will tell the service to return the schema information. This can be turned to true during development 
1115    //      but should stay false in production because it will lessen the payload from the result giving better performance.
1116    //   fields: 
1117    //      the wild card will return all fields in the base resource (parcels); below we specify the fields
1118    //      that we want to return, e.g., "&fields=APN,FIPS,LOCID"
1119    var point = new MM.Point(event.getX(), event.getY());
1120    var pixelLocation = _map.tryPixelToLocation(point);
1121    if (!pixelLocation) return;
1122
1123    var url = 'getByGeometry.aspx?returnGeoType=1&dataSource=' + layer.resource +
1124      '&geo=POINT(' + pixelLocation.longitude + ' ' + pixelLocation.latitude + ')' +
1125      '&nested=true&showSchema=false&fields=APN,LOCID';
1126
1127    Dmp.Env.Connections['SS'].getJson(url, processParcelResult, function (err) {
1128      console.log('Error getting parcel hover data: ' + err);
1129    });
1130  }
1131
1132  function getLayerIndexById(layerId) {
1133    // Example layerId: "Highlight"
1134    var layerIndex = 0;
1135    if (layerId) {
1136      for (var idx = 0; idx < options.dmpLayers.length; idx++) {
1137        if (options.dmpLayers[idx].id === layerId) {
1138          layerIndex = idx;
1139          break;
1140        }
1141      }
1142    }
1143
1144    return layerIndex;
1145  }
1146
1147  // Pin event handlers
1148
1149  function onPinClick(e) {
1150    if (e.target && e.target.tag === 'ClusterPin') {
1151      clusterClicked(e.target);
1152    } else if (e.target && e.target.pinType && e.target.pinType === 'Valuation') {
1153      var propertyId = e.target.id.replace('val_1_', '');
1154      triggerValuationClick(propertyId);
1155    } else if (e.targetType === "pushpin" || e.targetType === 'HtmlPushpin') {
1156      showBubble(e.target);
1157      $(_mapContainer).trigger("map-pushpin-click", e);
1158    }
1159
1160    return true;
1161  }
1162
1163  function onPinDoubleClick(e) {
1164    if (e.target && e.target.tag && e.target.tag === 'ClusterPin') {
1165      // Zoom in, don't show the bubble
1166      clusterClicked(e.target);
1167    } else if (e.targetType === "pushpin") {
1168      showBubble(e.target);
1169      $(_mapContainer).trigger("map-pushpin-doubleclick", e);
1170    }
1171  }
1172
1173  function onPinMouseOver(e) {
1174    if (!e) return;
1175    var time = options.pinMouseOverDelay;
1176    if (e.target && e.target.tag && e.target.tag === 'ClusterPin') {
1177      e.target.setOptions(e.target.hoverOpts);
1178      time = 400;
1179    }
1180    if (e.targetType === "pushpin" || e.targetType === "HtmlPushpin") {
1181      _currentPinId = e.target.id;
1182      setTimeout(function () {
1183        if (_currentPinId === e.target.id) {
1184          showBubble(e.target);
1185          $(_mapContainer).trigger("map-pushpin-mouseover", e);
1186        }
1187      }, time);
1188    }
1189  }
1190
1191  function onPinMouseOut(e) {
1192    _currentPinId = 0;
1193    var time = options.pinMouseOutDelay;
1194    if (e.target && e.target.tag && e.target.tag === 'ClusterPin') {
1195      e.target.setOptions(e.target.pinOpts);
1196      time = 500;
1197    }
1198    _currentPinId = 0;
1199    setTimeout(function () {
1200      if ($("div.pushpinPopup:hover").length === 0 && $("div.pin:hover").length === 0) {
1201        _self.hideListingBubble();
1202      }
1203      $(_mapContainer).trigger("map-pushpin-mouseout", e);
1204    }, time);
1205  }
1206
1207  function onInfoBoxClick(e) {
1208    if (typeof (options.isSM3) !== 'undefined' && options.isSM3) {
1209      var propertyId = $(e.originalEvent.target).parents('.valuationPopup').first().data('property-id');
1210      if (propertyId) {
1211        triggerValuationClick(propertyId);
1212      }
1213    }
1214  }
1215
1216  function triggerValuationClick(propertyId) {
1217    if (propertyId && _publicRecords[propertyId]) {
1218      event.preventDefault();
1219      event.stopImmediatePropagation();
1220      $(document).trigger('map-valuation-click', {
1221        propertyId: propertyId,
1222        url: _publicRecords[propertyId].url
1223      });
1224    }
1225  }
1226
1227
1228  // Pin management functions
1229
1230  function gotoLocation(loc, addPin, zoom) {
1231    if (typeof zoom !== "number")
1232      zoom = 16; // Default zoom level
1233
1234    $.getJSON('https://dev.virtualearth.net/REST/v1/Locations?query=' + loc + '&key=' + Utils.BingMapKey + '&jsonp=?', function (data) {
1235      if (data && data.resourceSets.length && data.resourceSets[0].resources.length && data.resourceSets[0].resources[0].geocodePoints.length) {
1236        var lat = data.resourceSets[0].resources[0].geocodePoints[0].coordinates[0];
1237        var lon = data.resourceSets[0].resources[0].geocodePoints[0].coordinates[1];
1238        if (lat && lon) {
1239          _self.setCenterAndZoom(lat, lon, zoom);
1240          setTimeout(function () {
1241            if (addPin) {
1242              var center = _map.getCenter();
1243              _self.startAddingPins("Address");
1244              _self.addPin("Address", "1", center.latitude, center.longitude, BingMap.PinType.Address);
1245              _self.endAddingPins("Address");
1246            }
1247          }, 500);
1248        }
1249      }
1250    });
1251  }
1252
1253  function setMapViewBoundaries(pins) {
1254    if (!pins || !pins.length) {
1255      _mapMoveOrigin = null;
1256      return;
1257    }
1258    if (pins.length === 1) {
1259      var pin = pins[0];
1260      _self.setCenterAndZoom(pin.latitude, pin.longitude, 16);
1261    } else if (options.enableMyNeighborhoodPins) {
1262      var bound = MM.LocationRect.fromLocations(pins);
1263      _map.setView({ bounds: bound, padding: 100 }); // padding value can be set to width/height of pushpin 
1264    } else {
1265      var rect = MM.LocationRect.fromLocations(pins);
1266      var northLat = rect.getNorth(),
1267        southLat = rect.getSouth(),
1268        eastLon = rect.getEast(),
1269        westLon = rect.getWest();
1270      var centerLat = (northLat + southLat) / 2;
1271      var centerLon = (eastLon + westLon) / 2;
1272
1273      // Handle the situation where one of the listings has wrong lat/lon
1274      // Example, you search for Los Angeles and the lat/lon for one of the entries is in San Diego
1275      // To see if that happened, we must compare calculated boundaries (northLat,southLat,eastLat,westLat)
1276      // to the boundaries of Los Angeles. If calculated exceeds the boundary of Los Angeles 
1277      // by a significant margin, then reduce the box to Los Angeles boundary, then decrease the zoom by 1 
1278      // to pick up any stragglers that are just outside of Los Angeles.
1279      if (_boundaryShapeLayerCount == 1 && _boundaryShapeLayerName &&
1280        _shapes[_boundaryShapeLayerName] && _shapes[_boundaryShapeLayerName].length > 0) {
1281
1282        var index_NorthLat = 0, index_SouthLat = 2, index_EastLon = 1, index_WestLon = 3;
1283
1284        // set the defaults so that any legit coordinates will overwrite them
1285        var boundaryNorthLat = -1000, boundarySouthLat = 1000,
1286          boundaryEastLon = -1000, boundaryWestLon = 1000;
1287
1288        // note that there maybe multiple boundaries even in a single city
1289        // thus we loop through all the bounds in the shape and set the edges of the total boundary
1290        for (var i = 0; i < _shapes[_boundaryShapeLayerName].length; i++) {
1291          var currentBounds = _shapes[_boundaryShapeLayerName][i].geometry.bounds;
1292
1293          if (currentBounds[index_NorthLat] > boundaryNorthLat) boundaryNorthLat = currentBounds[index_NorthLat];
1294          if (currentBounds[index_SouthLat] < boundarySouthLat) boundarySouthLat = currentBounds[index_SouthLat];
1295          if (currentBounds[index_EastLon] > boundaryEastLon) boundaryEastLon = currentBounds[index_EastLon];
1296          if (currentBounds[index_WestLon] < boundaryWestLon) boundaryWestLon = currentBounds[index_WestLon];
1297        }
1298
1299        var pinsLatDistance = northLat - southLat;
1300        var boundaryLatDistance = boundaryNorthLat - boundarySouthLat;
1301
1302        var pinsLonDistance = eastLon - westLon;
1303        var boundaryLonDistance = boundaryEastLon - boundaryWestLon;
1304
1305        if (Math.abs(pinsLatDistance - boundaryLatDistance) > 0.5 ||
1306          Math.abs(pinsLonDistance - boundaryLonDistance) > 0.5) {
1307
1308          // The difference between pins and boundary is big enough 
1309          // that we need to constrain the pins and recalculate the center lat / lon
1310          northLat = boundaryNorthLat;
1311          southLat = boundarySouthLat;
1312          eastLon = boundaryEastLon;
1313          westLon = boundaryWestLon;
1314
1315          centerLat = (northLat + southLat) / 2;
1316          centerLon = (eastLon + westLon) / 2;
1317        }
1318      }
1319
1320      // BROKER-4675, Bing maps throws exception at zoom level 12, keep here until they fix
1321      var zoom = calculateZoom(northLat, southLat, eastLon, westLon);
1322      zoom = zoom === 12 ? 11 : zoom;
1323      _self.setCenterAndZoom(centerLat, centerLon, zoom);
1324    }
1325
1326    // Sometimes the set view doesn't trigger an onChangeView, let's make sure
1327    setTimeout(function () {
1328      if (_mapMoveOrigin === "SetMapView") {
1329        _self.onChangeView();
1330      }
1331    }, 1000);
1332  }
1333
1334  function getFullPinImagePath(pinInfo, defaultPath) {
1335    //if pins not search motif v3
1336    var imageFilename = typeof pinInfo.Image !== "undefined" ? pinInfo.Image : pinInfo.ImageOn;
1337    return imageFilename.indexOf("/") !== -1 ? imageFilename : defaultPath + "/" + imageFilename;
1338  }
1339
1340  function calculateZoom(maxLat, minLat, maxLon, minLon) {
1341    // Single point, zoom to 16
1342    if (maxLon === minLon && maxLat === minLat) {
1343      return 18;
1344    }
1345
1346    //Determine the best zoom level based on the map scale and bounding coordinate information
1347    //best zoom level based on map width
1348    var zoom1 = Math.round(Math.log(360.0 / 256.0 * (_mapW - 2) / (maxLon - minLon)) / Math.log(2));
1349    //best zoom level based on map height
1350    var zoom2 = Math.round(Math.log(180.0 / 256.0 * (_mapH - 2) / (maxLat - minLat)) / Math.log(2));
1351
1352    //use the most zoomed out of the two zoom levels
1353    return (zoom1 < zoom2) ? zoom1 : zoom2;
1354  }
1355
1356  function clusterClicked(pin) {
1357    // Get the center zoom level for this pin
1358    var centerLat = (pin.northLat + pin.southLat) / 2;
1359    var centerLon = (pin.eastLon + pin.westLon) / 2;
1360    var calculatedZoom = calculateZoom(pin.northLat, pin.southLat, pin.eastLon, pin.westLon);
1361
1362    // Let's make sure we are actually going to zoom in. In some cases, especially with 
1363    // a narrow map, this doesn't result in a new zoom
1364    var oldZoom = _map.getZoom();
1365    if (calculatedZoom <= oldZoom) calculatedZoom += 1;
1366
1367    _self.setCenterAndZoom(centerLat, centerLon, calculatedZoom);
1368  }
1369
1370  function displayPins(pinLayer) {
1371    if (isSupportedHtmlPushpin(pinLayer.name)) {
1372      if (_enableClusteringForPricePins) {
1373        displayPinsWithClusters(pinLayer);
1374      } else if (options.enableMyNeighborhoodPins) {
1375        displayMyNeighborhoodPins(pinLayer);
1376      } else if (pinLayer.name === 'Multi Listing') {
1377        // We have special handling for multi listing layer.
1378        displayPinsWithPriceCollection(pinLayer);
1379      } else {
1380        displayPinsWithPrice(pinLayer);
1381      }
1382
1383    } else {
1384      displayPinsNonPrice(pinLayer);
1385    }
1386  }
1387
1388  function displayPinsWithClusters(pinLayer) {
1389    var pins = pinLayer.pins;
1390
1391    pinLayer.layer.clear();
1392    pinLayer.pinIds = {};
1393
1394    for (var i = 0; i < pins.length; i++) {
1395      var pin = pins[i];
1396      // validate each cluster
1397      if (pin.pinsInCluster < 1) continue;
1398
1399      var location = new MM.Location(pin.latitude, pin.longitude);
1400      var clusterPinDimensions = getClusterPinRadius(pin, false);
1401      var clusterPinHoverDimensions = getClusterPinRadius(pin, true);
1402      var clusterPinOpts = {
1403        icon: createCustomClusteredPinSvg(clusterPinDimensions.radius,
1404          clusterPinDimensions.outlineWidth,
1405          pin.pinsInCluster),
1406        anchor: new MM.Point(clusterPinDimensions.radius, clusterPinDimensions.radius)
1407      };
1408      var clusterPinHoverOpts = {
1409        icon: createCustomClusteredPinSvg(clusterPinHoverDimensions.radius,
1410          clusterPinHoverDimensions.outlineWidth,
1411          pin.pinsInCluster),
1412        anchor: new MM.Point(clusterPinHoverDimensions.radius, clusterPinHoverDimensions.radius)
1413      };
1414      var clusterPin = new MM.Pushpin(location, clusterPinOpts);
1415
1416      clusterPin.id = pin.id;
1417      clusterPin.pinDesc = pin.pinDesc;
1418      clusterPin.pinType = pin.pinType;
1419
1420      // set a tag on this object so that it can be identified
1421      // in onPinMouseOver and onPinClick methods
1422      clusterPin.tag = 'ClusterPin';
1423      clusterPin.northLat = pin.northLat;
1424      clusterPin.southLat = pin.southLat;
1425      clusterPin.westLon = pin.westLon;
1426      clusterPin.eastLon = pin.eastLon;
1427      clusterPin.pinsInCluster = pin.pinsInCluster;
1428      clusterPin.avgPrice = pin.avgPrice;
1429      clusterPin.minPrice = pin.minPrice;
1430      clusterPin.maxPrice = pin.maxPrice;
1431      clusterPin.pinOpts = clusterPinOpts;
1432      clusterPin.hoverOpts = clusterPinHoverOpts;
1433
1434      // setup events for the pushpin 
1435      MM.Events.addHandler(clusterPin, 'mouseover', onPinMouseOver);
1436      MM.Events.addHandler(clusterPin, 'mouseout', onPinMouseOut);
1437      MM.Events.addHandler(clusterPin, 'click', onPinClick);
1438      MM.Events.addHandler(clusterPin, 'dblclick', onPinDoubleClick);
1439      pinLayer.layer.add(clusterPin);
1440      pinLayer.pinIds[pin.id] = pin;
1441    }
1442    //Ensure we are displaying the pinlayer layer that was set to visible false in UpdatePOI when Zoom level<12
1443    if (pinLayer.isVisible && pinLayer.layer) {
1444      pinLayer.layer.setVisible(true);
1445    }
1446    _map.layers.insert(pinLayer.layer);
1447  }
1448
1449  function getClusterPinRadius(clusterPin, hoverFlag) {
1450    //Get the number of pushpins in the cluster
1451    var clusterSize = clusterPin.pinsInCluster;
1452
1453    // Define variables for minimum cluster radius, and how wide the outline area of the circle should be.
1454    var outlineWidth = hoverFlag ? 5 : 3;
1455    var radius = hoverFlag ? 14 : 10;
1456    if (clusterSize >= 10 && clusterSize <= 99) {
1457      radius = hoverFlag ? 18 : 14;
1458    } else if (clusterSize >= 100 && clusterSize <= 999) {
1459      radius = hoverFlag ? 22 : 18;
1460    } else if (clusterSize >= 1000 && clusterSize <= 9999) {
1461      radius = hoverFlag ? 30 : 26;
1462    } else if (clusterSize >= 10000) {
1463      radius = hoverFlag ? 38 : 34;
1464    }
1465
1466    return { radius: radius, outlineWidth: outlineWidth };
1467  }
1468
1469  function createCustomClusteredPinSvg(radius, outlineWidth, count) {
1470    //Create an SVG string of two circles, one on top of the other, with the specified radius and color.
1471    return [
1472      '<svg xmlns="http://www.w3.org/2000/svg" width="', (radius * 2), '" height="', (radius * 2), '">',
1473      '<style>.count { font: bold 0.6em sans-serif; }</style>',
1474      '<circle cx="', radius, '" cy="', radius, '" r="', radius, '" fill="', options.clusterPinColor, '" fill-opacity="0.4" />', // outer
1475      '<circle cx="', radius, '" cy="', radius, '" r="', radius - outlineWidth, '" fill="', options.clusterPinColor, '" />',
1476      '<text x="50%" y="50%" text-anchor="middle" dy="0.3em" fill="#ffffff" fill-opacity="0.9" class="count">
1476', count.toLocaleString(), '</text>', // inner
1477      '</svg>'
1478    ].join('');
1479  }
1480
1481  function displayPinsWithPrice(pinLayer) {
1482    var openHouseHtml = '<i class="listing-price-open-new-house">Open</i>';
1483    var newHouseHtml = '<i class="listing-price-open-new-house">New</i>';
1484    var customHtml = '<div class="listing-price {listing-price-no-favorite} {listing-price-home-for-sale-class} {isVisible}">{listing-price-open-new-houseHouseHtml}{homeValue}</div>';
1485    var pins = pinLayer.pins;
1486
1487    pinLayer.layer.clear();
1488    pinLayer.pinIds = {};
1489
1490    var layerCharacteristic = getLayerCharacteristics(pinLayer);
1491
1492    for (var i = 0; i < pins.length; i++) {
1493      var pin = pins[i];
1494      var location = new MM.Location(pin.latitude, pin.longitude);
1495      if (!pin.isVisible)
1496        continue;
1497      var constructedHtml = customHtml.replace('{homeValue}', pin.pinPrice)
1498        .replace('{listing-price-home-for-sale-class}', layerCharacteristic.homeSaleClass)
1499        .replace('{listing-price-open-new-houseHouseHtml}', (layerCharacteristic.isOpenHouse ? openHouseHtml : (layerCharacteristic.isNewHouse ? newHouseHtml : '')))
1500        .replace('{listing-price-no-favorite}', (pin.pinIsFavorite ? '' : 'listing-price-no-favorite'));
1501
1502      var offset = _zoomLevelOffsets[_map.getZoom()];
1503      var htmlPin = new HtmlPushpin(location, constructedHtml, { anchor: new MM.Point(offset.x, offset.y) });
1504
1505      htmlPin.id = pin.id;
1506      htmlPin.pinDesc = pin.pinDesc;
1507      htmlPin.pinType = pin.pinType;
1508
1509      pinLayer.layer.add(htmlPin);
1510      pinLayer.pinIds[pin.id] = pin;
1511
1512      // setup events for the pushpin 
1513      MM.Events.addHandler(htmlPin, 'mouseover', function htmlPinMouseOver(event) {
1514        // bring pin to forefront when mouse is over them
1515        // it helps when there are too many pins on the map
1516        event.target._element.style.zIndex = 1000;
1517        onPinMouseOver(event);
1518      });
1519
1520      MM.Events.addHandler(htmlPin, 'mouseout', function htmlPinMouseOut(event) {
1521        event.target._element.style.zIndex = '';
1522        onPinMouseOut(event);
1523      });
1524
1525      MM.Events.addHandler(htmlPin, 'click', onPinClick);
1526      MM.Events.addHandler(htmlPin, 'dblclick', onPinDoubleClick);
1527    }
1528
1529    _map.layers.insert(pinLayer.layer);
1530  }
1531
1532  function displayPinsWithPriceCollection(pinLayer) {
1533    var openHouseHtml = '<i class="listing-price-open-new-house">Open</i>';
1534    var newHouseHtml = '<i class="listing-price-open-new-house">New</i>';
1535    var customHtml = '<div class="listing-price {listing-price-no-favorite} {listing-price-home-for-sale-class}">{listing-price-open-new-houseHouseHtml}{homeValue}</div>';
1536
1537    var pins = pinLayer.pins;
1538    pinLayer.layer.clear();
1539    pinLayer.pinIds = {};
1540
1541    for (var i = 0; i < pins.length; i++) {
1542      var pin = pins[i];
1543      var count = pin.id.split('-').length;
1544      var location = new MM.Location(pin.latitude, pin.longitude);
1545
1546      var constructedHtml = createCustomClusteredPinSvg(15, 5, count);
1547
1548      var offset = _zoomLevelOffsets[_map.getZoom()];
1549      var htmlPin = new HtmlPushpin(location, constructedHtml, { anchor: new MM.Point(offset.x, offset.y) });
1550
1551      htmlPin.id = pin.id;
1552      htmlPin.pinDesc = pin.pinDesc;
1553      htmlPin.pinType = pin.pinType;
1554
1555      pinLayer.layer.add(htmlPin);
1556      pinLayer.pinIds[pin.id] = pin;
1557
1558      // setup events for the pushpin 
1559      MM.Events.addHandler(htmlPin, 'mouseover', function htmlPinMouseOver(event) {
1560        // bring pin to forefront when mouse is over them
1561        // it helps when there are too many pins on the map
1562        event.target._element.style.zIndex = 1000;
1563        onPinMouseOver(event);
1564      });
1565
1566      MM.Events.addHandler(htmlPin, 'mouseout', function htmlPinMouseOut(event) {
1567        event.target._element.style.zIndex = '';
1568        onPinMouseOut(event);
1569      });
1570
1571      MM.Events.addHandler(htmlPin, 'click', onPinClick);
1572      MM.Events.addHandler(htmlPin, 'dblclick', onPinDoubleClick);
1573    }
1574
1575    _map.layers.insert(pinLayer.layer);
1576  }
1577
1578  function getImagePricePinHtml(pinInfo) {
1579
1580    var template = [
1581      '<div class="mapPin">',
1582      '<div class="price">' + (pinInfo.pinPrice ? "$" + pinInfo.pinPrice.toString().replace(/(\d)(?=(\d{3})+$)/g, "$1,") : "$0") + '</div>',
1583      '<div class="img" style="background:transparent url(' + pinInfo.pinImageUrl + ') no-repeat center;"></div>',
1584      '</div>'
1585    ];
1586
1587    return template.join('\n');
1588  }
1589
1590  function displayMyNeighborhoodPins(pinLayer) {
1591
1592    var pins = pinLayer.pins;
1593
1594    pinLayer.layer.clear();
1595    pinLayer.pinIds = {};
1596
1597    $.each(pins, function (index, pin) {
1598      var location = new MM.Location(pin.latitude, pin.longitude);
1599
1600      var pinHtml = getImagePricePinHtml(pin);
1601
1602      var offset = _zoomLevelOffsets[_map.getZoom()];
1603      var htmlPin = new HtmlPushpin(location, pinHtml, { anchor: new MM.Point(offset.x, offset.y) });
1604
1605      htmlPin.id = pin.id;
1606      htmlPin.pinDesc = pin.pinDesc;
1607      htmlPin.pinType = pin.pinType;
1608
1609      pinLayer.layer.add(htmlPin);
1610      pinLayer.pinIds[pin.id] = pin;
1611
1612      // setup events for the pushpin 
1613      MM.Events.addHandler(htmlPin, 'mouseover', function htmlPinMouseOver(event) {
1614        // bring pin to forefront when mouse is over them
1615        // it helps when there are too many pins on the map
1616        event.target._element.style.zIndex = 1000;
1617        onPinMouseOver(event);
1618      });
1619
1620      MM.Events.addHandler(htmlPin, 'mouseout', function htmlPinMouseOut(event) {
1621        event.target._element.style.zIndex = '';
1622        onPinMouseOut(event);
1623      });
1624
1625      MM.Events.addHandler(htmlPin, 'click', onPinClick);
1626      MM.Events.addHandler(htmlPin, 'dblclick', onPinDoubleClick);
1627    });
1628
1629    _map.layers.insert(pinLayer.layer);
1630  }
1631
1632  function displayPinsNonPrice(pinLayer) {
1633    var pins = pinLayer.pins;
1634    pinLayer.layer.clear();
1635    pinLayer.pinIds = {};
1636
1637    var sm3ListingPinTypes = [
1638      BingMap.PinType.ListingActive,
1639      BingMap.PinType.ListingNew,
1640      BingMap.PinType.ListingOpenHouse,
1641      BingMap.PinType.ListingPending,
1642      BingMap.PinType.ListingSold];
1643
1644    for (var i = 0; i < pins.length; i++) {
1645      var pin = pins[i];
1646      var clusterCount = 1;
1647      if (typeof pin.id === "string") {
1648        clusterCount = pin.id.split("-").length;
1649      }
1650      var pinLatLong = new MM.Location(pin.latitude, pin.longitude);
1651      var text = "";
1652      var textOffset = new MM.Point(0, 5);
1653      var iconUrl = "";
1654      var color = null;
1655
1656      //Show Office pins on Map view based on RecoSupportsFlag (only for SM3)
1657      if (pin.pinType === BingMap.PinType.Office && !options.showOfficePinsOnMap)
1658        continue;
1659
1660      //Allow ClusterCount get populated for all the listing statuses ) {
1661      text = clusterCount > 1 ? clusterCount.toString() : ""; //can't pass a number to the text field
1662
1663      if ((sm3ListingPinTypes.indexOf(pin.pinType) > -1)) {
1664        /**
1665         * The following function `getOverrideIconUrlForSM3ListingPinTypes` is used to override or customize
1666         * the listing pin icons on the map for specific RECo (Real Estate Company) on Listing Detail Page (LDP). 
1667         * 
1668         * If the system is unable to locate this function or if the function does not return icon URL, 
1669         * the system will fall back to the default implementation by using the `getDefaultIconUrlForSM3ListingPinTypes` function.
1670         * 
1671         * Any RECo can override this functionality by defining the function in the Layout Property Detail - Top Summary 
1672         * or any other Property Detail's Layout to customize the pins for Listing Detail Page (LDP). 
1673         * To use custom images, place the new icon files under the path `/images/recos/{recoid}/icons/`.
1674         * 
1675         * Note: Currently, these pins are only being used for the LDP. The Search Result Page (SRP) uses new price pins,
1676         * so overriding is not necessary there. However, the same technique can be applied to the SRP if customization is required in the future.
1677         */
1678
1679        if (typeof getOverrideIconUrlForSM3ListingPinTypes === "function")
1680          iconUrl = getOverrideIconUrlForSM3ListingPinTypes(pin.pinType, clusterCount > 1);
1681
1682        // Fallback to the default implementation if no URL is returned or the function does not provide a valid string.
1683        if (!iconUrl || typeof iconUrl !== 'string')
1684          iconUrl = getDefaultIconUrlForSM3ListingPinTypes(pin.pinType, clusterCount > 1);
1685
1686      }
1687      else if (pin.pinType === BingMap.PinType.Valuation) {
1688        text = pin.pinTitle;
1689        iconUrl = getFullPinImagePath(BingMap.PinInfo[pin.pinType], "/images/common/map/bingmapv8/");
1690      }
1691      else {
1692        var pinType = pin.pinType;
1693        if (clusterCount > 1 && typeof pin.multipleType !== "undefined") {
1694          pinType = pin.multipleType;
1695        }
1696        if (clusterCount === 1) {
1697          //Should not set the text when the description is like a HTML Markup
1698          if (!isNaN(pin.pinDesc)) {
1699            text = pin.pinDesc;
1700          }
1701          textOffset = new MM.Point(0, 12);
1702        }
1703
1704        if (typeof pinType !== "undefined" && pinType !== "") {
1705          iconUrl = getFullPinImagePath(BingMap.PinInfo[pinType], "/images/common/map/bingmapv7/");
1706        }
1707      }
1708
1709      var pinOpts = {
1710        text: text,
1711        textOffset: textOffset,
1712        id: "pin-" + pin.id,
1713        icon: iconUrl
1714      };
1715
1716      if (color !== null) {
1717        pinOpts.color = color;
1718      }
1719
1720      var pushPin = new MM.Pushpin(pinLatLong, pinOpts);
1721      pushPin.id = pin.id;
1722      pushPin.pinDesc = pin.pinDesc;
1723      pushPin.pinType = pinLayer.name;
1724      MM.Events.addHandler(pushPin, "mouseover", onPinMouseOver);
1725      MM.Events.addHandler(pushPin, "mouseout", onPinMouseOut);
1726      MM.Events.addHandler(pushPin, "click", onPinClick);
1727      MM.Events.addHandler(pushPin, "dblclick", onPinDoubleClick);
1728
1729      pinLayer.layer.add(pushPin);
1730      pinLayer.pinIds[pin.id] = pin;
1731    }
1732    //Ensure we are displaying the pinlayer layer that was set to visible false in UpdatePOI when Zoom level<12
1733    if (pinLayer.isVisible && pinLayer.layer) {
1734      pinLayer.layer.setVisible(true);
1735    }
1736    _map.layers.insert(pinLayer.layer);
1737
1738    return;
1739  }
1740
1741  function getDefaultIconUrlForSM3ListingPinTypes(pinType, isCluster) {
1742
1743    if (isCluster)
1744      return "/images/common/map/images/icons/search-motif-v3-clustered.png";
1745
1746    //Green dot image for Active,New and Openhouse listings
1747    if (pinType === BingMap.PinType.ListingActive || pinType === BingMap.PinType.ListingNew || pinType === BingMap.PinType.ListingOpenHouse)
1748      return "/images/common/map/images/icons/search-motif-v3-listing.png";
1749
1750    //Yellow dot image for Pending,Contingent and Back up offers
1751    if (pinType === BingMap.PinType.ListingPending)
1752      return "/images/common/map/images/icons/search-motif-v3-listing-pending.png";
1753
1754    //Red dot image for anything that denotes the property is no longer active nor in pending / contingent status
1755    return "/images/common/map/images/icons/search-motif-v3-listing-sold.png";
1756  }
1757  function getLayerCharacteristics(pinLayer) {
1758    // populate dictionary on first use    
1759    if (Object.keys(_layerCharacteristics).length === 0) {
1760      var colorSold = '#C81845',
1761        colorSale = '#009934',
1762        colorPending = '#FBA902',
1763        colorComingSoon = '#026AA2',
1764        colorAuction = '#ba24d5',
1765        colorRent = '#7C3FDE';
1766
1767      _layerCharacteristics['Property'] = {
1768        isOpenHouse: false,
1769        isNewHouse: false,
1770        homeSaleClass: 'listing-price-home-for-sale',
1771        color: colorSale
1772      };
1773      _layerCharacteristics['Open House Available'] = {
1774        isOpenHouse: true,
1775        isNewHouse: false,
1776        homeSaleClass: 'listing-price-home-for-sale',
1777        color: colorSale
1778      };
1779      _layerCharacteristics['Sale Pending'] = {
1780        isOpenHouse: false,
1781        isNewHouse: false,
1782        homeSaleClass: 'listing-price-home-sale-pending',
1783        color: colorPending
1784      };
1785      _layerCharacteristics['Sold Property'] = {
1786        isOpenHouse: false,
1787        isNewHouse: false,
1788        homeSaleClass: 'listing-price-home-sold',
1789        color: colorSold
1790        };
1791        _layerCharacteristics['Rent'] = {
1792            isOpenHouse: false,
1793            isNewHouse: false,
1794            homeSaleClass: 'listing-price-home-rent',
1795            color: colorRent
1796        };
1797      _layerCharacteristics['New Property'] = {
1798        isOpenHouse: false,
1799        isNewHouse: true,
1800        homeSaleClass: 'listing-price-home-for-sale',
1801        color: colorSale
1802      };
1803      _layerCharacteristics['Commercial Property'] = {
1804        isOpenHouse: false,
1805        isNewHouse: false,
1806        homeSaleClass: 'listing-price-home-for-sale',
1807        color: colorSale
1808      };
1809      _layerCharacteristics['Commercial Sale Pending'] = {
1810        isOpenHouse: false,
1811        isNewHouse: false,
1812        homeSaleClass: 'listing-price-home-sale-pending',
1813        color: colorPending
1814      };
1815      _layerCharacteristics['Sold Commercial'] = {
1816        isOpenHouse: false,
1817        isNewHouse: false,
1818        homeSaleClass: 'listing-price-home-sold',
1819        color: colorSold
1820      };
1821      _layerCharacteristics['Coming Soon'] = {
1822        isOpenHouse: false,
1823        isNewHouse: false,
1824        homeSaleClass: 'listing-price-home-coming-soon',        
1825        color: colorComingSoon
1826      };
1827      _layerCharacteristics['Auction'] = {
1828        isOpenHouse: false,
1829        isNewHouse: false,
1830        homeSaleClass: 'listing-price-home-auction',
1831        color: colorAuction
1832      };
1833    }
1834
1835    var characteristic = _layerCharacteristics[pinLayer.name];
1836    if (characteristic === undefined) {
1837      // return default
1838      characteristic = _layerCharacteristics['Property'];
1839      console.warn('Unknown layer name encountered: ' + pinLayer.name);
1840    }
1841
1842    return characteristic;
1843  }
1844
1845  function meanAverageGridCluster(pins) {
1846    var i, j, len;
1847    var gridCells;
1848    var gridSize = 35;
1849    var shapeCount = pins.length;
1850    if (shapeCount === 0) {
1851      return [];
1852    }
1853    _mapW = $(_mapContainer).outerWidth();
1854    _mapH = $(_mapContainer).outerHeight();
1855
1856    // Break the map up into a grid
1857    var numXCells = parseInt(Math.ceil(_mapW / gridSize), 10);
1858    var numYCells = parseInt(Math.ceil(_mapH / gridSize), 10);
1859
1860    //create an array to store all the grid data.
1861    gridCells = new Array(numXCells * numYCells);
1862    //Iterate through the shapes in the base layer
1863    for (i = 0; i < shapeCount; i++) {
1864      //convert the shapes latlong to a pixel location
1865      var shape = pins[i];
1866      var latLong = new MM.Location(shape.latitude, shape.longitude);
1867      var pixel = _map.tryLocationToPixel(latLong, MM.PixelReference.control);
1868      var xPixel = pixel.x;
1869      var yPixel = pixel.y;
1870
1871      //check to see if the shape is within the bounds of the viewable map
1872      if (_mapW >= xPixel && _mapH >= yPixel && xPixel >= 0 && yPixel >= 0) {
1873        //calculate the grid position on the map of where the shape is located
1874        var a = Math.floor(xPixel / gridSize);
1875        var b = Math.floor(yPixel / gridSize);
1876        //calculates the grid location in the array
1877        var key = a + b * numXCells;
1878
1879        var spec = gridCells[key];
1880        if (spec == null) {
1881          spec = {};
1882          spec.Pins = [];
1883          gridCells[key] = spec;
1884        }
1885        spec.Pins.push(shape);
1886      }
1887    }
1888
1889    //Iterate through the clustered data in the grid array
1890    var cluster = [];
1891    for (i = 0, len = gridCells.length; i < len; i++) {
1892      var cell = gridCells[i];
1893      //calculate mean clustered coordinate
1894      if (cell != null && cell.Pins.length > 0) {
1895        var size = cell.Pins.length;
1896        var latSum = 0;
1897        var lonSum = 0;
1898        for (j = 0; j < size; j++) {
1899          var pin = cell.Pins[j];
1900          latSum += pin.latitude;
1901          lonSum += pin.longitude;
1902        }
1903        cell.LatLong = new MM.Location(latSum / size, lonSum / size);
1904        cluster.push(cell);
1905      }
1906    }
1907
1908    return cluster;
1909  }
1910
1911  function sortShapes(a, b, shapesSortOrder) {
1912    var swapOrder = 0;
1913    var index = 0;
1914    while (!swapOrder && index <= shapesSortOrder.length) {
1915      swapOrder = (a ^ shapesSortOrder[index]) ? (b ^ shapesSortOrder[index]) ? 0 : 1 : -1;
1916      index++;
1917    }
1918    return swapOrder;
1919  }
1920
1921  function getPinsFromClusterSpecs(clusters) {
1922    var clusterPins = [];
1923    // sort the shapes by order of PinType as follows:
1924    var pinsSortOrder = [
1925      BingMap.PinType.ListingActive,
1926      BingMap.PinType.ListingSold,
1927      BingMap.PinType.ListingPending,
1928      BingMap.PinType.ListingNew,
1929      BingMap.PinType.ListingOpenHouse,
1930      BingMap.PinType.ListingCommercialActive,
1931      BingMap.PinType.ListingCommercialSold,
1932      BingMap.PinType.ListingCommercialPending
1933    ];
1934    for (var i = 0, clusterCount = clusters.length; i < clusterCount; ++i) {
1935      var clusterSpec = clusters[i];
1936      var pins = clusterSpec.Pins;
1937      pins.sort(function (a, b) { return sortShapes(a.pinType, b.pinType, pinsSortOrder); });
1938      var clusterPinType = pins[0].pinType;
1939      var pinInfo = BingMap.PinInfo[clusterPinType];
1940      var pinClass;
1941      if (pinInfo) {
1942        if (pins.length === 1) {
1943          pinClass = pinInfo.PinClass;
1944        } else {
1945          if (pinInfo.PinType === BingMap.PinType.ListingSold) {
1946            pinClass = BingMap.PinInfo[BingMap.PinType.ListingSoldMultiple].PinClass;
1947          } else if (pinInfo.PinType === BingMap.PinType.ListingOpenHouse) {
1948            pinClass = BingMap.PinInfo[BingMap.PinType.ListingOpenHouseMultiple].PinClass;
1949          } else if (pinInfo.PinType === BingMap.PinType.ListingPending) {
1950            pinClass = BingMap.PinInfo[BingMap.PinType.ListingPendingMultiple].PinClass;
1951          } else if (pinInfo.PinType === BingMap.PinType.ListingActive || pinInfo.PinType === BingMap.PinType.ListingNew) {
1952            pinClass = BingMap.PinInfo[BingMap.PinType.ListingMultiple].PinClass;
1953          } else if (pinInfo.PinType === BingMap.PinType.ListingCommercialActive || pinInfo.PinType === BingMap.PinType.ListingCommercialNew) {
1954            pinClass = BingMap.PinInfo[BingMap.PinType.ListingCommercialMultiple].PinClass;
1955          } else if (pinInfo.PinType === BingMap.PinType.ListingCommercialPending) {
1956            pinClass = BingMap.PinInfo[BingMap.PinType.ListingCommercialPendingMultiple].PinClass;
1957          } else if (pinInfo.PinType === BingMap.PinType.ListingCommercialSold) {
1958            pinClass = BingMap.PinInfo[BingMap.PinType.ListingCommercialSoldMultiple].PinClass;
1959          } else {
1960            pinClass = pinInfo.PinClass;
1961          }
1962        }
1963      } else {
1964        pinClass = BingMap.PinInfo[BingMap.PinType.ListingActive].PinClass;
1965      }
1966      var pinIds = new StringBuilder();
1967      for (var j = 0, shapeCountj = pins.length; j < shapeCountj; j++) {
1968        if (j > 0) {
1969          pinIds.append("-");
1970        }
1971        pinIds.append(pins[j].id);
1972      }
1973      var clusterPin = {};
1974      clusterPin.id = pinIds.toString();
1975      clusterPin.latitude = clusterSpec.LatLong.latitude;
1976      clusterPin.longitude = clusterSpec.LatLong.longitude;
1977      clusterPin.pinType = pinInfo.PinType;
1978      clusterPin.pinClass = pinClass;
1979      clusterPins.push(clusterPin);
1980    }
1981    return clusterPins;
1982  }
1983
1984  function getClusteredPins(pins) {
1985    var clusterSpecs = meanAverageGridCluster(pins);
1986    return getPinsFromClusterSpecs(clusterSpecs);
1987  }
1988
1989  function clusterPinLayer(pinLayer) {
1990    pinLayer.pins = getClusteredPins(pinLayer.pins);
1991    _map.layers.remove(pinLayer.layer);
1992    displayPins(pinLayer);
1993  }
1994
1995  // Draw rectangle functions
1996
1997  function drawRectangle(currentMouseLoc, startingLoc, polyName) {
1998    var north = Math.max(currentMouseLoc.latitude, startingLoc.latitude);
1999    var south = Math.min(currentMouseLoc.latitude, startingLoc.latitude);
2000    var east = Math.max(currentMouseLoc.longitude, startingLoc.longitude);
2001    var west = Math.min(currentMouseLoc.longitude, startingLoc.longitude);
2002    var points = [
2003      new MM.Location(north, west),
2004      new MM.Location(north, east),
2005      new MM.Location(south, east),
2006      new MM.Location(south, west),
2007      new MM.Location(north, west)
2008    ];
2009    var shape = new MM.Polygon(points, { fillColor: options.shapeFillColor, strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness });
2010    shape.name = polyName;
2011    shape.isCustom = true;
2012    shape.start = startingLoc;
2013    var layer = getLayer(polyName, false, LayerType.Shape, true);
2014    addShapesToLayer([shape], layer);
2015  }
2016
2017  // Draw polygon functions
2018
2019  /*
2020  The following functions are from simplify-js, copyright notice here:
2021
2022  Copyright (c) 2015, Vladimir Agafonkin
2023  All rights reserved.
2024
2025  Redistribution and use in source and binary forms, with or without modification, are
2026  permitted provided that the following conditions are met:
2027
2028  1. Redistributions of source code must retain the above copyright notice, this list of
2029  conditions and the following disclaimer.
2030
2031  2. Redistributions in binary form must reproduce the above copyright notice, this list
2032  of conditions and the following disclaimer in the documentation and/or other materials
2033  provided with the distribution.
2034
2035  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
2036  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
2037  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
2038  COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
2039  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
2040  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
2041  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
2042  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2043  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2044  */
2045
2046  // square distance between 2 points
2047  function getSqDist(p1, p2) {
2048
2049    var dx = p1.longitude - p2.longitude,
2050      dy = p1.latitude - p2.latitude;
2051
2052    return dx * dx + dy * dy;
2053  }
2054
2055  // square distance from a point to a segment
2056  function getSqSegDist(p, p1, p2) {
2057
2058    var x = p1.longitude,
2059      y = p1.latitude,
2060      dx = p2.longitude - x,
2061      dy = p2.latitude - y;
2062
2063    if (dx !== 0 || dy !== 0) {
2064
2065      var t = ((p.longitude - x) * dx + (p.latitude - y) * dy) / (dx * dx + dy * dy);
2066
2067      if (t > 1) {
2068        x = p2.longitude;
2069        y = p2.latitude;
2070
2071      } else if (t > 0) {
2072        x += dx * t;
2073        y += dy * t;
2074      }
2075    }
2076
2077    dx = p.longitude - x;
2078    dy = p.latitude - y;
2079
2080    return dx * dx + dy * dy;
2081  }
2082  // rest of the code doesn't care about point format
2083
2084  // basic distance-based simplification
2085  function simplifyRadialDist(points, sqTolerance) {
2086
2087    var prevPoint = points[0],
2088      newPoints = [prevPoint],
2089      point;
2090
2091    for (var i = 1, len = points.length; i < len; i++) {
2092      point = points[i];
2093
2094      if (getSqDist(point, prevPoint) > sqTolerance) {
2095        newPoints.push(point);
2096        prevPoint = point;
2097      }
2098    }
2099
2100    if (prevPoint !== point) newPoints.push(point);
2101
2102    return newPoints;
2103  }
2104
2105  function simplifyDPStep(points, first, last, sqTolerance, simplified) {
2106    var maxSqDist = sqTolerance,
2107      index;
2108
2109    for (var i = first + 1; i < last; i++) {
2110      var sqDist = getSqSegDist(points[i], points[first], points[last]);
2111
2112      if (sqDist > maxSqDist) {
2113        index = i;
2114        maxSqDist = sqDist;
2115      }
2116    }
2117
2118    if (maxSqDist > sqTolerance) {
2119      if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
2120      simplified.push(points[index]);
2121      if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
2122    }
2123  }
2124
2125  // simplification using Ramer-Douglas-Peucker algorithm
2126  function simplifyDouglasPeucker(points, sqTolerance) {
2127    var last = points.length - 1;
2128
2129    var simplified = [points[0]];
2130    simplifyDPStep(points, 0, last, sqTolerance, simplified);
2131    simplified.push(points[last]);
2132
2133    return simplified;
2134  }
2135
2136  // both algorithms combined for awesome performance
2137  function simplify(points, tolerance, highestQuality) {
2138
2139    if (points.length <= 4) return points;
2140
2141    var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;
2142
2143    points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
2144    points = simplifyDouglasPeucker(points, sqTolerance);
2145
2146    return points;
2147  }
2148  /* End copied from simplify-js */
2149
2150  function getToleranceByZoomLevel() {
2151    var zoom = _map.getZoom();
2152    switch (zoom) {
2153      case 24:
2154      case 23:
2155      case 22:
2156      case 21:
2157      case 20:
2158      case 19:
2159        return 0.000001;
2160      case 18:
2161        return 0.000003;
2162      case 17:
2163        return 0.000006;
2164      case 16:
2165        return 0.00001;
2166      case 15:
2167        return 0.000025;
2168      case 14:
2169        return 0.00005;
2170      case 13:
2171        return 0.0001;
2172      case 12:
2173        return 0.0002;
2174      case 11:
2175        return 0.0004;
2176      case 10:
2177        return 0.0008;
2178      case 9:
2179        return 0.0015;
2180      case 8:
2181        return 0.003;
2182      case 7:
2183        return 0.009;
2184      case 6:
2185        return 0.02;
2186      case 5:
2187        return 0.04;
2188      case 4:
2189        return 0.07;
2190      default:
2191        return 0.1;
2192    }
2193  }
2194
2195  function finishDrawing(shape) {
2196    var shapeLocations = shape[0].getLocations();
2197    var firstLocation = shapeLocations[0];
2198    var lastLocation = shapeLocations.slice(-1)[0];
2199    if (firstLocation.latitude !== lastLocation.latitude || firstLocation.longitude !== lastLocation.longitude) {
2200      shapeLocations.push(firstLocation);
2201    }
2202    if (shapeLocations.length < 4) {
2203      return null;
2204    }
2205    var tolerance = getToleranceByZoomLevel();
2206
2207    var simplifiedPoints = simplify(shapeLocations, tolerance);
2208    var polygon = new MM.Polygon(simplifiedPoints, { fillColor: options.shapeFillColor, strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness });
2209    polygon.name = _currentShapeName;
2210    polygon.isCustom = true;
2211    shape = [polygon];
2212    var layer = getLayer(_currentShapeName, false, LayerType.Shape, true);
2213    addShapesToLayer(shape, layer);
2214    return shape;
2215  }
2216
2217  function drawPolyLine(loc, polyName) {
2218    var shape = new MM.Polyline([loc], { strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness });
2219    shape.name = polyName;
2220    var layer = getLayer(polyName, false, LayerType.Shape, true);
2221    addShapesToLayer([shape], layer);
2222  }
2223
2224  // Draw radius functions
2225
2226  function distance(lat1, lon1, lat2, lon2, unit) {
2227    var radlat1 = Math.PI * lat1 / 180;
2228    var radlat2 = Math.PI * lat2 / 180;
2229    var theta = lon1 - lon2;
2230    var radtheta = Math.PI * theta / 180;
2231    var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
2232    dist = Math.acos(dist);
2233    dist = dist * 180 / Math.PI;
2234    dist = dist * 60 * 1.1515;
2235    if (unit === "K") { dist = dist * 1.609344 };
2236    if (unit === "N") { dist = dist * 0.8684 };
2237    return dist;
2238  }
2239
2240  function drawRadiusDistance(position, radius) {
2241    var point = _map.tryLocationToPixel(position, MM.PixelReference.control);
2242    $("div.map-radius-distance", _mapContainer)
2243      .text(Math.round(radius * 100) / 100 + " miles")
2244      .css({ top: point.y + 15, left: point.x + 15 })
2245      .show();
2246  }
2247
2248  function removeRadiusLine(polyName) {
2249    _self.clearPolygon("radiusline-" + polyName);
2250    $("div.map-radius-distance", _mapContainer).hide();
2251  }
2252
2253  function drawRadiusLine(loc1, loc2, polyName) {
2254    var shape = new MM.Polyline([loc1, loc2], { strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness, strokeDashArray: options.strokeDashArray });
2255    var layer = getLayer("radiusline-" + polyName, false, LayerType.Shape, true);
2256    addShapesToLayer([shape], layer);
2257  }
2258
2259  function drawRadiusShape(radius, latitude, longitude, polyName, cb) {
2260    var lat = (latitude * Math.PI) / 180;
2261    var lon = (longitude * Math.PI) / 180;
2262    var R = 3959; // earth's mean radius in km
2263    var d = parseFloat(radius) / R;
2264    var circlePoints = [];
2265
2266    for (var x = 0; x <= 360; x += 5) {
2267      var p2 = new MM.Location(0, 0);
2268      var brng = x * Math.PI / 180;
2269      p2.latitude = Math.asin(Math.sin(lat) * Math.cos(d) + Math.cos(lat) * Math.sin(d) * Math.cos(brng));
2270
2271      p2.longitude = ((lon + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(lat),
2272        Math.cos(d) - Math.sin(lat) * Math.sin(p2.latitude))) * 180) / Math.PI;
2273      p2.latitude = (p2.latitude * 180) / Math.PI;
2274      circlePoints.push(p2);
2275    }
2276    var shape = new MM.Polygon(circlePoints, { fillColor: options.shapeFillColor, strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness });
2277    shape.center = new MM.Location(latitude, longitude);
2278    shape.radius = radius;
2279    shape.isCustom = true;
2280    shape.name = polyName;
2281    var layer = getLayer(polyName, false, LayerType.Shape, true);
2282    addShapesToLayer([shape], layer, cb);
2283  }
2284
2285  // Functions for adding pre-defined shapes to map
2286
2287  function addShapesByPolyPoints(points, polyLayer, cb) {
2288    var ctr = 0;
2289    try {
2290      var locations = [];
2291      var pointsSplit = points.split(",");
2292      for (var i = 0, len = pointsSplit.length; i < len / 2; i++) {
2293        var latVal = parseFloat(pointsSplit[ctr]);
2294        var longVal = MM.Location.normalizeLongitude(parseFloat(pointsSplit[ctr + 1]));
2295
2296        locations.push(new MM.Location(latVal, longVal));
2297        ctr += 2;
2298      }
2299      var shapes = [];
2300      if (locations.length >= 4) {
2301        var shape = new MM.Polygon(locations, { fillColor: options.shapeFillColor, strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness });
2302        shape.name = polyLayer.name;
2303        shape.isCustom = true;
2304        addShapesToLayer([shape], polyLayer);
2305        shapes.push(shape);
2306      }
2307
2308      if (cb && typeof cb === "function") {
2309        cb(shapes);
2310      }
2311    } catch (err) {
2312      Utils.Logger.warn("Error adding polygon from points: " + err.message);
2313    }
2314  }
2315
2316  function addShapesByRadius(polyLayer, pointsRadius, cb) {
2317    try {
2318      var pointsStr = pointsRadius.split('~')[0];
2319      var radius = parseFloat(pointsRadius.split('~')[1]);
2320      var pointsSplit = pointsStr.split(',');
2321      var latVal = parseFloat(pointsSplit[0]);
2322      var longVal = MM.Location.normalizeLongitude(parseFloat(pointsSplit[1]));
2323      drawRadiusShape(radius, latVal, longVal, polyLayer.name, cb);
2324
2325    } catch (err) {
2326      Utils.Logger.warn("Error adding polygon from radius: " + err.message);
2327    }
2328  }
2329
2330  function addShapeByDriveTime(polyName, layer, qsParams, cb) {
2331    if (typeof qsParams == "undefined" || qsParams == null) {
2332      return;
2333    }
2334    //Clean up some parameter value
2335    var paramStr = qsParams.split(",");
2336
2337    // AGENT-1672 - Using range type to form boundary polygon
2338    var param = { address: polyName, time: paramStr[0], duration: paramStr[1], rangeType: paramStr[2] };
2339
2340    $.ajax({
2341      type: "GET",
2342      url: Utils.AppPath + "/Include/AJAX/mapsearch/GetDriveTimeBoundary.aspx",
2343      data: param,
2344      dataType: "json",
2345      error: function (msg) {
2346        console.log(msg);
2347      },
2348      success: function (data) {
2349        //data object contains two children, Boundary and CenterOfMap
2350        //Boundary is a list of poly points and Cetner of Map is a Point indicating the target of the search
2351        var boundary = data.Boundary;
2352        var centerOfSearch = data.CenterOfMap;
2353
2354        var shapes = [];
2355        try {
2356          var latlongs = [];
2357          var j = boundary.Points.length;
2358          // Weird loop structure for performance
2359          if (j > 0) {
2360            do {
2361              var point = boundary.Points[j - 1];
2362              // Retrieve the latitude and longitude values- normalize the longitude value
2363              var latVal = parseFloat(point[0]);
2364              var longVal = MM.Location.normalizeLongitude(parseFloat(point[1]));
2365              latlongs.push(new MM.Location(latVal, longVal));
2366            } while (--j);
2367          }
2368          var shape = new MM.Polygon(latlongs, { fillColor: options.shapeFillColor, strokeColor: options.shapeLineColor, strokeThickness: options.shapeLineThickness });
2369          shapes.push(shape);
2370        } catch (ex) {
2371          Utils.Logger.warn(ex.message);
2372        }
2373
2374        if (shapes.length > 0) {
2375          addShapesToLayer(shapes, layer, cb);
2376        }
2377        if (centerOfSearch != null) {
2378          _self.startAddingPins("target", false);
2379          _self.addPin("target", "pinTarget", centerOfSearch.X, centerOfSearch.Y, 40);
2380          _self.endAddingPins("target");
2381
2382          //Adding a boot strap pop over to the target
2383          var pinLayer = getLayer("target");
2384          var pins = pinLayer.pins;
2385          if (pins != null && pins.length > 0) {
2386            //there should only be one target
2387            var pin = pins[0];
2388            var pinTarget = $("#pin_" + pin.ID);
2389            $(pinTarget.parent()).addClass("bootstrap");
2390            pinTarget.popover({ content: polyName, placement: 'right', trigger: 'hover', title: 'Drive Time Search' });
2391          }
2392        }
2393        // The InrixDriveTimeSearch.js is a current listener to this event, It checked
2394        // if getting drive time boundary is return from the server to prevent user from
2395        // requesting another search before the current search is finished.
2396        $(_mapContainer).trigger("drivetime-shapedrawn");
2397
2398        if (typeof cb === "function") {
2399          cb(shapes);
2400        }
2401      }
2402    });
2403  }
2404
2405  function addShapesByBoundary(qsParams, layer, cb) {
2406    if (typeof qsParams === "undefined") { return; }
2407
2408    MM.loadModule('Microsoft.Maps.WellKnownText', function () {
2409      qsParams.wkt = "true";
2410
2411      $.ajax({
2412        type: "GET",
2413        url: Utils.AppPath + "/Include/AJAX/mapsearch/GetBoundary.aspx",
2414        data: qsParams,
2415        dataType: "json",
2416        success: function (wkt) {
2417          //Parse well known text string.
2418          var shapes;
2419
2420          if (!wkt || wkt.length <= 0 || typeof wkt !== "string") {
2421            shapes = [];
2422          } else {
2423            if (!wkt.toUpperCase().contains('POLYGON')) {
2424              // Is just a string of coordinates, "lat,long,lat,long"
2425              // Convert to WKT, MULTIPOLYGON(((long lat, long lat)))
2426
2427              var polygonString = 'MULTIPOLYGON(((';
2428              var coordArr = wkt.split(',');
2429
2430              for (var i = 0; i < coordArr.length; i += 2) {
2431                polygonString += coordArr[i + 1] + ' ' + coordArr[i] + ',';
2432              }
2433
2434              wkt = polygonString.substring(0, polygonString.length - 1) + ')))';
2435            }
2436
2437            shapes = MM.WellKnownText.read(wkt,
2438              {
2439                polygonOptions:
2440                {
2441                  fillColor: options.shapeFillColor,
2442                  strokeColor: options.shapeLineColor,
2443                  strokeThickness: options.shapeLineThickness
2444                }
2445              }
2446            );
2447          }
2448
2449          _boundaryShapeLayerName = layer.name;
2450          _boundaryShapeLayerCount++;
2451          addShapesToLayer(shapes, layer, cb);
2452        }
2453      });
2454    });
2455
2456  }
2457
2458  function addShapes(polyLayer, polyType, polyValue, cb) {
2459    if (!polyType || polyType === "points") {
2460      addShapesByPolyPoints(polyValue, polyLayer, cb);
2461    } else if (polyType === "drivetime") {
2462      addShapeByDriveTime(polyLayer.name, polyLayer, polyValue, cb);
2463    } else if (polyType === "radius") {
2464      addShapesByRadius(polyLayer, polyValue, cb);
2465    } else {
2466      var state = "";
2467      if (polyType === "city" || polyType === "county") {
2468        state = window.defaultState;
2469        if (polyValue.indexOf(",") > -1) { // includes state
2470          var locationState = polyValue.split(",");
2471          polyValue = locationState[0].trim();
2472          state = locationState[1].trim();
2473        }
2474      }
2475
2476      addShapesByBoundary({
2477        val: polyValue,
2478        type: polyType,
2479        state: state
2480      }, polyLayer, cb);
2481    }
2482  }
2483
2484  // Functions for responding to new custom shapes
2485
2486  function getPointsString(points) {
2487    var pointsStr = '';
2488    for (var i = 0; i < points.length; i++) {
2489      var point = points[i];
2490      pointsStr += point.latitude + ',' + point.longitude + ',';
2491    }
2492    if (pointsStr.length > 0) {
2493      pointsStr = pointsStr.substr(0, pointsStr.length - 1);
2494    }
2495    return pointsStr;
2496  }
2497
2498  function onCustomShapeAdded(shapes) {
2499    var shape = shapes[0];
2500    var pointsStr;
2501    if (shape.center) {
2502      pointsStr = getPointsString([shape.center]);
2503    } else {
2504      pointsStr = getPointsString(shape.getLocations());
2505    }
2506    // When a shape is added we trigger an event so that listeners can take action
2507    // MapSearch.js uses this to add a location to its criteria
2508    $(_mapContainer).trigger("shapedrawn", [shape.name, pointsStr, shape.radius]);
2509  }
2510
2511  // Map type functions
2512
2513  function getMapTypeId(mapView) {
2514    // These are some 'legacy' view strings from way back that are still in use
2515    if (mapView === "b" || mapView === "o" || mapView === "be") {
2516      return MM.MapTypeId.birdseye;
2517    } else if (mapView === "a") {
2518      return MM.MapTypeId.aerial;
2519    } else {
2520      return MM.MapTypeId.road;
2521    }
2522  }
2523
2524  // Drawing mouse event handlers
2525
2526  function drawMouseDown(e) {
2527    _map.setOptions({ disablePanning: true });
2528    _drawing = true;
2529    _enableMouseMoveTimer = window.setInterval(function () {
2530      _enableMouseMoveHandler = true;
2531    }, 50);
2532    var center = _map.tryPixelToLocation(new MM.Point(e.getX(), e.getY()));
2533    if (_currentDrawType === "radius") {
2534      drawRadiusShape(0.0, center.latitude, center.longitude, _currentShapeName);
2535    } else if (_currentDrawType === "rectangle") {
2536      drawRectangle(center, center, _currentShapeName);
2537    } else {
2538      drawPolyLine(center, _currentShapeName);
2539    }
2540    //e.handled = true;
2541  }
2542
2543  function drawMouseMove(e) {
2544    if (!_drawing || !_enableMouseMoveHandler) return;
2545    _enableMouseMoveHandler = false;
2546    var shape = _shapes[_currentShapeName][0];
2547    var currentMousePoint = new MM.Point(e.getX(), e.getY());
2548    var currentMouseLocation = _map.tryPixelToLocation(currentMousePoint);
2549    if (_currentDrawType === "radius") {
2550      var radius = distance(currentMouseLocation.latitude, currentMouseLocation.longitude, shape.center.latitude, shape.center.longitude);
2551      drawRadiusShape(radius, shape.center.latitude, shape.center.longitude, _currentShapeName);
2552      drawRadiusLine(currentMouseLocation, shape.center, _currentShapeName);
2553      drawRadiusDistance(currentMouseLocation, radius);
2554    } else if (_currentDrawType === "draw") {
2555      var shapeLocations = shape.getLocations();
2556      var lastLocation = shapeLocations.slice(-1)[0];
2557      if (!lastLocation || currentMouseLocation.latitude !== lastLocation.latitude || currentMouseLocation.longitude !== lastLocation.longitude) {
2558        shapeLocations.push(currentMouseLocation);
2559        shape.setLocations(shapeLocations);
2560        var layer = getLayer(_currentShapeName, false, LayerType.Shape, true);
2561        addShapesToLayer([shape], layer);
2562      }
2563    } else if (_currentDrawType === "rectangle") {
2564      drawRectangle(currentMouseLocation, shape.start, _currentShapeName);
2565    }
2566    //e.handled = true;
2567  }
2568
2569  function drawMouseUp(e) {
2570    _map.setOptions({ disablePanning: false });
2571    _drawing = false;
2572    clearInterval(_enableMouseMoveTimer);
2573    $(_map.getRootElement()).css("cursor", "default");
2574    _self.showAllPins(true);
2575    var shape = _shapes[_currentShapeName];
2576    if (shape) {
2577      if (_currentDrawType === "radius") {
2578        removeRadiusLine(_currentShapeName);
2579      }
2580      if (_currentDrawType === "draw") {
2581        // When we're drawing this is a polyline.  We need to close it
2582        // off and add it as a polygon
2583        shape = finishDrawing(shape);
2584      }
2585      // It might be null if it was an invalid shape
2586      if (shape != null) {
2587        onCustomShapeAdded(shape);
2588      }
2589    }
2590    _currentShapeName = null;
2591    _currentDrawType = null;
2592
2593    MM.Events.removeHandler(_drawMouseDownHandler);
2594    MM.Events.removeHandler(_drawMouseMoveHandler);
2595    MM.Events.removeHandler(_drawMouseUpHandler);
2596
2597    //if (e) {
2598    //    e.handled = true;
2599    //}
2600  }
2601
2602  function startDrawing(drawType) {
2603    _currentDrawType = drawType;
2604    _self.hideAllPins(false);
2605    $(_map.getRootElement()).css("cursor", "crosshair");
2606
2607    _drawMouseDownHandler = MM.Events.addHandler(_map, "mousedown", drawMouseDown);
2608    _drawMouseMoveHandler = MM.Events.addHandler(_map, "mousemove", drawMouseMove);
2609    _drawMouseUpHandler = MM.Events.addHandler(_map, "mouseup", drawMouseUp);
2610  }
2611
2612  // Poi functions
2613
2614  function updatePoi(removeExisting) {
2615    if (_mapState.Zoom >= 12) {
2616      var pinTypes = "";
2617      for (var i = 0; i < BingMap.PinInfo.length; i++) {
2618        var pinInfo = BingMap.PinInfo[i];
2619        if (pinInfo.PinClass && pinInfo.PinClass.indexOf("poi-") === 0 && pinInfo.Disabled !== true) {
2620          var pinLayer = getLayer(pinInfo.ShortName, false, LayerType.Poi);
2621          if (pinLayer.isVisible) {
2622            if (pinTypes.length > 0) {
2623              pinTypes += ",";
2624            }
2625            pinTypes += pinInfo.PinType;
2626          } else if (removeExisting) {
2627            _self.deleteAllPins(pinLayer.name);
2628          }
2629        }
2630      }
2631      if (pinTypes.length === 0) {
2632        if (!_poiEnabled) {
2633          $(_mapContainer).trigger({
2634            type: "poichange", poiEnabled: true
2635          });
2636          _poiEnabled = true;
2637        }
2638      } else {
2639        var url = Utils.AppPath + "/Include/AJAX/MapSearch/GetPOI.aspx" +
2640          "?neLat=" + _mapState.NELat +
2641          "&neLong=" + _mapState.NELon +
2642          "&swLat=" + _mapState.SWLat +
2643          "&swLong=" + _mapState.SWLon +
2644          "&types=" + pinTypes;
2645
2646        $.ajax({
2647          mode: "abort",
2648          port: "poi",
2649          type: "GET",
2650          url: url,
2651          cache: true,
2652          dataType: "json",
2653          success: function (data) {
2654            //add poi pins to the map
2655            for (var i = 0, len = data.length; i < len; i++) {
2656              var pinType = data[i].pinType;
2657              var layerName = BingMap.PinInfo[pinType].ShortName;
2658              _self.startAddingPins(layerName);
2659              for (var j = 0, poiLen = data[i].poi.length; j < poiLen; j++) {
2660                var poi = data[i].poi[j];
2661                _self.addPin(layerName, "poi_" + pinType + "_" + j, poi.latitude, poi.longitude, pinType, poi.name, poi.html);
2662              }
2663              _self.endAddingPins(layerName);
2664            }
2665            if (!_poiEnabled) {
2666              $(_mapContainer).trigger({ type: "poichange", poiEnabled: true });
2667              _poiEnabled = true;
2668            }
2669          }
2670        });
2671      }
2672    } else {
2673      if (_poiEnabled) {
2674        //Hide the pin layer and keep the pinlayer visible (avoid uncheck the checkbox)
2675        _self.hideAllPOI(false);
2676        _poiEnabled = false;
2677        $(_mapContainer).trigger({ type: "poichange", poiEnabled: false });
2678      }
2679    }
2680  }
2681
2682  // Valuation functions
2683
2684  function updateValuations() {
2685    var highlightIndex = getLayerIndexById('Highlight');
2686    var layer = highlightIndex && options ? options.dmpLayers[highlightIndex] : null;
2687
2688    if (!layer) return;
2689
2690    if (Math.round(_mapState.Zoom) < layer.zoomRange.min ||
2691      Math.round(_mapState.Zoom) > layer.zoomRange.max) {
2692      _self.deleteAllPins('Valuations');
2693    } else {
2694      var boundingBox = {
2695        NeLat: _mapState.NELat,
2696        SwLat: _mapState.SWLat,
2697        NeLon: _mapState.NELon,
2698        SwLon: _mapState.SWLon
2699      };
2700      $.ajax({
2701        mode: 'abort',
2702        port: 'valuationpins',
2703        type: 'POST',
2704        url: Utils.AppPath + '/Include/AJAX/MapSearch/GetValuationPins.aspx',
2705        data: boundingBox,
2706        dataType: 'json',
2707        beforeSend: function (jqXHR, settings) {
2708          jqXHR.setRequestHeader("X-Form", JSON.stringify(boundingBox));
2709        }
2710      })
2711        .done(function (data) {
2712          // Initialize Valuation layer as non-cluster. 'Valuations' layer holds 'Valuation' pins.
2713          getLayer('Valuation', false);
2714
2715          // Add the valuation "pins"
2716          if (data.IsSuccess && data.Entity && data.Entity.length) {
2717            _self.startAddingPins('Valuations', false);
2718            $.each(data.Entity, function (index, prop) {
2719              if (prop.ParcelNumber) {
2720                _parcelPropertyIds[prop.ParcelNumber] = prop.PropertyId;
2721              }
2722              _publicRecords[prop.PropertyId] = {
2723                parcelNumber: prop.ParcelNumber,
2724                url: prop.Url
2725              };
2726              _self.addPin('Valuations',
2727                'val_1_' + prop.PropertyId,
2728                prop.Lat,
2729                prop.Lon,
2730                BingMap.PinType.Valuation,
2731                prop.EstimatedValue,
2732                null);
2733            });
2734            _self.endAddingPins('Valuations');
2735          } else {
2736            _self.deleteAllPins('Valuations');
2737          }
2738        });
2739    }
2740  }
2741
2742  // Layers functions
2743
2744  function removeDmpLayer(layerIndex, isLabels) {
2745    var layerDef = options.dmpLayers[layerIndex];
2746    var layerId;
2747    if (isLabels) {
2748      layerId = layerDef.id + "L" + options.instanceId;
2749      layerDef.labelIsVisible = false;
2750    } else {
2751      layerId = layerDef.id + "B" + options.instanceId;
2752      layerDef.isVisible = false;
2753    }
2754    var layer = (typeof _map.getDmpLayerById !== "undefined") && _map.getDmpLayerById(layerId);
2755    if (layer) {
2756      _map.removeEntity(layer);
2757    }
2758  }
2759
2760  function addDmpLayer(layerIndex, isLabels, params) {
2761    var layerDef = options.dmpLayers[layerIndex];
2762    var layerId, layerSld;
2763    if (isLabels) {
2764      layerId = layerDef.id + "L" + options.instanceId;
2765      layerSld = layerDef.lsld;
2766      layerDef.labelIsVisible = true;
2767    } else {
2768      layerId = layerDef.id + "B" + options.instanceId;
2769      layerSld = layerDef.sld;
2770      layerDef.isVisible = true;
2771    }
2772
2773    if ((typeof _map.getDmpLayerById === "undefined") || _map.getDmpLayerById(layerId)) {
2774      return;
2775    }
2776
2777    var layer;
2778    var layerParams = $.extend({ antiAlias: true }, params);
2779    if (layerDef.id === "ParcelLine" || layerDef.id === "FloodZone") {
2780      layer = new Dmp.Layer.TileLayer(layerId, "SS", layerParams);
2781    } else {
2782      layer = new Dmp.Layer.WMSLayer(layerId, "SS", layerParams);
2783    }
2784    layer.addChild(layerId, layerDef.resource, layerSld, { zIndex: layerDef.zIndex, zoomRange: layerDef.zoomRange });
2785    _map.addEntity(layer);
2786  }
2787
2788  function showBingTraffic(layerDef) {
2789    if (_trafficManager) {
2790      _trafficManager.showFlow();
2791      _trafficManager.showLegend();
2792      layerDef.isVisible = true;
2793    }
2794  }
2795
2796  function hideBingTraffic(layerDef) {
2797    if (_trafficManager) {
2798      _trafficManager.hide();
2799    }
2800    layerDef.isVisible = false;
2801  }
2802
2803  // Nav bar functions
2804
2805  function zoomIn() {
2806    var zoom = _map.getZoom();
2807    if (zoom < _map.getZoomRange().max) {
2808      _map.setView({
2809        'zoom': zoom + 1
2810      });
2811    }
2812
2813    return false;
2814  }
2815
2816  function zoomOut() {
2817    var zoom = _map.getZoom();
2818    if (zoom > _map.getZoomRange().min) {
2819      _map.setView({
2820        'zoom': zoom - 1
2821      });
2822    }
2823    return false;
2824  }
2825
2826  function rotateLeft() {
2827    var heading = _map.getHeading();
2828    var newHeading = heading === 0 ? 270 : heading - 90;
2829    _map.setView({ heading: newHeading });
2830    return false;
2831  }
2832
2833  function rotateRight() {
2834    var heading = _map.getHeading();
2835    var newHeading = heading === 270 ? 0 : heading + 90;
2836    _map.setView({ heading: newHeading });
2837    return false;
2838  }
2839
2840  function resetOrientation() {
2841    _map.setView({ heading: 0 });
2842    return false;
2843  }
2844
2845  function toggleRotateButtons() {
2846    var imageryId = _map.getImageryId();
2847    if (imageryId.indexOf("Birdseye") !== -1) {
2848      $("#RotateButtonContainer", _mapContainer).show();
2849    } else {
2850      $("#RotateButtonContainer", _mapContainer).hide();
2851    }
2852  }
2853
2854  // Menu functions
2855
2856  function showHideMenuItems($menubar) {
2857    var countShown = 0;
2858
2859    var showHide = function (className, show) {
2860      $("li.map-menuitem a." + className, $menubar).parent()[show ? "show" : "hide"]();
2861      if (show) countShown += 1;
2862    };
2863
2864    showHide('drawIcon', options.showDrawingTools);
2865    showHide('layersIcon', options.showLayerMenu);
2866    showHide('poiIcon', options.showPoi);
2867
2868    var showPinsIcon = !enablePricePins() && ((typeof options.showPinLayerPicker === "undefined" || options.showPinLayerPicker == null)
2869      ? options.showLayerMenu
2870      : options.showPinLayerPicker);
2871    showHide('pinsIcon', showPinsIcon);
2872
2873    $menubar[countShown > 0 ? "show" : "hide"]();
2874  }
2875
2876  function onMapWidthChanged() {
2877    if ($(_mapContainer).width() < 300) {
2878      $("div.map-menubar", _mapContainer).removeClass("narrow verynarrow").addClass("narrow verynarrow");
2879      _map.setOptions({ navigationBarMode: MM.NavigationBarMode.minified });
2880    } else if ($(_mapContainer).width() < 525) {
2881      $("div.map-menubar", _mapContainer).removeClass("narrow verynarrow").addClass("narrow");
2882      _map.setOptions({ navigationBarMode: MM.NavigationBarMode.minified });
2883    } else {
2884      $("div.map-menubar", _mapContainer).removeClass("narrow verynarrow");
2885      _map.setOptions({ navigationBarMode: MM.NavigationBarMode.default });
2886    }
2887  }
2888
2889  function getBarColorClass() {
2890    var mapTypeId = _map.getMapTypeId();
2891    return mapTypeId === "r" ? "map-light" : "map-dark";
2892  }
2893
2894  function hideAllDialogs() {
2895    _self.hideListingBubble();
2896    hideMenuDialog();
2897    hideMapTypeMenu();
2898    hideShapeRemover();
2899  }
2900
2901  function toggleMapLabelsSelector(view) {
2902    if (typeof view === "undefined") {
2903      view = _map.getMapTypeId();
2904    }
2905    if (view !== "r") {
2906      $(".labelToggle", _mapContainer).removeClass("disabled");
2907      $("#labels-onoff", _mapContainer).prop("disabled", false);
2908    } else {
2909      $(".labelToggle", _mapContainer).removeClass("disabled").addClass("disabled");
2910      $("#labels-onoff", _mapContainer).prop("disabled", true);
2911    }
2912  }
2913
2914  function updateMapTypeMenu(newView) {
2915    var mapTypeName = options.mapText.mapTypeName[newView];
2916    var mapTypeNameClass = mapTypeName.replace(" ", "").replace("'", "");
2917    $("#MapTypeText a", _mapContainer).text(mapTypeName);
2918    $("#MapTypeIcon", _mapContainer)
2919      .removeClass("selectorIconRoad selectorIconAerial selectorIconBirdseye")
2920      .addClass("selectorIcon" + mapTypeNameClass);
2921    $(".map-navbar,.map-menubar", _mapContainer).removeClass("map-dark").removeClass("map-light");
2922    if (newView === "r") {
2923      $(".map-navbar,.map-menubar", _mapContainer).addClass("map-light");
2924    } else {
2925      $(".map-navbar,.map-menubar", _mapContainer).addClass("map-dark");
2926    }
2927    toggleMapLabelsSelector(newView);
2928  }
2929
2930  function hideMapTypeMenu() {
2931    $("#MapTypeMenu", _mapContainer).hide(250);
2932    $("#MapStyleSelector", _mapContainer).removeClass("active-dialog");
2933    return false;
2934  }
2935
2936  function showMapTypeMenu() {
2937    if (!$("#MapTypeMenu", _mapContainer).is(":visible") || $("#MapTypeMenu", _mapContainer).is(":animated")) {
2938      hideAllDialogs();
2939      $("#MapTypeMenu", _mapContainer).show(250);
2940      $("#MapStyleSelector", _mapContainer).addClass("active-dialog");
2941    } else {
2942      hideMapTypeMenu();
2943    }
2944    return false;
2945  }
2946
2947  function hideMenuDialog() {
2948    if ($(".map-dialog", _mapContainer).is(":visible")) {
2949      $(".map-dialog", _mapContainer).slideUp(250, function () {
2950        $(".map-dialog", _mapContainer).css("overflow", "visible");
2951      });
2952      $(".map-menuitem a", _mapContainer).removeClass("active-dialog");
2953
2954      _activeDialog = null;
2955    }
2956    return false;
2957  }
2958
2959  function showMenuDialog(dialogName, body) {
2960    var show = false;
2961    if (_activeDialog != null) {
2962      var previousDialog = _activeDialog;
2963      if (previousDialog === dialogName) {
2964        hideMenuDialog();
2965        return;
2966      }
2967    } else {
2968      hideAllDialogs();
2969      show = true;
2970    }
2971    _activeDialog = dialogName;
2972    $(".map-menuitem a", _mapContainer).removeClass("active-dialog");
2973    $("a." + dialogName + "Icon", _mapContainer).addClass("active-dialog");
2974    $(".map-dialog-header-icon", _mapContainer)
2975      .removeClass()
2976      .addClass("map-dialog-header-icon")
2977      .addClass(dialogName + "Icon");
2978    $(".map-dialog-body", _mapContainer).html(body);
2979    if (show) {
2980      $(".map-dialog", _mapContainer)
2981        .css("opacity", 0)
2982        .slideDown("fast")
2983        .animate(
2984          { opacity: 1 },
2985          { queue: false, duration: "slow" }
2986        );
2987      $(".map-dialog", _mapContainer).css("overflow", "visible");
2988    }
2989  }
2990
2991  function showDrawMenu() {
2992    // Header
2993    var bodyHtml = "<ul class='map-draw-header'>" +
2994      "<li class='map-draw-header-item drawIcon selected'><div class='map-menu-icon map-menu-icon-draw'></div><span>" + options.mapText.dialogs.draw.drawTab + "</span></li>" +
2995      "<li class='map-draw-header-item radiusIcon'><div class='map-menu-icon map-menu-icon-radius'></div><span>" + options.mapText.dialogs.draw.radiusTab + "</span></li>" +
2996      "<li class='map-draw-header-item rectangleIcon'><div class='map-menu-icon map-menu-icon-square'></div><span>" + options.mapText.dialogs.draw.rectangleTab + "</span></li>" +
2997      "</ul>";
2998    // Draw menu
2999    bodyHtml += "<div class='map-draw-container draw'>
2999" +
3000      "<label class='dialog-section-header'>" + options.mapText.dialogs.draw.drawHeader + "</label>" +
3001      "</div>";
3002    // Radius menu
3003    bodyHtml += "<div class='map-draw-container radius' style='display: none'>" +
3004      "<label class='dialog-section-header'>" + options.mapText.dialogs.draw.radiusHeader + "</label>" +
3005      "<ul class='map-radius-list map-checkbox-list'>" +
3006      "<li><label class='radio'><input type='radio' name='map-radius-select' value='click' checked='checked' /> " + options.mapText.dialogs.draw.radiusFree + "</label></li>" +
3007      "<li><label class='radio'><input type='radio' name='map-radius-select' value='center' /> " + options.mapText.dialogs.draw.radiusCenter + " </label>" +
3008      "<select id='map-dialog-radius-distance-center' class='map-dialog-radius-distance' >" +
3009      "<option value='.5'>.5 " + Utils.translate('miles') + "</option>" +
3010      "<option value='1'>1 " + Utils.translate('mile') + "</option>" +
3011      "<option value='2' selected='selected'>2 " + Utils.translate('miles') + "</option>" +
3012      "<option value='3'>3 " + Utils.translate('miles') + "</option>" +
3013      "<option value='4'>4 " + Utils.translate('miles') + "</option>" +
3014      "<option value='5'>5 " + Utils.translate('miles') + "</option>" +
3015      "<option value='10'>10 " + Utils.translate('miles') + "</option>" +
3016      "<option value='15'>15 " + Utils.translate('miles') + "</option>" +
3017      "<option value='20'>20 " + Utils.translate('miles') + "</option>" +
3018      "</select>" +
3019      "</li>";
3020    if (rbw.geolocation.browserSupportsGeolocation) {
3021      bodyHtml += "<li><label class='radio'><input type='radio' name='map-radius-select' value='mylocation' /> " + options.mapText.dialogs.draw.radiusMyLocation + " </label>" +
3022        "<select id='map-dialog-radius-distance-mylocation' class='map-dialog-radius-distance' >" +
3023        "<option value='.5'>.5 " + Utils.translate('miles') + "</option>" +
3024        "<option value='1'>1 " + Utils.translate('mile') + "</option>" +
3025        "<option value='2' selected='selected'>2 " + Utils.translate('miles') + "</option>" +
3026        "<option value='3'>3 " + Utils.translate('miles') + "</option>" +
3027        "<option value='4'>4 " + Utils.translate('miles') + "</option>" +
3028        "<option value='5'>5 " + Utils.translate('miles') + "</option>" +
3029        "<option value='10'>10 " + Utils.translate('miles') + "</option>" +
3030        "<option value='15'>15 " + Utils.translate('miles') + "</option>" +
3031        "<option value='20'>20 " + Utils.translate('miles') + "</option>" +
3032        "</select>" +
3033        "<label class='dialog-note'>" + options.mapText.dialogs.draw.radiusMyLocationNote + "</label>" +
3034        "</li>";
3035    }
3036    bodyHtml += "</ul></div>";
3037    // Rectangle menu
3038    bodyHtml += "<div class='map-draw-container rectangle' style='display: none'>" +
3039      "<label class='dialog-section-header'>" + options.mapText.dialogs.draw.rectangleHeader + "</label>" +
3040      "</div>";
3041    // Shape name and Draw button
3042    bodyHtml += "<div class='map-draw-footer'>" +
3043      "<label class='map-draw-shapename-container'>" + options.mapText.dialogs.draw.footer + "<br/><input type='text' class='input-block-level' id='map-dialog-draw-shapename' value='Drawn Shape " + _boundaryCount + "' /></label>" +
3044      "<button id='map-draw-button' class='btn btn-primary'>" + options.mapText.dialogs.draw.buttonText + "</button></div>" +
3045      "</div>";
3046    showMenuDialog("draw", bodyHtml);
3047
3048    var drawType, shapeName;
3049    function getDrawType($selectedTab) {
3050      if ($selectedTab.hasClass("rectangleIcon")) {
3051        drawType = "rectangle";
3052        shapeName = "Rectangle";
3053      } else if ($selectedTab.hasClass("radiusIcon")) {
3054        drawType = "radius";
3055        shapeName = "Radius";
3056      } else {
3057        drawType = "draw";
3058        shapeName = "Drawn Shape";
3059      }
3060    }
3061    $("li.map-draw-header-item", _mapContainer).on("click", function () {
3062      getDrawType($(this));
3063      $("#map-dialog-draw-shapename", _mapContainer).val(shapeName + " " + _boundaryCount);
3064      $("li.map-draw-header-item", _mapContainer).removeClass("selected");
3065      $("li.map-draw-header-item." + drawType + "Icon", _mapContainer).addClass("selected");
3066      $(".map-draw-container", _mapContainer).hide();
3067      $(".map-draw-container." + drawType, _mapContainer).show();
3068    });
3069
3070    $("#map-draw-button", _mapContainer).on("click", function () {
3071      getDrawType($("li.map-draw-header-item.selected", _mapContainer));
3072      _boundaryCount++;
3073      _currentShapeName = $("#map-dialog-draw-shapename", _mapContainer).val();
3074      if (_currentShapeName.length === 0) {
3075        _currentShapeName = shapeName;
3076        $("#map-dialog-draw-shapename", _mapContainer).val(_currentShapeName);
3077      }
3078      if (drawType === "radius") {
3079        var selected = $(".map-radius-list input[name='map-radius-select']:checked", _mapContainer).val();
3080        if (selected === "click") {
3081          startDrawing("radius");
3082        } else {
3083          var distance = Number($("#map-dialog-radius-distance-" + selected, _mapContainer).val());
3084          if (selected === "center") {
3085            var loc = _map.getCenter();
3086            drawRadiusShape(distance, loc.latitude, loc.longitude, _currentShapeName, onCustomShapeAdded);
3087          } else {
3088            rbw.geolocation.getCurrentPosition(function (position) {
3089              drawRadiusShape(distance, position.coords.latitude, position.coords.longitude, _currentShapeName, onCustomShapeAdded);
3090            });
3091          }
3092        }
3093      } else {
3094        startDrawing(drawType);
3095      }
3096      hideMenuDialog();
3097      return false;
3098    });
3099    $("#map-dialog-draw-shapename", _mapContainer).on("focus", function () {
3100      this.select();
3101    });
3102    return false;
3103  }
3104
3105  function showLayersMenu() {
3106    // Map view selector
3107    var bodyHtml = "<div class='map-view-selector-container'>" +
3108      "<label class='dialog-section-header'>" + options.mapText.dialogs.layers.headerMapView + "</label>" +
3109      "<ul class='map-views'>" +
3110      "<li class='map-view Road' data-view='r'><label class='radio'><input type='radio' name='map-view-select' value='r' /> " + options.mapText.mapTypeName.r + "</label><div class='slot'><div class='icon'></div></div></li>" +
3111      "<li class='map-view Aerial' data-view='a'><label class='radio'><input type='radio' name='map-view-select' value='a' /> " + options.mapText.mapTypeName.a + "</label><div class='slot'><div class='icon'></div></div></li>" +
3112      "</ul></div>";
3113
3114    var dmpHtml = "";
3115    if (options.dmpLayers.length > 0) {
3116      dmpHtml = "<div class='map-dmp-selector-container'>" +
3117        "<label class='dialog-section-header'>" + options.mapText.dialogs.layers.header + "</label>" +
3118        "<label class='dialog-note'>" + options.mapText.dialogs.layers.note + "</label>" +
3119        "<ul class='map-dmp-list map-checkbox-list'>" +
3120        "<li class='boundary-select select-header'>" + options.mapText.dialogs.layers.boundaryHeader + "</li><li class='label-select select-header'>" + options.mapText.dialogs.layers.labelHeader + "</li>";
3121      for (var i = 0; i < options.dmpLayers.length; i++) {
3122        var layerDef = options.dmpLayers[i];
3123        dmpHtml += "<li class='boundary-select'><label class='checkbox' for='checkbox-" + i + "'><input id='checkbox-" + i + "' type='checkbox'";
3124        if (layerDef.isVisible) {
3125          dmpHtml += " checked='checked'";
3126        }
3127        dmpHtml += " />" + layerDef.label + "</label></li>";
3128        dmpHtml += "<li class='label-select'>";
3129        if (layerDef.lsld && layerDef.lsld.length > 0) {
3130          dmpHtml += "<input type='checkbox' id='checkbox-label-" + i + "'";
3131          if (layerDef.labelIsVisible) {
3132            dmpHtml += " checked='checked'";
3133          }
3134          dmpHtml += " />";
3135        }
3136        dmpHtml += "</li>";
3137      }
3138      dmpHtml += "<li class='clearall'><a href='#' id='map-layers-clearall'>" + options.mapText.dialogs.layers.clear + "</a></li>";
3139      dmpHtml += "</ul></div>";
3140    }
3141    bodyHtml += dmpHtml;
3142
3143    // Show the dialog
3144    showMenuDialog("layers", bodyHtml);
3145    $("ul.map-views input[value='" + _map.getMapTypeId() + "']").prop("checked", true);
3146    $("ul.map-views li.map-view").on("click", function () {
3147      var mapView = $(this).data("view");
3148      _self.setView(mapView);
3149      $("ul.map-views input[value='" + mapView + "']").prop("checked", true);
3150    });
3151    $("ul.map-dmp-list li.boundary-select input[type='checkbox']", _mapContainer).change(function () {
3152      var layerIndex = this.id.replace("checkbox-", "");
3153      var layerDef = options.dmpLayers[layerIndex];
3154      if (this.checked) {
3155        if (layerDef.id === "BingTraffic") {
3156          showBingTraffic(layerDef);
3157        } else {
3158          addDmpLayer(layerIndex, false);
3159        }
3160      } else {
3161        if (layerDef.id === "BingTraffic") {
3162          hideBingTraffic(layerDef);
3163        } else {
3164          removeDmpLayer(layerIndex, false);
3165        }
3166      }
3167    });
3168    $("ul.map-dmp-list li.label-select input[type='checkbox']", _mapContainer).change(function () {
3169      var layerIndex = this.id.replace("checkbox-label-", "");
3170      if (this.checked) {
3171        addDmpLayer(layerIndex, true);
3172      } else {
3173        removeDmpLayer(layerIndex, true);
3174      }
3175    });
3176    $("ul.map-dmp-list li.clearall a").on("click", function () {
3177      for (var i = 0; i < options.dmpLayers.length; i++) {
3178        var layerDef = options.dmpLayers[i];
3179        if (layerDef.id === "BingTraffic") {
3180          hideBingTraffic(layerDef);
3181        } else {
3182          removeDmpLayer(i, false);
3183          removeDmpLayer(i, true);
3184        }
3185      }
3186      $("ul.map-dmp-list input[type='checkbox']").prop("checked", false);
3187      return false;
3188    });
3189    return false;
3190  }
3191
3192  function showPoiMenu() {
3193    // Create the body with list of POI
3194    var bodyHtml = "<label class='dialog-section-header'>" + options.mapText.dialogs.poi.header + "</label>" +
3195      "<label class='dialog-note'>" + options.mapText.dialogs.poi.note + "</label>" +
3196      "<ul class='map-poi-list map-checkbox-list'>";
3197    for (var i = 0; i < BingMap.PinInfo.length; i++) {
3198      var pinInfo = BingMap.PinInfo[i];
3199      if (pinInfo.PinClass && pinInfo.PinClass.indexOf("poi-") === 0 && pinInfo.Disabled !== true) {
3200        var pinLayer = getLayer(pinInfo.ShortName);
3201        bodyHtml += "<li><label class='checkbox' for='checkbox-" + pinInfo.ShortName + "'><input id='checkbox-" + pinInfo.ShortName + "' type='checkbox'";
3202        if (pinLayer.isVisible) {
3203          bodyHtml += " checked='checked'";
3204        }
3205        bodyHtml += " /><span class='" + pinInfo.PinClass + " poi-icon'>" + pinInfo.DisplayName + "</span></label></li>";
3206      }
3207    }
3208    bodyHtml += "<li class='clearall'><a href='#' id='map-poi-clearall'>" + options.mapText.dialogs.poi.clear + "</a></li>" +
3209      "</ul><div style='clear:left;'></div>";
3210
3211    // Show the dialog
3212    showMenuDialog("poi", bodyHtml);
3213    // Activate the checkboxes
3214    $("ul.map-poi-list li input[type='checkbox']", _mapContainer).change(function () {
3215      var pinType = this.id.replace("checkbox-", "");
3216      if (this.checked) {
3217        _self.showPOI(pinType);
3218      } else {
3219        _self.hidePOI(pinType);
3220      }
3221    });
3222    $("#map-poi-clearall", _mapContainer).on("click", function () {
3223      _self.hideAllPOI(true);
3224      return false;
3225    });
3226    return false;
3227  }
3228
3229  function showPinsMenu() {
3230    // Create the body with the list of layers       
3231    var bodyHtml = "";
3232    var i = 0;
3233
3234    var addedLegendNames = [];
3235
3236    Object.keys(_layers).forEach(function (layerName) {
3237      var pinLayer = _layers[layerName];
3238      var pinTypeClass = {};
3239
3240      if (pinLayer.name === 'ClusterPins') return;
3241      //Don't add POI (layerType 2) to Pins Section
3242      if (pinLayer.pins && pinLayer.pins.length > 0 && pinLayer.layerType === LayerType.Pin) {
3243        for (var j = 0; j < pinLayer.pins.length; ++j) {
3244          if (pinLayer.pins[j].tag === 'ClusterPin') continue;
3245
3246          var hasCustomLayerName = translatePinTypeToLayerName(pinLayer.pins[j].pinType, false);
3247
3248          if (hasCustomLayerName && typeof pinTypeClass[pinLayer.pins[j].pinType] === "undefined")
3249            pinTypeClass[pinLayer.pins[j].pinType] = pinLayer.pins[j].pinClass;
3250        }
3251      }
3252
3253      Object.keys(pinTypeClass).forEach(function (pinType) {
3254        var legendLayerName = translatePinTypeToLayerName(pinType);
3255
3256        if (addedLegendNames.indexOf(legendLayerName) !== -1)
3257          return;
3258
3259        addedLegendNames.push(legendLayerName);
3260
3261        bodyHtml += "<li><label class='checkbox' for='checkbox-" + ++i + "'><input id='checkbox-" + i + "' type='checkbox'";
3262
3263        if (pinLayer.isVisible)
3264          bodyHtml += " checked='checked'";
3265
3266        bodyHtml += " /><span class='" + pinTypeClass[pinType] + " pin'>" + legendLayerName + "</span></label></li>";
3267      });
3268
3269      if (bodyHtml == "" && pinLayer.pins && pinLayer.pins.length > 0 && pinLayer.layerType === LayerType.Pin) {
3270        var pinClass = pinLayer.pins[0].pinClass;
3271        // Currently in the pins we can have different statuses and such, we only
3272        // want the active one to show here.  In the near future we'll be making this
3273        // A full legend where you can click on and off all the different statuses
3274        // and we can fix this
3275        if (pinClass.indexOf("pin-listing") === 0) {
3276          pinClass = "pin-listing-active";
3277        }
3278        bodyHtml += "<li><label class='checkbox' for='checkbox-" + ++i + "'><input id='checkbox-" + i + "' type='checkbox'";
3279        if (pinLayer.isVisible) {
3280          bodyHtml += " checked='checked'";
3281        }
3282        bodyHtml += " /><span class='" + pinClass + " pin'>" + layerName + "</span></label></li>";
3283      }
3284    });
3285
3286    if (bodyHtml.length === 0) {
3287      bodyHtml = "<label class='dialog-section-header'>" + options.mapText.dialogs.pins.none + "</label>";
3288    } else {
3289      bodyHtml = "<label class='dialog-section-header'>" + options.mapText.dialogs.pins.header + "</label>" +
3290        "<ul class='map-pinlayers-list map-checkbox-list'>" + bodyHtml + "</ul>";
3291    }
3292    // Show the dialog
3293    showMenuDialog("pins", bodyHtml);
3294    // Activate the checkboxes
3295    $("ul.map-pinlayers-list li input[type='checkbox']", _mapContainer).change(function () {
3296      if (this.checked) {
3297        _self.showPins($(this).parent().text());
3298      } else {
3299        _self.hidePins($(this).parent().text());
3300      }
3301    });
3302    return false;
3303  }
3304
3305  function checkClick(e) {
3306    // If we click out of the map, close the dialogs
3307    if ($(e.target).closest('#' + options.container).length) {
3308      return;
3309    }
3310    hideAllDialogs();
3311  }
3312
3313  function switchToBirdseye() {
3314    //Verify that birdseye imagery is available before trying to switch into it.
3315    MM.getIsBirdseyeAvailable(_map.getCenter(),
3316      _map.getHeading(),
3317      function (isAvailable) {
3318        if (isAvailable) {
3319          _map.setView({ mapTypeId: MM.MapTypeId.birdseye, heading: _map.getHeading() });
3320        } else {
3321          _map.setView({ mapTypeId: MM.MapTypeId.aerial });
3322        }
3323      });
3324  }
3325
3326  function onMapTypeChange() {
3327    var newView = _map.getMapTypeId();
3328    if (newView === MM.MapTypeId.birdseye) {
3329      if (_currentView === MM.MapTypeId.birdseye) {
3330        $(".MicrosoftMap .NavBar_Container .NavBar_MapTypeButtonContainerWrapper")
3331          .css('visibility', 'hidden');
3332      } else {
3333        $(".MicrosoftMap .NavBar_Container .NavBar_MapTypeButtonContainerWrapper")
3334          .css('visibility', 'visible');
3335      }
3336    }
3337    //When we switch back from Bird's eye view MapType Container should be visble
3338    else
3339      $(".MicrosoftMap .NavBar_Container .NavBar_MapTypeButtonContainerWrapper")
3340        .css('visibility', 'visible');
3341
3342    _currentView = newView;
3343  }
3344
3345  // Add menu bar, nav bar, and related events
3346  function addEvents() {
3347    // If the menu bar is resized under 550px, hide the menubar labels
3348    $(_mapContainer).on("widthChanged", onMapWidthChanged);
3349    onMapWidthChanged();
3350
3351    //resize event no longer supported?
3352    MM.Events.addHandler(_map, "mapresize", function (evt) {
3353      _self.onMapResize();
3354    });
3355
3356    // If we click out of the map, close the dialogs
3357    $(document).on('click', checkClick);
3358
3359    MM.Events.addHandler(_map,
3360      "maptypechanged",
3361      onMapTypeChange);
3362
3363    // This will do handy things like close the autocomplete lists
3364    MM.Events.addHandler(_map, "click", function () {
3365      hideAllDialogs();
3366      document.activeElement.blur();
3367    });
3368    if (!options.fixedMap && options.fireOnChange) {
3369      MM.Events.addHandler(_map, "viewchangeend", function () {
3370        // When the map first loads, it fires an event.  We have our own
3371        // first load event when the shapes have finished loading, so 
3372        // we don't need to call this twice
3373        if (_firstViewChangeEnd) {
3374          _firstViewChangeEnd = false;
3375          return;
3376        }
3377        _self.onChangeView();
3378      });
3379      MM.Events.addHandler(_map, "viewchangestart", function () {
3380        // When the map first loads, it fires an event.  We have our own
3381        // first load event when the shapes have finished loading, so 
3382        // we don't need to call this twice
3383        if (_firstViewChangeEnd) {
3384          _firstViewChangeEnd = false;
3385          return;
3386        }
3387        _self.onChangingView();
3388      });
3389    }
3390    // Show or hide the birds eye rotate buttons depending on the current imagery
3391    toggleRotateButtons();
3392
3393    // The menu events
3394    // Map type selector
3395    $(".mapTypeButtonContainerWrapper", _mapContainer).on("click", showMapTypeMenu);
3396    $("#MapTypeMenu .slot", _mapContainer).on("click", function () {
3397      var mapTypeId = $(this).attr("data-maptypeid");
3398      _self.setView(mapTypeId);
3399      hideMapTypeMenu();
3400      return false;
3401    });
3402    $("#labels-onoff", _mapContainer).change(function () {
3403      var overlay = $(this).is(":checked")
3404        ? MM.LabelOverlay.visible
3405        : MM.LabelOverlay.hidden;
3406      _map.setView({ labelOverlay: overlay });
3407    });
3408
3409    $(".map-menuitem .drawIcon", _mapContainer).on("click", showDrawMenu);
3410    $(".map-menuitem .layersIcon", _mapContainer).on("click", showLayersMenu);
3411    $(".map-menuitem .poiIcon", _mapContainer).on("click", showPoiMenu);
3412    $(".map-menuitem .pinsIcon", _mapContainer).on("click", showPinsMenu);
3413    $(".map-dialog-header .close", _mapContainer).on("click", hideMenuDialog);
3414    $(".map-shape-remover", _mapContainer).on("click", removeCustomShape);
3415
3416    // Nav bar events
3417    $("#ZoomInButton", _mapContainer).on("click", zoomIn);
3418    $("#ZoomOutButton", _mapContainer).on("click", zoomOut);
3419    $("#RotateLeftButton", _mapContainer).on("click", rotateLeft);
3420    $("#RotateButton", _mapContainer).on("click", resetOrientation);
3421    $("#RotateRightButton", _mapContainer).on("click", rotateRight);
3422  }
3423
3424  function addNavBar() {
3425    var navbar = "<div id='MapNavBar' class='map-navbar " + getBarColorClass() + "'>" +
3426      "<a id='ZoomInButton' class='map-navbar-button map-navbar-zoomin' href='#' title='" + options.mapText.navBar.zoomIn + "'></a>" +
3427      "<a id='ZoomOutButton' class='map-navbar-button map-navbar-zoomout' href='#' title='" + options.mapText.navBar.zoomOut + "'></a>" +
3428      "<div id='RotateButtonContainer' class='map-navbar-button-container'>" +
3429      "<a id='RotateLeftButton' class='map-navbar-button map-navbar-rotateleft' href='#' title='" + options.mapText.navBar.rotateLeft + "'></a>" +
3430      "<a id='RotateButton' class='map-navbar-button map-navbar-rotate' href='#' title='" + options.mapText.navBar.reset + "'></a>" +
3431      "<a id='RotateRightButton' class='map-navbar-button map-navbar-rotateright' href='#' title='" + options.mapText.navBar.rotateRight + "'></a>" +
3432      "</div>" +
3433      "</div>";
3434    // $(_mapContainer).prepend(navbar);
3435  }
3436
3437  function addMenuBar() {
3438    // Menu item and menu bar containers
3439    // The bootstrap class is for old legacy sites that don't generally already have bootstrap
3440    var $menubar = $("<div class='map-menubar " + getBarColorClass() + " bootstrap'></div>");
3441
3442    // Menu dialog box.  This is the container for all dialogs, and initially is hidden
3443    $menubar.append("<div class='map-menubar-container map-dialog'>" +
3444      "<div class='map-dialog-panel'>" +
3445      "<div class='map-dialog-header'>" +
3446      "<div class='map-dialog-header-icon'></div>" +
3447      "<div class='map-dialog-header-title map-menu-item'></div>" +
3448      "<button class='close'>Close <span class='fa fa-times'></span></button>" +
3449      "</div>" +
3450      "<div class='map-dialog-body'></div>" +
3451      "</div></div>");
3452
3453    // The individual menu items
3454    var $menuItems = $("<ul class='map-menuitems map-menubar-container'></ul>");
3455    $menuItems.append("<li class='map-menuitem'><a href='#' class='drawIcon map-selector'><div class='map-menu-icon map-menu-icon-draw'></div><span class='map-menuitem-label'>" + options.mapText.menuBar.draw + "</span></a></li>");
3456    $menuItems.append("<li class='map-menuitem'><a href='#' class='layersIcon map-selector'><div class='map-menu-icon map-menu-icon-layers'></div><span class='map-menuitem-label'>" + options.mapText.menuBar.layer + "</span></a></li>");
3457    $menuItems.append("<li class='map-menuitem'><a href='#' class='poiIcon map-selector'><div class='map-menu-icon map-menu-icon-poi'></div><span class='map-menuitem-label'>" + options.mapText.menuBar.poi + "</span></a></li>");
3458    // for sm3 sites do not display this pin menu on map
3459    if ((typeof (options.isSM3) !== 'undefined' && !options.isSM3)) {
3460      $menuItems.append("<li class='map-menuitem'><a href='#' class='pinsIcon map-selector'><div class='map-menu-icon map-menu-icon-pin'></div><span class='map-menuitem-label'>" + options.mapText.menuBar.pin + "</span></a></li>");
3461    }
3462    $menubar.append($menuItems);
3463
3464    // These overlays are used with drawn shapes
3465    $(_mapContainer).append("<div class='map-radius-distance'></div>");
3466    $(_mapContainer).append("<div class='map-shape-remover'>" + options.mapText.overlayText.shapeRemover + "</div>");
3467
3468    // The map view selector
3469    var mapTypeName = options.mapText.mapTypeName[getMapTypeId(options.mapView)];
3470    var mapTypeNameClass = mapTypeName.replace(" ", "").replace("'", "");
3471    /*
3472            $menubar.append("<div class='mapTypeButtonContainerWrapper map-menubar-container'>"
3473                + "<div id='MapStyleSelector' class='mapTypeContainer mapTypeButtonHeight map-selector'>"
3474                    + "<div class='mapTypeButtonDropDownIcon mapTypeButtonHeight selectorIconDropDown'></div>"
3475                    + "<div id='MapTypeText' class='mapTypeButtonText mapTypeButtonHeight'><a href='#'>" + mapTypeName + "</a></div>"
3476                    + "<div id='MapTypeIcon' class='mapTypeButtonIcon mapTypeButtonHeight selectorIcon" + mapTypeNameClass + "'></div>"
3477                + "</div>"
3478            + "</div>");
3479    
3480            // Map view selector dropdown
3481            var $menu = "<div id='MapTypeMenu' class='map-menubar-dropdown menu'>"
3482                + "<div class='slot Road' data-maptypeid='r'>"
3483                + "<div class='icon'></div><div class='pie-label labelStyle'>" + options.mapText.mapTypeName.r + "</div><div class='pie-label-description labelStyleDescription'>" + options.mapText.mapTypeDescription.r + "</div></div>"
3484                + "<div class='slot Aerial' title='Aerial' data-maptypeid='a'>"
3485                + "<div class='icon'></div><div class='pie-label labelStyle'>" + options.mapText.mapTypeName.a + "</div><div class='pie-label-description labelStyleDescription'>" + options.mapText.mapTypeDescription.a + "</div></div>"
3486                + "<div class='slot Birdseye' title='Bird\'s eye' data-maptypeid='be'>"
3487                + "<div class='icon'></div><div class='pie-label labelStyle'>" + options.mapText.mapTypeName.be + "</div><div class='pie-label-description labelStyleDescription'>" + options.mapText.mapTypeDescription.be + "</div></div>"
3488                + "<div class='switchSlot labelToggle'>"
3489                    + "<div class='onoffswitchcontainer'><div class='onoffswitch'>" +
3490                        "<input type='checkbox' name='labels-onoff' class='onoffswitch-checkbox' id='labels-onoff' checked='checked' />"
3491                        + "<label class='onoffswitch-label' for='labels-onoff'>"
3492                        + "<span class='onoffswitch-inner'></span>"
3493                        + "<span class='onoffswitch-switch'></span>"
3494                        + "</label>"
3495                    + "</div></div>"
3496                    + "<div class='pie-label labelStyle'>" + options.mapText.mapTypeName.label + "</div><div class='pie-label-description labelStyleDescription'>" + options.mapText.mapTypeDescription.label + "</div></div>"
3497                + "</div>"
3498                + "</div>";
3499            $menubar.append($menu);
3500    */
3501
3502    $("#MapTypeMenu ." + mapTypeNameClass, _mapContainer).addClass("maptype-selected");
3503
3504    $(_mapContainer).append($menubar);
3505    $("div.map-menubar .onoffswitch-inner").attr('data-content-on', Utils.translate('ON'));
3506    $("div.map-menubar .onoffswitch-inner").attr('data-content-off', Utils.translate('OFF'));
3507    showHideMenuItems($menuItems);
3508
3509    toggleMapLabelsSelector();
3510  }
3511
3512  function initDmpConnection(dmpKey) {
3513    rbw.utils.wait.until(function () {
3514      return window.dmpLoaded;
3515    }).then(function () {
3516      // If for some reason Dmp is not available, just get out
3517      if (typeof Dmp === "undefined" || typeof Dmp.Map === "undefined") {
3518        return;
3519      }
3520      // This never calls the callback if the key is bad
3521      Dmp.Map.init(_map);
3522      Dmp.Env.Connections["SS"].init(dmpKey,
3523        function () {
3524          _dmpInitState = 2;
3525          // This adds the parcel lines layer and makes it visible
3526          addDmpLayer(0, false);
3527        });
3528      if (_dmpInitState === 0) {
3529        setTimeout(function () {
3530          if (_dmpInitState === 0) {
3531            eraseCookie("dmp-key");
3532            initializeDmpLayers();
3533          }
3534        },
3535          2000);
3536      }
3537    });
3538  }
3539
3540  function initializeDmpLayers() {
3541    // This makes the current traffic layer available
3542    MM.loadModule('Microsoft.Maps.Traffic', {
3543      callback: function () {
3544        _trafficManager = new MM.Traffic.TrafficManager(_map);
3545      }
3546    });
3547
3548    function loadDmp(dmpKey, dontSetDmpCookie) {
3549      if (dmpKey && dmpKey.length) {
3550        if (!dontSetDmpCookie) {
3551          createCookieInSeconds("dmp-key", dmpKey, 1200);
3552          _dmpInitState = 1;
3553        }
3554        if (typeof window.Dmp !== "undefined") {
3555          window.dmpLoaded = true;
3556          window.dmpLoadStarted = true;
3557        }
3558        if (window.dmpLoadStarted && !window.ldpOverlayVisible) {
3559          initDmpConnection(dmpKey);
3560        } else {
3561          window.dmpLoadStarted = true;
3562          rbw.utils.wait.until(function () {
3563            return _self.isLoaded();
3564          }).then(function () {
3565            var script = document.createElement("script");
3566            script.type = "text/javascript";
3567            script.async = true;
3568            script.defer = true;
3569            script.src = "/include/js/mapsearch/DmpApi_BingV8.js?_=" + rbw.utils.version;
3570            script.onload = function () {
3571              window.dmpLoaded = true;
3572              initDmpConnection(dmpKey);
3573            };
3574            script.onerror = function () {
3575              Utils.logger.warn("Error loading DMP");
3576            };
3577            document.body.appendChild(script);
3578          });
3579        }
3580      }
3581    }
3582    // Get the Dmp key.  Try from the cookie first to cut down 
3583    // on server transactions
3584    var dmpCookie = readCookie("dmp-key");
3585    if (dmpCookie && dmpCookie.length > 0) {
3586      dmpCookie = unescape(dmpCookie);
3587      loadDmp(dmpCookie, true);
3588    } else {
3589      $.ajax({
3590        type: "GET",
3591        url: Utils.AppPath + "/include/ajax/api.aspx",
3592        data: "op=GetDmpApiKey",
3593        dataType: "json",
3594        contentType: "application/json; charset=utf-8",
3595        success: function (data) {
3596          loadDmp(data, false);
3597        }
3598      });
3599    }
3600  }
3601
3602  function initializePoiLayers() {
3603    for (var i = 0; i < BingMap.PinInfo.length; i++) {
3604      var pinInfo = BingMap.PinInfo[i];
3605      if (pinInfo.PinClass && pinInfo.PinClass.indexOf("poi-") === 0 && pinInfo.Disabled !== true) {
3606        // Pre-load the pin layer and initialize
3607        // them all to hidden for the menu checkboxes
3608        getLayer(pinInfo.ShortName, false, LayerType.Poi, false);
3609      }
3610    }
3611  }
3612
3613  function addInitialShapes() {
3614      var dontSetMapView;
3615    // if poly params not empty draw polygon on load
3616    if (options.startingLocations && options.startingLocations.length > 0) {
3617      var names = searchLocation.getFlattenedLocations("name");
3618      var types = searchLocation.getFlattenedLocations("type");
3619      var values = searchLocation.getFlattenedLocations("value");
3620      for (var i = 0; i < names.length; i++) {
3621        var polyName = names[i];
3622        var polyType = types[i].toLowerCase();
3623        if (polyType === "zip code") {
3624          polyType = "zip";
3625        }
3626        var polyVal = values[i];
3627        if (!polyVal || !polyVal.length) {
3628          polyVal = polyName;
3629        }
3630
3631        // If this is neighborhood or points, we'll do it immediately, otherwise
3632        // we'll defer until after search execution.  This is because complex 
3633        // polygons can really delay things.
3634          if (!polyType || polyType === "radius" || polyType === "neighborhood" || polyType === "township" || polyType === "borough" || polyType === "points" || options.container === "community-main-map") {
3635          // Get the latest boundary count from the drawn boundaries
3636          if (polyName.indexOf("Drawn ") === 0 || polyName.indexOf("Radius ") === 0 || polyName.indexOf("Rectangle ") === 0) {
3637            var polyNameWords = polyName.split(" ");
3638            var boundaryIndex = parseInt(polyNameWords[polyNameWords.length - 1]);
3639            if (boundaryIndex >= _boundaryCount) {
3640              _boundaryCount = boundaryIndex + 1;
3641            }
3642          }
3643          _initialShapes[polyName] = false; 
3644          _self.gotoPoly(polyName, polyType, polyVal, true);
3645        } else {
3646          _initialShapes[polyName] = true;
3647          parent.gotoPolyDeferred(polyName, polyType, polyVal, true);
3648        }
3649      }
3650    } else if (options.startingPolyValue && options.startingPolyValue !== "") {
3651      // If this is neighborhood or points, we'll do it immediately, otherwise
3652      // we'll defer until after search execution.  This is because complex 
3653      // polygons can really delay things.
3654        if (!options.startingPolyType || options.startingPolyType === "" || options.startingPolyType === "neighborhood" || options.startingPolyType === "township" || options.startingPolyType === "borough" || options.startingPolyType === "points" || options.container === "community-main-map") {
3655        // If this is the default map view, set the map view
3656        dontSetMapView = typeof options.defaultBoundingBox !== "undefined" &&
3657          options.defaultBoundingBox !== null &&
3658          JSON.stringify(options.startingBoundingBox) !== JSON.stringify(options.defaultBoundingBox);
3659        _initialShapes[options.startingPolyValue] = false;
3660        _self.gotoPoly(options.startingPolyValue, options.startingPolyType, options.startingPolyValue, dontSetMapView);
3661      } else {
3662        _initialShapes[options.startingPolyValue] = true;
3663        parent.gotoPolyDeferred(options.startingPolyValue, options.startingPolyType, options.startingPolyValue);
3664      }
3665    }
3666  }
3667
3668  // Thisdoes the very first onChangeView call when the map is loaded, 
3669  // to initiate the search.
3670  // It is called in a loop until all of the initial shapes (cities, 
3671  // drawn shapes, etc) are loaded on the map.
3672  var firstOnChangeViewTries = 0;
3673  function firstOnChangeView() {
3674    return new Promise(function (resolve) {
3675      if (_initialShapesTimer !== null) {
3676        clearTimeout(_initialShapesTimer);
3677        _initialShapesTimer = null;
3678      }
3679
3680      var initialShapesAreLoaded = true;
3681      for (var polyName in _initialShapes) {
3682        if (_initialShapes.hasOwnProperty(polyName)) {
3683          if (_initialShapes[polyName] === false) {
3684            initialShapesAreLoaded = false;
3685            break;
3686          }
3687        }
3688      }
3689      // All shapes are loaded, call onChangeView
3690      if (initialShapesAreLoaded) {
3691        _loadingInitialShapes = false;
3692        return resolve(_self.onChangeView());
3693      }
3694      // If we got here, then there are some shapes that aren't loaded,
3695      // so try again in 200ms.  If we've tried for 1 second, 
3696      // go ahead anyway.
3697      if (firstOnChangeViewTries++ > 4) {
3698        return resolve(_self.onChangeView());
3699      }
3700      _initialShapesTimer = setTimeout(firstOnChangeView, 500);
3701    });
3702  }
3703
3704  function loadMap() {
3705    var customMapStyle = undefined;
3706    if (typeof options.customMapStyle === "string") {
3707      customMapStyle = customMapStyles[options.customMapStyle];
3708    } else if (typeof options.customMapStyle === "object") {
3709      customMapStyle = options.customMapStyle;
3710    }
3711
3712    var mapLoadOpts = {
3713      credentials: Utils.BingMapKey,
3714      mapTypeId: getMapTypeId(options.mapView),
3715      showLocateMeButton: false,
3716      showMapTypeSelector: options.showMapTypeSelector,
3717      enableSearchLogo: false,
3718      enableClickableLogo: false,
3719      disableMapTypeSelectorMouseOver: true,
3720      disableScrollWheelZoom: options.disableMouseWheel,
3721      disableStreetside: options.disableStreetside,
3722      customMapStyle: customMapStyle,
3723      minZoom: options.minZoom
3724    };
3725
3726    var isDefaultMapView = typeof options.startingBoundingBox !== "undefined" && options.startingBoundingBox !== null
3727      && typeof options.defaultBoundingBox !== "undefined" && options.defaultBoundingBox !== null
3728      && JSON.stringify(options.startingBoundingBox) === JSON.stringify(options.defaultBoundingBox);
3729
3730    // Use boundary to load map if it for default map view, else you have to use map center. See story for details.
3731    if (isDefaultMapView) {
3732      mapLoadOpts.bounds = MM.LocationRect.fromEdges(
3733        options.defaultBoundingBox.NeLat,
3734        options.defaultBoundingBox.SwLon,
3735        options.defaultBoundingBox.SwLat,
3736        options.defaultBoundingBox.NeLon
3737      );
3738    } else {
3739      mapLoadOpts.center = new MM.Location(options.startingMidLat, options.startingMidLong);
3740      mapLoadOpts.zoom = options.startingZoom;
3741    }
3742
3743    _map = new MM.Map(_mapContainer, mapLoadOpts);
3744  }
3745
3746  /* Start of externally visible functions */
3747
3748  this.onMapResize = function () {
3749    var center = _map.getCenter();
3750    var rect = _map.getBounds();
3751    _mapState = {
3752      Lat: center.latitude,
3753      Lon: center.longitude,
3754      NELat: rect.getNorth(),
3755      NELon: rect.getEast(),
3756      SWLat: rect.getSouth(),
3757      SWLon: rect.getWest(),
3758      Zoom: _map.getZoom(),
3759      Style: _map.getMapTypeId(),
3760      MapH: _mapH,
3761      MapW: _mapW
3762    };
3763  }
3764
3765  this.onChangingView = function () {
3766    $(_mapContainer).trigger("mapchanging", null);
3767  };
3768
3769  this.onChangeView = function () {
3770    if (_map == null) return;
3771
3772    var mapMoveOrigin = _mapMoveOrigin;
3773    _mapMoveOrigin = null;
3774
3775    hideAllDialogs();
3776
3777    if (mapMoveOrigin === "Resize") {
3778      return;
3779    }
3780
3781    var oldMapState = _mapState;
3782    var center = _map.getCenter();
3783    var rect = _map.getBounds();
3784    _mapState = {
3785      Lat: center.latitude,
3786      Lon: center.longitude,
3787      NELat: rect.getNorth(),
3788      NELon: rect.getEast(),
3789      SWLat: rect.getSouth(),
3790      SWLon: rect.getWest(),
3791      Zoom: _map.getZoom(),
3792      Style: _map.getMapTypeId(),
3793      MapW: $(_mapContainer).outerWidth(),
3794      MapH: $(_mapContainer).outerHeight()
3795    };
3796
3797    _loaded = true;
3798    $(_mapContainer).trigger("mapchange", [mapMoveOrigin]);
3799
3800    if (typeof _mapMoveCallback === "function" && _mapMoveCallback != null) {
3801      _mapMoveCallback();
3802      _mapMoveCallback = null;
3803    }
3804
3805    // Update POI
3806    if (mapMoveOrigin !== "SetMapView" && options.poiEnabled) {
3807      updatePoi(true);
3808    }
3809    // Update office pins
3810    if (mapMoveOrigin !== "SetMapView" && options.getOfficePins) {
3811      parent.updateOffice();
3812    }
3813
3814    // Update Valuation pins
3815    if (typeof (options.isSM3) !== 'undefined' && options.isSM3) {
3816      updateValuations();
3817    }
3818  };
3819
3820  this.isLoaded = function () {
3821    return _loaded;
3822  };
3823
3824  function enablePricePins() {
3825    return (typeof (options.enablePricePins) !== 'undefined' && options.enablePricePins);
3826  }
3827
3828  function enableMyNeighborhoodPins() {
3829    return (typeof (options.enableMyNeighborhoodPins) !== 'undefined' && options.enableMyNeighborhoodPins);
3830  }
3831
3832  function isLayerNameForPricePins(layerName) {
3833    var layerNamesForPricePins = [
3834      'Property',
3835      'Open House Available',
3836      'Sale Pending',
3837      'Sold Property',
3838      'New Property',
3839      'Commercial Property',
3840      'Commercial Sale Pending',
3841      'Sold Commercial',
3842      'Coming Soon',
3843      'Auction',
3844      'Multi Listing',
3845      'Rent'
3846    ];
3847
3848    return layerNamesForPricePins.indexOf(layerName) > -1;
3849  }
3850
3851  function isSupportedHtmlPushpin(layerName, layerType) {
3852    return (typeof layerType === 'undefined' || layerType !== LayerType.Cluster) &&
3853      (enablePricePins() && isLayerNameForPricePins(layerName) || enableMyNeighborhoodPins());
3854  }
3855
3856  this.resize = function (w, h) {
3857    if (typeof h === "undefined") {
3858      h = _mapH;
3859    }
3860
3861    if (w === 0 || h === 0) {
3862      return;
3863    }
3864
3865    if (w !== _mapW || h !== _mapH) {
3866      _mapW = w;
3867      _mapH = h;
3868
3869      var currCenter = _map.getCenter();
3870      // This will cause the first move to not search
3871      // The second one will
3872      _mapMoveOrigin = "Resize";
3873      $(_mapContainer).width(w);
3874      $(_mapContainer).height(h);
3875      _map.setView({ center: currCenter });
3876    }
3877  };
3878
3879  this.dispose = function () {
3880    if (_map) {
3881      try {
3882        _map.dispose();
3883      } catch (e) {
3884        // Just ignore any errors on dispose
3885      }
3886    }
3887    _loaded = false;
3888    $(document).off('click', checkClick);
3889  };
3890
3891  this.setMapExtent = function (boundingBox) {
3892    if (!boundingBox) {
3893      Utils.Logger.warn("Bounding box is null");
3894      return;
3895    }
3896    // If this is getting called from a single pin being centered, leave the 
3897    // _mapMoveOrigin alone
3898    if (_mapMoveOrigin !== "SetMapView") {
3899      _mapMoveOrigin = "SetCenterAndZoom";
3900    }
3901
3902    var rect = MM.LocationRect.fromEdges(boundingBox.NeLat,
3903      boundingBox.SwLon,
3904      boundingBox.SwLat,
3905      boundingBox.NeLon);
3906    _map.setView({ bounds: rect, padding: 35 });
3907
3908    // Sometimes the map change doesn't fire an event
3909    setTimeout(function () {
3910      if (_mapMoveOrigin === "SetCenterAndZoom") {
3911        _mapMoveOrigin = null;
3912        _self.onChangeView();
3913      }
3914    }, 500);
3915  }
3916
3917  this.setCenterAndZoom = function (lat, lon, zoom) {
3918    // If this is getting called from a single pin being centered, leave the 
3919    // _mapMoveOrigin alone
3920    if (_mapMoveOrigin !== "SetMapView") {
3921      _mapMoveOrigin = "SetCenterAndZoom";
3922    }
3923    _map.setView({
3924      zoom: zoom,
3925      center: new MM.Location(lat, lon)
3926    });
3927    // Sometimes the map change doesn't fire an event
3928    setTimeout(function () {
3929      if (_mapMoveOrigin === "SetCenterAndZoom") {
3930        _mapMoveOrigin = null;
3931        _self.onChangeView();
3932      }
3933    }, 500);
3934  };
3935
3936  this.setZoomLevel = function (zoom) {
3937    var range = _map.getZoomRange();
3938    if (zoom >= range.min && zoom <= range.max) {
3939      _map.setView({ zoom: zoom });
3940    }
3941  };
3942
3943  this.setView = function (view) {
3944    var oldView = _map.getMapTypeId();
3945    var newView = getMapTypeId(view);
3946    if (newView !== oldView) {
3947      // Set the view
3948      if (newView === "be") {
3949        switchToBirdseye();
3950      } else {
3951        _map.setMapType(getMapTypeId(newView));
3952      }
3953      // Update the menu selector
3954      // updateMapTypeMenu(newView);
3955    }
3956    if (_mapState) {
3957      _mapState.Style = newView;
3958    }
3959  };
3960
3961  this.getMapState = function () {
3962    return _mapState;
3963  };
3964
3965  // rather than creating a single layer per the requested layerName,
3966  // we create a "collection" linking all the pins to be added and
3967  // maintaining the actual layers to which they are linked
3968  this.startAddingPins = function (layerName, cluster) {
3969    var collection = getLayerCollection(layerName);
3970    clearCollectionLayer(collection);
3971    collection.cluster = cluster;
3972  }
3973
3974  this.addPin = function (layerName, id, pinLat, pinLong, pinType, pinTitle, pinDesc, pinPrice, pinIsFavorite, pinsInCluster, northLat, southLat, westLon, eastLon, avgPrice, minPrice, maxPrice, pinImageUrl, isVisible = true) {
3975    var collection = getLayerCollection(layerName);
3976    var clusterPin = typeof northLat !== 'undefined';
3977    layerName = clusterPin ? 'ClusterPins' : translatePinTypeToLayerName(pinType, layerName);
3978    var pinLayer = getLayer(layerName, collection.cluster);
3979    var pinClass = "";
3980
3981    var pin = {
3982      id: id,
3983      latitude: pinLat,
3984      longitude: pinLong,
3985      pinType: pinType,
3986      pinClass: pinClass,
3987      pinTitle: pinTitle,
3988      pinDesc: pinDesc,
3989      pinPrice: pinPrice,
3990      pinIsFavorite: pinIsFavorite,
3991      pinsInCluster: pinsInCluster,
3992      pinImageUrl: pinImageUrl,
3993      isVisible: isVisible
3994    };
3995
3996    // if it's a cluster, add the additional data
3997    if (clusterPin) {
3998      pin.northLat = northLat;
3999      pin.southLat = southLat;
4000      pin.westLon = westLon;
4001      pin.eastLon = eastLon;
4002      pin.avgPrice = avgPrice;
4003      pin.minPrice = minPrice;
4004      pin.maxPrice = maxPrice;
4005      // Must come after the prices are set
4006      pin.pinDesc = getBubbleHtmlCluster(pin);
4007    }
4008
4009    pinLayer.pins.push(pin);
4010
4011    _pinIdLayers[id] = pinLayer;
4012
4013    collection.pins.push(pin);
4014    collection.pinIds[id] = pin;
4015
4016    if (typeof collection.layers[layerName] === "undefined")
4017      collection.layers[layerName] = pinLayer;
4018  }
4019
4020  this.endAddingPins = function (layerName, zoomMapToThisPinCollection, includeShapesInZoom) {
4021    var collection = getLayerCollection(layerName);
4022
4023    Object.keys(collection.layers).forEach(function (innerLayerName) {
4024      var pinLayer = collection.layers[innerLayerName];
4025      try {
4026        var useCustomClustering =
4027          (layerName === 'Properties' || (pinLayer.cluster && !zoomMapToThisPinCollection)) &&
4028          pinLayer.name !== 'ClusterPins' &&
4029          !isSupportedHtmlPushpin(pinLayer.name);
4030
4031        if (useCustomClustering) {
4032          pinLayer.pins = getClusteredPins(pinLayer.pins);
4033        }
4034
4035        if (pinLayer.pins.length > 0) {
4036          if (pinLayer.name === 'ClusterPins') {
4037            displayPinsWithClusters(pinLayer);
4038          } else {
4039            displayPins(pinLayer);
4040          }
4041        }
4042      } catch (e) {
4043        Utils.Logger.warn("Error adding " + layerName + " pins to map: " + e.message);
4044      }
4045    });
4046
4047    if (zoomMapToThisPinCollection) {
4048      // <strike>cannot fit map to pins on any given individual layer; pass null
4049      // so it will fit to all pins on all layers</strike>
4050      // Passing the pins since null created BROKER-2855
4051      if (typeof includeShapesInZoom === 'undefined') includeShapesInZoom = true;
4052      _self.fitMapToPins(collection.pins, includeShapesInZoom, true);
4053    }
4054
4055    // When pins are loaded for the first time, one of the MS Canvas overlay elements
4056    // have a zIndex that is greater than the HTMLPushpins zIndex, until the mouse is zoomed into or resized.
4057    // This causes pin pointer events to not work. Set zindex of the canvas element to 0 as a workaround.
4058    var mapOverlayCanvas = $(".labelCanvas").prev("canvas");
4059    if(mapOverlayCanvas.length) {
4060      mapOverlayCanvas.css("zIndex", 0);
4061    }
4062  }
4063
4064  /**
4065   * This removes pins which are located at the same cordinates and adds a single multi pin
4066   * so that on hovering over use may see all of the available properties at that location
4067   * 
4068   * This should be called after adding all pins and before calling endAddingPins() methods.
4069   * 
4070   * Intended to use with 'Property' top layer.
4071   * @param {any} layerName
4072   */
4073  this.collocatePins = function (layerName) {
4074    /**
4075     * The evaluates two pins, whether or not
4076     * should they collapse to one multi pin.
4077     * @param {any} pin1 First pin,.
4078     * @param {any} pin2 Second pin.
4079     * @returns true, if give pin's cordinates should collapse to single multi property pin, otherwise, false.
4080     */
4081    let isSameLocation = function (pin1, pin2) {
4082      // Breaking down conditions in multiple statements for better understanding.
4083      if (!pin1 || !pin2)
4084        return false;
4085
4086      if (!pin1.longitude || !pin1.latitude)
4087        return false;
4088
4089      if (!pin2.longitude || !pin2.latitude)
4090        return false;
4091
4092      if (pin1.longitude === pin2.longitude && pin1.latitude == pin2.latitude)
4093        return true;
4094
4095      if ((pin1.longitude.toString().contains(pin2.longitude.toString()) || pin2.longitude.toString().contains(pin1.longitude.toString()))
4096        && (pin1.latitude.toString().contains(pin2.latitude.toString()) || pin2.latitude.toString().contains(pin1.latitude.toString())))
4097        return true;
4098
4099      return false;
4100    };
4101
4102    /**
4103     * Find and removes specified pin from the layer which contains it within layerCollection.
4104     * @param {any} layerCollection
4105     * @param {any} pinId
4106     * @param {any} pinType
4107     * @returns
4108     */
4109    let removePin = function (layerCollection, pinId, pinType) {
4110      let layerName = translatePinTypeToLayerName(pinType);
4111      let subLayer = layerCollection.layers[layerName];
4112
4113      if (typeof subLayer === 'undefined')
4114        return;
4115
4116      // Remove this item from the layer which contains this pin.
4117      let index;
4118      while ((index = subLayer.pins.findIndex(item => item.id === pinId)) >= 0) {
4119        subLayer.pins.splice(index, 1);
4120      }
4121
4122      if (typeof _pinIdLayers[pinId] !== 'undefined')
4123        delete _pinIdLayers[pinId];
4124    }
4125
4126    let multiPropertyLayerName = 'Multi Listing';
4127    let collection = getLayerCollection(layerName);
4128
4129    let pinLayer = getLayer(multiPropertyLayerName, false, LayerType.Pin, true);
4130    let matchingPins = [];
4131
4132    for (let i = 0; i < collection.pins.length; i++) {
4133      if (matchingPins.some(value => value.pins.find(w => w.id === collection.pins[i].id)))
4134        continue;
4135
4136      let match = collection.pins.filter(t =>
4137        //t.id !== collection.pins[i].id &&
4138        isSameLocation(t, collection.pins[i]));
4139
4140      if (match.length > 1) {
4141        match.forEach(v => {
4142          removePin(collection, v.id, v.pinType);
4143          _pinIdLayers[v.id] = pinLayer;
4144        });
4145
4146        matchingPins.push({
4147          longitude: collection.pins[i].longitude,
4148          latitude: collection.pins[i].latitude,
4149          ids: match.map(value => value.id),
4150          pins: match
4151        });
4152      }
4153    }
4154
4155    let pins = matchingPins.map(function (v) {
4156      let pin = {
4157        id: v.pins.map(value => value.id).join('-'),
4158        latitude: v.latitude,
4159        longitude: v.longitude,
4160        pinClass: '',
4161        pinImageUrl: null,
4162        pinDesc: null,
4163        pinIsFavorite: false,
4164        pinPrice: null,
4165        pinTitle: null,
4166        pinType: BingMap.PinType.ListingMultiple,
4167        pinInCluster: null
4168      };
4169      return pin;
4170    });
4171
4172    pinLayer.pins = pins;
4173
4174    if (typeof collection.layers[multiPropertyLayerName] === "undefined")
4175      collection.layers[multiPropertyLayerName] = pinLayer;
4176  }
4177
4178  this.setPropertiesPinCount = function (count) {
4179    _totalPropertiesPinCount = count;
4180    _enableClusteringForPricePins = _totalPropertiesPinCount > _maxPricePinsToRender;
4181  }
4182
4183  this.clearPolygon = function (polyName, change) {
4184    delete _shapes[polyName];
4185    if (_boundaryShapeLayerCount > 0) _boundaryShapeLayerCount--;
4186    clearLayer(getLayer(polyName, false));
4187    if (change) {
4188      _self.onChangeView();
4189    }
4190  };
4191
4192  this.clearPolygons = function (change) {
4193    for (var shape in _shapes) {
4194      if (_shapes.hasOwnProperty(shape)) {
4195        _self.clearPolygon(shape, false);
4196      }
4197    }
4198    if (change) {
4199      _self.onChangeView();
4200    }
4201  };
4202
4203  this.gotoPoly = function (polyName, polyType, polyValue, dontSetMapView, dontExecSearch) {
4204    if (typeof polyName === "undefined" || typeof polyType === "undefined" || polyName == null || polyType == null || polyName.length === 0 || polyType.length === 0) {
4205      return;
4206    }
4207    if (typeof polyValue === "undefined" || polyValue == null || polyValue.length === 0) {
4208      polyValue = polyName;
4209    }
4210    _addingShapes[polyName] = true;
4211    addShapes(getLayer(polyName, false, LayerType.Shape), polyType, polyValue, function (shapes) {
4212      _initialShapes[polyName] = true;
4213      if (!dontSetMapView) {
4214        _mapMoveOrigin = "GotoPoly";
4215        if (shapes && shapes.length > 0) {
4216          // position map over the drawn boundary. this will submit the search too, if the map moves
4217          var shapePoints = [];
4218          for (var i = 0; i < shapes.length; i++) {
4219            $.merge(shapePoints, shapes[i].getLocations());
4220          }
4221          setMapViewBoundaries(shapePoints);
4222        } else {
4223          gotoLocation(polyValue);
4224        }
4225        // Sometimes the map change doesn't fire an event
4226        setTimeout(function () {
4227          if (_mapMoveOrigin === "GotoPoly") {
4228            _mapMoveOrigin = null;
4229            $(_mapContainer).trigger("mapchange");
4230          }
4231        }, 500);
4232      } else {
4233        if (!dontExecSearch) {
4234          _mapMoveOrigin = "GotoPoly";
4235          _self.onChangeView();
4236        }
4237      }
4238      delete _addingShapes[polyName];
4239    });
4240  };
4241
4242  this.showListingBubble = function (bubbleId) {
4243
4244    var collection = getLayerCollection("Properties");
4245
4246    for (var innerLayerName in collection.layers) {
4247      // Get the clustered pin that includes this ID
4248      var pinLayer = getLayer(innerLayerName);
4249
4250      if (pinLayer.layerModule === LayerModuleType.ClusteredModule || pinLayer.name === "ClusterPins" || !pinLayer.isVisible) {
4251        continue; // not valid for Clustered modules
4252      }
4253
4254      var pinIndex = 0;
4255      var foundBubble = false;
4256      var pinToShow = undefined;
4257      var layerPins = pinLayer.layerModule === LayerModuleType.HtmlPushpinModule ? pinLayer.layer.getPushpins() : pinLayer.layer.getPrimitives();
4258      var layerCount = layerPins.length;
4259      for (var i = 0; i < layerCount; i++) {
4260        var pin = layerPins[i];
4261        var pinIdSplit = pin.id.split('-');
4262        for (var j = 0; j < pinIdSplit.length; j++) {
4263          if (pinIdSplit[j] === bubbleId) {
4264            pinToShow = pin;
4265            pinIndex = j;
4266            foundBubble = true;
4267            break;
4268          }
4269        }
4270        if (foundBubble) {
4271          break;
4272        }
4273      }
4274      try {
4275        // Setting the visible bubble
4276        if (typeof pinToShow !== "undefined") {
4277          _visibleBubblePosition = pinIndex;
4278          showBubble(pinToShow);
4279          break;
4280        }
4281      } catch (ex) {
4282        console.log(ex.message);
4283      }
4284    }
4285  }
4286  // Applicable to Listing bubbles and Valuation bubbles (the assessor info hover data)
4287  this.hideListingBubble = function () {
4288    if (typeof _infoBox === "undefined" || _infoBox == null) return;
4289    _infoBox.setOptions({ visible: false });
4290    _visibleBubblePosition = 0;
4291    $("div.map-menubar", _mapContainer).slideDown(250);
4292    $("div.map-menubar", _mapContainer).css("overflow", "visible");
4293    // Ensure zoom buttons are visible. Pick the correct #MapNavBar (i.e., not the one from Public Records overlay).
4294    $("#MapNavBar", "#mapsearch-mainmap").show();
4295  };
4296
4297  this.showAllPins = function (checkIsVisible) {
4298    for (var layerName in _layers) {
4299      if (_layers.hasOwnProperty(layerName)) {
4300        var layer = getLayer(layerName);
4301        if (layer.layerType === LayerType.Pin) {
4302          _self.showPins(layerName, checkIsVisible);
4303        }
4304      }
4305    }
4306  };
4307
4308  this.hideAllPins = function (setIsVisible) {
4309    for (var layerName in _layers) {
4310      if (_layers.hasOwnProperty(layerName)) {
4311        var layer = getLayer(layerName);
4312        if (layer.layerType === LayerType.Pin) {
4313          _self.hidePins(layerName, setIsVisible);
4314        }
4315      }
4316    }
4317  };
4318
4319  this.hidePins = function (layerName, setIsVisible) {
4320    setIsVisible = typeof setIsVisible === "undefined" || setIsVisible;
4321    var pinLayer = getLayer(layerName);
4322    if (pinLayer.isVisible && pinLayer.layer && typeof pinLayer.layer.setVisible === 'function') {
4323      pinLayer.layer.setVisible(false);
4324    }
4325    if (setIsVisible) {
4326      pinLayer.isVisible = false;
4327    }
4328  };
4329
4330  this.showPins = function (layerName, checkIsVisible) {
4331    var layer = getLayer(layerName);
4332    var show = (checkIsVisible && layer.isVisible) || (!checkIsVisible);
4333    if (show && layer.layer && typeof layer.layer.setVisible === 'function') {
4334      layer.layer.setVisible(true);
4335      layer.isVisible = true;
4336    }
4337  };
4338
4339  this.pinsAreVisible = function (layerName) {
4340    var pinLayer = getLayer(layerName);
4341    return pinLayer.isVisible;
4342  };
4343
4344  this.deleteAllPins = function (layerName) {
4345    var collection = getLayerCollection(layerName);
4346    clearCollectionLayer(collection);
4347  };
4348
4349  this.hidePOI = function (poiType) {
4350    _self.hidePins(poiType);
4351  };
4352
4353  this.showPOI = function (poiType) {
4354    var layer = getLayer(poiType, false);
4355    _self.showPins(poiType);
4356    if (layer.pins.length === 0) {
4357      updatePoi(false);
4358    }
4359  };
4360
4361  this.hideAllPOI = function (shouldHide) {
4362    if (shouldHide) $("ul.map-poi-list input[type='checkbox']").prop("checked", false);
4363    $.each(_layers, function (layerName, layer) {
4364      if (layer.layerType === LayerType.Poi) {
4365        _self.hidePins(layerName, shouldHide);
4366      }
4367    });
4368  }
4369
4370  this.showAllPOI = function () {
4371    var update = false;
4372    $("ul.map-poi-list input[type='checkbox']").prop("checked", true);
4373    $.each(_layers, function (layerName, layer) {
4374      if (layer.layerType === LayerType.Poi) {
4375        _self.showPins(layerName);
4376        if (layer.pins && layer.pins.length === 0) {
4377          update = true;
4378        }
4379      }
4380    });
4381    if (update) {
4382      updatePoi(false);
4383    }
4384  }
4385
4386  this.addAddressPin = function (address) {
4387    if (address.indexOf(", USA") === -1 && address.indexOf("mx") === -1)
4388      address += ", USA";
4389    gotoLocation(address, true);
4390  };
4391
4392  this.addLocationPin = function (latitude, longitude, address, zoom) {
4393    if (address.indexOf(", USA") === -1 && address.indexOf("mx") === -1)
4394      address += ", USA";
4395    _self.startAddingPins("Address", false);
4396    _self.addPin("Address", "1", latitude, longitude, BingMap.PinType.Address, "", address);
4397    _self.endAddingPins("Address", true);
4398    if (zoom) {
4399      _self.setZoomLevel(zoom);
4400    }
4401  };
4402
4403  this.plotComps = function (latLons) {
4404    var arrLatLon = latLons.split(",");
4405    var len = arrLatLon.length;
4406    var latLon, lat, lon;
4407    _self.startAddingPins("Comps", false);
4408    for (var i = 0; i < len; i++) {
4409      if (arrLatLon[i] === "") {
4410        continue;
4411      }
4412      latLon = arrLatLon[i].split(" ");
4413      lat = latLon[0];
4414      lon = latLon[1];
4415      if (lat !== "" && lon !== "" && lat !== "undefined" && lon !== "undefined") {
4416        _self.addPin("Comps", "comp_" + i, lat, lon, BingMap.PinType["listing_" + (i + 1)], "", "");
4417      }
4418    }
4419    _self.endAddingPins("Comps", true);
4420    _self.showPins("Comps");
4421  };
4422
4423  this.fitMapToPins = function (pins, includeShapes) {
4424    // Make sure the asynchronous shapes have all loaded.  Retry on delay
4425    if (_addingShapesTimer !== null) {
4426      clearTimeout(_addingShapesTimer);
4427      _addingShapesTimer = null;
4428    }
4429    if (includeShapes === true && Object.keys(_addingShapes).length > 0 && _addingShapesTryCount++ < 30) {
4430      _addingShapesTimer = setTimeout(function () {
4431        _self.fitMapToPins(pins, includeShapes);
4432      }, 50);
4433      return;
4434    }
4435    _addingShapesTryCount = 0;
4436    _mapMoveOrigin = "SetMapView";
4437
4438    // If no pins passed in, then use all current pins
4439    if (typeof pins === "undefined" || pins === null) {
4440      pins = [];
4441      for (var pinLayer in _layers) {
4442        if (_layers.hasOwnProperty(pinLayer)) {
4443          var layer = _layers[pinLayer];
4444          if (layer.isVisible && layer.layerType === LayerType.Pin) {
4445            pins = pins.concat(layer.pins);
4446          }
4447        }
4448      }
4449    }
4450
4451    // If we didn't get any pins, try to zoom to fit the current
4452    // state of the location criteria
4453    if (pins.length === 0) {
4454      if (includeShapes === true && Object.keys(_shapes).length > 0) {
4455        var shapePoints = [];
4456        for (var key in _shapes) {
4457          if (_shapes.hasOwnProperty(key)) {
4458            for (var shapeIdx = 0; shapeIdx < _shapes[key].length; shapeIdx++) {
4459              shapePoints = shapePoints.concat(_shapes[key][shapeIdx].getLocations());
4460            }
4461          }
4462        }
4463        setMapViewBoundaries(shapePoints);
4464      }
4465    } else {
4466      // Set the map view to the max of the pins and shapes
4467      var mapViewPins = pins.slice(0);
4468      if (includeShapes && Object.keys(_shapes).length > 0) {
4469        for (var s in _shapes) {
4470          if (_shapes.hasOwnProperty(s)) {
4471            for (var i = 0; i < _shapes[s].length; i++) {
4472              mapViewPins = mapViewPins.concat(_shapes[s][i].getLocations());
4473            }
4474          }
4475        }
4476      }
4477      setMapViewBoundaries(mapViewPins);
4478    }
4479  }
4480
4481  this.gotoLocation = function (loc, addPin, zoom) {
4482    gotoLocation(loc, addPin, zoom);
4483  }
4484
4485  this.setOptions = function (opts) {
4486    $.extend(true, options, opts);
4487
4488    showHideMenuItems($("ul.map-menuitems"));
4489  }
4490
4491  this.load = function () {
4492    return new Promise(function (resolve) {
4493      options = $.extend(true, {}, defaultOptions, options);
4494
4495      // create a unique instance id that gets appends to layer id so different instances of map does not overlap
4496      options.instanceId = new Date().valueOf();
4497
4498      if (typeof window.imageBase !== "undefined") {
4499        options.imageBase = window.imageBase;
4500      }
4501
4502      _mapContainer = document.getElementById(options.container);
4503      _mapW = $(_mapContainer).outerHeight();
4504      _mapH = $(_mapContainer).outerWidth();
4505
4506      // Load the base map 
4507      loadMap();
4508
4509      // initialize the Html Layers module
4510      MM.registerModule('HtmlPushpinLayerModule', '/include/js/mapsearch/HtmlPushpinLayerModule.js');
4511
4512      // load all modules
4513      MM.loadModule(['HtmlPushpinLayerModule', 'Microsoft.Maps.Clustering', 'Microsoft.Maps.SpatialMath'],
4514        function modulesLoaded() {
4515
4516          // Initialize the POI layers
4517          if (options.poiEnabled) {
4518            initializePoiLayers();
4519          }
4520
4521          // Initialize the DMP layers
4522          if (options.showParcelLines) {
4523            initializeDmpLayers();
4524            if (typeof (options.isSM3) !== 'undefined' && options.isSM3) {
4525              MM.Events.addHandler(_map, "mousemove", parcelHoverHandler);
4526            }
4527          }
4528
4529          // Load the navigation and events
4530          if (!options.fixedMap) {
4531            addMenuBar();
4532            addNavBar();
4533            addEvents();
4534          }
4535          // Load the initial shapes
4536          addInitialShapes();
4537
4538          // IF we're adding an initial pin, do that now
4539          if (options.centerPin) {
4540            var centerLat, centerLon;
4541            if (typeof options.startingMidLat !== "undefined") {
4542              centerLat = options.startingMidLat;
4543              centerLon = options.startingMidLong;
4544            } else {
4545              var center = _map.getCenter();
4546              centerLat = center.latitude;
4547              centerLon = center.longitude;
4548            }
4549            
4550            var pinType = options.pinType || 1;
4551
4552            _self.startAddingPins("Center", false);
4553            _self.addPin("Center", "1", centerLat, centerLon, pinType);
4554            _self.endAddingPins("Center");
4555          }
4556
4557          // Fire off the initial load notification
4558          return resolve(firstOnChangeView());
4559        });
4560    });
4561  }
4562}
4563
4564$.event.special.widthChanged = {
4565  remove: function () {
4566    $(this).children("iframe.width-changed").remove();
4567  },
4568  add: function () {
4569    var elm = $(this);
4570    var iframe = elm.children("iframe.width-changed");
4571    if (!iframe.length) {
4572      iframe = $("<iframe/>").addClass("width-changed").attr("role","none").prependTo(this);
4573    }
4574    var oldWidth = elm.width();
4575    const threshold = 20;
4576    function elmResized() {
4577      var width = elm.width();
4578      if (Math.abs(oldWidth - width) >= threshold) {
4579        elm.trigger("widthChanged", [width, oldWidth]);
4580        oldWidth = width;
4581      }
4582    }
4583
4584    var timer = 0;
4585    var ielm = iframe[0];
4586    (ielm.contentWindow || ielm).onresize = function () {
4587      clearTimeout(timer);
4588      timer = setTimeout(elmResized, 20);
4589    };
4590  }
4591};

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.