Leaftlet, MapBox, MapLibre, Google Maps & the Antimeridian

As per usual, before I write a blog like this, I never know that what I’m about to do would actually become a part of blog. So, as usual with this blog, here’s another struggle I’ve had and created a solution that I didn’t know I needed… until I did need it.

So, let me present the problem, and some solutions I had along the way.

I’ve got an app I’m presently developing. All things are going well in regards to the quirks and frustrations of a typical angular project, promises, awaits, async, all that annoying stuff.

Well, It turns out that getting a map in to an Ionic project is pretty easy. There’s tutorials all over the place, but none of them covered what I needed, and I’ll explain why.

We’ve presently got a list of points on a Google shareable map. You enter points on the map, give them an assignment, even edit points on a table and boom. Your set, and you’ve got a shareable map to give to your friends. “Great”, I said. I wonder how we can get that in to our app. How hard can it be?

Here’s out list of points from Google Maps.

So, in Ionic, you can ‘easily’ embed a web site in to your app with the ‘in-app browser’ plugin. I’m not going in to depth over this, but you can watch a video here of the most simplistic and easy way to do that.

I just tweaked his open command based on what version of operating system you are running.

  openBlank()
  {
     if(this.platform.is('ios')==true)
     {
        //ios PlatForm
        this.iab.create('https://live.bible.is/bible/TPIPNG/GEN/1?audio_type=audio_drama', "_blank");
     }else{
        //Android PlatForm
        this.iab.create('https://live.bible.is/bible/TPIPNG/GEN/1?audio_type=audio_drama', "_system");
     }
  }

So, on to our problem. I decided to try out the Google Maps method. It didn’t go well.

The UI was compressed and awful, but everything else worked. I’ll get the real heart of the issue next, something known as the Antimeridian.

So, while Google Maps ‘can’ do what we need, the UX is terrible and clunky. So, surely I could do better and give the map experience in the app a native look and feel, rather than a “we have a map, and that’ll do” option.

So, the first thing was to maybe export the KML file. But, there’s a problem, because the LatLng data is not exported.

I looked a Leaflet. This was an obvious choice initially as it is widely used and supported in the Open Source community.

I was able to get the points working with a set of geojson points. Simple? Right? Everything is there?

But, uh oh. There was a problem. Wait, what the heck is that problem? Well, this is our Antimeridian that I had mentioned.

As you can see, and yes, this is an overly wide example, it does cause issues in how a map should look and work. Firstly, zooming. I should have the Cook Islands on the map, next to the Solomon Islands. So, how do you solve this? Well, in short, I couldn’t. I looked at plugins and repeatable things to no avail, so I tried something else.

I also tried MapBox as well which gave the same result, and others. In the end, I found MapLibre which is the open source fork of MapBox and I was able to test out my ideas with it.

Is it perfect? No, but it solves the one biggest problem of the Antimeridian, and now allows me to style the app properly.

This blog post was a big help in getting things up and running, but still created problems, as I wasn’t using a single page app.

To help people solve this problem for others, here’s my current example code for my maps page:

Just remember, this code is not final and is provided as an example, of which can and will be more optimised and change in the future.

Locations.ts


import { getLocaleDateFormat } from '@angular/common';
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import {  Map, NavigationControl,  } from 'maplibre-gl';  
import { PopoverController } from '@ionic/angular'; 

import { MappopoverComponent } from '../mappopover/mappopover.component';

@Component({
  selector: 'app-locations',
  templateUrl: './locations.page.html',
  styleUrls: ['./locations.page.scss'],
}) 

export class LocationsPage  implements AfterViewInit {

  constructor( 
    public popoverController: PopoverController) { }
   

  map: Map | undefined;
  data: any = [];

  propertyList = [];
  json: any = [];

  @ViewChild('locations') 
  private mapContainer: ElementRef<HTMLElement>;

  
  async presentPopover(description: string, country : string, location : string, station: string, frequency: string, status: string) {
    const popover = await this.popoverController.create({
      component: MappopoverComponent, 
      componentProps: {key1: description, key2: country, key3: location, key4: station, key5: frequency, key6: status} 
    });

    await popover.present();      
    
  } 
  
  // https://documentation.maptiler.com/hc/en-us/articles/4411342514193-How-to-display-a-map-in-Angular-using-MapLibre-GL-JS

  async ngAfterViewInit() {
 
    const initialState = {
      lng: 11,
      lat: 49,
      zoom: 9,
    };  
 
    const map = new Map({
      container: this.mapContainer.nativeElement,
      style: "https://api.maptiler.com/maps/streets/style.json?key=INSERT_YOUR_KEY",
      center: [initialState.lng, initialState.lat],
      zoom: initialState.zoom
    });
 
    
    //map.on('load', function() {

      await this.http.get("./assets/mapdata.geojson").subscribe((json: any) => {
        console.log(json);
      
            this.json = json.features;

            console.log(this.json);

            // 5 second delay
            setTimeout(function(){
              console.log("Executed after 5 seconds");

              map.addSource('places', {
                'type': 'geojson',
                'data': json
            }); 

            // Add a layer showing the places.
            map.addLayer({
              'id': 'places',
              'type': 'symbol',
              'source': 'places',
              'layout': {
                'icon-image': '{icon}_15'
                }
              }); 

              map.addControl(new NavigationControl());  

            }, 5000); 
    });
   
    // When a click event occurs on a feature in the places layer, open a popup at the
    // location of the feature, with description HTML from its properties.
    await map.on('click', 'places', (e) => {

        console.log(e);
        var description;
        var coordinates = e.features[0].geometry;    
        var country = e.features[0].properties.Country;
        var location = e.features[0].properties.Location;
        var station = e.features[0].properties.Station;
        var frequency = e.features[0].properties.Frequency;
        var status = e.features[0].properties.Status; 
        console.log(coordinates); 

        // Ensure that if the map is zoomed out such that multiple
        // copies of the feature are visible, the popup appears
        // over the copy being pointed to.
        while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
        coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
        }
        
        this.presentPopover(description, country, location, station, frequency, status);
    });
 
    map.resize();
    
  };

}

Locations.page.html


<div class="locations-container" #locations></div>
 

As for the popover problems, that is another journey, but you can follow this video tutorial for more info.

But, in short, you need to make a ‘component’. I called mine ‘mappopover’.

In the ‘Locations.page.html’ I have this:

<ion-content class="ion-padding">
    <b>Station:</b> {{key4}}<br>
    Country: {{key2}}<br>
    Location: {{key3}}<br>    
    Frequency: {{key5}}<br>
    Status: {{key6}}<br> 
</ion-content>

In the ‘mappopover.component.st’ I have this:

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-mappopover',
  templateUrl: './mappopover.component.html',
  styleUrls: ['./mappopover.component.scss'],
})
export class MappopoverComponent implements OnInit {

  @Input() key1: string;
  @Input() key2: string;
  @Input() key3: string;
  @Input() key4: string;
  @Input() key5: string;
  @Input() key6: string;

  constructor() { }

  ngOnInit() {
 
  } 
  
}

You’ll also need a geojson file with some points in it, and get an api key for the maps. You should be able to figure out the rest. This only took me about 3 or so days of tutorials and code browsing and trial and error. Good luck!

Here’s where I’m going with this in the end… A work in progress.

Also, as a side note, if you are planning on doing some GEOJSON things and want to see the code in real-time and be able to paste backwards and forth, this site is a must.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: