From 7b2aee7004929eca43aeb979ca60fbd43d59ef63 Mon Sep 17 00:00:00 2001 From: jopster Date: Mon, 8 Jul 2024 14:52:24 +0200 Subject: [PATCH] Project Maps, Trigger usw --- __manifest__.py | 2 +- controllers/main.py | 12 +- models/dss.py | 1 + static/src/css/website_google_map.css | 28 + static/src/img/partners.png | Bin 0 -> 719 bytes static/src/js/website_google_map.js | 89 ++ static/src/lib/images/conv30.png | Bin 0 -> 1030 bytes static/src/lib/images/conv40.png | Bin 0 -> 1512 bytes static/src/lib/images/conv50.png | Bin 0 -> 2202 bytes static/src/lib/images/heart30.png | Bin 0 -> 1448 bytes static/src/lib/images/heart40.png | Bin 0 -> 2161 bytes static/src/lib/images/heart50.png | Bin 0 -> 2999 bytes static/src/lib/images/m1.png | Bin 0 -> 3003 bytes static/src/lib/images/m2.png | Bin 0 -> 3259 bytes static/src/lib/images/m3.png | Bin 0 -> 3956 bytes static/src/lib/images/m4.png | Bin 0 -> 5705 bytes static/src/lib/images/m5.png | Bin 0 -> 6839 bytes static/src/lib/images/people35.png | Bin 0 -> 1392 bytes static/src/lib/images/people45.png | Bin 0 -> 2076 bytes static/src/lib/images/people55.png | Bin 0 -> 2928 bytes static/src/lib/images/pin.png | Bin 0 -> 1135 bytes static/src/lib/markerclusterer.js | 1315 +++++++++++++++++ .../src/xml}/google_map_templates.xml | 8 +- views/dss_settings.xml | 25 +- 24 files changed, 1459 insertions(+), 21 deletions(-) create mode 100644 static/src/css/website_google_map.css create mode 100644 static/src/img/partners.png create mode 100644 static/src/js/website_google_map.js create mode 100644 static/src/lib/images/conv30.png create mode 100644 static/src/lib/images/conv40.png create mode 100644 static/src/lib/images/conv50.png create mode 100644 static/src/lib/images/heart30.png create mode 100644 static/src/lib/images/heart40.png create mode 100644 static/src/lib/images/heart50.png create mode 100644 static/src/lib/images/m1.png create mode 100644 static/src/lib/images/m2.png create mode 100644 static/src/lib/images/m3.png create mode 100644 static/src/lib/images/m4.png create mode 100644 static/src/lib/images/m5.png create mode 100644 static/src/lib/images/people35.png create mode 100644 static/src/lib/images/people45.png create mode 100644 static/src/lib/images/people55.png create mode 100644 static/src/lib/images/pin.png create mode 100755 static/src/lib/markerclusterer.js rename {views => static/src/xml}/google_map_templates.xml (63%) diff --git a/__manifest__.py b/__manifest__.py index 4064d10..fa3cac9 100755 --- a/__manifest__.py +++ b/__manifest__.py @@ -32,7 +32,6 @@ 'views/mainsystem_view.xml', 'views/menu.xml', 'views/company_view.xml', - 'views/google_map_templates.xml' ], 'demo': [], 'installable': True, @@ -47,6 +46,7 @@ 'web.assets_common': [ 'DigitalSignage/static/images/**/*', 'DigitalSignage/static/src/css/dss.css', + 'DigitalSignage/static/src/xml/*', ], 'web.assets_qweb': [ 'DigitalSignage/static/src/xml/*', diff --git a/controllers/main.py b/controllers/main.py index 36a2864..2b29ad1 100644 --- a/controllers/main.py +++ b/controllers/main.py @@ -26,12 +26,17 @@ class GoogleMap(http.Controller): @http.route(['/google_map'], type='http', auth="public", website=True, sitemap=False) def google_map(self, *arg, **post): + + projects = request.env['dss.projects'].sudo().search([('standort_visible', '=', True)]) + settings = (request.env['dss.settings'].search([],limit=1)) + google_maps_api_key = settings.google_maps_key _logger.info("Google Maps " + str(projects)+ " and Record : "+str(len(projects))) projects_data = { "counter": len(projects), "projects": [] } + for project in projects.with_context(show_address=True): projects_data["projects"].append({ 'id': project.id, @@ -39,15 +44,10 @@ class GoogleMap(http.Controller): 'latitude': str(project.standort_lati) if project.standort_lati else False, 'longitude': str(project.standort_long) if project.standort_long else False, }) - if 'customers' in post.get('partner_url', ''): - partner_url = '/customers/' - else: - partner_url = '/partners/' - google_maps_api_key = request.website.google_maps_api_key values = { 'partner_url': partner_url, 'partner_data': scriptsafe.dumps(projects_data), 'google_maps_api_key': google_maps_api_key, } - return request.render("website_google_map.google_map", values) + return request.render("DigitalSignage.google_map", values) diff --git a/models/dss.py b/models/dss.py index 27551de..849d7d0 100755 --- a/models/dss.py +++ b/models/dss.py @@ -71,6 +71,7 @@ class dssSettings(models.Model): tuya_access_id = fields.Char('Tuya Access ID') tuya_access_key = fields.Char('Tuya Access KEY') tuya_endpoint = fields.Char('Tuya Access Endpoint') + google_maps_key = fields.Char('Google API Key (Maps)') def _get_settingvalue(self,valuename): settings = (self.env['dss.settings'].search([],limit=1)) wert = settings._origin.read([valuename])[0][valuename] diff --git a/static/src/css/website_google_map.css b/static/src/css/website_google_map.css new file mode 100644 index 0000000..3d5ec1d --- /dev/null +++ b/static/src/css/website_google_map.css @@ -0,0 +1,28 @@ +html { + height: 100%; +} + +body { + margin: 0; + padding: 0; + height: 100%; +} + +#odoo-google-map { + width: 100%; + height: 100%; +} + +.marker { + font-size: 13px !important; +} + +.marker a { + text-decoration: none; +} + +.marker pre { + margin-top: 0; + margin-bottom: 0; + font-family: sans-serif !important; +} diff --git a/static/src/img/partners.png b/static/src/img/partners.png new file mode 100644 index 0000000000000000000000000000000000000000..a5693dbe3d555b278bde65d8538445e8e83aa88e GIT binary patch literal 719 zcmV;=0xP000>X1^@s6#OZ}&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_00M7GL_t(Y$DP(ss8v-I2JqiG8Tt}^ z6H$l;`RAzv`}c6-6AB>@G!PV=DH|L(kRXS#z+w@fud+ z0dC?v&SEB{_^7w9KZ+p&+wm19FlKN(#_#wbq?mF-WjA>n%bTh`i3@mrga9nTCY)`m z`c#kK$v3x>rmDyA8`fHU+z2Ud{@-!6s^hqZmq#Rdnmgvcn86!(7*bSr&hYCY0(Wr; z8(Sm1iO=w40DmLCXnXr#CA;x`hWr2@hZK+Y8>934C=L%0+Zs~5QZ!Y)3|Cthb>kmQ zh7_lV*}Q#z7w0mxt9Yx#2E3Xfd=yfg?z*HK9RVT5A8;%~o51>_sp|J}q7S?iQha5h zwrZ;SX=ZtUiMKPrhYL*JdpweXca)i4z>flvAW=o^XmbMycJ002ovPDHLkV1o9c BM+E=? literal 0 HcmV?d00001 diff --git a/static/src/js/website_google_map.js b/static/src/js/website_google_map.js new file mode 100644 index 0000000..f013a0d --- /dev/null +++ b/static/src/js/website_google_map.js @@ -0,0 +1,89 @@ +/* global MarkerClusterer, google */ +function initialize_map() { + 'use strict'; + + // MAP CONFIG AND LOADING + var map = new google.maps.Map(document.getElementById('odoo-google-map'), { + zoom: 1, + center: {lat: 0.0, lng: 0.0}, + mapTypeId: google.maps.MapTypeId.ROADMAP + }); + + // ENABLE ADDRESS GEOCODING + var Geocoder = new google.maps.Geocoder(); + + // INFO BUBBLES + var infoWindow = new google.maps.InfoWindow(); + var partners = new google.maps.MarkerImage('/website_google_map/static/src/img/partners.png', new google.maps.Size(25, 25)); + var partner_url = document.body.getAttribute('data-partner-url') || ''; + var markers = []; + var options = { + imagePath: '/website_google_map/static/src/lib/images/m' + }; + + google.maps.event.addListener(map, 'click', function() { + infoWindow.close(); + }); + + // Display the bubble once clicked + var onMarkerClick = function() { + var marker = this; + var p = marker.partner; + infoWindow.setContent( + '
'+ + (partner_url.length ? ''+p.name +'' : ''+p.name+'' )+ + (p.type ? ' ' + p.type + '' : '')+ + '
' + p.address + '
'+ + '
' + ); + infoWindow.open(map, marker); + }; + + // Create a bubble for a partner + var set_marker = function(partner) { + // If no lat & long, geocode address + // TODO: a server cronjob that will store these coordinates in database instead of resolving them on-the-fly + if (!partner.latitude && !partner.longitude) { + Geocoder.geocode({'address': partner.address}, function(results, status) { + if (status === google.maps.GeocoderStatus.OK) { + var location = results[0].geometry.location; + partner.latitude = location.ob; + partner.longitude = location.pb; + var marker = new google.maps.Marker({ + partner: partner, + map: map, + icon: partners, + position: location + }); + google.maps.event.addListener(marker, 'click', onMarkerClick); + markers.push(marker); + } else { + console.debug('Geocode was not successful for the following reason: ' + status); + } + }); + } else { + var latLng = new google.maps.LatLng(partner.latitude, partner.longitude); + var marker = new google.maps.Marker({ + partner: partner, + icon: partners, + map: map, + position: latLng + }); + google.maps.event.addListener(marker, 'click', onMarkerClick); + markers.push(marker); + } + }; + + /* eslint-disable no-undef */ + // Create the markers and cluster them on the map + if (odoo_partner_data){ /* odoo_partner_data special variable should have been defined in google_map.xml */ + for (var i = 0; i < odoo_partner_data.counter; i++) { + set_marker(odoo_partner_data.partners[i]); + } + new MarkerClusterer(map, markers, options); + } + /* eslint-enable no-undef */ +} + +// Initialize map once the DOM has been loaded +google.maps.event.addDomListener(window, 'load', initialize_map); diff --git a/static/src/lib/images/conv30.png b/static/src/lib/images/conv30.png new file mode 100644 index 0000000000000000000000000000000000000000..a50f61f0b9ed0dc22d77eb8b350d2c4cc0b1df61 GIT binary patch literal 1030 zcmV+h1o``kP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXH3 z4-YkLNJWnT00V|eL_t(Y$F-GDXkA4V#(!t-z3=rUFMnHGt69V*gosTLq(V2wML}p? zXqN>yE~?Olt`xdaELD+!3#H;h+=#eULQzzRE?lVHWus3~L1Ji{K$_&eJ9qvZ7x{Z* zB%0)XFayKPobNklzL^n#TerVC>eKK^;q!Ay#3UI$Q4sbA)zUEa1?u7#!6)xgF*hfU zKk4NA`RQXzZt-hVmvIrndkZ8W3MhvGHHIw7P$xL6H+w6+m(o10UU${zcngR1IZ8kg zOb}$B#46=#i4%-fYFQmqsh+pFd0wXOe584@OtWZ#L8NQ zwbjbcX|5|Q`(=UzbR)xqR(O|F5*1VxXDx~88%c}Uq@}B<(cJ^=2U7N>ZuBA$A&*B$ zU9>oI;!YDE*wng1ivWV66w#8dnkFr2uA8YnP$|oB(jpD&DYU?7yJ-K35FM$H1tGSG zV%DU!BjX*0DPlqDqQi#*Bc^Q`swuw+P76|}Or1i9a(MqG=lVRf4TC*4N_NyllOT#j;H#s5(362Cs ze0nSG(5|QIdF|U)+5+7wb=)Jm`n*}NF7mW}=q9=`A^qKo#HGRGJe0Og$aHYHG zS_md$j}+L{<7F+t?o9~daZ=z-z$X!J5~3*1c%lSE;${ot0q^Ruwp^F)i&sb9nc9^3 z_U|vAnQzWs=%{r*lPOi31)}_0-e%2+5(v@qNB&dCO4jVFc)=%Y4mP^A_XH=u(xo`r zlN_CXWBfDAVcnnSW*;BcrTFD@U{ndo*8E`ep|>VafBJG+`XS)``=);x&Q3ou z5JrBCg8H;<%%)j5_u<^Nota%ZfOvz6k%j8*hksc9am6Pt&9WkD~(@fkAe{GiA%*@Qx4*bXO zf{VS+#vy%lYU1vtcgGeVl^cb14ie^bq@I$IRF3v07*qoM6N<$g8b6n AXaE2J literal 0 HcmV?d00001 diff --git a/static/src/lib/images/conv40.png b/static/src/lib/images/conv40.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe31a5758cfc19ac85927e81d84f369a5e39f1a GIT binary patch literal 1512 zcmVP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXH3 z4-Pqe0?yT5CF*G1T~%jFVmcM-G4Xf?U~n@uA-K#a}NjwJY6uPmC?G>0xS!4~SfQ#&k>s#0?|VVrVq# z?i^L;E_NfN2;#sD%%(@7I2&US1jFG(P&K@-2%*c)X7yaK zybawRrAd3tjsBJC#&}#KH$rWD{92;qhN&uWd<0<0p3RHrC^!}ss$|x5jSnI-hxQL z@;GYKOwf|i7!e!PqS>Id@oP;emI+v)l#FAq|IPxS3|inYWQSQMm_}UHqS@j6tP~=M zmoWyNwFFHOsUnsfpfgHB!HS!JI}(fpIe|G$%S=>^2sk$;9I--xj9J9gf#|G?;Gopp z46q2ABLOL(8ACA9S!o2y$YGH|C$U~4hYU?ZJ2R?>I0TIQC5gU*892)NVd+3J8Rv!M#*7aYW{n=LzziqYuqZluKq^dU$dC{t*4&oKr zPkr1daXhrsnz&PW3mrI{wm$N1l3ZVrKWR zJ{$U*|NdHTw(rdmx(718+DD9CplHUKVI{M@u}<5ZeI(R7r$~mG;ky+@Gcqz5d$;tr zrMc5*4!-9H=SSU!(Sy0APh{Z7(njrjbGw_w$_IXOp*w%{ z@k-wQef9HiR7(yQ3OGk8f@Z_9gRi&0v%l(k8|~$@pMG?%y7V;iR@eCsrHwdoh;J#m zdr86vCx+t;HHEPwU84q>+*cI;?%bcd zf2a=jdc6a{)>N2TUFB95n@d;U`)b@>{;BHW&-naL;lIny&%bl%f&T*ulEOWN)@3dL O0000Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXH3 z4+;oLSN+=n00AgLWNM&NSV@)p)-zmYjnd(@M~)TNuOMZ_0l}j>3-4hzNr4ftUfRr|&Kpk_6L4$c_{y?h3?6t|}}mW{Qj;w5>fw z-UEyw&S7Sl8BT;MC%}WIX0R%vy&h_Y2$+7Lk9&cT)LfC}vL=EVn0UyEK#FL$H{gOG zWG=CCT(4bgJ0xseqc z$5vvse}`36HG`xCT{3d7f?!BDVTA)Pm&}o@C?2P$_0@VxAP0v;#btxiW5GevLCP7S z*Xt8SKsIyn+N*54-)U=1ewvjY%dptluA+GaK*kuAjNL zI<|Cu(TSLZ5wg$fGTQ*Le1Y$Wq_0>l&<)u;#1vIfle#FWLqh>uw=T3#9=k7`I(&Ga zo1VTb_u6}9@pucT!)Kb|8sK}Y(q@B3Hb||38p0_3YDm>Iz*JBXat2GF&S8_SBYvX! zsGpgAD>PDnDX-tKKXp1+$0~lxrT(0^Dima7AUc?HsQ6W?H-miEdIuNhuz@6Mg#+h7 zweDZ4weL*M=AkUlQ--MGtXAt)9Xio^Tgv87!pRel_};1Gp)tEDSE?_!TQlF&t&=a7 zx%1WBS-6h&yG6*+8II(OvsQvpV#rw~)&V|1%!p;BK3U62msS+d^Vp?kk;;>`` zDz4bgfh=BgCGV<)R|(;uwbngv6O&I`?7X%4iD%D{;lNk!k;`Xv)_U!aR13%E8q?Fa z1s`rETB&$SpKy{_v6?z4>NQld*1}q^qW#|^Of39ogQuEETAil&S$bvSzP5F6%Sy{oF?-M-yvN)?g9q0ZOWu8>Ao06Cg=-uRuo^M;>p|KX3i z!)_Oboh)zvk^*lGj!SNfpZnOWrzWNgjvl$YTzJO^Aa^37hAZ89LPPdpddZi!R3}gV z66mf*n^;p@zyEJXSUG^+;O+;4OHNW|Dp4<1-b5(20 z_xsyE{A8~&d&2rDHLSw>j4K2y3#`%ns2)A~`LRl^VK(>p;|FBtuKV4!yB>=>u6V6i zw))yjNV|_EMV1z)-JSQ-MnvM_a_z>8yi{b2U*mq z>9(xfSS=kpYl;n>PG<`+|31hEtFnnZ?$mdlcvF+Q1 cb2iNX0W@NKfQ0e{SO5S307*qoM6N<$g6tG9*Z=?k literal 0 HcmV?d00001 diff --git a/static/src/lib/images/heart30.png b/static/src/lib/images/heart30.png new file mode 100644 index 0000000000000000000000000000000000000000..c6067a7e6c5a5d2ead34037d7025d12c764a46d4 GIT binary patch literal 1448 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 literal 0 HcmV?d00001 diff --git a/static/src/lib/images/heart50.png b/static/src/lib/images/heart50.png new file mode 100644 index 0000000000000000000000000000000000000000..26c2252477f5a9c6dca63697b2d3dd6dbfd0d5ef GIT binary patch literal 2999 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 literal 0 HcmV?d00001 diff --git a/static/src/lib/images/m1.png b/static/src/lib/images/m1.png new file mode 100644 index 0000000000000000000000000000000000000000..329ff524c59e4ac6ba91db135e2257106253d7b9 GIT binary patch literal 3003 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 literal 0 HcmV?d00001 diff --git a/static/src/lib/images/m2.png b/static/src/lib/images/m2.png new file mode 100644 index 0000000000000000000000000000000000000000..b999cbcf69441f194768157d79a113b0fae03e3c GIT binary patch literal 3259 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 O0000KLZ*U+=)p!fv7f#TG` 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 literal 0 HcmV?d00001 diff --git a/static/src/lib/images/m5.png b/static/src/lib/images/m5.png new file mode 100644 index 0000000000000000000000000000000000000000..61387d2ab5c8d22efef4846617567a3b1ae6d72e GIT binary patch literal 6839 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 literal 0 HcmV?d00001 diff --git a/static/src/lib/images/people35.png b/static/src/lib/images/people35.png new file mode 100644 index 0000000000000000000000000000000000000000..6a5a209decc01df760a8c46fe18d28a49b900930 GIT binary patch literal 1392 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 + * This is a v3 implementation of the + * v2 MarkerClusterer. + */ + +/** + * @license + * Copyright 2010 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * A Marker Clusterer that clusters markers. + * + * @param {google.maps.Map} map The Google map to attach to. + * @param {Array.=} opt_markers Optional markers to add to + * the cluster. + * @param {Object=} opt_options support the following options: + * 'gridSize': (number) The grid size of a cluster in pixels. + * 'maxZoom': (number) The maximum zoom level that a marker can be part of a + * cluster. + * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a + * cluster is to zoom into it. + * 'averageCenter': (boolean) Whether the center of each cluster should be + * the average of all markers in the cluster. + * 'minimumClusterSize': (number) The minimum number of markers to be in a + * cluster before the markers are hidden and a count + * is shown. + * 'styles': (object) An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition': (string) The position of the backgound x, y. + * 'iconAnchor': (Array) The anchor position of the icon x, y. + * @constructor + * @extends google.maps.OverlayView + */ +function MarkerClusterer(map, opt_markers, opt_options) { + // MarkerClusterer implements google.maps.OverlayView interface. We use the + // extend function to extend MarkerClusterer with google.maps.OverlayView + // because it might not always be available when the code is defined so we + // look for it at the last possible moment. If it doesn't exist now then + // there is no point going ahead :) + this.extend(MarkerClusterer, google.maps.OverlayView); + this.map_ = map; + + /** + * @type {Array.} + * @private + */ + this.markers_ = []; + + /** + * @type {Array.} + */ + this.clusters_ = []; + + this.sizes = [53, 56, 66, 78, 90]; + + /** + * @private + */ + this.styles_ = []; + + /** + * @type {boolean} + * @private + */ + this.ready_ = false; + + var options = opt_options || {}; + + /** + * @type {number} + * @private + */ + this.gridSize_ = options['gridSize'] || 60; + + /** + * @private + */ + this.minClusterSize_ = options['minimumClusterSize'] || 2; + + + /** + * @type {?number} + * @private + */ + this.maxZoom_ = options['maxZoom'] || null; + + this.styles_ = options['styles'] || []; + + /** + * @type {string} + * @private + */ + this.imagePath_ = options['imagePath'] || + this.MARKER_CLUSTER_IMAGE_PATH_; + + /** + * @type {string} + * @private + */ + this.imageExtension_ = options['imageExtension'] || + this.MARKER_CLUSTER_IMAGE_EXTENSION_; + + /** + * @type {boolean} + * @private + */ + this.zoomOnClick_ = true; + + if (options['zoomOnClick'] != undefined) { + this.zoomOnClick_ = options['zoomOnClick']; + } + + /** + * @type {boolean} + * @private + */ + this.averageCenter_ = false; + + if (options['averageCenter'] != undefined) { + this.averageCenter_ = options['averageCenter']; + } + + this.setupStyles_(); + + this.setMap(map); + + /** + * @type {number} + * @private + */ + this.prevZoom_ = this.map_.getZoom(); + + // Add the map event listeners + var that = this; + google.maps.event.addListener(this.map_, 'zoom_changed', function() { + var zoom = that.map_.getZoom(); + + if (that.prevZoom_ != zoom) { + that.prevZoom_ = zoom; + that.resetViewport(); + } + }); + + google.maps.event.addListener(this.map_, 'idle', function() { + that.redraw(); + }); + + // Finally, add the markers + if (opt_markers && opt_markers.length) { + this.addMarkers(opt_markers, false); + } +} + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m'; + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png'; + + +/** + * Extends a objects prototype by anothers. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + * @ignore + */ +MarkerClusterer.prototype.extend = function(obj1, obj2) { + return (function(object) { + for (var property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.onAdd = function() { + this.setReady_(true); +}; + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.draw = function() {}; + +/** + * Sets up the styles object. + * + * @private + */ +MarkerClusterer.prototype.setupStyles_ = function() { + if (this.styles_.length) { + return; + } + + for (var i = 0, size; size = this.sizes[i]; i++) { + this.styles_.push({ + url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_, + height: size, + width: size + }); + } +}; + +/** + * Fit the map to the bounds of the markers in the clusterer. + */ +MarkerClusterer.prototype.fitMapToMarkers = function() { + var markers = this.getMarkers(); + var bounds = new google.maps.LatLngBounds(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + + this.map_.fitBounds(bounds); +}; + + +/** + * Sets the styles. + * + * @param {Object} styles The style to set. + */ +MarkerClusterer.prototype.setStyles = function(styles) { + this.styles_ = styles; +}; + + +/** + * Gets the styles. + * + * @return {Object} The styles object. + */ +MarkerClusterer.prototype.getStyles = function() { + return this.styles_; +}; + + +/** + * Whether zoom on click is set. + * + * @return {boolean} True if zoomOnClick_ is set. + */ +MarkerClusterer.prototype.isZoomOnClick = function() { + return this.zoomOnClick_; +}; + +/** + * Whether average center is set. + * + * @return {boolean} True if averageCenter_ is set. + */ +MarkerClusterer.prototype.isAverageCenter = function() { + return this.averageCenter_; +}; + + +/** + * Returns the array of markers in the clusterer. + * + * @return {Array.} The markers. + */ +MarkerClusterer.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the number of markers in the clusterer + * + * @return {Number} The number of markers. + */ +MarkerClusterer.prototype.getTotalMarkers = function() { + return this.markers_.length; +}; + + +/** + * Sets the max zoom for the clusterer. + * + * @param {number} maxZoom The max zoom level. + */ +MarkerClusterer.prototype.setMaxZoom = function(maxZoom) { + this.maxZoom_ = maxZoom; +}; + + +/** + * Gets the max zoom for the clusterer. + * + * @return {number} The max zoom level. + */ +MarkerClusterer.prototype.getMaxZoom = function() { + return this.maxZoom_; +}; + + +/** + * The function for calculating the cluster icon image. + * + * @param {Array.} markers The markers in the clusterer. + * @param {number} numStyles The number of styles available. + * @return {Object} A object properties: 'text' (string) and 'index' (number). + * @private + */ +MarkerClusterer.prototype.calculator_ = function(markers, numStyles) { + var index = 0; + var count = markers.length; + var dv = count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index++; + } + + index = Math.min(index, numStyles); + return { + text: count, + index: index + }; +}; + + +/** + * Set the calculator function. + * + * @param {function(Array, number)} calculator The function to set as the + * calculator. The function should return a object properties: + * 'text' (string) and 'index' (number). + * + */ +MarkerClusterer.prototype.setCalculator = function(calculator) { + this.calculator_ = calculator; +}; + + +/** + * Get the calculator function. + * + * @return {function(Array, number)} the calculator function. + */ +MarkerClusterer.prototype.getCalculator = function() { + return this.calculator_; +}; + + +/** + * Add an array of markers to the clusterer. + * + * @param {Array.} markers The markers to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) { + for (var i = 0, marker; marker = markers[i]; i++) { + this.pushMarkerTo_(marker); + } + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Pushes a marker to the clusterer. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.pushMarkerTo_ = function(marker) { + marker.isAdded = false; + if (marker['draggable']) { + // If the marker is draggable add a listener so we update the clusters on + // the drag end. + var that = this; + google.maps.event.addListener(marker, 'dragend', function() { + marker.isAdded = false; + that.repaint(); + }); + } + this.markers_.push(marker); +}; + + +/** + * Adds a marker to the clusterer and redraws if needed. + * + * @param {google.maps.Marker} marker The marker to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) { + this.pushMarkerTo_(marker); + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Removes a marker and returns true if removed, false if not + * + * @param {google.maps.Marker} marker The marker to remove + * @return {boolean} Whether the marker was removed or not + * @private + */ +MarkerClusterer.prototype.removeMarker_ = function(marker) { + var index = -1; + if (this.markers_.indexOf) { + index = this.markers_.indexOf(marker); + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + index = i; + break; + } + } + } + + if (index == -1) { + // Marker is not in our list of markers. + return false; + } + + marker.setMap(null); + + this.markers_.splice(index, 1); + + return true; +}; + + +/** + * Remove a marker from the cluster. + * + * @param {google.maps.Marker} marker The marker to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + * @return {boolean} True if the marker was removed. + */ +MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) { + var removed = this.removeMarker_(marker); + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } else { + return false; + } +}; + + +/** + * Removes an array of markers from the cluster. + * + * @param {Array.} markers The markers to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + */ +MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) { + var removed = false; + + for (var i = 0, marker; marker = markers[i]; i++) { + var r = this.removeMarker_(marker); + removed = removed || r; + } + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } +}; + + +/** + * Sets the clusterer's ready state. + * + * @param {boolean} ready The state. + * @private + */ +MarkerClusterer.prototype.setReady_ = function(ready) { + if (!this.ready_) { + this.ready_ = ready; + this.createClusters_(); + } +}; + + +/** + * Returns the number of clusters in the clusterer. + * + * @return {number} The number of clusters. + */ +MarkerClusterer.prototype.getTotalClusters = function() { + return this.clusters_.length; +}; + + +/** + * Returns the google map that the clusterer is associated with. + * + * @return {google.maps.Map} The map. + */ +MarkerClusterer.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Sets the google map that the clusterer is associated with. + * + * @param {google.maps.Map} map The map. + */ +MarkerClusterer.prototype.setMap = function(map) { + this.map_ = map; +}; + + +/** + * Returns the size of the grid. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getGridSize = function() { + return this.gridSize_; +}; + + +/** + * Sets the size of the grid. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setGridSize = function(size) { + this.gridSize_ = size; +}; + + +/** + * Returns the min cluster size. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getMinClusterSize = function() { + return this.minClusterSize_; +}; + +/** + * Sets the min cluster size. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setMinClusterSize = function(size) { + this.minClusterSize_ = size; +}; + + +/** + * Extends a bounds object by the grid size. + * + * @param {google.maps.LatLngBounds} bounds The bounds to extend. + * @return {google.maps.LatLngBounds} The extended bounds. + */ +MarkerClusterer.prototype.getExtendedBounds = function(bounds) { + var projection = this.getProjection(); + + // Turn the bounds into latlng. + var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), + bounds.getNorthEast().lng()); + var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), + bounds.getSouthWest().lng()); + + // Convert the points to pixels and the extend out by the grid size. + var trPix = projection.fromLatLngToDivPixel(tr); + trPix.x += this.gridSize_; + trPix.y -= this.gridSize_; + + var blPix = projection.fromLatLngToDivPixel(bl); + blPix.x -= this.gridSize_; + blPix.y += this.gridSize_; + + // Convert the pixel points back to LatLng + var ne = projection.fromDivPixelToLatLng(trPix); + var sw = projection.fromDivPixelToLatLng(blPix); + + // Extend the bounds to contain the new bounds. + bounds.extend(ne); + bounds.extend(sw); + + return bounds; +}; + + +/** + * Determins if a marker is contained in a bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @param {google.maps.LatLngBounds} bounds The bounds to check against. + * @return {boolean} True if the marker is in the bounds. + * @private + */ +MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) { + return bounds.contains(marker.getPosition()); +}; + + +/** + * Clears all clusters and markers from the clusterer. + */ +MarkerClusterer.prototype.clearMarkers = function() { + this.resetViewport(true); + + // Set the markers a empty array. + this.markers_ = []; +}; + + +/** + * Clears all existing clusters and recreates them. + * @param {boolean} opt_hide To also hide the marker. + */ +MarkerClusterer.prototype.resetViewport = function(opt_hide) { + // Remove all the clusters + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + cluster.remove(); + } + + // Reset the markers to not be added and to be invisible. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.isAdded = false; + if (opt_hide) { + marker.setMap(null); + } + } + + this.clusters_ = []; +}; + +/** + * + */ +MarkerClusterer.prototype.repaint = function() { + var oldClusters = this.clusters_.slice(); + this.clusters_.length = 0; + this.resetViewport(); + this.redraw(); + + // Remove the old clusters. + // Do it in a timeout so the other clusters have been drawn first. + window.setTimeout(function() { + for (var i = 0, cluster; cluster = oldClusters[i]; i++) { + cluster.remove(); + } + }, 0); +}; + + +/** + * Redraws the clusters. + */ +MarkerClusterer.prototype.redraw = function() { + this.createClusters_(); +}; + + +/** + * Calculates the distance between two latlng locations in km. + * @see http://www.movable-type.co.uk/scripts/latlong.html + * + * @param {google.maps.LatLng} p1 The first lat lng point. + * @param {google.maps.LatLng} p2 The second lat lng point. + * @return {number} The distance between the two points in km. + * @private +*/ +MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) { + if (!p1 || !p2) { + return 0; + } + + var R = 6371; // Radius of the Earth in km + var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; + var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +}; + + +/** + * Add a marker to a cluster, or creates a new cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.addToClosestCluster_ = function(marker) { + var distance = 40000; // Some large number + var clusterToAddTo = null; + var pos = marker.getPosition(); + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + var center = cluster.getCenter(); + if (center) { + var d = this.distanceBetweenPoints_(center, marker.getPosition()); + if (d < distance) { + distance = d; + clusterToAddTo = cluster; + } + } + } + + if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) { + clusterToAddTo.addMarker(marker); + } else { + var cluster = new Cluster(this); + cluster.addMarker(marker); + this.clusters_.push(cluster); + } +}; + + +/** + * Creates the clusters. + * + * @private + */ +MarkerClusterer.prototype.createClusters_ = function() { + if (!this.ready_) { + return; + } + + // Get our current map view bounds. + // Create a new bounds object so we don't affect the map. + var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(), + this.map_.getBounds().getNorthEast()); + var bounds = this.getExtendedBounds(mapBounds); + + for (var i = 0, marker; marker = this.markers_[i]; i++) { + if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) { + this.addToClosestCluster_(marker); + } + } +}; + + +/** + * A cluster that contains markers. + * + * @param {MarkerClusterer} markerClusterer The markerclusterer that this + * cluster is associated with. + * @constructor + * @ignore + */ +function Cluster(markerClusterer) { + this.markerClusterer_ = markerClusterer; + this.map_ = markerClusterer.getMap(); + this.gridSize_ = markerClusterer.getGridSize(); + this.minClusterSize_ = markerClusterer.getMinClusterSize(); + this.averageCenter_ = markerClusterer.isAverageCenter(); + this.center_ = null; + this.markers_ = []; + this.bounds_ = null; + this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(), + markerClusterer.getGridSize()); +} + +/** + * Determins if a marker is already added to the cluster. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker is already added. + */ +Cluster.prototype.isMarkerAlreadyAdded = function(marker) { + if (this.markers_.indexOf) { + return this.markers_.indexOf(marker) != -1; + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + return true; + } + } + } + return false; +}; + + +/** + * Add a marker the cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @return {boolean} True if the marker was added. + */ +Cluster.prototype.addMarker = function(marker) { + if (this.isMarkerAlreadyAdded(marker)) { + return false; + } + + if (!this.center_) { + this.center_ = marker.getPosition(); + this.calculateBounds_(); + } else { + if (this.averageCenter_) { + var l = this.markers_.length + 1; + var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l; + var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l; + this.center_ = new google.maps.LatLng(lat, lng); + this.calculateBounds_(); + } + } + + marker.isAdded = true; + this.markers_.push(marker); + + var len = this.markers_.length; + if (len < this.minClusterSize_ && marker.getMap() != this.map_) { + // Min cluster size not reached so show the marker. + marker.setMap(this.map_); + } + + if (len == this.minClusterSize_) { + // Hide the markers that were showing. + for (var i = 0; i < len; i++) { + this.markers_[i].setMap(null); + } + } + + if (len >= this.minClusterSize_) { + marker.setMap(null); + } + + this.updateIcon(); + return true; +}; + + +/** + * Returns the marker clusterer that the cluster is associated with. + * + * @return {MarkerClusterer} The associated marker clusterer. + */ +Cluster.prototype.getMarkerClusterer = function() { + return this.markerClusterer_; +}; + + +/** + * Returns the bounds of the cluster. + * + * @return {google.maps.LatLngBounds} the cluster bounds. + */ +Cluster.prototype.getBounds = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + var markers = this.getMarkers(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + return bounds; +}; + + +/** + * Removes the cluster + */ +Cluster.prototype.remove = function() { + this.clusterIcon_.remove(); + this.markers_.length = 0; + delete this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {number} The cluster center. + */ +Cluster.prototype.getSize = function() { + return this.markers_.length; +}; + + +/** + * Returns the center of the cluster. + * + * @return {Array.} The cluster center. + */ +Cluster.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {google.maps.LatLng} The cluster center. + */ +Cluster.prototype.getCenter = function() { + return this.center_; +}; + + +/** + * Calculated the extended bounds of the cluster with the grid. + * + * @private + */ +Cluster.prototype.calculateBounds_ = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds); +}; + + +/** + * Determines if a marker lies in the clusters bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker lies in the bounds. + */ +Cluster.prototype.isMarkerInClusterBounds = function(marker) { + return this.bounds_.contains(marker.getPosition()); +}; + + +/** + * Returns the map that the cluster is associated with. + * + * @return {google.maps.Map} The map. + */ +Cluster.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Updates the cluster icon + */ +Cluster.prototype.updateIcon = function() { + var zoom = this.map_.getZoom(); + var mz = this.markerClusterer_.getMaxZoom(); + + if (mz && zoom > mz) { + // The zoom is greater than our max zoom so show all the markers in cluster. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.setMap(this.map_); + } + return; + } + + if (this.markers_.length < this.minClusterSize_) { + // Min cluster size not yet reached. + this.clusterIcon_.hide(); + return; + } + + var numStyles = this.markerClusterer_.getStyles().length; + var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles); + this.clusterIcon_.setCenter(this.center_); + this.clusterIcon_.setSums(sums); + this.clusterIcon_.show(); +}; + + +/** + * A cluster icon + * + * @param {Cluster} cluster The cluster to be associated with. + * @param {Object} styles An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition: (string) The background postition x, y. + * @param {number=} opt_padding Optional padding to apply to the cluster icon. + * @constructor + * @extends google.maps.OverlayView + * @ignore + */ +function ClusterIcon(cluster, styles, opt_padding) { + cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView); + + this.styles_ = styles; + this.padding_ = opt_padding || 0; + this.cluster_ = cluster; + this.center_ = null; + this.map_ = cluster.getMap(); + this.div_ = null; + this.sums_ = null; + this.visible_ = false; + + this.setMap(this.map_); +} + + +/** + * Triggers the clusterclick event and zoom's if the option is set. + * + * @param {google.maps.MouseEvent} event The event to propagate + */ +ClusterIcon.prototype.triggerClusterClick = function(event) { + var markerClusterer = this.cluster_.getMarkerClusterer(); + + // Trigger the clusterclick event. + google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_, event); + + if (markerClusterer.isZoomOnClick()) { + // Zoom into the cluster. + this.map_.fitBounds(this.cluster_.getBounds()); + } +}; + + +/** + * Adding the cluster icon to the dom. + * @ignore + */ +ClusterIcon.prototype.onAdd = function() { + this.div_ = document.createElement('DIV'); + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.innerHTML = this.sums_.text; + } + + var panes = this.getPanes(); + panes.overlayMouseTarget.appendChild(this.div_); + + var that = this; + var isDragging = false; + google.maps.event.addDomListener(this.div_, 'click', function(event) { + // Only perform click when not preceded by a drag + if (!isDragging) { + that.triggerClusterClick(event); + } + }); + google.maps.event.addDomListener(this.div_, 'mousedown', function() { + isDragging = false; + }); + google.maps.event.addDomListener(this.div_, 'mousemove', function() { + isDragging = true; + }); +}; + + +/** + * Returns the position to place the div dending on the latlng. + * + * @param {google.maps.LatLng} latlng The position in latlng. + * @return {google.maps.Point} The position in pixels. + * @private + */ +ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) { + var pos = this.getProjection().fromLatLngToDivPixel(latlng); + + if (typeof this.iconAnchor_ === 'object' && this.iconAnchor_.length === 2) { + pos.x -= this.iconAnchor_[0]; + pos.y -= this.iconAnchor_[1]; + } else { + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + } + return pos; +}; + + +/** + * Draw the icon. + * @ignore + */ +ClusterIcon.prototype.draw = function() { + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.top = pos.y + 'px'; + this.div_.style.left = pos.x + 'px'; + } +}; + + +/** + * Hide the icon. + */ +ClusterIcon.prototype.hide = function() { + if (this.div_) { + this.div_.style.display = 'none'; + } + this.visible_ = false; +}; + + +/** + * Position and show the icon. + */ +ClusterIcon.prototype.show = function() { + if (this.div_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.style.display = ''; + } + this.visible_ = true; +}; + + +/** + * Remove the icon from the map + */ +ClusterIcon.prototype.remove = function() { + this.setMap(null); +}; + + +/** + * Implementation of the onRemove interface. + * @ignore + */ +ClusterIcon.prototype.onRemove = function() { + if (this.div_ && this.div_.parentNode) { + this.hide(); + this.div_.parentNode.removeChild(this.div_); + this.div_ = null; + } +}; + + +/** + * Set the sums of the icon. + * + * @param {Object} sums The sums containing: + * 'text': (string) The text to display in the icon. + * 'index': (number) The style index of the icon. + */ +ClusterIcon.prototype.setSums = function(sums) { + this.sums_ = sums; + this.text_ = sums.text; + this.index_ = sums.index; + if (this.div_) { + this.div_.innerHTML = sums.text; + } + + this.useStyle(); +}; + + +/** + * Sets the icon to the styles. + */ +ClusterIcon.prototype.useStyle = function() { + var index = Math.max(0, this.sums_.index - 1); + index = Math.min(this.styles_.length - 1, index); + var style = this.styles_[index]; + this.url_ = style['url']; + this.height_ = style['height']; + this.width_ = style['width']; + this.textColor_ = style['textColor']; + this.anchor_ = style['anchor']; + this.textSize_ = style['textSize']; + this.backgroundPosition_ = style['backgroundPosition']; + this.iconAnchor_ = style['iconAnchor']; +}; + + +/** + * Sets the center of the icon. + * + * @param {google.maps.LatLng} center The latlng to set as the center. + */ +ClusterIcon.prototype.setCenter = function(center) { + this.center_ = center; +}; + + +/** + * Create the css text based on the position of the icon. + * + * @param {google.maps.Point} pos The position. + * @return {string} The css style text. + */ +ClusterIcon.prototype.createCss = function(pos) { + var style = []; + style.push('background-image:url(' + this.url_ + ');'); + var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0'; + style.push('background-position:' + backgroundPosition + ';'); + + if (typeof this.anchor_ === 'object') { + if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 && + this.anchor_[0] < this.height_) { + style.push('height:' + (this.height_ - this.anchor_[0]) + + 'px; padding-top:' + this.anchor_[0] + 'px;'); + } else if (typeof this.anchor_[0] === 'number' && this.anchor_[0] < 0 && + -this.anchor_[0] < this.height_) { + style.push('height:' + this.height_ + 'px; line-height:' + (this.height_ + this.anchor_[0]) + + 'px;'); + } else { + style.push('height:' + this.height_ + 'px; line-height:' + this.height_ + + 'px;'); + } + if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 && + this.anchor_[1] < this.width_) { + style.push('width:' + (this.width_ - this.anchor_[1]) + + 'px; padding-left:' + this.anchor_[1] + 'px;'); + } else { + style.push('width:' + this.width_ + 'px; text-align:center;'); + } + } else { + style.push('height:' + this.height_ + 'px; line-height:' + + this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;'); + } + + var txtColor = this.textColor_ ? this.textColor_ : 'black'; + var txtSize = this.textSize_ ? this.textSize_ : 11; + + style.push('cursor:pointer; top:' + pos.y + 'px; left:' + + pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' + + txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return style.join(''); +}; + + +// Export Symbols for Closure +// If you are not going to compile with closure then you can remove the +// code below. +window['MarkerClusterer'] = MarkerClusterer; +MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker; +MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers; +MarkerClusterer.prototype['clearMarkers'] = + MarkerClusterer.prototype.clearMarkers; +MarkerClusterer.prototype['fitMapToMarkers'] = + MarkerClusterer.prototype.fitMapToMarkers; +MarkerClusterer.prototype['getCalculator'] = + MarkerClusterer.prototype.getCalculator; +MarkerClusterer.prototype['getGridSize'] = + MarkerClusterer.prototype.getGridSize; +MarkerClusterer.prototype['getExtendedBounds'] = + MarkerClusterer.prototype.getExtendedBounds; +MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap; +MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers; +MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom; +MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles; +MarkerClusterer.prototype['getTotalClusters'] = + MarkerClusterer.prototype.getTotalClusters; +MarkerClusterer.prototype['getTotalMarkers'] = + MarkerClusterer.prototype.getTotalMarkers; +MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw; +MarkerClusterer.prototype['removeMarker'] = + MarkerClusterer.prototype.removeMarker; +MarkerClusterer.prototype['removeMarkers'] = + MarkerClusterer.prototype.removeMarkers; +MarkerClusterer.prototype['resetViewport'] = + MarkerClusterer.prototype.resetViewport; +MarkerClusterer.prototype['repaint'] = + MarkerClusterer.prototype.repaint; +MarkerClusterer.prototype['setCalculator'] = + MarkerClusterer.prototype.setCalculator; +MarkerClusterer.prototype['setGridSize'] = + MarkerClusterer.prototype.setGridSize; +MarkerClusterer.prototype['setMaxZoom'] = + MarkerClusterer.prototype.setMaxZoom; +MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd; +MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw; + +Cluster.prototype['getCenter'] = Cluster.prototype.getCenter; +Cluster.prototype['getSize'] = Cluster.prototype.getSize; +Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers; + +ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd; +ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw; +ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove; diff --git a/views/google_map_templates.xml b/static/src/xml/google_map_templates.xml similarity index 63% rename from views/google_map_templates.xml rename to static/src/xml/google_map_templates.xml index 755e0da..93f60b6 100644 --- a/views/google_map_templates.xml +++ b/static/src/xml/google_map_templates.xml @@ -7,11 +7,11 @@ World Map - +
@@ -20,8 +20,8 @@ - - + + diff --git a/views/dss_settings.xml b/views/dss_settings.xml index e5869e1..f064fff 100755 --- a/views/dss_settings.xml +++ b/views/dss_settings.xml @@ -19,15 +19,15 @@
-

Standard Cloud Verzeichnisse:

-

-
- - - - -
-
+

Standard Cloud Verzeichnisse:

+

+
+ + + + +
+
@@ -40,12 +40,17 @@
- +
+
+ + + +