CSC/ECE 517 Spring 2015/oss S1504 AAC

From Expertiza_Wiki
Jump to navigation Jump to search

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:


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.

This code gets 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 the code performs 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 = {}

To parallel the level codes returned from geonames.org, the internal database translates the level field from the gis_location table into equivalent codes from geonames.org. See http://www.geonames.org/export/codes.html for a complete list of feature codes.

        #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"

Lastly, we gather the id, fcode, name, lat, and lng fields in a hash and append them to the results variable

       
        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)

JSONP Formatted Response

The following code shows the format of the final response. The return value is a hash that contains 2 fields:

  • gislocations - a hash of the search results. The fields of this hash must be defined in the GeoNamesSearchCombo.js (see lines 252-268 of the pull request for an example).
  • totalResultsCount - a integer count of how many results are in the gislocations hash

Also note that unlike JSON, JSONP requires the return value to be wrapped inside a callback function.

    returnVal = {}
    returnVal["gislocations"] = results
    returnVal["totalResultsCount"] = count
    
    #Autocomplete caller expects JSONP response. Callback wrapper.
    return callback_func+'('+json.dumps(returnVal)+')'