2015

Deeplink to a sub-page within a Facebook iframe app

22nd May, 2015 - Posted by david

Facebook have long had the ability for developers to write custom apps and embed them as tabs in people’s or company’s Facebook profile pages. What I’m talking about here is when you write your own HTML app and host it at e.g. https://fb.mysite.com, which is then embedded into profile pages via an iframe. These apps then have URLs like https://www.facebook.com/profile.site/app_123456789, where 123456789 is your app ID within Facebook.

I’ve written one such app, in PHP, which has sub-pages, so while the root is https://fb.mysite.com, using the app will call pages such as https://fb.mysite.com/product/1. Seeing as this is within an iframe, the URL within the browser remains at https://www.facebook.com/profile.site/app_123456789 while you browse around the app. I recently had a request from a client on how they could link to a sub-page from within a post on their profile page, so they wanted to post something like ‘Check out my product at <link>’, where clicking on the link will load up the iframe app and bring them to the specific product. This is achievable, but it’s not exactly straight-forward and requires some work on the developers behalf. In an ideal world the link would simply be https://www.facebook.com/profile.site/app_123456789/product/1!

The way I managed to achieve this was using Facebook’s app_data parameter. Here, you can pass any data you want to the variable and it’ll end up as part of the signed_request variable in $_REQUEST at https://fb.mysite.com/index.php . The way we’re going to structure these deeplinks is to pass a JSON object to app_data containing a page key, with the sub-page we want, in this instance products/1, so our deeplink is going to look like https://www.facebook.com/profile.site/app_123456789?app_data={page:products/1} . Not exactly elegant but it’ll have to do! You could simply set app_data to products/1, but there may come a time when you want to pass other data in addition to the page, so I opted to go down the JSON route.

