labnotes

Playing with openrouteservice-js

learning about mapping

tags
leaflet
openrouteservice
openstreetmap

Get an api key

Go to signup page of openrouteservice, and accept the terms of service. I'm storing it in apiKey.js, as

1
  export default '5b3c...';

Setting up leaflet

Lets start coding. Lets first get a map that we can display.

1
  npm i leaflet vite

map-view.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  import 'leaflet/dist/leaflet.css';
  import L from 'leaflet';

  class MapView extends HTMLElement {
      static get observedAttributes() {
          return ["latlon"];
      }
      
      connectedCallback() {
          this.insertAdjacentHTML( 'beforeend', '<div id="map" style="height:100%"></div>' );

          this.setLatLon();
          
          this.map = L.map('map', {
              center: [ this.lat, this.lon ],
              zoom: 11,
              zoomControl: true
          });

          L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
              maxZoom: 19,
              attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
          }).addTo(this.map);

          L.control.scale({
              imperial: true,
              maxWidth: 300
          }).addTo(this.map);

          this.setMarker();
      }

      setLatLon() {
          let latlon = this.getAttribute( 'latlon' ) || '41.84371,-73.32928';

          let s = latlon.split( "," );
          this.lat = s[0];
          this.lon = s[1];
          this.setMarker()
      }

      attributeChangedCallback( name ) {
          if( name == 'latlon' ) {
              this.setLatLon();
          }
      }

      setMarker() {
          if( this.map ) {
              this.map.setView( [this.lat, this.lon] );
              if( this.marker ) {
                  this.marker.remove();
              }

              this.marker = L.marker([this.lat, this.lon]).addTo(this.map);
          }
      }
  }

  customElements.define( 'map-view', MapView )

Which we can use with

<map-view></map-view>

Which we can see by

1
  npx vite

And then opening up http://localhost:5173/map-view.html

Location lookup

Here we are going to use the Geocode api to lookup a place based upon the name we enter. We'll display the list of results and fire off an event once the user selects something.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
  import Openrouteservice from 'openrouteservice-js'
  import apiKey from './apiKey.js'

  // So we don't do a zillion searches
  export function debounce(func, timeout = 500){
      let timer;
      return (...args) => {
          clearTimeout(timer);
          timer = setTimeout(() => { func.apply(this, args); }, timeout);
      };
  }

  const Geocode = new Openrouteservice.Geocode({
      api_key: apiKey
  })

  class LocationLookup extends HTMLElement {
      connectedCallback() {
          const text = this.getAttribute( 'prompt' ) || "Location Search";
          
          this.insertAdjacentHTML( 'beforeend', `
            <p>${text}</p>
            <form>
              <input
                type="text"
                id="location"
                placeholder="Location"
                class="border-2 rounded"/>
            </form>
            <ul>
            </ul>
  `);
          this.location = this.querySelector( 'input' );

          const processChange = debounce(() => this.lookupLocation());    
          this.location.addEventListener( 'keyup', processChange );

          this.results = this.querySelector( 'ul' );
          this.results.addEventListener( "click", this.fireEvent );
      }

      async lookupLocation() {
          this.results.innerHTML = `<li>Loading up ${location.value}</li>`
        
          const json = await Geocode.geocode( {text: this.location.value} )

          this.results.innerHTML = ""
          for( let i = 0; i < json.features.length; i++ ) {
              const feature = json.features[i];
              this.results.innerHTML +=
                  `<li><a
                         data-lat="${feature.geometry.coordinates[1]}"
                         data-lon="${feature.geometry.coordinates[0]}"
                         class="text-blue-700">${feature.properties.label}</a></li>`
          }
      }

      fireEvent( event ) {
          let location = event.target.dataset;
          const myevent = new CustomEvent("location", {
              bubbles: true,
              detail: location
          });
          this.dispatchEvent( myevent );
      }
  }

  customElements.define( 'location-lookup', LocationLookup )

We can combine this with the map-view component. We'll add an event listener and when the location is selected we will update the attribute of the map, which will set the marker in the right place.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  <html>
    <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <script src="location-lookup.js" type="module"></script>
      <script src="map-view.js" type="module"></script>
      <script src="https://cdn.tailwindcss.com"></script>
    </head>
    <body>
      <div class="flex h-screen">
        <div class="w-64 p-4">
          <location-lookup id="from" prompt="Starting Location"></location-lookup>
        </div>
        <div id="map" class="w-full vh">
          <map-view></map-view>
        </div>
      </div>

      <script>
        const ll = document.getElementById( "from" )
        const mv = document.querySelector( "map-view" )

        ll.addEventListener( 'location', (e) => {
          const latlon = `${e.detail.lat},${e.detail.lon}`

          mv.setAttribute( "latlon", latlon );
        } );
        
      </script>
    </body>
  </html>

