Extend TMS WEB Core with JS Libraries with Andrew: Leaflet

Photo of Andrew Simard

There are so many useful JS libraries yet to explore that it can be a challenge to pick what to cover next.  But
this time out, we’re going to have a look at a JS library suggested by blog reader Eddy Poullet, way back in
April when this blog series first started.  The request?  Leaflet,
which bills itself as ‘a JavaScript library for interactive maps.’  And who doesn’t like interactive maps? Easily
one of the more useful applications to be found on any mobile device. And while there are a number of other ways
to get interactive maps into your TMS WEB Core applications, this is a good example where it is nice to have a few
choices available. 

Motivation.

Google Maps and Apple Maps tend to get most of the attention, as one of these is likely to be the default when it
comes to using maps on any mobile device.  And these tend to be very well integrated into their respective
devices, naturally, as they’re also responsible for their respective operating systems as well.  No surprise
there.  Desktop apps, and websites specifically, tend to be more commonly configured to use Google Maps or perhaps
MapQuest or less commonly other providers, and far less frequently Apple Maps.  Perhaps that will change over time
as developers integrate Apple Maps into their websites.  Perhaps we’ll cover that another time. But that’s
historically been one of the troubles of interactive maps – jumping through hoops to get at the data.  Whether it
is an API key or a developer account or some kind of license or an unhelpful rate limit.   Leaflet offers a bit of a
different approach in that there’s nothing to do to get started in terms of API keys or developer account
registration.  Just load up a JS library and be immediately productive.

Getting Started.

As has become custom, we’ll start with a new TMS WEB Core project, using the Bootstrap template, to begin with. 
And then add in the necessary links to the Leaflet JavaScript and CSS files.

    
    

In the project, add a TWebHTMLDiv to the form and set its Name and ElementID to ‘divMap’ just so we can reference
it.  To immediately see it in action, it just needs to be initialized.  This can be done directly from within
WebFormCreate like this, taken directly from the excellent documentation in their Quick
Start Guide
.

procedure TForm1.WebFormCreate(Sender: TObject);
begin
  asm
    var map = L.map('divMap').setView([51.505, -0.09], 13);
    var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
          maxZoom: 19,
          attribution: '© OpenStreetMap'
      }).addTo(map);
  end;
end;

Doesn’t get much simpler than that.  Using the new TWebCSSClass component in combination with Bootstrap, we can
add in some rounded corners here and there and immediately get a pretty reasonable interactive map that supports
zooming in/out and panning around. 

TMS Software Delphi  Components

Leaflet up and running

The interface that is rendered uses pure HTML/CSS so it is possible to change the look of things like the zoom
in/out controls or other elements. The content (the map itself) consists of a collection of image tiles.  Can’t
do much with those directly in HTML/CSS.  But there are a lot of things that can be done with what is being
generated inside the tiles.  And speaking of tiles, while everything so far is pretty quick and easy, there are
some considerations that come with the set of tiles.  In this example, OpenStreetMap is the source of the tile
data, and they have their own policies
to keep in mind. As with many of these kinds of things, this is a community-supported and shared resource, and as
such the limits imposed are generally related to ensuring people don’t abuse the service.  No downloading all the
tiles at once, for example.

Finding Yourself.

Modern browsers typically know where they are in the world, whether it is the browser on your phone or the one on
your desk. Permissions are an issue here, so by default browsers should be asking permission before giving up
their location to just any old website.  Also, if you publish an app, it should be served via HTTPS in order for
this built-in geolocation to work.  Fortunately, it works fine out-of-the-box when developing the app, so no need
to worry about that.  Leaflet even has some extra helpers to make this really easy.  If we add a button to our
app, we can just add a function to implement this entirely.  Clicking the button pops up a request to share your location.

procedure TForm1.btnFindMeClick(Sender: TObject);
begin
  asm
    this.map.locate({setView:true});
  end;
end;

And indeed it comes up with a map that contains my exact location (Vancouver, British Columbia, Canada).

TMS Software Delphi  Components

Browser Geolocation Support

Finding Other Locations.

Easy enough.  What about other locations?  The process of converting an address or a location into a set of
coordinates is referred to as geolocation.   And Leaflet doesn’t really have any geolocation facilities built into
it.  Easily addressed with another JS library, though.  We’ll use universal-geocoder
for this.  It works with several providers, but for now, we’ll just use it with OpenStreetMap so we can continue to
not care about API keys.  And this also works directly in the browser, so we don’t have to set up any kind of
server to protect those non-existent API keys.

    

With the library in place, we can add another button and a TWebEdit control to get input
from the user.  We’ll then pass that over to the library and get back a bunch of information.  But mostly what
we’re after are the coordinates, so we can update the map.  The other data that is returned is fairly
extensive.  For example, multiple search results are returned, which could be used to create a dropdown of
selection options. Various related region names and codes are returned if you were interested in applying
filters.  And the search query itself can be extended to include its own filters.  For example, limiting
searches to a set of countries using the ubiquitous ISO
3166 codes
.  But a quick and simple search function can be implemented like this.

procedure TForm1.btnSearchClick(Sender: TObject);
var
  search: String;
