Blog
All Blog Posts | Next Post | Previous Post
Extend TMS WEB Core with JS Libraries with Andrew:
Leaflet
Tuesday, July 19, 2022
There are so many useful JavaScript 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 JavaScript 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 the developers in these cases are also responsible for their respective operating systems. 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 JavaScript 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 the necessary links to the Leaflet JavaScript and CSS files.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.8.0/dist/leaflet.min.css"> <script src="https://cdn.jsdelivr.net/npm/leaflet@1.8.0/dist/leaflet.min.js"></script>
In the project itself, add a TWebHTMLDiv to the form and set its Name and ElementID to 'divMap' just so we can reference
it. To immediately see Leaflet 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: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).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.
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).
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 JavaScript 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.
<script src="https://cdn.jsdelivr.net/npm/universal-geocoder@0.14.2/dist/universal-geocoder.min.js"></script>
With the JavaScript 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. Mostly what
we're after are the latitude and longitude coordinates, so we can update the map.
The other data that is returned is relatively 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, without animation of any kind.
Search for Locations.
Annotations and Overlays.
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 of 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;
We could also get location bounds from our search results (expressed as corners of a
rectangle, which could be converted into a circle/diameter easily enough) and use that to draw a shape
instead. But a fixed-size circle looks pretty good here.
Highlight the Search Results.
There is also support for popups, custom marker overlays, and other annotations.
Event Handling.
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.
Event Handling in Leaflet.
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: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).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.).
Other Directions.
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 JavaScript library and a solid addition to any TMS WEB Core project.
Thanks, Eddy!
Follow Andrew on 𝕏 at @WebCoreAndMore or join our 𝕏 Web Core and More Community.
Andrew Simard
This blog post has received 7 comments.
Your from Vancouver, Wow!
Great job you are doing here, I know I am going to use as much of these posts in my future projects as I can.
Thanks for all you details & hard work.
Green Lawrence n. Green
Andrew Simard
Your are from Vancouver and one my employe will move with his family in September to Vancouver and I ask me perhaps he could continue to work for us.
The only thing he needs some course for using TMS WEB CORE and perhaps I don''t know if you could do such course.
Vincenzo Sita
Vincenzo Sita
Andrew Simard
Poullet Eddy
Andrew Simard
All Blog Posts | Next Post | Previous Post
Price Rhett