9
votes

I'm displaying a weather map in a GWT application. I'm using GWT 2.7 and the GWT wrapper of the GoogleMaps JavaScript API available here (gwt-maps-3.8.0-pre1.zip).

I use a tile server to fetch the weather, which updates every 5 minutes. On the 5 minute mark, I refresh the map by setting the zoom to 1 and then back to the original, by triggering a resize, and by removing and then adding the weather layer again.

This worked fine. However, recently I noticed that this no longer works: the refresh never even goes to the tile server, so no new weather is displayed. If you leave my map up for 12 hours, you'll be looking at weather that's 12 hours old. Previously, the map would automatically stay updated. I haven't changed any of my code. So my guess is that something changed in the underlying GoogleMaps JavaScript API.

However, I then created this simple pure-JavaScript example:

<!DOCTYPE html>
<html>
    <head>
        <title>Map Test</title>
        <style type="text/css">
            html, body { height: 100%; margin: 0; padding: 0; }
            #map {
                width:90%;
                height: 90%;
                display:inline-block;
            }
        </style>
    </head>
<body>
<div id="map"></div>
<button type="button" onClick="refreshMap()">Refresh</button>
<script type="text/javascript">

    var map;
    var tileNEX;

    function initMap() {

        var mapOptions = {
            zoom: 8,
            center: new google.maps.LatLng(42.5, -95.5),
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };

        map = new google.maps.Map(document.getElementById('map'), mapOptions);

        tileNEX = new google.maps.ImageMapType({
            getTileUrl: function(tile, zoom) {
                return "http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/" + zoom + "/" + tile.x + "/" + tile.y +".png?"+ (new Date()).getTime(); 
            },
            tileSize: new google.maps.Size(256, 256),
            opacity:0.60,
            name : 'NEXRAD',
            isPng: true
        });

        map.overlayMapTypes.setAt("0",tileNEX);
    }

    function refreshMap(){
        var zoom = map.getZoom();
        map.setZoom(1);
        map.setZoom(zoom);

        //google.maps.event.trigger(map, 'resize');

        //var layer = map.overlayMapTypes.getAt(0);
    //map.overlayMapTypes.setAt(0, null);
    //map.overlayMapTypes.setAt(0, layer);
    }

</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?callback=initMap">
</script>
</body>
</html>

To my surprise, this still works fine. Whenever you click the Refresh button, the map goes to the tile server and fetches new tiles.

So I tried doing the exact same thing in GWT:

package com.example.client;

import com.google.gwt.ajaxloader.client.AjaxLoader;
import com.google.gwt.ajaxloader.client.AjaxLoader.AjaxLoaderOptions;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Document;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.maps.gwt.client.GoogleMap;
import com.google.maps.gwt.client.LatLng;
import com.google.maps.gwt.client.MapOptions;
import com.google.maps.gwt.client.MapType;
import com.google.maps.gwt.client.MapTypeId;

public class GwtMapTest implements EntryPoint {

    @Override
    public void onModuleLoad() {
        AjaxLoaderOptions options = AjaxLoaderOptions.newInstance();
        options.setOtherParms("sensor=false");
        Runnable callback = new Runnable() {
            public void run() {
                createMap();
            }
        };
        AjaxLoader.loadApi("maps", "3", callback, options);
    }

    public void createMap() {

        MapOptions mapOpts = MapOptions.create();
        mapOpts.setZoom(4);
        mapOpts.setCenter(LatLng.create(37.09024, -95.712891));
        mapOpts.setMapTypeId(MapTypeId.TERRAIN);
        mapOpts.setStreetViewControl(false);

        final GoogleMap map = GoogleMap.create(Document.get().getElementById("map_canvas"), mapOpts);
        addWeatherLayer(map);   

        Button button = new Button("Gwt Refresh");
        button.addClickHandler(new ClickHandler(){

            @Override
            public void onClick(ClickEvent event) {
                refreshWeatherLayer(map);
            }
        });

        Button nativeButton = new Button("Native Refresh");
        nativeButton.addClickHandler(new ClickHandler(){

            @Override
            public void onClick(ClickEvent event) {
                nativeRefreshWeatherLayer(map);
            }
        });

        RootPanel.get().add(button);
        RootPanel.get().add(nativeButton);
    }