Now that we know what to expect, we need to decode $_REQUEST['signed_request'] (which should be available to your https://fb.mysite.com/index.php), json_decode app_data from the result, validate page, then redirect the browser accordingly.

To decode $_REQUEST['signed_request'] I used Facebook’s PHP SDK. Once we have the signed request as an array, we decode the JSON from app_data. Then, we check for the presence of page, validate it (I’ll leave the validation code up to yourself!) and send them on their way. This is pretty straight-forward, so is probably best illustrated with some code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.php
require_once 'base_facebook.php';
$fb = new Facebook(array(
    'appId'  => 'YOUR_APP_ID',
    'secret' => 'YOUR_APP_SECRET',
));
$req = $fb->getSignedRequest(); // parses $_REQUEST['signed_request']
$app_data = json_decode($req['app_data'], true); // true to ensure an associative array
if (!empty($app_data['page']) && valid_page($app_data['page'])) {
    header('Location: /'.$app_data['page']);
    exit;
}

// rest of regular index.php

Pity it’s not more straight-forward, but this is the way I got it to work. Hope this helps!

Read more...

Phantom rewrite rule in Apache

7th March, 2015 - Posted by david

TL;DR The MultiViews option in Apache automatically will map e.g. /xfz/ to /xyz.php

I was recently creating a new section of the website I work for and decided to opt for tidy URLs, for SEO purposes, instead of our standard.long?url=format URLs that we have elsewhere. Let’s say the new section I was creating was called David’s Boxes, so I wanted to have relative URLs like /davids-boxes/big/blue map to davids-boxes.php?size=big&colour=blue. Purely co-incidentally, there happened to be a defunct davids-boxes folder in our www directory, which contained an old WordPress install, which I prompty deleted (more on this later). Then, I set up rewrite rules in our www/.htacess to do the example mapping above.

Everything was working fine locally: /davids-boxes/ matched to /davids-boxes.php and /davids-boxes/big/blue mapped to /davids-boxes.php?size=big&bolour=blue, all as expected. However, when I put the .htaccess file onto our test server, I couldn’t get the rules to match properly: everything mapped to the basic /davids-boxes.php, i.e. with no extra GET parameters. I tried different order of rules, moving the rules to the top of the .htaccess etc., but nothing worked. Then I simply deleted the rules from the .htaccess, expecting /davids-boxes/ not to map to anything, but it still strangely mapped to /davids.boxes.php as before. This led me to believe there was another rewrite rule somewhere else (a fact that was also helped by the previous WordPress install). Searching the entire codebase, which includes all ‘sub-‘.htaccess files, yielded no results, so then I began thinking it might be the server…

I had a look in our sites-available Apache configs, expecting there may be some sort of obvious generic rewrite to map any e.g. /xyz/ to xyz.php; no such luck. Going through each line in the config, I noticed we had the FollowSymLinks and MultiViews options enabled in the <Directory> tag. I was familiar with the former, but not the latter. Investigating into MultiViews, it turns out this was the thing doing the automatic mapping I was experiencing! The documentation states “if /some/dir has MultiViews enabled, and /some/dir/foo does not exist, then the server reads the directory looking for files named foo.*, and effectively fakes up a type map which names all those files”. Such relief to figure it out. I checked with our CTO, he didn’t know how it got there, so after removing it on testing and doing a quick test, we got rid of it everywhere and my problems were solved.

Read more...

How to call some Pixastic functions

21st February, 2015 - Posted by david

I was doing a bit of work with Canvas recently, manipulating images in the browser and writing the results out to files. I was looking for a package that could do various effects on the images, such as sharpen, blur etc. and came across the Pixastic package used in similar applications. However, unfortunately, the website for the package is currently down and I couldn’t find much documentation on it anywhere. So, I had to look at the source code to figure out how to call the various functions. Not the end of the world, but I just thought I’d stick some simple examples here, to maybe help get others started with the package and to direct them to the source code for more information!

I got the source code from JSdeilvr.net, which is minified, but there is unminified source on GitHub.

The 3 functions I was looking to use were sharpen, brighten/darken and blur. I’ll go through each individually. Firstly though, I’ll mention that the first parameter to each function is a Javascript Image object, which obviously has a src attribute with your image’s contents. The whole point of this was that I was building a tool with buttons which, when clicked, would perform each of the functions above on the current Image. When applying a filter, it’s best track the current level of e.g. brightness, increase/decrease this value, reset the image to how it originally looked and then apply the filter with the new value. This is better than say brightening it by 10%, then brightening the result by 10% again.

Seeing as I used brightness for the example above, I’ll start with that. Also, to darken an image, you simply reduce the brightness value, obviously.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// initial code setup
var img = new Image();
img.src = 'whatever';
img.original_src = img.src; // for doing the resets
// add the current value of each filter to the Image
$.extend(img, {brightness: 0, sharpen: 0, blur: 0});

// ...

// 'Brighten' button click handler
$('#brighten').click(function() {
    img.brightness += 2; // brighten by 2 (for darken, reduce by an amount, will work with negative values)
    // max brightness of 255
    if (img.brightness > 255) img.brightness = 255;

    // now we reset the image by creating a copy
    img.src = img.original_src;
    // now, apply the filter
    img = Pixastic.process(img, "brightness", {
        brightness : img.brightness
    });
});

For sharpen, I found it best not to sharpen by the same amount each time, it just didn’t lead to nice results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$('#sharpen').click(function() {
    if (img.sharpen < 0.5){
        img.sharpen += 0.1;
    }
    else if (img.sharpen > 0.75){
        img.sharpen += 0.01;
    }
    else {
        img.sharpen += 0.02;
    }
    if (img.sharpen < 0.88) {
        img.sharpen = 0.88;
    }

    // now we reset the image by creating a copy
    img.src = img.original_src;
    // now, apply the filter
    img = Pixastic.process(img, "sharpen", {
        amount : img.sharpen
    });
});

Lastly, blur was a bit more straightforward, increasing by 1 every time:

1
2
3
4
5
6
7
8
9
10
$('#blur').click(function() {
    img.blur++;

    // now we reset the image by creating a copy
    img.src = img.original_src;
    // now, apply the filter
    img = Pixastic.process(imageObjs[selected_image].image, "blur", {
        amount : img.blur
    });
});

As I’m sure you’ve spotted, there’s a certain amount of repetition in the code above, starting with the line “// now we reset…”. What I did was to write a function called applyFilters, which you call once you’ve calculated your new value for brightness/sharpen/blur. This will then reset the image and apply all 3 filters. With the code above, if you were to say brighten, then blur, only the blur would be applied, as the image is reset each time. Doing it this way removes that problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function applyFilters() {
    // reset the image
    img.src = img.original_src;

    // brighten
    img = Pixastic.process(img, "brightness", {
        brightness : img.brightness
    });

    // sharpen
    img = Pixastic.process(img, "sharpen", {
        amount : img.sharpen
    });

    // blur
    img = Pixastic.process(imageObjs[selected_image].image, "blur", {
        amount : img.blur
    });
}

// ... then your handlers can look something like, e.g.
$('#blur').click(function() {
    img.blur++;
    applyFilters();
});

