cordova.define("cordova-plugin-googlemaps.PluginMarkerCluster", function(require, exports, module) { var utils = require('cordova/utils'), event = require('cordova-plugin-googlemaps.event'), PluginMarker = require('cordova-plugin-googlemaps.PluginMarker'), Thread = require('cordova-plugin-googlemaps.Thread'), LatLng = require('cordova-plugin-googlemaps.LatLng'), BaseArrayClass = require('cordova-plugin-googlemaps.BaseArrayClass'); var STATUS = { 'WORKING': 0, 'CREATED': 1, 'DELETED': 2 }; function PluginMarkerCluster(pluginMap) { var self = this; PluginMarker.call(self, pluginMap); Object.defineProperty(self, 'pluginMarkers', { value: {}, writable: false }); Object.defineProperty(self, 'debugFlags', { value: {}, writable: false }); var deleteMarkers = new BaseArrayClass(); Object.defineProperty(self, 'deleteMarkers', { value: deleteMarkers, writable: false }); deleteMarkers.on('insert_at', function() { var key = deleteMarkers.removeAt(0); var marker = self.pluginMap.objects[key]; if (marker) { self._removeMarker(marker); } self.pluginMap.objects[key] = undefined; self.pluginMap.objects['marker_property_' + key] = undefined; self.pluginMarkers[key] = undefined; delete self.pluginMap.objects[key]; delete self.pluginMap.objects['marker_property_' + key]; delete self.pluginMarkers[key]; self.pluginMarkers[key] = STATUS.DELETED; }); } utils.extend(PluginMarkerCluster, PluginMarker); PluginMarkerCluster.prototype._create = function(onSuccess, onError, args) { var self = this, params = args[1], hashCode = args[2], positionList = params.positionList; //--------------------------------------------- // Calculate geocell hashCode on multi thread //--------------------------------------------- var tasks = []; while(positionList.length > 0) { var list = positionList.splice(0, 10); tasks.push(new Promise(function(resolve, reject) { var thread = new Thread(function(params) { var GEOCELL_GRID_SIZE = 4; var GEOCELL_ALPHABET = '0123456789abcdef'; function getGeocell(lat, lng, resolution) { var north = 90.0, south = -90.0, east = 180.0, west = -180.0, subcell_lng_span, subcell_lat_span, x, y, cell = []; while(cell.length < resolution + 1) { subcell_lng_span = (east - west) / GEOCELL_GRID_SIZE; subcell_lat_span = (north - south) / GEOCELL_GRID_SIZE; x = Math.min(Math.floor(GEOCELL_GRID_SIZE * (lng - west) / (east - west)), GEOCELL_GRID_SIZE - 1); y = Math.min(Math.floor(GEOCELL_GRID_SIZE * (lat - south) / (north - south)), GEOCELL_GRID_SIZE - 1); cell.push(_subdiv_char(x, y)); south += subcell_lat_span * y; north = south + subcell_lat_span; west += subcell_lng_span * x; east = west + subcell_lng_span; } return cell.join(''); } function _subdiv_char(posX, posY) { return GEOCELL_ALPHABET.charAt( (posY & 2) << 2 | (posX & 2) << 1 | (posY & 1) << 1 | (posX & 1) << 0); } return params.positionList.map(function(position) { return getGeocell(position.lat, position.lng, params.resolution); }); }); thread .once({ 'positionList': list, 'resolution': 12 }) .done(resolve) .fail(reject); })); } Promise.all(tasks) .then(function(results) { var id = 'markerclister_' + hashCode; self.debugFlags[id] = params.debug; var result = { 'geocellList': Array.prototype.concat.apply([], results), 'hashCode': hashCode, '__pgmId': id }; onSuccess(result); }) .catch(onError); }; PluginMarkerCluster.prototype.redrawClusters = function(onSuccess, onError, args) { var self = this; var updateClusterIDs = [], changeProperties = {}, clusterId = args[0], isDebug = self.debugFlags[clusterId], params = args[1], map = self.pluginMap.get('map'); if ('new_or_update' in params) { //--------------------------- // Determine new or update //--------------------------- params.new_or_update.forEach(function(clusterData) { var positionJSON = clusterData.position, markerId = clusterData.__pgmId, clusterId_markerId = clusterId + '-' + markerId; // Save the marker properties self.pluginMap.objects['marker_property_' + clusterId_markerId] = clusterData; // Set the WORKING status flag self.pluginMarkers[clusterId_markerId] = STATUS.WORKING; updateClusterIDs.push(clusterId_markerId); // Prepare the marker properties for addMarker() var properties = { 'lat': positionJSON.lat, 'lng': positionJSON.lng, '__pgmId': clusterId_markerId }; if ('title' in clusterData) { properties.title = clusterData.title; } if ('icon' in clusterData) { var iconObj = clusterData.icon, iconProperties = {}, label; if (typeof iconObj === 'string') { iconProperties.url = iconObj; properties.icon = iconProperties; } else if (typeof iconObj === 'object') { iconProperties = iconObj; if (clusterData.isClusterIcon) { if (iconObj.label) { label = iconObj.label; if (isDebug) { label.text = markerId; } else { label.text = clusterData.count; } } else { label = {}; if (isDebug) { label.text = markerId; } else { label.fontSize = 15; label.bold = true; label.text = clusterData.count; } } iconProperties.label = label; } if ('anchor' in iconObj) { iconProperties.anchor = iconObj.anchor; } if ('infoWindowAnchor' in iconObj) { iconProperties.anchor = iconObj.infoWindowAnchor; } properties.icon = iconProperties; } } changeProperties[clusterId_markerId] = JSON.parse(JSON.stringify(properties)); }); if (updateClusterIDs.length === 0) { self.deleteProcess(clusterId, params); onSuccess({}); return; } //--------------------------- // mapping markers on the map //--------------------------- var allResults = {}; //--------------- // new or update //--------------- var tasks = []; updateClusterIDs.forEach(function(clusterId_markerId) { self.pluginMarkers[clusterId_markerId] = STATUS.WORKING; var isNew = !(clusterId_markerId in self.pluginMap.objects); // Get the marker properties var markerProperties = changeProperties[clusterId_markerId], properties, marker; if (clusterId_markerId.indexOf('-marker_') > -1) { //------------------- // regular marker //------------------- if (isNew) { properties = self.pluginMap.objects['marker_property_' + clusterId_markerId]; tasks.push(new Promise(function(resolve) { self.__create.call(self, clusterId_markerId, { 'position': properties.position, 'icon': properties.icon, 'disableAutoPan': properties.disableAutoPan }, function(marker, properties) { if (markerProperties.title) { marker.set('title', markerProperties.title); } if (markerProperties.snippet) { marker.set('snippet', markerProperties.snippet); } if (self.pluginMarkers[clusterId_markerId] === STATUS.DELETED) { self._removeMarker.call(self, marker); delete self.pluginMarkers[clusterId_markerId]; resolve(); } else { self.pluginMarkers[clusterId_markerId] = STATUS.CREATED; allResults[clusterId_markerId.split('-')[1]] = { 'width': properties.width, 'height': properties.height }; resolve(); } }); })); } else { marker = self.pluginMap.objects[clusterId_markerId]; //---------------------------------------- // Set the title and snippet properties //---------------------------------------- if (markerProperties.title) { marker.set('title', markerProperties.title); } if (markerProperties.snippet) { marker.set('snippet', markerProperties.snippet); } if (self.pluginMarkers[clusterId_markerId] === STATUS.DELETED) { self._removeMarker.call(self, marker); delete self.pluginMarkers[clusterId_markerId]; } else { self.pluginMarkers[clusterId_markerId] = STATUS.CREATED; } } } else { //-------------------------- // cluster icon //-------------------------- if (isNew) { // If the requested id is new location, create a marker marker = newClusterIcon({ 'map': map, 'position': { 'lat': markerProperties.lat, 'lng': markerProperties.lng }, 'overlayId': clusterId_markerId, 'opacity': 0 }); marker.addListener('click', self.onClusterEvent.bind(self, 'cluster_click', marker)); // Store the marker instance with markerId self.pluginMap.objects[clusterId_markerId] = marker; } else { marker = self.pluginMap.objects[clusterId_markerId]; } //---------------------------------------- // Set the title and snippet properties //---------------------------------------- if (markerProperties.title) { marker.set('title', markerProperties.title); } if (markerProperties.snippet) { marker.set('snippet', markerProperties.snippet); } if (markerProperties.icon) { var icon = markerProperties.icon; tasks.push(new Promise(function(resolve) { self.setIconToClusterMarker.call(self, clusterId_markerId, marker, icon) .then(function() { //-------------------------------------- // Marker was updated //-------------------------------------- marker.setVisible(true); self.pluginMarkers[clusterId_markerId] = STATUS.CREATED; resolve(); }) .catch(function(error) { //-------------------------------------- // Could not read icon for some reason //-------------------------------------- if (marker.get('overlayId')) { self._removeMarker.call(self, marker); } self.pluginMarkers[clusterId_markerId] = STATUS.DELETED; console.warn(error.getMessage()); self.deleteMarkers.push(clusterId_markerId); resolve(); }); })); } else { //-------------------- // No icon for marker //-------------------- self.pluginMarkers[clusterId_markerId] = STATUS.CREATED; } } }); Promise.all(tasks).then(function() { self.deleteProcess(clusterId, params); onSuccess(allResults); }).catch(onError); } }; PluginMarkerCluster.prototype.deleteProcess = function(clusterId, params) { var self = this; if (!params.delete || params.delete.length === 0) { return; } params.delete.forEach(function(key) { self.deleteMarkers.push(clusterId + '-' + key); }); }; PluginMarkerCluster.prototype.setIconToClusterMarker = function(markerId, marker, iconProperty) { var self = this; return new Promise(function(resolve, reject) { if (self.pluginMarkers[markerId] === STATUS.DELETED) { self._removeMarker.call(self, marker); delete self.pluginMap.objects[markerId]; delete self.pluginMap.objects['marker_property_' + markerId]; delete self.pluginMarkers[markerId]; reject('marker has been removed'); return; } self.setIcon_.call(self, marker, iconProperty) .then(function() { if (self.pluginMarkers[markerId] === STATUS.DELETED) { self._removeMarker.call(self, marker); delete self.pluginMap.objects[markerId]; delete self.pluginMap.objects['marker_property_' + markerId]; self.pluginMarkers.remove(markerId); resolve(); return; } marker.setVisible(true); self.pluginMarkers[markerId] = STATUS.CREATED; resolve(); }); }); }; PluginMarkerCluster.prototype.remove = function(onSuccess, onError, args) { var self = this, clusterId = args[0], keys = Object.keys(self.pluginMarkers); keys.forEach(function(key) { if (key.indexOf(clusterId) === 0) { if (self.pluginMap.objects[key]) { self.pluginMap.objects[key].setMap(null); } self.pluginMarkers[key] = STATUS.DELETED; delete self.pluginMap.objects[key]; delete self.pluginMap.objects['marker_property_' + key]; } }); onSuccess(); }; PluginMarkerCluster.prototype.onClusterEvent = function(evtName, marker) { var self = this, mapId = self.pluginMap.__pgmId; var overlayId = marker.get('overlayId'); var tmp = overlayId.split('-'); var clusterId = tmp[0]; var markerId = tmp[1]; var latLng = marker.getPosition(); if (mapId in plugin.google.maps) { if (self.pluginMap.activeMarker) { self.onMarkerEvent(event.INFO_CLOSE, self.pluginMap.activeMarker); } plugin.google.maps[mapId]({ 'evtName': evtName, 'callback': '_onClusterEvent', 'args': [clusterId, markerId, new LatLng(latLng.lat(), latLng.lng())] }); } }; module.exports = PluginMarkerCluster; function newClusterIcon(options) { // https://github.com/apache/cordova-js/blob/c75e8059114255d1dbae1ede398e6626708ee9f3/src/common/utils.js#L167 if (ClusterIconClass.__super__ !== google.maps.OverlayView.prototype) { var key, defined = {}; for (key in ClusterIconClass.prototype) { defined[key] = ClusterIconClass.prototype[key]; } utils.extend(ClusterIconClass, google.maps.OverlayView); for (key in defined) { ClusterIconClass.prototype[key] = defined[key]; } } return new ClusterIconClass(options); } function ClusterIconClass(options) { google.maps.OverlayView.apply(this); var self = this; //----------------------------------------- // Create a canvas to draw label //----------------------------------------- var canvas = document.createElement('canvas'); canvas.width = 50; canvas.height = 50; canvas.style.visibility = 'hidden'; canvas.style.position = 'absolute'; canvas.style.zIndex = -100; canvas.setAttribute('id', 'canvas_' + options.overlayId); self.set('canvas', canvas); //----------------------------------------- // Create two markers for icon and label //----------------------------------------- var iconMarker = new google.maps.Marker({ 'clickable': false, 'icon': options.icon, 'zIndex': 0, 'opacity': 0 }); var labelMarker = new google.maps.Marker({ 'clickable': true, 'zIndex': 1, 'icon': self.get('label'), 'opacity': 0 }); labelMarker.addListener('click', function() { google.maps.event.trigger(self, 'click'); }); self.set('iconMarker', iconMarker); self.set('labelMarker', labelMarker); self.set('opacity', 0); iconMarker.bindTo('opacity', labelMarker); iconMarker.bindTo('visible', labelMarker); iconMarker.bindTo('position', labelMarker); iconMarker.bindTo('map', labelMarker); self.bindTo('opacity', iconMarker); self.bindTo('visible', iconMarker); self.bindTo('map', iconMarker); self.bindTo('position', iconMarker); self.set('labelMarkerAnchor', new google.maps.Point(canvas.width / 2, canvas.height / 2)); self.addListener('icon_changed', function() { var icon = self.get('icon'); if (typeof icon === 'string') { icon = { 'url': icon }; } var iconUrl = icon.url; if (typeof icon === 'object') { if (typeof icon.size === 'object' && icon.size.width && icon.size.height) { icon.anchor = new google.maps.Point(icon.size.width / 2, icon.size.height / 2); iconMarker.setIcon(icon); return; } } var img = new Image(); img.onload = function() { icon.size = new google.maps.Size(img.width, img.height); icon.scaledSize = new google.maps.Size(img.width, img.height); icon.anchor = new google.maps.Point(img.width / 2, img.height / 2); self.set('labelMarkerAnchor', new google.maps.Point(img.width / 2, img.height / 2)); iconMarker.setIcon(icon); }; img.onerror = function(e) { console.error(e); }; img.src = iconUrl; }); //debug //var positionConfirmMarker = new google.maps.Marker(); //labelMarker.bindTo('position', positionConfirmMarker); //labelMarker.bindTo('map', positionConfirmMarker); for (var key in options) { self.set(key, options[key]); } } ClusterIconClass.prototype.onAdd = function() { var self = this; var canvas = self.get('canvas'); self.get('map').getDiv().appendChild(canvas); }; ClusterIconClass.prototype.onRemove = function() { var self = this; var canvas = self.get('canvas'); self.set('map', null); google.maps.event.clearInstanceListeners(self.get('iconMarker')); google.maps.event.clearInstanceListeners(self.get('labelMarker')); var parent = canvas.parentNode; if (parent) { parent.removeChild(canvas); } canvas = undefined; }; ClusterIconClass.prototype.draw = function() { var self = this, icon = self.get('icon'); if (typeof icon === 'string') { icon = { 'url': icon }; } icon.label = icon.label || {}; if (self.get('prevText') === icon.label.text) { return; } self.set('prevText', icon.label.text); self.get('labelMarker').set('opacity', 0); (new Promise(function(resolve) { var iconUrl = icon.url; if (typeof icon === 'object') { if (typeof icon.size === 'object' && icon.size.width && icon.size.height) { return resolve(icon.size); } } var img = new Image(); img.onload = function() { var newIconInfo = { width: img.width, height: img.height }; icon.size = newIconInfo; self.set('labelMarkerAnchor', new google.maps.Point(newIconInfo.width / 2, newIconInfo.height / 2)); self.set('icon', icon, true); resolve(newIconInfo); }; img.onerror = function() { var newIconInfo = { width: img.width, height: img.height }; icon.size = newIconInfo; self.set('icon', icon, true); resolve(newIconInfo); }; img.src = iconUrl; })) .then(function(iconSize) { var canvas = self.get('canvas'), ctx = canvas.getContext('2d'); canvas.width = iconSize.width; canvas.height = iconSize.height; var labelOptions = icon.label || { fontSize: 10, bold: false, italic: false }; ctx.clearRect(0, 0, canvas.width, canvas.height); // debug //ctx.fillStyle="#FF000077"; //ctx.fillRect(0, 0, canvas.width, canvas.height); if (labelOptions.text) { var fontStyles = []; if ('color' in labelOptions) { ctx.fillStyle = [ 'rgba(', labelOptions.color[0], ',', labelOptions.color[1], ',', labelOptions.color[2], ',', labelOptions.color[3] / 255, ')'].join(''); } else { ctx.fillStyle = 'black'; } if (labelOptions.italic === true) { fontStyles.push('italic'); } if (labelOptions.bold === true) { fontStyles.push('bold'); } fontStyles.push(parseInt(labelOptions.fontSize || '10', 10) + 'px'); fontStyles.push('Arial'); ctx.font = fontStyles.join(' '); ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; ctx.fillText(labelOptions.text, iconSize.width / 2, iconSize.height / 2); // debug //ctx.fillText(selfId.split("-")[1], iconSize.width / 2, iconSize.height / 2); } self.get('labelMarker').set('icon', { 'url': canvas.toDataURL(), 'anchor': self.get('labelMarkerAnchor') }); setTimeout(function() { self.set('opacity', 1); }, 10); }); }; ClusterIconClass.prototype.setIcon = function(icon) { var self = this; self.set('icon', icon); }; ClusterIconClass.prototype.setVisible = function(visible) { var self = this; self.set('visible', visible); }; ClusterIconClass.prototype.setPosition = function(position) { var self = this; self.set('position', position); }; ClusterIconClass.prototype.getPosition = function() { var self = this; return self.get('position'); }; ClusterIconClass.prototype.setMap = function(map) { var self = this; self.set('map', map); }; });