Redis GEO find nearest US cities Example
C# HTML ASP
Latest commit be11bd1 Sep 18, 2016 @mythz mythz committed on GitHub Update README.md
Permalink
Failed to load latest commit information.
src Upgrade to v4.5.0 Aug 28, 2016
.gitignore Initial commit May 9, 2016
README.md Update README.md Sep 18, 2016

README.md

Redis GEO Example App

Redis GEO is a simple example showing how to make use of Redis 3.2.0 new GEO capabilities:

Live Demo: http://redisgeo.servicestack.net

If Redis hasn't already cemented itself as the venerable Swiss-Army-Knife component present in many high-performance server solutions, the latest 3.2.0 release has made it even more versatile and enhanced it with new GEO powers.

Aiming for the simplest possible useful demonstration of this new functionality, Redis GEO App lets you click on anywhere in the U.S. to find the list of nearest cities within a given radius.

Install Redis 3.2.0

In order to use the new GEO operations you'll need the latest stable 3.2.0 release of redis which you can install in your preferred *NIX system with:

$ wget http://download.redis.io/releases/redis-3.2.0.tar.gz
$ tar xzf redis-3.2.0.tar.gz
$ cd redis-3.2.0
$ make

This will build the redis-server binaries that can be run locally. To also install it as a service that's globally available and automatically started on each boot, run:

$ sudo make install
$ cd utils
$ sudo ./install_server.sh

Create Empty ServiceStack Project

Redis GEO was created from the ServiceStack ASP.NET Empty project template.

Import Geonames dataset

To populate Redis with useful GEO data we'll import the geonames.org postal data which provides the zipcodes of all US cities as well as their useful longitude and latitude coordinates.

The dataset is maintained in a tab-delimited US.txt text file which we do a fresh import of using the ServiceStack.Redis C# Client when the AppHost first starts up:

public class AppHost : AppHostBase
{
    public AppHost()
        : base("RedisGeo", typeof(RedisGeoServices).Assembly) {}

    public override void Configure(Container container)
    {
        JsConfig.EmitCamelCaseNames = true;

        container.Register<IRedisClientsManager>(c => 
            new RedisManagerPool(AppSettings.Get("RedisHost", defaultValue:"localhost")));

        ImportCountry(container.Resolve<IRedisClientsManager>(), "US");
    }

    public static void ImportCountry(IRedisClientsManager redisManager, string countryCode)
    {
        using (var redis = redisManager.GetClient())
        using (var reader = new StreamReader(
            File.OpenRead("~/App_Data/{0}.txt".Fmt(countryCode).MapHostAbsolutePath())))
        {
            string line, lastState = null, lastCity = null;
            var results = new List<ServiceStack.Redis.RedisGeo>();
            while ((line = reader.ReadLine()) != null)
            {
                var parts = line.Split('\t');
                var city = parts[2];
                var state = parts[4];
                var latitude = double.Parse(parts[9]);
                var longitude = double.Parse(parts[10]);

                if (city == lastCity) //Skip duplicate entries
                    continue;
                else
                    lastCity = city;

                if (lastState == null)
                    lastState = state;

                if (state != lastState)
                {
                    redis.AddGeoMembers(lastState, results.ToArray());
                    lastState = state;
                    results.Clear();
                }

                results.Add(new ServiceStack.Redis.RedisGeo(longitude, latitude, city));
            }
        }
    }
}

This just parses the US.txt file in our Web Applications /App_Data folder and extracts the state which we'll use as the key for our Redis GEO sorted set and populate it with the longitude and latitude of each city, skipping any duplicates. The script also imports the dataset for each state in separate batches using GEOADD multi argument API.

Implement the FindGeoResults Service

Our App only needs a single Service which we define the contract with using the FindGeoResults Request DTO:

[Route("/georesults/{State}")]
public class FindGeoResults : IReturn<List<RedisGeoResult>>
{
    public string State { get; set; }
    public long? WithinKm { get; set; }
    public double Lng { get; set; }
    public double Lat { get; set; }
}

That's the only DTO our App needs which returns a List<RedisGeoResult>. Implementing the RedisGeoServices is then just a matter fulfilling the above contract by delegating our populated Request DTO properties to the IRedisClient.FindGeoResultsInRadius() Redis Client API which itself just calls GEORADIUS and returns its results:

public class RedisGeoServices : Service
{
    public object Any(FindGeoResults request)
    {
        var results = Redis.FindGeoResultsInRadius(request.State, 
            longitude: request.Lng, latitude: request.Lat,
            radius: request.WithinKm.GetValueOrDefault(20), unit: RedisGeoUnit.Kilometers,
            sortByNearest: true);

        return results;
    }
}

Implement the Client

The entire client App is implemented in the static default.html which is just a jQuery App that just consists of the following markup:

<div id="sidebar">
    <div class="inner">
        <h3>Redis GEO Example</h3>

        <div id="instructions">
            Click on Map to find nearest cities using
            <a href="http://redis.io/commands/georadius">Redis GEO</a>
        </div>

        <div id="info">
            Find cities in <b id="state"></b> within <input id="km" type="text" value="20" /> km
        </div>

        <ol id="results"></ol>
    </div>
