Using the Javascript Geolocation API with Alpine.js and OpenLayers

No screencast available yet for this post.

We have a very nice and functional map now, but what about locating the user? In this (short) lesson, we will add a "Go to my location" button on the map so the user can automatically pan and zoom to its current location. The best way to get the user's location on the browser side is to use the built-in Javascript Geolocation API. This API will use several techniques to get the user's device's best location (GPS, WIFI, or IP location). As it can be a privacy breach, the position will only be available if the user accepts granting this website permission.

  • To play around with the API, add a new button to the map to call a gotoCurrentLocation() function. To do so, make the following changes to the resources/views/components/map.blade.php file:
<div x-data="map()" x-init="initComponent()" x-on:new-monument.window="gotoMonument(JSON.parse($event.detail.monument))">
    <div x-ref="map" class="relative h-[600px] overflow-clip rounded-md border border-slate-300 shadow-lg">
        <div class="absolute top-2 right-8 z-10 rounded-md bg-white bg-opacity-75">
            <div class="ol-unselectable ol-control">
                <button x-on:click.prevent="legendOpened = ! legendOpened" title="Open/Close details"
                    class="absolute inset-0 flex items-center justify-center">
                    <!-- Heroicon name: outline/globe -->
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 pl-0.5" fill="none" viewBox="0 0 24 24"
                        stroke="currentColor" stroke-width="1">
                        <path stroke-linecap="round" stroke-linejoin="round"
                            d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                    </svg>
                </button>
            </div>
        <div class="ol-unselectable ol-control absolute top-9 right-8 z-10 rounded-md bg-white bg-opacity-75">
            <button x-on:click.prevent="gotoCurrentLocation()" title="Go to my location"
                class="absolute inset-0 flex items-center justify-center">
                <!-- Heroicon name: outline/mappin -->
                <svg class="h-5 w-5 pl-0.5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"></path>
                    <path stroke-linecap="round" stroke-linejoin="round"
                        d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"></path>
                </svg>
            </button>
        </div>
        <div class="ol-unselectable ol-control absolute top-2 right-8 z-10 rounded-md bg-white bg-opacity-75">
            <button x-on:click.prevent="legendOpened = ! legendOpened" title="Open/Close details"
                class="absolute inset-0 flex items-center justify-center">
                <!-- Heroicon name: outline/globe -->
                <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 pl-0.5" fill="none" viewBox="0 0 24 24"
                    stroke="currentColor" stroke-width="1">
                    <path stroke-linecap="round" stroke-linejoin="round"
                        d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
            </button>
        </div>

        <div x-cloak x-show="legendOpened" x-transition:enter="transition-opacity duration-300"
            x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
            x-transition:leave="transition-opacity duration-300" x-transition:leave-start="opacity-100"
            x-transition:leave-end="opacity-0"
            class="absolute right-0 top-16 left-2 bottom-2 z-10 max-w-sm rounded-md border border-slate-300 bg-white bg-opacity-50 shadow-sm">
(...)
  • We now have a map with an extra "Go to my location" button: The Javascript Geolocation API

  • This button is calling a gotoCurrentLocation() function on the map component; let's create it with the following code in the resources/js/components/map.js file:

(...)
            stopDrawMonument() {
                this.source.clear();

                this.map.removeInteraction(this.draw);

                this.mode = "view";
            },
            gotoCurrentLocation() {
                // We first make sure the browser supports geolocation
                if ("geolocation" in navigator) {
                    // geolocation is available
                    navigator.geolocation.getCurrentPosition((position) => {
                        console.log(position)
                    });
                }
            }
        };
    });
});
  • When clicking on the new button, the browser will ask the user to grant access to the location (as stated at the beginning of the lesson): The Javascript Geolocation API
  • If the user clicks "Block," nothing will happen, and we won't be able to access the user's position with Javascript. However, if the user clicks "Allow," we should have access to the GeolocationPosition object and display it in the console of the browser's Developer's Tools: The Javascript Geolocation API
  • As you can see, even on a development desktop (without a GPS device), we get a location back with longitude and latitude. Our problem is that the navigator.geolocation.getCurrentPosition function accepts a callback, and in this callback, we don't have our component context. To get around this problem and center the map to the location, we will use a Javascript Promise object in a getPosition function on the component:
(...)
            stopDrawMonument() {
                this.source.clear();

                this.map.removeInteraction(this.draw);

                this.mode = "view";
            },
            gotoCurrentLocation() {
                // We first make sure the browser supports geolocation
                if ("geolocation" in navigator) {
                    // geolocation is available
                    navigator.geolocation.getCurrentPosition((position) => {
                        console.log(position)
                    });
                    this.getPosition()
                        .then((position) => {
                            this.map.getView().animate({
                                center: [position.coords.longitude, position.coords.latitude],
                                zoom: 16,
                                duration: 1000,
                            });
                        })
                }
            }
            },
            getPosition(options = {
                maximumAge: 0,
                timeout: 5000,
                enableHighAccuracy: true
            }) {
                return new Promise((resolve, reject) =>
                    navigator.geolocation.getCurrentPosition(resolve, reject, options)
                );
            }
        };
    });
});
  • Let's also put the initial center of the map back to 0, 0 with a zoom level of 3 so we can see all of the world's extent:
(...)
                this.map = new Map({
                    target: this.$refs.map,
                    layers: [
                        new TileLayer({
                            source: new OSM(),
                            label: 'OpenStreetMap',
                        }),
                        worldAdministrativeBoundariesLayer,
                        worldRiversLayer,
                        this.monumentsLayer,
                        drawLayer
                    ],
                    view: new View({
                        projection: "EPSG:4326",
                        center: [-78.2161, -0.7022],
                        zoom: 8,
                        center: [0, 0],
                        zoom: 3,
                    }),
                    overlays: [
                        new Overlay({
                            id: 'info',
                            element: this.$refs.popup,
                            stopEvent: true,
                        }),
                    ],
                });
(...)

  • Great, we have a button to go to the current user's location automatically! It is to be noted that we are not doing any error handling; if an error occurs (or if geolocation API isn't available), we don't do anything, which is fine for me at this point.

The Javascript Geolocation API also supports another function to track the user's position (for when the user is moving): watchPosition(), but we will not use it in the current project; you can see the API's complete documentation on the MDN Web Docs site. In a future lesson, we will also cover another way to locate the user, IP-based and server side with an external service. In the following lecture, we will see how to add more base data to the map with XYZ and Bing data.

The commit for this post is available here: javascript-geolocation-api

First published 1 year ago
Latest update 1 year ago
Sergio
Posted by Sergio 9 months ago

Hola!

First of all, many thanks for this detailed tutorial. I'm planning a project that will use these features but on a small scale (only

covering Portugal and maybe its islands) so I think that adding GeoServer to the mix would be a complication as I don't need it's powerful features, the project would be used only by a few people (<50).

For managing the data, I will be using Filament with an approach similar to its community plugin to

manage Google Maps.

In your experience, do you think this is a sound approach, or I'll be sure to need GeoServer from the start for reasons I can't see yet?


No response yet
You need to be signed in to post comments, you can sign in here if you already have an account or register here if you don't.