begin
  search := edtSearch.Text;
  asm
    const openStreetMapGeocoder = UniversalGeocoder.createGeocoder({
      provider: "openstreetmap",
      userAgent: "Leaflet TMS WEB Core Demo"
    });
    openStreetMapGeocoder.geocode(search, (results) => {
      if (results.length > 0) {
        this.map.flyTo([results[0].coordinates.latitude,results[0].coordinates.longitude], 13);
      }
      else {
        alert('No results found');
      }
    });
  end;
end;

procedure TForm1.WebFormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if key = VK_RETURN then btnSearchClick(Sender);
end;

Entering some text and hitting the enter key will result in a little animation, flying you
over to your selected destination.  Plenty of options are available to control how you get from point A to
point B, whether via this little “flyTo” method, via panning, or just simply updating the map with the new
location, sans animation of any kind.  

A large portion of the documentation is devoted to handling annotations and overlays. 
Adding your own shapes to the map, for example, or even directly overlaying video or SVG content. Weather
satellite imagery is used as an example for video content.  Shapes can be the usual rectangles and circles,
but also any polygon.  All of these can be fitted to boundaries on the displayed map so alignment isn’t really
something to be concerned with.  As an example, let’s draw a 250m circle around our search results.

procedure TForm1.btnSearchClick(Sender: TObject);
var
  search: String;
begin
  search := edtSearch.Text;
  asm
    const openStreetMapGeocoder = UniversalGeocoder.createGeocoder({
      provider: "openstreetmap",
      userAgent: "Leaflet TMS WEB Core Demo"
    });
    openStreetMapGeocoder.geocode(search, (results) => {
      if (results.length > 0) {
        this.map.flyTo([results[0].coordinates.latitude,results[0].coordinates.longitude], 13);
        L.circle([results[0].coordinates.latitude,results[0].coordinates.longitude],{radius:250}).addTo(this.map);
      }
      else {
        alert('No results found');
      }
    });
  end;
end;

Knowing where a user has clicked is handled through the Leaflet event system.  Let’s say for
example you wanted a user to enter a series of points to use as a geofence.  We’ll need a button to start and
stop selecting points, and then create a new polygon based on the points that were selected. For our example,
we’ll use the VLT (Very Large Telescope) in South America.  This is actually a collection of telescopes, but
there are four main telescopes that make up the VLT itself.  So we’ll draw a polygon around the four main
telescopes, and then we’ll be able to detect whether we’ve clicked inside this geofence by attaching an event
handler to it.  Here’s what it looks like.

We’ll set up the Create GeoFence button as a toggle of sorts.  Click it to start entering
points.  And then click it again to make the polygon.  Nothing as fancy as editing or canceling points in our
example here and the polygon isn’t drawn progressively, but the idea here is sound and works pretty well.

procedure TForm1.AddToPolygon(lat: String; lng: String);
begin
  // Building GeoFence
  if btnGeoFence.Tag = 1 then
  begin
    Polygon.Add(',');
    Polygon.Add('['+lat+','+lng+']');
    btnGeoFence.Caption := IntToStr(Polygon.Count div 2)+' point(s)';
  end;
end;

procedure TForm1.btnGeoFenceClick(Sender: TObject);
var
  i: integer;
  strarray:String;
  fencelabel:string;
begin
  if btnGeoFence.Tag = 0 then
  begin
    // Enter "entry mode"
    Fences := Fences + 1;
    btnGeoFence.Tag := 1;
    btnGeoFence.ElementHandle.classList.replace('btn-primary','btn-danger');
  end
  else
  begin

    if (Polygon.Count >= 3) then
    begin

      strarray := '[';
      for i := 1 to Polygon.Count-1 do
      begin
        strarray := strArray+Polygon[i];
      end;
      strarray := strarray+']';
      fencelabel := 'GeoFence #'+IntToStr(Fences);
      asm
        var latlngs = JSON.parse(strarray);
        var geofence = L.polygon(latlngs, {color:"red"}).addTo(this.map);
        geofence.on('click', function(e){ console.log('Clicked on '+fencelabel)});
      end;
    end;

    // Reset back to normal
    btnGeoFence.Tag := 0;
    btnGeoFence.ElementHandle.classList.replace('btn-danger','btn-primary');
    btnGeoFence.Caption := 'Create GeoFence';
    Polygon.Text := '';
  end;
end;

procedure TForm1.WebFormCreate(Sender: TObject);
begin

  // Lets start with the focus here
  edtSearch.setFocus;

  // Polygon user can build to define geofence
  Polygon := TStringList.Create;

  asm
    this.map = L.map('divMap').locate({setView:true});

    var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
          maxZoom: 19,
          attribution: '© OpenStreetMap'
      }).addTo(this.map);


    function onMapClick(e) {
      pas.Unit1.Form1.AddToPolygon(e.latlng.lat, e.latlng.lng);
    }
    this.map.on('click', onMapClick);

  end;

end;

Now, the mouse cursor automatically changes to a pointer when over the geofence that is
created and clicking it adds the log entry to the console.  More UI could be added to name the geofence, and
perhaps a list component to cycle between them or otherwise manage them (delete, edit, etc.). 

For an interactive mapping library, Leaflet is surprisingly easy to set up and works pretty
well without having to set up anything like an account or API keys or anything else.  And what we’ve covered in
this post isn’t really much more than what is in the “getting started” tutorial. Other tutorials cover topics
like GeoJSON, WMS, TMS, and even non-geographical maps. And there’s plenty of documentation that describes how
it all works.  Overall a very nice JS library and a solid addition to any TMS WEB Core project.  Thanks, Eddy!