CSC/ECE 517 Spring 2015/oss S1504 AAC
About Sahana
Sahana Eden is an Open Source Humanitarian Platform which can be used to provide solutions for Disaster Management, Development, and Environmental Management sectors. Being open source, it is easily customisable, extensible and free. It is supported by the Sahana Software Foundation. Sahana Eden was first developed in Sri Lanka as a response to the Indian Ocean Tsunami in 2005. The code for the Sahana Eden project is hosted at Github and it is published under the MIT License. The demo version of Sahana Eden can be found here.
Eden is a flexible humanitarian platform with a rich feature set which can be rapidly customized to adapt to existing processes and integrate with existing systems to provide effective solutions for critical humanitarian needs management either prior to or during a crisis. Sahana Eden contains a number of different modules like 'Organization Registry', 'Project Tracking', 'Human Resources', 'Inventory', 'Assets', 'Assessments', 'Scenarios & Events', 'Mapping', 'Messaging' which can be configured to provide a wide range of functionality. We are contributing to Sahana Eden as a part of our Object-Oriented Design and Development's Open-Source Software (OSS) Project. In this Wiki Page, we would be explaining the goals of our project and how we implemented them.
Goals of the project
- Extend upon the Geonames searchCombo feature embedded in Sahana map module
- Search the internal gis_locations table for a partial match of the entered string in the search field
- Partial matches should populate in the autocomplete drop down
- When user selects a result from the drop down, zoom to that location
- Default to geonames.org when internal search returns no results
Implementation
The implementation of the zoom to location feature is contained entirely in the search_gis_locations() function in controllers/gis.py and static/scripts/gis/GeoExt/ux/GeoNamesSearchCombo.js. The only significant change to the GeoNamesSearchCombo.js was the url option which makes the search box call the search_gis_locations() method instead of querying the geonames.org website for a search location (see lines 220-221 on the pull request page).
The search_gis_locations() function primarily uses an adapter design. It allows the GeoNamesSearchCombo.js to query the internal database and geonames.org for data during a user search.
The implementation of the search_gis_locations() function can be broken down into two sections: Searching the Internal database, and Searching Geonames.
Notes:
- Any groups wishing to extend, understand, or review the zoom to location feature enhancement should see this github pull request: https://github.com/flavour/eden/pull/1075/files
- For tracking purposes, the dialog for this project can be found on this google groups thread: https://groups.google.com/forum/#!topic/sahana-eden/vS54iJEDqvQ
Searching the Internal database
The following code searches the 'name' column in the gis_location database for items "starting with" the string entered in the search field of the map page on Eden.
Here we are getting the parameters from the url. The user string is stored in a parameter called "name_startsWith". Because eden expects a JSONP response, a callback function is needed. The name of the call back function is stored in the "callback_func" parameter of the url. We also select the "gis_location" table to get ready for the query.
#Get vars from url user_str = get_vars["name_startsWith"] callback_func = request.vars["callback"] atable = db.gis_location
Next we perform the query on the "name" column of the gis_location table. Note that the "%" is a wildcard and allows us to do a partial match with the string the user entered into the search field. The rows variable selects the fields of our interest. For the search, we only care about the entry id, zoom level (aka. level), name, latitude, and longitude.
We also count and store the number of results from the search since it's a required field for the search box.
query = atable.name.lower().like(user_str + '%') rows = db(query).select(atable.id, atable.level, atable.name, atable.lat, atable.lon ) results = [] count = 0 for row in rows: count += 1 result = {}
#Convert the level colum into the ADM codes geonames returns #fcode = row["gis_location.level"] level = row["gis_location.level"] if level=="L0": #Country fcode = "PCL" #Zoom 5 elif level=="L1": #State/Province fcode = "ADM1" elif level=="L2": #County/District fcode = "ADM2" elif level=="L3": #Village/Suburb fcode = "ADM3" else: #City/Town/Village fcode = "ADM4" result = {"id" : row["gis_location.id"], "fcode" : fcode, "name" : row["gis_location.name"], "lat" : row["gis_location.lat"], "lng" : row["gis_location.lon"]} results.append(result)
Searching Geonames
If the initial search for the user query on the internal 'Locations' table, then a search is done on Geonames as a fallback. This fallback search is implemented within the 'gis' controller using 'urllib2' an extensible Python module for opening URLs. The base URL for the Geonames search is http://ws.geonames.org/searchJSON?. The searchJSON action expects the Geonames username, the prefix of the location names to be searched, number of rows in the JSON result, etc. as parameters. The following code performs the HTTP GET request and loads the JSON response in a dictionary.
username = settings.get_gis_geonames_username() maxrows = "20" lang = "en" charset = "UTF8" nameStartsWith = user_str geonames_base_url = "http://ws.geonames.org/searchJSON?" url = "%susername=%s&maxRows=%s&lang=%s&charset=%s&name_startsWith=%s" % (geonames_base_url,username,maxrows,lang,charset,nameStartsWith) response = urllib2.urlopen(url) dictResponse = json.loads(response.read().decode(response.info().getparam('charset') or 'utf-8')) response.close()
The JSON object in the response has two keys 'totalResultsCount' and 'geonames'. The object corresponding to the 'geonames' key is an array of the Geonames search results, each of which is a dictionary in itself. Of all the Keys present in a single result dictionary, the relevant ones are 'id', 'fcode', 'name', 'lat' and 'lng'. The relevant keys are extracted and a new dictionary is created for each search result. The array of new dictionaries is then returned as a response. The following code performs the decoding of the JSON object, creating new dictionary objects with the relevant keys and then collecting them as a single array.
results = [] if dictResponse["totalResultsCount"] != 0: geonamesResults = dictResponse["geonames"] for geonamesResult in geonamesResults: result = {} result = {"id" : int(geonamesResult["geonameId"]), "fcode" : str(geonamesResult["fcode"]), "name" : str(geonamesResult["name"]),"lat" : float(geonamesResult["lat"]), "lng" : float(geonamesResult["lng"])} results.append(result)