Leaflet – Updating Map When External GeoJSON Data is Modified

geojsonhtmlleaflet

We have a Leaflet map embedded into an OLE Browser Control in our client application named [T].

The HTML file we have today loads dynamic data from a local GeoJSON file that is generated on request from [T].

User interaction on the Leaflet map contains a callback to [T] application that regenerates the GeoJSON content and requests the HTML to reload.

We use Google as a Tile provider and since they changed their payment model, we are looking at minimising the amount of requests to their Tile server.

Would there be a way after sending the callback to the application [T] in reloading only the layers derived from data in the GeoJSON file preserving the tile content?

Below is our HTML script.

'T' – Leaflet

<script src="leaflet.js"></script>

<link rel="stylesheet" href="abc-tankers.css"/>
<script src="abc-geojson.js" type="text/javascript"></script>

<script>

function changeHash(obj) {
window.setTimeout(function( ) {
    window.location.hash = obj.className+','+obj.id;
    }, 500);
}


function init() {
/* routing point markers - call the GEOJSON file*/
var RPUnlockedIcon = L.icon({
      iconUrl: './images/unlocked-green-white-32x32.png',
      iconSize: [32, 32],
      iconAnchor: [16, 16],
      popupAnchor: [0, -28]
    }); 

var RPLockedIcon = L.icon({
  iconUrl: './images/locked-red-white-32x32.png',
  iconSize: [32, 32],
  iconAnchor: [16, 16],
  popupAnchor: [0, -28]
}); 

var legcount = Object.keys(abc_routing.features).length;
var leglayers = new Array(legcount);

/* firstly work through the primary routing points - these are the routing points that are found on the active route */
for (legindex = 0; legindex <= leglayers.length - 1; legindex++) {

    leglayers[legindex] = new L.layerGroup();
    var rp = L.geoJson(abc_routing_points, {
      pointToLayer: function (feature, latlng) {
        return L.marker(latlng);
      }, onEachFeature: function(feature, layer) {
        var datatable = "<div class='PopupPortTable'><div class='PopupPortTableBody'><div class='PopupPortTableRow'><div class='PopupPortTableCell'>Leg</div><div class='PopupPortTableCellContent'>" + (parseInt(feature.properties.LegIndex) + 1) + "</div></div></div>";

            if(feature.properties.Open == false && feature.properties.Advanced == false) {
                layer.setIcon(RPLockedIcon)
                layer.bindPopup("<label class='switch-lock' ><input type='checkbox'><span class='tankers-slider-lock' id='" + feature.properties.ShortCode + "-" + feature.properties.LegIndex + "' onclick='changeHash(this)'></span></label><h3>" + feature.properties.Name  + " - Closed</h3>" + datatable );
            } else if(feature.properties.Open == true && feature.properties.Advanced == false) {
                layer.setIcon(RPUnlockedIcon)
                layer.bindPopup("<label class='switch-unlock' ><input type='checkbox' checked><span class='tankers-slider-unlock' id='" + feature.properties.ShortCode + "-" + feature.properties.LegIndex + "' onclick='changeHash(this)'></span></label><h3>" + feature.properties.Name  + " - Open</h3>" + datatable);               
            }
        }, filter: function (feature, layer) {

            if (feature.properties.LegIndex==legindex && feature.properties.Advanced == false) {
                return true;
            }
            return false;
        }
    }).addTo(leglayers[legindex]); 
}

var legadvancedlayers = new Array(legcount);

/* secondly we work through the advanced routings.  these basically are all routings outside the primary ones */
for (legindex = 0; legindex <= legadvancedlayers.length - 1; legindex++) {

    legadvancedlayers[legindex] = new L.layerGroup();
    var rp = L.geoJson(abc_routing_points, {
      pointToLayer: function (feature, latlng) {
        return L.marker(latlng);
      }, onEachFeature: function(feature, layer) {
            var datatable = "<div class='PopupPortTable'><div class='PopupPortTableBody'><div class='PopupPortTableRow'><div class='PopupPortTableCell'>Leg</div><div class='PopupPortTableCellContent'>" + (parseInt(feature.properties.LegIndex) + 1) + "</div></div></div>";


            if(feature.properties.Open == false && feature.properties.Advanced == true) {
                layer.setIcon(RPLockedIcon)
                layer.bindPopup("<label class='switch-lock' ><input type='checkbox'><span class='tankers-slider-lock' id='" + feature.properties.ShortCode + "-" + feature.properties.LegIndex + "' onclick='changeHash(this)'></span></label><h3>" + feature.properties.Name  + " - Closed</h3>" + datatable );
            } else if(feature.properties.Open == true && feature.properties.Advanced == true) {
                layer.setIcon(RPUnlockedIcon)
                layer.bindPopup("<label class='switch-unlock' ><input type='checkbox' checked><span class='tankers-slider-unlock' id='" + feature.properties.ShortCode + "-" + feature.properties.LegIndex + "' onclick='changeHash(this)'></span></label><h3>" + feature.properties.Name  + " - Open</h3>" + datatable);               
            }
        }, filter: function (feature, layer) {

            if (feature.properties.LegIndex==legindex && feature.properties.Advanced == true) {
                return true;
            }

            return false;
        }
    }).addTo(legadvancedlayers[legindex]); 
}

/* route line - call the GEOJSON file*/
var routelayer = L.layerGroup();

var route = L.geoJson(abc_routing, {
  style: function(feature) {
    return {
      color: '#224A59',
       weight: 4,
       opacity: 0.9
    };
  },
  onEachFeature: function(feature, layer) {
    layer.bindPopup("<h3>Leg  " + (parseInt(feature.id) + 1) + "</h3><br>" +
      feature.properties.Description);
  }
}).addTo(routelayer);



for (legindex = 0; legindex <= leglayers.length - 1; legindex++) {

    var singleroute = L.geoJson(abc_routing, {
        style: function(feature) {
        return {
            color: 'orange',
            weight: 4,
            opacity: 0.9
        };
    },
    filter: function (feature, layer) {
        if (feature.id==legindex) {
            return true;
        }
        return false;
    }
    }).addTo(leglayers[legindex]);
}       

/* port markers - call the GEOJSON file */
var portmarkerslayer = L.layerGroup();

var PortIcon = L.icon({
  iconUrl: './images/marker-icon-2x.png',
  iconSize: [25, 41],
  iconAnchor: [16, 37],
  popupAnchor: [0, -28]
});

var portmarkers = L.geoJson(abc_marker_ports, {
  pointToLayer: function (feature, latlng) {
    return L.marker(latlng, {icon: PortIcon});
  }, 
  onEachFeature: function(feature, layer) {
      var popupContent = "<h3>" + feature.properties.PortName  + "</h3>";
      if (feature.properties) {
        popupContent += "<div class='PopupPortTable'><div class='PopupPortTableBody'><div class='PopupPortTableRow'><div class='PopupPortTableCell'>Latitude</div><div class='PopupPortTableCellContent'>" + feature.properties.Lat + "</div></div><div class='PopupPortTableRow'><div class='PopupPortTableCell'>Longitude</div><div class='PopupPortTableCellContent'>" +  feature.properties.Lon + "</div></div><div class='PopupPortTableRow'><div class='PopupPortTableCell'>Distance from Start</div><div class='PopupPortTableCellContent'>" +  feature.properties.DistanceFromStart + "</div></div></div></div>";
      }
      layer.bindPopup(popupContent);
    }         
}).addTo(portmarkerslayer);


/* firstly we set the base tile up */
var openstreetmap   = L.tileLayer('http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png', {
            maxZoom: 18,
            attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
        });

/* this tile is actually used as an overlay  */     
var opensealayer  = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
            maxZoom: 18,
            attribution: 'Map data: &copy; <a href="http://www.openseamap.org">OpenSeaMap</a> contributors'
        });