So, that should be enough to get you working with Pixastic. As for working with Canvas, that’s another blog post!

Read more...

Homepage takeover with existing skyscraper ads

14th February, 2015 - Posted by david

Sometimes in work we can be asked to do things we don’t like and recently I was asked to look into implementing one of those homepage takovers. Personally, I think these are awful and would like to think I wouldn’t degrade my site by implementing one, but they do make money and have a high click rate, so I can see why sites like to use them.

Normally they’re done using a fixed background wallpaper that’s clickable all the way to the edge of the page. However, I was asked to simulate this look using 2 existing skyscraper ads, 170px in width, to be positioned either side of the main content and fixed to the top of the page. Since it wasn’t entirely straightforward, I thought I’d block about it here, to help anyone else in a similar situation. I’m not going to go into the specifics of displaying the ads, simply the CSS and Javascript involved in positioning them where you want them.

I should point out, this might be possible with just CSS, but changing a site’s fundamental structure to accomodate the new columns isn’t always possible. Also, you might only want the takeover on the homepage and not other pages. This solution should have minimal impact, as it simply adds 2 divs, that can go anywhere in the HTML.

So, to describe the set-up, let’s say our main content is 1000px in width, centred in the page and we want 2 170px x 1068px divs to contain our ads and line up on the right and left of that content, as well as for the 2 ads to remain fixed at the top of the page, no matter how far we scroll down. We’ll give each of these divs a class of side-banner, with left-banner and right-banner IDs. Since these are going to be positioned explicitly, it doesn’t really matter where in the HTML you put them, maybe just inside your main content div. Initially, we’re simply going to position them in the extreme top corners of each side. I’m also going to give them different background colours, so we can know they’re positioned correctly without having to worry about loading the ads (which can come later).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.side-banner {
    width: 170px;
    height: 1068px;
    position: fixed;
    top: 0;
}
#right-banner {
    right: 0;
    background: red;
}
#left-banner {
    left: 0;
    background: green;
}

To align these alongside the content, I needed to write a Javascript function (position_banners()) to position them correctly. This function is called when the page finishes loading, as well as when the window is resized. It simply gets the body’s width, subtracts the width of our main content (remember 1000px), divides the result by 2 (as we’ve 2 sides), then further subtracts the width of our banners. This fairly basic formula works out the amount to move each div in from their corresponding edge, to line up with our main content. Then, we just use CSS left and right to position them nicely.

1
2
3
4
5
6
7
8
9
10
11
12
$(document).ready(function() {
    position_banners();
    $(window).resize(position_banners);
});

function position_banners() {
    var margin = ($('body').width() - 1000) / 2 - $('#left-banner').width(),
        left = Math.floor(margin),
        right = Math.ceil(margin);
    $('#left-banner').css({left: left + 'px'});
    $('#right-banner').css({right: left + 'px'});
}

I know this code isn’t the tidiest, but should be enough to get the idea of what you need to do.

To further enhance the ‘takeover’ effect, you could display a 970px x 250px ‘billboard’ right at the top of your main content.

Read more...

Opening hours done right, in PHP and MySQL

7th February, 2015 - Posted by david

I often see different ways of storing and displaying opening hours for various businesses when browsing the web. Some seem to simply store them as an unstructured blob of plaintext and spit that back to the user. Others will store the exact times for each day and display all 7 days on 7 separate rows.

I think neither of these options are great and I’ve come up with what I think is the ideal solution. Basically, we want to store the exact time for each day, but group similar days together. So, you could have lines like ‘Mon – Thurs: 9 – 5’, but if it’s say Tuesday at 10am, you’d also know that the business is currently open. I also think you could have alternative text to display when the business is closed (indicated by open and closed being NULL), so you could have something like ‘Sat – Sun: Open by appointment’.

So, let’s start with our table layout, see below. The primary key is id, which is just your standard auto_increment value. business_id is if you have multiple businesses, each with their own opening hours, as was the case for me. You might want to build an index on this field too. If you’re just storing your own opening hours, dow could be the primary key and you could drop those last 2 fields. open and closed are just simple strings, to store the time in 24-hour ‘HH:MM’ format. Doing it this way, you can still to comparisons like WHERE open > '09:00' and get the result you were expecting.

