API:Nearby places viewer

From mediawiki.org

Overview[edit]

In this tutorial, you will learn how to search for wiki pages near your location using the MediaWiki Action API.

This tutorial will teach you how to do this using:

A step-by-step process for building this application[edit]

Step 1: Set up Python and Flask development environment[edit]

To set up the Python development environment for a Flask application, you will need to install Python, create a virtual environment, and install Flask.

This application uses Python3, the recommended version for new Python projects. Learn more about the differences between Python2 and Python3 here. To install Python3 on your local machine, follow step-by-step instructions in these installation guides.

Here is how to set up the development environment for building the application:

$ mkdir nearby-places-viewer
$ cd nearby-places-viewer/
This will create a new directory and change into it
$ python3 --version #Python 3.6.5
This command checks your Python version 
$ python3 -m venv venv
This command will create a virtual environment named 'venv'
$ source venv/bin/activate
This will activate the virtual environment
$ pip install Flask
This command will install the Flask package with all its dependencies

Step 2: Create a simple Flask application[edit]

Render a simple static page[edit]

Place the following code in $HOME/nearby-places-viewer/nearby.py

#!/usr/bin/python3

"""
    nearby.py

    MediaWiki Action API Code Samples
    
    Nearby places viewer app: Demo of geo search for wiki pages 
    near a location using the Geolocation API and MediaWiki Action 
    API's Geosearch module. 

    MIT license
"""

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    """ Displays the index page accessible at '/'
    """
    return render_template('places.html')

if __name__ == '__main__':
    app.run()

Drop this one line of code <h1>Nearby places viewer</h1> in a HTML file inside the templates folder: $HOME/nearby-places-viewer/templates/places.html

Note: In this simple application, we are using render_template method which renders the template named places.html from the templates directory.

Next, run your Flask app with the command python nearby.py and open http://127.0.0.1:5000/ to view your app in the browser. You should be able to see "Nearby places viewer" in your browser window.

Style your app[edit]

Let's do some app styling. Add a button element for accepting search input in the HTML file and link tags to load an external and internal stylesheet. External stylesheet, in this case, is the URL of a CSS file for the font Amatic.

Replace the existing code in $HOME/nearby-places-viewer/templates/places.html with the following:

<link rel="stylesheet" href="//tools-static.wmflabs.org/fontcdn/css?family=Amatic+SC:700">
<link rel="stylesheet" href="/static/style.css">

<h1>Nearby places viewer</h1>
<button>Click here to search</button>

Place the following code in $HOME/nearby-places-viewer/static/style.css

h1 {
    font-family: 'Amatic SC', cursive;
    font-size: 2.5em;
    font-weight: normal;
    color: black;
}

button {
    font-size: 16px;
    padding: 10px 25px;
    cursor: pointer;
    text-decoration: none;
    color: white;
    border-radius: 4px;
    background-color: #7c7ce0;
    margin-bottom: 20px;
}
Nearby places viewer demo app screenshot (1)

Application layout[edit]

$HOME/nearby-places-viewer
├── templates/
│   └── places.html
├── static/
│   └── static.css
├── nearby.py
└── venv/

Step 3: Obtain coordinates of your current location[edit]

To search for wiki pages nearby, you first need to obtain coordinates of your current location. To do so, you can use the Geolocation API along with some JavaScript code.

When you click the Click here to search button, the app makes a call to the Geolocation API and retrieves the current position of your device via the API's Navigator.geolocation object. The API's response in return is a Position object from which you can obtain the latitude and longitude.

Note: When your app makes a call to the API you will be notified and asked to grant permission to your browser to access your location.

Place the following code in $HOME/nearby-places-viewer/static/places.js

$( document ).ready(function() {
	var x = document.getElementById( "places-list" );
	
	$( 'button' ).click(function() { 
		getLocation(); 
	});

	function getLocation() {
		x.innerHTML = "Searching your location..";

		if (navigator.geolocation) {
			navigator.geolocation.getCurrentPosition(fetchPlaces);
		} else { 
			x.innerHTML = "Geolocation is not supported by this browser.";
		}
	}

	function fetchPlaces(position) {
		x.innerHTML = position.coords.latitude + "|" + position.coords.longitude;
	}
});

Link the custom JavaScript /static/places.js and jQuery from the HTML file: $HOME/nearby-places-viewer/templates/places.html

<!-- Add these two lines at the top -->
<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/places.js"></script> 

<!-- Add this line after the button element -->
<div id="places-list"></div>

Step 4: Send location data to the server with AJAX[edit]

This app uses jQuery's AJAX method to send the location data obtained in Step 3 to the server and POST it without page refresh to a Python Flask route / in $HOME/nearby-places-viewer/nearby.py.