    public native void addWeatherLayer(GoogleMap map) /*-{
        var imageMapType = new $wnd.google.maps.ImageMapType({
            getTileUrl: function(coord, zoom) {
                return "http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/"+ zoom + "/" + coord.x + "/" + coord.y + ".png";
            },
            tileSize: new $wnd.google.maps.Size(256, 256),
            opacity:.50,
            isPng: true
        });

        map.overlayMapTypes.setAt("0", imageMapType);

    }-*/;

    private void refreshWeatherLayer(GoogleMap map){
        double zoom = map.getZoom();
        map.setZoom(1);
        map.setZoom(zoom);

        MapType layer = map.getOverlayMapTypes().getAt(0);
        map.getOverlayMapTypes().setAt(0, null);
        map.getOverlayMapTypes().setAt(0, layer);
    }

    private native void nativeRefreshWeatherLayer(GoogleMap map) /*-{
        var zoom = map.getZoom();
        map.setZoom(1);
        map.setZoom(zoom);

        $wnd.google.maps.event.trigger(map, 'resize');

        var layer = map.overlayMapTypes.getAt(0);
        map.overlayMapTypes.setAt(0, null);
        map.overlayMapTypes.setAt(0, layer);
    }-*/;

}

This should do the same thing as the pure-JavaScript example. Instead, when I click the button, the map does refresh (I see the weather layer blink), but it doesn't actually go to the tile server for new tiles.

Weirder still, this "sorta" works in Internet Explorer: maybe 1 out of 3 times I click the button, the map actually goes to the tile server. But in Chrome, it never goes to the tile server when I click the button.

To determine this, I am looking at the network tab of the browser tools. I would expect the map to hit the tile server every time I click the button. That's what it does in pure JavaScript, and that's what it used to do in GWT, but sometime in the last couple months the GWT behavior has changed.

I don't think this is a browser caching issue. The problem is not that the map tries to fetch new tiles but gets old ones, it's that it never tries to fetch new tiles. I click the button, and I see nothing happening in the network tab of the developer tools.

  • Why do I see a different behavior in JavaScript vs GWT?
  • Why do I see a different behavior in different browsers?
  • Is the GWT GoogleMaps library doing some kind of internal caching? Is there a way to disable this?
  • Why has this behavior apparently changed?
  • How can I make the GWT map refresh by going to the tile server for new tiles?
1
I have tested the script in both browsers and tried it on GWT. One of the diferences that i found is that in the browsers they put a timestamp after the tile (/ZOOM/X/Y.png?TIME). The GWT code apparently dont do this. In the browser if the time is ommited IE retrieves it from the server and Chrome get it from localcache. Maybe the change was in the tile server (or a specific proxy) that is caching the tile. Can you try update the request in GWT code whith a "?TIME" param? - fhofmann
@fhofmann That actually worked, which is surprising to me. Usually you add a ?time part to avoid the browser cache, which in this case wasn't the problem, since it was never going to the server in the first place, and therefore never even checking the browser cache. So it looks like there must be some internal caching, but only in the GWT version, which doesn't make any sense to me. Can you explain why the GWT and JS versions are different or why the ?time thing works in GWT? If you want to add that as an answer, I'd definitely appreciate it. - Kevin Workman
Actually i never used GWT before. But now iam curious and I will try to find how it works and where it could be caching. - fhofmann
@fhofmann I just realized that you're right: this is a problem in the underlying JavaScript, not in GWT. I just happened to include the fix in the JavaScript version, but not in the GWT version. GWT uses the JavaScript library under the hood. I'm still curious about why this behavior seems to have changed, but the Google Maps JavaScript is not available in an unobfuscated format, so it's hard to debug. If you post your comment as an answer, I'll award you the bounty. - Kevin Workman

1 Answers

1
votes

This is not the final answer. But contains significant information.

To manage http calls GWT have a set of classes that act like a browser, and is where i belive the problem is.

If the same code worked before is possible that the tile server include the Cache header (Cache-Control:max-age=300) recently. And for some reason the GWT code are not managing this header properly.

If i am right, a way to evict the problem is to append a parameter with the current time in the GWT call (like the browser version of your code).

public native void addWeatherLayer(GoogleMap map) /*-{
    var imageMapType = new $wnd.google.maps.ImageMapType({
        getTileUrl: function(coord, zoom) {
            return "http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/"+ zoom + "/" + coord.x + "/" + coord.y + ".png?" + new Date().getTime();
        },
        tileSize: new $wnd.google.maps.Size(256, 256),
        opacity:.50,
        isPng: true
    });

    map.overlayMapTypes.setAt("0", imageMapType);

}-*/;