var overlays = {
    "Journey": routelayer,
    "Port Markers": portmarkerslayer,
    "OpenSeaMap" : opensealayer
};


/* add the arrays for Primary/Advanced routings */
for (legindex = 0; legindex <= leglayers.length - 1; legindex++) {
    overlays["PRP-Leg " + (legindex + 1)] = leglayers[legindex];
}

for (legindex = 0; legindex <= legadvancedlayers.length - 1; legindex++) {
    overlays["ARP-Leg " + (legindex + 1)] = legadvancedlayers[legindex];    
}


var map = L.map('map', {
    zoom: 10,
    zoomControl: false,
    layers: [openstreetmap]
});

var hashdata = ""

/* now looking into the saveddata in JSON file that was passed through the url hash - we check if any of the settings should be restored since last call */
var found = saveddata.search("xJourneyx");
if (found>0) {
    routelayer.addTo(map);
    hashdata += "xJourneyx";
}
found = saveddata.search("xPort Markersx");
if (found>0) {
    portmarkerslayer.addTo(map);
    hashdata += "xPort Markersx";
}
found = saveddata.search("OpenSeaMap");
if (found>0) {
    opensealayer.addTo(map);
    hashdata += "xOpenSeaMapx";
}

for (legindex = 0; legindex <= leglayers.length - 1; legindex++) {
    found = saveddata.search("xPRP" + (legindex + 1) + "x");
    if (found>=0) {
        leglayers[legindex].addTo(map);
        hashdata += "xPRP" + (legindex + 1 + "x");
    }   
}