1
2
3
4
5
6
7
8
9
10
+---------------+-----------------------+------+-----+---------+----------------+
| Field         | Type                  | Null | Key | Default | Extra          |
+---------------+-----------------------+------+-----+---------+----------------+
| id            | mediumint(9) unsigned | NO   | PRI | NULL    | auto_increment |
| business_id   | mediumint(8) unsigned | NO   | MUL | NULL    |                |
| dow           | tinyint(1) unsigned   | NO   |     | NULL    |                |
| open          | char(5)               | YES  |     | 09:00   |                |
| closed        | char(5)               | YES  |     | 17:30   |                |
| optional_text | char(100)             | YES  |     | NULL    |                |
+---------------+-----------------------+------+-----+---------+----------------+

Next up, I want to show you a quick function to format the time. Being a programmer, the time ’13:30′ is easily translated to ‘1.30pm’, but for the general public it might not be so simple, so this function will display your time in a more human readable format, such as the example given above. Basically, we want to drop any leading 0s, any 0-value minutes, convert the time to a 12-hour version with ‘am’ and ‘pm’ and change the ‘:’ to a ‘.’ (this last bit is probably a bit region-specific). For midnight, we’ll store that as ’24:00′ (which I know is technically the start of the next day!) and display that to the user as ‘midnight’, instead of the slightly confusing ‘0am’.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Function to change an opening hour to a more human readable version,
* e.g., 09:00 to 9am or 13:30 to 1.30pm
*
* @param   String  $time  Time to format, in HH:MM format
*
* @return  String         Formatted time
*
*/

function format_opening_hour($time) {
    if ($time == '24:00') {
        $new_time = 'midnight';
    }
    else {
        list($hours, $minutes) = explode(':', $time);
        $hours = ltrim($hours, '0');
        $am_pm = ($hours >= 12) ? 'pm' : 'am';
        if ($hours > 12) $hours -= 12;
        $new_time = $hours;
        if ($minutes != '00') {
            $new_time .= '.'.$minutes;
        }
        $new_time .= $am_pm;
    }
    return $new_time;
}

OK, so displaying the data in a nice table, with similar days grouped together, is the next bit. We’ll have 2 columns, one for the day/days, the other for the time. If a day has a value for optional_text, then that value will be displayed and the times are ignored. I’m also going to add another block of optional text ($extra_text below) that will be displayed at the end of the table and is applied for all days, to be used for something like ‘phone anytime’. Finally, there’s a $short_day_names option, so you can choose between say ‘Mon’ and ‘Monday’.

I should also mention at this point: I’m returning a block of HTML here from a function, as well as mixing business logic with display logic; I realise this is generally a bad idea and some of this could be split into a function and a template, but seeing as it’s a simple 2-column table, I just kept it all together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
 * Function to generate a simple html table for a business' opening hours
 *
 * @param   Array   $opening_hours    Array of rows from opening_hours table, sorted by dow (0-indexed, starting with Monday)
 * @param   String  $extra_text       Extra block of generic text that applies to all days, goes at end of table
 * @param   String  $short_day_names  Whether to use e.g. 'Mon' or 'Monday'
 *
 * @return  String                    HTML <table> output
 *
 */

