/** * @file * GIcon manager for GMap. * Required for markers to operate properly. */ /*global jQuery, Drupal, GIcon, GPoint, GSize, G_DEFAULT_ICON */ /** * Get the GIcon corresponding to a setname / sequence. * There is only one GIcon for each slot in the sequence. * The marker set wraps around when reaching the end of the sequence. * @@@ TODO: Move this directly into the preparemarker event binding. */ Drupal.gmap.getIcon = function (setname, sequence) { var othimg = ['printImage', 'mozPrintImage', 'printShadow', 'transparent']; // If no setname, return google's default icon. if (!setname) { return; } if (!this.gicons) { this.gicons = {}; } if (!this.gshadows) { this.gshadows = {}; } // If no sequence, synthesise one. if (!sequence) { // @TODO make this per-map. if (!this.sequences) { this.sequences = {}; } if (!this.sequences[setname]) { this.sequences[setname] = -1; } this.sequences[setname]++; sequence = this.sequences[setname]; } if (!this.gicons[setname]) { if (!Drupal.gmap.icons[setname]) { alert('Request for invalid marker set ' + setname + '!'); } this.gicons[setname] = []; this.gshadows[setname] = []; var q = Drupal.gmap.icons[setname]; var p, t; for (var i = 0; i < q.sequence.length; i++) { /* t = new GIcon(); p = q.path; t.image = p + q.sequence[i].f; if (q.shadow.f !== '') { t.shadow = p + q.shadow.f; t.shadowSize = new GSize(q.shadow.w, q.shadow.h); } t.iconSize = new GSize(q.sequence[i].w, q.sequence[i].h); t.iconAnchor = new GPoint(q.anchorX, q.anchorY); t.infoWindowAnchor = new GPoint(q.infoX, q.infoY); */ p = Drupal.settings.basePath + q.path; t = new google.maps.MarkerImage(p + q.sequence[i].f, new google.maps.Size(q.sequence[i].w, q.sequence[i].h), null, new google.maps.Point(q.anchorX, q.anchorY) ); if (q.shadow.f !== '') { this.gshadows[setname][i] = new google.maps.MarkerImage(p + q.shadow.f, new google.maps.Size(q.shadow.w, q.shadow.h), null, new google.maps.Point(q.anchorX, q.anchorY) ); } else { this.gshadows[setname][i] = null; } for (var j = 0; j < othimg.length; j++) { if (q[othimg[j]] !== '') { t[othimg[j]] = p + q[othimg[j]]; } } // @@@ imageMap? this.gicons[setname][i] = t; } delete Drupal.gmap.icons[setname]; } // TODO: Random, other cycle methods. return this.gicons[setname][sequence % this.gicons[setname].length]; }; Drupal.gmap.getShadow = function (setname, sequence) { if (this.gshadows) return this.gshadows[setname][sequence % this.gicons[setname].length]; }; /** * JSON callback to set up the icon defs. * When doing the JSON call, the data comes back in a packed format. * We need to expand it and file it away in a more useful format. */ Drupal.gmap.iconSetup = function () { Drupal.gmap.icons = {}; var m = Drupal.gmap.icondata; var filef, filew, fileh, files; for (var path in m) { if (m.hasOwnProperty(path)) { // Reconstitute files array filef = m[path].f; filew = Drupal.gmap.expandArray(m[path].w, filef.length); fileh = Drupal.gmap.expandArray(m[path].h, filef.length); files = []; for (var i = 0; i < filef.length; i++) { files[i] = {f: filef[i], w: filew[i], h: fileh[i]}; } for (var ini in m[path].i) { if (m[path].i.hasOwnProperty(ini)) { jQuery.extend(Drupal.gmap.icons, Drupal.gmap.expandIconDef(m[path].i[ini], path, files)); } } } } }; /** * Expand a compressed array. * This will pad arr up to len using the last value of the old array. */ Drupal.gmap.expandArray = function (arr, len) { var d = arr[0]; for (var i = 0; i < len; i++) { if (!arr[i]) { arr[i] = d; } else { d = arr[i]; } } return arr; }; /** * Expand icon definition. * This helper function is the reverse of the packer function found in * gmap_markerinfo.inc. */ Drupal.gmap.expandIconDef = function (c, path, files) { var decomp = ['key', 'name', 'sequence', 'anchorX', 'anchorY', 'infoX', 'infoY', 'shadow', 'printImage', 'mozPrintImage', 'printShadow', 'transparent']; var fallback = ['', '', [], 0, 0, 0, 0, {f: '', h: 0, w: 0}, '', '', '', '']; var imagerep = ['shadow', 'printImage', 'mozPrintImage', 'printShadow', 'transparent']; var defaults = {}; var sets = []; var i, j; // Part 1: Defaults / Markersets // Expand arrays and fill in missing ones with fallbacks for (i = 0; i < decomp.length; i++) { if (!c[0][i]) { c[0][i] = [ fallback[i] ]; } c[0][i] = Drupal.gmap.expandArray(c[0][i], c[0][0].length); } for (i = 0; i < c[0][0].length; i++) { for (j = 0; j < decomp.length; j++) { if (i === 0) { defaults[decomp[j]] = c[0][j][i]; } else { if (!sets[i - 1]) { sets[i - 1] = {}; } sets[i - 1][decomp[j]] = c[0][j][i]; } } } for (i = 0; i < sets.length; i++) { for (j = 0; j < decomp.length; j++) { if (sets[i][decomp[j]] === fallback[j]) { sets[i][decomp[j]] = defaults[decomp[j]]; } } } var icons = {}; for (i = 0; i < sets.length; i++) { var key = sets[i].key; icons[key] = sets[i]; icons[key].path = path; delete icons[key].key; delete sets[i]; for (j = 0; j < icons[key].sequence.length; j++) { icons[key].sequence[j] = files[icons[key].sequence[j]]; } for (j = 0; j < imagerep.length; j++) { if (typeof(icons[key][imagerep[j]]) === 'number') { icons[key][imagerep[j]] = files[icons[key][imagerep[j]]]; } } } return icons; }; /** * We attach ourselves if we find a map somewhere needing markers. * Note: Since we broadcast our ready event to all maps, it doesn't * matter which one we attached to! */ Drupal.gmap.addHandler('gmap', function (elem) { var obj = this; obj.bind('init', function () { // Only expand once. if (!Drupal.gmap.icons) { Drupal.gmap.iconSetup(); } }); obj.bind('ready', function () { // Compatibility event. if (Drupal.gmap.icondata) { obj.deferChange('iconsready', -1); } }); // Provide icons to markers. obj.bind('preparemarker', function (marker) { if (!obj.vars.behavior.customicons || (marker.markername && !marker.opts.icon)) { marker.opts.icon = Drupal.gmap.getIcon(marker.markername, marker.offset); marker.opts.shadow = Drupal.gmap.getShadow(marker.markername, marker.offset); } }); }); ; /** * @file * Common marker routines. */ /*global jQuery, Drupal, GEvent, GInfoWindowTab, GLatLng, GLatLngBounds */ Drupal.gmap.addHandler('gmap', function (elem) { var obj = this; var infowindow; if (obj.vars.styleBubble && obj.vars.styleBubble.enableBubbleStyle == 1) { infowindow = new InfoBubble(obj.vars.styleBubble.styleBubbleOptions); } else { infowindow = new google.maps.InfoWindow(); } obj.bind('init', function () { if (obj.vars.behavior.autozoom) { obj.bounds = new google.maps.LatLngBounds(); } }); obj.bind('addmarker', function (marker) { marker.opts.position = new google.maps.LatLng(marker.latitude, marker.longitude); marker.opts.map = obj.map; var m = Drupal.gmap.factory.marker(marker.opts); marker.marker = m; google.maps.event.addListener(m, 'click', function () { obj.change('clickmarker', -1, marker); }); if (obj.vars.behavior.extramarkerevents) { google.maps.event.addListener(m, 'mouseover', function () { obj.change('mouseovermarker', -1, marker); }); google.maps.event.addListener(m, 'mouseout', function () { obj.change('mouseoutmarker', -1, marker); }); google.maps.event.addListener(m, 'dblclick', function () { obj.change('dblclickmarker', -1, marker); }); } /** * Perform a synthetic marker click on this marker on load. */ if (marker.autoclick || (marker.options && marker.options.autoclick)) { obj.deferChange('clickmarker', -1, marker); } if (obj.vars.behavior.autozoom) { obj.bounds.extend(new google.maps.LatLng(marker.latitude, marker.longitude)); } }); // Default marker actions. obj.bind('clickmarker', function (marker) { // Close infowindow if open to prevent multiple windows if (infowindow !== null) { infowindow.close(); } if (marker.text) { infowindow.setContent(marker.text); infowindow.open(obj.map, marker.marker); } // Info Window Query / Info Window Offset else if (marker.iwq || (obj.vars.iwq && typeof marker.iwo != 'undefined')) { var iwq, iwo; if (obj.vars.iwq) { iwq = obj.vars.iwq; } if (marker.iwq) { iwq = marker.iwq; } iwo = 0; if (marker.iwo) { iwo = marker.iwo; } // Create a container to store the cloned DOM elements. var el = document.createElement('div'); // Clone the matched object, run through the clone, stripping off ids, and move the clone into the container. jQuery(iwq).eq(iwo).clone(false).find('*').removeAttr('id').appendTo(jQuery(el)); infowindow.setContent(el); infowindow.open(obj.map, marker.marker); } // AJAX content else if (marker.rmt) { //Immediately add a 'loading' bubble on click while we wait for AJAX infowindow.setContent('
Loading
'); infowindow.open(obj.map, marker.marker); var uri = marker.rmt; // If there was a callback, prefix that. // (If there wasn't, marker.rmt was the FULL path.) if (obj.vars.rmtcallback) { uri = Drupal.settings.basePath + Drupal.settings.pathPrefix + obj.vars.rmtcallback + '/' + marker.rmt; } // @Bevan: I think it makes more sense to do it in this order. // @Bevan: I don't like your choice of variable btw, seems to me like // @Bevan: it belongs in the map object, or at *least* somewhere in // @Bevan: the gmap settings proper... //if (!marker.text && Drupal.settings.loadingImage) { // marker.marker.openInfoWindowHtml(Drupal.settings.loadingImage); //} jQuery.get(uri, {}, function (data) { infowindow.setContent(data); infowindow.open(obj.map, marker.marker); }); } // Tabbed content else if (marker.tabs) { var data = ""; //tabs in an infowindow is no longer supported in API ver3. for (var m in marker.tabs) { data += marker.tabs[m]; } infowindow.setContent(data); infowindow.open(obj.map, marker.marker); } // No content -- marker is a link else if (marker.link) { open(marker.link, '_self'); } }); obj.bind('markersready', function () { // If we are autozooming, set the map center at this time. if (obj.vars.behavior.autozoom) { if (!obj.bounds.isEmpty()) { obj.map.fitBounds(obj.bounds); var listener = google.maps.event.addListener(obj.map, "idle", function () { if (obj.vars.maxzoom) { var maxzoom = parseInt(obj.vars.maxzoom); if (obj.map.getZoom() > maxzoom) obj.map.setZoom(maxzoom); google.maps.event.removeListener(listener); } }); } } }); obj.bind('clearmarkers', function () { // Reset bounds if autozooming // @@@ Perhaps we should have a bounds for both markers and shapes? if (obj.vars.behavior.autozoom) { obj.bounds = new google.maps.LatLngBounds(); } }); Drupal.gmap.getInfoWindow = function () { return infowindow; }; // @@@ TODO: Some sort of bounds handling for deletemarker? We'd have to walk the whole thing to figure out the new bounds... }); ; /** * @file * Common marker highlighting routines. */ /** * Highlights marker on rollover. * Removes highlight on previous marker. * * Creates a "circle" at the given point * Circle is global variable as there is only one highlighted marker at a time * and we want to remove the previously placed polygon before placing a new one. * * Original code "Google Maps JavaScript API Example" * JN201304: * converted rpolygons to circles (not using the shapes.js API, should we be?) * move marker highlight events to custom handler here, to handle radius in pixels (note behavior.radiusInMeters to skip geodesic calcs) * removed google.events and moved events to gmaps binds * added overlay object for creating a shape based on pixels instead of meters (seems to be the use case?) * added gmaps binds for marker higlights, and general highlights. * JN201305 refactored to use a single overlay. move functions from draw method to solve zoom problem, and multiple * highlights problem. * * You can add highlights to a map with: * obj.change('highlightAdd',-1, {latitude:#, longitude:#} ); * You can highlight a marker with: * obj.change('markerHighlight',-1, marker); * marker: that marker object used when creating the marker. It can have options set at marker.highlight * * A Highlight object has to have either a Position or a latitude and longitude * Note the new highlight options = { * radius: 10, // radius in pixels * color: '#777777', * weight: 2, * opacity: 0.7, * behavior: { * draggable: false, * editable: false, * } * opts: { actual google.maps.Circle opts can be put here for super custom cases } * } */ Drupal.gmap.factory.highlight = function (options) { /** @note it could be argued that we use the shapes library to create a circle, * but this requires the shapes library be loaded and it would make all highlights * repond to shapes events. */ return new google.maps.Circle(options); }; Drupal.gmap.addHandler('gmap', function (elem) { var obj = this; obj.highlights = {}; /** * This is a single overlay that can hold multiple highlight. * All highlight shapes will be creted in this overlay, and use * it to translate pixel dimensions to meters. */ var highlightOverlay = function () { this.highlights = []; // this will hold all of the highlights that we created, in case we need to recalculate/deactivate them }; highlightOverlay.prototype = new google.maps.OverlayView(); // overlay method for when you .setMap( some map ); highlightOverlay.prototype.onAdd = function (map) { }; // overlay method for when you .setMap(null); highlightOverlay.prototype.onRemove = function () { // we have to recalculate radii for all shapes var self = this; jQuery.each(this.highlights, function (index, highlight) { if (highlight.shape.getMap()) { // don't calculate if we don't have a map. self.calculateHighlight(highlight); //recalculate all of those radii } }); }; // overlay method executed on any map change methods (zoom/move) highlightOverlay.prototype.draw = function () { // we have to recalculate radii for all shapes var self = this; jQuery.each(this.highlights, function (index, highlight) { if (highlight.shape.getMap()) { // don't calculate if we don't have a map. self.deactivateHighlight(highlight); //recalculate all of those radii } }); }; highlightOverlay.prototype.configHighlight = function (highlight) { if (!highlight.opts) { highlight.opts = {}; } // sanity if (!highlight.behavior) { highlight.behavior = {}; } // sanity if (!highlight.position) { highlight.position = new google.maps.LatLng(highlight.latitude, highlight.longitude); } // if you have a pos already then use it, otherwise gimme a lat/lon jQuery.each({ // collect the options from either the highlight.opts object, from the passed target value, as a behavior or a defaultVal value. radius: {target: 'radius', defaultVal: 10}, // radius in pixels strokeColor: {target: 'border', defaultVal: '#777777'}, strokeWeight: {target: 'weight', defaultVal: 2}, strokeOpacity: {target: 'opacity', defaultVal: 0.7}, fillColor: {target: 'color', defaultVal: '#777777'}, fillOpacity: {target: 'opacity', defaultVal: 0.7}, draggable: {behavior: 'draggable', defaultVal: false}, editable: {behavior: 'editable', defaultVal: false} }, function (key, config) { if (highlight.opts[key]) { // options was passed in return true; } else if (config.target && highlight[ config.target ]) { // highight[target] can give us a setting highlight.opts[key] = highlight[ config.target ]; } else if (config.behavior && highlight.behavior && highlight.behavior[ config.behavior ]) { // value is a behaviour, should it be enabled? highlight.opts[key] = highlight.behavior[ config.behavior ]; } else if (config.defaultVal) { // defaultVal value highlight.opts[key] = config.defaultVal; } }); highlight.opts.center = highlight.position; // note that there is no opts.map, unless you passed one in. maybe we should make sure that you didn't? // add this highlight to our list, so that we can draw it in the draw method (which will also redraw it after map change events. this.highlights.push(highlight); }; // determine how big the circle should be in meters (as we were likely passed pixels). This radius changes on zoom and move events. highlightOverlay.prototype.calculateHighlight = function (highlight) { // this nees a better name if (highlight.behavior.radiusInMeters) { highligh.opts.radiusInMeters = highlight.opts.radius; } else { var mapZoom = this.map.getZoom(); var projection = this.getProjection(); var center = projection.fromLatLngToDivPixel(highlight.opts.center, mapZoom); var radius = highlight.opts.radius; var radial = projection.fromDivPixelToLatLng(new google.maps.Point(center.x, center.y + radius), mapZoom); // find a point that is the radius distance away in pixels from the ccenter point. highlight.opts.radiusInMeters = google.maps.geometry.spherical.computeDistanceBetween(highlight.opts.center, radial); } if (highlight.shape) { highlight.shape.setOptions(highlight.opts); // we can use this if we don't care about other options changing : highlight.shape.setRadius(highlight.opts.radiusInMeters) } else { highlight.shape = Drupal.gmap.factory.highlight(jQuery.extend({}, highlight.opts, {radius: highlight.opts.radiusInMeters})); // only pass radiusinmeters to g.m.circle. We keep the pixel radius in case we need to calculate again after a zoom } }; highlightOverlay.prototype.activateHighlight = function (highlight) { if (!highlight.shape) { this.configHighlight(highlight); this.calculateHighlight(highlight); } highlight.shape.setMap(this.map); }; highlightOverlay.prototype.deactivateHighlight = function (highlight) { if (highlight.shape) { highlight.shape.setMap(null); } }; highlightOverlay.prototype.updateHighlight = function (highlight) { if (highlight.shape) { this.configHighlight(highlight); this.calculateHighlight(highlight); } }; // prepare a single highlight overlay to be used for all highlights obj.bind('init', function (highlight) { obj.highlightOverlay = new highlightOverlay(obj.map); obj.highlightOverlay.setMap(obj.map); // this will trigger the onAdd() method, and the first draw() }); // set and remove map highlights obj.bind('highlightAdd', function (highlight) { // if you activate an activated highlight, nothing happens. obj.highlightOverlay.activateHighlight(highlight); }); obj.bind('highlightRemove', function (highlight) { obj.highlightOverlay.deactivateHighlight(highlight); }); obj.bind('highlightUpdate', function (highlight) { obj.highlightOverlay.updateHighlight(highlight); }); // Marker specific highlight events: var highlightedMarkers = []; // remember markers that have been highlighted. so that we can un-highlight them all at one. The defaultVal behaviour is to allow only 1 marker highlighted at any time. obj.bind('markerHighlight', function (marker) { highlightedMarkers.push(marker); // If the highlight arg option is used in views highlight the marker. if (!marker.highlight) { marker.highlight = {}; } if (!marker.highlight.color && obj.vars.styles.highlight_color) { marker.highlight.color = '#' + obj.vars.styles.highlight_color; } marker.highlight.position = marker.marker.getPosition(); obj.change('highlightAdd', -1, marker.highlight); }); obj.bind('markerUnHighlight', function (marker) { if (marker.highlight) { obj.change('highlightRemove', -1, marker.highlight); delete marker.highlight; } }); obj.bind('markerUnHighlightActive', function () { var marker; while (!!(marker = highlightedMarkers.pop())) { obj.change('highlightRemove', -1, marker); } }); /** * Marker Binds * * Marker highlight code has been moved to this file from the marker.js * * Note that we rely on the obj.vars.behavior.highlight var to * decide if should highlight markers on events. * This decision could be made as an outer if conditional, instead * of repeated inside each bind, but this arrangement allows for * the behaviour to change, at a small cost. */ obj.bind('addmarker', function (marker) { if (obj.vars.behavior.highlight) { google.maps.event.addListener(marker.marker, 'mouseover', function () { obj.change('markerHighlight', -1, marker); }); google.maps.event.addListener(marker.marker, 'mouseout', function () { obj.change('markerUnHighlight', -1, marker); }); google.maps.event.addListener(marker.marker, 'mouseout', function () { obj.change('markerUnHighlight', -1, marker); }); } // If the highlight arg option is used in views highlight the marker. if (marker.opts.highlight == 1) { obj.change('markerHighlight', -1, marker); } }); // Originally I moved mouse highlights to the extra event binds before I realized that there is likely a usecase for highlights without enabling extra events // obj.bind('mouseovermarker', function(marker) { // if (obj.vars.behavior.highlight && marker) { // obj.change('markerHighlight',-1,marker); // } // }); // obj.bind('mouseoutmarker', function(marker) { // if (obj.vars.behavior.highlight && marker) { // obj.change('markerUnHighlight',-1,marker); // } // }); }); ; /** * @file * GPolyLine / GPolygon manager */ /*global Drupal, GLatLng, GPoint */ Drupal.gmap.map.prototype.poly = {}; /** * Distance in pixels between 2 points. */ Drupal.gmap.map.prototype.poly.distance = function (point1, point2) { return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)); }; /** * Circle -- Following projection. */ Drupal.gmap.map.prototype.poly.computeCircle = function (obj, center, point2) { var numSides = 36; var sideInc = 10; // 360 / 20 = 18 degrees var convFactor = Math.PI / 180; var points = []; var radius = obj.poly.distance(center, point2); // 36 sided poly ~= circle for (var i = 0; i <= numSides; i++) { var rad = i * sideInc * convFactor; var x = center.x + radius * Math.cos(rad); var y = center.y + radius * Math.sin(rad); //points.push(obj.map.getCurrentMapType().getProjection().fromPixelToLatLng(new GPoint(x,y),obj.map.getZoom())); points.push(new GPoint(x, y)); } return points; }; Drupal.gmap.map.prototype.poly.calcPolyPoints = function (center, radM, numPoints, sAngle) { if (!numPoints) { numPoints = 32; } if (!sAngle) { sAngle = 0; } var d2r = Math.PI / 180.0; var r2d = 180.0 / Math.PI; var angleRad = sAngle * d2r; // earth semi major axis is about 6378137 m var latScale = radM / 6378137 * r2d; var lngScale = latScale / Math.cos(center.latRadians()); var angInc = 2 * Math.PI / numPoints; var points = []; for (var i = 0; i < numPoints; i++) { var lat = parseFloat(center.lat()) + latScale * Math.sin(angleRad); var lng = parseFloat(center.lng()) + lngScale * Math.cos(angleRad); points.push(new GLatLng(lat, lng)); angleRad += angInc; } // close the shape and return it points.push(points[0]); return points; }; ;