Sit back, relax, and enjoy the code.

Google Maps events in Mobile Safari and PhoneGap for iPhone

Posted: April 29th, 2009 | Author: Brad | Filed under: Google Maps, HTML, Javascript, Programming, hacks, iPhone | Tags: , , , , | 3 Comments »

Having trouble getting your Google Maps div to respond to events like you want in Mobile Safari or a PhoneGap app for iPhone? So was I. Disabling pinch-zoom is simple enough, but getting a finger drag to (a) not move the map and (b) draw something on the map was a major pain. I share my solution below: Proxying events through a transparent div overlaid on the Google Maps div.

To play along at home, you’ll need your own Google Maps key or you can just aim your mobile browser to my example code at http://kickasslabs.com/examples/gmap_events.html. (Note that this will not work in a regular desktop browser.) Besides a <script> tag importing the GMaps JavaScript, you should not need any other libraries or tools besides Mobile Safari to view and use the example page. If the page is zoomed way out when you start, just double-tap the map tile in the upper-left corner – it should zoom to fill your viewport.

What you should see is a map tile with a line painted on it. If you touch the screen, you should see one end of the line follow your finger.

It looks simple but I had a bit of trouble getting it to work, because touch events are not like mouse events (curious as to why?), and GMaps doesn’t respond to touch events by default.

Step by Step

First, you need 2 divs:

    <!-- This div holds our map -->
    <div id="map" style="width: 320; height: 320px"></div>
 
    <!-- This div lies on top of the map and acts as an event proxy -->
    <div id="mapoverlay" style="height:320px; width: 320px; position: absolute;"></div>

And you need to position one div over the other:

    mapDiv = document.getElementById('map');
    mapOverlayDiv = document.getElementById('mapoverlay');
    mapOverlayDiv.style.top = (mapDiv.offsetTop + 0) + 'px';
    mapOverlayDiv.style.left = (mapDiv.offsetLeft + 0) + 'px';

You set up the map div like you would for any other GMaps-enabled page:

    // the map
    _map = new GMap2(document.getElementById("map"));
 
    // coordinates for home base
    _lat = 37;
    _lng = -95;
    mapCenter = new GLatLng(_lat, _lng);
    _map.setCenter(mapCenter, 15);

Set up the overlay div to respond to touch events by firing a custom event for your map, e.g.:

    mapOverlayDiv.ontouchstart = function(e) {
      GEvent.trigger(_map, 'customTouchStart',
        (e.touches[0].pageX - mapDiv.offsetLeft),
        (e.touches[0].pageY - mapDiv.offsetTop));
    }

Now have your map respond to that event:

    GEvent.addListener(_map, 'customTouchStart', mapTouchStart);

…where mapTouchStart is a callback function that does something useful in response to a touch:

    function mapTouchStart(xPixel, yPixel) {
      redrawLine(xPixel, yPixel);
    }

And… well, then you’re done. My example also responds to the touchMove event and has a little code for drawing lines, but you’ve seen all you need to know to get event proxying up and running for your app.

Got a question? Got working code for an easier way? Drop me a line in the comments!

  • Share/Bookmark

Rails Gotcha: ActiveRecord Caches Associated Records by Default

Posted: April 23rd, 2009 | Author: gabe | Filed under: Rails | Tags: , , , , | 2 Comments »

ActiveRecord will cache the results of association method calls by default, unless you tell it not to.

(This applies to Rails 2.3.2 and perhaps earlier versions.)

From the documentation:

project.milestones             # fetches milestones from the database
project.milestones.size        # uses the milestone cache
project.milestones.empty?      # uses the milestone cache
project.milestones(true).size  # fetches milestones from the database

Normally, this is great. However, if you’re not aware of this default caching, you might see some strange behavior in your app or tests and have no idea what’s going on. This default caching behavior had me and Abel stumped until we read about it in the docs.

Working with the same concept as the example in the documentation, where a Project has many Milestones, here’s a more explicit example of the caching behavior in action:

project_1 = Project.find_by_id(1)  
project_2 = Project.find_by_id(1)  
# Load the same Project into two variables
 
project_1.milestones.length   
# - Hits the db's Milestones table.  
# - Caches milestones object on project_1.
# - Returns 0. 
 
Milestone.create(:project_id => 1, :name => 'New Milestone')  
# Adds a milestone to the project. 
# But we don't do it through the project_1.milestones association
# because that _would_ update project_1.milestones's cached value
 
project_1.milestones.length   
# Returns 0 (not 1, like you'd expect) 
# because project_1.milestones was cached when 
# previously requested above.
 
project_2.milestones.length  
# Returns 1, because project_2.milestones 
# hasn't been requested/cached yet.
 
# Note: project_2.milestones is 
# a COMPLETELY DIFFERENT IN-MEMORY OBJECT 
# than project_1.milestones.
# Taking this point further, here's an explicit example:
 
project_2.milestones << Milestone.create(:name => 'Another Milestone')
# Put another milestone on the project through 
# project_2's milestones association.
 
project_1.milestones.length
# Still returns 0, because project_1 already cached it's copy of 
# the milestones association back when there were 0.
 
project_2.milestones.length 
# Now returns 2, because only project_2 knows about the new milestone.
 
project_1.milestones(true).length  
# Returns 2, because ActiveRecord updates the cache 
# when association(true) is present.

Another funny something to note about the association caching behavior is that even when ActiveRecord uses a cached value, it still emits SQL to the log file. So, don’t let that trip you up either.

  • Share/Bookmark