function opening_hours_table($opening_hours, $extra_text='', $short_day_names=false) {
    $dow = array(
        array('long' => 'Monday', 'short' => 'Mon'),
        array('long' => 'Tuesday', 'short' => 'Tue'),
        array('long' => 'Wednesday', 'short' => 'Wed'),
        array('long' => 'Thursday', 'short' => 'Thu'),
        array('long' => 'Friday', 'short' => 'Fri'),
        array('long' => 'Saturday', 'short' => 'Sat'),
        array('long' => 'Sunday', 'short' => 'Sun')
    );
    $key = ($short_day_names) ? 'short' : 'long';

    // first, find similar days and group them together
    if (!empty($opening_hours)) {
        $opening_short = array();
        // start with current day
        for ($i=0; $i<7; $i++) {
            $temp = array($i);
            // try to find matching adjacent days
            for ($j=$i+1;$j<7;$j++) {
                if (empty($opening_hours[$i]['optional_text']) &&
                    empty($opening_hours[$j]['optional_text']) &&
                    $opening_hours[$i]['open'] == $opening_hours[$j]['open'] &&
                    $opening_hours[$i]['closed'] == $opening_hours[$j]['closed'] ||
                    !empty($opening_hours[$i]['optional_text']) &&
                    !empty($opening_hours[$j]['optional_text']) &&
                    strtolower($opening_hours[$i]['optional_text']) == strtolower($opening_hours[$j]['optional_text']) ) {
                    // we have a match, store the day
                    $temp[] = $j;
                    if ($j == 6) $i = 6; // edge case
                }
                else {
                    // otherwise, move on to the next day
                    $i = $j-1;
                    $j = 7; // break
                }
            }
            $opening_short[] = $temp; // $temp will be an array of matching days (possibly only 1 day)
        }
    }

    $html = '<table>';
    $colspan = '';

    if (!empty($opening_short)) {
        $colspan = ' colspan="2"';
        foreach ($opening_short as $os) {
            $day_text = $dow[$os[0]][$key];
            if (count($os) > 1) { // if there's another, adjacent day with the same time
                $end = array_pop($os); // get the last one
                $end = $dow[$end][$key];
                $day_text = $day_text . ' - ' . $end; // append the day to the string
            }
            // at this point, $day_text will be something like 'Monday' or 'Monday - Thursday'
            if (!empty($opening_hours[$os[0]]['optional_text'])) {
                // optional string takes precedent over any opening hours that may be set
                $hours_text = htmlentities($opening_hours[$os[0]]['optional_text']);
            }
            elseif (!empty($opening_hours[$os[0]]['open'])) {
                // otherwise generate something like '9am - 5.30pm'
                $hours_text = format_opening_hour($opening_hours[$os[0]]['open']) . ' - ' .format_opening_hour($opening_hours[$os[0]]['closed']);
            }
            else {
                // if nothing, it must be closed on that day/days
                $hours_text = 'Closed';
            }
            // new row for our table
            $html .= '<tr>
                <td>'
.$day_text.':</td>
                <td>'
.$hours_text.'</td>
            </tr>'
;
        }
    }

    // append the extra block of text at the end of the table
    if (!empty($extra_text)) {
        $html .= '<tr>
            <td'
.$colspan.'>'.htmlentities($extra_text).'</td>
        </tr>'
;
    }

    $html .= '</table>';
    return $html;
}

So, with the following data…

1
2
3
4
5
6
7
8
9
10
11
+-----+-------+--------+----------------+
| dow | open  | closed | extra          |
+-----+-------+--------+----------------+
|   0 | 09:00 | 17:30  | NULL           |
|   1 | 09:00 | 17:30  | NULL           |
|   2 | 09:00 | 17:30  | NULL           |
|   3 | 09:30 | 19:00  | NULL           |
|   4 | 09:30 | 19:00  | NULL           |
|   5 | 09:30 | 16:00  | NULL           |
|   6 | NULL  | NULL   | By appointment |
+-----+-------+--------+----------------+

you should end up with the following table:

Monday – Wednesday 9am – 5.30pm
Thursday – Friday 9.30am – 7pm
Saturday 9.30am – 4pm
Sunday By appointment
Phone anytime before 11pm

Read more...

Backbone JS Models and Collections in same file with RequireJS

31st January, 2015 - Posted by david

In BackboneJS, classes/objects are called Models and groups of these models go into Collections. Using RequireJS, you define something and return that something for use in other files. In many BackboneJS examples, I’ve seen say a Person Model and a Person Collection stored in separate files, with each one being imported separately to the script they’re used in. Seeing as you generally use Models and Collections together at the same time, I was looking for a way to have them both in the one file. The solution is pretty simple and involves some simple Javascript object manipulation. I should give credit for this solution to my friend Ronan, although I’m not sure if he came up with it originally!

So, just to illustrate the normal Backbone way of doing things, sticking wih our Person class/object, you might have 2 files in your JS folder, models/person.js and collections/persons.js, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// models/person.js
define(['underscore', 'backbone'],
    function(_, Backbone) {
         Person = Backbone.Model.extend({
             url: '/person',
             // ... etc
         });

         return Person;
     }
);

//collections/persons.js
define(['underscore', 'backbone', 'models/person']
    function(_, Backbone, Person) {
        Persons = Backbone.Collection.extend({
            model: Person,
            // ... etc
        });
        return Persons;
    }
);

// some other JS file
require('models/person');
require('collections/persons');
var people = new Persons();
people.add(new Person());

