├── .gitignore ├── README.md ├── images ├── blue-marble-1280.jpg ├── custom.php ├── gray-1280.png ├── gray-400.png ├── gray-600.png ├── gray-800.png ├── living-1280.jpg ├── map-outlines.psd ├── night-electric-1280.jpg └── pin.png ├── includes └── parser.inc ├── index.php ├── json.php ├── lib ├── jquery.maphilight.min.js ├── jquery.timezone-picker.js └── jquery.timezone-picker.min.js ├── style.css ├── tz_islands.txt └── tz_world.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /world -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | timezonepicker 2 | ============== 3 | 4 | A jQuery and ImageMap based timezone picker. 5 | 6 | This library only works with pre-built imagemaps generated from 7 | http://timezonepicker.com. 8 | 9 | Features 10 | -------- 11 | 12 | - Simple implementation, lightweight footprint (160KB, 40KB gzipped). 13 | - Includes 440+ clickable areas. 14 | - HTML5 Geolocation to identify timezone. 15 | - Islands include padding to increase ease of selection. 16 | - Country mapping can be used to set timezone and country at the same time. 17 | - Timezone highlighting on rollover (thanks to [jQuery maphilight](http://davidlynch.org/projects/maphilight/docs/)) 18 | 19 | Usage 20 | ----- 21 | 22 | Basic call using all defaults: 23 | ```javascript 24 | $('#img-with-usemap-attr').timezonePicker(); 25 | ``` 26 | 27 | A few simple options: 28 | ```javascript 29 | $('#img-with-usemap-attr').timezonePicker({ 30 | pin: '.timezone-pin', 31 | fillColor: 'FFCCCC', 32 | }); 33 | ``` 34 | 35 | Options 36 | ------- 37 | As pulled from the set of defaults. 38 | 39 | ```javascript 40 | $.fn.timezonePicker.defaults = { 41 | // Selector for the pin that should be used. This selector only works in the 42 | // immediate parent of the image map img tag. 43 | pin: '.timezone-pin', 44 | // Specify a URL for the pin image instead of using a DOM element. 45 | pinUrl: null, 46 | // Preselect a particular timezone. 47 | timezone: null, 48 | // Pass through options to the jQuery maphilight plugin. 49 | maphilight: true, 50 | // Selector for the select list, textfield, or hidden to update upon click. 51 | target: null, 52 | // Selector for the select list, textfield, or hidden to update upon click 53 | // with the specified country. 54 | countryTarget: null, 55 | // If changing the country should use the first timezone within that country. 56 | countryGuess: true, 57 | // A list of country guess exceptions. These should only be needed if a 58 | // country spans multiple timezones. 59 | countryGuesses: { 60 | 'AU': 'Australia/Sydney', 61 | 'BR': 'America/Sao_Paulo', 62 | 'CA': 'America/Toronto', 63 | 'CN': 'Asia/Shanghai', 64 | 'ES': 'Europe/Madrid', 65 | 'MX': 'America/Mexico_City', 66 | 'RU': 'Europe/Moscow', 67 | 'US': 'America/New_York' 68 | }, 69 | // If this map should automatically adjust its size if scaled. Note that 70 | // this can be very expensive computationally and will likely have a delay 71 | // on resize. The maphilight library also is incompatible with this setting 72 | // and will be disabled. 73 | responsive: false, 74 | // A function to be called upon timezone change 75 | // timezoneName, countryName, and offset will be passed as arguments 76 | changeHandler: null, 77 | 78 | // Default options passed along to the maphilight plugin. 79 | fade: false, 80 | stroke: true, 81 | strokeColor: 'FFFFFF', 82 | strokeOpacity: 0.4, 83 | fillColor: 'FFFFFF', 84 | fillOpacity: 0.4, 85 | groupBy: 'data-offset' 86 | }; 87 | ``` 88 | 89 | Additional methods 90 | ------------------ 91 | After creating a timezone picker from an image tag, you can execute additional 92 | commands on the image map with these methods: 93 | 94 | ```javascript 95 | // Query the user's browser for the current location and set timezone from that. 96 | $('#img-with-usemap-attr').timezonePicker('detectTimezone'); 97 | 98 | // The detectTimezone method may also provide event callbacks. 99 | $('#img-with-usemap-attr').timezonePicker('detectTimezone', { 100 | success: successCallback, 101 | error: errorCallback, 102 | complete: completeCallback, // Called on both success or failure. 103 | }); 104 | 105 | // Set the active timezone to some value programatically. 106 | $('#img-with-usemap-attr').timezonePicker('updateTimezone', 'America/New_York'); 107 | 108 | // Resize the image map coordinates to match an adjusted size of the image. 109 | // Note that this option does not work well and is very slow. Not recommended. 110 | $('#img-with-usemap-attr').timezonePicker('resize'); 111 | ``` 112 | 113 | Building new definition files 114 | ----------------------------- 115 | 116 | The definition files are used to determine the polygons and rectangles used to 117 | generate the resulting imagemap. Note this should rarely be necessary for normal 118 | users as the timezone picker project will rebuild the shape files after updates 119 | to the timezone database. 120 | 121 | 1. Download latest shape file "tz_world" from 122 | http://efele.net/maps/tz/world/. 123 | 124 | wget http://efele.net/maps/tz/world/tz_world.zip 125 | unzip tz_world.zip 126 | 127 | 2. Install PostGIS, which provides the shp2pgsql executable. 128 | http://postgis.refractions.net/download/ 129 | 130 | For Mac OS X, I installed PostGres, GDAL Complete, and PostGIS binaries from 131 | http://www.kyngchaos.com/software:postgres 132 | 133 | Then add psql and shp2pgsql to your $PATH variable in your shell profile. 134 | export PATH=/usr/local/pgsql-9.1/bin:$PATH 135 | 136 | 3. Convert the tz_world.shp file into SQL: 137 | 138 | ``` 139 | cd world 140 | shp2pgsql tz_world.shp timezones > tz_world.sql 141 | ``` 142 | 143 | 4. Create a temporary database and import the SQL file. 144 | 145 | ``` 146 | psql -U postgres -c "CREATE DATABASE timezones" -d template1 147 | ``` 148 | 149 | And import the PostGIS functions into the database. 150 | 151 | ``` 152 | psql -U postgres -d timezones -f /usr/local/pgsql-9.1/share/contrib/postgis-2.0/postgis.sql 153 | 154 | psql -U postgres -d timezones < tz_world.sql 155 | ``` 156 | 157 | 5. Export the data as text in a simplified format. 158 | 159 | ``` 160 | psql -U postgres -d timezones -t -A -c " 161 | 162 | SELECT tzid, ST_AsText(ST_Simplify(ST_SnapToGrid(geom, 0.001), 0.3)) FROM timezones 163 | 164 | WHERE (ST_Area(geom) > 3 OR (gid IN ( 165 | 166 | SELECT MAX(gid) FROM timezones WHERE ST_Area(geom) <= 3 AND tzid NOT IN ( 167 | 168 | SELECT tzid FROM timezones WHERE ST_Area(geom) > 3 169 | 170 | ) group by tzid ORDER BY MAX(ST_AREA(geom)) 171 | 172 | ))) AND tzid != 'uninhabited'; 173 | 174 | " > tz_world.txt 175 | ``` 176 | 177 | And a special export for Islands that are hard to select otherwise. 178 | 179 | ``` 180 | psql -U postgres -d timezones -t -A -c " 181 | SELECT tzid, ST_Expand(ST_Extent(geom), GREATEST(3 - ST_Area(ST_Extent(geom)), 0)) FROM timezones 182 | 183 | WHERE ST_Area(geom) < 3 AND (tzid LIKE 'Pacific/%' OR tzid LIKE 'Indian/%' OR tzid LIKE 'Atlantic/%') GROUP BY tzid ORDER BY tzid; 184 | " > tz_islands.txt 185 | ``` 186 | 187 | LICENSE 188 | --------- 189 | 190 | Copyright 2011-2013 Nathan Haug 191 | 192 | Released under the MIT License. 193 | 194 | -------------------------------------------------------------------------------- /images/blue-marble-1280.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/blue-marble-1280.jpg -------------------------------------------------------------------------------- /images/custom.php: -------------------------------------------------------------------------------- 1 | 'png', 5 | 'blue-marble' => 'jpg', 6 | 'living' => 'jpg', 7 | 'night-electric' => 'jpg', 8 | ); 9 | $width = isset($_GET['w']) ? min((int) $_GET['w'], 1280) : 600; 10 | $height = round($width / 2); 11 | 12 | if (isset($_GET['base']) && isset($bases[$_GET['base']])) { 13 | $base = $_GET['base']; 14 | $extension = $bases[$_GET['base']]; 15 | } 16 | else { 17 | $base = reset(array_keys($bases)); 18 | $extension = reset($bases); 19 | } 20 | $source = $base . '-1280.' . $extension; 21 | $open_extension = str_replace('jpg', 'jpeg', $extension); 22 | $open_func = 'imagecreatefrom' . $open_extension; 23 | 24 | $im = $open_func($source); 25 | if (!$im) { 26 | return FALSE; 27 | } 28 | 29 | list($original_width, $original_height) = getimagesize($source); 30 | 31 | $res = imagecreatetruecolor($width, $height); 32 | if ($extension == 'png') { 33 | $transparency = imagecolorallocatealpha($res, 0, 0, 0, 127); 34 | imagealphablending($res, FALSE); 35 | imagefilledrectangle($res, 0, 0, $width, $height, $transparency); 36 | imagealphablending($res, TRUE); 37 | imagesavealpha($res, TRUE); 38 | } 39 | elseif ($extension == 'gif') { 40 | // If we have a specific transparent color. 41 | $transparency_index = imagecolortransparent($im); 42 | if ($transparency_index >= 0) { 43 | // Get the original image's transparent color's RGB values. 44 | $transparent_color = imagecolorsforindex($im, $transparency_index); 45 | // Allocate the same color in the new image resource. 46 | $transparency_index = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); 47 | // Completely fill the background of the new image with allocated color. 48 | imagefill($res, 0, 0, $transparency_index); 49 | // Set the background color for new image to transparent. 50 | imagecolortransparent($res, $transparency_index); 51 | // Find number of colors in the images palette. 52 | $number_colors = imagecolorstotal($im); 53 | // Convert from true color to palette to fix transparency issues. 54 | imagetruecolortopalette($res, TRUE, $number_colors); 55 | } 56 | } 57 | imagecopyresampled($res, $im, 0, 0, 0, 0, $width, $height, $original_width, $original_height); 58 | 59 | header('Content-Type: image/' . $extension); 60 | header('Cache-Control: public, max-age: 3600'); 61 | 62 | $close_function = 'image' . $open_extension; 63 | $close_function($res); 64 | 65 | imagedestroy($res); 66 | imagedestroy($im); 67 | -------------------------------------------------------------------------------- /images/gray-1280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/gray-1280.png -------------------------------------------------------------------------------- /images/gray-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/gray-400.png -------------------------------------------------------------------------------- /images/gray-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/gray-600.png -------------------------------------------------------------------------------- /images/gray-800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/gray-800.png -------------------------------------------------------------------------------- /images/living-1280.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/living-1280.jpg -------------------------------------------------------------------------------- /images/map-outlines.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/map-outlines.psd -------------------------------------------------------------------------------- /images/night-electric-1280.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/night-electric-1280.jpg -------------------------------------------------------------------------------- /images/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quicksketch/timezonepicker/884950ae874088cce2ed7003990959658e520b07/images/pin.png -------------------------------------------------------------------------------- /includes/parser.inc: -------------------------------------------------------------------------------- 1 | 'Gray', 10 | 'blue-marble' => 'Blue marble', 11 | 'night-electric' => 'Night Electric', 12 | 'living' => 'Living Earth', 13 | ); 14 | 15 | $base_url = 'http://' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['REQUEST_URI']), '/'); 16 | if (!empty($_GET['w']) && is_numeric($_GET['w']) && (int) $_GET['w'] < 1280) { 17 | $local_file = 'images/custom.php?w=' . (int) $_GET['w']; 18 | $local_file .= '&base=' . (isset($_GET['base']) && array_key_exists($_GET['base'], $bases) ? $_GET['base'] : reset($bases)); 19 | } 20 | else { 21 | $local_file = isset($_GET['src']) && isset($options[$_GET['src']]) ? $_GET['src'] : 'images/gray-600.png'; 22 | } 23 | 24 | $remote_file = $base_url . '/' . $local_file; 25 | 26 | // Dimensions must always be exact since the imagemap does not scale. 27 | list($map_width, $map_height) = getimagesize($remote_file); 28 | $timezones = timezone_picker_parse_files($map_width, $map_height, 'tz_world.txt', 'tz_islands.txt'); 29 | ?> 30 | 31 | 32 | 33 | 34 | jQuery and Imagemap Timezone Picker 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | 60 |
61 |
62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 74 | 75 |
76 | 77 | 78 |
79 | 80 | 81 | 82 | $timezone): ?> 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | 93 | 94 | 95 |
96 | Select your country and timezone 97 |
98 |
99 | 100 | 101 | 102 |
103 |
104 | 105 | 106 |
107 |
108 |
109 | 110 | 111 |
112 | 113 |

Timezone Picker jQuery and Imagemap Library

114 |
115 |

116 | Timezone Picker is intended to be a free use combination library of a jQuery 117 | library (jquery.timezone-picker.js) 118 | and an ImageMap format (as demonstrated here). 119 | 120 | To generate your ImageMap, simply choose an image width from above. All images 121 | are half-height of the width, in the standard 122 | 123 | Equirectangular projection. You should be able to use any variety of 124 | images in this standard format. 125 |

126 | 127 |

Features

128 |
    129 |
  • 400+ individual clickable areas
  • 130 |
  • Expanded island regions for easy selection
  • 131 |
  • Lightweight implementation (140KB)
  • 132 |
  • Completely standalone; no internet connection or APIs required
  • 133 |
  • Universally compatible (IE6+, Firefox, Chrome, Safari, Opera, mobile browsers)
  • 134 |
  • Geolocation when available (IE9+, and all the above)
  • 135 |
  • Mobile and Touch event friendly
  • 136 |
  • Automatic select list updating, both country and timezone
  • 137 |
  • Timezone pin locations
  • 138 |
139 | 140 |

Recommended Use

141 |

142 | Generation of the ImageMap files can be rather computationally expensive, 143 | especially from the raw data files. It's therefor recommended that you simply 144 | copy/paste the entire ImageMap from the source code, rather than 145 | generating the ImageMap code on your own. However, please do not attempt 146 | to hotlink images or files from TimezonePicker.com. Just copy/paste the 147 | pieces, it's all meant to work independently of an external internet 148 | connections, making it perfect for offline, intranet, or secure websites. 149 |