As a next step, let's add an AJAX call to the fetchPlaces function in $HOME/nearby-places-viewer/static/places.js.

Note: At this point, if you try to run your app, it is likely that you will see an error in the browser window as we have not added support to the route / to handle POST requests.

function fetchPlaces(position) {
	var data = { 
		"latitude": position.coords.latitude, 
		"longitude": position.coords.longitude
	};

	$.ajax({
		url: "/",
		type: "POST",
		data: JSON.stringify(data),
		contentType: "application/json",
		dataType: "json",

		success: function (response) { 
			x.innerHTML = "Success!";
		}, 
		error: function () { 
			x.innerHTML = "An error occurred while fetching places!"; 
		}
	});
}

Step 5: Fetch nearby places via MediaWiki Action API[edit]

First, extend the Python Flask route / in $HOME/nearby-places-viewer/nearby.py to handle POST requests. You can do so by adding both GET and POST in the methods argument list in the route decorator. Next, you can obtain location data available in JSON format from the request object and pass it to fetch_places_nearby() function for further processing.

@app.route('/', methods=['GET', 'POST'])
def index():
    """ Displays the index page accessible at '/'
    """

    if request.method == "POST":
        data = request.get_json()
        latitude = data['latitude']
        longitude = data['longitude']

        results = fetch_places_nearby(latitude, longitude)
        return jsonify(results=results)

    return render_template('places.html')

The code in the fetch_places_nearby() function makes a GET request to the Action API to search for wiki pages near a location. API call consists of an endpoint https://en.wikipedia.org/w/api.php and query string parameters. Some of the key parameters are:

  • action=query main module to query information
  • generator=geosearch query module's submodule list used as a generator module to get search results for a set of pages
  • prop=coordinates|pageimages|description|info tells which properties to return for pages

Note: For more information on the geosearch module visit API:Geosearch .

def fetch_places_nearby(lat, lon):
    params = {
        "action": "query",
        "prop": "coordinates|pageimages|description|info",
        "inprop": "url",
        "pithumbsize": 144,
        "generator": "geosearch",
        "ggsradius": 10000,
        "ggslimit": 10,
        "ggscoord": str(lat) + "|" + str(lon),
        "format": "json",
    }

    res = SESSION.get(url=API_ENDPOINT, params=params)
    data = res.json()
    places = data['query'] and data['query']['pages'] 
    # TODO: further process 'places' list
    
    return places

View complete Python and Flask code with import statements and processed places list. This app uses Haversine package available in Python to calculate distance between two geographic coordinates.

$HOME/nearby-places-viewer/nearby.py
#!/usr/bin/python3

"""
    nearby.py

    MediaWiki Action API Code Samples

    Nearby places viewer app: Demo of geo search for wiki pages near a location using
    the Geolocation API and MediaWiki Action API's Geosearch module.

    MIT license
"""

from flask import Flask, request, render_template, jsonify
import requests
from haversine import haversine


APP = Flask(__name__)
SESSION = requests.Session()
API_ENDPOINT = 'https://en.wikipedia.org/w/api.php'


@APP.route('/', methods=['GET', 'POST'])
def index():
    """ Displays the index page accessible at '/'
    """

    if request.method == "POST":
        data = request.get_json()
        latitude = data['latitude']
        longitude = data['longitude']

        results = fetch_places_nearby(latitude, longitude)
        return jsonify(results=results)

    return render_template('places.html')

def fetch_places_nearby(lat, lon):
    """ Fetches nearby places via MediaWiki Action API's Geosearch module
    """
    params = {
        "action": "query",
        "prop": "coordinates|pageimages|description|info",
        "inprop": "url",
        "pithumbsize": 144,
        "generator": "geosearch",
        "ggsradius": 10000,
        "ggslimit": 10,
        "ggscoord": str(lat) + "|" + str(lon),
        "format": "json",
    }

    res = SESSION.get(url=API_ENDPOINT, params=params)
    data = res.json()
    places = data['query'] and data['query']['pages']
    results = []

    for k in places:
        title = places[k]['title']
        description = places[k]['description'] if "description" in places[k] else ''
        thumbnail = places[k]['thumbnail']['source'] if "thumbnail" in places[k] else ''
        article_url = places[k]['fullurl']

        cur_loc = (lat, lon)
        place_loc = (places[k]['coordinates'][0]['lat'], places[k]['coordinates'][0]['lon'])

        distance = round(haversine(cur_loc, place_loc, unit='mi'), 2)

        results.append({
            'title': title,
            'description': description,
            'thumbnail': thumbnail,
            'articleUrl': article_url,
            'distance': distance
        })

    return results