Check it: http://localhost:5173/location-lookup.html

Route planning

Lets take a look at how to build a route planner.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
  import Openrouteservice from 'openrouteservice-js'
  import apiKey from './apiKey.js'

  const Directions = new Openrouteservice.Directions({
      api_key: apiKey
  });

  export default class FindRoute extends HTMLElement {
      static get observedAttributes() {
          return ["from-latlon", "to-latlon" ];
      }
      
      attributeChangedCallback( name ) {
          if( name == 'from-latlon' ) {
              this.fromlatlon = this.getAttribute( "from-latlon" );
          }
          
          if( name == 'to-latlon' ) {
              this.tolatlon = this.getAttribute( "to-latlon" );
          }
          
          if( this.tolatlon && this.fromlatlon ) {
              this.doLookup();
          }
          
          this.render();
      }
      
      parseComma( latlon ) {
          let pair = latlon.split( "," );
          pair[0] = parseFloat(pair[0]);
          pair[1] = parseFloat(pair[1]);
          // hmm
          let swap = pair[0]
          pair[0] = pair[1]
          pair[1] = swap
          return pair;
      }

      async doLookup() {
          if( this.started == true ) {
              // To nothing
          }

          this.started = true;
          
          let response = await Directions.calculate({
              coordinates: [
                  this.parseComma(this.fromlatlon),
                  this.parseComma(this.tolatlon)
              ],
              profile: 'driving-car',
              format: 'json',
              api_version: 'v2'
          });

          console.log( "Directions response", response );

          this.started = false;
          this.response = response;
          this.render();
      }

      render() {
          let status = `
    <p>${this.fromlatlon}</p>
    <p>${this.tolatlon}</p>
    <p>${this.started ? "Loading" : "Done"}</p>
  `
          if( this.response ) {
              status += '<table>';
              status += '<tr><th>Instruction</th><th>Distance</th><th>Duration</th><th>Name</th></tr>';
              
              for( let segment of this.response.routes[0].segments[0].steps ) {
                  status += `<tr>
  <td>${segment.instruction}</td>
  <td>${segment.distance}</td>
  <td>${segment.duration}</td>
  <td>${segment.name}</td>
  </tr>`
              }
          }
                
          this.innerHTML = status
            
      }
  }

  customElements.define( 'find-route', FindRoute )

And http://localhost:5173/find-route.html

Drawing the route on the map

The geometry that comes back from the routing system is compressed, and we'll need to pull in a library to get out the details.

1
  npm i @mapbox/polyline

Now we can take that and mount in the polyline where the car is driving. I'm extending the previous component so it uses the same logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  import FindRoute from './find-route.js';
  import polyline from '@mapbox/polyline';
  import 'leaflet/dist/leaflet.css';
  import L from 'leaflet';

  class RouteDisplay extends FindRoute {
      connectedCallback() {
          this.insertAdjacentHTML( 'beforeend', '<div id="map" style="height:100%"></div>' );

          this.map = L.map('map', {
              //center: [ this.lat, this.lon ],
              zoom: 11,
              zoomControl: true
          });
          
          L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
              maxZoom: 19,
              attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
          }).addTo(this.map);
          
          L.control.scale({
              imperial: true,
              maxWidth: 300
          }).addTo(this.map);
      }

      render() {
          if( !this.response || !this.map) {
              return
          }
          
          const geometry = this.response.routes[0].geometry
          const decodedGeometry = polyline.decode(geometry);

          if( this.map && this.polyline ) {
              this.map.removeLayer( this.polyline )
              this.polyline = undefined;
          }
          
          this.polyline = L.polyline(decodedGeometry, {color: 'red'}).addTo(this.map);

          // zoom the map to the polyline
          this.map.fitBounds(this.polyline.getBounds());

        }
    }

    customElements.define( 'route-display', RouteDisplay )

Isochrones