150 | 151 |

152 | Alternatively, you may also use this data feed in JSON format to generate your 153 | own ImageMap data. Adjust the ?w= query string parameter as needed for your 154 | desired map dimensions. 155 | 156 |

159 |

160 | 161 |

On Mobile

162 |

163 | ImageMaps are not very friendy to adaptive/responsive designs. Because the 164 | coordinates are coded to a particular image width and height, they do not 165 | resize easily (though the library does support this, but it's slow). However, 166 | it's not recommended to decrease the size of the map for the mobile version 167 | anyway, because selection becomes more difficult on mobile due to the 168 | "touch behavior" required. Your finger is a lot less accurate than a mouse. 169 | Therefor it's recommended to use an "overflow: auto;" on the containing DIV, 170 | which adds a horizontal scrollbar to the map. This can keep the map the same 171 | size, while making it easier for mobile users to select the timezone. Of 172 | course they could also just use the "Detect" button. :) 173 |

174 | 175 |

Credits

176 | 183 | 184 |
185 | 186 | 187 | 188 |
189 |
190 | 191 | 192 | -------------------------------------------------------------------------------- /json.php: -------------------------------------------------------------------------------- 1 | 1280 ? 600 : (int) $_GET['w']; 8 | $map_height = round($map_width/2); 9 | $timezones = timezone_picker_parse_files($map_width, $map_height, 'tz_world.txt', 'tz_islands.txt'); 10 | 11 | header('Content-Type: application/json'); 12 | header('Cache-Control: public, max-age: 3600'); 13 | 14 | print json_encode($timezones, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); 15 | -------------------------------------------------------------------------------- /lib/jquery.maphilight.min.js: -------------------------------------------------------------------------------- 1 | (function(G){var B,J,C,K,N,M,I,E,H,A,L;B=document.namespaces;J=!!document.createElement("canvas").getContext;if(!(J||B)){G.fn.maphilight=function(){return this};return }if(J){E=function(O){return Math.max(0,Math.min(parseInt(O,16),255))};H=function(O,P){return"rgba("+E(O.substr(0,2))+","+E(O.substr(2,2))+","+E(O.substr(4,2))+","+P+")"};C=function(O){var P=G('').get(0);P.getContext("2d").clearRect(0,0,P.width,P.height);return P};var F=function(Q,O,R,P,S){P=P||0;S=S||0;Q.beginPath();if(O=="rect"){Q.rect(R[0]+P,R[1]+S,R[2]-R[0],R[3]-R[1])}else{if(O=="poly"){Q.moveTo(R[0]+P,R[1]+S);for(i=2;i').get(0)};K=function(P,S,T,W,O){var U,V,Q,R;U='';V=(W.stroke?'strokeweight="'+W.strokeWidth+'" stroked="t" strokecolor="#'+W.strokeColor+'"':'stroked="f"');Q='';if(S=="rect"){R=G('')}else{if(S=="poly"){R=G('')}else{if(S=="circ"){R=G('')}}}R.get(0).innerHTML=U+Q;G(P).append(R)};N=function(O){G(O).find("[name=highlighted]").remove()}}M=function(P){var O,Q=P.getAttribute("coords").split(",");for(O=0;O0)){return }if(W.hasClass("maphilighted")){var R=W.parent();W.insertBefore(R);R.remove();G(S).unbind(".maphilight").find("area[coords]").unbind(".maphilight")}T=G("
").css({display:"block",background:'url("'+this.src+'")',position:"relative",padding:0,width:this.width,height:this.height});if(a.wrapClass){if(a.wrapClass===true){T.addClass(G(this).attr("class"))}else{T.addClass(a.wrapClass)}}W.before(T).css("opacity",0).css(I).remove();if(G.browser.msie){W.css("filter","Alpha(opacity=0)")}T.append(W);V=C(this);G(V).css(I);V.height=this.height;V.width=this.width;Z=function(f){var c,d;d=L(this,a);if(!d.neverOn&&!d.alwaysOn){c=M(this);K(V,c[0],c[1],d,"highlighted");if(d.groupBy){var b;if(/^[a-zA-Z][\-a-zA-Z]+$/.test(d.groupBy)){b=S.find("area["+d.groupBy+'="'+G(this).attr(d.groupBy)+'"]')}else{b=S.find(d.groupBy)}var g=this;b.each(function(){if(this!=g){var h=L(this,a);if(!h.neverOn&&!h.alwaysOn){var e=M(this);K(V,e[0],e[1],h,"highlighted")}}})}if(!J){G(V).append("")}}};G(S).bind("alwaysOn.maphilight",function(){if(X){N(X)}if(!J){G(V).empty()}G(S).find("area[coords]").each(function(){var b,c;c=L(this,a);if(c.alwaysOn){if(!X&&J){X=C(W[0]);G(X).css(I);X.width=W[0].width;X.height=W[0].height;W.before(X)}c.fade=c.alwaysOnFade;b=M(this);if(J){K(X,b[0],b[1],c,"")}else{K(V,b[0],b[1],c,"")}}})});G(S).trigger("alwaysOn.maphilight").find("area[coords]").bind("mouseover.maphilight",Z).bind("mouseout.maphilight",function(b){N(V)});W.before(V);W.addClass("maphilighted")})};G.fn.maphilight.defaults={fill:true,fillColor:"000000",fillOpacity:0.2,stroke:true,strokeColor:"ff0000",strokeOpacity:1,strokeWidth:1,fade:true,alwaysOn:false,neverOn:false,groupBy:false,wrapClass:true,shadow:false,shadowX:0,shadowY:0,shadowRadius:6,shadowColor:"000000",shadowOpacity:0.8,shadowPosition:"outside",shadowFrom:false}})(jQuery); -------------------------------------------------------------------------------- /lib/jquery.timezone-picker.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | // We only support a single instance per call, so these variables are available 4 | // for all subsequent calls. 5 | var methods = {}; 6 | var opts = {}; 7 | var selectedTimezone = null; 8 | var imgElement = null; 9 | var mapElement = null; 10 | var $pin = null; 11 | 12 | // Gets called upon timezone change. 13 | // The expected method signature is changeHandler(timezoneName, countryName, offset). 14 | var changeHandler = null; 15 | 16 | methods.init = function(initOpts) { 17 | var $origCall = this; 18 | 19 | // Set the instance options. 20 | opts = $.extend({}, $.fn.timezonePicker.defaults, initOpts); 21 | selectedTimezone = opts.timezone; 22 | 23 | changeHandler = opts.changeHandler; 24 | 25 | return $origCall.each(function(index, item) { 26 | imgElement = item; 27 | mapElement = document.getElementsByName(imgElement.useMap.replace(/^#/, ''))[0]; 28 | 29 | // Wrap the img tag in a relatively positioned DIV for the pin. 30 | $(imgElement).wrap('
').parent().css({ 31 | position: 'relative', 32 | width: $(imgElement).width() + 'px' 33 | }); 34 | 35 | // Add the pin. 36 | if (opts.pinUrl) { 37 | $pin = $('').appendTo(imgElement.parentNode).css('display', 'none'); 38 | } 39 | else if (opts.pin) { 40 | $pin = $(imgElement).parent().parent().find(opts.pin).appendTo(imgElement.parentNode).css('display', 'none'); 41 | } 42 | 43 | // Main event handler when a timezone is clicked. 44 | $(mapElement).find('area').click(function() { 45 | var areaElement = this; 46 | // Enable the pin adjustment. 47 | if ($pin) { 48 | $pin.css('display', 'block'); 49 | var pinCoords = $(areaElement).attr('data-pin').split(','); 50 | var pinWidth = parseInt($pin.width() / 2); 51 | var pinHeight = $pin.height(); 52 | 53 | $pin.css({ 54 | position: 'absolute', 55 | left: (pinCoords[0] - pinWidth) + 'px', 56 | top: (pinCoords[1] - pinHeight) + 'px' 57 | }); 58 | } 59 | 60 | var timezoneName = $(areaElement).attr('data-timezone'); 61 | var countryName = $(areaElement).attr('data-country'); 62 | var offset = $(areaElement).attr('data-offset'); 63 | 64 | // Call the change handler 65 | if (typeof changeHandler === 'function') { 66 | changeHandler(timezoneName, countryName, offset); 67 | } 68 | 69 | // Update the target select list. 70 | if (opts.target) { 71 | if (timezoneName) $(opts.target).val(timezoneName); 72 | } 73 | if (opts.countryTarget) { 74 | if (countryName) $(opts.countryTarget).val(countryName); 75 | } 76 | 77 | return false; 78 | }); 79 | 80 | // Adjust the timezone if the target changes. 81 | if (opts.target) { 82 | $(opts.target).bind('change', function() { 83 | $origCall.timezonePicker('updateTimezone', $(this).val()); 84 | }); 85 | } 86 | 87 | // Adjust the timezone if the countryTarget changes. 88 | if (opts.countryTarget && opts.countryGuess) { 89 | $(opts.countryTarget).bind('change', function() { 90 | var countryCode = $(this).val(); 91 | if (opts.countryGuesses[countryCode]) { 92 | $(mapElement).find('area[data-timezone="' + opts.countryGuesses[countryCode] + '"]').click() 93 | } 94 | else { 95 | $(mapElement).find('area[data-country=' + countryCode + ']:first').click(); 96 | } 97 | }); 98 | } 99 | 100 | // This is very expensive, so only run if enabled. 101 | if (opts.responsive) { 102 | var resizeTimeout = null; 103 | $(window).resize(function() { 104 | if (resizeTimeout) { 105 | clearTimeout(resizeTimeout); 106 | } 107 | resizeTimeout = setTimeout(function() { 108 | $origCall.timezonePicker('resize'); 109 | }, 200); 110 | }); 111 | } 112 | 113 | // Give the page a slight time to load before selecting the default 114 | // timezone on the map. 115 | setTimeout(function() { 116 | if (opts.responsive && parseInt(imgElement.width) !== parseInt(imgElement.getAttribute('width'))) { 117 | $origCall.timezonePicker('resize'); 118 | } 119 | else if (opts.maphilight && $.fn.maphilight) { 120 | $(imgElement).maphilight(opts); 121 | } 122 | if (opts.target) { 123 | $(opts.target).triggerHandler('change'); 124 | } 125 | }, 500); 126 | 127 | }); 128 | }; 129 | 130 | /** 131 | * Update the currently selected timezone and update the pin location. 132 | */ 133 | methods.updateTimezone = function(newTimezone) { 134 | selectedTimezone = newTimezone; 135 | $pin.css('display', 'none'); 136 | $(mapElement).find('area').each(function(m, areaElement) { 137 | if (areaElement.getAttribute('data-timezone') === selectedTimezone) { 138 | $(areaElement).triggerHandler('click'); 139 | return false; 140 | } 141 | }); 142 | 143 | return this; 144 | }; 145 | 146 | /** 147 | * Use browser geolocation to update the currently selected timezone and update the pin location. 148 | */ 149 | methods.detectLocation = function(detectOpts) { 150 | var detectDefaults = { 151 | success: undefined, 152 | error: undefined, 153 | complete: undefined 154 | }; 155 | detectOpts = $.extend(detectDefaults, detectOpts); 156 | 157 | if (navigator.geolocation) { 158 | navigator.geolocation.getCurrentPosition(showPosition, handleErrors); 159 | } 160 | 161 | function showPosition(position) { 162 | var $imgElement = $(imgElement); 163 | var imageXY = convertXY(position.coords.latitude, position.coords.longitude, $imgElement.width(), $imgElement.height()); 164 | 165 | $(mapElement).find('area').each(function(m, areaElement) { 166 | var coords = areaElement.getAttribute('coords').split(','); 167 | var shape = areaElement.getAttribute('shape'); 168 | var poly = []; 169 | for (var n = 0; n < coords.length/2; n++) { 170 | poly[n] = [ coords[n * 2], coords[n * 2 + 1] ]; 171 | } 172 | 173 | if ((shape === 'poly' && isPointInPoly(poly, imageXY[0], imageXY[1])) || 174 | (shape === 'rect' && isPointInRect(coords, imageXY[0], imageXY[1])) 175 | ) { 176 | $(areaElement).triggerHandler('click', detectOpts['success']); 177 | return false; 178 | } 179 | }); 180 | if (detectOpts['complete']) { 181 | detectOpts['complete'](position); 182 | } 183 | } 184 | 185 | function handleErrors(error) { 186 | if (detectOpts['error']) { 187 | detectOpts['error'](error); 188 | } 189 | if (detectOpts['complete']) { 190 | detectOpts['complete'](error); 191 | } 192 | } 193 | 194 | // Converts lat and long into X,Y coodinates on a Equirectangular map. 195 | function convertXY(latitude, longitude, map_width, map_height) { 196 | var x = Math.round((longitude + 180) * (map_width / 360)); 197 | var y = Math.round(((latitude * -1) + 90) * (map_height / 180)); 198 | return [x, y]; 199 | } 200 | 201 | // Do a dual-check here to ensure accuracy. Ray-tracing algorithm gives us the 202 | // basic idea of if we're in a polygon, but may be inaccurate if the ray goes 203 | // through a single point exactly at its vertex. We double check positives 204 | // against a bounding box, ensuring the item is actually in that area. 205 | function isPointInPoly(poly, x, y){ 206 | var inside = false; 207 | var bbox = [1000000,1000000,-1000000,-1000000]; 208 | for (var i = 0, j = poly.length - 1; i < poly.length; j = i++) { 209 | var xi = poly[i][0], yi = poly[i][1]; 210 | var xj = poly[j][0], yj = poly[j][1]; 211 | bbox[0] = Math.min(bbox[0], xi); 212 | bbox[1] = Math.min(bbox[1], yi); 213 | bbox[2] = Math.max(bbox[2], xi); 214 | bbox[3] = Math.max(bbox[3], yi); 215 | 216 | var intersect = ((yi > y) != (yj > y)) 217 | && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); 218 | if (intersect) inside = !inside; 219 | } 220 | 221 | return inside && isPointInRect(bbox, x, y); 222 | } 223 | 224 | // Simple check if a point is in between two X/Y coordinates. Input may be 225 | // any two points, with a box made between them. 226 | function isPointInRect(rect, x, y) { 227 | // Adjust so we're always going top-left to lower-right. 228 | rect = [ 229 | Math.min(rect[0], rect[2]), 230 | Math.min(rect[1], rect[3]), 231 | Math.max(rect[0], rect[2]), 232 | Math.max(rect[1], rect[3]) 233 | ]; 234 | return (x >= rect[0] && x <= rect[2] && y >= rect[1] && y <= rect[2]); 235 | } 236 | 237 | return this; 238 | }; 239 | 240 | /** 241 | * Experimental method to rewrite the imagemap based on new image dimensions. 242 | * 243 | * This does not resize the image itself, it recalculates the imagemap to match 244 | * the current dimensions of the image. 245 | */ 246 | methods.resize = function() { 247 | $(mapElement).find('area').each(function(m, areaElement) { 248 | // Save the original coordinates for further resizing. 249 | if (!areaElement.originalCoords) { 250 | areaElement.originalCoords = { 251 | timezone: areaElement.getAttribute('data-timezone'), 252 | country: areaElement.getAttribute('data-country'), 253 | coords: areaElement.getAttribute('coords'), 254 | pin: areaElement.getAttribute('data-pin') 255 | }; 256 | } 257 | var rescale = imgElement.width/imgElement.getAttribute('width'); 258 | 259 | // Adjust the image size. 260 | $(imgElement).parent().css({ 261 | width: $(imgElement).width() + 'px' 262 | }); 263 | 264 | // Adjust the coords attribute. 265 | var originalCoords = areaElement.originalCoords.coords.split(','); 266 | var newCoords = new Array(); 267 | for (var j = 0; j < originalCoords.length; j++) { 268 | newCoords[j] = Math.round(parseInt(originalCoords[j]) * rescale); 269 | } 270 | areaElement.setAttribute('coords', newCoords.join(',')); 271 | 272 | // Adjust the pin coordinates. 273 | var pinCoords = areaElement.originalCoords.pin.split(','); 274 | pinCoords[0] = Math.round(parseInt(pinCoords[0]) * rescale); 275 | pinCoords[1] = Math.round(parseInt(pinCoords[1]) * rescale); 276 | areaElement.setAttribute('data-pin', pinCoords.join(',')); 277 | 278 | // Fire the change handler on the target. 279 | if (opts.target) { 280 | $(opts.target).triggerHandler('change'); 281 | } 282 | 283 | }); 284 | 285 | return this; 286 | }; 287 | 288 | $.fn.timezonePicker = function(method) { 289 | // Method calling logic. 290 | if (methods[method]) { 291 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 292 | } 293 | else if (typeof method === 'object' || !method) { 294 | return methods.init.apply(this, arguments); 295 | } 296 | else { 297 | $.error('Method ' + method + ' does not exist on jQuery.timezonePicker'); 298 | } 299 | }; 300 | 301 | $.fn.timezonePicker.defaults = { 302 | // Selector for the pin that should be used. This selector only works in the 303 | // immediate parent of the image map img tag. 304 | pin: '.timezone-pin', 305 | // Specify a URL for the pin image instead of using a DOM element. 306 | pinUrl: null, 307 | // Preselect a particular timezone. 308 | timezone: null, 309 | // Pass through options to the jQuery maphilight plugin. 310 | maphilight: true, 311 | // Selector for the select list, textfield, or hidden to update upon click. 312 | target: null, 313 | // Selector for the select list, textfield, or hidden to update upon click 314 | // with the specified country. 315 | countryTarget: null, 316 | // If changing the country should use the first timezone within that country. 317 | countryGuess: true, 318 | // A list of country guess exceptions. These should only be needed if a 319 | // country spans multiple timezones. 320 | countryGuesses: { 321 | 'AU': 'Australia/Sydney', 322 | 'BR': 'America/Sao_Paulo', 323 | 'CA': 'America/Toronto', 324 | 'CN': 'Asia/Shanghai', 325 | 'ES': 'Europe/Madrid', 326 | 'MX': 'America/Mexico_City', 327 | 'RU': 'Europe/Moscow', 328 | 'US': 'America/New_York' 329 | }, 330 | // If this map should automatically adjust its size if scaled. Note that 331 | // this can be very expensive computationally and will likely have a delay 332 | // on resize. The maphilight library also is incompatible with this setting 333 | // and will be disabled. 334 | responsive: false, 335 | 336 | // Default options passed along to the maphilight plugin. 337 | fade: false, 338 | stroke: true, 339 | strokeColor: 'FFFFFF', 340 | strokeOpacity: 0.4, 341 | fillColor: 'FFFFFF', 342 | fillOpacity: 0.4, 343 | groupBy: 'data-offset' 344 | }; 345 | 346 | })(jQuery); 347 | -------------------------------------------------------------------------------- /lib/jquery.timezone-picker.min.js: -------------------------------------------------------------------------------- 1 | (function($){var methods={};var opts={};var selectedTimzone=null;var imgElement=null;var mapElement=null;var $pin=null;var changeHandler=null;methods.init=function(initOpts){var $origCall=this;opts=$.extend({},$.fn.timezonePicker.defaults,initOpts);selectedTimzone=opts.timezone;changeHandler=opts.changeHandler;return $origCall.each(function(index,item){imgElement=item;mapElement=document.getElementsByName(imgElement.useMap.replace(/^#/,""))[0];$(imgElement).wrap('
').parent().css({position:"relative",width:$(imgElement).width()+"px"});if(opts.pinUrl){$pin=$('').appendTo(imgElement.parentNode).css("display","none")}else if(opts.pin){$pin=$(imgElement).parent().parent().find(opts.pin).appendTo(imgElement.parentNode).css("display","none")}$(mapElement).find("area").click(function(){var areaElement=this;if($pin){$pin.css("display","block");var pinCoords=$(areaElement).attr("data-pin").split(",");var pinWidth=parseInt($pin.width()/2);var pinHeight=$pin.height();$pin.css({position:"absolute",left:pinCoords[0]-pinWidth+"px",top:pinCoords[1]-pinHeight+"px"})}var timezoneName=$(areaElement).attr("data-timezone");var countryName=$(areaElement).attr("data-country");var offset=$(areaElement).attr("data-offset");if(typeof changeHandler==="function"){changeHandler(timezoneName,countryName,offset)}if(opts.target){if(timezoneName)$(opts.target).val(timezoneName)}if(opts.countryTarget){if(countryName)$(opts.countryTarget).val(countryName)}return false});if(opts.target){$(opts.target).bind("change",function(){$origCall.timezonePicker("updateTimezone",$(this).val())})}if(opts.countryTarget&&opts.countryGuess){$(opts.countryTarget).bind("change",function(){var countryCode=$(this).val();if(opts.countryGuesses[countryCode]){$(mapElement).find('area[data-timezone="'+opts.countryGuesses[countryCode]+'"]').click()}else{$(mapElement).find("area[data-country="+countryCode+"]:first").click()}})}if(opts.responsive){var resizeTimeout=null;$(window).resize(function(){if(resizeTimeout){clearTimeout(resizeTimeout)}resizeTimeout=setTimeout(function(){$origCall.timezonePicker("resize")},200)})}setTimeout(function(){if(opts.responsive&&parseInt(imgElement.width)!==parseInt(imgElement.getAttribute("width"))){$origCall.timezonePicker("resize")}else if(opts.maphilight&&$.fn.maphilight){$(imgElement).maphilight(opts)}if(opts.target){$(opts.target).triggerHandler("change")}},500)})};methods.updateTimezone=function(newTimezone){selectedTimzone=newTimezone;$pin.css("display","none");$(mapElement).find("area").each(function(m,areaElement){if(areaElement.getAttribute("data-timezone")===selectedTimzone){$(areaElement).triggerHandler("click");return false}});return this};methods.detectLocation=function(detectOpts){var detectDefaults={success:undefined,error:undefined,complete:undefined};detectOpts=$.extend(detectDefaults,detectOpts);if(navigator.geolocation){navigator.geolocation.getCurrentPosition(showPosition,handleErrors)}function showPosition(position){var $imgElement=$(imgElement);var imageXY=convertXY(position.coords.latitude,position.coords.longitude,$imgElement.width(),$imgElement.height());$(mapElement).find("area").each(function(m,areaElement){var coords=areaElement.getAttribute("coords").split(",");var shape=areaElement.getAttribute("shape");var poly=[];for(var n=0;ny!=yj>y&&x<(xj-xi)*(y-yi)/(yj-yi)+xi;if(intersect)inside=!inside}return inside&&isPointInRect(bbox,x,y)}function isPointInRect(rect,x,y){rect=[Math.min(rect[0],rect[2]),Math.min(rect[1],rect[3]),Math.max(rect[0],rect[2]),Math.max(rect[1],rect[3])];return x>=rect[0]&&x<=rect[2]&&y>=rect[1]&&y<=rect[2]}return this};methods.resize=function(){$(mapElement).find("area").each(function(m,areaElement){if(!areaElement.originalCoords){areaElement.originalCoords={timezone:areaElement.getAttribute("data-timezone"),country:areaElement.getAttribute("data-country"),coords:areaElement.getAttribute("coords"),pin:areaElement.getAttribute("data-pin")}}var rescale=imgElement.width/imgElement.getAttribute("width");$(imgElement).parent().css({width:$(imgElement).width()+"px"});var originalCoords=areaElement.originalCoords.coords.split(",");var newCoords=new Array;for(var j=0;j