diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5bd668f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2011-2014 Twitter, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/readme.markdown b/README.md
similarity index 51%
rename from readme.markdown
rename to README.md
index 916d0fa..b5bcb89 100644
--- a/readme.markdown
+++ b/README.md
@@ -10,3 +10,23 @@ My first working example is GCM - Google Calendar Map. It uses a google calenda
The example code is messy. I wanted to get something up and running so i can experiment with features. My methods often involved pasting code from other sources, so forgive me for the quality of the code. I only recently pulled out some of the core javascript, cleaned it up, and put it in the src directory. It still could use some work, and i welcome suggestions.
+### Google Calendar V3 ###
+
+On Nov 17, 2014, Google deprecated v1 and v2 [src](http://googleappsupdates.blogspot.com/2014/10/deprecated-google-calendar-apis-v1-v2.html)
+In December 2014, GCM was updated to use v3. One of the key things in v3 is usage limits. These are controlled by either API keys (simple, public calendars only) or OAuth (more complicated, but can get private calendar data). GCM uses API keys, which are tied to domains.
+
+Therefore, **If you want to run this from any domain besides chadnorwood.com, you need to**
+
+1. Generate your own API key - https://developers.google.com/api-client-library/javascript/start/start-js#Setup
+1. Replace API key in examples/gcm/js/cnMapFilter.js with your key - search js for "Google API Key"
+
+### License ###
+
+This code is released under the terms of the [MIT license](LICENSE).
+
+The MIT License is simple and easy to understand and it places almost no restrictions on what you can do.
+You are free to use in any other project (even commercial projects) as long as the copyright header is left intact.
+
+
+
+[](http://githalytics.com/chadn/mapfilter)
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..9cea62b
--- /dev/null
+++ b/TODO
@@ -0,0 +1,42 @@
+What is the 'src' directory (ask Chad)?
+
+Remove external calls
+ $.getJSON("http://chadnorwood.com/saveJson/?callback=?", {sj: cnMF.reportData});
+
+
+[DONE] Write a feature list. See http://chadnorwood.com/projects/gcm/#KeyFeatures
+
+[DONE] What happens when the list of events is too long? See vert slider in
+ http://chadnorwood.com/gcm/?u=http://www.google.com/calendar/feeds/jo0ubvoi1gutr95kdtl1245u3g@group.calendar.google.com/public/basic
+
+[DONE] remove PHP code. See index.html
+
+[DONE] Create test calendar.
+ https://www.google.com/calendar/feeds/0ei0284so407vu24o7o0q5ares%40group.calendar.google.com/public/basic
+
+[DONE] non-ascii characters break the geo-locating code. Removed encode(), use instead http://www.w3schools.com/jsref/jsref_encodeuri.asp
+
+[TRIVIAL] Change dates has a debug popup that should be removed
+
+[TRIVIAL] Add link to FAQ in information message: "The Following Events Had Addresses (Where) That Could Not Be Found. See FAQ"
+ See FAQ.html for a start.
+
+[EASY] Remove the "_ errors" in the top right box. It's redundant with the warning link that appears.
+
+[EASY] When events are on the whole day, do not display time (second part of 2011-12-10 Sat 00:00)
+
+[EASY] Jump to an address, city, or zip - the zoom level should match the granularity: "Indiana" is a state-wide request.
+
+[MEDIUM] Inform the user of inconsistencies (in the calendar entries). Use a javascript alert?
+
+[MEDIUM] plot icon on the map should bear information. For example, number of events at location.
+
+[MEDIUM] Display several calendars at once: differentiate by color
+
+[MEDIUM] Inform the user for time-consuming operations (load calendar)
+
+[BUG] horizontal time slider filters OK the calendar table, but not the map (look for first chronolgical entry)
+
+[TEDIOUS] Translate application (add flags under the top right GCM logo; auto-detect language by IP)
+
+[???] Write an integration process: checkSyntax, minify, push to Web server
diff --git a/changelog.rst b/changelog.rst
new file mode 100644
index 0000000..026536a
--- /dev/null
+++ b/changelog.rst
@@ -0,0 +1,62 @@
+CHANGELOG
+=========
+
+* This file: https://github.com/chadn/mapfilter/blob/development/changelog.rst
+* Project homepage: http://chadnorwood.com/projects/gcm/
+* Working example: http://chadnorwood.com/gcm/
+
+2014-12-3
+----------
+
+* switched to Google Calendars API v3
+
+2012-6-6
+----------
+
+* sliders update - For IE, FF12+, and iPad, switched default to using "overlapping sliders", which is how the latest jqueryUI slider code works.
+ Originally the slider handles could be dragged up until they touched, but would not overlap.
+ Overlapping slider handles can be forced on by setting ``os=1`` in URL parameters.
+ Likewise, the original slider behavior can be forced on by setting ``os=0`` in URL parameters. Example:
+ http://chadnorwood.com/gcm/?gc=asa5k2phscdna68j9e5410v6mc@group.calendar.google.com&ed=5&os=0
+
+2012-5-23 tag 0.3
+------------------
+
+* Created this changelog, which summarizes github commit msgs
+* Completed upgrade from google maps v2 to v3
+* Added Drawer Concept (aka right tab): Drawer can open/close by pressing +/-, it contains calendar info, date sliders, and table of matching events (all the things that used to always be present on the right side)
+* Changed default from xml url for calendars to calendar id, updated Help to explain this.
+* Fixed browser bugs, now works on IE7, IE8, IE9, Chrome, Safari, Firefox
+* Upgraded to jquery 1.7.1 and jqueryUI 1.8.18
+* Removed jScrollPane
+* Updates to analytics
+* Lots of code cleanup (always more to do)
+
+
+2012-5-1 tag 0.2
+------------------
+
+* Created development branch
+* Added multiple calendar support
+* Removed php, created and merged igorrosenberg-no-php branch
+* Removed ``src/cnMapFilter.js``, which was a duplicate of ``examples/gcm/js/cnMapFilter.js``
+* Upgraded to jquery 1.7, began api v3 upgrade
+* Updates to analytics
+
+
+2011-9-21 tag 0.1, from start in 2009
+-------------------------------------
+
+* The following was originally on the project homepage, http://chadnorwood.com/projects/gcm/
+* 2009-6-12 - Initial release of prototype
+* 2009-8-4 - change to 200 max events (from 25)
+* 2009-11-19 - updated to work with new google maps api.
+* 2010-01 - made a test version using date sliders.
+* 2010-6-20 - fixed slider handles so they do not overlap.
+* 2010-9-12 - Second version of GCM prototype. Initial one moved to gcm2009.
+* 2011-1-9 - Added basic support for recurring events.
+* 2011-3-29 - Finished some under-the-hood coding and released source code as mapfilter project on github. Read more on the GCM on github blog post.
+* 2011-9-21 - Added timezone support. See details in URL Options above.
+
+
+
diff --git a/examples/gcm/FAQ.html b/examples/gcm/FAQ.html
new file mode 100644
index 0000000..9498d8f
--- /dev/null
+++ b/examples/gcm/FAQ.html
@@ -0,0 +1,20 @@
+
+
+GCM FAQ
+
+
+
+GCM FAQ
+
+ The Following Events Had Addresses (Where) That Could Not Be Found
+ GCM relies on the “where” field found in the google calendar which has been specified.
+The value of this “where” field is converted into a latitude and longitude using Google's geolocation API;
+the conversion sometimes may fail for various reasons, but most usually it is because the where data is incomplete.
+To make sure the data is sufficient, copy and paste the “where” field into
+google maps . If a single location is returned, then the data is correct.
+
+
+
+
+
+
diff --git a/examples/gcm/config.php b/examples/gcm/config.php
deleted file mode 100644
index 2e89815..0000000
--- a/examples/gcm/config.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
\ No newline at end of file
diff --git a/examples/gcm/css/jScrollPane.css b/examples/gcm/css/jScrollPane.css
index fb410df..7694db1 100644
--- a/examples/gcm/css/jScrollPane.css
+++ b/examples/gcm/css/jScrollPane.css
@@ -62,4 +62,4 @@ a.jScrollArrowDown:hover {
}
a.jScrollActiveArrowButton, a.jScrollActiveArrowButton:hover {
background-color: #f00;
-}
\ No newline at end of file
+}
diff --git a/examples/gcm/css/mapFilter.css b/examples/gcm/css/mapFilter.css
index 458751c..886b5aa 100644
--- a/examples/gcm/css/mapFilter.css
+++ b/examples/gcm/css/mapFilter.css
@@ -1,64 +1,60 @@
/*
- * copy-paste prototype for mapFilter
- * 2010-4-8 chad norwood
- * 2010-6-1 chad norwood
- * 2010-9-2 chad norwood
+ * GCM Prototype - mapFilter
+ *
+ * TABLE OF CONTENTS
+ *
+ * Main
+ * Map
+ * GCM
+ * Popups
+ * Plugins
+ * tablesorter
+ * jScrollPane
+ * other
*/
+
-/* originally from tablesorter.style.css */
-table.tablesorter {
- font-family:arial;
- background-color: #CDCDCD;
- margin:10px 0pt 15px;
- font-size: 8pt;
- width: 100%;
- text-align: left;
-}
-table.tablesorter thead tr th, table.tablesorter tfoot tr th {
- background-color: #e6EEEE;
- border: 1px solid #FFF;
- font-size: 8pt;
- padding: 4px;
-}
-table.tablesorter thead tr .header {
- background-image: url(../img/bg.gif);
- background-repeat: no-repeat;
- background-position: center right;
- cursor: pointer;
-}
-table.tablesorter tbody td {
- color: #3D3D3D;
- padding: 4px;
- background-color: #FFF;
- vertical-align: top;
-}
-table.tablesorter tbody tr.odd td {
- background-color:#F0F0F6;
-}
-table.tablesorter thead tr .headerSortUp {
- background-image: url(../img/asc.gif);
-}
-table.tablesorter thead tr .headerSortDown {
- background-image: url(../img/desc.gif);
+/*
+ * Main
+ */
+body {
+ margin: 0;
+ padding: 0;
+ font-size:80%;
}
-table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp {
- background-color: #8dbdd8;
+
+
+/*
+ * Main | Map
+ */
+/*
+ * Google colors
+ * ocean blue: #99B3CC
+ * icon blue: #307BC2
+ * forrest green: #B5D29C
+ * highway exit green: #41A774
+ * orange highway: #FFC345
+ * yellow road: #FFFD8B
+ * grey city block: #EDEAE2
+ * orange-brown buidling: #DED2AC
+ */
+
+#map_id {
+ position:absolute;
+ top:0;
+ left:0;
+ height:100%;
+ width:100%;
+ background:#99B3CC;
}
-table.tablesorter {margin: 0 0 0 0;}
- /* Generic map, side bar holder styles */
- .MapBuilder {font: normal small verdana, arial, helvetica, sans-serif; font-size: 10pt; margin: 0px;}
- .MapBuilder a {text-decoration: none; color: #0066CC; background-color: transparent;}
- .MapBuilder a:hover {color: #F60; background-color: transparent;}
- .MapBuilder h1 {font-weight: bold; font-size: 16pt; color: #369; border-bottom: 2px solid #369;}
-
- /* Info Window styles */
- .IW { width: 340px;}
- .IWContent {height: 120px; overflow:auto;}
- .IWCaption {font-weight: bold; font-size: 12pt; color: #369; border-bottom: 2px solid #369;}
- .IWFooter {margin-top: 5px; font-size: 8pt; }
- .IWFooterZoom {}
- .IWDirections{background-color:#FFF;}
+/* Info Window styles */
+.IW { width: 340px;}
+.IWContent {height: 120px; overflow:auto;}
+.IWCaption {font-weight: bold; font-size: 12pt; color: #369; border-bottom: 2px solid #369;}
+.IWFooter {margin-top: 5px; font-size: 8pt; }
+.IWFooterZoom {}
+.IWDirections{background-color:#FFF;}
/* want to save linebreaks but also wordwrap */
.preWrapped {
@@ -74,179 +70,56 @@ table.tablesorter {margin: 0 0 0 0;}
margin-bottom: 10px;
}
-body {text-align:left;}
-.main {height: 100%; width:980px !important; width:982px;}
-.columns {width:145px; float:left; padding:0px; position:relative; z-index:100; overflow:auto;}
-.float {float:left; display:inline;}
-.topBottomBdr {border-width:1px 0px 1px 0px;}
-.leftRightBdr {border-width:0px 1px 0px 1px;}
-.content {margin:0px;}
-.grey1 {background-color:#DCC;}
-.incols {margin-right:-164px; float:right;}
-
-#wrap {margin-left:auto; margin-right:auto; text-align:left;}
-#float {background-color:#CCC;}
-#center {margin-left:-1px; margin-right:195px; width:780px !important; width:780px; background-color:#FFF; position:relative;}
-#rightC {margin-right:-174px; float:right;}
-#MapID2 {height:600px;}
-#footer {width:100%; height:20px; background-color:#FFF;}
-
-.classlist {
- margin: 0;
- padding: 0;
- list-style: none;
-}
-.classlist li {
- background: #ecfad7;
- padding: 10px 20px 10px;
- border-top: solid 1px #c4df9b;
- cursor: pointer;
-}
-.classlist li:hover {
- background: #f6ffe9;
-}
-
- body{ margin-top: 0; margin: 0; padding: 0; font-size:80%;}
- .ui-state-active { background: white; }
- .containerWrap {position:absolute; width: 99%; height: 98%; background: #99B3CC; }
- #gcmDiv { position:relative; width: 100%; height: 100%; background: white; }
- #MapID { width:67%; height:99%; float:left; background:blue;}
- #rtSide { width:32%; height:99%; float:right; }
- #tab-header { margin: 5px; font-size:200%; }
- #MapStats { font-size:70%; }
- #tab-container { margin: 1px; }
- #resizable3 { padding:5px; font-size:62%; position:absolute }
- #resizable4 { overflow:auto; }
- #footer { padding:5px; z-index:100;}
- #containerDELME h3 { text-align: center; margin: 0; margin-bottom: 10px; }
- #filtersTab { text-align: left; }
- /*a:hover { background: #E6E6E6; color: #555555; }*/
- .highlight2 { font: bold;background: #E6E6E6; color: #555555; }
- .highlight2 { font: bold;background: yellow; color: black; }
-
-p {clear:both;}
-
-#addTab { text-align:left;}
-#addForms { margin:0; padding: 0; clear: both; border:0;}
-#addForm { margin:2; padding: 1; border:0;}
-#addForm label {
-float:left;
-padding:0 8px 0 0;
-text-align:left;
-width:6em;
-}
-#addForm label.error { float: none; color: red; padding-left: .5em; vertical-align: top; }
-#addForm span {
-float:left;
-align:left;
-padding:0 0 0 8px;
-text-align:left;
-}
-
-
-.helpClass {
- float:left; text-align:left; padding:10px;
-}
-div.scrollContainer {
- overflow:hidden;
-}
-div.scrollContent {
- position:relative;
- top:0;
-}
-.scrollPane {
- height: auto;
- overflow: visible;
- background: #EEEEEE;
- float: left;
- text-align:left;
- position:absolute;
-}
-
-.jScrollPaneContainer {
- border: 5px solid #9999CC;
-}
-
-
-
-
-
-
- /*
- * for jScrollPane
- * http://www.kelvinluck.com/assets/jquery/jScrollPane/examples.html
- * note - if winxp is used, must pass options like this
- * $('#pane1').jScrollPane({showArrows:true, scrollbarWidth: 17});
- * note - if osX is used, like this
- * $('#pane2').jScrollPane({showArrows:true, scrollbarWidth: 15, arrowSize: 16});
- */
-.winXP .jScrollPaneTrack {
- background: url(../img/windows_track.gif) repeat-y;
-}
-.winXP .jScrollPaneDrag {
- background: url(../img/windows_drag_middle.gif) no-repeat 0 50%;
-}
-.winXP .jScrollPaneDragTop {
- background: url(../img/windows_drag_top.gif) no-repeat;
- height: 4px;
-}
-.winXP .jScrollPaneDragBottom {
- background: url(../img/windows_drag_bottom.gif) no-repeat;
- height: 4px;
-}
-.winXP a.jScrollArrowUp {
- height: 17px;
- background: url(../img/windows_arrow_up.gif) no-repeat 0 0;
-}
-.winXP a.jScrollArrowUp:hover {
- background-position: 0 -20px;
-}
-.winXP a.jScrollArrowDown {
- height: 17px;
- background: url(../img/windows_arrow_down.gif) no-repeat 0 0;
-}
-.winXP a.jScrollArrowDown:hover {
- background-position: 0 -20px;
-}
-.winXP a.jScrollActiveArrowButton, .winXP a.jScrollActiveArrowButton:hover {
- background-position: 0 -40px;
-}
+/*
+ * Main | GCM
+ *
+ * GCM components that go on top of the map
+ */
-.osX .jScrollPaneTrack {
- background: url(../img/osx_track.gif) repeat-y;
-}
-.osX .jScrollPaneDrag {
- background: url(../img/osx_drag_middle.gif) repeat-y;
-}
-.osX .jScrollPaneDragTop {
- background: url(../img/osx_drag_top.gif) no-repeat;
- height: 6px;
-}
-.osX .jScrollPaneDragBottom {
- background: url(../img/osx_drag_bottom.gif) no-repeat;
- height: 7px;
-}
-.osX a.jScrollArrowUp {
- height: 24px;
- background: url(../img/osx_arrow_up.png) no-repeat 0 -30px;
-}
-.osX a.jScrollArrowUp:hover {
- background-position: 0 0;
-}
-.osX a.jScrollArrowDown {
- height: 24px;
- background: url(../img/osx_arrow_down.png) no-repeat 0 -30px;
-}
-.osX a.jScrollArrowDown:hover {
- background-position: 0 0;
+.transparent
+{
+ filter:alpha(opacity=90);
+ -moz-opacity: 0.90;
+ -khtml-opacity: 0.90;
+ opacity: 0.90;
+}
+
+#rightTab {
+ position:absolute;
+ top:40px;
+ right:0;
+ display:block;
+ height:30px;
+ width:30px;
+ cursor:pointer;
+ z-index:10;
+ background-color: #FFF;
+ font-size:24px;
+ font-weight:bold;
+ padding-left:6px;
+}
+#rtSide {
+ position:absolute;
+ top:70px;
+ right:0;
+ margin-right:0;
+ overflow:hidden;
+ z-index:2;
+}
+
+#gcmMapLogo {
+ position:absolute;
+ bottom:20px;
+ right:4px;
+ background: url(https://chadnorwood.com/beermug.ico) no-repeat 0 0;
+ height:17px;
+ width:17px;
+ overflow:hidden;
+ z-index:25500;
}
-
-
-
-
.ui-slider .ui-slider-handle {
width: 34px;
height: 25px;
@@ -328,8 +201,9 @@ div.scrollContent {
margin: 0;
}
#resultsData {
- overflow:auto;
+ overflow:hidden;
padding-right: 100px; /* match gcmLogo width */
+ background-color: #FFF;
}
#resultsDataFilters {
/*background-color: #E6EEEE;*/
@@ -345,60 +219,198 @@ div.scrollContent {
background-color: #B5D29C;
}
#ResultsMapHdr {
- padding: 2px 5px;
+ padding: 2px 5px;
+ background: #99B3CC;
}
-#gcmDiv .jumpLink {
+.jumpLink {
color: #0085D0;
color: #0045CC;
text-decoration: none;
}
-#gcmDiv .actionable {
+.actionable {
color: #005599;
text-decoration: none;
}
-#gcmDiv .actionable:hover,
-#gcmDiv .jumpLink:hover {
+.actionable:hover,
+.jumpLink:hover {
text-decoration: underline;
}
.helpContainer {
overflow: auto;
+ background: #99B3CC;
}
.helpContainer h3 {
padding-left: 5px;
}
-.exampleCalendars {
- float: left;
+
+
+
+#ResultsMapEventsTable tbody,
+#ResultsMapEvents {
+ overflow:auto;
+}
+.event_table {
+ display:block;
}
+.highlight2 {
+ font: bold;
+ background: #FFFD8B;
+ color: #555555;
+}
+
+/*
+ * Popups
+ */
+
#newDates {
display:none;
overflow: hidden;
/* width set in dialogDates in js */
text-align:left;
}
-#pickDates {
- position: relative;
- float: left;
+
+
+/*
+ * Plugins | tablesorter
+ *
+ * originally from tablesorter.style.css
+ */
+table.tablesorter {
+ font-family:arial;
+ margin:10px 0pt 15px;
+ font-size: 8pt;
+ width: 100%;
+ text-align: left;
}
-#submitDates {
- position: relative;
- float: left;
+table.tablesorter thead tr th, table.tablesorter tfoot tr th {
+ background-color: #e6EEEE;
+ border: 1px solid #FFF;
+ font-size: 8pt;
+ padding: 4px;
+}
+table.tablesorter thead tr .header {
+ background-image: url(../img/bg.gif);
+ background-repeat: no-repeat;
+ background-position: center right;
+ cursor: pointer;
+}
+table.tablesorter tbody td {
+ color: #3D3D3D;
+ padding: 4px;
+ background-color: #FFF;
+ vertical-align: top;
+ filter:alpha(opacity=90);
+ -moz-opacity: 0.90;
+ -khtml-opacity: 0.90;
+ opacity: 0.90;
+}
+table.tablesorter tbody tr.odd td {
+ background-color:#F0F0F6;
}
+table.tablesorter thead tr .headerSortUp {
+ background-image: url(../img/asc.gif);
+}
+table.tablesorter thead tr .headerSortDown {
+ background-image: url(../img/desc.gif);
+}
+table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp {
+ background-color: #8dbdd8;
+}
+table.tablesorter {margin: 0 0 0 0;}
+
+
+ /*
+ * Plugins | jScrollPane
+ *
+ * http://www.kelvinluck.com/assets/jquery/jScrollPane/examples.html
+ * note - if winxp is used, must pass options like this
+ * $('#pane1').jScrollPane({showArrows:true, scrollbarWidth: 17});
+ * note - if osX is used, like this
+ * $('#pane2').jScrollPane({showArrows:true, scrollbarWidth: 15, arrowSize: 16});
+ */
+
+.scrollPane {
+ height: auto;
+ overflow: visible;
+ background: #EEEEEE;
+ float: left;
+ text-align:left;
+ position:absolute;
+}
+
+.jScrollPaneContainer {
+ /*border: 5px solid #9999CC;*/
+}
+
+.winXP .jScrollPaneTrack {
+ background: url(../img/windows_track.gif) repeat-y;
+}
+.winXP .jScrollPaneDrag {
+ background: url(../img/windows_drag_middle.gif) no-repeat 0 50%;
+}
+.winXP .jScrollPaneDragTop {
+ background: url(../img/windows_drag_top.gif) no-repeat;
+ height: 4px;
+}
+.winXP .jScrollPaneDragBottom {
+ background: url(../img/windows_drag_bottom.gif) no-repeat;
+ height: 4px;
+}
+.winXP a.jScrollArrowUp {
+ height: 17px;
+ background: url(../img/windows_arrow_up.gif) no-repeat 0 0;
+}
+.winXP a.jScrollArrowUp:hover {
+ background-position: 0 -20px;
+}
+.winXP a.jScrollArrowDown {
+ height: 17px;
+ background: url(../img/windows_arrow_down.gif) no-repeat 0 0;
+}
+.winXP a.jScrollArrowDown:hover {
+ background-position: 0 -20px;
+}
+.winXP a.jScrollActiveArrowButton, .winXP a.jScrollActiveArrowButton:hover {
+ background-position: 0 -40px;
+}
+
+
+.osX .jScrollPaneTrack {
+ background: url(../img/osx_track.gif) repeat-y;
+}
+.osX .jScrollPaneDrag {
+ background: url(../img/osx_drag_middle.gif) repeat-y;
+}
+.osX .jScrollPaneDragTop {
+ background: url(../img/osx_drag_top.gif) no-repeat;
+ height: 6px;
+}
+.osX .jScrollPaneDragBottom {
+ background: url(../img/osx_drag_bottom.gif) no-repeat;
+ height: 7px;
+}
+.osX a.jScrollArrowUp {
+ height: 24px;
+ background: url(../img/osx_arrow_up.png) no-repeat 0 -30px;
+}
+.osX a.jScrollArrowUp:hover {
+ background-position: 0 0;
+}
+.osX a.jScrollArrowDown {
+ height: 24px;
+ background: url(../img/osx_arrow_down.png) no-repeat 0 -30px;
+}
+.osX a.jScrollArrowDown:hover {
+ background-position: 0 0;
+}
+
/*
- * Google calendar colors
- * ocean blue: #99B3CC
- * icon blue: #307BC2
- * forrest green: #B5D29C
- * highway exit green: #41A774
- * orange highway: #FFC345
- * yellow road: #FFFD8B
- * grey city block: #EDEAE2
- * orange-brown buidling: #DED2AC
+ * Plugins | other
*/
-/* end of file */
+.ui-state-active { background: white; }
+
+
-
-
-
\ No newline at end of file
diff --git a/examples/gcm/index.html b/examples/gcm/index.html
new file mode 100644
index 0000000..77f17f4
--- /dev/null
+++ b/examples/gcm/index.html
@@ -0,0 +1,100 @@
+
+
+
+
+ Google Calendar Map
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
What is this?
+
+Google Calendar Map (GCM) puts all events with a location from a google calendar on to a google map. The map is a filter, You can
+ zoom in and move map around to only view events that occur on the map as you see it. Additionally, you can use the date sliders to further filter events.
+
+
+Click on the big GCM in the top right view the GCM Homepage with more information.
+Click on - above GCM to hide this drawer, Click again to open it.
+
+
Example Calendars:
+
+ Geocaching (Spain)
+ Tear Jerkers (Camping USA)
+
+ Chad's Chicago: Summer Festivals and More
+ A New York Track Club
+ Collections (Bikes For The World)
+ All calendars above at once! <--- NEW !!!
+
+
+
+
How do I make it Go?
+
First, make your calendar public (if it ain't your calendar, ask owner to do it).
+
+ In the Google Calendar interface, locate "My Calendars" or "Other Calendars" lists on the left.
+ Hover over desired calendar, and click the arrow the appears. A menu will appear.
+ Click "Share this calendar"
+ Check "Make this calendar public"
+ Make sure "Share only my free/busy information" is unchecked.
+ Click "Save"
+
+
Second, find your Calendar's ID
+
+ In the Google Calendar interface, locate "My Calendars" or "Other Calendars" lists on the left.
+ Hover over desired calendar, and click the arrow the appears. A menu will appear.
+ Click "Calendar settings"
+ Look at the "Calendar Address" section of the screen, near XML, ICAL, HTML icons.
+ Copy string after "Calendar ID:" - it may be your email, or may look like vf3u7s6odj0r74q4lrnb730phk@group.calendar.google.com
+ NOTE: the calendar ID can also be extracted from the XML Feed. For example, xxxx@group.calendar.google.com is the id for this XML Feed:
+ https://www.google.com/calendar/feeds/xxxx@group.calendar.google.com/public/basic
+
+
Lastly, Paste the Google Calendar ID in the box above. Optionally add more Calendar IDs, separated by spaces. Click "Add Calendar" .. you're off!
+
.
+
+
+
+
+
diff --git a/examples/gcm/index.php b/examples/gcm/index.php
deleted file mode 100644
index 7610ca6..0000000
--- a/examples/gcm/index.php
+++ /dev/null
@@ -1,227 +0,0 @@
- $max) return $max;
- return $var;
-}
-
-foreach (array('r','c','z','m','sz','lat','lng','sd','ed') as $val) {
- if (@$_GET[$val]) {
- if (1 || preg_match('/^[\d\-\.]+$/',$_GET[$val])) {
- $valid[$val] = $_GET[$val];
- }
- }
-}
-foreach ($valid as $key => $val) {
- if ($key == 'r') $rating = validateRange($val,0,5, $rating);
- if ($key == 'c') $catg = validateRange($val,0,100, $catg);
- if ($key == 'z') $gZoomLevel = validateRange($val,1,20, $gZoomLevel);
- if ($key == 'm') $maptype = validateRange($val,0,4, $maptype);
- if ($key == 'lng') $ll = validateRange($val,-180,180, $ll);
- if ($key == 'lat') $lt = validateRange($val,-180,180, $lt);
- if ($key == 'sd') $sday = validateRange($val,-10,366, $sday);
- if ($key == 'ed') $eday = validateRange($val,-10,366, $eday);
- if ($key == 'sd') $sday = $val; // support 2010-9-23
- if ($key == 'ed') $eday = $val;
- if ($key == 'sz') {
- list($mh,$mw) = split('x',$key);
- $maph = validateRange($mh,200,2000, $maph);
- $mapw = validateRange($mw,200,2000, $mapw);
- }
-}
-$xmlurl = isset($_GET['u']) ? $_GET['u'] : 'u';
-$foo = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum commodo mollis tortor. Ut dapibus turpis consequat quam. Nulla lacinia. Donec nunc. Donec sollicitudin. Vivamus orci. Pellentesque tempus velit vitae odio. Maecenas enim arcu, volutpat ac, viverra id, bibendum eu, felis. Vestibulum imperdiet arcu. Ut nisi. Cras vel lectus consectetuer mauris luctus ultrices. Duis fringilla pellentesque sapien.';
-
-
-
-?>
-
-
-
-
- Google Calendar Map
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -->
- */ ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
What is this?
-
- Google Calendar Map (GCM) puts all events with a location from a google calendar on to a google map. The map is a filter, You can
- zoom in and move map around to only view events that occur on the map as you see it.
-
-
- Click on the big GCM in the top right view the GCM Homepage with more information.
-
-
Example Calendars:
-
- Chad's Chicago: Summer Festivals and More
- A New York Track Club
- 2011 Trail Races
- Golf Survey Tour
- Pumping Station One
-
-
-
How do I make it Go?
-
First, make your calendar public (if it ain't your calendar, ask owner for XML feed URL).
-
- In the Google Calendar interface, locate "My Calendar" box on the left.
- Click the arrow next to the calendar you need.
- A menu will appear. Click "Share this calendar"
- Check "Make this calendar public"
- Make sure "Share only my free/busy information" is unchecked.
- Click "Save"
-
-
Second, find your calendar's XML feed URL
-
- In the Google Calendar interface, locate the "My Calendar" box on the left
- Click the arrow next to the calendar you need.
- A menu will appear. Click "Calendar settings"
- In the "Calendar Address" section of the screen, click the XML badge.
- Your feed's URL will appear. Copy It
-
-
Third, Paste the URL of your Google Calendar XML Feed in the box above, click "Add Cal"
-
-
-
-
-
-
diff --git a/examples/gcm/js/cnMapFilter.js b/examples/gcm/js/cnMapFilter.js
index b6f25d3..37c0bff 100644
--- a/examples/gcm/js/cnMapFilter.js
+++ b/examples/gcm/js/cnMapFilter.js
@@ -5,7 +5,7 @@
// only showing items that have coordinates/address on map's currently displayed canvas.
//
//
-// Copyright (c) 2011 Chad Norwood
+// Copyright (c) 2009-2012 Chad Norwood
// Dual licensed under the MIT and GPL licenses:
// http://www.opensource.org/licenses/mit-license.php
// http://www.gnu.org/licenses/gpl.html
@@ -13,23 +13,30 @@
// cnMF, or window.cnMF, is the one and only global. See cnMF.init() below
-(function (window){
+(function (){
// ba-debug.js - use debug.log() instead of console.log()
// debug.setLevel(0) turns off logging,
// 1 is just errors and timers
- // 2 includes warnings
- // 3 includes info - logs external data (calendar)
- // 9 is everything (log, info, warn, error)
+ // 2 includes warn - these are things that should probably not happen, but nothing breaks if they do
+ // 3 includes info - tracks user initiated clicks, etc. Does not include google maps zoom, drags, etc.
+ // 4 includes debug - network related and key data (fetching calendar data, geocoding, etc)
+ // 5+ includes log, which can be anything
var lvl = window.location.href.match(/\bdebuglevel=(\d)/i);
if (lvl && lvl[1]) {
debug && debug.setLevel(parseInt(lvl[1],10));
//console.log("setLevel=",lvl[1]);
} else {
- debug && debug.setLevel(0); // turns off all logging
+ debug && debug.setLevel(9); // turns off all logging
}
debug.includeMsecs(true);
+ //
+ // Note on architecture of markers and events:
+ // EventClass - info on one event, including associated marker
+ // MarkerClass - info on one marker, its coords, google marker, and event list
+ // MarkersClass - contains all markers and info window
+ //
// EventClass - event objects created from this class contain all the
// info needed for an event, including original info from the calendar data
@@ -43,6 +50,7 @@
this.lg = 0;
this.validCoords = false;
this.isDisplayed = false;
+ this.markerObj = null;
this.addrOrig = '';
this.addrToGoogle = '';
this.addrFromGoogle = '';
@@ -54,18 +62,16 @@
// these are based on params
this.dateStartObj = cnMF.parseDate(this.dateStart);
this.dateEndObj = cnMF.parseDate(this.dateEnd);
+ return this;
};
EventClass.prototype.getCoordsStr = function(){
return this.validCoords ? this.lt + "," + this.lg : '';
};
EventClass.prototype.getDirectionsUrlStr = function(){
- return 'http://maps.google.com/maps?f=d&q=' + this.addrToGoogle.replace(/ /g, '+').replace(/"/g, '%22');
- };
- EventClass.prototype.getDirectionsHtmlStr = function(){
- return 'Directions ';
+ return 'https://maps.google.com/maps?q=' + this.addrToGoogle.replace(/ /g, '+').replace(/"/g, '%22');
};
EventClass.prototype.insideCurMap = function(mapbox){
- return this.validCoords ? mapbox.containsLatLng(new GLatLng(this.lt, this.lg, true)) : false;
+ return this.validCoords ? mapbox.contains(new google.maps.LatLng(this.lt, this.lg)) : false;
};
// Returns true if current event occurs before start or after end.
EventClass.prototype.isFilteredbyDate = function(startDayOffset,endDayOffset){
@@ -79,84 +85,154 @@
EventClass.prototype.setId = function ( id ) {
this.id = id;
};
- EventClass.prototype.setMarkerObj = function (mrkr) {
- //this.myGLatLng = existingGLatLng || new GLatLng(this.lt, this.lg);
- this.markerObj = mrkr;
+ EventClass.prototype.setMarkerObj = function (markerObj) {
+ this.markerObj = markerObj;
};
+ // returns markerObj or null if no marker created
EventClass.prototype.getMarkerObj = function () {
return this.markerObj;
};
+ EventClass.prototype.getGoogleMarker = function () {
+ return this.markerObj && this.markerObj.googleMarker
+ };
-
- // MarkerClass - designed to contain all google markers for map.
+ //
+ // MarkersClass - designed to contain all google markers for map.
// Note that one marker can contain multiple events.
- var MarkerClass = makeClass();
- MarkerClass.prototype.init = function ( gMap ) {
+ var MarkersClass = makeClass();
+ MarkersClass.prototype.init = function ( gMap ) {
// allMarkers obj is the main obj, where key is the coordinates and the
- // value is markerObject - see addMarker
+ // value is markerObject - see createMarker()
this.allMarkers = {};
this.gMap = gMap; // the google map object
+ this.infoWindowMarker = null;
+ this.infoWindow = new google.maps.InfoWindow({
+ size: new google.maps.Size(50,50)
+ });
+ var myMarkers = this;
+ google.maps.event.addListener(this.infoWindow, 'closeclick', function() {
+ myMarkers.closeInfoWindow();
+ });
+ }
+ MarkersClass.prototype.infoWindowIsOpen = function(){
+ return this.infoWindowMarker != null;
+ }
+ MarkersClass.prototype.openInfoWindow = function(content, markerObj){
+ if (this.infoWindowMarker) {
+ // close infoWindow attached to another google marker
+ this.infoWindow.close(); // API3 Use setPosition() instead of closing / opening?
+ }
+ this.infoWindowMarker = markerObj.googleMarker;
+ this.infoWindow.setContent(content);
+ this.infoWindow.open(this.gMap, markerObj.googleMarker);
+ cnMF.coreOptions.cbOpenedInfoWindow();
+ }
+ MarkersClass.prototype.closeInfoWindow = function(){
+ this.infoWindow.close();
+ this.infoWindowMarker = null;
+ cnMF.coreOptions.cbClosedInfoWindow();
}
- // getMarkerObj() returns the marker object
- MarkerClass.prototype.getMarkerObj = function(coordsStr){
+ // getMarkerObj() returns the marker object or null if not found
+ // coordsStrOrEvent can be one of three things: coordinates string, event object, event index number
+ MarkersClass.prototype.getMarkerObj = function(coordsStrOrEvent){
// TODO also accept event obj (then we get coords from that)
- return this.allMarkers[coordsStr];
+ if (typeof coordsStrOrEvent === 'string') {
+ return this.allMarkers[coordsStrOrEvent]; // coordsStr
+
+ } else if (typeof coordsStrOrEvent === 'object') {
+ // note that an event object can be created without a marker object,
+ // if need to check marker existance, best use eventObj.getMarkerObj()
+ return this.allMarkers[coordsStrOrEvent.getCoordsStr()]; // coordsStrOrEvent = eventObj
+
+ } else if (typeof coordsStrOrEvent === 'number') {
+ // note that an event object can be created without a marker object,
+ // if need to check marker existance, best use eventObj.getMarkerObj()
+ return this.allMarkers[cnMF.eventList[coordsStrOrEvent].getCoordsStr()]; // coordsStrOrEvent = event index
+
+ } else {
+ debug.warn("**** getMarkerObj(): only accept event obj or coords string, not: ", typeof coordsStrOrEvent, coordsStrOrEvent);
+ return null;
+ }
}
// getGoogleMarker() returns the GMarker object created by google.
- MarkerClass.prototype.getGoogleMarker = function(coordsStr){
- // TODO also accept event obj (then we get coords from that)
- return this.allMarkers[coordsStr] && this.allMarkers[coordsStr].googleMarker;
+ MarkersClass.prototype.getGoogleMarker = function(coordsStrOrEvent){
+ return this.getMarkerObj(coordsStrOrEvent).googleMarker;
}
// getEvents() returns an array of all event objects at the provided coordinates.
- MarkerClass.prototype.getEvents = function(coordsStr){
- // TODO also accept event obj (then we get coords from that)
- return this.allMarkers[coordsStr] && this.allMarkers[coordsStr].eventList;
+ MarkersClass.prototype.getEvents = function(coordsStrOrEvent){
+ return this.getMarkerObj(coordsStrOrEvent).eventList;
+ }
+ // showEvent() shows google marker via APIv3 setVisible, creating if necessary
+ MarkersClass.prototype.showEvent = function(eventObj){
+ var markerObj = this.getMarkerObj(eventObj) || this.createMarkerObj(eventObj);
+
+ markerObj.googleMarker.setVisible(true);
+ eventObj.setMarkerObj(markerObj);
+
+ // make sure event is in markerObj's event list
+ for (var ii=0; ii < markerObj.eventList.length; ii++) {
+ if (markerObj.eventList[ii] === eventObj.id) {
+ // found it, so no need to add
+ return;
+ }
+ }
+ // event was not found in markerObj, so add it.
+ markerObj.eventList.push(eventObj.id);
}
- // addMarker() creates google markers and event listener.
- MarkerClass.prototype.addMarker = function(eventObj){
+ // createMarkerObj() creates google markers and event listener.
+ MarkersClass.prototype.createMarkerObj = function(eventObj){
var coordsStr = eventObj.getCoordsStr();
- if (this.allMarkers[coordsStr]) {
- // Already have a marker at this location, so add event to list.
- this.allMarkers[coordsStr].eventList.push(eventObj.id);
- //debug.log("addMarker() added to existing marker, eventObj.id="+eventObj.id);
+ debug.log("createMarkerObj() creating marker, eventObj.id=", eventObj.id, coordsStr, eventObj);
+ return this.allMarkers[coordsStr] = MarkerClass(eventObj, this.gMap);
+ }
+ MarkersClass.prototype.hideEvent = function(eventObj){
+ var markerObj = this.getMarkerObj(eventObj);
+ if (!markerObj) {
return;
}
- // No markers existing at this location, so create one.
- //debug.log("addMarker() creating marker, eventObj.id=%s, %o", eventObj.id, eventObj.getCoordsStr());
- var myGLatLng = new GLatLng(eventObj.lt, eventObj.lg);
- // http://code.google.com/apis/maps/documentation/javascript/v2/reference.html#GMarker
- var gMrkr = new GMarker( myGLatLng, {
- icon:iconDefault
- });
- GEvent.addListener(gMrkr, "click", function() {
+ for (var ii=0; ii < markerObj.eventList.length; ii++) {
+ if (markerObj.eventList[ii] === eventObj.id) {
+ // found it, remove
+ markerObj.eventList.splice(ii, 1);
+ }
+ }
+ if (markerObj.eventList.length === 0) {
+ markerObj.googleMarker.setVisible(false);
+ }
+ }
+
+
+ // markerObj used in markersClass
+ var MarkerClass = makeClass();
+ MarkerClass.prototype.init = function ( eventObj, gMap ) {
+ this.coordsStr = eventObj.getCoordsStr();
+ this.eventList = [eventObj.id];
+ this.googleMarker = new google.maps.Marker({
+ position: new google.maps.LatLng(eventObj.lt, eventObj.lg),
+ map: gMap
+ });
+
+ // associate with eventObj
+ eventObj.setMarkerObj(this);
+
+ // add listener and callback hooks
+ var thisMarkerObj = this;
+ google.maps.event.addListener(thisMarkerObj.googleMarker, 'click', function() {
+ debug.log("googleMarker clicked at "+ thisMarkerObj.coordsStr);
+ cnMF.coreOptions.cbMarkerClicked(thisMarkerObj); // cbMarkerClicked function should call openInfoWindow()
try {
- _gaq.push(['_trackEvent', 'Interaction', 'gMrkr', 'click']);
+ _gaq.push(['_trackEvent', 'Interaction', 'gMarker', 'click']);
} catch (e) {}
- cnMF.coreOptions.cbHighlightItem(eventObj.getCoordsStr());
});
- this.gMap.addOverlay(gMrkr);
- this.allMarkers[coordsStr] = {
- googleMarker: gMrkr,
- gLatLng: myGLatLng, // note this can also be accessed via googleMarker.gLatLng()
- eventList: [eventObj.id]
- }
- return;
+ return this;
+ }
+ MarkerClass.prototype.getEvents = function(){
+ return this.eventList;
}
- MarkerClass.prototype.removeMarkers = function(eventObj){
- var coordsStr = eventObj.getCoordsStr();
- for (var ii=0; this.allMarkers[coordsStr].eventList[ii]; ii++) {
- if (this.allMarkers[coordsStr].eventList[ii] === eventObj.id) {
- // found it, remove
- this.allMarkers[coordsStr].eventList.splice(ii, 1);
- }
- }
- if (this.allMarkers[coordsStr].eventList.length === 0) {
- this.gMap.removeOverlay(this.allMarkers[coordsStr].googleMarker);
- // remove gLatLng?
- delete this.allMarkers[coordsStr];
- }
+ MarkerClass.prototype.getEvent = function(markerEventIndex){
+ return this.eventList[markerEventIndex];
}
@@ -181,7 +257,6 @@
var cnMF = {
gcTitle: 'A Calendar', // this should always get overwritten by calendar data
gcLink: '',
- googleApiKey: '',
reportData: {},
processGeocodeTimer: 0,
numDisplayed: 0,
@@ -197,17 +272,27 @@
//
cnMF.init = function (coreOptions) {
cnMF.coreOptions = coreOptions;
- cnMF.myMarkers = MarkerClass(coreOptions.gMap); // stores google map marker, and marker's corresponding events
+ cnMF.myMarkers = MarkersClass(coreOptions.gMap); // stores google map marker, and marker's corresponding events
cnMF.curStartDay = coreOptions.oStartDay;
cnMF.curEndDay = coreOptions.oEndDay;
cnMF.origStartDay = coreOptions.oStartDay;
cnMF.origEndDay = coreOptions.oEndDay;
- cnMF.googleApiKey = coreOptions.googleApiKey;
+ // if any callback functions are not defined as functions, assign them to empty functions so
+ // code can assume they are functions after this point
+ $.each(['cbOpenedInfoWindow','cbClosedInfoWindow','cbMapRedraw','cbMarkerClicked'],
+ function(index, cbfunc){
+ if (typeof cnMF.coreOptions[cbfunc] !== 'function') {
+ cnMF.coreOptions[cbfunc] = function() {}
+ }
+ }
+ );
cnMF.tz.offset = coreOptions.tzOffset ? coreOptions.tzOffset : ''; // Offset in hours and minutes from UTC
cnMF.tz.name = coreOptions.tzName ? coreOptions.tzName : 'unknown'; // Olson database timezone key (ex: Europe/Berlin)
cnMF.tz.dst = coreOptions.tzDst ? coreOptions.tzDst : 'unknown'; // bool for whether the tz uses daylight saving time
cnMF.tz.computedFromBrowser = (cnMF.tz.name != 'unknown');
+
+ cnMF.myGeo = cnMF.geocodeManager();
}
cnMF.countTotal = function () {
@@ -216,8 +301,8 @@
}
cnMF.countKnownAddresses = function () {
var xx = 0;
- for (var ii in cnMF.eventList) {
- if (cnMF.eventList[ii].validCoords) xx++;
+ for (var ii=0; ii < cnMF.eventList.length; ii++) {
+ if (cnMF.eventList[ii].validCoords) xx++;
}
//cnMF.reportData.knownAddr = xx;
return xx;
@@ -243,6 +328,59 @@
cnMF.types.push(e);
return e;
}
+ // returns event obj given event index number
+ cnMF.getEventObj = function(eventIndex){
+ var x = cnMF.eventList[parseInt(eventIndex)];
+ return x;
+ }
+ // returns event obj given gCalId, or null if not found
+ cnMF.getEventObjByCalId = function(gCalId){
+ for (var ii=0; cnMF.eventList[ii]; ii++) {
+ if (gCalId === cnMF.eventList[ii].gCalId) {
+ return cnMF.eventList[ii];
+ }
+ }
+ return null;
+ }
+ // returns event obj given gCalId, or null if not found
+ cnMF.updateEventByCalId = function(params){
+ var e = cnMF.getEventObjByCalId(params.gCalId);
+ if (e) {
+ return e.init(params);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Iterates through all GCM Events and builds array of unique addresses
+ * @return {object}
+ */
+ cnMF.gatherUniqAddr = function gatherUniqAddr() {
+ var kk, uniqAddr = {};
+ for (var ii=0; cnMF.eventList[ii]; ii++) {
+ kk = cnMF.eventList[ii];
+ kk.addrToGoogle = kk.addrToGoogle.replace(/\([^\)]+\)\s*$/, ''); // remove parens and text inside parens
+ if (kk.addrToGoogle) {
+ uniqAddr[kk.addrToGoogle] = 1;
+ } else {
+ debug.debug(" Skipping blank address for "+kk.name+" ["+kk.addrOrig+"]",kk);
+ }
+ }
+ return uniqAddr;
+ }
+
+ cnMF.addCal = function(calendarId, calData){
+ // console.log("cnMF.addCal", calendarId, calData);
+
+ if (!cnMF.calData) {
+ cnMF.calData = {};
+ }
+ cnMF.calData[calendarId] = calData;
+
+ //$.extend(cnMF, calData); // TODO2 this overwrites, figure out better way to display multiple calendar names
+ }
+
//
// mapAllEvents() adjusts zoom and coords in order to fit all events on map
@@ -250,26 +388,29 @@
cnMF.mapAllEvents = function(){
// first create the box that holds all event locations
var box = null;
+ var gMap = cnMF.coreOptions.gMap;
+ var numValidCoords = 0;
for (var i in cnMF.eventList) {
var kk = cnMF.eventList[i];
if (! kk.validCoords) continue; // skip unrecognized addresses
+ numValidCoords++;
+ //debug.log("mapAllEvents(): adding event "+ kk.id + ", "+ kk.name);
if (box === null) {
- var corner = new GLatLng(kk.lt, kk.lg, true);
- box = new GLatLngBounds(corner, corner);
+ var corner = new google.maps.LatLng(kk.lt, kk.lg);
+ box = new google.maps.LatLngBounds(corner, corner);
} else {
- box.extend(new GLatLng(kk.lt, kk.lg, true));
+ box.extend( new google.maps.LatLng(kk.lt, kk.lg) );
}
}
-
if (!box) {
debug.log("mapAllEvents(): no events");
return false;
}
-
- debug.log("mapAllEvents(): setting new map ");
- zoom = cnMF.coreOptions.gMap.getBoundsZoomLevel(box);
- cnMF.coreOptions.gMap.setCenter( box.getCenter(), (zoom < 2) ? zoom : zoom - 1 );
+ debug.log("mapAllEvents(): setting new map, "+ numValidCoords + " of "+ cnMF.eventList.length + " with valid coords. bounds: " + box.toString());
+ //zoom = gMap.getBoundsZoomLevel(box);
+ //gMap.setCenter( box.getCenter(), (zoom < 2) ? zoom : zoom - 1 );
+ gMap.fitBounds(box); // API3 see also panToBounds()
}
cnMF.processGeocode = function(gObj) {
@@ -308,28 +449,32 @@
- // returns true if changes were made to map, false otherwise
+ // Checks all events, hiding/showing corresponding markers if need be.
+ // Returns true if changes were made to map, false otherwise
//
cnMF.updateMarkers= function(){
- debug.log( "cnMF.updateMarkers() called ..");
- mapbox = cnMF.coreOptions.gMap.getBounds();
+ //debug.log( "cnMF.updateMarkers() called ..");
+ var mapbox = cnMF.coreOptions.gMap.getBounds();
+ //debug.log( "cnMF.updateMarkers() called, bounds:"+ mapbox.toString());
- if(0) debug.time('checking all markers');
- added = 0;
- removed = 0;
- unchanged = 0;
+ //debug.time('checking all markers');
+ var added = 0;
+ var removed = 0;
+ var unchanged = 0;
+ var filtered = 0;
cnMF.filteredByDate = false;
cnMF.filteredByMap = false;
//debug.log( "reset filteredByDate and filteredByMap to FALSE");
+ // loop through all events and see which ones are in map, etc.
for (var i in cnMF.eventList) {
var kk = cnMF.eventList[i];
insideCurMap = kk.insideCurMap(mapbox);
- debug.log( "marker "+ (insideCurMap ? 'in':'out') +"side map %o", kk);
- if (!insideCurMap) {
- debug.log( " filteredByMap = true for ",kk);
+ //debug.log( "marker "+ (insideCurMap ? 'in':'out') +"side map. valid, id, obj: ", kk.validCoords, kk.id, kk);
+ if (!insideCurMap && kk.validCoords) {
+ //debug.log( " filteredByMap = true for ",kk);
cnMF.filteredByMap = true;
}
@@ -343,6 +488,8 @@
*/
if (filteredOut) {
cnMF.filteredByDate = true;
+ cnMF.myMarkers.hideEvent(kk);
+ filtered++;
}
if (kk.isDisplayed && insideCurMap && !filteredOut) {
unchanged++;
@@ -350,30 +497,28 @@
else if (kk.isDisplayed && (!insideCurMap || filteredOut)) {
// hide all markers outside of map or current filters
kk.isDisplayed = false;
- cnMF.myMarkers.removeMarkers(kk);
+ cnMF.myMarkers.hideEvent(kk);
removed++;
}
else if (!kk.isDisplayed && insideCurMap && !filteredOut) {
// display events new to map
- cnMF.coreOptions.cbBuildInfoHtml(kk);
- cnMF.myMarkers.addMarker(kk);
+ cnMF.myMarkers.showEvent(kk);
kk.isDisplayed = true;
added++;
}
}
- if(0) debug.timeEnd('checking all markers');
+ //debug.timeEnd('checking all markers');
cnMF.numDisplayed = unchanged + added;
//cnMF.reportData.numDisplayed = cnMF.numDisplayed;
- debug.info("updateMarkers() "+removed+" removed, "+added+" added, "+unchanged+" unchanged, "
+ debug.debug(" updateMarkers() "+removed+" removed, "+added+" added, "+unchanged+" unchanged, "
+cnMF.numDisplayed+" total ");
return (removed || added);
}
-
// showDays(): Shows all events from newStartDay to newEndDay, called when date slider dragging stops
// newStartDay to newEndDay are both number of days relative to today
// Currently, newStartDay and newEndDay should be within original calendar start/end days (no ajax req'd)
@@ -386,125 +531,248 @@
}
- cnMF.getGCalData = function(gCalUrl, startDays, endDays, callbacks ) {
- if (gCalUrl.search(/^http/i) < 0) {
- debug.warn("getGCalData(): bad url: "+ gCalUrl);
- return;
- }
- gCalUrl = gCalUrl.replace(/\/basic$/, '/full');
+ cnMF.getGCalData = function getGCalData(calendarId, startDays, endDays, callbacks ) {
+ var gCalUrl = 'https://content.googleapis.com/calendar/v3/calendars/'+ calendarId +'/events';
+
//startmax = '2009-07-09T10:57:00-08:00';
// TODO: change rfc3339 to accept StartDays
startDate = new Date();
startDate.setTime(startDate.getTime() + startDays*24*3600*1000);
startmin = cnMF.rfc3339(startDate,false);
- debug.info("getGCalData(): start-min: "+startmin);
+ debug.debug(" getGCalData(): start-min: "+startmin);
endDate = new Date();
endDate.setTime(endDate.getTime() + endDays*24*3600*1000);
startmax = cnMF.rfc3339(endDate,true);
- debug.info("getGCalData(): start-max: "+startmax);
-
- // http://code.google.com/apis/calendar/docs/2.0/reference.html
+ debug.debug(" getGCalData(): start-max: "+startmax);
+
+ // https://developers.google.com/google-apps/calendar/v3/reference/#Events
+ // list events: GET /calendars/calendarId/events
+ // https://developers.google.com/apis-explorer/#s/calendar/v3/calendar.events.list?calendarId=dnr6osjdrtn4fqpf70ep8ck1rc%2540group.calendar.google.com&_h=1&
+ // TODO: support 'nextPageToken' for multi-page response
+ // https://developers.google.com/google-apps/calendar/v3/reference/events/list
+ // http://googleappsdeveloper.blogspot.com/2011/12/calendar-v3-best-practices-recurring.html
gCalObj = {
- 'start-min': startmin,
- 'start-max': startmax,
- 'max-results': 200,
- 'orderby' : 'starttime',
- 'sortorder': 'ascending',
- 'singleevents': false
+ 'timeMin': startmin,
+ 'timeMax': startmax,
+ 'max-results': 250, // default, can go up to 2500
+ //'orderBy' : 'startTime',
+ 'singleevents': true,
+ 'key':'AIzaSyD3pfT1PsOmnmGMLOqfcPuLUErZ0ZIfF-Q' // Google API Key 2018-7-16 no restrictions
+ //'key':'AIzaSyBYxFbC7UOW7RreLsERo1gOgKVcup4jPqk' // Google API Key for chadnorwood.com, created 2015-1-6
+ // 'key':'AIzaSyCdpWK6w91IDKmaGtbhPkPtWrZfroi07WQ' // Google API Key for chadnorwood.com 2014
};
if (cnMF.tz.name != 'unknown') {
gCalObj.ctz = cnMF.tz.name; // ex: 'America/Chicago'
- debug.info("Displaying calendar times using this timezone: "+ gCalObj.ctz);
+ debug.debug(" Displaying calendar times using this timezone: "+ gCalObj.ctz);
}
- $.getJSON(gCalUrl + "?alt=json-in-script&callback=?", gCalObj, function(cdata) {
- parseGCalData(cdata, startDate, endDate, callbacks);
+ $.ajax({
+ url: gCalUrl + "?callback=?",
+ dataType: 'json',
+ data: gCalObj,
+ timeout: 12000, // 12 secs
+ success: function(cdata) {
+ cnMF.parseGCalData(calendarId, cdata, startDate, endDate, callbacks);
+ },
+ complete: function (jqXHR, textStatus) {
+ if (textStatus !== 'success') {
+ debug.warn("**** getGCalData problem: ", textStatus, calendarId, jqXHR);
+ if ('function' === typeof callbacks.onError) {
+ callbacks.onError(jqXHR, textStatus);
+ }
+ }
+ }
});
}
- function parseGCalData (cdata, startDate, endDate, callbacks ) {
+ cnMF.parseGCalData = function parseGCalData (calendarId, cdata, startDate, endDate, callbacks ) {
+ var calendarInfo = {},
+ uniqAddr = {};
+
+ debug.debug(" parseGCalData() calendar data: ",cdata);
- debug.info("parseGCalData() calendar data: ",cdata);
+ calendarInfo.calendarId = calendarId;
+ calendarInfo.gcTitle = cdata.summary || 'title unknown';
+ calendarInfo.gcTitle.replace(/"/,'"');
+ calendarInfo.desc = cdata.description || '';
+ calendarInfo.gcLink = 'https://www.google.com/calendar/embed?src='+ calendarId;
+ calendarInfo.totalEntries = 0; // each instance of recurring events are counted
- cnMF.gcTitle = cdata.feed.title ? cdata.feed.title['$t'] : 'title unknown';
- cnMF.gcLink = cdata.feed.link ? cdata.feed.link[0]['href'] : '';
- cnMF.desc = cdata.feed.subtitle ? cdata.feed.subtitle['$t'] : 'subtitle unknown';
- cnMF.reportData['fn'] = cnMF.gcTitle.replace(/\W/,"_");
- cnMF.gcTitle = cdata.feed.title ? cdata.feed.title['$t'] : 'title unknown';
+ // TODO2 move this to a method
if (!cnMF.tz.computedFromBrowser) {
- cnMF.tz.name = cdata.feed.gCal$timezone.value;
- debug.info("Displaying calendar times using calendar timezone: "+ cnMF.tz.name);
+ cnMF.tz.name = cdata.timezone;
+ debug.debug(" Displaying calendar times using calendar timezone: "+ cnMF.tz.name);
+ // TODO2 count in analytics how many people use timezones in browser vs calendar
}
- var uniqAddr={};
- /* do we need this at all anymore?
- eType = cnMF.addEventType({
- tableHeadHtml: "one two ",
- tableCols: [3,5],
- title: cnMF.gcTitle,
- titleLink: cnMF.gcLink
- });
- */
- for (var ii=0; cdata.feed.entry && cdata.feed.entry[ii]; ii++) {
- var curEntry = cdata.feed.entry[ii];
- if (!(curEntry['gd$when'] && curEntry['gd$when'][0]['startTime'])) {
- debug.info("skipping cal curEntry (no gd$when) %s (%o)", curEntry['title']['$t'], curEntry);
- return true; // continue to next one
+ // Create multiple gcm events from gcal recurring events first,
+ // then process one-offs from recurring events (which will update created ones) and create non recurring events.
+ for (var ii=0; cdata.items && cdata.items[ii]; ii++) {
+ var curItem = cdata.items[ii];
+ if (curItem.recurrence) {
+ calendarInfo.totalEntries += cnMF.addEventsFromRecur(curItem, startDate, endDate, calendarInfo);
+ }
+ }
+ for (var ii=0; cdata.items && cdata.items[ii]; ii++) {
+ var curItem = cdata.items[ii];
+ if (curItem.recurrence) {
+ continue;
+ }
+ /*
+ if (!(curItem['gd$when'] && curItem['gd$when'][0]['startTime'])) {
+ debug.debug(" skipping cal curItem (no gd$when) %s (%o)", curItem['title']['$t'], curItem);
+ continue;
};
var url = {};
- for (var jj=0; curEntry.link[jj]; jj++) {
- var curLink = curEntry.link[jj];
+ for (var jj=0; curItem.link[jj]; jj++) {
+ var curLink = curItem.link[jj];
if (curLink.type == 'text/html') {
// looks like when rel='related', href is original event info (like meetup.com)
// when rel='alternate', href is the google.com calendar event info
url[curLink.rel] = curLink.href;
}
}
- kk = cnMF.addEvent({
+ */
+ // https://developers.google.com/google-apps/calendar/v3/reference/events#resource
+ if (curItem.endTimeUnspecified) {
+ debug.debug('Note that endTimeUnspecified==true ', curItem);
+ }
+ kk = {
//type: eType.id,
- name: curEntry['title']['$t'],
- desc: curEntry['content']['$t'],
- addrOrig: curEntry['gd$where'][0]['valueString'] || '', // addrOrig is the location field of the event
- addrToGoogle: curEntry['gd$where'][0]['valueString'] || '',
- gCalId: curEntry['gCal$uid']['value'],
- url: url.related || url.alternate, // TODO - is this what we want? see href above
- dateStart: cnMF.parseDate(curEntry['gd$when'][0]['startTime']),
- dateEnd: cnMF.parseDate(curEntry['gd$when'][0]['endTime'])
- });
- // make ready for geocode TODO: remove this? or move this line to addrToGoogle above
- kk.addrToGoogle = kk.addrToGoogle.replace(/\([^\)]+\)\s*$/, ''); // remove parens and text inside parens
- if (kk.addrToGoogle) {
- uniqAddr[kk.addrToGoogle] = 1;
+ calTitle: calendarInfo.gcTitle,
+ name: curItem.summary,
+ desc: curItem.description,
+ addrOrig: curItem.location || '', // addrOrig is the location field of the event
+ addrToGoogle: curItem.location || '',
+ gCalId: curItem.id,
+ //url: url.related || url.alternate, // TODO - is this what we want? see href above
+ url: curItem.htmlLink || '',
+ dateStart: cnMF.parseGCalDate(curItem.start, 'start'),
+ dateEnd: cnMF.parseGCalDate(curItem.end, 'end')
+ };
+ if (curItem.id.match(/_\d\d/) && cnMF.getEventObjByCalId(curItem.id)) {
+ kk = cnMF.updateEventByCalId(kk);
} else {
- debug.info("Skipping blank address for "+kk.name+" ["+kk.addrOrig+"]",kk);
+ kk = cnMF.addEvent(kk);
+ calendarInfo.totalEntries++;
}
- debug.log("parsed curEntry "+ii+": ", kk.name, curEntry, kk);
+ debug.log("parsed curItem "+ii+": ", kk.name, curItem, kk);
}
- cnMF.totalEntries = ii;
- cnMF.totalEvents = cdata.feed.openSearch$totalResults.$t || cnMF.totalEntries;
+ calendarInfo.totalEvents = ii; // recurring events counted once
+ cnMF.addCal(calendarId, calendarInfo);
+ cnMF.reportData['fn'] = calendarInfo.gcTitle.replace(/\W/,"_");
+ uniqAddr = cnMF.gatherUniqAddr();
debug.log("calling mapfilter.geocode(): ", uniqAddr );
cnMF.myGeoDecodeComplete = false;
- cnMF.myGeo = cnMF.geocodeManager({
- addresses: uniqAddr,
- googleApiKey: cnMF.googleApiKey,
- geocodedAddrCallback: function (gObj) {
+ cnMF.myGeo.addr2coords( uniqAddr,
+ function (gObj) {
cnMF.processGeocode(gObj); // TODO: this can be private
if ('function' === typeof callbacks.onGeoDecodeAddr) callbacks.onGeoDecodeAddr();
},
- geocodeCompleteCallback: function() {
+ function() {
//onGeoDecodeComplete();
cnMF.myGeoDecodeComplete = true;
if ('function' === typeof callbacks.onGeoDecodeComplete) callbacks.onGeoDecodeComplete();
}
- });
- if ('function' === typeof callbacks.onCalendarLoad) callbacks.onCalendarLoad();
+ );
+ if ('function' === typeof callbacks.onCalendarLoad) {
+ callbacks.onCalendarLoad(calendarInfo);
+ }
+ }
+
+
+ /**
+ * Converts google date object to javascript date object
+ * @param {object} gCalDate - google's date object used in event items.
+ * @param {string} startEnd - either 'start' or 'end'.
+ * @return {object Date}
+ */
+ cnMF.parseGCalDate = function parseGCalDate (gCalDate, startEnd) {
+ // https://developers.google.com/google-apps/calendar/v3/reference/events#resource
+ if (!gCalDate) {
+ debug.log("parseGCalDate() - warning: no gCalDate");
+ return new Date(1); // return epoch
+ }
+ if (gCalDate.dateTime) {
+ // ex: 2011-03-31T19:00:49.000Z
+ if (gCalDate.timeZone) {
+ debug.log("ignoring event timezone: ", gCalDate.timeZone );
+ }
+ return cnMF.parseDate(gCalDate.dateTime);
+ }
+ if (gCalDate.date) {
+ // all-day event
+ if ('end' === startEnd) {
+ return cnMF.parseDate(gCalDate.date + 'T23:59:59.000Z');
+ } else {
+ return cnMF.parseDate(gCalDate.date + 'T00:00:01.000Z');
+ }
+ }
+ }
+
+ /**
+ * Google calendar supports RFC 2445 recurrence syntax. Create multiple gcm events for each one.
+ * @param {object} curItem google's calendar event item.
+ * @param {object Date} startDate start of gcm date window
+ * @param {object Date} endDate end of gcm date window
+ * @return {number} count of events added between startDate and endDate
+ */
+ cnMF.addEventsFromRecur = function addEventsFromRecur(curItem, startDate, endDate, calendarInfo) {
+ if (!curItem.recurrence) {
+ return 0;
+ }
+ var dates, ii, options, rule, dateStart, gCalId;
+ /** @type {object Date} */
+ var curItemStart = cnMF.parseGCalDate(curItem.start, 'start');
+ /** @type {object Date} */
+ var durationMs = (cnMF.parseGCalDate(curItem.end, 'end')).getTime() - curItemStart.getTime();
+
+ // https://github.com/jakubroztocil/rrule
+ // note that constructor requires freq
+ options = RRule.parseString(curItem.recurrence[0].replace(/^RRULE:/,''));
+ options.dtstart = curItemStart;
+ rule = new RRule(options);
+ debug.debug('addEventsFromRecur rule', rule);
+ for (ii=1; curItem.recurrence[ii]; ii++) {
+ debug.debug('addEventsFromRecur recurrence '+ ii, curItem.recurrence[ii]);
+ rule.fromString(curItem.recurrence[ii].replace(/^RRULE:/,''));
+ }
+ // get list of dates from recurring event that falls between startDate & endDate
+ dates = rule.between(startDate, endDate, true);
+ debug.debug('addEventsFromRecur dates', dates);
+
+ // create gcm event for each of dates
+ for (ii=0; dates[ii]; ii++) {
+ dateStart = new Date(dates[ii]);
+
+ /* want to gCalId format to be same as google's item.id, so if google feed contains an item that
+ * is a one-off edit of a recurring event, we can easily allow that one-off to overwrite
+ * our generated events. ex: 2fbgvshoedaub6t0rctn7vq264_20141226T233000Z
+ */
+ gCalId = curItem.id + '_' + cnMF.rfc3339(dateStart,false)
+ .replace(/-\d\d:\d\d$/,'Z') // remove timezone info to match google format
+ .replace(/(-|:)/g,''); // remove to match google format
+ cnMF.addEvent({
+ gcalItem: curItem,
+ calTitle: calendarInfo.gcTitle,
+ name: curItem.summary,
+ desc: curItem.description,
+ addrOrig: curItem.location || '', // addrOrig is the location field of the event
+ addrToGoogle: curItem.location || '',
+ gCalId: gCalId,
+ url: curItem.htmlLink || '',
+ dateStart: dateStart,
+ dateEnd: new Date(dateStart.getTime() + durationMs)
+ });
+ }
+ return ii;
}
// geocodeManager handles the geocoding of addresses
- //
+ //
// TODO: use more than just google maps api
//
// http://tinygeocoder.com/blog/how-to-use/
@@ -520,14 +788,48 @@
// count addreses and unique addreses.
// don't want duplicates - wasting calls to google
- var uniqAddresses = {};
- var numAddresses = numUniqAddresses = numUniqAddrDecoded = numUniqAddrErrors = 0;
- var geoCache = {};
-
- var numReqs = 0;
- var startTime = new Date().getTime();
+
+ var GeocodeManager = {}, // return obj
+ addrObjects = [], // stores all address objects
+ resolveRequests = [], // array of requestObjects, each obj has add, cb1, cb2
+
+ //uniqAddresses = {}, // stores addr objects
+ numAddresses = 0,
+ //numUniqAddresses = 0,
+ //numUniqAddrDecoded = 0,
+ //numUniqAddrErrors = 0,
+ geoCache = {},
+ numReqs = 0,
+ startTime = new Date().getTime(),
+
+ // Google specific geocoding variables
+ desiredRate = 100, // long-term average should be one query every 'desiredRate' ms
+ reqTimeout = 2000, // reset after this
+ maxBurstReq = 4, // if timeout gets delayed, say 500ms, we can send 'maxBurstReq' at a time till we catch up
+ maxRetries = 2; // how many times an attempt to geocode service is made per address
- // won't stop till all address objects are resolved (resolved==true)
+
+ GeocodeManager.count = function() {
+ var c = {
+ uniqAddrDecoded: 0,
+ uniqAddrErrors: 0,
+ uniqAddrUnknown: 0,
+ uniqAddrTotal: 0
+ }
+ // resolved = uniqAddrDecoded + uniqAddrErrors
+ // uniqAddrUnknown = uniqAddrTotal - resolved
+ for (var addr in geoCache) {
+ ao = geoCache[addr];
+ c.uniqAddrTotal++;
+ if (!ao.resolved) c.uniqAddrUnknown++;
+ if (ao.resolved && ao.validCoords) c.uniqAddrDecoded++;
+ if (ao.resolved && !ao.validCoords) c.uniqAddrErrors++;
+ }
+ return c;
+ }
+
+
+ // GeocodeManager won't stop till all address objects are resolved (resolved==true)
// when resolved is true, then if (validCoords) it was successful
// otherwise look to error
//
@@ -542,79 +844,111 @@
this.lt = '';
this.lg = '';
}
-
- function geoMgrInit(){
- for (var ii in gOpts.addresses) {
+ function deDup(addresses) {
+ var uniqAddresses = {}, // stores addr objects
+ ret = [];
+ if (typeof addresses == 'string') {
+ return [addresses]
+ }
+ for (var ii in addresses) {
if (!(ii.length > 0)) {
- debug.warn("geocodeManager-geoMgrInit() skipping blank address");
+ debug.warn("**** geocodeManager-geoMgrInit() skipping blank address");
continue;
}
numAddresses++;
if (!isNaN(ii)) {
- uniqAddresses[gOpts.addresses[ii]] = 1; // array
+ uniqAddresses[addresses[ii]] = 1; // array
+ //console.log("CHAD TODO2 xx1", ii);
} else {
+ //console.log("CHAD TODO2 xx2", ii);
uniqAddresses[ii] = 1; // object or string
}
}
- for (var addr in uniqAddresses) {
- if (geoCache[addr]) {
- gOpts.geocodedAddrCallback(geoCache[addr]);
- continue;
- }
- geoCache[addr] = new addrObject(addr);
- numUniqAddresses++;
+ for (var ii in addresses) {
+ ret.push(ii);
}
- gGeocodeQueue();
+ return ret;
}
- function allResolved() {
- for (var addr in geoCache) {
- if (!geoCache[addr].resolved) return false;
+ // GeocodeManager.addr2coords(addresses, cb1, cbAll) - resolve a list of addresses
+ // addresses - object - key is address string, where each address string will map to a address object.
+ // cb1 - callback function - called when an address is resolved
+ // cbAll - callback function - called when all addresses are resolved. cb1 is not called after this.
+
+ GeocodeManager.addr2coords = function(addresses, cb1, cbAll) {
+ var uniqAddresses = deDup(addresses);
+ // store request
+ resolveRequests.push({
+ state: 'new',
+ addressesNew: uniqAddresses,
+ addressesResolved: [],
+ cb1 : cb1,
+ cbAll : cbAll
+ });
+
+ for (var ii = 0; ii < uniqAddresses.length; ii++) {
+ if (!geoCache[uniqAddresses[ii]]) {
+ geoCache[uniqAddresses[ii]] = new addrObject(uniqAddresses[ii]);
+ }
}
- return true;
+ checkRequests();
+ googleGeocodeQueue();
}
- function getUnresolved() {
- for (var addr in geoCache) {
- ao = geoCache[addr];
- if (!ao.resolved && !ao.inProgress) return ao;
- }
- return false;
- }
- function checkInProgress(ems) {
- for (var addr in geoCache) {
- ao = geoCache[addr];
- if (ao.inProgress && (ems - ao.sentLast > 2000)) {
- if (ao.sentTimes > 3) {
- ao.resolved = true;
- numUniqAddrErrors++;
- debug.log('checkInProgress() forgetting request '+ao.reqNum, ao);
- gOpts.geocodedAddrCallback(ao);
+ // compares resolveRequests against geoCache and does callbacks if addresses are resolved
+ // returns true if all request addresses are resolved, false if still need resolution.
+ function checkRequests() {
+ var curReq,
+ allResolved = true,
+ addrNotResolved,
+ addr;
+
+ for (var ii = 0; ii < resolveRequests.length; ii++) {
+ curReq = resolveRequests[ii],
+ addrNotResolved = [];
+
+ if (curReq.state === 'allResolved') {
+ continue;
+ }
+ allResolved = false;
+ for (var jj = 0; jj < curReq.addressesNew.length; jj++) {
+ addr = curReq.addressesNew[jj];
+
+ if (geoCache[addr].resolved) {
+ curReq.addressesResolved.push(addr);
+ if (typeof curReq.cb1 === 'function') {
+ curReq.cb1(geoCache[addr]);
+ }
} else {
- ao.inProgress = false;
- debug.log('checkInProgress() resetting request '+ao.reqNum+' after '+(ems-ao.sentLast)+'ms', ao);
+ addrNotResolved.push(addr);
+ }
+ }
+ curReq.addressesNew = addrNotResolved;
+ if (curReq.addressesNew.length === 0) {
+ debug.log("checkRequests() resolveRequests done");
+ curReq.state = 'allResolved';
+ if (typeof curReq.cbAll === 'function') {
+ curReq.cbAll();
}
}
}
- return false;
+ return allResolved;
}
- // gGeocodeQueue()
+
+ // googleGeocodeQueue()
//
- // Note: Google allows 15k lookups per day per IP. However, too many requests
- // at the same time triggers a 620 code from google. Therefore we want about 100ms
- // delay between each request using gGeocodeQueue. Likewise, when we get a 620 code,
+ // Note: Google allows 2.5k lookups per day per browser. However, too many requests
+ // at the same time triggers a OVER_QUERY_LIMIT code from google. Therefore we want about 100ms
+ // delay between each request using googleGeocodeQueue. Likewise, when we get a 620 code,
// we wait a bit and resubmit.
- // http://code.google.com/apis/maps/faq.html#geocoder_limit
+ // https://developers.google.com/maps/documentation/geocoding/#Limits
//
// NOTE: yahoo allows 5k lookups per day per IP
// http://developer.yahoo.com/maps/rest/V1/geocode.html
//
- var desiredRate = 100; // long-term average should be one query every 'desiredRate' ms
- var maxBurstReq = 4; // if timeout gets delayed, say 500ms, we can send 'maxBurstReq' at a time till we catch up
- var maxRetry = 4;
- function gGeocodeQueue () {
+ function googleGeocodeQueue () {
ems = new Date().getTime() - startTime;
bursts = maxBurstReq;
while (bursts-- && (ems > numReqs*desiredRate ) && (ao = getUnresolved())) {
@@ -622,33 +956,57 @@
ao.inProgress = true;
ao.sentLast = ems;
ao.sentTimes++;
- debug.log(" gGeocodeQueue() sending req "+numReqs+" at "+ems+"ms, addr: ", ao.addr1);
- cnMF.gGeocode( ao.addr1, gOpts.googleApiKey, function (gObj) {
+ debug.log(" googleGeocodeQueue() sending req "+numReqs+" at "+ems+"ms, addr: ", ao.addr1);
+ googleGeocode( ao.addr1, function (gObj) {
parseGObj(gObj);
});
ems = new Date().getTime() - startTime;
}
checkInProgress(ems);
- if (allResolved()) {
- debug.log("gGeocodeQueue() all queries complete, geocoder done");
- gOpts.geocodeCompleteCallback();
- return;
+ if (checkRequests()) {
+ debug.log("googleGeocodeQueue() all queries complete, geocoder done");
+ //gOpts.geocodeCompleteCallback();
+ } else {
+ setTimeout(function() { googleGeocodeQueue() }, desiredRate);
}
- setTimeout(function() { gGeocodeQueue() }, desiredRate);
+ }
+ function getUnresolved() {
+ for (var addr in geoCache) {
+ ao = geoCache[addr];
+ if (!ao.resolved && !ao.inProgress) return ao;
+ }
+ return false;
+ }
+
+ function checkInProgress(ems) {
+ for (var addr in geoCache) {
+ ao = geoCache[addr];
+ if (ao.inProgress && (ems - ao.sentLast > reqTimeout)) {
+ if (ao.sentTimes > maxRetries) {
+ ao.resolved = true;
+ //numUniqAddrErrors++;
+ debug.log('checkInProgress() forgetting request '+ao.reqNum, ao);
+ //gOpts.geocodedAddrCallback(ao);
+ } else {
+ ao.inProgress = false;
+ debug.log('checkInProgress() resetting request '+ao.reqNum+' after '+(ems-ao.sentLast)+'ms (reqTimeout)', ao);
+ }
+ }
+ }
+ return false;
}
function parseGObj(gObj) {
if (typeof(gObj) != 'object') {
- debug.warn("parseGObj() shouldn't be here " + typeof(gObj), gObj);
+ debug.warn("**** parseGObj() shouldn't be here " + typeof gObj, gObj);
return;
}
if (gObj.tmpError) {
- if (gObj.errorCode == 620) {
+ // https://developers.google.com/maps/documentation/geocoding/#Limits
+ if (gObj.errorCode == google.maps.GeocoderStatus.OVER_QUERY_LIMIT) {
debug.log("parseGObj() resubmit (too fast)", gObj.addr1, gObj);
desiredRate = 1.1 * desiredRate; // slow down requests
- } else {
- debug.log("parseGObj() resubmit (timeout) ", gObj.addr1, gObj);
}
if (gObj.addr1) {
geoCache[gObj.addr1].inProgress = false;
@@ -656,17 +1014,17 @@
}
} else if (gObj.error) {
- debug.info("parseGObj() error ", gObj);
+ debug.debug(" parseGObj() geocode error " + gObj.error, gObj);
geoCache[gObj.addr1].resolved = true;
geoCache[gObj.addr1].validCoords = false;
geoCache[gObj.addr1].inProgress = false;
geoCache[gObj.addr1].error = gObj.error;
- numUniqAddrErrors++;
- gOpts.geocodedAddrCallback(geoCache[gObj.addr1]);
+ //numUniqAddrErrors++;
+ //gOpts.geocodedAddrCallback(geoCache[gObj.addr1]);
} else if (gObj.lt) {
if (!gObj.addr1 || !geoCache[gObj.addr1]) {
- debug.warn("parseGObj() debug me", gObj);
+ debug.warn("**** parseGObj() debug me", gObj);
return;
}
geoCache[gObj.addr1].lt = gObj.lt;
@@ -677,103 +1035,79 @@
geoCache[gObj.addr1].inProgress = false;
debug.log("parseGObj() got coords ", gObj.addr1, gObj, geoCache[gObj.addr1]);
- numUniqAddrDecoded++;
- gOpts.geocodedAddrCallback(geoCache[gObj.addr1]);
+ //numUniqAddrDecoded++;
+ //gOpts.geocodedAddrCallback(geoCache[gObj.addr1]);
} else {
- debug.warn("parseGObj() should not be here ", gObj);
+ debug.warn("**** parseGObj() should not be here ", gObj);
}
}
- geoMgrInit();
+ // description of gObj
+ // geocodeObj: {
+ // lg: null, // number, -180 +180
+ // lt: null, // number, -90 +90
+ // addr2: null, // string, google's rewording of addr1
+ // addr1: null, // string, address passed to geocoder
+ // errorCode: null, // number
+ // tmpError: false, // boolean
+ // error: null // string error msg
+ // },
+ //
- return {
- numAddresses: numAddresses,
- numUniqAddresses: numUniqAddresses,
- count: function() {
- var c = {
- uniqAddrDecoded: 0,
- uniqAddrErrors: 0,
- uniqAddrTotal: 0
- }
- for (var addr in geoCache) {
- ao = geoCache[addr];
- c.uniqAddrTotal++;
- if (ao.resolved && ao.validCoords) c.uniqAddrDecoded++;
- if (ao.resolved && !ao.validCoords) c.uniqAddrErrors++;
+ // googleGeocode() translates addresses into array of lat/lng coords using Google, also see googleGeocodeQueue()
+ //
+ function googleGeocode( addr, callback ) {
+
+ //debug.log("gGeocode() submitting addr to google: " + addr);
+ //$("#"+ cnMFUI.opts.listId ).append('.');
+
+ var geocoder = new google.maps.Geocoder();
+ geocoder.geocode( {
+ 'address': addr
+ }, function(results, status) {
+ var s2e = {};
+ // https://developers.google.com/maps/documentation/javascript/reference#GeocoderStatus
+ s2e[google.maps.GeocoderStatus.OK] = false;
+ s2e[google.maps.GeocoderStatus.ERROR] = 'ERROR';
+ s2e[google.maps.GeocoderStatus.INVALID_REQUEST] ='INVALID_REQUEST';
+ s2e[google.maps.GeocoderStatus.OVER_QUERY_LIMIT] = 'OVER_QUERY_LIMIT';
+ s2e[google.maps.GeocoderStatus.REQUEST_DENIED] = 'REQUEST_DENIED';
+ s2e[google.maps.GeocoderStatus.UNKNOWN_ERROR] = 'UNKNOWN_ERROR';
+ s2e[google.maps.GeocoderStatus.ZERO_RESULTS] = 'ZERO_RESULTS';
+
+ if (status == google.maps.GeocoderStatus.OK) {
+ //console.log("geocoder WORKS", results, results[0].geometry.location.lng(), results[0].geometry.location.lat());
+ callback( {
+ lg: results[0].geometry.location.lng(),
+ lt: results[0].geometry.location.lat(),
+ loc: results[0].geometry.location,
+ addr2: results[0].formatted_address,
+ addr1: addr
+ });
+ } else {
+ //console.log("geocoder NO WORKS, status: ", status);
+ callback( {
+ addr1: addr,
+ data: results,
+ errorCode: status,
+ tmpError: (status == google.maps.GeocoderStatus.OVER_QUERY_LIMIT),
+ error: (s2e[status]) ? s2e[status] : "Google geocode api changed"
+ });
}
- return c;
- }
- }
- }
+ });
+ };
- // description of gObj
- // geocodeObj: {
- // lg: null, // number, -180 +180
- // lt: null, // number, -90 +90
- // addr2: null, // string, google's rewording of addr1
- // addr1: null, // string, address passed to geocoder
- // errorCode: null, // number
- // tmpError: false, // boolean
- // error: null // string error msg
- // },
- //
- // gGeocode() translates addresses into array of lat/lng coords using Google, also see gGeocodeQueue()
- //
- cnMF.gGeocode = function( addr, googleApiKey, callback ) {
+ return GeocodeManager;
- //debug.log("gGeocode() submitting addr to google: " + addr);
- //$("#"+ cnMFUI.opts.listId ).append('.');
+ };
+ (function(){
+ // TODO: separate geocode manager out
+ //console.log("LOADED GeocodeManager")
+ })();
+
- // switched from getJson to ajax to handle errors. However, looks like error function is not called
- // when google responds with http 400 and text/html (versus http 200 with text/javascript)
- //
- // http://groups.google.com/group/google-maps-api/browse_thread/thread/e347b370e8586767/ddf95bdb0fc6a9f7?lnk=raot
- geoUrl = 'http://maps.google.com/maps/geo?'
- + '&key='+ googleApiKey
- + '&q='+ escape(addr)
- + '&sensor=false&output=json'
- + '&callback=?';
- //geoUrl = 'http://maps.google.com/maps/geo?callback=?';
- $.ajax({
- type: "GET",
- url: geoUrl,
- dataType: "json",
- //global: false,
- error: function (XMLHttpRequest, textStatus, errorThrown) {
- debug.log("gGeocode() error for "+ geoUrl);
- },
- // complete is only called after success, not on error, therefore useless
- //complete: function (XMLHttpRequest, textStatus) {
- // debug.log("gGeocode() complete ", textStatus, XMLHttpRequest);
- //},
- success: function(data, textStatus) {
- //debug.log("gGeocode() success() status,data: ", textStatus, data);
- //$("#"+ cnMFUI.opts.listId ).append('.');
- if (data.Placemark) {
- callback( {
- lg: data.Placemark[0].Point.coordinates[0],
- lt: data.Placemark[0].Point.coordinates[1],
- addr2: data.Placemark[0].address,
- addr1: data.name
- });
- } else {
- callback( {
- // http://code.google.com/apis/maps/documentation/geocoding/index.html#StatusCodes
- addr1: data.name,
- data: data,
- errorCode: data.Status.code,
- tmpError: (data.Status.code == 620) || (data.Status.code == 500) || (data.Status.code == 610),
- error: (data.Status.code) ?
- ((602==data.Status.code) ? "602: Unknown Address" :
- ((620==data.Status.code) ? "620: Too Many Lookups" : "Google code: "+data.Status.code)) :
- "Google geocode api changed"
- });
- }
- }
- }); //close $.ajax
- }
// END GEOCODE
@@ -800,6 +1134,13 @@
return (n < 10 ? '0' : '') + n;
}
+ /*
+ * Converts a javascript date object to RFC 3339 string, format needed for google APIs.
+ * Note that RFC 3339, made for internet, is basically a subset of ISO 8601
+ * @param {object Date} d javascript date object to convert
+ * @param {boolean} clearhours zeros out the hours field
+ * @return {string} ex: 2014-12-05T21:24:06-06:00
+ */
cnMF.rfc3339 = function(d, clearhours) {
s = d.getUTCFullYear()
+ "-" + zeroPad(d.getUTCMonth() + 1)
@@ -820,9 +1161,17 @@
cnMF.dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
cnMF.dayAbbrevs = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
+ /*
+ * @param {object Date} d
+ * @param {string} format
+ * @return {string}
+ */
cnMF.formatDate = function(d, format) {
var f = cnMF.dateFormatters;
var s = '';
+
+ if (!d) return s;
+
for (var i=0; iMap Link";
+ $("#MapStatus").append(msg);
+ }
+ };
+ if (myURL.params.os) {
+ opts.useOverlappingSliders = (myURL.params.os == 1); // sliderChad is not overlapping sliders (os)
+ }
+ cnMFUI.init(opts);
+
+}); // end document ready
;(function($) {
- var startMs2 = new Date().getTime();
+ var startMs2 = (new Date()).getTime();
// google variables
var myGmap;
- var gGeocoder; // TODO: merge with mapfilter.geocoder
var jScrollPaneInitOpitons = {
showArrows:true,
@@ -34,936 +187,1267 @@
maintainPosition:false // this forces it to scroll to top when table is updated
}; // demo-1.2.3/scrollToSpeed.html
-
/*
* main init function
*/
function cnMFUI_init (options) {
- cnMFUI.opts = $.extend({}, cnMFUI.defaults, options);
+ cnMFUI.opts = $.extend({}, cnMFUI.defaults, options);
- /*
- * Variables used by only $().mapFilter()
- */
+ /*
+ * Variables used by only $().mapFilter()
+ */
- var ndays = cnMFUI.opts.gCalDays;
- var jumptxt = cnMFUI.opts.jumpTxt;
- var mapClickListener = false,
- mapDragstartListener = false,
- lastUpdate4MapLoad = false,
- emptyTableHtml = "nope no match uh-uh nowhere ";
+ var ndays = cnMFUI.opts.gCalDays;
+ var jumptxt = cnMFUI.opts.jumpTxt;
+ var openDrawerDurationMs = 300;
+ var mapClickListener = false,
+ mapDragstartListener = false,
+ lastUpdate4MapLoad = false,
+ redrawing = false,
+ moreThanOneCal = false,
+ calendarsDecoding,
+ emptyTableHtml = "nope no match uh-uh nowhere ";
- /*
- * static variables
- */
- var
- urlIconDefault = "http://www.google.com/mapfiles/marker.png",
- urlIconOrange = "http://gmaps-samples.googlecode.com/svn/trunk/markers/orange/blank.png",
- urlIconBlue = "http://gmaps-samples.googlecode.com/svn/trunk/markers/blue/blank.png",
- mapVersion = "Map Version 2011-3-12";
-
- // end variables
-
- cnMF.reportData.loadTime = (startMs2 +'').replace(/(\d{3})$/,".$1") // add period so its secs.msec
- cnMF.reportData.userInteracted = false;
-
- function init() {
-
- debug.log('mapfilter().init(), cnMF:',cnMF);
- var timezone = jstz.determine_timezone(); // https://bitbucket.org/pellepim/jstimezonedetect/wiki/Home
- initDivs();
- myGmap = initGMap();
- //updateSizes();
-
- // initialize the core MapFilter
- var initObj = {
- // cb = callback functions
- cbBuildInfoHtml : buildInfoHtml,
- cbMapRedraw : mapRedraw,
- cbHighlightItem : cnMFUI.hItem,
- oStartDay : date2days(cnMFUI.opts.startDay),
- oEndDay : date2days(cnMFUI.opts.endDay),
- googleApiKey: cnMFUI.opts.googleApiKey,
- gMap : myGmap
- };
- if (!window.location.href.match(/tz=cal/) && timezone) {
- // if tz=cal is in URL, we use default of calendar timezone. Otherwise use local timezone from browser
- initObj.tzName = timezone.name();
- initObj.tzOffset = timezone.offset();
- initObj.tzDst = timezone.dst();
- }
- cnMF.init(initObj);
- // TODO - move initGMap to cnMF.init(), then do this shortcut: var myGmap = cnMF.gMap;
- debug.log("init cnMF:",cnMF);
-
- // check for data sources and add them
- //
- if (cnMFUI.opts.gCalUrl && cnMFUI.opts.gCalUrl != 'u') {
- $('#calendarTitleContent').html("");
- updateStatus('Map Loaded, Loading Calendar ..');
- //$('#calendarDiv').html('');
- initResults();
- // TODO: create a cnMF.showDates(start,end) which gets from google calendar if required
- if (cnMFUI.opts.gCalUrl == 'test1') {
- fakeGCalData();
- } else if (cnMFUI.opts.gCalUrl == 'test2') {
- getXmlData();
+ /*
+ * static variables
+ */
+ var
+ urlIconDefault = "https://www.google.com/mapfiles/marker.png",
+ urlIconOrange = "https://gmaps-samples.googlecode.com/svn/trunk/markers/orange/blank.png",
+ urlIconBlue = "https://gmaps-samples.googlecode.com/svn/trunk/markers/blue/blank.png",
+ mapVersion = "Map Version 2012-4-8";
+
+ cnMF.reportData.loadTime = (startMs2 +'').replace(/(\d{3})$/,".$1") // add period so its secs.msec
+
+ function init() {
+
+ var timezone = jstz.determine_timezone(); // https://bitbucket.org/pellepim/jstimezonedetect/wiki/Home
+ var calendarIds = getCalendarIds();
+ debug.log('mapfilter().init(), cnMF:',cnMF);
+ myGmap = initGMap();
+ userInteraction.init();
+ initDivs();
+ mapJumpBox();
+ mapRightTab(cnMFUI.opts.mapId);
+
+ //updateSizes();
+
+ // initialize the core MapFilter
+ var initObj = {
+ // cb = callback functions
+ cbOpenedInfoWindow : infoWindowOpened,
+ cbClosedInfoWindow : infoWindowClosed,
+ cbMapRedraw : mapRedraw,
+ cbMarkerClicked : markerClicked,
+ oStartDay : date2days(cnMFUI.opts.startDay),
+ oEndDay : date2days(cnMFUI.opts.endDay),
+ gMap : myGmap
+ };
+ if (!window.location.href.match(/tz=cal/) && timezone) {
+ // if tz=cal is in URL, we use default of calendar timezone. Otherwise use local timezone from browser
+ initObj.tzName = timezone.name();
+ initObj.tzOffset = timezone.offset();
+ initObj.tzDst = timezone.dst();
+ }
+ cnMF.init(initObj);
+ // TODO - move initGMap to cnMF.init(), then do this shortcut: var myGmap = cnMF.gMap;
+ debug.log("init cnMF:",cnMF);
+
+ // check for data sources and add them
+ //
+ if (calendarIds.length) {
+ $('#calendarTitleContent').html("");
+ updateStatus('Map Loaded, Fetching '+ calendarIds.length +' Calendar(s) ..');
+ //$('#calendarDiv').html('');
+ initResults();
+ calendarsDecoding = calendarIds.length;
+ for (var ii = 0; ii < calendarIds.length; ii++) {
+ var calendarId = calendarIds[ii];
+ // TODO: create a cnMF.showDates(start,end) which gets from google calendar if required
+ if (calendarId == 'test1') {
+ fakeGCalData();
+ } else if (calendarId == 'test2') {
+ getXmlData();
+ } else {
+ _gaq.push(['_trackEvent', 'Loading', 'cal-begin', calendarId]);
+ cnMF.getGCalData(calendarId, cnMF.origStartDay, cnMF.origEndDay, {
+ onCalendarLoad: cbCalendarLoad,
+ onGeoDecodeAddr: cbGeoDecodeAddr,
+ onGeoDecodeComplete: function() {
+ cbGeoDecodeComplete(calendarId, calendarIds)
+ },
+ onError: function(jqXHR, textStatus) {
+ _gaq.push(['_trackEvent', 'Loading', 'cal-error', textStatus + ": "+ calendarId]);
+ }
+ });
+ }
+
+ }
} else {
- _gaq.push(['_trackEvent', 'Loading', 'calender-u', cnMFUI.opts.gCalUrl]);
- cnMF.getGCalData(cnMFUI.opts.gCalUrl, cnMF.origStartDay, cnMF.origEndDay, {
- onCalendarLoad: cbCalendarLoad,
- onGeoDecodeAddr: cbGeoDecodeAddr,
- onGeoDecodeComplete: cbGeoDecodeComplete
- });
+ updateStatus('Map Loaded.');
+
+ //$('#resultsDiv').html(""+ $("#myHelp").html() +"
");
+ //$('#resultsDivHelp').addClass('scrollPane');
+ $('#resultsDiv').html( $("#myHelp").html() );
+ updateEventsContainerSize('.helpContainer'); // resize help TODO
+
+ $('#calendarTitleContent').html( addCalForm() );
}
- } else {
- updateStatus('Map Loaded.');
-
- //$('#resultsDiv').html(""+ $("#myHelp").html() +"
");
- //$('#resultsDivHelp').addClass('scrollPane');
- $('#resultsDiv').html( $("#myHelp").html() );
- updateEventsContainerSize('.helpContainer'); // resize help
- $('.helpContainer').addClass('scrollPane');
- $('#resultsDiv').addClass("winXP"); // to make jScrollpane have a winxp scrollbar - see mapFilter.css
- $('.scrollPane').jScrollPane(jScrollPaneInitOpitons); // add scroll pane
- $('#calendarTitleContent').html( addCalForm() );
+ debug.log("mapFilter().init() Completed.");
+ if (cnMFUI.opts.closeDrawer == 2) {
+ closeDrawer()
+ }
}
- debug.log("mapFilter().init() Completed.");
- }
-
-
- function initDivs() {
-
- // this html does not belong in javascript, but keeping here to aid in prototype development
- $('#'+cnMFUI.opts.containerId).html(
- ''
- + '
'
- + '
'
- + '
'
- + '
'
- + '
'
- + '
'
- + '
'
- + '
'
- + '
'
- + '
'
- + '
');
-
- $('#gcmLogo').html("");
- // need to init resultsDiv and put MapStatus under resultsDataStatus
- $('#resultsDataStatus').html(".. Loading Map ..
");
-
- // need to redo how we deal with inital help screen. Should switch between 2 main divs (help vs content), one is display:none
- //$('#resultsDiv').html( $("#myHelp").html() );
-
- }
-
+ function getCalendarIds() {
+ var calendarIds = [],
+ tmp;
+
+ if (cnMFUI.opts.gCalUrls) {
+ if (Object.prototype.toString.call(cnMFUI.opts.gCalUrls) != '[object Array]') {
+ debug.error("gCalUrls must be an array, not "+ typeof cnMFUI.opts.gCalUrls, cnMFUI.opts.gCalUrls);
+ return [];
+ }
+ for (var ii = 0; ii < cnMFUI.opts.gCalUrls.length; ii++) {
+ tmp = cnMFUI.opts.gCalUrls[ii];
+ if (tmp.match(/\%/)) {
+ tmp = unescape(tmp);
+ }
+ calendarIds.push(tmp);
+ }
+ }
+ if (cnMFUI.opts.gCalGroups) { // gcg=xxx,yyy
+ $.each(cnMFUI.opts.gCalGroups.split(','), function(index, value) {
+ calendarIds.push(value + "@group.calendar.google.com");
+ });
+ }
+ if (cnMFUI.opts.gCalImports) { // gci=xxx,yyy
+ $.each(cnMFUI.opts.gCalImports.split(','), function(index, value) {
+ calendarIds.push(value + "@import.calendar.google.com");
+ });
+ }
+ if (cnMFUI.opts.gCalEmails) { // gc=xxx@groups.calendar.google.com,yyy@my.domain.com
+ if (!cnMFUI.opts.gCalEmails.match(/@/)) {
+ // decodeURIComponent() does not handle + to spaces, we want to drop spaces
+ cnMFUI.opts.gCalEmails = decodeURIComponent((cnMFUI.opts.gCalEmails+'').replace(/\+/g, '')); // %40 to '@'
+ }
+ $.each(cnMFUI.opts.gCalEmails.split(','), function(index, value) {
+ value = value.replace(/\/$/,"");
+ calendarIds.push(value);
+ });
+ }
+ moreThanOneCal = calendarIds.length > 1;
+ return calendarIds;
+ }
+
+ function initDivs() {
+ $('#'+ cnMFUI.opts.mapId).append(
+ ''
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
');
+
+ $('#rtSide').width( getRtSideWidth() ).height( getRtSideHeight() );
+
+
+ $('#gcmLogo').html("");
+ // need to init resultsDiv and put MapStatus under resultsDataStatus
+ $('#resultsDataStatus').html(".. Loading Map ..
");
+
+ // need to redo how we deal with inital help screen. Should switch between 2 main divs (help vs content), one is display:none
+ //$('#resultsDiv').html( $("#myHelp").html() );
+
+ // next / prev on infohtml in markers
+ $('body').on('click','a.marker_event_index', function(){
+ var eventIndex = parseInt($(this).data('event_index'));
+ var markerEventIndex = parseInt( $(this).data('marker_event_index') );
+ markerClicked(cnMF.myMarkers.getMarkerObj(eventIndex), markerEventIndex);
+ userInteraction.recordInteraction();
+ return false;
+ });
+
+ $('body').on('click touch','a.event_table', function(){
+ //$.getJSON("/debug.mobile", {'touch':'event_table index='+$(this).data('event_index')});
+ eventClicked( cnMF.getEventObj($(this).data('event_index')) );
+ _gaq.push(['_trackEvent', 'Interaction', 'a.event_table']);
+ userInteraction.recordInteraction();
+ return false;
+ });
+ $('body').on('click','a.zoom_to', function(){
+ zoomTo( cnMF.getEventObj($(this).data('event_index')) );
+ _gaq.push(['_trackEvent', 'Interaction', 'a.zoom_to']);
+ return false;
+ });
+ }
- function logDimensions(s) {
- h = $(s).height();
- ih = $(s).innerHeight();
- oh = $(s).outerHeight(true);
- w = $(s).width();
- iw = $(s).innerWidth();
- ow = $(s).outerWidth(true);
- debug.log('dimensions of selector('+s+') '
- +'w x h: '+w+'x'+h+', '
- +'inner: '+iw+'x'+ih+', '
- +'outer: '+ow+'x'+oh
- );
- }
- function addCalForm() {
- return "";
- }
+ function getRtSideWidth() {
+ // make rtSide a 1/3 the width of container, with a minimum of 300px
+ return $(window).width() > 600 ? Math.floor($(window).width()/3) : 300;
+ }
+ function getRtSideHeight() {
+ return $(window).height() - 120; // subtract more to give more room at bottom right
+ }
- function initGMap() {
+ function logDimensions(s) {
+ h = $(s).height();
+ ih = $(s).innerHeight();
+ oh = $(s).outerHeight(true);
+ w = $(s).width();
+ iw = $(s).innerWidth();
+ ow = $(s).outerWidth(true);
+ debug.log('dimensions of selector('+s+') '
+ +'w x h: '+w+'x'+h+', '
+ +'inner: '+iw+'x'+ih+', '
+ +'outer: '+ow+'x'+oh
+ );
+ }
+ function addCalForm() {
+ return "";
+ }
- updateStatus('Loading Google Map');
- if (!GBrowserIsCompatible()) {
- document.getElementById(cnMFUI.opts.mapId).innerHTML = unSupportedHtml;
- return;
+ function initGMap() {
+
+ updateStatus('Loading Google Map');
+
+ // https://google-developers.appspot.com/maps/documentation/javascript/reference#Map
+ var myOptions = {
+ center: new google.maps.LatLng(cnMFUI.opts.mapCenterLt, cnMFUI.opts.mapCenterLg),
+ zoom: cnMFUI.opts.mapZoom,
+ mapTypeId: google.maps.MapTypeId.ROADMAP // TODO: use cnMFUI.opts.mapType
+ };
+ var myGmap = new google.maps.Map(document.getElementById(cnMFUI.opts.mapId), myOptions);
+
+ // listen for events that alter what markers may appear on map
+ $.each(['dragend', 'bounds_changed', 'zoom_changed'], function(index, mapEvent){
+ google.maps.event.addListener(myGmap, mapEvent, function() {
+ mapMovedListener();
+ });
+ });
+
+ return myGmap;
}
- var myGmap = new GMap2(document.getElementById(cnMFUI.opts.mapId));
- myGmap.setUIToDefault();
- myGmap.setCenter(new GLatLng(cnMFUI.opts.mapCenterLt, cnMFUI.opts.mapCenterLg), cnMFUI.opts.mapZoom, myGmap.getMapTypes()[cnMFUI.opts.mapType]);
- gGeocoder = new GClientGeocoder();
- GEvent.addListener(myGmap, 'moveend', function(){
- mapMovedListener();
- });
- mapClickListener = GEvent.addListener(myGmap, 'click', function(){
- // handles click and double-clicks, but not click-and-drag
- cbUserInteracted();
- });
- mapDragstartListener = GEvent.addListener(myGmap, 'dragstart', function(){
- // handles click-and-drag
- cbUserInteracted();
- });
- mapLogo(myGmap);
- mapJumpBox(myGmap);
- iconDefault = new GIcon(G_DEFAULT_ICON, urlIconDefault);
- return myGmap;
- }
+ function zoomTo(eventObj) {
+ var maxZoom = 19,
+ curZoom = myGmap.getZoom(),
+ newZoom = maxZoom;
- function initSlider(sliderParentId) {
-
- var elemId = 'sliders';
- $("#"+sliderParentId).html('
');
- resizeSlider(elemId); // resize date sliders
-
- var sliderId = elemId + "Slider";
- var sHandleTxt = 'title="Click and drag to instantly filter by dates"';
-
- html = ''
- + '
'
- + '
'
- //+ ''
- + '
Show events from: - ';
-
- $("#"+elemId).append(html);
-
- // chad hates that slider handles can overlap, but fixed: http://chadnorwood.com/code/slider-fix.html
- // http://dev.jqueryui.com/ticket/3467
- // example of not overlapping - kayak - and mootools
- // http://developer.expressionz.in/downloads/mootools_double_pinned_slider_with_clipped_gutter_image_v2.2/slider_using_mootols_1.2.html
- $('#'+sliderId).sliderChad( {
- range:true,
- min: cnMF.origStartDay,
- max: cnMF.origEndDay,
- values: [cnMF.origStartDay, cnMF.origEndDay],
- step: 1,
- slide:function(n,j) {
- //console.log('sliderChad() callback ' );
- $('#'+ sliderId+'1').html(sliderDate(j.values[0]));
- $('#'+ sliderId+'2').html(sliderDate(j.values[1]));
- //$('#'+ sliderId+'2').text(j.values[1]);
- //$('#'+ sliderId+'Start').text( sliderDate(j.values[0]) );
- //$('#'+ sliderId+'End' ).text( sliderDate(j.values[1]) )
- },
- // change is stop + updated from values call in updateSlider
- // http://jqueryui.com/demos/slider/#event-slide
- change:function(n,j){
- debug.log('initSlider() callback - changed start,stop: ', j.values[0], j.values[1] );
- cnMF.showDays(j.values[0], j.values[1]);
- //updateResults();
+ // zoom in half as much between current zoom level and max zoomed in level
+ if (curZoom < maxZoom) {
+ var increaseZoom = Math.floor((maxZoom - curZoom)/2);
+ newZoom = curZoom + (increaseZoom > 1 ? increaseZoom : 1);
}
- });
- $('#'+ sliderId+'1').html(sliderDate(cnMF.origStartDay));
- $('#'+ sliderId+'2').html(sliderDate(cnMF.origEndDay));
- /*
- $('#'+ sliderId+'Start').text( sliderDate(cnMF.origStartDay) );
- $('#'+ sliderId+'End' ).text( sliderDate(cnMF.origEndDay) );
- */
- }
-
- function resizeSlider(elemId) {
- var sliderId = elemId; // + "Slider";
- var sWidth = $('#rtSide').width() - $('#resultsDataStatus').width() - 10;
- debug.log('resizeSlider():'+sWidth);
- $('#'+sliderId).css({ 'width': sWidth });
- $('.ui-slider-horizontal').css({ 'width': sWidth - 10 });
+ debug.info("--- zoomTo(): maxZoom="+maxZoom+", curZoom="+curZoom+", newZoom="+newZoom);
+ myGmap.setCenter( eventObj.getGoogleMarker().getPosition() );
+ myGmap.setZoom(newZoom);
- }
+ window.setTimeout(function(){
+ myGmap.panBy(drawerIsOpen() ? 100 : 0, -100); // pan down slightly so infowindow can be seen better
+ }, 600);
+ }
- function resetStartEndDays(elemId){
- cnMF.curStartDay = cnMF.origStartDay;
- cnMF.curEndDay = cnMF.origEndDay;
- updateSlider(elemId);
- }
- function updateSlider(elemId){
- debug.log('******** updateSlider('+elemId+') '+cnMF.curStartDay, cnMF.curEndDay, cnMF);
+ function initSlider(sliderParentId) {
+
+ var elemId = 'sliders';
+ $("#"+sliderParentId).html('
');
+ resizeSlider(elemId); // resize date sliders
+
+ var sliderId = elemId + "Slider";
+ var sHandleTxt = 'title="Click and drag to instantly filter by dates"';
+
+ var html = ''
+ + '
'
+ + '
'
+ //+ ''
+ + '
Show events from: - ';
+
+ $("#"+elemId).append(html);
+
+ // chad hates that slider handles can overlap, but fixed: http://chadnorwood.com/code/slider-fix.html
+ // http://dev.jqueryui.com/ticket/3467
+ // example of not overlapping - kayak - and mootools
+ // http://developer.expressionz.in/downloads/mootools_double_pinned_slider_with_clipped_gutter_image_v2.2/slider_using_mootols_1.2.html
+ // 2012 Update: overlapping version has bug fixes and works better on latest firefox and IE, so added an option useOverlappingSliders
+ //debug.log("useOverlappingSliders="+ cnMFUI.opts.useOverlappingSliders +", jQuery.browser: ", $(jQuery.browser).serialize() );
+ //$.getJSON("/debug.mobile", {useOverlappingSliders:cnMFUI.opts.useOverlappingSliders,jQueryBrowser:jQuery.browser});
+
+ var sliderOptions = {
+ range:true,
+ min: cnMF.origStartDay,
+ max: cnMF.origEndDay,
+ values: [cnMF.origStartDay, cnMF.origEndDay],
+ step: 1,
+ slide:function(n,j) {
+ //console.log('sliderChad() callback ' );
+ $('#'+ sliderId+'1').html(sliderDate(j.values[0]));
+ $('#'+ sliderId+'2').html(sliderDate(j.values[1]));
+ //$('#'+ sliderId+'2').text(j.values[1]);
+ //$('#'+ sliderId+'Start').text( sliderDate(j.values[0]) );
+ //$('#'+ sliderId+'End' ).text( sliderDate(j.values[1]) )
+ },
+ // change is stop + updated from values call in updateSlider
+ // http://jqueryui.com/demos/slider/#event-slide
+ change:function(n,j){
+ debug.log('initSlider() callback - changed start,stop: ', j.values[0], j.values[1] );
+ cnMF.showDays(j.values[0], j.values[1]);
+ //updateResults();
+ }
+ };
+ if (cnMFUI.opts.useOverlappingSliders) {
+ $('#'+sliderId).slider( sliderOptions);
+ } else {
+ $('#'+sliderId).sliderChad( sliderOptions);
+ }
+ $('#'+ sliderId+'1').html(sliderDate(cnMF.origStartDay));
+ $('#'+ sliderId+'2').html(sliderDate(cnMF.origEndDay));
+ /*
+ $('#'+ sliderId+'Start').text( sliderDate(cnMF.origStartDay) );
+ $('#'+ sliderId+'End' ).text( sliderDate(cnMF.origEndDay) );
+ */
+ }
- sliderId = elemId + "Slider";
- $('#'+sliderId).sliderChad("option", "values", [cnMF.curStartDay,cnMF.curEndDay]);
- debug.log('******** updateSlider('+elemId+') VALUES UPDATED ');
- $('#'+ sliderId+'1').html(sliderDate(cnMF.curStartDay));
- $('#'+ sliderId+'2').html(sliderDate(cnMF.curEndDay));
- debug.log('******** updateSlider('+elemId+') COMPLETE ');
- }
+ function resizeSlider(elemId) {
+ var sliderId = elemId; // + "Slider";
+
+ var sWidth = getRtSideWidth() - $('#resultsDataStatus').width() - 10;
+ debug.log('resizeSlider():'+sWidth);
+ $('#'+sliderId).css({ 'width': sWidth });
+ $('.ui-slider-horizontal').css({ 'width': sWidth - 10 });
- function date2days(date) {
- date = date + '';
- if (date.length < 6) {
- // assume its days offset
- //debug.log('******** date2days('+date+') GOOD !!!!!!!!!!!! !!!!');
- return 1 * date; // return number
- }
- // we just support these YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, and YYYYMMDD, like 2010/02/12, 2010-02-12, 20100212
- var fields = date.match(/(\d{4})([\/\.-])(\d\d?)\2(\d\d?)/);
- if (null == fields) {
- fields = date.match(/(\d{4})()(\d\d)(\d\d)/);
- }
- if (fields) {
- //debug.log('******** date2days match:', fields[1], fields[3], fields[4]);
- //var dateObj = new Date(fields[1], fields[2], fields[3]); //Month is 0-11 in JavaScript
- var dateObj = new Date();
- dateObj.setFullYear(1*fields[1]);
- dateObj.setMonth((1*fields[3])-1);
- dateObj.setDate(1*fields[4]);
- var now = new Date();
- msDiff = dateObj.getTime() - now.getTime();
- return Math.round(msDiff / (24*3600*1000));
- }
- debug.log('******** date2days('+date+') invalid date format !!!!!!!!!!!! TODO !!!!');
- return 0;
- }
+ }
- function days2date(nDays) {
- var day = new Date();
- day.setTime(day.getTime() + nDays*24*3600*1000);
- debug.log('******** days2date('+nDays+') '+ cnMF.formatDate(day, 'Y-m-L'));
- return cnMF.formatDate(day, 'Y-m-L');
- }
- function sliderDate(nDays) {
- var day = new Date();
- day.setTime(day.getTime() + nDays*24*3600*1000); //expires in ndays days (milliseconds)
- return cnMF.formatDate(day, 'n-D') +' '+cnMF.formatDate(day, 'd');
- }
+ function resetStartEndDays(elemId){
+ cnMF.curStartDay = cnMF.origStartDay;
+ cnMF.curEndDay = cnMF.origEndDay;
+ updateSlider(elemId);
+ }
+ function updateSlider(elemId){
+ debug.log('updateSlider('+elemId+') '+cnMF.curStartDay, cnMF.curEndDay, cnMF);
- function initResults(){
+ sliderId = elemId + "Slider";
+ if (cnMFUI.opts.useOverlappingSliders) {
+ $('#'+sliderId).slider("option", "values", [cnMF.curStartDay,cnMF.curEndDay]);
+ } else {
+ $('#'+sliderId).sliderChad("option", "values", [cnMF.curStartDay,cnMF.curEndDay]);
+ }
+ //debug.log('******** updateSlider('+elemId+') VALUES UPDATED ');
+ $('#'+ sliderId+'1').html(sliderDate(cnMF.curStartDay));
+ $('#'+ sliderId+'2').html(sliderDate(cnMF.curEndDay));
+ //debug.log('******** updateSlider('+elemId+') COMPLETE ');
+ }
- initSlider('resultsDataFilters');
+ function date2days(date) {
+ date = date + '';
+ if (date.length < 6) {
+ // assume its days offset
+ //debug.log('******** date2days('+date+') GOOD !!!!!!!!!!!! !!!!');
+ return 1 * date; // return number
+ }
+ // we just support these YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, and YYYYMMDD, like 2010/02/12, 2010-02-12, 20100212
+ var fields = date.match(/(\d{4})([\/\.-])(\d\d?)\2(\d\d?)/);
+ if (null == fields) {
+ fields = date.match(/(\d{4})()(\d\d)(\d\d)/);
+ }
+ if (fields) {
+ //debug.log('******** date2days match:', fields[1], fields[3], fields[4]);
+ //var dateObj = new Date(fields[1], fields[2], fields[3]); //Month is 0-11 in JavaScript
+ var dateObj = new Date();
+ dateObj.setFullYear(1*fields[1]);
+ dateObj.setMonth((1*fields[3])-1);
+ dateObj.setDate(1*fields[4]);
+ var now = new Date();
+ msDiff = dateObj.getTime() - now.getTime();
+ return Math.round(msDiff / (24*3600*1000));
+ }
+ debug.warn('**** date2days('+date+') invalid date format !!!!!!!!!!!! TODO !!!!');
+ return 0;
+ }
- resultsHtml = ""
- //+ "
"
- + "Map Showing The Following
Events"
- + "
Warning: [ ] "
- + "
";
- $("#ResultsMapHdr").html(resultsHtml);
- $("#ResultsMapHdrFilterFrozen").css('display','none');
+ function days2date(nDays) {
+ var day = new Date();
+ day.setTime(day.getTime() + nDays*24*3600*1000);
+ //debug.log('******** days2date('+nDays+') '+ cnMF.formatDate(day, 'Y-m-L'));
+ return cnMF.formatDate(day, 'Y-m-L');
+ }
+ function sliderDate(nDays) {
+ var day = new Date();
+ day.setTime(day.getTime() + nDays*24*3600*1000); //expires in ndays days (milliseconds)
+ return cnMF.formatDate(day, 'n-D') +' '+cnMF.formatDate(day, 'd');
+ }
- $("#ResultsMapHdrFilterByMap").click(function(){
- debug.warn('------ ResultsMapHdrFilterByMap clicked');
- _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrFilterByMap']);
- cnMF.mapAllEvents();
- debug.warn('------ ResultsMapHdrFilterByMap clicked');
+ function initResults(){
- });
- $("#ResultsMapHdrFilterByDate").click(function(){
- _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrFilterByDate']);
- resetStartEndDays('sliders');
- //mapAllEvents();
- });
- $("#ResultsMapHdrFilterFrozen").click(function(){
- // on the map, close a marker's info window
- myGmap.closeInfoWindow();
- _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrFilterFrozen']);
- //$("#ResultsMapHdrFilterFrozen").css('display','none');
- });
+ initSlider('resultsDataFilters');
- // need to addListener to fire when infowindow is closed
- GEvent.addListener(myGmap, "infowindowclose", function() {
- _gaq.push(['_trackEvent', 'Interaction', 'gMrkr', 'infowindowclose']);
- debug.log('infowindowclose event fired, redrawing map');
- //$('#ResultsMapHdrFilterFrozen').css('display','none');
- mapRedraw();
- });
+ resultsHtml = ""
+ //+ "
"
+ + "Map Showing The Following
Events"
+ + "
Warning: [ ] "
+ + "
";
+ $("#ResultsMapHdr").html(resultsHtml);
+ $("#ResultsMapHdrFilterFrozen").css('display','none');
- $("#ResultsMapUnknown").html('
');
- $("#ResultsMapUnknownHdr").html('The Following Events Had Addresses (Where) That Could Not Be Found. See FAQ');
+ $("#ResultsMapHdrFilterByMap").click(function(){
+ debug.info('--- ResultsMapHdrFilterByMap clicked');
+ _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrFilterByMap']);
+ cnMF.mapAllEvents();
+ });
+ $("#ResultsMapHdrFilterByDate").click(function(){
+ debug.info('--- ResultsMapHdrFilterByDate clicked');
+ _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrFilterByDate']);
+ resetStartEndDays('sliders');
+ //mapAllEvents();
+ });
+ $("#ResultsMapHdrFilterFrozen").click(function(){
+ debug.info('--- ResultsMapHdrFilterFrozen clicked');
+ $("#ResultsMapHdrFilterFrozen").css('display','none');
+ cnMF.myMarkers.closeInfoWindow(); // this will call infoWindowClosed()
+ _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrFilterFrozen']);
+ });
- var dialogUnknowns = $("#ResultsMapUnknown").dialog({
- bgiframe: true, autoOpen: false, resizable: true,
- modal: true
- });
- $("#ResultsMapHdrWarning").click(function(){
- _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrWarning']);
- dialogUnknowns.dialog('open');
- });
+ $("#ResultsMapUnknown").html('
');
+ $("#ResultsMapUnknownHdr").html('The Following Events Had Addresses (Where) That Could Not Be Found. See FAQ');
- $.tablesorter.defaults.sortList = [[0,0]];
- $.tablesorter.defaults.widthFixed = true;
- createResultsTable('ResultsMapEvents', true);
- createResultsTable('ResultsMapUnknownTable', false);
+ var dialogUnknowns = $("#ResultsMapUnknown").dialog({
+ bgiframe: true, autoOpen: false, resizable: true,
+ modal: true
+ });
+ $("#ResultsMapHdrWarning").click(function(){
+ debug.info('--- ResultsMapHdrWarning clicked');
+ _gaq.push(['_trackEvent', 'Interaction', 'ResultsMapHdrWarning']);
+ dialogUnknowns.dialog('open');
+ });
- updateEventsContainerSize('#ResultsMapEventsTable');
- $('#ResultsMapEventsTable').addClass("scrollPane");
- $('#ResultsMapEvents').addClass("winXP"); // to make jScrollpane have a winxp scrollbar - see mapFilter.css
+ $.tablesorter.defaults.sortList = [[0,0]];
+ $.tablesorter.defaults.widthFixed = true;
+ createResultsTable('ResultsMapUnknownTable', false);
- updateResults();
- //$('#ResultsMapEvents').css({'height':'auto','overflow':'visible'});
+ updateResults();
- $(window).resize(function(){
- debug.log('windowResizing, new WxH: '+$(window).width()+"x"+$(window).height());
+ //$('#ResultsMapEvents').css({'height':'auto','overflow':'visible'});
- if ((typeof windowResizing == 'boolean') && windowResizing) {
- debug.log('windowResizing too fast !!');
- return;
- }
- windowResizing=true;
- setTimeout( function() {
- windowResizedListener();
- windowResizing=false;
- }, 200);
- });
- }
+ $(window).resize(function(){
+ //debug.log('windowResizing, new WxH: '+ $(window).width() +"x"+ $(window).height() );
+ cnMF.throttle.setTimeout( function(throttled) {
+ debug.log("windowResizing throttled, now updating after "+ throttled.elapsedMs +"ms and "+ throttled.called +" call(s).");
+ windowResizedListener();
+ }, 500, "window_resize", "afterLast");
+ });
+ }
+
+ function infoWindowOpened() {
+ $("#ResultsMapHdrFilterByMap").css('display','none'); // put this here?
+ $("#ResultsMapHdrFilterFrozen").css('display','inline');
+ }
+
+ function infoWindowClosed() {
+
+ // API3 TODO: this only triggers from mouse closing, not from closing by clicking on another marker
+ /*
+ google.maps.event.addListener(cnMF.myMarkers.infoWindow, "closeclick", function() {
+ _gaq.push(['_trackEvent', 'Interaction', 'gMrkr', 'infowindowclose']);
+ debug.log('infowindowclose event fired, redrawing map');
+ //$('#ResultsMapHdrFilterFrozen').css('display','none');
+ });
+ */
+ debug.log('infoWindowClosed, redrawing map');
+ $(".highlight2").removeClass("highlight2");
+ mapRedraw();
+ }
- function windowResizedListener() {
- debug.log('windowResizedListener, new WxH: '+$(window).width()+"x"+$(window).height());
+ function windowResizedListener() {
+ debug.log('windowResizedListener, new WxH: '+$(window).width()+"x"+$(window).height());
- initSlider('resultsDataFilters'); // resize then redraw sliders
+ initSlider('resultsDataFilters'); // resize then redraw sliders
// clear table and remove scroll pane in order to resize it properly
- $('.scrollPane').jScrollPaneRemove();
- updateResultsTable('ResultsMapEvents', true, true); // clear table
- updateEventsContainerSize('#ResultsMapEventsTable'); // resize table
- $('.scrollPane').jScrollPane(jScrollPaneInitOpitons); // add scroll pane
+ //$('.scrollPane').jScrollPaneRemove();
+ //updateResultsTable('ResultsMapEvents', true, true); // clear table hack for tablesorter
+ //updateEventsContainerSize('#ResultsMapEvents'); // resize table
+ // $('.scrollPane').jScrollPane(jScrollPaneInitOpitons); // add scroll pane
// since we cleared table, need repopulate it, too.
//updateResultsTable('ResultsMapEvents', true, false);
- updateResults();
+
- // check to see if we need to add/delete markers
- mapRedraw();
- }
+ $('#rtSide').width( getRtSideWidth() ).height( getRtSideHeight() );
+ updateResults(); // this may be called again in mapRedraw
- function updateEventsContainerSize(eventsContainerSelector) {
- if ((typeof updating == 'boolean') && updating) {
- debug.log(ems+"updateEventsContainerSize() in process, skipping.");
- return;
+ mapRedraw(); // check to see if we need to add/delete markers and updateResults
}
- updating=true;
-
- // we want rtSide height to be same as window height, not go over
- otherHeight = $('#titleDiv').height() + $('#resultsData').height() + $('#ResultsMapHdr').height();
-
- eventsContainerHeight = $(window).height() - 32 - otherHeight;
- eventsContainerWidth = $('#rtSide').width() - 10;
- debug.log('updateEventsContainerSize('+eventsContainerSelector+') eventsContainerHeight:'+eventsContainerHeight+', eventsContainerWidth:'+eventsContainerWidth);
- $(eventsContainerSelector).css({
- 'height': eventsContainerHeight,
- 'width': eventsContainerWidth
- });
- updating=false;
- }
+ function getRtSideLeftoverHeight() {
+ /*
+ debug.log("getRtSideLeftoverHeight(), getRtSideHeight - titleDiv, resultsData, ResultsMapHdr",
+ getRtSideHeight() - ($('#titleDiv').height() + $('#resultsData').height() + $('#ResultsMapHdr').height()),
+ getRtSideHeight(),
+ $('#titleDiv').height(),
+ $('#resultsData').height(),
+ $('#ResultsMapHdr').height() );
+ */
+ return getRtSideHeight() - ($('#titleDiv').height() + $('#resultsData').height() + $('#ResultsMapHdr').height());
+ }
- function updateResults() {
- $("#ResultsMapHdrNum").html(cnMF.numDisplayed);
- //ht=$("#ResultsMapHdrNum").html();
- //debug.log('updateResults() cnMF.numDisplayed:', ht, cnMF.
+ function updateEventsContainerSize(eventsContainerSelector) {
+ if ((typeof updating == 'boolean') && updating) {
+ debug.log(ems+"updateEventsContainerSize() in process, skipping.");
+ return;
+ }
+ updating=true;
+
+ // we want rtSide height to be same as window height, not go over
+ //otherHeight = $('#titleDiv').height() + $('#resultsData').height() + $('#ResultsMapHdr').height();
+
+ eventsContainerHeight = getRtSideLeftoverHeight() - 2;
+ eventsContainerWidth = getRtSideWidth() - 15; // allow for Scroll bar
+ debug.log('updateEventsContainerSize('+eventsContainerSelector+') eventsContainerHeight:'+eventsContainerHeight+', eventsContainerWidth:'+eventsContainerWidth);
+ /*
+ $(eventsContainerSelector).css({ // TODO2: set with width() and height()
+ 'height': eventsContainerHeight,
+ 'width': eventsContainerWidth
+ });
+ */
+ $(eventsContainerSelector).width(eventsContainerWidth).height(eventsContainerHeight);
- if (cnMF.countUnknownAddresses() > 0) {
- $("#ResultsMapHdrWarning span").html(cnMF.countUnknownAddresses());
- $("#ResultsMapHdrWarning").css('display','inline'); // display: block
- } else {
- $("#ResultsMapHdrWarning").css('display','none'); // none: hide warning if no unknown addr
+ updating=false;
}
- updateFilters();
- updateResultsTable('ResultsMapEvents', true, false);
- updateResultsTable('ResultsMapUnknownTable', false, false);
-
- $('.scrollPane').jScrollPane(jScrollPaneInitOpitons);
- }
- /*
- * updateFilters() handles 2 filters:
- * (1) if not showing all events, show "Filtered by MAP"
- * (2) when infowindow is open, don't redraw map, don't update results, don't display "Filtered by MAP"
- */
- function updateFilters() {
- if (cnMF.filteredByDate) {
- $("#ResultsMapHdrFilterByDate").css('display','inline');
- debug.log('cnMF.filteredByDate is true: ', cnMF.filteredByDate);
- } else {
- $("#ResultsMapHdrFilterByDate").css('display','none');
- debug.log('cnMF.filteredByDate is false: ', cnMF.filteredByDate);
+ // updateResults should be called
+ // - whenever height/width of results container changes
+ // - any time the number of visible events changes.
+ // this deletes and recreates results tables
+ function updateResults() {
+ $("#ResultsMapHdrNum").html(cnMF.numDisplayed);
+ //ht=$("#ResultsMapHdrNum").html();
+ //debug.log('updateResults() cnMF.numDisplayed:', ht, cnMF.
+
+ if (cnMF.countUnknownAddresses() > 0) {
+ $("#ResultsMapHdrWarning span").html(cnMF.countUnknownAddresses());
+ $("#ResultsMapHdrWarning").css('display','inline'); // display: block
+ } else {
+ $("#ResultsMapHdrWarning").css('display','none'); // none: hide warning if no unknown addr
+ }
+ updateFilters();
+
+ //updateEventsContainerSize('#ResultsMapEvents');
+ $('#ResultsMapEvents')
+ .width( getRtSideWidth() )
+ .height(getRtSideLeftoverHeight() - 2);
+
+ createResultsTable('ResultsMapEvents'); // this erases all html, including jScrollPane
+
+ $('#ResultsMapEventsTable').width(getRtSideWidth()-15); //.height(getRtSideLeftoverHeight() - 4);
+ //$('#ResultsMapEventsTable tbody').height(getRtSideLeftoverHeight() - 4);
+ //updateEventsContainerSize('#ResultsMapEventsTable');
+
+ updateResultsTable('ResultsMapEvents', true, false);
+ //$('#ResultsMapEventsWrapper').height( $('#ResultsMapEventsTable').height() );
+ updateResultsTable('ResultsMapUnknownTable', false, false);
}
- if (!myGmap.getInfoWindow().isHidden()) {
- $("#ResultsMapHdrFilterByMap").css('display','none');
- $("#ResultsMapHdrFilterFrozen").css('display','inline');
- debug.log('getInfoWindow is showing');
- } else {
- $("#ResultsMapHdrFilterFrozen").css('display','none');
- if (cnMF.filteredByMap) {
- //if (cnMF.numDisplayed == cnMF.countKnownAddresses())
- debug.log('cnMF.filteredByMap is true: ', cnMF.filteredByMap);
- $("#ResultsMapHdrFilterByMap").css('display','inline');
+
+ /*
+ * updateFilters() handles 2 filters:
+ * (1) if not showing all events, show "Filtered by MAP"
+ * (2) when infowindow is open, don't redraw map, don't update results, don't display "Filtered by MAP"
+ */
+ function updateFilters() {
+ //debug.log('updateFilters() cnMF.filteredByDate is ', cnMF.filteredByDate);
+ if (cnMF.filteredByDate) {
+ $("#ResultsMapHdrFilterByDate").css('display','inline');
} else {
- debug.log('cnMF.filteredByMap is false: ', cnMF.filteredByMap);
+ $("#ResultsMapHdrFilterByDate").css('display','none');
+ }
+ if (cnMF.myMarkers.infoWindowIsOpen()) {
$("#ResultsMapHdrFilterByMap").css('display','none');
+ $("#ResultsMapHdrFilterFrozen").css('display','inline');
+ debug.log('updateFilters() InfoWindow is showing, not updating based on map changes');
+ } else {
+ $("#ResultsMapHdrFilterFrozen").css('display','none');
+ //debug.log('updateFilters() cnMF.filteredByMap is '+ cnMF.filteredByMap);
+ if (cnMF.filteredByMap) {
+ $("#ResultsMapHdrFilterByMap").css('display','inline');
+ } else {
+ $("#ResultsMapHdrFilterByMap").css('display','none');
+ }
}
- }
- }
+ }
- function createResultsTable(divId, onlyValidCoords){
- debug.warn("createResultsTable() ", divId, onlyValidCoords);
-
- // note: give table dummy tbody data or tablesorter gives "parsers is undefined" error
- tableHtml = ""
- + "Date "
- + "Name "
- + "Description "
- + "Where "
- + " "+emptyTableHtml+"
";
- pagerHtml = "";
- pagerHtml = '';
- $('#' + divId).html(tableHtml+pagerHtml);
-
- //$('#' + divId +"Pager .pagesize").val(cnMFUI.opts.numTableRows); // rows in table
- $("#" + divId + "Table").tablesorter()
- /*
- .tablesorterPager({
- seperator: " of ",
- positionFixed: false, // TODO: set to true, move pager to top of table
- container: $("#"+divId+"Pager")
- })
- */
- .bind("sortEnd",function() {
- debug.log('tablesorter finished sorting', this);
- })
- ;
+ function createResultsTable(divId){
+ //debug.log("createResultsTable() ", divId);
+
+ // note: give table dummy tbody data or tablesorter gives "parsers is undefined" error
+ tableHtml = ""
+ + "Date "
+ + "Name "
+ + "Description "
+ + "Where "
+ + " "+emptyTableHtml+"
";
+ pagerHtml = "";
+ pagerHtml = '';
+ $('#' + divId).html(tableHtml+pagerHtml);
+
+ //$('#' + divId +"Pager .pagesize").val(cnMFUI.opts.numTableRows); // rows in table
+ $("#" + divId + "Table").tablesorter()
+ /*
+ .tablesorterPager({
+ seperator: " of ",
+ positionFixed: false, // TODO: set to true, move pager to top of table
+ container: $("#"+divId+"Pager")
+ })
+ .bind("sortEnd",function() {
+ debug.log('tablesorter finished sorting', this);
+ });
+ */
- }
+ }
- function updateResultsTable(divId, onlyValidCoords, clearTable){
- var rowHTML = '';
- for (var i in cnMF.eventList) {
- if (clearTable)
- continue;
- var kk = cnMF.eventList[i];
- if (onlyValidCoords && !kk.isDisplayed)
- continue;
- if (!onlyValidCoords && kk.validCoords)
- continue;
-
- rowHTML += "";
- rowHTML += "" + cnMF.formatDate(kk.dateStart, 'Y-m-L d H:i') + " ";
- //rowHTML += "" + cnMF.formatDate(kk.dateStart, 'm/D g:i a d') + " ";
- // TODO: use this and/or custom sort http://tablesorter.com/docs/example-meta-parsers.html
- // rowHTML += "" + cnMF.formatDate(kk.dateStart, 'm/D d g:ia') + " ";
-
-
- rowHTML += onlyValidCoords ? ''
+ function updateResultsTable(divId, onlyValidCoords, clearTable){
+ var rowHTML = '';
+ for (var i in cnMF.eventList) {
+ if (clearTable)
+ continue;
+ var kk = cnMF.eventList[i];
+ if (onlyValidCoords && !kk.isDisplayed)
+ continue;
+ if (!onlyValidCoords && kk.validCoords)
+ continue;
+
+ rowHTML += "";
+ //rowHTML += "" + cnMF.formatDate(kk.dateStart, 'Y-m-L d H:i') + " ";
+ //rowHTML += "" + cnMF.formatDate(kk.dateStart, 'Y-m-L d g:i a') + " "; // does not sort correctly
+ rowHTML += "" + cnMF.formatDate(kk.dateStart, 'd m/D/Y g:i a') + " "; // does sort
+ //rowHTML += "" + cnMF.formatDate(kk.dateStart, 'd M D Y g:i a') + " "; // does not sort correctly
+ // TODO: use this and/or custom sort http://tablesorter.com/docs/example-meta-parsers.html
+ // rowHTML += "" + cnMF.formatDate(kk.dateStart, 'm/D d g:ia') + " ";
+
+
+ rowHTML += onlyValidCoords ?
+ ''
+ cnMFUI.htmlEncode(cnMFUI.maxStr(kk.name, 100, 0, '', 1)) + " "
- : ''+ cnMFUI.htmlEncode(cnMFUI.maxStr(kk.name, 100, 0, '', 1)) + ' ';
+ : ''+ cnMFUI.htmlEncode(cnMFUI.maxStr(kk.name, 100, 0, '', 1)) + ' ';
- rowHTML += "[Event Details ] "
+ rowHTML += " [Event Details ] "
+ cnMFUI.htmlEncode(cnMFUI.maxStr(kk.desc, 140, 0, '', 1)) + " ";
- rowHTML += onlyValidCoords ? '' + kk.addrFromGoogle + ' (orig ) '
- : ''+(kk.addrOrig.match(/\w/) ? kk.addrOrig : '[empty]' )
- +'Edit Event Address Error: ' + kk.error + ' ';
- rowHTML += " \n";
- }
+ rowHTML += onlyValidCoords
+ ? ' '+ kk.addrFromGoogle +' '
+ : ''+(kk.addrOrig.match(/\w/) ? kk.addrOrig : '[empty]' );
+ rowHTML += 'Edit Event Address Error: ' + kk.error + ' ';
- // empty tbody triggers "parsers is undefined" error from tablesorter
- if (rowHTML == '') {
- rowHTML = ' '+emptyTableHtml+ ' ';
- $("#" + divId + "Table tbody").html(rowHTML);
- return;
- }
- $("#" + divId + "Table tbody").html(rowHTML);
-
- // trigger tablesorter to sort
- $("#" + divId + "Table").trigger("update");
- var sorting = [[0,0]]; // sort on the first column
- $("#" + divId + "Table").trigger("sorton",[sorting]);
-
- // trigger update of pagedisplay
- //$('#' + divId +"Pager .pagesize").val(cnMFUI.opts.numTableRows+1).val(cnMFUI.opts.numTableRows);
+ rowHTML += "\n";
+ }
+ // empty tbody triggers "parsers is undefined" error from tablesorter
+ if (rowHTML == '') {
+ rowHTML = ''+emptyTableHtml+ ' ';
+ $("#" + divId + "Table tbody").html(rowHTML);
+ return;
+ }
+ $("#" + divId + "Table tbody").html(rowHTML);
- }
+ // trigger tablesorter to sort
+ $("#" + divId + "Table").trigger("update");
+ var sorting = [[0,0]]; // sort on the first column
+ $("#" + divId + "Table").trigger("sorton",[sorting]);
+ // trigger update of pagedisplay
+ //$('#' + divId +"Pager .pagesize").val(cnMFUI.opts.numTableRows+1).val(cnMFUI.opts.numTableRows);
- /* TODO: if event is an all day event, represent that instead of currently saying 12a-12am
- * TODO: also note recurring (weekly/monthly/etc)
-*/
- function getMapType(oMap) {
- mm = oMap.getMapTypes().length;
- for (nn = 0; nn < mm; nn++){
- if (oMap.getMapTypes()[nn] == oMap.getCurrentMapType())
- return nn;
}
- return -1;
- }
-
-
- function mapRedraw(skipUpdateStatus) {
+ /* TODO: if event is an all day event, represent that instead of currently saying 12a-12am
+*/
- if ((typeof redrawing == 'boolean') && redrawing) {
- debug.log(ems+"mapRedraw() redrawing already in process, skipping.");
- return;
+ function getMapType(oMap) {
+ return -1;
+ mm = oMap.getMapTypes().length; // API3 TODO
+ for (nn = 0; nn < mm; nn++){
+ if (oMap.getMapTypes()[nn] == oMap.getCurrentMapType())
+ return nn;
+ }
+ return -1;
}
- redrawing=true;
- if(0) debug.time('total mapRedraw');
- if (cnMF.updateMarkers()) {
- debug.log("mapRedraw() updated markers, changes found");
- if (!skipUpdateStatus) updateStatus("... Redrawing Map ...");
- updateResults();
- } else {
- debug.log("mapRedraw() updated markers - no changes, no updateResults()");
- updateFilters();
- }
- if(0) debug.timeEnd('total mapRedraw');
- // todo: don't update status every map redraw. then we can call mapRedraw during geocoding, but
- // will need to change maplink
- //if (!skipUpdateStatus) updateStatus(""+ gcTitle +" ");
- if (!skipUpdateStatus) updateStatus('');
- //sleep(2000);
- mapChanged();
- //updateSizes();
- redrawing=false;
- }
+ // mapRedraw calls updateMarkers(), which compares map to markers and show/hides markers appropriately.
+ // if markers change, then event results table is updated as well
+ // Therefore this should be called anytime map changes (moves, zoom) or if filters change (date sliders)
+ function mapRedraw(skipUpdateStatus) {
+ if ((typeof redrawing == 'boolean') && redrawing) {
+ debug.log(ems+" mapRedraw() redrawing already in process, skipping.");
+ return;
+ }
+ redrawing=true;
+ //debug.time('total mapRedraw');
- // TODO - chad - move to index.html as callback function
- function mapChanged() {
- if (cnMFUI.opts.mapChangeCallback) cnMFUI.opts.mapChangeCallback({
- mapZoom: myGmap.getZoom(),
- mapType: getMapType(myGmap),
- mapCenterLt: myGmap.getCenter().lat().toString().replace(/(\.\d\d\d\d\d\d)\d+/,"$1"),
- mapCenterLg: myGmap.getCenter().lng().toString().replace(/(\.\d\d\d\d\d\d)\d+/,"$1"),
- startDay: cnMF.curStartDay,
- endDay: cnMF.curEndDay
- });
- }
+ if (cnMF.updateMarkers()) {
+ debug.log("mapRedraw() updated markers, changes found");
+ if (!skipUpdateStatus) updateStatus("... Redrawing Map ...");
+ updateResults();
+ } else {
+ debug.log("mapRedraw() updated markers - no changes, no updateResults()");
+ updateFilters();
+ }
+ //debug.timeEnd('total mapRedraw');
- function mapMovedListener() {
+ // todo: don't update status every map redraw. then we can call mapRedraw during geocoding, but
+ // will need to change maplink
+ //if (!skipUpdateStatus) updateStatus(""+ gcTitle +" ");
+ if (!skipUpdateStatus) updateStatus('');
- if (!myGmap.getInfoWindow().isHidden()) {
- debug.log("mapMovedListener(): infowindow is open, not redrawing.");
- return;
- }
- debug.log("mapMovedListener(): redrawing ...");
- mapRedraw();
- //debug.log("sleeping for 2secs");
//sleep(2000);
- //debug.log("woke up !!");
- }
+ mapChangedCallback();
+ //updateSizes();
+ redrawing=false;
+ }
+ // TODO - chad - move to index.html as callback function
+ function mapChangedCallback() {
+ if (cnMFUI.opts.mapChangeCallback) cnMFUI.opts.mapChangeCallback({
+ mapZoom: myGmap.getZoom(),
+ mapType: getMapType(myGmap),
+ mapCenterLt: myGmap.getCenter().lat().toString().replace(/(\.\d\d\d\d\d\d)\d+/,"$1"),
+ mapCenterLg: myGmap.getCenter().lng().toString().replace(/(\.\d\d\d\d\d\d)\d+/,"$1"),
+ startDay: cnMF.curStartDay,
+ endDay: cnMF.curEndDay
+ });
+ }
- // Insert Jump Box (aka goto address)
- function mapJumpBox(oMap) {
- var info=document.createElement('div');
- //info.id='SearchBox';
- info.style.position='absolute';
- info.style.right='7px';
- info.style.top='42px';
- //info.style.backgroundColor='transparent';
- info.style.zIndex=10; // TODO: find proper zIndex - less than infowindow, but greater than map
- //info.innerHTML=' ';
- info.innerHTML = '';
+ function mapMovedListener() {
+ if ( cnMF.myMarkers.infoWindowIsOpen() ) {
+ cnMF.throttle.setTimeout( function(throttled) {
+ debug.log("mapMovedListener(): infowindow is open, not redrawing.");
+ }, 500, "mapMovedListenerNotRedrawing");
+ return;
+ }
+ cnMF.throttle.setTimeout( function(throttled) {
+ debug.log("mapMovedListener throttled, now redrawing after "+ throttled.elapsedMs +"ms and "+ throttled.called +" call(s).");
+ mapRedraw();
+ }, 500, "mapMovedListener");
+ }
- oMap.getContainer().appendChild(info);
- }
- function mapLogo(oMap) {
- /* Insert Logo on Map */
- var info=document.createElement('div');
- info.id='LogoInfo';
- info.style.position='absolute';
- info.style.right='4px';
- info.style.bottom='20px';
- info.style.backgroundColor='transparent';
- info.style.zIndex=25500;
- info.innerHTML=' ';
+ // Insert Jump Box (aka goto address)
+ function mapJumpBox() {
- oMap.getContainer().appendChild(info);
- }
+ var info=document.createElement('div');
+ //info.id='SearchBox';
+ info.style.position='absolute';
+ info.style.right='47px';
+ info.style.top='42px';
+ //info.style.backgroundColor='transparent';
+ info.style.zIndex=10; // TODO: find proper zIndex - less than infowindow, but greater than map
+ //info.innerHTML=' ';
+ info.innerHTML = '';
+ document.getElementById(cnMFUI.opts.mapId).appendChild(info);
+ }
- function addLinks (txt) {
- return txt.replace(/(http:\/\/[^<>\s]+)/gi,'$1 ');
- }
+
+ function mapRightTab(mapId) {
+ $('#'+mapId).append("- ");
+ $('#rightTab').click(function(){
+ debug.info('--- clicked tab, '+ (drawerIsOpen() ? 'clos':'open') + 'ing drawer');
+ drawerIsOpen() ? closeDrawer() : openDrawer();
+ });
+ }
+ function drawerIsOpen() {
+ return '0px' == $('#rtSide').css('margin-right');
+ }
+ function openDrawer() {
+ $('#rightTab').html('-');
+ $('#rtSide').animate({
+ 'margin-right' : '0px'
+ }, openDrawerDurationMs, 'linear');
+ }
+ function closeDrawer() {
+ $('#rightTab').html('+');
+ $('#rtSide').animate({
+ 'margin-right' : '-'+ getRtSideWidth() + 'px'
+ }, openDrawerDurationMs, 'linear');
+ }
- function buildInfoHtml (kk) {
- if (kk.infoHtml) return;
- //str = i+" id("+kk['id']+"): "+kk['n']+"\n"+ kk['lt'] +", "+ kk['lg'] ;
- //alert(str);
- //desc = "Star(s): "+ kk['r'] +" Category: "+ rawJson[0][kk['c']] + "\n" + kk['d'];
- //infoHTML = "
"+ kk['n'] +"<\/h1> "+ desc +"
"+footer+" ";
-
- kk.infoHtml = ""+ kk.name +"<\/h1>";
- kk.infoHtml += " "+ cnMFUI.maxStr( addLinks(kk.desc), 900, 26, kk.url) +"
";
- kk.infoHtml += '';
- kk.infoHtml += cnMF.formatDate(kk.dateStart, 'F D, l gx') +"-"+ cnMF.formatDate(kk.dateEnd, 'gx') +"
";
- kk.infoHtml += '
Zoom To - ';
- kk.infoHtml += kk.addrOrig +" - " + kk.getDirectionsHtmlStr();
- kk.infoHtml += '
';
- }
+ function addLinks (txt) {
+ if (txt && typeof txt == 'string') {
+ return txt.replace(/(https:\/\/[^<>\s]+)/gi,'$1 ');
+ } else {
+ return txt;
+ }
+ }
- function updateStatus(msg) {
- $("#MapStatus").html(msg);
- debug.log("updateStatus(): "+msg);
- }
+ function buildInfoHtml (eventObj) {
+ if (!eventObj || !eventObj.name) {
+ return 'Bad event object';
+ }
+ var infoHtml = ""+ eventObj.name +"<\/h1>";
+ infoHtml += moreThanOneCal ? 'Calendar: '+eventObj.calTitle+' ' : '';
+ infoHtml += " "+ cnMFUI.maxStr( addLinks(eventObj.desc), 900, 26, eventObj.url) +"
";
+ infoHtml += '';
+ infoHtml += cnMF.formatDate(eventObj.dateStart, 'F D, l g:ix') +" - "+ cnMF.formatDate(eventObj.dateEnd, 'g:ix') +"
";
+ infoHtml += '
Zoom To - ';
+ infoHtml += eventObj.addrOrig +" - ";
+ infoHtml += '
Directions ';
+ infoHtml += '
';
+ return infoHtml;
+ }
- function updateStatus2(msg) {
- $("#MapStatus2").html(msg);
- debug.log("updateStatus2(): "+msg);
- }
+ // markerEventIndex - optional, when there's more than one event at a location, show this event
+ function getUpdatedInfoHtml(markerObj, markerEventIndex) {
+ debug.log("getUpdatedInfoHtml", markerObj, markerEventIndex);
+
+ var markerEvents = markerObj.getEvents();
+ // Note that markerEventIndex is the index of an array of events at a specific marker location,
+ // whereas eventIndex is the index of an array of all events.
+ markerEventIndex = (typeof markerEventIndex === 'undefined') ? 0 : markerEventIndex; // cur
+ var eventIndex = markerEvents[markerEventIndex];
+ var eventObj = cnMF.getEventObj(eventIndex);
+
+ var infoHtml = buildInfoHtml(eventObj);
+
+ if (markerEvents.length > 1) {
+ // More than one event, add clickable next/prev to bottom of infoHtml
+ var href = '<< prev - ';
+ var next = (markerEventIndex == (markerEvents.length-1)) ? '' : ' - '+ href + (markerEventIndex+1) + '">next >> ';
+ infoHtml += "
"+ prev +"Showing "+ (markerEventIndex+1) +" of "+ markerEvents.length;
+ infoHtml += " Events at this location"+ next +"
";
+ }
+ return infoHtml;
+ }
+
+ function highlightEvent(eventIds) {
+ //debug.log("highlightEvent", eventIds);
+ eventIds = typeof eventIds == 'number' ? [eventIds]
+ : typeof eventIds == 'string' ? [parseInt(eventIds)] : eventIds;
+ /*
+ CHAD TODO:
+ - highlight: when marker highlights more than 1 event, need 2 highlights - brighter shade for current one, and less bright for all others at same location
+ - make event list move to highlighted event (jscrollpane - update css: top)
+ */
+ // remove any previous highlights and highlight new ones
+ $(".highlight2").removeClass("highlight2");
+ $("a.event_table").each(function(){
+ for (var ii=0; ii < eventIds.length; ii++) {
+ if ($(this).data('event_index') == eventIds[ii]) {
+ //debug.log("highlightEvent found event id: "+ eventIds[ii]);
+ $(this).addClass("highlight2");
+ }
+ }
+ });
+ }
+ function eventClicked(eventObj) {
+ debug.info("--- eventClicked", eventObj.id, eventObj);
+ // note - addListener setup in initResults() for map's infowindowclose event, calls mapRedraw
+ cnMF.myMarkers.openInfoWindow( buildInfoHtml(eventObj), eventObj.getMarkerObj() );
+ highlightEvent([eventObj.id]);
+ myGmap.setCenter( eventObj.getGoogleMarker().getPosition() );
+ myGmap.panBy(100, -100); // pan slightly away from drawer so infowindow can be seen better
+ }
+ function markerClicked(markerObj, markerEventIndex) {
+ debug.info("--- markerClicked", markerObj, markerEventIndex);
+ markerEventIndex = (typeof markerEventIndex === 'undefined') ? 0 : markerEventIndex;
+ cnMF.myMarkers.openInfoWindow(getUpdatedInfoHtml(markerObj, markerEventIndex), markerObj);
+ highlightEvent(markerObj.getEvent(markerEventIndex));
+ }
+ function updateStatus(msg) {
+ var oldMsg = $("#MapStatus").html();
+ if (oldMsg != msg) {
+ $("#MapStatus").html(msg);
+ debug.log("updateStatus(): "+ msg);
+ }
+ }
+ function updateStatus2(msg) {
+ var oldMsg = $("#MapStatus2").html();
+ if (oldMsg != msg) {
+ $("#MapStatus2").html(msg);
+ debug.log("updateStatus2(): "+ msg);
+ }
+ }
- // TODO: remove this?
- function getXmlData () {
- // jsoncallback=?
- xmlUrl = 'http://feeds2.feedburner.com/torontoevents?format=xml&jsoncallback=?';
- if (xmlUrl.search(/^(http|feed)/i) < 0) {
- debug.log("getXmlData(): bad url: "+ gCalUrl);
- return;
- }
- $.ajax({
- type: "GET",
- url: xmlUrl,
- dataType: "xml",
- dataType: "json",
- global: false,
- processData: false,
- dataFilter: function(xml) {
- debug.log("xml2 data: %o", xml);
- },
- success: function(xml) {
- debug.log("xml3 data: %o", xml);
+ // TODO: remove this?
+ function getXmlData () {
+ // jsoncallback=?
+ xmlUrl = 'https://feeds2.feedburner.com/torontoevents?format=xml&jsoncallback=?';
+ if (xmlUrl.search(/^(http|feed)/i) < 0) {
+ debug.log("getXmlData(): bad url: "+ calendarId);
+ return;
+ }
+ $.ajax({
+ type: "GET",
+ url: xmlUrl,
+ dataType: "xml",
+ dataType: "json",
+ global: false,
+ processData: false,
+ dataFilter: function(xml) {
+ debug.log("xml2 data: %o", xml);
+ },
+ success: function(xml) {
+ debug.log("xml3 data: %o", xml);
/*
- $(xml).find('label').each(function(){
- var id_text = $(this).attr('id')
- var name_text = $(this).find('name').text()
-
- $(' ')
- .html(name_text + ' (' + id_text + ')')
- .appendTo('#update-target ol');
- }); //close each(
+ $(xml).find('label').each(function(){
+ var id_text = $(this).attr('id')
+ var name_text = $(this).find('name').text()
+
+ $(' ')
+ .html(name_text + ' (' + id_text + ')')
+ .appendTo('#update-target ol');
+ }); //close each(
*/
- }
- }); //close $.ajax
+ }
+ }); //close $.ajax
- return;
- }
+ return;
+ }
- function fakeGCalData () {
- debug.log("fakeGCalData() init");
- cnMF.addEventType({
- title: 'Test1',
- titleLink: ''
- });
-
- //mapRedraw(); return;
-
- days = 6;
- now = new Date();
- times = 3;
- while (times--) {
- for (var ii in fakeMarkersList) {
- kk = cnMF.addEvent(fakeMarkersList[ii]);
- kk.validCoords = true;
- kk.dateStart = cnMF.parseDate(now.getTime() + Math.floor(Math.random() * days * 24 * 3600 * 1000));
- kk.dateEnd = cnMF.parseDate(kk.dateStart.getTime() + 2 * 3600 * 1000);
- //debug.log("fakeGCalData() changed start/end: ", kk);
- }
- }
- debug.log("fakeGCalData() using fake calendar data: ", cnMF);
- mapRedraw();
- }
+ function fakeGCalData () {
+ debug.log("fakeGCalData() init");
+ cnMF.addEventType({
+ title: 'Test1',
+ titleLink: ''
+ });
+
+ //mapRedraw(); return;
+
+ days = 6;
+ now = new Date();
+ times = 3;
+ while (times--) {
+ for (var ii in fakeMarkersList) {
+ kk = cnMF.addEvent(fakeMarkersList[ii]);
+ kk.validCoords = true;
+ kk.dateStart = cnMF.parseDate(now.getTime() + Math.floor(Math.random() * days * 24 * 3600 * 1000));
+ kk.dateEnd = cnMF.parseDate(kk.dateStart.getTime() + 2 * 3600 * 1000);
+ //debug.log("fakeGCalData() changed start/end: ", kk);
+ }
+ }
+ debug.log("fakeGCalData() using fake calendar data: ", cnMF);
+ mapRedraw();
+ }
/*
- * TODO: Performance
- * build dom fragment then insert: http://ejohn.org/blog/dom-documentfragments/
- */
+ * TODO: Performance
+ * build dom fragment then insert: http://ejohn.org/blog/dom-documentfragments/
+ */
- function setupChangeDates() {
- var html = ''
- + 'New start date: '
- + 'New end date: '
- + 'Reloading will fetch all calendar event data between start and end date, then reload GCM. This is needed if you want to expand the date range.'
- + ' However, if you just want a shorter date range, use the date sliders to instantly filter events.
'
- + 'Reload
'
- + 'Cancel
';
- $('#newDates').html(html);
-
- var dialogDates = $("#newDates").dialog({
- //bgiframe: true,
- autoOpen: false, resizable: true,
- position: ['center','center'],
- width: 600, height: 380,
- modal: true
- });
- dialogDates.dialog('open');
- var dateFormatStr = "%Y-%m-%d"; // must match date format in days2date
- new JsDatePick({
- target:"startDate",
- useMode:2,
- dateFormat:dateFormatStr
+ function setupChangeDates() {
+ var html = ''
+ + 'New start date: '
+ + 'New end date: '
+ + 'Reloading will fetch all calendar event data between start and end date, then reload GCM. This is needed if you want to expand the date range.'
+ + ' However, if you just want a shorter date range, use the date sliders to instantly filter events.
'
+ + 'Reload
'
+ + 'Cancel
';
+ $('#newDates').html(html);
+
+ var dialogDates = $("#newDates").dialog({
+ //bgiframe: true,
+ autoOpen: false, resizable: true,
+ position: ['center','center'],
+ width: 600, height: 380,
+ modal: true
});
- new JsDatePick({
- target:"endDate",
- useMode:2,
- dateFormat:dateFormatStr
- });
- $('#cancelReloadPage').click(function(){
- _gaq.push(['_trackEvent', 'Interaction', 'cancelReloadPage']);
- $("#newDates").dialog('close');
- });
- $('#reloadPage').click(function(){
- var sd = $("#startDate").val().replace(/\//, '-');
- var ed = $("#endDate").val().replace(/\//, '-');
- var url = window.location.href;
- if (window.location.search.match(/sd=\d+/)) {
- url = url.replace(/sd=[\d\-]+/,'sd='+sd).replace(/ed=[\d\-]+/,'ed='+ed);
- } else {
- url = url.replace(/u=/,'sd='+sd+'&ed='+ed+'&u=');
- }
- debug.log("reloadPage new url: ", url);
- _gaq.push(['_trackEvent', 'Interaction', 'reloadPage', url]);
- alert('going to url: '+url);
- window.location = url;
- //$("#newDates").dialog('close');
- //getGCalData(cnMFUI.opts.gCalUrl, date2days( $("#startDate").val()), date2days( $("#endDate").val()));
- });
- }
+ dialogDates.dialog('open');
+ var dateFormatStr = "%Y-%m-%d"; // must match date format in days2date
+ new JsDatePick({
+ target:"startDate",
+ useMode:2,
+ dateFormat:dateFormatStr
+ });
+ new JsDatePick({
+ target:"endDate",
+ useMode:2,
+ dateFormat:dateFormatStr
+ });
+ $('#cancelReloadPage').click(function(){
+ debug.info('--- clicked cancelReloadPage, ');
+ _gaq.push(['_trackEvent', 'Interaction', 'cancelReloadPage']);
+ $("#newDates").dialog('close');
+ });
+ $('#reloadPage').click(function(){
+ var sd = $("#startDate").val().replace(/\//, '-');
+ var ed = $("#endDate").val().replace(/\//, '-');
+ var url = window.location.href;
+ if (window.location.search.match(/sd=\d+/)) {
+ url = url.replace(/sd=[\d\-]+/,'sd='+sd).replace(/ed=[\d\-]+/,'ed='+ed);
+ } else {
+ url = url.replace(/u=/,'sd='+sd+'&ed='+ed+'&u=');
+ }
+ debug.info('--- clicked reloadPage, new url: ', url);
+ _gaq.push(['_trackEvent', 'Interaction', 'reloadPage', url]);
+ alert('Reloading to url: '+url);
+ window.location = url;
+ //$("#newDates").dialog('close');
+ //getGCalData(cnMFUI.opts.calendarId, date2days( $("#startDate").val()), date2days( $("#endDate").val()));
+ });
+ }
- function cbCalendarLoad() {
- document.title = document.title +" - "+ cnMF.gcTitle;
+ function cbCalendarLoad(calendarInfo) {
+ var html,
+ titles = [],
+ totalEvents = 0;
- updateStatus(""
- + cnMF.gcTitle +" Mapping Events ... ");
- $('#calendarTitleContent').html("");
-
- html = 'Calendar has '+ cnMF.totalEvents + (cnMF.totalEvents==cnMF.totalEntries ? ''
- :' ('+cnMF.totalEntries+' )')
- +' events from '+ cnMF.formatDate(startDate, 'Y-n-D') +' '
- +' to '+ cnMF.formatDate(endDate, 'Y-n-D') +' '
- +' Change dates '
- + '
';
- $('#calendarTitleContent').append(html);
- _gaq.push(['_trackEvent', 'Loading', 'calenders', 'cnMF.totalEvents', cnMF.totalEvents]);
- _gaq.push(['_trackEvent', 'Loading', 'calender-cnMF.gcLink', cnMF.gcLink]);
-
- $('#changeDates').click(function(){
- _gaq.push(['_trackEvent', 'Interaction', 'changeDates']);
- setupChangeDates();
- });
+ cnMF.calData = cnMF.calData || {};
+ cnMF.calData[calendarInfo.calendarId] = calendarInfo;
+
+ $.each(cnMF.calData, function(calendarId, calendarInfo) {
+ totalEvents += calendarInfo.totalEvents;
+ titles.push(calendarInfo.gcTitle);
+ });
+
+ // check to see if we are processing more than one calendar or not.
+ if (cnMF.validCalendars) {
+ cnMF.validCalendars++;
+ document.title = "GCM "+ cnMF.validCalendars + " valid calendars";
+
+ updateStatus(""
+ + calendarInfo.gcTitle +" Mapping Events ... ");
+
+ $('#calendarTitleContent').html(''
+ +' '+ cnMF.validCalendars +' Calendars have '+ totalEvents
+ +' events from '+ cnMF.formatDate(startDate, 'Y-n-D') +' '
+ +' to '+ cnMF.formatDate(endDate, 'Y-n-D') +' '
+ +' Change dates '
+ + '
');
+ } else {
+ cnMF.validCalendars = 1;
+ document.title = "GCM - "+ calendarInfo.gcTitle;
+
+ updateStatus(""
+ + calendarInfo.gcTitle +" Mapping Events ... ");
+
+ $('#calendarTitleContent').html(""
+ + 'Calendar has '+ calendarInfo.totalEvents +
+ (calendarInfo.totalEvents==calendarInfo.totalEntries ? ''
+ :' ('+calendarInfo.totalEntries+' )')
+ +' events from '+ cnMF.formatDate(startDate, 'Y-n-D') +' '
+ +' to '+ cnMF.formatDate(endDate, 'Y-n-D') +' '
+ +' Change dates '
+ + '
');
+
+ $('#changeDates').click(function(){
+ debug.info('--- clicked changeDates');
+ _gaq.push(['_trackEvent', 'Interaction', 'changeDates']);
+ setupChangeDates();
+ });
- $('#cancelChangeDates').click(function(){
- _gaq.push(['_trackEvent', 'Interaction', 'cancelChangeDates']);
- $("#newDates").css('display','none');
- });
- $('#thDate').attr('title','Click to Sort by Event Date, Timezone '+cnMF.tz.name);
+ $('#cancelChangeDates').click(function(){
+ debug.info('--- clicked cancelChangeDates');
+ _gaq.push(['_trackEvent', 'Interaction', 'cancelChangeDates']);
+ $("#newDates").css('display','none');
+ });
+ $('#thDate').attr('title','Click to Sort by Event Date, Timezone '+cnMF.tz.name);
- //updateStatus2('Found '+ cnMF.totalEvents +' ('+cnMF.totalEntries+') events, '+ uniqAddrCount +' unique addresses, decoding .. ');
- updateStatus2('Found '+ cnMF.myGeo.numAddresses +' events, '+ cnMF.myGeo.numUniqAddresses +' unique addresses, decoding .. ');
- }
+ }
+
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'calendarInfo.totalEvents', calendarInfo.totalEvents]);
+ _gaq.push(['_trackEvent', 'Loading', 'calender-calendarInfo.gcLink', calendarInfo.gcLink]);
- // cbGeoDecodeComplete() called everytime we get a response from the internet
- function cbGeoDecodeAddr () {
- cnt = cnMF.myGeo.count();
- updateStatus2(cnt.uniqAddrDecoded +' of '+ cnt.uniqAddrTotal +' decoded ' + cnt.uniqAddrErrors +' errors' );
- cnMF.reportData['uniqAddrDecoded'] = cnt.uniqAddrDecoded;
- cnMF.reportData['uniqAddrTotal'] = cnt.uniqAddrTotal;
- cnMF.reportData['uniqAddrErrors'] = cnt.uniqAddrErrors;
+ //updateStatus2('Found '+ calendarInfo.totalEvents +' ('+calendarInfo.totalEntries+') events, '+ uniqAddrCount +' unique addresses, decoding .. ');
+ cnt = cnMF.myGeo.count();
- // if user has NOT interacted with map,
- // AND its been over 3000 ms since last updated map with results from
- // newly decoded addresses (we don't want to update map too frequently),
- // THEN update the loading map
- if (cnMF.reportData.userInteracted) {
- return;
+ updateStatus2('Found '+ cnMF.myGeo.numAddresses +' events, '+ cnt.uniqAddrTotal +' unique addresses, decoding .. ');
}
- var now = new Date().getTime();
- if (lastUpdate4MapLoad) {
- if (3000 < (now - lastUpdate4MapLoad)) {
+
+ // cbGeoDecodeAddr() called everytime we get a response from the internet
+ function cbGeoDecodeAddr () {
+ cnt = cnMF.myGeo.count();
+ updateStatus2(cnt.uniqAddrDecoded +' of '+ cnt.uniqAddrTotal +' decoded ' + cnt.uniqAddrErrors +' errors' );
+ cnMF.reportData.uniqAddrDecoded = cnt.uniqAddrDecoded;
+ cnMF.reportData.uniqAddrTotal = cnt.uniqAddrTotal;
+ cnMF.reportData.uniqAddrErrors = cnt.uniqAddrErrors;
+
+ // if user has NOT interacted with map,
+ // AND its been over 3000 ms since last updated map with results from
+ // newly decoded addresses (we don't want to update map too frequently),
+ // THEN update the loading map
+
+ if (userInteraction.hasOccurred()) {
+ debug.log("cbGeoDecodeAddr() user has interacted with map, skip updateLoadingMap");
+ return;
+ }
+ var now = new Date().getTime();
+ if (lastUpdate4MapLoad) {
+ if (3000 < (now - lastUpdate4MapLoad)) {
updateLoadingMap();
+ }
+ } else {
+ lastUpdate4MapLoad = now;
}
- } else {
- lastUpdate4MapLoad = now;
}
- }
- function cbUserInteracted() {
- if (cnMF.reportData.userInteracted) {
- // already recorded first user interaction
- return;
- }
- GEvent.removeListener(mapClickListener);
- GEvent.removeListener(mapDragstartListener);
- cnMF.reportData.userInteracted = '' + new Date().getTime();
- cnMF.reportData.userInteracted = cnMF.reportData.userInteracted.replace(/(\d{3})$/,".$1"); // add period so its secs.msec
- debug.log("cnMF.reportData.userInteracted ",cnMF.reportData.userInteracted);
- }
- function updateLoadingMap() {
- // only showing all events (move from chicago to NY, zoom out, etc) IF
- // - user has not specified a specific zoom (handled in index.php logic and passed to us via mapAllOnInit)
- // - user has not already started interacting with the map
- if (cnMFUI.opts.mapAllOnInit && !cnMF.reportData.userInteracted) {
- cnMF.mapAllEvents();
- } else {
- mapRedraw();
+ // Architecture note - experimenting with the idea of using closures to group similar chunks of code
+ //
+ // userInteraction.init() to init, setting up map listeners
+ // userInteraction.recordInteraction() to record that use interacted (clicked on something)
+ // userInteraction.hasOccurred() returns true if user has interacted
+ //
+ var userInteraction = (function(){
+ var listeners = {};
+ var usersFirstInteraction = null;
+ var userInteraction = {
+ init : function() {
+ $.each(['click', 'dragstart'], function(index, mapEvent){
+ // if any listener fires, then user has interacted. Clear all listeners and set usersFirstInteraction to current time (non-false).
+ listeners[mapEvent] = google.maps.event.addListener(myGmap, mapEvent, function() {
+ userInteraction.recordInteraction();
+ });
+ });
+
+ },
+ recordInteraction : function() {
+ usersFirstInteraction = '' + new Date().getTime();
+ usersFirstInteraction = usersFirstInteraction.replace(/(\d{3})$/,".$1"); // add period so its secs.msec
+ $.each(listeners, function(theMapEvent, mapsEventListener){
+ google.maps.event.removeListener(mapsEventListener);
+ });
+ debug.log("userInteraction.recordInteraction ", usersFirstInteraction);
+ },
+ hasOccurred : function() {
+ return usersFirstInteraction != null;
+ }
+ }
+ return userInteraction;
+ })();
+
+ function updateLoadingMap() {
+ // Only resize map to show all events (move from chicago to NY, zoom out, etc),
+ // IF user has not specified a specific zoom in URL,
+ // AND user has not already started interacting with the map.
+ if (cnMFUI.opts.mapAllOnInit && !userInteraction.hasOccurred()) {
+ debug.log("updateLoadingMap() resizing map to show events, mapAllEvents");
+ cnMF.mapAllEvents();
+ } else {
+ debug.log("updateLoadingMap() not resizing map to show events, just mapRedraw", cnMFUI.opts.mapAllOnInit, userInteraction.hasOccurred());
+ mapRedraw();
+ }
}
- }
- // cbGeoDecodeComplete() called once all addresses are decoded
- function cbGeoDecodeComplete() {
- debug.log("cbGeoDecodeComplete() begins");
- //if (cnMFUI.opts.mapCenterLt == cnMFUI.defaults.mapCenterLt)
-
- cnMF.reportData.submitTime = '' + new Date().getTime();
- cnMF.reportData.submitTime = cnMF.reportData.submitTime.replace(/(\d{3})$/,".$1"); // add period so its secs.msec
- debug.info("cbGeoDecodeComplete() post cnMF.reportData:", cnMF.reportData);
-
- _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrDecoded', cnMF.reportData.uniqAddrDecoded]);
- _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrTotal', cnMF.reportData.uniqAddrTotal]);
- _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrErrors', cnMF.reportData.uniqAddrErrors]);
- _gaq.push(['_trackEvent', 'Loading', 'calenders', 'totalGeoDecodes', 1]);
- _gaq.push(['_trackEvent', 'Loading', 'calenders', 'totalGeoDecodeMsecs', parseInt(cnMF.reportData.submitTime.replace(/\./,'')) - parseInt(cnMF.reportData.loadTime.replace(/\./,''))]);
- // only script tag can bypass same-origin-policy, so use jsonp hack by adding callback=?
- // note that URL limit is 1024 (?) chars, so can't send too much data
- //$.post("http://chadnorwood.com/saveJson/", {sj: cnMF.reportData});
- $.getJSON("http://chadnorwood.com/saveJson/?callback=?", {sj: cnMF.reportData});
+ // cbGeoDecodeComplete() called once all addresses are decoded for a given calendar
+ function cbGeoDecodeComplete(calendarId, calendarIds) {
+ debug.log("cbGeoDecodeComplete() begins");
+ //if (cnMFUI.opts.mapCenterLt == cnMFUI.defaults.mapCenterLt)
+
+ cnMF.reportData.submitTime = '' + new Date().getTime();
+ cnMF.reportData.submitTime = cnMF.reportData.submitTime.replace(/(\d{3})$/,".$1"); // add period so its secs.msec
+ debug.debug(" cbGeoDecodeComplete() post cnMF.reportData:", cnMF.reportData);
+
+ var decodeMs = parseInt(cnMF.reportData.submitTime.replace(/\./,'')) - parseInt(cnMF.reportData.loadTime.replace(/\./,'') );
+
+ _gaq.push(['_trackEvent', 'Loading', 'cal-complete', calendarId]);
+ _gaq.push(['_trackEvent', 'Loading', 'cal-loadTime', calendarId, decodeMs]); // we can find avg decode time per calendar
+ debug.debug(" cbGeoDecodeComplete() calendars decode time:", calendarId, decodeMs );
+
+ calendarsDecoding -= 1;
+ if (calendarsDecoding === 0) {
+ // All calendars have been geoDecoded
+ // Event Category: Loading - Event Action: calendars - Event Label: uniqAddrDecoded
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrDecoded', cnMF.reportData.uniqAddrDecoded]);
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrTotal', cnMF.reportData.uniqAddrTotal]);
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrUnknown', cnMF.reportData.uniqAddrUnknown]);
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrErrors', cnMF.reportData.uniqAddrErrors]);
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'uniqAddrErrorPercent',
+ Math.round(10000*cnMF.reportData.uniqAddrErrors/cnMF.reportData.uniqAddrTotal)/100 ]);
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'totalGeoDecodes', 1]);
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'totalGeoDecodeMsecs', decodeMs]); // so we can find avg decode time
+ _gaq.push(['_trackEvent', 'Loading', 'calenders', 'calendarCount', calendarIds.length]); // we can find avg decode time per calendar
+ for (var ii in calendarIds) {
+ _gaq.push(['_trackEvent', 'Loading', 'cal-decoded' + calendarIds.length, calendarIds[ii], cnMF.reportData.uniqAddrDecoded] );
+ _gaq.push(['_trackEvent', 'Loading', 'cal-total' + calendarIds.length, calendarIds[ii], cnMF.reportData.uniqAddrTotal] );
+ _gaq.push(['_trackEvent', 'Loading', 'cal-errors' + calendarIds.length, calendarIds[ii], cnMF.reportData.uniqAddrErrors] );
+ }
+ debug.debug(" cbGeoDecodeComplete() All calendars decoded.", decodeMs, cnMF.reportData );
+ } else {
+ debug.debug(" cbGeoDecodeComplete() still decoding "+ calendarsDecoding + " more calendar.");
+
+ }
+
+ // TODO: remove cnMF.reportData ?
- updateLoadingMap();
- }
+ updateLoadingMap();
+
+ if (cnMFUI.opts.closeDrawer) {
+ closeDrawer()
+ }
+ }
+ function dumpError(err) {
+ var msg = '';
+ if (typeof err === 'object') {
+ if (err.message) {
+ msg += '\nMessage: ' + err.message;
+ }
+ if (err.stack) {
+ msg += '\nStacktrace:';
+ msg += '\n====================\n'+ err.stack;
+ }
+ } else {
+ msg += 'dumpError :: argument is not an object';
+ }
+
+ }
- init();
- return this;
+ init();
+ return this;
} // end cnMFUI_init()
@@ -1089,7 +1573,7 @@
jumpTxt: "Jump to City, Address, or Zip",
// div id's used by mapFilter
- mapId: "MapID",
+ mapId: "map_id",
listId: "resultsTab",
statusId: "MapStatus",
@@ -1102,11 +1586,13 @@
mapType: 0,
mapAllOnInit: true,
- numTableRows: 5,
-
- unSupportedHtml: "Unfortunately your browser doesn't support Google Maps. To check browser compatibility visit the following link .",
+ // sliderChad does not work well on IE, FF12+, and iPad (?)
+ useOverlappingSliders: (jQuery.browser.msie) // IE
+ || (jQuery.browser.mozilla && parseFloat(jQuery.browser.version) >= 12) // FF12+
+ || (navigator.userAgent.match(/iPad/i) != null), // iPad
- googleApiKey: 'ABQIAAAAQ8l06ldZX6JSGI8gETtVhhTrRIj9DJoJiLGtM4J1SrTlGmVDcxQDT5BVw88R8j75IQxYlwFcEw6w9w' // chadnorwood.com
+ numTableRows: 5
+ // googleApiKey: 'ABQIAAAAQ8l06ldZX6JSGI8gETtVhhTrRIj9DJoJiLGtM4J1SrTlGmVDcxQDT5BVw88R8j75IQxYlwFcEw6w9w' // v2 api for chadnorwood.com
},
@@ -1114,99 +1600,33 @@
opts: {},
- // jquery.mapFilter.hItem() should be called whenever user clicks on
- // map marker or clicks on name in results tab,
- // Opens info window for marker and highights name in results tab
- hItem: function(n, cur) {
-
- /*
- CHAD TODO:
- - highlight: when marker highlights more than 1 event, need 2 highlights - brighter shade for current one, and less bright for all others at same location
- - make event list move to highlighted event (jscrollpane - update css: top)
- */
-
- if (typeof n == 'number') {
- // clicked on list, only one to be displayed in map info window
- kks = [n];
- gMrkr = cnMF.myMarkers.getGoogleMarker(cnMF.eventList[n].getCoordsStr());
- } else if (typeof n == 'string') {
- // n is coordinaetes string. clicked on map, could be more than one event
- kks = cnMF.myMarkers.getEvents(n);
- gMrkr = cnMF.myMarkers.getGoogleMarker(n);
- }
-
- if (!cur) cur = 0;
- infoHtml = cnMF.eventList[kks[cur]].infoHtml;
- if (kks.length > 1) {
- href = '<< prev - ';
- next = (cur == (kks.length-1)) ? '' : ' - '+ href + (cur+1) + ')">next >> ';
- infoHtml += "
"+ prev +"Showing "+ (cur+1) +" of "+ kks.length;
- infoHtml += " Events at this location"+ next +"
";
- }
-
- debug.log("hItem("+n+") list: ", kks);
- debug.log("hItem("+n+") cnMFUI.opts.listId: ", cnMFUI.opts.listId);
-
- // remove any previous highlights and highlight new ones
- //$("#"+ cnMFUI.opts.listId +" a").removeClass("highlight2");
- // TODO use this - $("a.eventNameTrigger.highlight2").removeClass("highlight2");
- $("a.eventNameTrigger").removeClass("highlight2");
- $("a.eventNameTrigger").click(function(){
- _gaq.push(['_trackEvent', 'Interaction', 'a.eventNameTrigger']);
- });
-
- //$("#"+ cnMFUI.opts.listId +" a[onclick*=hItem("+ kks[cur] +")]").addClass("highlight2");
- for (var ii in kks) {
- $("#"+ cnMFUI.opts.listId +" a[onclick*=hItem("+ kks[ii] +")]").addClass("highlight2");
- $("a.eventNameTrigger[onclick*=hItem("+ kks[ii] +")]").addClass("highlight2");
- }
-
- // note - addListener setup in initResults() for map's infowindowclose event, calls mapRedraw
- // open info window for item and center it
- myGmap.closeInfoWindow();
- //myGmap.panTo(gMrkr.getLatLng());
- gMrkr.openInfoWindowHtml(infoHtml);
-
- $("#ResultsMapHdrFilterByMap").css('display','none');
- $("#ResultsMapHdrFilterFrozen").css('display','inline');
-
- //mapChanged();
- return false;
- },
- zoomTo: function(id) {
- var maxZoom = 19,
- curZoom = myGmap.getZoom(),
- newZoom = maxZoom;
- if (curZoom < maxZoom) {
- var increaseZoom = Math.floor((maxZoom - curZoom)/2);
- newZoom = curZoom + (increaseZoom > 1 ? increaseZoom : 1);
- }
- debug.warn("zoomTo(): maxZoom="+maxZoom+", curZoom="+curZoom+", newZoom="+newZoom);
- coords = cnMF.eventList[id].getCoordsStr();
- myGmap.setCenter(cnMF.myMarkers.getGoogleMarker(coords).getLatLng(), newZoom);
- //mapChanged();
- },
jumpToAddress: function(address) {
- // If its a street address, we want to zoom in more than if city or country
- // For now, assuming street address if address contains a comma
- if (address.search(/,/) == -1) {cZoom=11;} else {cZoom=16;}
+ // If its a street address, we want to zoom in more than if city or country
+ // For now, assuming street address if address contains a comma
+ if (address.search(/,/) == -1) {cZoom=11;} else {cZoom=16;}
+
+ cnMF.myMarkers.closeInfoWindow();
- myGmap.closeInfoWindow();
- gGeocoder.getLatLng( address, function(point) {
- if (!point) {
+ cnMF.myGeo.addr2coords( address, function (gObj) {
+ if (gObj.lt) {
+ if (typeof(kk) == 'object') {
+ kk.lt = gObj.lt;
+ kk.lg = gObj.lg;
+ }
+ jumptxt = '';
+ myGmap.setCenter(new google.maps.LatLng(gObj.lt, gObj.lg) );
+ myGmap.setZoom(cZoom);
+ } else {
+ // log gObj.error;
// focus on something else so when user clicks on jumpBox again, it will clear
- $("#LogoInfo").focus;
+ $("#gcmMapLogo").focus;
jumptxt = "NOT FOUND: "+ address;
$("#jumpBox").val(jumptxt);
- } else {
- jumptxt = '';
- myGmap.setCenter(point, cZoom);
- }
- });
+ }
+ }, null);
},
htmlEncode: function (value){
@@ -1219,6 +1639,7 @@
maxStr: function(str, maxChars, maxLines, link, chopLongStrings) {
shorten = false;
+ if (!(str && typeof str == 'string')) return '';
if ((maxChars > 1) && (str.length > maxChars)) {
shorten = true;
@@ -1231,7 +1652,7 @@
str = str.substring(0,ii);
break;
}
- }
+ }
}
// chopLongStrings..
// table column width gets screwed with long strings (urls), so chop'em if longer than 15 chars!!
@@ -1244,7 +1665,7 @@
while (rgx.test(str)) {
str = str.replace(rgx, '$1' +' '+ '$2');
- }
+ }
//alert ("maxStr YES match, new:\n\n"+ str);
//debug.log("-- maxStr YES match, old:\n"+ss+"\n\n new:\n"+ str);
} else {
diff --git a/examples/gcm/js/jScrollPane.js b/examples/gcm/js/jScrollPane.js
deleted file mode 100644
index cbd046b..0000000
--- a/examples/gcm/js/jScrollPane.js
+++ /dev/null
@@ -1,667 +0,0 @@
-/* Copyright (c) 2009 Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com)
- * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
- * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
- *
- * See http://kelvinluck.com/assets/jquery/jScrollPane/
- * $Id: jScrollPane.js 90 2010-01-25 03:52:10Z kelvin.luck $
- */
-
-/**
- * Replace the vertical scroll bars on any matched elements with a fancy
- * styleable (via CSS) version. With JS disabled the elements will
- * gracefully degrade to the browsers own implementation of overflow:auto.
- * If the mousewheel plugin has been included on the page then the scrollable areas will also
- * respond to the mouse wheel.
- *
- * @example jQuery(".scroll-pane").jScrollPane();
- *
- * @name jScrollPane
- * @type jQuery
- * @param Object settings hash with options, described below.
- * scrollbarWidth - The width of the generated scrollbar in pixels
- * scrollbarMargin - The amount of space to leave on the side of the scrollbar in pixels
- * wheelSpeed - The speed the pane will scroll in response to the mouse wheel in pixels
- * showArrows - Whether to display arrows for the user to scroll with
- * arrowSize - The height of the arrow buttons if showArrows=true
- * animateTo - Whether to animate when calling scrollTo and scrollBy
- * dragMinHeight - The minimum height to allow the drag bar to be
- * dragMaxHeight - The maximum height to allow the drag bar to be
- * animateInterval - The interval in milliseconds to update an animating scrollPane (default 100)
- * animateStep - The amount to divide the remaining scroll distance by when animating (default 3)
- * maintainPosition- Whether you want the contents of the scroll pane to maintain it's position when you re-initialise it - so it doesn't scroll as you add more content (default true)
- * tabIndex - The tabindex for this jScrollPane to control when it is tabbed to when navigating via keyboard (default 0)
- * enableKeyboardNavigation - Whether to allow keyboard scrolling of this jScrollPane when it is focused (default true)
- * animateToInternalLinks - Whether the move to an internal link (e.g. when it's focused by tabbing or by a hash change in the URL) should be animated or instant (default false)
- * scrollbarOnLeft - Display the scrollbar on the left side? (needs stylesheet changes, see examples.html)
- * reinitialiseOnImageLoad - Whether the jScrollPane should automatically re-initialise itself when any contained images are loaded (default false)
- * topCapHeight - The height of the "cap" area between the top of the jScrollPane and the top of the track/ buttons
- * bottomCapHeight - The height of the "cap" area between the bottom of the jScrollPane and the bottom of the track/ buttons
- * observeHash - Whether jScrollPane should attempt to automagically scroll to the correct place when an anchor inside the scrollpane is linked to (default true)
- * @return jQuery
- * @cat Plugins/jScrollPane
- * @author Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com)
- */
-
-(function($) {
-
-$.jScrollPane = {
- active : []
-};
-$.fn.jScrollPane = function(settings)
-{
- settings = $.extend({}, $.fn.jScrollPane.defaults, settings);
-
- var rf = function() { return false; };
-
- return this.each(
- function()
- {
- var $this = $(this);
- var paneEle = this;
- var currentScrollPosition = 0;
- var paneWidth;
- var paneHeight;
- var trackHeight;
- var trackOffset = settings.topCapHeight;
- var $container;
-
- if ($(this).parent().is('.jScrollPaneContainer')) {
- $container = $(this).parent();
- currentScrollPosition = settings.maintainPosition ? $this.position().top : 0;
- var $c = $(this).parent();
- paneWidth = $c.innerWidth();
- paneHeight = $c.outerHeight();
- $('>.jScrollPaneTrack, >.jScrollArrowUp, >.jScrollArrowDown, >.jScrollCap', $c).remove();
- $this.css({'top':0});
- } else {
- $this.data('originalStyleTag', $this.attr('style'));
- // Switch the element's overflow to hidden to ensure we get the size of the element without the scrollbars [http://plugins.jquery.com/node/1208]
- $this.css('overflow', 'hidden');
- this.originalPadding = $this.css('paddingTop') + ' ' + $this.css('paddingRight') + ' ' + $this.css('paddingBottom') + ' ' + $this.css('paddingLeft');
- this.originalSidePaddingTotal = (parseInt($this.css('paddingLeft')) || 0) + (parseInt($this.css('paddingRight')) || 0);
- paneWidth = $this.innerWidth();
- paneHeight = $this.innerHeight();
- $container = $('
')
- .attr({'className':'jScrollPaneContainer'})
- .css(
- {
- 'height':paneHeight+'px',
- 'width':paneWidth+'px'
- }
- );
- if (settings.enableKeyboardNavigation) {
- $container.attr(
- 'tabindex',
- settings.tabIndex
- );
- }
- $this.wrap($container);
- $container = $this.parent();
- // deal with text size changes (if the jquery.em plugin is included)
- // and re-initialise the scrollPane so the track maintains the
- // correct size
- $(document).bind(
- 'emchange',
- function(e, cur, prev)
- {
- $this.jScrollPane(settings);
- }
- );
-
- }
- trackHeight = paneHeight;
-
- if (settings.reinitialiseOnImageLoad) {
- // code inspired by jquery.onImagesLoad: http://plugins.jquery.com/project/onImagesLoad
- // except we re-initialise the scroll pane when each image loads so that the scroll pane is always up to size...
- // TODO: Do I even need to store it in $.data? Is a local variable here the same since I don't pass the reinitialiseOnImageLoad when I re-initialise?
- var $imagesToLoad = $.data(paneEle, 'jScrollPaneImagesToLoad') || $('img', $this);
- var loadedImages = [];
-
- if ($imagesToLoad.length) {
- $imagesToLoad.each(function(i, val) {
- $(this).bind('load readystatechange', function() {
- if($.inArray(i, loadedImages) == -1){ //don't double count images
- loadedImages.push(val); //keep a record of images we've seen
- $imagesToLoad = $.grep($imagesToLoad, function(n, i) {
- return n != val;
- });
- $.data(paneEle, 'jScrollPaneImagesToLoad', $imagesToLoad);
- var s2 = $.extend(settings, {reinitialiseOnImageLoad:false});
- $this.jScrollPane(s2); // re-initialise
- }
- }).each(function(i, val) {
- if(this.complete || this.complete===undefined) {
- //needed for potential cached images
- this.src = this.src;
- }
- });
- });
- };
- }
-
- var p = this.originalSidePaddingTotal;
- var realPaneWidth = paneWidth - settings.scrollbarWidth - settings.scrollbarMargin - p;
-
- var cssToApply = {
- 'height':'auto',
- 'width': realPaneWidth + 'px'
- }
-
- if(settings.scrollbarOnLeft) {
- cssToApply.paddingLeft = settings.scrollbarMargin + settings.scrollbarWidth + 'px';
- } else {
- cssToApply.paddingRight = settings.scrollbarMargin + 'px';
- }
-
- $this.css(cssToApply);
-
- var contentHeight = $this.outerHeight();
- var percentInView = paneHeight / contentHeight;
-
- var isScrollable = percentInView < .99;
- $container[isScrollable ? 'addClass' : 'removeClass']('jScrollPaneScrollable');
-
- if (isScrollable) {
- $container.append(
- $('
').addClass('jScrollCap jScrollCapTop').css({height:settings.topCapHeight}),
- $('
').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append(
- $('
').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append(
- $('
').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}),
- $('
').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth+'px'})
- )
- ),
- $('
').addClass('jScrollCap jScrollCapBottom').css({height:settings.bottomCapHeight})
- );
-
- var $track = $('>.jScrollPaneTrack', $container);
- var $drag = $('>.jScrollPaneTrack .jScrollPaneDrag', $container);
-
-
- var currentArrowDirection;
- var currentArrowTimerArr = [];// Array is used to store timers since they can stack up when dealing with keyboard events. This ensures all timers are cleaned up in the end, preventing an acceleration bug.
- var currentArrowInc;
- var whileArrowButtonDown = function()
- {
- if (currentArrowInc > 4 || currentArrowInc % 4 == 0) {
- positionDrag(dragPosition + currentArrowDirection * mouseWheelMultiplier);
- }
- currentArrowInc++;
- };
-
- if (settings.enableKeyboardNavigation) {
- $container.bind(
- 'keydown.jscrollpane',
- function(e)
- {
- switch (e.keyCode) {
- case 38: //up
- currentArrowDirection = -1;
- currentArrowInc = 0;
- whileArrowButtonDown();
- currentArrowTimerArr[currentArrowTimerArr.length] = setInterval(whileArrowButtonDown, 100);
- return false;
- case 40: //down
- currentArrowDirection = 1;
- currentArrowInc = 0;
- whileArrowButtonDown();
- currentArrowTimerArr[currentArrowTimerArr.length] = setInterval(whileArrowButtonDown, 100);
- return false;
- case 33: // page up
- case 34: // page down
- // TODO
- return false;
- default:
- }
- }
- ).bind(
- 'keyup.jscrollpane',
- function(e)
- {
- if (e.keyCode == 38 || e.keyCode == 40) {
- for (var i = 0; i < currentArrowTimerArr.length; i++) {
- clearInterval(currentArrowTimerArr[i]);
- }
- return false;
- }
- }
- );
- }
-
- if (settings.showArrows) {
-
- var currentArrowButton;
- var currentArrowInterval;
-
- var onArrowMouseUp = function(event)
- {
- $('html').unbind('mouseup', onArrowMouseUp);
- currentArrowButton.removeClass('jScrollActiveArrowButton');
- clearInterval(currentArrowInterval);
- };
- var onArrowMouseDown = function() {
- $('html').bind('mouseup', onArrowMouseUp);
- currentArrowButton.addClass('jScrollActiveArrowButton');
- currentArrowInc = 0;
- whileArrowButtonDown();
- currentArrowInterval = setInterval(whileArrowButtonDown, 100);
- };
- $container
- .append(
- $(' ')
- .attr(
- {
- 'href':'javascript:;',
- 'className':'jScrollArrowUp',
- 'tabindex':-1
- }
- )
- .css(
- {
- 'width':settings.scrollbarWidth+'px',
- 'top':settings.topCapHeight + 'px'
- }
- )
- .html('Scroll up')
- .bind('mousedown', function()
- {
- currentArrowButton = $(this);
- currentArrowDirection = -1;
- onArrowMouseDown();
- this.blur();
- return false;
- })
- .bind('click', rf),
- $(' ')
- .attr(
- {
- 'href':'javascript:;',
- 'className':'jScrollArrowDown',
- 'tabindex':-1
- }
- )
- .css(
- {
- 'width':settings.scrollbarWidth+'px',
- 'bottom':settings.bottomCapHeight + 'px'
- }
- )
- .html('Scroll down')
- .bind('mousedown', function()
- {
- currentArrowButton = $(this);
- currentArrowDirection = 1;
- onArrowMouseDown();
- this.blur();
- return false;
- })
- .bind('click', rf)
- );
- var $upArrow = $('>.jScrollArrowUp', $container);
- var $downArrow = $('>.jScrollArrowDown', $container);
- }
-
- if (settings.arrowSize) {
- trackHeight = paneHeight - settings.arrowSize - settings.arrowSize;
- trackOffset += settings.arrowSize;
- } else if ($upArrow) {
- var topArrowHeight = $upArrow.height();
- settings.arrowSize = topArrowHeight;
- trackHeight = paneHeight - topArrowHeight - $downArrow.height();
- trackOffset += topArrowHeight;
- }
- trackHeight -= settings.topCapHeight + settings.bottomCapHeight;
- $track.css({'height': trackHeight+'px', top:trackOffset+'px'})
-
- var $pane = $(this).css({'position':'absolute', 'overflow':'visible'});
-
- var currentOffset;
- var maxY;
- var mouseWheelMultiplier;
- // store this in a seperate variable so we can keep track more accurately than just updating the css property..
- var dragPosition = 0;
- var dragMiddle = percentInView*paneHeight/2;
-
- // pos function borrowed from tooltip plugin and adapted...
- var getPos = function (event, c) {
- var p = c == 'X' ? 'Left' : 'Top';
- return event['page' + c] || (event['client' + c] + (document.documentElement['scroll' + p] || document.body['scroll' + p])) || 0;
- };
-
- var ignoreNativeDrag = function() { return false; };
-
- var initDrag = function()
- {
- ceaseAnimation();
- currentOffset = $drag.offset(false);
- currentOffset.top -= dragPosition;
- maxY = trackHeight - $drag[0].offsetHeight;
- mouseWheelMultiplier = 2 * settings.wheelSpeed * maxY / contentHeight;
- };
-
- var onStartDrag = function(event)
- {
- initDrag();
- dragMiddle = getPos(event, 'Y') - dragPosition - currentOffset.top;
- $('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll);
- if ($.browser.msie) {
- $('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag);
- }
- return false;
- };
- var onStopDrag = function()
- {
- $('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll);
- dragMiddle = percentInView*paneHeight/2;
- if ($.browser.msie) {
- $('html').unbind('dragstart', ignoreNativeDrag).unbind('selectstart', ignoreNativeDrag);
- }
- };
- var positionDrag = function(destY)
- {
- $container.scrollTop(0);
- destY = destY < 0 ? 0 : (destY > maxY ? maxY : destY);
- dragPosition = destY;
- $drag.css({'top':destY+'px'});
- var p = destY / maxY;
- $this.data('jScrollPanePosition', (paneHeight-contentHeight)*-p);
- $pane.css({'top':((paneHeight-contentHeight)*p) + 'px'});
- $this.trigger('scroll');
- if (settings.showArrows) {
- $upArrow[destY == 0 ? 'addClass' : 'removeClass']('disabled');
- $downArrow[destY == maxY ? 'addClass' : 'removeClass']('disabled');
- }
- };
- var updateScroll = function(e)
- {
- positionDrag(getPos(e, 'Y') - currentOffset.top - dragMiddle);
- };
-
- var dragH = Math.max(Math.min(percentInView*(paneHeight-settings.arrowSize*2), settings.dragMaxHeight), settings.dragMinHeight);
-
- $drag.css(
- {'height':dragH+'px'}
- ).bind('mousedown', onStartDrag);
-
- var trackScrollInterval;
- var trackScrollInc;
- var trackScrollMousePos;
- var doTrackScroll = function()
- {
- if (trackScrollInc > 8 || trackScrollInc%4==0) {
- positionDrag((dragPosition - ((dragPosition - trackScrollMousePos) / 2)));
- }
- trackScrollInc ++;
- };
- var onStopTrackClick = function()
- {
- clearInterval(trackScrollInterval);
- $('html').unbind('mouseup', onStopTrackClick).unbind('mousemove', onTrackMouseMove);
- };
- var onTrackMouseMove = function(event)
- {
- trackScrollMousePos = getPos(event, 'Y') - currentOffset.top - dragMiddle;
- };
- var onTrackClick = function(event)
- {
- initDrag();
- onTrackMouseMove(event);
- trackScrollInc = 0;
- $('html').bind('mouseup', onStopTrackClick).bind('mousemove', onTrackMouseMove);
- trackScrollInterval = setInterval(doTrackScroll, 100);
- doTrackScroll();
- return false;
- };
-
- $track.bind('mousedown', onTrackClick);
-
- $container.bind(
- 'mousewheel',
- function (event, delta) {
- delta = delta || (event.wheelDelta ? event.wheelDelta / 120 : (event.detail) ?
--event.detail/3 : 0);
- initDrag();
- ceaseAnimation();
- var d = dragPosition;
- positionDrag(dragPosition - delta * mouseWheelMultiplier);
- var dragOccured = d != dragPosition;
- return !dragOccured;
- }
- );
-
- var _animateToPosition;
- var _animateToInterval;
- function animateToPosition()
- {
- var diff = (_animateToPosition - dragPosition) / settings.animateStep;
- if (diff > 1 || diff < -1) {
- positionDrag(dragPosition + diff);
- } else {
- positionDrag(_animateToPosition);
- ceaseAnimation();
- }
- }
- var ceaseAnimation = function()
- {
- if (_animateToInterval) {
- clearInterval(_animateToInterval);
- delete _animateToPosition;
- }
- };
- var scrollTo = function(pos, preventAni)
- {
- if (typeof pos == "string") {
- // Legal hash values aren't necessarily legal jQuery selectors so we need to catch any
- // errors from the lookup...
- try {
- $e = $(pos, $this);
- } catch (err) {
- return;
- }
- if (!$e.length) return;
- pos = $e.offset().top - $this.offset().top;
- }
- ceaseAnimation();
- var maxScroll = contentHeight - paneHeight;
- pos = pos > maxScroll ? maxScroll : pos;
- $this.data('jScrollPaneMaxScroll', maxScroll);
- var destDragPosition = pos/maxScroll * maxY;
- if (preventAni || !settings.animateTo) {
- positionDrag(destDragPosition);
- } else {
- $container.scrollTop(0);
- _animateToPosition = destDragPosition;
- _animateToInterval = setInterval(animateToPosition, settings.animateInterval);
- }
- };
- $this[0].scrollTo = scrollTo;
-
- $this[0].scrollBy = function(delta)
- {
- var currentPos = -parseInt($pane.css('top')) || 0;
- scrollTo(currentPos + delta);
- };
-
- initDrag();
-
- scrollTo(-currentScrollPosition, true);
-
- // Deal with it when the user tabs to a link or form element within this scrollpane
- $('*', this).bind(
- 'focus',
- function(event)
- {
- var $e = $(this);
-
- // loop through parents adding the offset top of any elements that are relatively positioned between
- // the focused element and the jScrollPaneContainer so we can get the true distance from the top
- // of the focused element to the top of the scrollpane...
- var eleTop = 0;
-
- while ($e[0] != $this[0]) {
- eleTop += $e.position().top;
- $e = $e.offsetParent();
- }
-
- var viewportTop = -parseInt($pane.css('top')) || 0;
- var maxVisibleEleTop = viewportTop + paneHeight;
- var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop;
- if (!eleInView) {
- var destPos = eleTop - settings.scrollbarMargin;
- if (eleTop > viewportTop) { // element is below viewport - scroll so it is at bottom.
- destPos += $(this).height() + 15 + settings.scrollbarMargin - paneHeight;
- }
- scrollTo(destPos);
- }
- }
- )
-
-
- if (settings.observeHash) {
- if (location.hash && location.hash.length > 1) {
- setTimeout(function(){
- scrollTo(location.hash);
- }, $.browser.safari ? 100 : 0);
- }
-
- // use event delegation to listen for all clicks on links and hijack them if they are links to
- // anchors within our content...
- $(document).bind('click', function(e){
- $target = $(e.target);
- if ($target.is('a')) {
- var h = $target.attr('href');
- if (h && h.substr(0, 1) == '#' && h.length > 1) {
- setTimeout(function(){
- scrollTo(h, !settings.animateToInternalLinks);
- }, $.browser.safari ? 100 : 0);
- }
- }
- });
- }
-
- // Deal with dragging and selecting text to make the scrollpane scroll...
- function onSelectScrollMouseDown(e)
- {
- $(document).bind('mousemove.jScrollPaneDragging', onTextSelectionScrollMouseMove);
- $(document).bind('mouseup.jScrollPaneDragging', onSelectScrollMouseUp);
-
- }
-
- var textDragDistanceAway;
- var textSelectionInterval;
-
- function onTextSelectionInterval()
- {
- direction = textDragDistanceAway < 0 ? -1 : 1;
- $this[0].scrollBy(textDragDistanceAway / 2);
- }
-
- function clearTextSelectionInterval()
- {
- if (textSelectionInterval) {
- clearInterval(textSelectionInterval);
- textSelectionInterval = undefined;
- }
- }
-
- function onTextSelectionScrollMouseMove(e)
- {
- var offset = $this.parent().offset().top;
- var maxOffset = offset + paneHeight;
- var mouseOffset = getPos(e, 'Y');
- textDragDistanceAway = mouseOffset < offset ? mouseOffset - offset : (mouseOffset > maxOffset ? mouseOffset - maxOffset : 0);
- if (textDragDistanceAway == 0) {
- clearTextSelectionInterval();
- } else {
- if (!textSelectionInterval) {
- textSelectionInterval = setInterval(onTextSelectionInterval, 100);
- }
- }
- }
-
- function onSelectScrollMouseUp(e)
- {
- $(document)
- .unbind('mousemove.jScrollPaneDragging')
- .unbind('mouseup.jScrollPaneDragging');
- clearTextSelectionInterval();
- }
-
- $container.bind('mousedown.jScrollPane', onSelectScrollMouseDown);
-
-
- $.jScrollPane.active.push($this[0]);
-
- } else {
- $this.css(
- {
- 'height':paneHeight+'px',
- 'width':paneWidth-this.originalSidePaddingTotal+'px',
- 'padding':this.originalPadding
- }
- );
- $this[0].scrollTo = $this[0].scrollBy = function() {};
- // clean up listeners
- $this.parent().unbind('mousewheel').unbind('mousedown.jScrollPane').unbind('keydown.jscrollpane').unbind('keyup.jscrollpane');
- }
-
- }
- )
-};
-
-$.fn.jScrollPaneRemove = function()
-{
- $(this).each(function()
- {
- $this = $(this);
- var $c = $this.parent();
- if ($c.is('.jScrollPaneContainer')) {
- $this.css(
- {
- 'top':'',
- 'height':'',
- 'width':'',
- 'padding':'',
- 'overflow':'',
- 'position':''
- }
- );
- $this.attr('style', $this.data('originalStyleTag'));
- $c.after($this).remove();
- }
- });
-}
-
-$.fn.jScrollPane.defaults = {
- scrollbarWidth : 10,
- scrollbarMargin : 5,
- wheelSpeed : 18,
- showArrows : false,
- arrowSize : 0,
- animateTo : false,
- dragMinHeight : 1,
- dragMaxHeight : 99999,
- animateInterval : 100,
- animateStep: 3,
- maintainPosition: true,
- scrollbarOnLeft: false,
- reinitialiseOnImageLoad: false,
- tabIndex : 0,
- enableKeyboardNavigation: true,
- animateToInternalLinks: false,
- topCapHeight: 0,
- bottomCapHeight: 0,
- observeHash: true
-};
-
-// clean up the scrollTo expandos
-$(window)
- .bind('unload', function() {
- var els = $.jScrollPane.active;
- for (var i=0; i
+ * Copyright (c) 2012 - Tomi Pieviläinen
+ * https://github.com/jakubroztocil/rrule/blob/master/LICENCE
+ *
+ */
+(function(root){
+
+var serverSide = typeof module !== 'undefined' && module.exports;
+
+
+var getnlp = function() {
+ if (!getnlp._nlp) {
+ if (serverSide) {
+ // Lazy, runtime import to avoid circular refs.
+ getnlp._nlp = require('./nlp')
+ } else if (!(getnlp._nlp = root._RRuleNLP)) {
+ throw new Error(
+ 'You need to include rrule/nlp.js for fromText/toText to work.'
+ )
+ }
+ }
+ return getnlp._nlp;
+};
+
+
+//=============================================================================
+// Date utilities
+//=============================================================================
+
+/**
+ * General date-related utilities.
+ * Also handles several incompatibilities between JavaScript and Python
+ *
+ */
+var dateutil = {
+
+ MONTH_DAYS: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+
+ /**
+ * Number of milliseconds of one day
+ */
+ ONE_DAY: 1000 * 60 * 60 * 24,
+
+ /**
+ * @see:
+ */
+ MAXYEAR: 9999,
+
+ /**
+ * Python uses 1-Jan-1 as the base for calculating ordinals but we don't
+ * want to confuse the JS engine with milliseconds > Number.MAX_NUMBER,
+ * therefore we use 1-Jan-1970 instead
+ */
+ ORDINAL_BASE: new Date(1970, 0, 1),
+
+ /**
+ * Python: MO-SU: 0 - 6
+ * JS: SU-SAT 0 - 6
+ */
+ PY_WEEKDAYS: [6, 0, 1, 2, 3, 4, 5],
+
+ /**
+ * py_date.timetuple()[7]
+ */
+ getYearDay: function(date) {
+ var dateNoTime = new Date(
+ date.getFullYear(), date.getMonth(), date.getDate());
+ return Math.ceil(
+ (dateNoTime - new Date(date.getFullYear(), 0, 1))
+ / dateutil.ONE_DAY) + 1;
+ },
+
+ isLeapYear: function(year) {
+ if (year instanceof Date) {
+ year = year.getFullYear();
+ }
+ return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
+ },
+
+ /**
+ * @return {Number} the date's timezone offset in ms
+ */
+ tzOffset: function(date) {
+ return date.getTimezoneOffset() * 60 * 1000
+ },
+
+ /**
+ * @see:
+ */
+ daysBetween: function(date1, date2) {
+ // The number of milliseconds in one day
+ // Convert both dates to milliseconds
+ var date1_ms = date1.getTime() - dateutil.tzOffset(date1);
+ var date2_ms = date2.getTime() - dateutil.tzOffset(date2);
+ // Calculate the difference in milliseconds
+ var difference_ms = Math.abs(date1_ms - date2_ms);
+ // Convert back to days and return
+ return Math.round(difference_ms / dateutil.ONE_DAY);
+ },
+
+ /**
+ * @see:
+ */
+ toOrdinal: function(date) {
+ return dateutil.daysBetween(date, dateutil.ORDINAL_BASE);
+ },
+
+ /**
+ * @see -
+ */
+ fromOrdinal: function(ordinal) {
+ var millisecsFromBase = ordinal * dateutil.ONE_DAY;
+ return new Date(dateutil.ORDINAL_BASE.getTime()
+ - dateutil.tzOffset(dateutil.ORDINAL_BASE)
+ + millisecsFromBase
+ + dateutil.tzOffset(new Date(millisecsFromBase)));
+ },
+
+ /**
+ * @see:
+ */
+ monthRange: function(year, month) {
+ var date = new Date(year, month, 1);
+ return [dateutil.getWeekday(date), dateutil.getMonthDays(date)];
+ },
+
+ getMonthDays: function(date) {
+ var month = date.getMonth();
+ return month == 1 && dateutil.isLeapYear(date)
+ ? 29
+ : dateutil.MONTH_DAYS[month];
+ },
+
+ /**
+ * @return {Number} python-like weekday
+ */
+ getWeekday: function(date) {
+ return dateutil.PY_WEEKDAYS[date.getDay()];
+ },
+
+ /**
+ * @see:
+ */
+ combine: function(date, time) {
+ time = time || date;
+ return new Date(
+ date.getFullYear(), date.getMonth(), date.getDate(),
+ time.getHours(), time.getMinutes(), time.getSeconds()
+ );
+ },
+
+ clone: function(date) {
+ var dolly = new Date(date.getTime());
+ dolly.setMilliseconds(0);
+ return dolly;
+ },
+
+ cloneDates: function(dates) {
+ var clones = [];
+ for (var i = 0; i < dates.length; i++) {
+ clones.push(dateutil.clone(dates[i]));
+ }
+ return clones;
+ },
+
+ /**
+ * Sorts an array of Date or dateutil.Time objects
+ */
+ sort: function(dates) {
+ dates.sort(function(a, b){
+ return a.getTime() - b.getTime();
+ });
+ },
+
+ timeToUntilString: function(time) {
+ var date = new Date(time);
+ var comp, comps = [
+ date.getUTCFullYear(),
+ date.getUTCMonth() + 1,
+ date.getUTCDate(),
+ 'T',
+ date.getUTCHours(),
+ date.getUTCMinutes(),
+ date.getUTCSeconds(),
+ 'Z'
+ ];
+ for (var i = 0; i < comps.length; i++) {
+ comp = comps[i];
+ if (!/[TZ]/.test(comp) && comp < 10) {
+ comps[i] = '0' + String(comp);
+ }
+ }
+ return comps.join('');
+ },
+
+ untilStringToDate: function(until) {
+ var re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z)?$/;
+ var bits = re.exec(until);
+ if (!bits) {
+ throw new Error('Invalid UNTIL value: ' + until)
+ }
+ return new Date(
+ Date.UTC(bits[1],
+ bits[2] - 1,
+ bits[3],
+ bits[5] || 0,
+ bits[6] || 0,
+ bits[7] || 0
+ ));
+ }
+
+};
+
+dateutil.Time = function(hour, minute, second) {
+ this.hour = hour;
+ this.minute = minute;
+ this.second = second;
+};
+
+dateutil.Time.prototype = {
+ getHours: function() {
+ return this.hour;
+ },
+ getMinutes: function() {
+ return this.minute;
+ },
+ getSeconds: function() {
+ return this.second;
+ },
+ getTime: function() {
+ return ((this.hour * 60 * 60)
+ + (this.minute * 60)
+ + this.second)
+ * 1000;
+ }
+};
+
+
+//=============================================================================
+// Helper functions
+//=============================================================================
+
+
+/**
+ * Simplified version of python's range()
+ */
+var range = function(start, end) {
+ if (arguments.length === 1) {
+ end = start;
+ start = 0;
+ }
+ var rang = [];
+ for (var i = start; i < end; i++) {
+ rang.push(i);
+ }
+ return rang;
+};
+var repeat = function(value, times) {
+ var i = 0, array = [];
+ if (value instanceof Array) {
+ for (; i < times; i++) {
+ array[i] = [].concat(value);
+ }
+ } else {
+ for (; i < times; i++) {
+ array[i] = value;
+ }
+ }
+ return array;
+};
+
+
+/**
+ * closure/goog/math/math.js:modulo
+ * Copyright 2006 The Closure Library Authors.
+ * The % operator in JavaScript returns the remainder of a / b, but differs from
+ * some other languages in that the result will have the same sign as the
+ * dividend. For example, -1 % 8 == -1, whereas in some other languages
+ * (such as Python) the result would be 7. This function emulates the more
+ * correct modulo behavior, which is useful for certain applications such as
+ * calculating an offset index in a circular list.
+ *
+ * @param {number} a The dividend.
+ * @param {number} b The divisor.
+ * @return {number} a % b where the result is between 0 and b (either 0 <= x < b
+ * or b < x <= 0, depending on the sign of b).
+ */
+var pymod = function(a, b) {
+ var r = a % b;
+ // If r and b differ in sign, add b to wrap the result to the correct sign.
+ return (r * b < 0) ? r + b : r;
+};
+
+
+/**
+ * @see:
+ */
+var divmod = function(a, b) {
+ return {div: Math.floor(a / b), mod: pymod(a, b)};
+};
+
+
+/**
+ * Python-like boolean
+ * @return {Boolean} value of an object/primitive, taking into account
+ * the fact that in Python an empty list's/tuple's
+ * boolean value is False, whereas in JS it's true
+ */
+var plb = function(obj) {
+ return (obj instanceof Array && obj.length == 0)
+ ? false
+ : Boolean(obj);
+};
+
+
+/**
+ * Return true if a value is in an array
+ */
+var contains = function(arr, val) {
+ return arr.indexOf(val) != -1;
+};
+
+
+//=============================================================================
+// Date masks
+//=============================================================================
+
+// Every mask is 7 days longer to handle cross-year weekly periods.
+
+var M365MASK = [].concat(
+ repeat(1, 31), repeat(2, 28), repeat(3, 31),
+ repeat(4, 30), repeat(5, 31), repeat(6, 30),
+ repeat(7, 31), repeat(8, 31), repeat(9, 30),
+ repeat(10, 31), repeat(11, 30), repeat(12, 31),
+ repeat(1, 7)
+);
+var M366MASK = [].concat(
+ repeat(1, 31), repeat(2, 29), repeat(3, 31),
+ repeat(4, 30), repeat(5, 31), repeat(6, 30),
+ repeat(7, 31), repeat(8, 31), repeat(9, 30),
+ repeat(10, 31), repeat(11, 30), repeat(12, 31),
+ repeat(1, 7)
+);
+
+var
+ M28 = range(1, 29),
+ M29 = range(1, 30),
+ M30 = range(1, 31),
+ M31 = range(1, 32);
+var MDAY366MASK = [].concat(
+ M31, M29, M31,
+ M30, M31, M30,
+ M31, M31, M30,
+ M31, M30, M31,
+ M31.slice(0, 7)
+);
+var MDAY365MASK = [].concat(
+ M31, M28, M31,
+ M30, M31, M30,
+ M31, M31, M30,
+ M31, M30, M31,
+ M31.slice(0, 7)
+);
+
+M28 = range(-28, 0);
+M29 = range(-29, 0);
+M30 = range(-30, 0);
+M31 = range(-31, 0);
+var NMDAY366MASK = [].concat(
+ M31, M29, M31,
+ M30, M31, M30,
+ M31, M31, M30,
+ M31, M30, M31,
+ M31.slice(0, 7)
+);
+var NMDAY365MASK = [].concat(
+ M31, M28, M31,
+ M30, M31, M30,
+ M31, M31, M30,
+ M31, M30, M31,
+ M31.slice(0, 7)
+);
+
+var M366RANGE = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366];
+var M365RANGE = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
+
+var WDAYMASK = (function() {
+ for (var wdaymask = [], i = 0; i < 55; i++) {
+ wdaymask = wdaymask.concat(range(7));
+ }
+ return wdaymask;
+}());
+
+
+//=============================================================================
+// Weekday
+//=============================================================================
+
+var Weekday = function(weekday, n) {
+ if (n === 0) {
+ throw new Error('Can\'t create weekday with n == 0');
+ }
+ this.weekday = weekday;
+ this.n = n;
+};
+
+Weekday.prototype = {
+
+ // __call__ - Cannot call the object directly, do it through
+ // e.g. RRule.TH.nth(-1) instead,
+ nth: function(n) {
+ return this.n == n ? this : new Weekday(this.weekday, n);
+ },
+
+ // __eq__
+ equals: function(other) {
+ return this.weekday == other.weekday && this.n == other.n;
+ },
+
+ // __repr__
+ toString: function() {
+ var s = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'][this.weekday];
+ if (this.n) {
+ s = (this.n > 0 ? '+' : '') + String(this.n) + s;
+ }
+ return s;
+ },
+
+ getJsWeekday: function() {
+ return this.weekday == 6 ? 0 : this.weekday + 1;
+ }
+
+};
+
+
+//=============================================================================
+// RRule
+//=============================================================================
+
+/**
+ *
+ * @param {Object?} options - see
+ * The only required option is `freq`, one of RRule.YEARLY, RRule.MONTHLY, ...
+ * @constructor
+ */
+var RRule = function(options, noCache) {
+
+ // RFC string
+ this._string = null;
+
+ options = options || {};
+
+ this._cache = noCache ? null : {
+ all: false,
+ before: [],
+ after: [],
+ between: []
+ };
+
+ // used by toString()
+ this.origOptions = {};
+
+ var invalid = [],
+ keys = Object.keys(options),
+ defaultKeys = Object.keys(RRule.DEFAULT_OPTIONS);
+
+ // Shallow copy for origOptions and check for invalid
+ keys.forEach(function(key) {
+ this.origOptions[key] = options[key];
+ if (!contains(defaultKeys, key)) invalid.push(key);
+ }, this);
+
+ if (invalid.length) {
+ throw new Error('Invalid options: ' + invalid.join(', '))
+ }
+
+ if (!RRule.FREQUENCIES[options.freq] && options.byeaster === null) {
+ throw new Error('Invalid frequency: ' + String(options.freq))
+ }
+
+ // Merge in default options
+ defaultKeys.forEach(function(key) {
+ if (!contains(keys, key)) options[key] = RRule.DEFAULT_OPTIONS[key];
+ });
+
+ var opts = this.options = options;
+
+ if (opts.byeaster !== null) {
+ opts.freq = RRule.YEARLY;
+ }
+
+ if (!opts.dtstart) {
+ opts.dtstart = new Date();
+ opts.dtstart.setMilliseconds(0);
+ }
+
+ if (opts.wkst === null) {
+ opts.wkst = RRule.MO.weekday;
+ } else if (typeof opts.wkst == 'number') {
+ // cool, just keep it like that
+ } else {
+ opts.wkst = opts.wkst.weekday;
+ }
+
+ if (opts.bysetpos !== null) {
+ if (typeof opts.bysetpos == 'number') {
+ opts.bysetpos = [opts.bysetpos];
+ }
+ for (var i = 0; i < opts.bysetpos.length; i++) {
+ var v = opts.bysetpos[i];
+ if (v == 0 || !(-366 <= v && v <= 366)) {
+ throw new Error(
+ 'bysetpos must be between 1 and 366,' +
+ ' or between -366 and -1'
+ );
+ }
+ }
+ }
+
+ if (!(plb(opts.byweekno) || plb(opts.byyearday)
+ || plb(opts.bymonthday) || opts.byweekday !== null
+ || opts.byeaster !== null))
+ {
+ switch (opts.freq) {
+ case RRule.YEARLY:
+ if (!opts.bymonth) {
+ opts.bymonth = opts.dtstart.getMonth() + 1;
+ }
+ opts.bymonthday = opts.dtstart.getDate();
+ break;
+ case RRule.MONTHLY:
+ opts.bymonthday = opts.dtstart.getDate();
+ break;
+ case RRule.WEEKLY:
+ opts.byweekday = dateutil.getWeekday(
+ opts.dtstart);
+ break;
+ }
+ }
+
+ // bymonth
+ if (opts.bymonth !== null
+ && !(opts.bymonth instanceof Array)) {
+ opts.bymonth = [opts.bymonth];
+ }
+
+ // byyearday
+ if (opts.byyearday !== null
+ && !(opts.byyearday instanceof Array)) {
+ opts.byyearday = [opts.byyearday];
+ }
+
+ // bymonthday
+ if (opts.bymonthday === null) {
+ opts.bymonthday = [];
+ opts.bynmonthday = [];
+ } else if (opts.bymonthday instanceof Array) {
+ var bymonthday = [], bynmonthday = [];
+
+ for (i = 0; i < opts.bymonthday.length; i++) {
+ var v = opts.bymonthday[i];
+ if (v > 0) {
+ bymonthday.push(v);
+ } else if (v < 0) {
+ bynmonthday.push(v);
+ }
+ }
+ opts.bymonthday = bymonthday;
+ opts.bynmonthday = bynmonthday;
+ } else {
+ if (opts.bymonthday < 0) {
+ opts.bynmonthday = [opts.bymonthday];
+ opts.bymonthday = [];
+ } else {
+ opts.bynmonthday = [];
+ opts.bymonthday = [opts.bymonthday];
+ }
+ }
+
+ // byweekno
+ if (opts.byweekno !== null
+ && !(opts.byweekno instanceof Array)) {
+ opts.byweekno = [opts.byweekno];
+ }
+
+ // byweekday / bynweekday
+ if (opts.byweekday === null) {
+ opts.bynweekday = null;
+ } else if (typeof opts.byweekday == 'number') {
+ opts.byweekday = [opts.byweekday];
+ opts.bynweekday = null;
+
+ } else if (opts.byweekday instanceof Weekday) {
+
+ if (!opts.byweekday.n || opts.freq > RRule.MONTHLY) {
+ opts.byweekday = [opts.byweekday.weekday];
+ opts.bynweekday = null;
+ } else {
+ opts.bynweekday = [
+ [opts.byweekday.weekday,
+ opts.byweekday.n]
+ ];
+ opts.byweekday = null;
+ }
+
+ } else {
+ var byweekday = [], bynweekday = [];
+
+ for (i = 0; i < opts.byweekday.length; i++) {
+ var wday = opts.byweekday[i];
+
+ if (typeof wday == 'number') {
+ byweekday.push(wday);
+ } else if (!wday.n || opts.freq > RRule.MONTHLY) {
+ byweekday.push(wday.weekday);
+ } else {
+ bynweekday.push([wday.weekday, wday.n]);
+ }
+ }
+ opts.byweekday = plb(byweekday) ? byweekday : null;
+ opts.bynweekday = plb(bynweekday) ? bynweekday : null;
+ }
+
+ // byhour
+ if (opts.byhour === null) {
+ opts.byhour = (opts.freq < RRule.HOURLY)
+ ? [opts.dtstart.getHours()]
+ : null;
+ } else if (typeof opts.byhour == 'number') {
+ opts.byhour = [opts.byhour];
+ }
+
+ // byminute
+ if (opts.byminute === null) {
+ opts.byminute = (opts.freq < RRule.MINUTELY)
+ ? [opts.dtstart.getMinutes()]
+ : null;
+ } else if (typeof opts.byminute == 'number') {
+ opts.byminute = [opts.byminute];
+ }
+
+ // bysecond
+ if (opts.bysecond === null) {
+ opts.bysecond = (opts.freq < RRule.SECONDLY)
+ ? [opts.dtstart.getSeconds()]
+ : null;
+ } else if (typeof opts.bysecond == 'number') {
+ opts.bysecond = [opts.bysecond];
+ }
+
+ if (opts.freq >= RRule.HOURLY) {
+ this.timeset = null;
+ } else {
+ this.timeset = [];
+ for (i = 0; i < opts.byhour.length; i++) {
+ var hour = opts.byhour[i];
+ for (var j = 0; j < opts.byminute.length; j++) {
+ var minute = opts.byminute[j];
+ for (var k = 0; k < opts.bysecond.length; k++) {
+ var second = opts.bysecond[k];
+ // python:
+ // datetime.time(hour, minute, second,
+ // tzinfo=self._tzinfo))
+ this.timeset.push(new dateutil.Time(hour, minute, second));
+ }
+ }
+ }
+ dateutil.sort(this.timeset);
+ }
+
+};
+//}}}
+
+// RRule class 'constants'
+
+RRule.FREQUENCIES = [
+ 'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY',
+ 'HOURLY', 'MINUTELY', 'SECONDLY'
+];
+
+RRule.YEARLY = 0;
+RRule.MONTHLY = 1;
+RRule.WEEKLY = 2;
+RRule.DAILY = 3;
+RRule.HOURLY = 4;
+RRule.MINUTELY = 5;
+RRule.SECONDLY = 6;
+
+RRule.MO = new Weekday(0);
+RRule.TU = new Weekday(1);
+RRule.WE = new Weekday(2);
+RRule.TH = new Weekday(3);
+RRule.FR = new Weekday(4);
+RRule.SA = new Weekday(5);
+RRule.SU = new Weekday(6);
+
+RRule.DEFAULT_OPTIONS = {
+ freq: null,
+ dtstart: null,
+ interval: 1,
+ wkst: RRule.MO,
+ count: null,
+ until: null,
+ bysetpos: null,
+ bymonth: null,
+ bymonthday: null,
+ byyearday: null,
+ byweekno: null,
+ byweekday: null,
+ byhour: null,
+ byminute: null,
+ bysecond: null,
+ byeaster: null
+};
+
+
+
+RRule.parseText = function(text, language) {
+ return getnlp().parseText(text, language)
+};
+
+RRule.fromText = function(text, language) {
+ return getnlp().fromText(text, language)
+};
+
+RRule.optionsToString = function(options) {
+ var key, keys, defaultKeys, value, strValues, pairs = [];
+
+ keys = Object.keys(options);
+ defaultKeys = Object.keys(RRule.DEFAULT_OPTIONS);
+
+ for (var i = 0; i < keys.length; i++) {
+
+ if (!contains(defaultKeys, keys[i])) continue;
+
+ key = keys[i].toUpperCase();
+ value = options[keys[i]];
+ strValues = [];
+
+ if (value === null || value instanceof Array && !value.length) {
+ continue;
+ }
+
+ switch (key) {
+ case 'FREQ':
+ value = RRule.FREQUENCIES[options.freq];
+ break;
+ case 'WKST':
+ value = value.toString();
+ break;
+ case 'BYWEEKDAY':
+ /*
+ NOTE: BYWEEKDAY is a special case.
+ RRule() deconstructs the rule.options.byweekday array
+ into an array of Weekday arguments.
+ On the other hand, rule.origOptions is an array of Weekdays.
+ We need to handle both cases here.
+ It might be worth change RRule to keep the Weekdays.
+
+ Also, BYWEEKDAY (used by RRule) vs. BYDAY (RFC)
+
+ */
+ key = 'BYDAY';
+ if (!(value instanceof Array)) {
+ value = [value];
+ }
+ for (var wday, j = 0; j < value.length; j++) {
+ wday = value[j];
+ if (wday instanceof Weekday) {
+ // good
+ } else if (wday instanceof Array) {
+ wday = new Weekday(wday[0], wday[1]);
+ } else {
+ wday = new Weekday(wday);
+ }
+ strValues[j] = wday.toString();
+ }
+ value = strValues;
+ break;
+ case'DTSTART':
+ case'UNTIL':
+ value = dateutil.timeToUntilString(value);
+ break;
+ default:
+ if (value instanceof Array) {
+ for (var j = 0; j < value.length; j++) {
+ strValues[j] = String(value[j]);
+ }
+ value = strValues;
+ } else {
+ value = String(value);
+ }
+
+ }
+ pairs.push([key, value]);
+ }
+
+ var strings = [];
+ for (var i = 0; i < pairs.length; i++) {
+ var attr = pairs[i];
+ strings.push(attr[0] + '=' + attr[1].toString());
+ }
+ return strings.join(';');
+
+};
+
+RRule.prototype = {
+
+ /**
+ * @param {Function} iterator - optional function that will be called
+ * on each date that is added. It can return false
+ * to stop the iteration.
+ * @return Array containing all recurrences.
+ */
+ all: function(iterator) {
+ if (iterator) {
+ return this._iter(new CallbackIterResult('all', {}, iterator));
+ } else {
+ var result = this._cacheGet('all');
+ if (result === false) {
+ result = this._iter(new IterResult('all', {}));
+ this._cacheAdd('all', result);
+ }
+ return result;
+ }
+ },
+
+ /**
+ * Returns all the occurrences of the rrule between after and before.
+ * The inc keyword defines what happens if after and/or before are
+ * themselves occurrences. With inc == True, they will be included in the
+ * list, if they are found in the recurrence set.
+ * @return Array
+ */
+ between: function(after, before, inc, iterator) {
+ var args = {
+ before: before,
+ after: after,
+ inc: inc
+ }
+
+ if (iterator) {
+ return this._iter(
+ new CallbackIterResult('between', args, iterator));
+ } else {
+ var result = this._cacheGet('between', args);
+ if (result === false) {
+ result = this._iter(new IterResult('between', args));
+ this._cacheAdd('between', result, args);
+ }
+ return result;
+ }
+ },
+
+ /**
+ * Returns the last recurrence before the given datetime instance.
+ * The inc keyword defines what happens if dt is an occurrence.
+ * With inc == True, if dt itself is an occurrence, it will be returned.
+ * @return Date or null
+ */
+ before: function(dt, inc) {
+ var args = {
+ dt: dt,
+ inc: inc
+ },
+ result = this._cacheGet('before', args);
+ if (result === false) {
+ result = this._iter(new IterResult('before', args));
+ this._cacheAdd('before', result, args);
+ }
+ return result;
+ },
+
+ /**
+ * Returns the first recurrence after the given datetime instance.
+ * The inc keyword defines what happens if dt is an occurrence.
+ * With inc == True, if dt itself is an occurrence, it will be returned.
+ * @return Date or null
+ */
+ after: function(dt, inc) {
+ var args = {
+ dt: dt,
+ inc: inc
+ },
+ result = this._cacheGet('after', args);
+ if (result === false) {
+ result = this._iter(new IterResult('after', args));
+ this._cacheAdd('after', result, args);
+ }
+ return result;
+ },
+
+ /**
+ * Returns the number of recurrences in this set. It will have go trough
+ * the whole recurrence, if this hasn't been done before.
+ */
+ count: function() {
+ return this.all().length;
+ },
+
+ /**
+ * Converts the rrule into its string representation
+ * @see
+ * @return String
+ */
+ toString: function() {
+ return RRule.optionsToString(this.origOptions);
+ },
+
+ /**
+ * Will convert all rules described in nlp:ToText
+ * to text.
+ */
+ toText: function(gettext, language) {
+ return getnlp().toText(this, gettext, language);
+ },
+
+ isFullyConvertibleToText: function() {
+ return getnlp().isFullyConvertible(this)
+ },
+
+ /**
+ * @param {String} what - all/before/after/between
+ * @param {Array,Date} value - an array of dates, one date, or null
+ * @param {Object?} args - _iter arguments
+ */
+ _cacheAdd: function(what, value, args) {
+
+ if (!this._cache) return;
+
+ if (value) {
+ value = (value instanceof Date)
+ ? dateutil.clone(value)
+ : dateutil.cloneDates(value);
+ }
+
+ if (what == 'all') {
+ this._cache.all = value;
+ } else {
+ args._value = value;
+ this._cache[what].push(args);
+ }
+
+ },
+
+ /**
+ * @return false - not in the cache
+ * null - cached, but zero occurrences (before/after)
+ * Date - cached (before/after)
+ * [] - cached, but zero occurrences (all/between)
+ * [Date1, DateN] - cached (all/between)
+ */
+ _cacheGet: function(what, args) {
+
+ if (!this._cache) {
+ return false;
+ }
+
+ var cached = false;
+
+ if (what == 'all') {
+ cached = this._cache.all;
+ } else {
+ // Let's see whether we've already called the
+ // 'what' method with the same 'args'
+ loopItems:
+ for (var item, i = 0; i < this._cache[what].length; i++) {
+ item = this._cache[what][i];
+ for (var k in args) {
+ if (args.hasOwnProperty(k)
+ && String(args[k]) != String(item[k])) {
+ continue loopItems;
+ }
+ }
+ cached = item._value;
+ break;
+ }
+ }
+
+ if (!cached && this._cache.all) {
+ // Not in the cache, but we already know all the occurrences,
+ // so we can find the correct dates from the cached ones.
+ var iterResult = new IterResult(what, args);
+ for (var i = 0; i < this._cache.all.length; i++) {
+ if (!iterResult.accept(this._cache.all[i])) {
+ break;
+ }
+ }
+ cached = iterResult.getValue();
+ this._cacheAdd(what, cached, args);
+ }
+
+ return cached instanceof Array
+ ? dateutil.cloneDates(cached)
+ : (cached instanceof Date
+ ? dateutil.clone(cached)
+ : cached);
+ },
+
+ /**
+ * @return a RRule instance with the same freq and options
+ * as this one (cache is not cloned)
+ */
+ clone: function() {
+ return new RRule(this.origOptions);
+ },
+
+ _iter: function(iterResult) {
+
+ /* Since JavaScript doesn't have the python's yield operator (<1.7),
+ we use the IterResult object that tells us when to stop iterating.
+
+ */
+
+ var dtstart = this.options.dtstart;
+
+ var
+ year = dtstart.getFullYear(),
+ month = dtstart.getMonth() + 1,
+ day = dtstart.getDate(),
+ hour = dtstart.getHours(),
+ minute = dtstart.getMinutes(),
+ second = dtstart.getSeconds(),
+ weekday = dateutil.getWeekday(dtstart),
+ yearday = dateutil.getYearDay(dtstart);
+
+ // Some local variables to speed things up a bit
+ var
+ freq = this.options.freq,
+ interval = this.options.interval,
+ wkst = this.options.wkst,
+ until = this.options.until,
+ bymonth = this.options.bymonth,
+ byweekno = this.options.byweekno,
+ byyearday = this.options.byyearday,
+ byweekday = this.options.byweekday,
+ byeaster = this.options.byeaster,
+ bymonthday = this.options.bymonthday,
+ bynmonthday = this.options.bynmonthday,
+ bysetpos = this.options.bysetpos,
+ byhour = this.options.byhour,
+ byminute = this.options.byminute,
+ bysecond = this.options.bysecond;
+
+ var ii = new Iterinfo(this);
+ ii.rebuild(year, month);
+
+ var getdayset = {};
+ getdayset[RRule.YEARLY] = ii.ydayset;
+ getdayset[RRule.MONTHLY] = ii.mdayset;
+ getdayset[RRule.WEEKLY] = ii.wdayset;
+ getdayset[RRule.DAILY] = ii.ddayset;
+ getdayset[RRule.HOURLY] = ii.ddayset;
+ getdayset[RRule.MINUTELY] = ii.ddayset;
+ getdayset[RRule.SECONDLY] = ii.ddayset;
+
+ getdayset = getdayset[freq];
+
+ var timeset;
+ if (freq < RRule.HOURLY) {
+ timeset = this.timeset;
+ } else {
+ var gettimeset = {};
+ gettimeset[RRule.HOURLY] = ii.htimeset;
+ gettimeset[RRule.MINUTELY] = ii.mtimeset;
+ gettimeset[RRule.SECONDLY] = ii.stimeset;
+ gettimeset = gettimeset[freq];
+ if ((freq >= RRule.HOURLY && plb(byhour) && !contains(byhour, hour)) ||
+ (freq >= RRule.MINUTELY && plb(byminute) && !contains(byminute, minute)) ||
+ (freq >= RRule.SECONDLY && plb(bysecond) && !contains(bysecond, minute)))
+ {
+ timeset = [];
+ } else {
+ timeset = gettimeset.call(ii, hour, minute, second);
+ }
+ }
+
+ var filtered, total = 0, count = this.options.count;
+
+ var iterNo = 0;
+
+ var i, j, k, dm, div, mod, tmp, pos, dayset, start, end, fixday;
+
+ while (true) {
+
+ // Get dayset with the right frequency
+ tmp = getdayset.call(ii, year, month, day);
+ dayset = tmp[0]; start = tmp[1]; end = tmp[2];
+
+ // Do the "hard" work ;-)
+ filtered = false;
+ for (j = start; j < end; j++) {
+
+ i = dayset[j];
+
+ if ((plb(bymonth) && !contains(bymonth, ii.mmask[i])) ||
+ (plb(byweekno) && !ii.wnomask[i]) ||
+ (plb(byweekday) && !contains(byweekday, ii.wdaymask[i])) ||
+ (plb(ii.nwdaymask) && !ii.nwdaymask[i]) ||
+ (byeaster !== null && !contains(ii.eastermask, i)) ||
+ (
+ (plb(bymonthday) || plb(bynmonthday)) &&
+ !contains(bymonthday, ii.mdaymask[i]) &&
+ !contains(bynmonthday, ii.nmdaymask[i])
+ )
+ ||
+ (
+ plb(byyearday)
+ &&
+ (
+ (
+ i < ii.yearlen &&
+ !contains(byyearday, i + 1) &&
+ !contains(byyearday, -ii.yearlen + i)
+ )
+ ||
+ (
+ i >= ii.yearlen &&
+ !contains(byyearday, i + 1 - ii.yearlen) &&
+ !contains(byyearday, -ii.nextyearlen + i - ii.yearlen)
+ )
+ )
+ )
+ )
+ {
+ dayset[i] = null;
+ filtered = true;
+ }
+ }
+
+ // Output results
+ if (plb(bysetpos) && plb(timeset)) {
+
+ var daypos, timepos, poslist = [];
+
+ for (i, j = 0; j < bysetpos.length; j++) {
+ var pos = bysetpos[j];
+ if (pos < 0) {
+ daypos = Math.floor(pos / timeset.length);
+ timepos = pymod(pos, timeset.length);
+ } else {
+ daypos = Math.floor((pos - 1) / timeset.length);
+ timepos = pymod((pos - 1), timeset.length);
+ }
+
+ try {
+ tmp = [];
+ for (k = start; k < end; k++) {
+ var val = dayset[k];
+ if (val === null) {
+ continue;
+ }
+ tmp.push(val);
+ }
+ if (daypos < 0) {
+ // we're trying to emulate python's aList[-n]
+ i = tmp.slice(daypos)[0];
+ } else {
+ i = tmp[daypos];
+ }
+
+ var time = timeset[timepos];
+
+ var date = dateutil.fromOrdinal(ii.yearordinal + i);
+ var res = dateutil.combine(date, time);
+ // XXX: can this ever be in the array?
+ // - compare the actual date instead?
+ if (!contains(poslist, res)) {
+ poslist.push(res);
+ }
+ } catch (e) {}
+ }
+
+ dateutil.sort(poslist);
+
+ for (j = 0; j < poslist.length; j++) {
+ var res = poslist[j];
+ if (until && res > until) {
+ this._len = total;
+ return iterResult.getValue();
+ } else if (res >= dtstart) {
+ ++total;
+ if (!iterResult.accept(res)) {
+ return iterResult.getValue();
+ }
+ if (count) {
+ --count;
+ if (!count) {
+ this._len = total;
+ return iterResult.getValue();
+ }
+ }
+ }
+ }
+
+ } else {
+ for (j = start; j < end; j++) {
+ i = dayset[j];
+ if (i !== null) {
+ var date = dateutil.fromOrdinal(ii.yearordinal + i);
+ for (k = 0; k < timeset.length; k++) {
+ var time = timeset[k];
+ var res = dateutil.combine(date, time);
+ if (until && res > until) {
+ this._len = total;
+ return iterResult.getValue();
+ } else if (res >= dtstart) {
+ ++total;
+ if (!iterResult.accept(res)) {
+ return iterResult.getValue();
+ }
+ if (count) {
+ --count;
+ if (!count) {
+ this._len = total;
+ return iterResult.getValue();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Handle frequency and interval
+ fixday = false;
+ if (freq == RRule.YEARLY) {
+ year += interval;
+ if (year > dateutil.MAXYEAR) {
+ this._len = total;
+ return iterResult.getValue();
+ }
+ ii.rebuild(year, month);
+ } else if (freq == RRule.MONTHLY) {
+ month += interval;
+ if (month > 12) {
+ div = Math.floor(month / 12);
+ mod = pymod(month, 12);
+ month = mod;
+ year += div;
+ if (month == 0) {
+ month = 12;
+ --year;
+ }
+ if (year > dateutil.MAXYEAR) {
+ this._len = total;
+ return iterResult.getValue();
+ }
+ }
+ ii.rebuild(year, month);
+ } else if (freq == RRule.WEEKLY) {
+ if (wkst > weekday) {
+ day += -(weekday + 1 + (6 - wkst)) + interval * 7;
+ } else {
+ day += -(weekday - wkst) + interval * 7;
+ }
+ weekday = wkst;
+ fixday = true;
+ } else if (freq == RRule.DAILY) {
+ day += interval;
+ fixday = true;
+ } else if (freq == RRule.HOURLY) {
+ if (filtered) {
+ // Jump to one iteration before next day
+ hour += Math.floor((23 - hour) / interval) * interval;
+ }
+ while (true) {
+ hour += interval;
+ dm = divmod(hour, 24);
+ div = dm.div;
+ mod = dm.mod;
+ if (div) {
+ hour = mod;
+ day += div;
+ fixday = true;
+ }
+ if (!plb(byhour) || contains(byhour, hour)) {
+ break;
+ }
+ }
+ timeset = gettimeset.call(ii, hour, minute, second);
+ } else if (freq == RRule.MINUTELY) {
+ if (filtered) {
+ // Jump to one iteration before next day
+ minute += Math.floor(
+ (1439 - (hour * 60 + minute)) / interval) * interval;
+ }
+ while(true) {
+ minute += interval;
+ dm = divmod(minute, 60);
+ div = dm.div;
+ mod = dm.mod;
+ if (div) {
+ minute = mod;
+ hour += div;
+ dm = divmod(hour, 24);
+ div = dm.div;
+ mod = dm.mod;
+ if (div) {
+ hour = mod;
+ day += div;
+ fixday = true;
+ filtered = false;
+ }
+ }
+ if ((!plb(byhour) || contains(byhour, hour)) &&
+ (!plb(byminute) || contains(byminute, minute))) {
+ break;
+ }
+ }
+ timeset = gettimeset.call(ii, hour, minute, second);
+ } else if (freq == RRule.SECONDLY) {
+ if (filtered) {
+ // Jump to one iteration before next day
+ second += Math.floor(
+ (86399 - (hour * 3600 + minute * 60 + second))
+ / interval) * interval;
+ }
+ while (true) {
+ second += interval;
+ dm = divmod(second, 60);
+ div = dm.div;
+ mod = dm.mod;
+ if (div) {
+ second = mod;
+ minute += div;
+ dm = divmod(minute, 60);
+ div = dm.div;
+ mod = dm.mod;
+ if (div) {
+ minute = mod;
+ hour += div;
+ dm = divmod(hour, 24);
+ div = dm.div;
+ mod = dm.mod;
+ if (div) {
+ hour = mod;
+ day += div;
+ fixday = true;
+ }
+ }
+ }
+ if ((!plb(byhour) || contains(byhour, hour)) &&
+ (!plb(byminute) || contains(byminute, minute)) &&
+ (!plb(bysecond) || contains(bysecond, second)))
+ {
+ break;
+ }
+ }
+ timeset = gettimeset.call(ii, hour, minute, second);
+ }
+
+ if (fixday && day > 28) {
+ var daysinmonth = dateutil.monthRange(year, month - 1)[1];
+ if (day > daysinmonth) {
+ while (day > daysinmonth) {
+ day -= daysinmonth;
+ ++month;
+ if (month == 13) {
+ month = 1;
+ ++year;
+ if (year > dateutil.MAXYEAR) {
+ this._len = total;
+ return iterResult.getValue();
+ }
+ }
+ daysinmonth = dateutil.monthRange(year, month - 1)[1];
+ }
+ ii.rebuild(year, month);
+ }
+ }
+ }
+ }
+
+};
+
+
+RRule.parseString = function(rfcString) {
+ rfcString = rfcString.replace(/^\s+|\s+$/, '');
+ if (!rfcString.length) {
+ return null;
+ }
+
+ var i, j, key, value, attr,
+ attrs = rfcString.split(';'),
+ options = {};
+
+ for (i = 0; i < attrs.length; i++) {
+ attr = attrs[i].split('=');
+ key = attr[0];
+ value = attr[1];
+ switch (key) {
+ case 'FREQ':
+ options.freq = RRule[value];
+ break;
+ case 'WKST':
+ options.wkst = RRule[value];
+ break;
+ case 'COUNT':
+ case 'INTERVAL':
+ case 'BYSETPOS':
+ case 'BYMONTH':
+ case 'BYMONTHDAY':
+ case 'BYYEARDAY':
+ case 'BYWEEKNO':
+ case 'BYHOUR':
+ case 'BYMINUTE':
+ case 'BYSECOND':
+ if (value.indexOf(',') != -1) {
+ value = value.split(',');
+ for (j = 0; j < value.length; j++) {
+ if (/^[+-]?\d+$/.test(value[j])) {
+ value[j] = Number(value[j]);
+ }
+ }
+ } else if (/^[+-]?\d+$/.test(value)) {
+ value = Number(value);
+ }
+ key = key.toLowerCase();
+ options[key] = value;
+ break;
+ case 'BYDAY': // => byweekday
+ var n, wday, day, days = value.split(',');
+ options.byweekday = [];
+ for (j = 0; j < days.length; j++) {
+ day = days[j];
+ if (day.length == 2) { // MO, TU, ...
+ wday = RRule[day]; // wday instanceof Weekday
+ options.byweekday.push(wday);
+ } else { // -1MO, +3FR, 1SO, ...
+ day = day.match(/^([+-]?\d)([A-Z]{2})$/);
+ n = Number(day[1]);
+ wday = day[2];
+ wday = RRule[wday].weekday;
+ options.byweekday.push(new Weekday(wday, n));
+ }
+ }
+ break;
+ case 'DTSTART':
+ options.dtstart = dateutil.untilStringToDate(value);
+ break;
+ case 'UNTIL':
+ options.until = dateutil.untilStringToDate(value);
+ break;
+ case 'BYEASTER':
+ options.byeaster = Number(value);
+ break;
+ default:
+ throw new Error("Unknown RRULE property '" + key + "'");
+ }
+ }
+ return options;
+};
+
+
+RRule.fromString = function(string) {
+ return new RRule(RRule.parseString(string));
+};
+
+
+//=============================================================================
+// Iterinfo
+//=============================================================================
+
+var Iterinfo = function(rrule) {
+ this.rrule = rrule;
+ this.lastyear = null;
+ this.lastmonth = null;
+ this.yearlen = null;
+ this.nextyearlen = null;
+ this.yearordinal = null;
+ this.yearweekday = null;
+ this.mmask = null;
+ this.mrange = null;
+ this.mdaymask = null;
+ this.nmdaymask = null;
+ this.wdaymask = null;
+ this.wnomask = null;
+ this.nwdaymask = null;
+ this.eastermask = null;
+};
+
+Iterinfo.prototype.easter = function(y, offset) {
+ offset = offset || 0;
+
+ var a = y % 19,
+ b = Math.floor(y / 100),
+ c = y % 100,
+ d = Math.floor(b / 4),
+ e = b % 4,
+ f = Math.floor((b + 8) / 25),
+ g = Math.floor((b - f + 1) / 3),
+ h = Math.floor(19 * a + b - d - g + 15) % 30,
+ i = Math.floor(c / 4),
+ k = c % 4,
+ l = Math.floor(32 + 2 * e + 2 * i - h - k) % 7,
+ m = Math.floor((a + 11 * h + 22 * l) / 451),
+ month = Math.floor((h + l - 7 * m + 114) / 31),
+ day = (h + l - 7 * m + 114) % 31 + 1,
+ date = Date.UTC(y, month - 1, day + offset),
+ yearStart = Date.UTC(y, 0, 1);
+
+ return [ Math.ceil((date - yearStart) / (1000 * 60 * 60 * 24)) ];
+}
+
+Iterinfo.prototype.rebuild = function(year, month) {
+
+ var rr = this.rrule;
+
+ if (year != this.lastyear) {
+
+ this.yearlen = dateutil.isLeapYear(year) ? 366 : 365;
+ this.nextyearlen = dateutil.isLeapYear(year + 1) ? 366 : 365;
+ var firstyday = new Date(year, 0, 1);
+
+ this.yearordinal = dateutil.toOrdinal(firstyday);
+ this.yearweekday = dateutil.getWeekday(firstyday);
+
+ var wday = dateutil.getWeekday(new Date(year, 0, 1));
+
+ if (this.yearlen == 365) {
+ this.mmask = [].concat(M365MASK);
+ this.mdaymask = [].concat(MDAY365MASK);
+ this.nmdaymask = [].concat(NMDAY365MASK);
+ this.wdaymask = WDAYMASK.slice(wday);
+ this.mrange = [].concat(M365RANGE);
+ } else {
+ this.mmask = [].concat(M366MASK);
+ this.mdaymask = [].concat(MDAY366MASK);
+ this.nmdaymask = [].concat(NMDAY366MASK);
+ this.wdaymask = WDAYMASK.slice(wday);
+ this.mrange = [].concat(M366RANGE);
+ }
+
+ if (!plb(rr.options.byweekno)) {
+ this.wnomask = null;
+ } else {
+ this.wnomask = repeat(0, this.yearlen + 7);
+ var no1wkst, firstwkst, wyearlen;
+ no1wkst = firstwkst = pymod(
+ 7 - this.yearweekday + rr.options.wkst, 7);
+ if (no1wkst >= 4) {
+ no1wkst = 0;
+ // Number of days in the year, plus the days we got
+ // from last year.
+ wyearlen = this.yearlen + pymod(
+ this.yearweekday - rr.options.wkst, 7);
+ } else {
+ // Number of days in the year, minus the days we
+ // left in last year.
+ wyearlen = this.yearlen - no1wkst;
+ }
+ var div = Math.floor(wyearlen / 7);
+ var mod = pymod(wyearlen, 7);
+ var numweeks = Math.floor(div + (mod / 4));
+ for (var n, i, j = 0; j < rr.options.byweekno.length; j++) {
+ n = rr.options.byweekno[j];
+ if (n < 0) {
+ n += numweeks + 1;
+ } if (!(0 < n && n <= numweeks)) {
+ continue;
+ } if (n > 1) {
+ i = no1wkst + (n - 1) * 7;
+ if (no1wkst != firstwkst) {
+ i -= 7-firstwkst;
+ }
+ } else {
+ i = no1wkst;
+ }
+ for (var k = 0; k < 7; k++) {
+ this.wnomask[i] = 1;
+ i++;
+ if (this.wdaymask[i] == rr.options.wkst) {
+ break;
+ }
+ }
+ }
+
+ if (contains(rr.options.byweekno, 1)) {
+ // Check week number 1 of next year as well
+ // orig-TODO : Check -numweeks for next year.
+ var i = no1wkst + numweeks * 7;
+ if (no1wkst != firstwkst) {
+ i -= 7 - firstwkst;
+ }
+ if (i < this.yearlen) {
+ // If week starts in next year, we
+ // don't care about it.
+ for (var j = 0; j < 7; j++) {
+ this.wnomask[i] = 1;
+ i += 1;
+ if (this.wdaymask[i] == rr.options.wkst) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (no1wkst) {
+ // Check last week number of last year as
+ // well. If no1wkst is 0, either the year
+ // started on week start, or week number 1
+ // got days from last year, so there are no
+ // days from last year's last week number in
+ // this year.
+ var lnumweeks;
+ if (!contains(rr.options.byweekno, -1)) {
+ var lyearweekday = dateutil.getWeekday(
+ new Date(year - 1, 0, 1));
+ var lno1wkst = pymod(
+ 7 - lyearweekday + rr.options.wkst, 7);
+ var lyearlen = dateutil.isLeapYear(year - 1) ? 366 : 365;
+ if (lno1wkst >= 4) {
+ lno1wkst = 0;
+ lnumweeks = Math.floor(
+ 52
+ + pymod(
+ lyearlen + pymod(
+ lyearweekday - rr.options.wkst, 7), 7)
+ / 4);
+ } else {
+ lnumweeks = Math.floor(
+ 52 + pymod(this.yearlen - no1wkst, 7) / 4);
+ }
+ } else {
+ lnumweeks = -1;
+ }
+ if (contains(rr.options.byweekno, lnumweeks)) {
+ for (var i = 0; i < no1wkst; i++) {
+ this.wnomask[i] = 1;
+ }
+ }
+ }
+ }
+ }
+
+ if (plb(rr.options.bynweekday)
+ && (month != this.lastmonth || year != this.lastyear)) {
+ var ranges = [];
+ if (rr.options.freq == RRule.YEARLY) {
+ if (plb(rr.options.bymonth)) {
+ for (j = 0; j < rr.options.bymonth.length; j++) {
+ month = rr.options.bymonth[j];
+ ranges.push(this.mrange.slice(month - 1, month + 1));
+ }
+ } else {
+ ranges = [[0, this.yearlen]];
+ }
+ } else if (rr.options.freq == RRule.MONTHLY) {
+ ranges = [this.mrange.slice(month - 1, month + 1)];
+ }
+ if (plb(ranges)) {
+ // Weekly frequency won't get here, so we may not
+ // care about cross-year weekly periods.
+ this.nwdaymask = repeat(0, this.yearlen);
+
+ for (var j = 0; j < ranges.length; j++) {
+ var rang = ranges[j];
+ var first = rang[0], last = rang[1];
+ last -= 1;
+ for (var k = 0; k < rr.options.bynweekday.length; k++) {
+ var wday = rr.options.bynweekday[k][0],
+ n = rr.options.bynweekday[k][1];
+ if (n < 0) {
+ i = last + (n + 1) * 7;
+ i -= pymod(this.wdaymask[i] - wday, 7);
+ } else {
+ i = first + (n - 1) * 7;
+ i += pymod(7 - this.wdaymask[i] + wday, 7);
+ }
+ if (first <= i && i <= last) {
+ this.nwdaymask[i] = 1;
+ }
+ }
+ }
+
+ }
+
+ this.lastyear = year;
+ this.lastmonth = month;
+ }
+
+ if (rr.options.byeaster !== null) {
+ this.eastermask = this.easter(year, rr.options.byeaster);
+ }
+};
+
+Iterinfo.prototype.ydayset = function(year, month, day) {
+ return [range(this.yearlen), 0, this.yearlen];
+};
+
+Iterinfo.prototype.mdayset = function(year, month, day) {
+ var set = repeat(null, this.yearlen);
+ var start = this.mrange[month-1];
+ var end = this.mrange[month];
+ for (var i = start; i < end; i++) {
+ set[i] = i;
+ }
+ return [set, start, end];
+};
+
+Iterinfo.prototype.wdayset = function(year, month, day) {
+
+ // We need to handle cross-year weeks here.
+ var set = repeat(null, this.yearlen + 7);
+ var i = dateutil.toOrdinal(
+ new Date(year, month - 1, day)) - this.yearordinal;
+ var start = i;
+ for (var j = 0; j < 7; j++) {
+ set[i] = i;
+ ++i;
+ if (this.wdaymask[i] == this.rrule.options.wkst) {
+ break;
+ }
+ }
+ return [set, start, i];
+};
+
+Iterinfo.prototype.ddayset = function(year, month, day) {
+ var set = repeat(null, this.yearlen);
+ var i = dateutil.toOrdinal(
+ new Date(year, month - 1, day)) - this.yearordinal;
+ set[i] = i;
+ return [set, i, i + 1];
+};
+
+Iterinfo.prototype.htimeset = function(hour, minute, second) {
+ var set = [], rr = this.rrule;
+ for (var i = 0; i < rr.options.byminute.length; i++) {
+ minute = rr.options.byminute[i];
+ for (var j = 0; j < rr.options.bysecond.length; j++) {
+ second = rr.options.bysecond[j];
+ set.push(new dateutil.Time(hour, minute, second));
+ }
+ }
+ dateutil.sort(set);
+ return set;
+};
+
+Iterinfo.prototype.mtimeset = function(hour, minute, second) {
+ var set = [], rr = this.rrule;
+ for (var j = 0; j < rr.options.bysecond.length; j++) {
+ second = rr.options.bysecond[j];
+ set.push(new dateutil.Time(hour, minute, second));
+ }
+ dateutil.sort(set);
+ return set;
+};
+
+Iterinfo.prototype.stimeset = function(hour, minute, second) {
+ return [new dateutil.Time(hour, minute, second)];
+};
+
+
+//=============================================================================
+// Results
+//=============================================================================
+
+/**
+ * This class helps us to emulate python's generators, sorta.
+ */
+var IterResult = function(method, args) {
+ this.init(method, args)
+};
+
+IterResult.prototype = {
+
+ init: function(method, args) {
+ this.method = method;
+ this.args = args;
+
+ this._result = [];
+
+ this.minDate = null;
+ this.maxDate = null;
+
+ if (method == 'between') {
+ this.maxDate = args.inc
+ ? args.before
+ : new Date(args.before.getTime() - 1);
+ this.minDate = args.inc
+ ? args.after
+ : new Date(args.after.getTime() + 1);
+ } else if (method == 'before') {
+ this.maxDate = args.inc ? args.dt : new Date(args.dt.getTime() - 1);
+ } else if (method == 'after') {
+ this.minDate = args.inc ? args.dt : new Date(args.dt.getTime() + 1);
+ }
+ },
+
+ /**
+ * Possibly adds a date into the result.
+ *
+ * @param {Date} date - the date isn't necessarly added to the result
+ * list (if it is too late/too early)
+ * @return {Boolean} true if it makes sense to continue the iteration;
+ * false if we're done.
+ */
+ accept: function(date) {
+ var tooEarly = this.minDate && date < this.minDate,
+ tooLate = this.maxDate && date > this.maxDate;
+
+ if (this.method == 'between') {
+ if (tooEarly)
+ return true;
+ if (tooLate)
+ return false;
+ } else if (this.method == 'before') {
+ if (tooLate)
+ return false;
+ } else if (this.method == 'after') {
+ if (tooEarly)
+ return true;
+ this.add(date);
+ return false;
+ }
+
+ return this.add(date);
+
+ },
+
+ /**
+ *
+ * @param {Date} date that is part of the result.
+ * @return {Boolean} whether we are interested in more values.
+ */
+ add: function(date) {
+ this._result.push(date);
+ return true;
+ },
+
+ /**
+ * 'before' and 'after' return only one date, whereas 'all'
+ * and 'between' an array.
+ * @return {Date,Array?}
+ */
+ getValue: function() {
+ switch (this.method) {
+ case 'all':
+ case 'between':
+ return this._result;
+ case 'before':
+ case 'after':
+ return this._result.length
+ ? this._result[this._result.length - 1]
+ : null;
+ }
+ }
+
+};
+
+
+/**
+ * IterResult subclass that calls a callback function on each add,
+ * and stops iterating when the callback returns false.
+ */
+var CallbackIterResult = function(method, args, iterator) {
+ var allowedMethods = ['all', 'between'];
+ if (!contains(allowedMethods, method)) {
+ throw new Error('Invalid method "' + method
+ + '". Only all and between works with iterator.');
+ }
+ this.add = function(date) {
+ if (iterator(date, this._result.length)) {
+ this._result.push(date);
+ return true;
+ }
+ return false;
+
+ };
+
+ this.init(method, args);
+
+};
+CallbackIterResult.prototype = IterResult.prototype;
+
+
+//=============================================================================
+// Export
+//=============================================================================
+
+if (serverSide) {
+ module.exports = {
+ RRule: RRule
+ // rruleset: rruleset
+ }
+}
+if (typeof ender === 'undefined') {
+ root['RRule'] = RRule;
+ // root['rruleset'] = rruleset;
+}
+
+if (typeof define === "function" && define.amd) {
+ /*global define:false */
+ define("rrule", [], function () {
+ return RRule;
+ });
+}
+
+}(this));
diff --git a/src/cnMapFilter.js b/src/cnMapFilter.js
deleted file mode 100644
index b6f25d3..0000000
--- a/src/cnMapFilter.js
+++ /dev/null
@@ -1,928 +0,0 @@
-//
-// cnMapFilter.js
-//
-// View data on a map, where changing the map filters the data,
-// only showing items that have coordinates/address on map's currently displayed canvas.
-//
-//
-// Copyright (c) 2011 Chad Norwood
-// Dual licensed under the MIT and GPL licenses:
-// http://www.opensource.org/licenses/mit-license.php
-// http://www.gnu.org/licenses/gpl.html
-//
-
-// cnMF, or window.cnMF, is the one and only global. See cnMF.init() below
-
-(function (window){
-
- // ba-debug.js - use debug.log() instead of console.log()
- // debug.setLevel(0) turns off logging,
- // 1 is just errors and timers
- // 2 includes warnings
- // 3 includes info - logs external data (calendar)
- // 9 is everything (log, info, warn, error)
- var lvl = window.location.href.match(/\bdebuglevel=(\d)/i);
- if (lvl && lvl[1]) {
- debug && debug.setLevel(parseInt(lvl[1],10));
- //console.log("setLevel=",lvl[1]);
- } else {
- debug && debug.setLevel(0); // turns off all logging
- }
- debug.includeMsecs(true);
-
-
- // EventClass - event objects created from this class contain all the
- // info needed for an event, including original info from the calendar data
- // plus coordinates and accurate address from geolocation service.
- // This is a core object to mapFilter.
- var EventClass = makeClass();
- EventClass.prototype.init = function ( params ) {
- // defaults for event object
- this.id = -1;
- this.lt = 0;
- this.lg = 0;
- this.validCoords = false;
- this.isDisplayed = false;
- this.addrOrig = '';
- this.addrToGoogle = '';
- this.addrFromGoogle = '';
-
- // overwrite defaults with params
- for (var ii in params) {
- this[ii] = params[ii];
- }
- // these are based on params
- this.dateStartObj = cnMF.parseDate(this.dateStart);
- this.dateEndObj = cnMF.parseDate(this.dateEnd);
- };
- EventClass.prototype.getCoordsStr = function(){
- return this.validCoords ? this.lt + "," + this.lg : '';
- };
- EventClass.prototype.getDirectionsUrlStr = function(){
- return 'http://maps.google.com/maps?f=d&q=' + this.addrToGoogle.replace(/ /g, '+').replace(/"/g, '%22');
- };
- EventClass.prototype.getDirectionsHtmlStr = function(){
- return 'Directions ';
- };
- EventClass.prototype.insideCurMap = function(mapbox){
- return this.validCoords ? mapbox.containsLatLng(new GLatLng(this.lt, this.lg, true)) : false;
- };
- // Returns true if current event occurs before start or after end.
- EventClass.prototype.isFilteredbyDate = function(startDayOffset,endDayOffset){
- var nowMs = new Date().getTime();
- var startTime = nowMs + startDayOffset *24*3600*1000; // ms
- var endTime = nowMs + endDayOffset *24*3600*1000; // ms
- if (this.dateEndObj.getTime() < startTime) return true;
- if (this.dateStartObj.getTime() > endTime) return true;
- return false;
- };
- EventClass.prototype.setId = function ( id ) {
- this.id = id;
- };
- EventClass.prototype.setMarkerObj = function (mrkr) {
- //this.myGLatLng = existingGLatLng || new GLatLng(this.lt, this.lg);
- this.markerObj = mrkr;
- };
- EventClass.prototype.getMarkerObj = function () {
- return this.markerObj;
- };
-
-
-
-
- // MarkerClass - designed to contain all google markers for map.
- // Note that one marker can contain multiple events.
- var MarkerClass = makeClass();
- MarkerClass.prototype.init = function ( gMap ) {
- // allMarkers obj is the main obj, where key is the coordinates and the
- // value is markerObject - see addMarker
- this.allMarkers = {};
- this.gMap = gMap; // the google map object
- }
- // getMarkerObj() returns the marker object
- MarkerClass.prototype.getMarkerObj = function(coordsStr){
- // TODO also accept event obj (then we get coords from that)
- return this.allMarkers[coordsStr];
- }
- // getGoogleMarker() returns the GMarker object created by google.
- MarkerClass.prototype.getGoogleMarker = function(coordsStr){
- // TODO also accept event obj (then we get coords from that)
- return this.allMarkers[coordsStr] && this.allMarkers[coordsStr].googleMarker;
- }
- // getEvents() returns an array of all event objects at the provided coordinates.
- MarkerClass.prototype.getEvents = function(coordsStr){
- // TODO also accept event obj (then we get coords from that)
- return this.allMarkers[coordsStr] && this.allMarkers[coordsStr].eventList;
- }
- // addMarker() creates google markers and event listener.
- MarkerClass.prototype.addMarker = function(eventObj){
- var coordsStr = eventObj.getCoordsStr();
- if (this.allMarkers[coordsStr]) {
- // Already have a marker at this location, so add event to list.
- this.allMarkers[coordsStr].eventList.push(eventObj.id);
- //debug.log("addMarker() added to existing marker, eventObj.id="+eventObj.id);
- return;
- }
- // No markers existing at this location, so create one.
- //debug.log("addMarker() creating marker, eventObj.id=%s, %o", eventObj.id, eventObj.getCoordsStr());
- var myGLatLng = new GLatLng(eventObj.lt, eventObj.lg);
- // http://code.google.com/apis/maps/documentation/javascript/v2/reference.html#GMarker
- var gMrkr = new GMarker( myGLatLng, {
- icon:iconDefault
- });
- GEvent.addListener(gMrkr, "click", function() {
- try {
- _gaq.push(['_trackEvent', 'Interaction', 'gMrkr', 'click']);
- } catch (e) {}
- cnMF.coreOptions.cbHighlightItem(eventObj.getCoordsStr());
- });
- this.gMap.addOverlay(gMrkr);
- this.allMarkers[coordsStr] = {
- googleMarker: gMrkr,
- gLatLng: myGLatLng, // note this can also be accessed via googleMarker.gLatLng()
- eventList: [eventObj.id]
- }
- return;
- }
- MarkerClass.prototype.removeMarkers = function(eventObj){
- var coordsStr = eventObj.getCoordsStr();
- for (var ii=0; this.allMarkers[coordsStr].eventList[ii]; ii++) {
- if (this.allMarkers[coordsStr].eventList[ii] === eventObj.id) {
- // found it, remove
- this.allMarkers[coordsStr].eventList.splice(ii, 1);
- }
- }
- if (this.allMarkers[coordsStr].eventList.length === 0) {
- this.gMap.removeOverlay(this.allMarkers[coordsStr].googleMarker);
- // remove gLatLng?
- delete this.allMarkers[coordsStr];
- }
- }
-
-
- // TODO: do we use this anymore?
- function eventType(params){
- // http://stackoverflow.com/questions/383402/is-javascript-s-new-keyword-considered-harmful
- if (!(this instanceof eventType))
- return new eventType(params);
-
- this.filtersActive = false;
- for (var ii in params) {
- this[ii] = params[ii];
- }
- }
-
-
- //
- // END PRIVATE FUNCTIONS and CLASSES
- //
-
-
- var cnMF = {
- gcTitle: 'A Calendar', // this should always get overwritten by calendar data
- gcLink: '',
- googleApiKey: '',
- reportData: {},
- processGeocodeTimer: 0,
- numDisplayed: 0,
- filteredByDate: false,
- filteredByMap: false,
- types: [],
- myMarkers: {},
- tz: {},
- eventList: []
- }
-
- // cnMF.init() starts it all
- //
- cnMF.init = function (coreOptions) {
- cnMF.coreOptions = coreOptions;
- cnMF.myMarkers = MarkerClass(coreOptions.gMap); // stores google map marker, and marker's corresponding events
- cnMF.curStartDay = coreOptions.oStartDay;
- cnMF.curEndDay = coreOptions.oEndDay;
- cnMF.origStartDay = coreOptions.oStartDay;
- cnMF.origEndDay = coreOptions.oEndDay;
- cnMF.googleApiKey = coreOptions.googleApiKey;
-
- cnMF.tz.offset = coreOptions.tzOffset ? coreOptions.tzOffset : ''; // Offset in hours and minutes from UTC
- cnMF.tz.name = coreOptions.tzName ? coreOptions.tzName : 'unknown'; // Olson database timezone key (ex: Europe/Berlin)
- cnMF.tz.dst = coreOptions.tzDst ? coreOptions.tzDst : 'unknown'; // bool for whether the tz uses daylight saving time
- cnMF.tz.computedFromBrowser = (cnMF.tz.name != 'unknown');
- }
-
- cnMF.countTotal = function () {
- return cnMF.eventList.length;
- // todo: support start/end
- }
- cnMF.countKnownAddresses = function () {
- var xx = 0;
- for (var ii in cnMF.eventList) {
- if (cnMF.eventList[ii].validCoords) xx++;
- }
- //cnMF.reportData.knownAddr = xx;
- return xx;
- }
- cnMF.countUnknownAddresses = function(){
- var xx = 0;
- for (var ii in cnMF.eventList) {
- if (! cnMF.eventList[ii].validCoords) xx++;
- }
- //cnMF.reportData.unknownAddr = xx;
- return xx;
- }
- cnMF.addEvent = function(params){ // old addEventObj
- var e = EventClass(params);
- e.setId(cnMF.eventList.length);
- //cnMF.debugObj(e);
- cnMF.eventList.push(e);
- return e;
- }
- cnMF.addEventType = function(params){
- var e = new eventType(params);
- e.id = cnMF.types.length;
- cnMF.types.push(e);
- return e;
- }
-
- //
- // mapAllEvents() adjusts zoom and coords in order to fit all events on map
- //
- cnMF.mapAllEvents = function(){
- // first create the box that holds all event locations
- var box = null;
- for (var i in cnMF.eventList) {
- var kk = cnMF.eventList[i];
- if (! kk.validCoords) continue; // skip unrecognized addresses
-
- if (box === null) {
- var corner = new GLatLng(kk.lt, kk.lg, true);
- box = new GLatLngBounds(corner, corner);
- } else {
- box.extend(new GLatLng(kk.lt, kk.lg, true));
- }
- }
-
- if (!box) {
- debug.log("mapAllEvents(): no events");
- return false;
- }
-
- debug.log("mapAllEvents(): setting new map ");
- zoom = cnMF.coreOptions.gMap.getBoundsZoomLevel(box);
- cnMF.coreOptions.gMap.setCenter( box.getCenter(), (zoom < 2) ? zoom : zoom - 1 );
- }
-
- cnMF.processGeocode = function(gObj) {
- for (var ii in cnMF.eventList) {
- var kk = cnMF.eventList[ii];
- if (kk.addrToGoogle != gObj.addr1) {
- continue;
- }
- if (kk.validCoords) {
- debug.log( 'received duplicate geocode', gObj);
- continue;
- }
-
- if (gObj.lt) {
- kk.lt = gObj.lt;
- kk.lg = gObj.lg;
- kk.validCoords = true;
- kk.addrFromGoogle = gObj.addr2;
- }
- else if (gObj.error) {
- kk.addrFromGoogle = 'address unrecognizable';
- kk.error = gObj.error;
- }
- }
- // if geocoding takes longer than xx ms, then mapRedraw()
- var now = new Date().getTime();
- if (!cnMF.processGeocodeTimer) cnMF.processGeocodeTimer = now;
-
- if (now - cnMF.processGeocodeTimer > 1000) {
- debug.log( 'processGeocodeTimer:', now-cnMF.processGeocodeTimer)
- cnMF.processGeocodeTimer = now;
- //mapAllEvents();
- //onGeoDecodeComplete();
- }
- }
-
-
-
- // returns true if changes were made to map, false otherwise
- //
- cnMF.updateMarkers= function(){
-
- debug.log( "cnMF.updateMarkers() called ..");
- mapbox = cnMF.coreOptions.gMap.getBounds();
-
- if(0) debug.time('checking all markers');
- added = 0;
- removed = 0;
- unchanged = 0;
- cnMF.filteredByDate = false;
- cnMF.filteredByMap = false;
- //debug.log( "reset filteredByDate and filteredByMap to FALSE");
-
- for (var i in cnMF.eventList) {
- var kk = cnMF.eventList[i];
-
- insideCurMap = kk.insideCurMap(mapbox);
- debug.log( "marker "+ (insideCurMap ? 'in':'out') +"side map %o", kk);
- if (!insideCurMap) {
- debug.log( " filteredByMap = true for ",kk);
- cnMF.filteredByMap = true;
- }
-
- filteredOut = kk.isFilteredbyDate(cnMF.curStartDay,cnMF.curEndDay);
-
- /* OLDTODO: fix filters - add search, categories, time
- if (events.types[kk.type].filtersActive) {
- //isFiltered = markers.type[kk.type].filter(kk);
- filteredOut = false;
- }
- */
- if (filteredOut) {
- cnMF.filteredByDate = true;
- }
- if (kk.isDisplayed && insideCurMap && !filteredOut) {
- unchanged++;
- }
- else if (kk.isDisplayed && (!insideCurMap || filteredOut)) {
- // hide all markers outside of map or current filters
- kk.isDisplayed = false;
- cnMF.myMarkers.removeMarkers(kk);
- removed++;
- }
- else if (!kk.isDisplayed && insideCurMap && !filteredOut) {
- // display events new to map
- cnMF.coreOptions.cbBuildInfoHtml(kk);
- cnMF.myMarkers.addMarker(kk);
- kk.isDisplayed = true;
- added++;
- }
- }
- if(0) debug.timeEnd('checking all markers');
-
- cnMF.numDisplayed = unchanged + added;
- //cnMF.reportData.numDisplayed = cnMF.numDisplayed;
-
- debug.info("updateMarkers() "+removed+" removed, "+added+" added, "+unchanged+" unchanged, "
- +cnMF.numDisplayed+" total ");
-
- return (removed || added);
- }
-
-
-
- // showDays(): Shows all events from newStartDay to newEndDay, called when date slider dragging stops
- // newStartDay to newEndDay are both number of days relative to today
- // Currently, newStartDay and newEndDay should be within original calendar start/end days (no ajax req'd)
- ///
- cnMF.showDays = function (newStartDay, newEndDay) {
- // todo: query gcal
- cnMF.curStartDay = newStartDay;
- cnMF.curEndDay = newEndDay;
- cnMF.coreOptions.cbMapRedraw(); // triger updateMarkers();
- }
-
-
- cnMF.getGCalData = function(gCalUrl, startDays, endDays, callbacks ) {
- if (gCalUrl.search(/^http/i) < 0) {
- debug.warn("getGCalData(): bad url: "+ gCalUrl);
- return;
- }
- gCalUrl = gCalUrl.replace(/\/basic$/, '/full');
- //startmax = '2009-07-09T10:57:00-08:00';
-
- // TODO: change rfc3339 to accept StartDays
- startDate = new Date();
- startDate.setTime(startDate.getTime() + startDays*24*3600*1000);
- startmin = cnMF.rfc3339(startDate,false);
- debug.info("getGCalData(): start-min: "+startmin);
-
- endDate = new Date();
- endDate.setTime(endDate.getTime() + endDays*24*3600*1000);
- startmax = cnMF.rfc3339(endDate,true);
- debug.info("getGCalData(): start-max: "+startmax);
-
- // http://code.google.com/apis/calendar/docs/2.0/reference.html
- gCalObj = {
- 'start-min': startmin,
- 'start-max': startmax,
- 'max-results': 200,
- 'orderby' : 'starttime',
- 'sortorder': 'ascending',
- 'singleevents': false
- };
- if (cnMF.tz.name != 'unknown') {
- gCalObj.ctz = cnMF.tz.name; // ex: 'America/Chicago'
- debug.info("Displaying calendar times using this timezone: "+ gCalObj.ctz);
- }
- $.getJSON(gCalUrl + "?alt=json-in-script&callback=?", gCalObj, function(cdata) {
- parseGCalData(cdata, startDate, endDate, callbacks);
- });
- }
-
- function parseGCalData (cdata, startDate, endDate, callbacks ) {
-
- debug.info("parseGCalData() calendar data: ",cdata);
-
- cnMF.gcTitle = cdata.feed.title ? cdata.feed.title['$t'] : 'title unknown';
- cnMF.gcLink = cdata.feed.link ? cdata.feed.link[0]['href'] : '';
- cnMF.desc = cdata.feed.subtitle ? cdata.feed.subtitle['$t'] : 'subtitle unknown';
- cnMF.reportData['fn'] = cnMF.gcTitle.replace(/\W/,"_");
- cnMF.gcTitle = cdata.feed.title ? cdata.feed.title['$t'] : 'title unknown';
- if (!cnMF.tz.computedFromBrowser) {
- cnMF.tz.name = cdata.feed.gCal$timezone.value;
- debug.info("Displaying calendar times using calendar timezone: "+ cnMF.tz.name);
- }
- var uniqAddr={};
-
- /* do we need this at all anymore?
- eType = cnMF.addEventType({
- tableHeadHtml: "one two ",
- tableCols: [3,5],
- title: cnMF.gcTitle,
- titleLink: cnMF.gcLink
- });
- */
- for (var ii=0; cdata.feed.entry && cdata.feed.entry[ii]; ii++) {
- var curEntry = cdata.feed.entry[ii];
- if (!(curEntry['gd$when'] && curEntry['gd$when'][0]['startTime'])) {
- debug.info("skipping cal curEntry (no gd$when) %s (%o)", curEntry['title']['$t'], curEntry);
- return true; // continue to next one
- };
- var url = {};
- for (var jj=0; curEntry.link[jj]; jj++) {
- var curLink = curEntry.link[jj];
- if (curLink.type == 'text/html') {
- // looks like when rel='related', href is original event info (like meetup.com)
- // when rel='alternate', href is the google.com calendar event info
- url[curLink.rel] = curLink.href;
- }
- }
- kk = cnMF.addEvent({
- //type: eType.id,
- name: curEntry['title']['$t'],
- desc: curEntry['content']['$t'],
- addrOrig: curEntry['gd$where'][0]['valueString'] || '', // addrOrig is the location field of the event
- addrToGoogle: curEntry['gd$where'][0]['valueString'] || '',
- gCalId: curEntry['gCal$uid']['value'],
- url: url.related || url.alternate, // TODO - is this what we want? see href above
- dateStart: cnMF.parseDate(curEntry['gd$when'][0]['startTime']),
- dateEnd: cnMF.parseDate(curEntry['gd$when'][0]['endTime'])
- });
- // make ready for geocode TODO: remove this? or move this line to addrToGoogle above
- kk.addrToGoogle = kk.addrToGoogle.replace(/\([^\)]+\)\s*$/, ''); // remove parens and text inside parens
- if (kk.addrToGoogle) {
- uniqAddr[kk.addrToGoogle] = 1;
- } else {
- debug.info("Skipping blank address for "+kk.name+" ["+kk.addrOrig+"]",kk);
- }
- debug.log("parsed curEntry "+ii+": ", kk.name, curEntry, kk);
- }
- cnMF.totalEntries = ii;
- cnMF.totalEvents = cdata.feed.openSearch$totalResults.$t || cnMF.totalEntries;
-
- debug.log("calling mapfilter.geocode(): ", uniqAddr );
- cnMF.myGeoDecodeComplete = false;
- cnMF.myGeo = cnMF.geocodeManager({
- addresses: uniqAddr,
- googleApiKey: cnMF.googleApiKey,
- geocodedAddrCallback: function (gObj) {
- cnMF.processGeocode(gObj); // TODO: this can be private
- if ('function' === typeof callbacks.onGeoDecodeAddr) callbacks.onGeoDecodeAddr();
- },
- geocodeCompleteCallback: function() {
- //onGeoDecodeComplete();
- cnMF.myGeoDecodeComplete = true;
- if ('function' === typeof callbacks.onGeoDecodeComplete) callbacks.onGeoDecodeComplete();
- }
- });
- if ('function' === typeof callbacks.onCalendarLoad) callbacks.onCalendarLoad();
- }
-
-
- // geocodeManager handles the geocoding of addresses
- //
- // TODO: use more than just google maps api
- //
- // http://tinygeocoder.com/blog/how-to-use/
- // http://github.com/straup/js-geocoder
- // http://www.geowebguru.com/articles/185-arcgis-javascript-api-part-1-getting-started
- //
- // TODO: use GeoAPI's reverse geocoder to get neighborhood (SOMA/Mission/marina, SF CA)
- //
- // TODO: create a local geocode cache server and mysql db?
- // http://code.google.com/apis/maps/articles/phpsqlajax.html
- ///
- cnMF.geocodeManager = function( gOpts ) {
-
- // count addreses and unique addreses.
- // don't want duplicates - wasting calls to google
- var uniqAddresses = {};
- var numAddresses = numUniqAddresses = numUniqAddrDecoded = numUniqAddrErrors = 0;
- var geoCache = {};
-
- var numReqs = 0;
- var startTime = new Date().getTime();
-
- // won't stop till all address objects are resolved (resolved==true)
- // when resolved is true, then if (validCoords) it was successful
- // otherwise look to error
- //
- function addrObject(address) {
- this.addr1 = address;
- this.addr2 = ''; // from google
- this.inProgress = false;
- this.sentTimes = 0;
- this.resolved = false;
- this.validCoords = false;
- this.error = '';
- this.lt = '';
- this.lg = '';
- }
-
- function geoMgrInit(){
- for (var ii in gOpts.addresses) {
- if (!(ii.length > 0)) {
- debug.warn("geocodeManager-geoMgrInit() skipping blank address");
- continue;
- }
- numAddresses++;
- if (!isNaN(ii)) {
- uniqAddresses[gOpts.addresses[ii]] = 1; // array
- } else {
- uniqAddresses[ii] = 1; // object or string
- }
- }
- for (var addr in uniqAddresses) {
- if (geoCache[addr]) {
- gOpts.geocodedAddrCallback(geoCache[addr]);
- continue;
- }
- geoCache[addr] = new addrObject(addr);
- numUniqAddresses++;
- }
- gGeocodeQueue();
- }
-
- function allResolved() {
- for (var addr in geoCache) {
- if (!geoCache[addr].resolved) return false;
- }
- return true;
- }
-
- function getUnresolved() {
- for (var addr in geoCache) {
- ao = geoCache[addr];
- if (!ao.resolved && !ao.inProgress) return ao;
- }
- return false;
- }
-
- function checkInProgress(ems) {
- for (var addr in geoCache) {
- ao = geoCache[addr];
- if (ao.inProgress && (ems - ao.sentLast > 2000)) {
- if (ao.sentTimes > 3) {
- ao.resolved = true;
- numUniqAddrErrors++;
- debug.log('checkInProgress() forgetting request '+ao.reqNum, ao);
- gOpts.geocodedAddrCallback(ao);
- } else {
- ao.inProgress = false;
- debug.log('checkInProgress() resetting request '+ao.reqNum+' after '+(ems-ao.sentLast)+'ms', ao);
- }
- }
- }
- return false;
- }
-
- // gGeocodeQueue()
- //
- // Note: Google allows 15k lookups per day per IP. However, too many requests
- // at the same time triggers a 620 code from google. Therefore we want about 100ms
- // delay between each request using gGeocodeQueue. Likewise, when we get a 620 code,
- // we wait a bit and resubmit.
- // http://code.google.com/apis/maps/faq.html#geocoder_limit
- //
- // NOTE: yahoo allows 5k lookups per day per IP
- // http://developer.yahoo.com/maps/rest/V1/geocode.html
- //
- var desiredRate = 100; // long-term average should be one query every 'desiredRate' ms
- var maxBurstReq = 4; // if timeout gets delayed, say 500ms, we can send 'maxBurstReq' at a time till we catch up
- var maxRetry = 4;
- function gGeocodeQueue () {
- ems = new Date().getTime() - startTime;
- bursts = maxBurstReq;
- while (bursts-- && (ems > numReqs*desiredRate ) && (ao = getUnresolved())) {
- ao.reqNum = ++numReqs;
- ao.inProgress = true;
- ao.sentLast = ems;
- ao.sentTimes++;
- debug.log(" gGeocodeQueue() sending req "+numReqs+" at "+ems+"ms, addr: ", ao.addr1);
- cnMF.gGeocode( ao.addr1, gOpts.googleApiKey, function (gObj) {
- parseGObj(gObj);
- });
- ems = new Date().getTime() - startTime;
- }
- checkInProgress(ems);
-
- if (allResolved()) {
- debug.log("gGeocodeQueue() all queries complete, geocoder done");
- gOpts.geocodeCompleteCallback();
- return;
- }
- setTimeout(function() { gGeocodeQueue() }, desiredRate);
- }
-
- function parseGObj(gObj) {
- if (typeof(gObj) != 'object') {
- debug.warn("parseGObj() shouldn't be here " + typeof(gObj), gObj);
- return;
- }
- if (gObj.tmpError) {
- if (gObj.errorCode == 620) {
- debug.log("parseGObj() resubmit (too fast)", gObj.addr1, gObj);
- desiredRate = 1.1 * desiredRate; // slow down requests
- } else {
- debug.log("parseGObj() resubmit (timeout) ", gObj.addr1, gObj);
- }
- if (gObj.addr1) {
- geoCache[gObj.addr1].inProgress = false;
- geoCache[gObj.addr1].error = gObj.error;
- }
-
- } else if (gObj.error) {
- debug.info("parseGObj() error ", gObj);
- geoCache[gObj.addr1].resolved = true;
- geoCache[gObj.addr1].validCoords = false;
- geoCache[gObj.addr1].inProgress = false;
- geoCache[gObj.addr1].error = gObj.error;
- numUniqAddrErrors++;
- gOpts.geocodedAddrCallback(geoCache[gObj.addr1]);
-
- } else if (gObj.lt) {
- if (!gObj.addr1 || !geoCache[gObj.addr1]) {
- debug.warn("parseGObj() debug me", gObj);
- return;
- }
- geoCache[gObj.addr1].lt = gObj.lt;
- geoCache[gObj.addr1].lg = gObj.lg;
- geoCache[gObj.addr1].addr2 = gObj.addr2;
- geoCache[gObj.addr1].resolved = true;
- geoCache[gObj.addr1].validCoords = true;
- geoCache[gObj.addr1].inProgress = false;
- debug.log("parseGObj() got coords ", gObj.addr1, gObj, geoCache[gObj.addr1]);
-
- numUniqAddrDecoded++;
- gOpts.geocodedAddrCallback(geoCache[gObj.addr1]);
-
- } else {
- debug.warn("parseGObj() should not be here ", gObj);
- }
- }
-
- geoMgrInit();
-
- return {
- numAddresses: numAddresses,
- numUniqAddresses: numUniqAddresses,
- count: function() {
- var c = {
- uniqAddrDecoded: 0,
- uniqAddrErrors: 0,
- uniqAddrTotal: 0
- }
- for (var addr in geoCache) {
- ao = geoCache[addr];
- c.uniqAddrTotal++;
- if (ao.resolved && ao.validCoords) c.uniqAddrDecoded++;
- if (ao.resolved && !ao.validCoords) c.uniqAddrErrors++;
- }
- return c;
- }
- }
- }
-
- // description of gObj
- // geocodeObj: {
- // lg: null, // number, -180 +180
- // lt: null, // number, -90 +90
- // addr2: null, // string, google's rewording of addr1
- // addr1: null, // string, address passed to geocoder
- // errorCode: null, // number
- // tmpError: false, // boolean
- // error: null // string error msg
- // },
- //
-
- // gGeocode() translates addresses into array of lat/lng coords using Google, also see gGeocodeQueue()
- //
- cnMF.gGeocode = function( addr, googleApiKey, callback ) {
-
- //debug.log("gGeocode() submitting addr to google: " + addr);
- //$("#"+ cnMFUI.opts.listId ).append('.');
-
- // switched from getJson to ajax to handle errors. However, looks like error function is not called
- // when google responds with http 400 and text/html (versus http 200 with text/javascript)
- //
- // http://groups.google.com/group/google-maps-api/browse_thread/thread/e347b370e8586767/ddf95bdb0fc6a9f7?lnk=raot
- geoUrl = 'http://maps.google.com/maps/geo?'
- + '&key='+ googleApiKey
- + '&q='+ escape(addr)
- + '&sensor=false&output=json'
- + '&callback=?';
- //geoUrl = 'http://maps.google.com/maps/geo?callback=?';
- $.ajax({
- type: "GET",
- url: geoUrl,
- dataType: "json",
- //global: false,
- error: function (XMLHttpRequest, textStatus, errorThrown) {
- debug.log("gGeocode() error for "+ geoUrl);
- },
- // complete is only called after success, not on error, therefore useless
- //complete: function (XMLHttpRequest, textStatus) {
- // debug.log("gGeocode() complete ", textStatus, XMLHttpRequest);
- //},
- success: function(data, textStatus) {
- //debug.log("gGeocode() success() status,data: ", textStatus, data);
- //$("#"+ cnMFUI.opts.listId ).append('.');
- if (data.Placemark) {
- callback( {
- lg: data.Placemark[0].Point.coordinates[0],
- lt: data.Placemark[0].Point.coordinates[1],
- addr2: data.Placemark[0].address,
- addr1: data.name
- });
- } else {
- callback( {
- // http://code.google.com/apis/maps/documentation/geocoding/index.html#StatusCodes
- addr1: data.name,
- data: data,
- errorCode: data.Status.code,
- tmpError: (data.Status.code == 620) || (data.Status.code == 500) || (data.Status.code == 610),
- error: (data.Status.code) ?
- ((602==data.Status.code) ? "602: Unknown Address" :
- ((620==data.Status.code) ? "620: Too Many Lookups" : "Google code: "+data.Status.code)) :
- "Google geocode api changed"
- });
- }
- }
- }); //close $.ajax
- }
- // END GEOCODE
-
-
- //
- // BEGIN HELPER FUNCTIONS
- //
-
- // makeClass - By John Resig (MIT Licensed)
- function makeClass(){
- return function(args){
- if ( this instanceof arguments.callee ) {
- if ( typeof this.init == "function" )
- this.init.apply( this, args.callee ? args : arguments );
- } else {
- return new arguments.callee( arguments );
- }
- };
- }
-
- //
- // The following date and time routines from fullCalendar
- //
- function zeroPad(n) {
- return (n < 10 ? '0' : '') + n;
- }
-
- cnMF.rfc3339 = function(d, clearhours) {
- s = d.getUTCFullYear()
- + "-" + zeroPad(d.getUTCMonth() + 1)
- + "-" + zeroPad(d.getUTCDate());
- if (clearhours) {
- s += "T00:00:00";
- } else {
- s += "T" + zeroPad(d.getUTCHours())
- + ":" + zeroPad(d.getUTCMinutes())
- + ":" + zeroPad(d.getUTCSeconds());
- }
- return s + cnMF.tz.offset; // ex: "-06:00" is chicago offset
- }
-
-
- cnMF.monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];
- cnMF.monthAbbrevs = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
- cnMF.dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
- cnMF.dayAbbrevs = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
-
- cnMF.formatDate = function(d, format) {
- var f = cnMF.dateFormatters;
- var s = '';
- for (var i=0; i