if __name__ == '__main__':
    APP.run(debug=True)

Step 6: Create a user interface for list of places from JSON response[edit]

Handle the returned JSON data from the server in AJAX's success callback and use HTML DOM innerHTML property to alter the div contents.

success: function (response) { 
	var places = response["results"],
		no_thumb = "..";

	x.innerHTML = "";
	
	for (var p in places) {
		var thumbnail = places[p].thumbnail || no_thumb;

		x.innerHTML += "<div class=\"item\"><div class=\"col-xs-8 no-padding\"><h5><a href=\"" +
			places[p]["articleUrl"] + "\" target=\"_blank\">" +
			places[p]["title"] + "</a></h5><p>" +
			places[p]["description"] + "</p><span>📍" + places[p]["distance"] +
			" miles</p></div><div class=\"col-xs-4 no-padding\"><img src=\"" +
			thumbnail + " \"></div></div>";
	}
}

View complete JavaScript code with AJAX success callback.

$HOME/nearby-places-viewer/static/places.js
$( document ).ready(function() {
	var x = document.getElementById( "places-list" );
	
	$( ".btn-search" ).click(function() { 
		getLocation(); 
	});

	function getLocation() {
		x.innerHTML = "Searching your location..";

		if (navigator.geolocation) {
			navigator.geolocation.getCurrentPosition(fetchPlaces);
		} else { 
			x.innerHTML = "Geolocation is not supported by this browser.";
		}
	}

	function fetchPlaces(position) {
		var data = { 
			"latitude": position.coords.latitude, 
			"longitude": position.coords.longitude
		};

		$.ajax({
			url: "/",
			type: "POST",
			data: JSON.stringify(data),
			contentType: "application/json",
			dataType: "json",

			success: function (response) { 
				var places = response["results"],
					no_thumb = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Gnome-image-missing.svg/200px-Gnome-image-missing.svg.png";

				x.innerHTML = "";
				
				for (var p in places) {
					var thumbnail = places[p].thumbnail || no_thumb;

					x.innerHTML += "<div class=\"item\"><div class=\"col-xs-8 no-padding\"><h5><a href=\"" +
						places[p]["articleUrl"] + "\" target=\"_blank\">" +
						places[p]["title"] + "</a></h5><p>" +
						places[p]["description"] + "</p><span>📍" + places[p]["distance"] +
						" miles</p></div><div class=\"col-xs-4 no-padding\"><img src=\"" +
						thumbnail + " \"></div></div>";
				}
			}, 
			error: function () { x.innerHTML = "An error occurred while fetching places!"; }
		});
	}
});

Step 7: Style changes with Bootstrap[edit]

You might have noticed that the code snippet in Step 6 uses Bootstrap class names. Yes, we are integrating the Bootstrap framework in this step to add a responsive layout design to the Places UI. To do so, let's make some more tweaks to CSS and HTML files.

View complete CSS and HTML code.

$HOME/nearby-places-viewer/static/style.css
.places-container .no-padding {
    padding: 0;
}

.places-container .info {
    text-align: center;
}

.places-container .viewer-heading {
    font-family: 'Amatic SC', cursive;
    font-size: 2.5em;
    font-weight: normal;
    color: black;
}

.places-container .btn-search {
    font-size: 16px;
    padding: 10px 25px;
    cursor: pointer;
    text-decoration: none;
    color: white;
    border-radius: 4px;
    background-color: #7c7ce0;
}

.places-container .list {
    margin-top: 20px;
}

.places-container .item {
    min-height: 100px;
}

.places-container .item p,
span {
    font-size: 12px;
    margin: 2px;
}

.places-container .item span {
    color: gray;
}

.places-container .item img {
    float: right;
    width: 80px;
    height: 80px;
    border-radius: 5px;
    object-fit: cover;
}

.places-container .item a {
    color: #7c7ce0;
}
$HOME/nearby-places-viewer/templates/places.html
<title>Nearby places viewer</title>

<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="/static/places.js"></script>

<link rel="stylesheet" href="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="//tools-static.wmflabs.org/fontcdn/css?family=Amatic+SC:700">
<link rel="stylesheet" href="/static/style.css">

<div class="container places-container col-lg-4 col-xs-12">
  <div class="col-xs-12 no-padding info">
    <h1 class="viewer-heading">Nearby places viewer</h1>
    <button class="btn-search">Click here to search</button>
  </div>
  <div class="col-xs-12 no-padding list" id="places-list">
  </div>
</div>
Nearby places viewer demo app screenshot (2)

Next steps[edit]

See also[edit]