for (legindex = 0; legindex <= legadvancedlayers.length - 1; legindex++) {
    found = saveddata.search("xARP" + (legindex + 1) + "x");
    if (found>=0) {
        legadvancedlayers[legindex].addTo(map);
        hashdata  += "xARP" + (legindex + 1 + "x");
    }   
}

if (hashdata!="") {
    window.location.hash = hashdata
}

/* now we work out the group for the layer control */
var baseLayers = {
    "OpenStreetMap": openstreetmap
};

L.control.layers(baseLayers, overlays, {position: 'bottomright', collapsed: true}).addTo(map);

map.fitBounds(singleroute.getBounds());

L.control.zoom({
    position:'topright'
}).addTo(map);


map.on('overlayadd', function(e) {
    var newname = e.name.replace("-Leg ","")
    window.location.hash += "x" + newname + "x";
});

map.on('overlayremove', function(e) {

    var newname = "x" + e.name.replace("-Leg ","") + "x"
    var oldhash = window.location.hash;
    var newhash = oldhash.replace(newname, "");
    window.location.hash = newhash; 

});
}

The challenge here is I can only use IE9 emulating Edge and all files are local, using FILE protocol.

Best Answer

This is a challenging scenario indeed. The challenge boils down to "How do I make stuff happen in a webpage inside a IE9 OLE control without reloading the whole webpage?". This is hard because the deprecated API you linked in the comments allows only to navigate to a new URL and to refresh the current page. Compare this to the evaluateJavascript() method of Android's WebView, which allows you to run arbitrary javascript code in an embedded WebView.

The only approach that I can think of is using bookmarklet-style URLs. e.g. something like this in your PowerBuilder code:

myOleIE9instance.navigate("javascript:reloadGeoJson()")

and something like this in the javascript code:

// Create a reference to each refreshable GeoJSON-based layer in the
// global scope
var myGeoJsonRoute;

// Make sure that this function is either in the global scope, or 
// reachable as window.reloadGeoJson() or similar.
function reloadGeoJson() {
    map.removeLayer(myGeoJsonRoute);

    // Load GeoJSON from the network, by any means available - jquery,
    // L.GeoJSON.AJAX, or whatever method compatible with IE9. This
    // means that the Fetch interface is not an option here.
    myGeoJsonRoute = L.GeoJSON.AJAX('abc-geojson.geojson').addTo(map);
}

If you cannot run bookmarklets in the embedded IE9 instance, then I don't think this problem has a solution (other than "throw away the obsolete IE9 control and redo it with a more modern stack").