</div>
<div id="map"></div>

To show our results from our GEORADIUS query and a <div id="map"/> placeholder used by Google Maps JavaScript API to render a our interactive map of the US in.

The JavaScript below just listens to every click on the map then uses the Geocoder API to find out which state the user clicked on at which point it adds a custom Marker and a Circle with the radius that's specified in the km textbox.

It then calls our /georesults/{State} Service with the Lat/Lng of where the user clicked as well as the distance that it should search within, then displays all the cities within that radius in the Sidebar:

var map;
function initMap() {
    map = new google.maps.Map(document.getElementById('map'), {
        center: { lat: 37.09024, lng: -95.7128917 },
        zoom: 5
    });
    var geocoder = new google.maps.Geocoder();
    var lastMarker, lastRadius;

    google.maps.event.addListener(map, "click", function(e) {
        geocoder.geocode({ 'location': e.latLng }, function(results, status) {
            if (status === google.maps.GeocoderStatus.OK) {
                map.setCenter(e.latLng);

                if (lastMarker != null)
                    lastMarker.setMap(null);

                var marker = lastMarker = new google.maps.Marker({
                    map: map,
                    position: e.latLng
                });

                if (lastRadius != null)
                    lastRadius.setMap(null);

                var km = parseInt($("#km").val());
                var radius = lastRadius = new google.maps.Circle({
                    strokeColor: "#c3fc49",
                    strokeOpacity: 0.8,
                    strokeWeight: 2,
                    fillColor: "#c3fc49",
                    fillOpacity: 0.35,
                    map: map,
                    center: e.latLng,
                    radius: km * 1000
                });
                radius.bindTo('center', marker, 'position');

                var state = getStateAbbr(results);
                $("#state").html(state);
                $("#instructions").hide();
                $("#info").show();

                $.getJSON("/georesults/" + state,
                    { lat: e.latLng.lat(), lng: e.latLng.lng(), withinKm: km },
                    function (r) {
                        var html = $.map(r, function(x) {
                            return "<li>" + x.member + " (" + x.distance.toFixed(2) + "km)</li>";
                        }).join('');
                        $("#results").html(html);
                    });
            }});
        });

    function getStateAbbr(results) {
        for (var i = 0; i < results.length; i++) {
            for (var j = 0; j < results[i].address_components.length; j++) {
                var addr = results[i].address_components[j];
                if (addr.types.indexOf("administrative_area_level_1") >= 0)
                    return addr.short_name;
            }
        }
        return null;
    }
}

The result is a quick demonstration where the user can click on anywhere in the U.S. to return the nearest points of interest.

We hope this simple example piques your interest in Redis new GEO features and highlights some potential use-cases possible with these new capabilities.

Importing different country datasets

Whilst this example just imports US cities, you can change it to import your preferred country instead by extracting the Geonames dataset and copying it into the /App_Data folder then calling ImportCountry() with its country code.

E.g. we can import Ausrtalian Suburbs instead with:

//ImportCountry(container.Resolve<IRedisClientsManager>(), "US");
ImportCountry(container.Resolve<IRedisClientsManager>(), "AU");

ServiceStack.Redis GEO APIs

Human friendly and convenient versions of each Redis GEO API is available in IRedisClient below:

public interface IRedisClient
{
    //...
    long AddGeoMember(string key, double longitude, double latitude, string member);
    long AddGeoMembers(string key, params RedisGeo[] geoPoints);
    double CalculateDistanceBetweenGeoMembers(string key, string fromMember, string toMember, string unit=null);
    string[] GetGeohashes(string key, params string[] members);
    List<RedisGeo> GetGeoCoordinates(string key, params string[] members);
    string[] FindGeoMembersInRadius(string key, double longitude, double latitude, double radius, string unit);
    List<RedisGeoResult> FindGeoResultsInRadius(string key, double longitude, double latitude, double radius, 
        string unit, int? count = null, bool? sortByNearest = null);
    string[] FindGeoMembersInRadius(string key, string member, double radius, string unit);
    List<RedisGeoResult> FindGeoResultsInRadius(string key, string member, double radius, string unit, 
        int? count = null, bool? sortByNearest = null);
}

Whilst lower-level API's which map 1:1 with Redis server operations are available in IRedisNativeClient:

public interface IRedisNativeClient
{
    //...
    long GeoAdd(string key, double longitude, double latitude, string member);
    long GeoAdd(string key, params RedisGeo[] geoPoints);
    double GeoDist(string key, string fromMember, string toMember, string unit = null);
    string[] GeoHash(string key, params string[] members);
    List<RedisGeo> GeoPos(string key, params string[] members);
    List<RedisGeoResult> GeoRadius(string key, double longitude, double latitude, double radius, string unit,
        bool withCoords=false, bool withDist=false, bool withHash=false, int? count=null, bool? asc=null);
    List<RedisGeoResult> GeoRadiusByMember(string key, string member, double radius, string unit,
        bool withCoords=false, bool withDist=false, bool withHash=false, int? count=null, bool? asc=null);
}