So, I’m looking for a way to just have one require‘d file, that will import both my Model and Collection. A way this can be achieved is by defining a Person as a standard Javascript object, then having Model and Collection as attributes of that object. We need to add each attribute one step at a time, we can’t just do it all in one big Person = { // ... } block. Then, once the file is imported, we can do stuff like var people = new Person.Collection();.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// person.js
define(['underscore', 'backbone'],
    function(_, Backbone) {
        var Person = {}; // create initial object
        Person.Model = Backbone.Model.extend({
            url: '/person',
            // ... etc
        });

        // in order to access the Person.Model, we have to specify each attribute separately
        Person.Collection = Backbone.Collection.extend({
            model: Person.Model,
            // ... etc
        });
        return Person;
    }
);

// some other JS file
require('person');
var people = new Person.Collection();
people.add(new Person.Model());

Read more...

Extending Google Maps class with RequireJS and Underscore

24th January, 2015 - Posted by david

I’ve been working alot with RequireJS lately and love the whole modular JS movement, where blocks of code are safely contained within separate functions, that can be loaded in when necessary, without worrying about variable/function name clashes. Generally each module is stored in it’s own file but you can combine several modules into one giant file, in theory containing your entire JS code for your site!

When working with Google Maps, I was thinking it’d be nice to simply extend the google.maps.Map class and add in my own functions, e.g. showLoading to display a nice, semi-transparent ‘Loading…’ div in the centre of the map while the code does an AJAX request to get some data. So, you’d end up with something like:

1
2
var map = new Map();
map.showLoading();

So, first thing you need to do is add the Asynch plugin for requireJS to your requireJS config. For external plugins, I tend to source them from https://cdnjs.com/ if possible, cutting down on some of my own server bandwidth. I have my RequireJS config in my require.min.js file, after the main block of minified code. Here’s an edited version, with the asynch plugin on line 6:

1
2
3
4
5
6
7
8
9
10
11
12
13
requirejs.config({
        'baseUrl': '//cdn.mydomain.com/js',
        'paths': {
        'jquery': 'libs/jquery.min',
        'underscore': 'libs/underscore.min',
        'async': '//cdnjs.cloudflare.com/ajax/libs/requirejs-async/0.1.1/async'
    },
    'shim': {
        'underscore': {
            exports: '_'
        },
    }
});

Next up, we have a map.js file, which, via the define function, we’ll pass UnderscoreJS and the Google Maps JS code using the Asynch plugin. This file is going to return a Map object, which we can then use to instantiate new instances of the class. The file starts off with a Map function, which takes as a parameter the id of the div you want to put the map into and the latitude & longitude of the map’s centre. Obviously you could pass more parameters if you wish, e.g. initial zoom level etc. This Map function will then create a variable of type google.maps.Map, set the map up and return the variable as output from the function. This way when you do var myMap = new Map();, your myMap will be able to call all of google.maps.Map‘s functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define(
    ['underscore', 'async!https://maps.googleapis.com/maps/api/js?sensor=false'],
        function(_) {

            var Map = function (div, lat, lng) {
                zoom: 15,
                center: new google.maps.LatLng(lat, lng)
            });

            return map;
        }

        // more code below to follow...

        return Map;
});

We also need to give our Map‘s protoype the google.maps.Map prototype, which is simply:

1
Map.prototype = google.maps.Map.prototype;

Finally, if we want to add our own functions to our Map, we use UnderscoreJS to extend the Map.prototype with our functions. So, for example, I have one called isBigDrag to detect if the user has dragged the map a reasonable amount to fetch more data, as well as showLoading and hideLoading to show/hide my nice ‘Loading…’ div.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_.extend(Map.prototype, {
    isBigDrag: function (prevCentre) {
        var currentCentre = this.getCenter();
        // .. etc
    },

    showLoading: function() {
        // etc.
    },

    hideLoading: function() {
        // etc.
    }
});

See below for the entire file, which is stored in ‘classes/map.js’ in baseUrl above.

To pass this new Map class into a script and to be able to do var myMap = new Map();, we do so via a call to requirejs in the script, as follows:

1
2
3
4
5
6
7
requirejs([
    'underscore', 'classes/map',
], function(_, Map) {
    var myMap = new Map('id', 50.0, 10.0);
    myMap.showLoading();
    // etc.
});

Took me a while to figure it all out but works pretty well now. The classes/maps.js file in it’s entirety is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
define(
    ['underscore', 'async!https://maps.googleapis.com/maps/api/js?sensor=false'],
    function(_) {

        var Map = function (div, lat, lng) {
            zoom: 15,
            center: new google.maps.LatLng(lat, lng)
        });

        return map;
    }

    Map.prototype = google.maps.Map.prototype;

    _.extend(Map.prototype, {
        isBigDrag: function (prevCentre) {
        var currentCentre = this.getCenter();
            // .. etc
        },

        showLoading: function() {
            // etc.
        },

        hideLoading: function() {
            // etc.
        }
    });

    return Map;

});

Read more...