Lets map out how far you can drive from a certain place on a map.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
  import Openrouteservice from 'openrouteservice-js'
  import apiKey from './apiKey.js'
  import 'leaflet/dist/leaflet.css';
  import L from 'leaflet';

  const Isochrones = new Openrouteservice.Isochrones({
      api_key: apiKey
  })

  class IsochroneMap extends HTMLElement {
      static get observedAttributes() {
          return ["latlon"];
      }

      attributeChangedCallback( name ) {
          if( name == 'latlon' ) {
              this.setLatLon();
          }
      }

      setMarker() {
          if( this.map && this.lat && this.lon ) {
              this.map.setView( [this.lat, this.lon] );
              if( this.marker ) {
                  this.marker.remove();
              }
              
              this.marker = L.marker([this.lat, this.lon]).addTo(this.map);
          }
      }
      
      
      setLatLon() {
          let latlon = this.getAttribute( 'latlon' ) || '41.84371,-73.32928';
          
          let s = latlon.split( "," );
          this.lat = s[0];
          this.lon = s[1];
          this.setMarker()

          this.lookup();
      }

      lookup() {
          if( this.lat && this.lon ) {
              console.log( "Doing lookup" );
              Isochrones.calculate( {
                  profile: 'driving-car',
                  locations: [[this.lon, this.lat]],
                  range: [50000],
                  range_type: 'distance',
                  area_units: 'm'
              } ).then( (json) => {
                  console.log( json );
                  this.response = json;

                  let c = json.features[0].geometry.coordinates[0];
                  this.latlngs = []
                  for( let i = 0; i < c.length; i++ ) {
                      this.latlngs.push([c[i][1],c[i][0]]);
                  }
                  
                  if( this.map && this.polygon ) {
                      this.map.removeLayer( this.polygon )
                      this.polygon = undefined;
                  }

                  this.polygon = L.polygon(this.latlngs, {color: 'red'}).addTo(this.map);
                  this.map.fitBounds(this.polygon.getBounds());
              })
          }
      }

      connectedCallback() {
          this.insertAdjacentHTML( 'beforeend', '<div id="map" style="height:100%"></div>' );

          this.setLatLon();
          
          this.map = L.map('map', {
              center: [ this.lat, this.lon ],
              zoom: 11,
              zoomControl: true
          });
          
          L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
              maxZoom: 19,
              attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
          }).addTo(this.map);
          
          L.control.scale({
              imperial: true,
              maxWidth: 300
          }).addTo(this.map);
      }
  }
      
  customElements.define( 'isochrone-map', IsochroneMap );

Points of interest

We can also look for places of interest on the map. Looking around in the north east states, there isn't a whole lot but it's start!

pois.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
  import Openrouteservice from 'openrouteservice-js'
  import apiKey from './apiKey.js'

  const Pois = new Openrouteservice.Pois({
      api_key: apiKey
  })

  class PoisMap extends HTMLElement {
      static get observedAttributes() {
          return ["latlon"];
      }
      
      attributeChangedCallback( name ) {
          if( name == 'latlon' ) {
              this.setLatLon();
          }
      }

      connectedCallback() {
          this.setLatLon();
          }

      setLatLon() {
          let latlon = this.getAttribute( 'latlon' ) || '41.84371,-73.32928';
          
          let s = latlon.split( "," );
          this.lat = parseFloat(s[0]);
          this.lon = parseFloat(s[1]);
          
          this.lookup();
      }

      async lookup() {
          let box = [[this.lon, this.lat],
                     [this.lon - 0.05, this.lat - 0.05]]

          Pois.pois({
              geometry: {
                  bbox:box,
                  geojson:{
                      type:"Point",
                      coordinates:box[0],
                  },
                  buffer:250
              },
              timeout: 20000
          }).then( (json) => {
              console.log( json );
              this.response = json;

              this.render();
          })
      }

      render() {
          let stats = "<table><tr><th>Name</th><th>Category</th><th>Coors</th></tr>";

          if( this.response ) {
              for( let poi of this.response.features ) {
                  console.log( poi );
                  let name = ""
                  if( poi.properties.osm_tags ) {
                      name = poi.properties.osm_tags.name;
                  }
                  stats += `<tr>
  <td>${name}</td>
  <td>${JSON.stringify(poi.properties.category_ids)}</td>
  <td>${poi.geometry.coordinates}</td>
  </tr>`;
              }
          }

          stats += "</table>";
          this.innerHTML = stats;
      }
  }

  customElements.define( 'pois-map', PoisMap );

Previously

fragments

Things I love about my phone

tags

Next

labnotes

shoelace and vite

adding static assets

tags
shoelace
vite