r81586 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r81585‎ | r81586 | r81587 >
Date:04:23, 6 February 2011
Author:brion
Status:deferred (Comments)
Tags:
Comment:
Experimental TimeZonePicker extension; adds a world map w/ timezone selection helpers to Special:Preferences date & time tab.

Up to date with commit 53b3165 in my git workspace: http://www.gitorious.org/mediawiki-timezonepicker
http://www.mediawiki.org/wiki/Extension:TimeZonePicker
Modified paths:
  • /trunk/extensions/TimeZonePicker (added) (history)
  • /trunk/extensions/TimeZonePicker/README (added) (history)
  • /trunk/extensions/TimeZonePicker/TimeZonePicker.hooks.php (added) (history)
  • /trunk/extensions/TimeZonePicker/TimeZonePicker.php (added) (history)
  • /trunk/extensions/TimeZonePicker/resources (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/ext.tzpicker.css (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/ext.tzpicker.js (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/overlay-1440.png (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/overlay-720.png (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/world-1440.png (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/world-360.png (added) (history)
  • /trunk/extensions/TimeZonePicker/resources/world-720.png (added) (history)

Diff [purge]

Index: trunk/extensions/TimeZonePicker/TimeZonePicker.hooks.php
@@ -0,0 +1,45 @@
 2+<?php
 3+/**
 4+ * TimeZonePicker extension: hooks
 5+ * @copyright 2011 Brion Vibber <brion@pobox.com>
 6+ */
 7+
 8+class TimeZonePickerHooks {
 9+ /* Static Methods */
 10+
 11+ /**
 12+ * BeforePageDisplay hook
 13+ *
 14+ * Adds the modules to the page
 15+ *
 16+ * @param $out OutputPage output page
 17+ * @param $skin Skin current skin
 18+ */
 19+ public static function beforePageDisplay( $out, $skin ) {
 20+ $title = $out->getTitle();
 21+ if( $title->isSpecial( 'Preferences' ) ) {
 22+ $out->addModules('ext.tzpicker');
 23+ $out->addInlineScript("window.mw_ext_tzpicker_ZoneInfo=" .
 24+ FormatJson::encode(self::zoneInfo()));
 25+ }
 26+ return true;
 27+ }
 28+
 29+ /**
 30+ * Return a set of timezone information relevant to this joyful stuff :D
 31+ */
 32+ public static function zoneInfo() {
 33+ $zones = array();
 34+ $now = date_create();
 35+ foreach( timezone_identifiers_list() as $tz ) {
 36+ $zone = timezone_open( $tz );
 37+
 38+ $name = timezone_name_get( $zone );
 39+ $location = timezone_location_get( $zone );
 40+ $offset = timezone_offset_get( $zone, $now ) / 60; // convert seconds to minutes
 41+
 42+ $zones[] = array('name' => $name, 'offset' => $offset, 'location' => $location);
 43+ }
 44+ return $zones;
 45+ }
 46+}
Index: trunk/extensions/TimeZonePicker/resources/world-1440.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/TimeZonePicker/resources/world-1440.png
___________________________________________________________________
Added: svn:mime-type
147 + application/octet-stream
Index: trunk/extensions/TimeZonePicker/resources/world-360.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/TimeZonePicker/resources/world-360.png
___________________________________________________________________
Added: svn:mime-type
248 + application/octet-stream
Index: trunk/extensions/TimeZonePicker/resources/world-720.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/TimeZonePicker/resources/world-720.png
___________________________________________________________________
Added: svn:mime-type
349 + application/octet-stream
Index: trunk/extensions/TimeZonePicker/resources/ext.tzpicker.css
@@ -0,0 +1,76 @@
 2+/**
 3+ * TimeZonePicker extension
 4+ * @copyright 2011 Brion Vibber <brion@pobox.com>
 5+ */
 6+
 7+#mw-tzpicker {
 8+ width: 720px;
 9+ height: 558px;
 10+ overflow: auto;
 11+ overflow-x: hidden;
 12+}
 13+#mw-tzpicker-map {
 14+ position: relative; /* for the markers to do absolute against... */
 15+ /*
 16+ width: 360px;
 17+ height: 280px;
 18+ background: url('world-360.png');
 19+ */
 20+ width: 720px;
 21+ height: 558px;
 22+ background: url('world-720.png');
 23+}
 24+#mw-tzpicker-overlay {
 25+ position: absolute;
 26+ width: 720px;
 27+ height: 558px;
 28+ background: url('overlay-720.png');
 29+}
 30+
 31+#mw-tzpicker-map.zoom {
 32+ position: relative; /* for the markers to do absolute against... */
 33+ width: 720px;
 34+ height: 1116px;
 35+ background: url('world-1440.png') -360px 0px;
 36+}
 37+#mw-tzpicker-map.zoom #mw-tzpicker-overlay {
 38+ position: absolute;
 39+ width: 720px;
 40+ height: 1116px;
 41+ background: url('overlay-1440.png') -360px 0px;
 42+}
 43+
 44+.mw-tzpicker-label {
 45+ position: absolute;
 46+ color: white;
 47+ background: black;
 48+ z-index: 10000;
 49+ padding: 0px 6px;
 50+
 51+ -moz-border-radius: 4px;
 52+ -webkit-border-radius: 4px;
 53+ -ie-border-radius: 4px;
 54+ -opera-border-radius: 4px;
 55+ border-radius: 4px;
 56+
 57+ box-shadow: 2px 2px 4px #888;
 58+}
 59+.mw-tzpicker-marker {
 60+ position: absolute;
 61+ width: 8px;
 62+ height: 8px;
 63+ color: white;
 64+ background-color: darkred;
 65+ z-index: 5000;
 66+}
 67+.mw-tzpicker-marker.selected {
 68+ background-color: red !important;
 69+ -moz-box-shadow: 1px 1px 4px #f88;
 70+ -webkit-box-shadow: 1px 1px 4px #f88;
 71+ box-shadow: 1px 1px 4px #f88;
 72+ z-index: 9999;
 73+}
 74+.mw-tzpicker-marker.far {
 75+ background-color: #978b93;
 76+ z-index: 2000;
 77+}
Index: trunk/extensions/TimeZonePicker/resources/ext.tzpicker.js
@@ -0,0 +1,312 @@
 2+/**
 3+ * TimeZonePicker extension
 4+ * @copyright 2011 Brion Vibber <brion@pobox.com>
 5+ */
 6+
 7+(function($, mw) {
 8+
 9+// Run our setup after everything's done so we have DOM and data ready :D
 10+$(function() {
 11+
 12+var mapState = {
 13+ nearest: null,
 14+ selected: null,
 15+ offset: 0,
 16+ mapOffset: 0,
 17+ mouseThreshold: 50,
 18+ zoom: 1,
 19+ points: []
 20+};
 21+
 22+var hhmmToMinutes = function(hhmm) {
 23+ var matches = /^\s*([+-]?)(\d+)(?::(\d\d))?\s*$/.exec(hhmm);
 24+ if (!matches) {
 25+ return 0;
 26+ }
 27+
 28+ var plusminus = matches[1];
 29+ var hours = parseInt(matches[2], 10) || 0;
 30+ var mins = parseInt(matches[3], 10) || 0;
 31+
 32+ var mult = (plusminus == '-') ? -1 : 1;
 33+ var minutes = mult * (hours * 60 + mins);
 34+ return minutes;
 35+};
 36+
 37+/**
 38+ * Pull just the zones that match our current offset.
 39+ *
 40+ * @param localOffset: int
 41+ * @return Array of TZ info objects
 42+ */
 43+var matchingZones = function(localOffset) {
 44+ // A list of {name, offset, location}
 45+ // location is {country_code, latitude, longitude, comments}
 46+ // @fixme read these via AJAX so we only have to ask for matching ones?
 47+ var zoneInfo = window.mw_ext_tzpicker_ZoneInfo;
 48+ //return zoneInfo; // uncomment this to try showing all zones. not yet ideal
 49+
 50+ var zones = [];
 51+ $.each(zoneInfo, function() {
 52+ if (this.offset == localOffset) {
 53+ zones.push(this);
 54+ }
 55+ });
 56+ return zones;
 57+};
 58+
 59+var plotLabel = function( marker ) {
 60+ var labelNode = marker.data('label');
 61+ if (labelNode == null) {
 62+ var zone = marker.data('zone');
 63+ var sx = marker.data('sx');
 64+ var sy = marker.data('sy');
 65+
 66+ var label = $( '<div class="mw-tzpicker-label"></div>' );
 67+ label.text( zone.name );
 68+ label.data('zone', zone);
 69+ label.css('left', (sx + 16) + 'px');
 70+ label.css('top', (sy - 8) + 'px');
 71+ $('#mw-tzpicker-map').append(label);
 72+
 73+ marker.data('label', label[0]);
 74+ }
 75+};
 76+
 77+/**
 78+ * @param {object} zone
 79+ * @return DOMElement
 80+ */
 81+var plotZone = function( zone ) {
 82+ //var width = 360;
 83+ //var height = 280; //280;
 84+ //var centerX = 180;
 85+ //var centerY = 280 / 2;
 86+ var width = 720;
 87+ var height = 720;
 88+ var centerX = width / 2;
 89+ var centerY = 558 / 2;
 90+ var offset = mapState.mapOffset;
 91+
 92+ var lon = zone.location.longitude;
 93+ var lat = zone.location.latitude;
 94+ if (lat == -90.0) {
 95+ // hack for south pole
 96+ lat = -78.5; // fit it in the cut-off mercator :P
 97+ lon = 180;
 98+ }
 99+
 100+ // Normalize longitude for offset & wraparound
 101+ lon = lon - offset;
 102+ if (lon < -180) {
 103+ lon += 360;
 104+ } else if (lon > 180) {
 105+ lon -= 360;
 106+ }
 107+
 108+ // Mercator projection per http://en.wikipedia.org/wiki/Mercator_projection#Mathematics_of_the_projection
 109+ var lat_rad = lat * Math.PI / 180.0;
 110+ var x = lon / 180;
 111+ var y = 0.5 * Math.log(
 112+ (1 + Math.sin(lat_rad)) /
 113+ (1 - Math.sin(lat_rad))
 114+ );
 115+
 116+ var sx = ((x * width / 2.0) + centerX) * mapState.zoom;
 117+ var sy = ((y * height / -6.0) + centerY) * mapState.zoom;
 118+ if (mapState.zoom > 1) {
 119+ sx -= 360;
 120+ }
 121+
 122+ var marker = $('<div class="mw-tzpicker-marker"></div>')
 123+ .data('zone', zone)
 124+ .data('sx', sx)
 125+ .data('sy', sy)
 126+ .css('left', (sx - 4) + 'px')
 127+ .css('top', (sy - 4) + 'px');
 128+ if (parseInt(zone.offset) != parseInt(mapState.offset)) {
 129+ marker.addClass('far');
 130+ }
 131+ $('#mw-tzpicker-map').append(marker);
 132+
 133+ // hack hack
 134+ /*
 135+ if (className) {
 136+ plotLabel(marker);
 137+ }
 138+ */
 139+ mapState.points.push({
 140+ x: sx,
 141+ y: sy,
 142+ zone: zone,
 143+ marker: marker[0]
 144+ });
 145+ return marker[0];
 146+};
 147+
 148+var plotMatchingZones = function( localOffset, selectedName ) {
 149+ var map = $('#mw-tzpicker-map');
 150+ map.empty();
 151+ mapState.offset = localOffset;
 152+ mapState.points = [];
 153+
 154+ var overlay = $('<div id="mw-tzpicker-overlay"></div>');
 155+ var idealDegrees = localOffset * 360 / (24 * 60);
 156+ var idealPixels = idealDegrees * 2 * mapState.zoom;
 157+ if (mapState.zoom > 1) {
 158+ idealPixels += 360;
 159+ }
 160+ map.css('background-position', (-1 * idealPixels) + 'px 0');
 161+ mapState.mapOffset = idealDegrees;
 162+ map.append(overlay);
 163+
 164+ var zones = matchingZones(localOffset);
 165+ $.each(zones, function() {
 166+ var markerNode = plotZone(this);
 167+ if (selectedName == this.name) {
 168+ mapState.selected = markerNode;
 169+ $(markerNode).addClass('selected');
 170+ plotLabel($(markerNode));
 171+ }
 172+ });
 173+};
 174+
 175+var selectZoneByName = function(zoneName) {
 176+ $('#mw-input-wptimecorrection option').each(function() {
 177+ var data = $(this).val().split('|');
 178+ if( data[0] == 'ZoneInfo' ) {
 179+ var offset = data[1];
 180+ var name = data[2];
 181+ if (name == zoneName) {
 182+ $(this).attr('selected', true);
 183+ $('#mw-input-wptimecorrection').change();
 184+ }
 185+ }
 186+ });
 187+};
 188+
 189+var plotLocalZones = function() {
 190+ var localClock = new Date();
 191+
 192+ // Note that Date.getTimezoneOffset returns the inverse of what we use
 193+ // elsewhere: UTC+1:00 gives -60 minutes. Convert back to match ZoneInfo...
 194+ var localOffset = -1 * localClock.getTimezoneOffset();
 195+ plotMatchingZones(localOffset);
 196+};
 197+
 198+var findNearestMarker = function(x, y) {
 199+ var markers = $('#mw-tzpicker-map .mw-tzpicker-marker');
 200+ var nearest = null;
 201+ var min2 = mapState.mouseThreshold * mapState.mouseThreshold;
 202+
 203+ var points = mapState.points;
 204+ var n = points.length;
 205+ for (var i = 0; i < n; i++) {
 206+ var point = points[i];
 207+ var dx = point.x - x, dy = point.y - y;
 208+ var dx2 = dx * dx, dy2 = dy * dy;
 209+ if (dx2 > min2 || dy2 > min2) {
 210+ continue;
 211+ }
 212+ var dist2 = dx2 + dy2;
 213+ if (dist2 < min2) {
 214+ min2 = dist2;
 215+ nearest = point.marker;
 216+ }
 217+ }
 218+ return nearest;
 219+};
 220+
 221+// Maaaagic
 222+var setupTimezones = function() {
 223+ $('#mw-htmlform-timeoffset tbody')
 224+ .append('<tr class="mw-tzpicker-row"><td>' +
 225+ '<td class="mw-input">' +
 226+ '<div id="mw-tzpicker">' +
 227+ '<div id="mw-tzpicker-map"></div>' +
 228+ '</div>' +
 229+ '</td></tr>');
 230+
 231+ var map = $('#mw-tzpicker-map');
 232+ if (mapState.zoom > 1) {
 233+ map.addClass('zoom');
 234+ }
 235+ map.mousemove(function(e) {
 236+ var offset = map.offset();
 237+ var x = e.pageX - offset.left;
 238+ var y = e.pageY - offset.top;
 239+
 240+ var nearest = findNearestMarker(x, y, 100);
 241+ if (nearest) {
 242+ var marker = $(nearest);
 243+ plotLabel(marker);
 244+ marker.addClass('selected');
 245+ }
 246+ if (mapState.nearest && mapState.nearest != nearest && mapState.nearest != mapState.selected) {
 247+ var oldMarker = $(mapState.nearest);
 248+ oldMarker.removeClass('selected');
 249+ var oldLabel = oldMarker.data('label');
 250+ if (oldLabel) {
 251+ $(oldMarker).data('label', null);
 252+ $(oldLabel).remove();
 253+ }
 254+ }
 255+ mapState.nearest = nearest;
 256+ });
 257+ map.click(function(e) {
 258+ var offset = map.offset();
 259+ var x = e.pageX - offset.left;
 260+ var y = e.pageY - offset.top;
 261+
 262+ var nearest = findNearestMarker(x, y, 100);
 263+ if (nearest && nearest != mapState.selected) {
 264+ var marker = $(nearest);
 265+ var zone = marker.data('zone');
 266+ selectZoneByName(zone.name);
 267+ }
 268+ });
 269+ map.dblclick(function(e) {
 270+ if (mapState.zoom == 1) {
 271+ mapState.zoom = 2;
 272+ map.addClass('zoom');
 273+ } else if (mapState.zoom == 2) {
 274+ mapState.zoom = 1;
 275+ map.removeClass('zoom');
 276+ }
 277+ if (mapState.selected) {
 278+ var zone = $(mapState.selected).data('zone');
 279+ plotMatchingZones(zone.offset, zone.name);
 280+ } else {
 281+ plotLocalZones();
 282+ }
 283+ });
 284+
 285+ var selector = $('#mw-input-wptimecorrection');
 286+ var ping = function() {
 287+ var data = $(this).val().split('|');
 288+ if( data[0] == 'ZoneInfo' ) {
 289+ var offset = data[1];
 290+ var name = data[2];
 291+ plotMatchingZones(offset, name);
 292+ } else if (data[0] == 'guess') {
 293+ plotLocalZones();
 294+ } else {
 295+ var hhmm = $('#mw-input-wptimecorrection-other').val();
 296+ var minutes = hhmmToMinutes(hhmm);
 297+ plotMatchingZones(minutes);
 298+ }
 299+ };
 300+ selector.change(function() {
 301+ // horrible hack! slight delay to get the server offset
 302+ window.setTimeout(function() {
 303+ ping.call(selector[0]);
 304+ }, 50);
 305+ });
 306+ ping.call(selector[0]);
 307+};
 308+
 309+setupTimezones();
 310+
 311+});
 312+
 313+})(jQuery, mediaWiki);
Index: trunk/extensions/TimeZonePicker/resources/overlay-1440.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/TimeZonePicker/resources/overlay-1440.png
___________________________________________________________________
Added: svn:mime-type
1314 + application/octet-stream
Index: trunk/extensions/TimeZonePicker/resources/overlay-720.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/TimeZonePicker/resources/overlay-720.png
___________________________________________________________________
Added: svn:mime-type
2315 + application/octet-stream
Index: trunk/extensions/TimeZonePicker/README
@@ -0,0 +1,32 @@
 2+This extension adds an experimental time zone picker map to Special:Preference's date/time panel.
 3+
 4+PHP's date-and-time functions pass through latitude and longitude coordinates from the system
 5+timezone database, which we use here to plot on a map.
 6+
 7+To prune things down, only zone settings that match the currently selected offset are shown
 8+on the map; this lets us show what likely candidates are available that match either the
 9+configured server offset or the browser settings, then pick one from the map.
 10+
 11+Selecting a new setting (a specific zone or the server, guess, or other options) from the
 12+drop-down updates the map automatically, and centers the view on the selected offset.
 13+
 14+The map can be zoomed in 2x by double-clicking; this may require vertical scrolling in some
 15+instances.
 16+
 17+Todo:
 18+* pretty sure server default isn't being handled right if not UTC
 19+* clean up rambling code
 20+* find a cleaner way to get non-current items showing (can show them but it makes the map much busier)
 21+* redo the map refreshes so we're not removing and recreating everything when you click something already on the map!
 22+* make map zoom work on iPad (?)
 23+* show the TZ offset in HH:MM clearly at the top of the map, with selectors to jump over to other zones for browsing :D
 24+* South Pole entry is still a little off
 25+* scroll automatically in zoomed mode
 26+* clearer controls for zoom
 27+* (perhaps) use geolocation info to help pick something
 28+
 29+Note on the location information: we do *not* have boundaries available, and even if we did it wouldn't
 30+be wise to use them as borders are in dispute in a number of countries. Available locations are for the
 31+cities that are representative.
 32+
 33+The map has a shading overlay that highlights a fairly generic range that approximates the size of a time zone.
Index: trunk/extensions/TimeZonePicker/TimeZonePicker.php
@@ -0,0 +1,42 @@
 2+<?php
 3+
 4+/**
 5+ * Experimental next-gen timezone picker
 6+ * http://www.mediawiki.org/wiki/Extension:TimeZonePicker
 7+ *
 8+ * @copyright 2011 Brion Vibber <brion@pobox.com>
 9+ *
 10+ * MediaWiki-side code is GPL v2 or later.
 11+ *
 12+ * World map image based on http://commons.wikimedia.org/wiki/File:Mercator-projection.jpg
 13+ * Source image is from NASA's Earth Observatory "Blue Marble" series. (Public domain)
 14+ */
 15+
 16+$wgExtensionCredits['other'][] = array(
 17+ 'path' => __FILE__,
 18+ 'name' => 'TimeZonePicker',
 19+ 'author' => array( 'Brion Vibber' ),
 20+ 'url' => 'http://www.mediawiki.org/wiki/Extension:TimeZonePicker',
 21+);
 22+$wgExtensionMessagesFiles['SVGEdit'] = dirname(__FILE__) . '/SVGEdit.i18n.php';
 23+
 24+$wgHooks['BeforePageDisplay'][] = 'TimeZonePickerHooks::beforePageDisplay';
 25+
 26+$wgAutoloadClasses['TimeZonePickerHooks'] = dirname( __FILE__ ) . '/TimeZonePicker.hooks.php';
 27+
 28+$myResourceTemplate = array(
 29+ 'localBasePath' => dirname( __FILE__ ) . '/resources',
 30+ 'remoteExtPath' => 'TimeZonePicker/resources',
 31+ 'group' => 'ext.tzpicker',
 32+);
 33+$wgResourceModules['ext.tzpicker'] = $myResourceTemplate + array(
 34+ 'scripts' => array(
 35+ 'ext.tzpicker.js',
 36+ ),
 37+ 'styles' => array(
 38+ 'ext.tzpicker.css',
 39+ ),
 40+ 'dependencies' => array(
 41+ 'mediawiki.special.preferences'
 42+ )
 43+);
Property changes on: trunk/extensions/TimeZonePicker
___________________________________________________________________
Added: svn:ignore
144 + .git

Follow-up revisions

RevisionCommit summaryAuthorDate
r81594Fixup SVN autoprops from r81586reedy13:41, 6 February 2011
r81595More svn props fro r81586reedy13:44, 6 February 2011
r81614followup r81586: compress images with pngoutvyznev22:52, 6 February 2011

Comments

#Comment by Hashar (talk | contribs)   22:18, 6 February 2011

Are those PNG pngcrushed?

#Comment by Brion VIBBER (talk | contribs)   22:20, 6 February 2011

I'm sure they can be much more optimized and/or replaced.

#Comment by Hashar (talk | contribs)   22:20, 6 February 2011

I really like this extension brion. Do we care that much about antartica? You could probably remove it.

#Comment by Brion VIBBER (talk | contribs)   22:21, 6 February 2011

There are timezones in the database for the south pole and various research stations in Antarctica. True fact! :)

However smarter zoom/scroll would allow not bothering to show it most of the time.

#Comment by Nikerabbit (talk | contribs)   15:47, 7 February 2011

Message file issue was fixed in r81618 (was not followed-up).

Status & tagging log