Esri enriches maps with Tweets and the Streaming API

New Idea

On March 11, 2011, a 9.0 magnitude earthquake struck near the east coast of Honshu, Japan. As the news broke, many turned to Twitter to get the latest updates from people on the ground who experienced the disaster first hand. Within hours of learning about the devastating event, the @Esri team used Twitter to launch an interactive map that combined trusted sensor data with Tweets and other social feeds like Flickr and YouTube. The team layered Tweets over an information-rich map that showed earthquake location, a shakemap, and aftershocks from USGS. The resulting product helped the world understand the impact of the earthquake and resulting tsunami.

Adding Tweets to mapping technology gives insights into what people are saying and where they are saying it. It can highlight spatial trends in the conversation. Combining a Twitter conversation with authoritative data sources, like 911 calls, insurance claims, demographics, weather reports, and earthquake feeds, can provide a human perspective on the situation. By adding social intelligence to its mapping and analyzing the Twitter conversations, Esri visualizes the most engaging Tweets over space and time to get a better understanding of how a crisis event spreads and where resources are needed. Esri maps can even help predict the weather, just take a look below.

Implementation

@Esri‘s proof of concept uses public Tweets to tell the story of what is happening on the ground in real-time. This has been hugely successful and has allowed the team to generate more projects that use Twitter in decision making platforms for retail, public safety, and insurance customers.

Esri’s Twitter mapping work began with the Twitter Search API. Although the API provided a low barrier of entry for accessing geo-tweets filtered by topic, Esri quickly realized that the quantity of information available through the Search API could not facilitate decision making. Esri turned to Twitter’s data partner Gnip to provide a source of data that they combined with their spatial analysis engine. Esri has prototyped and will switch to using the Streaming API to find Tweets for all pubic facing applications on Esri.com, like this Severe Weather Map.

Impact

The Japan earthquake map was picked up by news organizations including CNN, ABC, Al Jazeera, and Wired resulting in over 500,000 page views to the application on Esri.com in the days following the event. Typical page views for Esri.com disaster response pages are around 5,200 per week. Esri customers and partners have also had great success implementing Twitter in their maps. Esri’s Spain’s map of the 2011 elections, combining demographics, polling locations, and social conversation, received 4 million requests per hour at its peak and was linked from the home page of Spain’s largest media organization.

As a side note, Esri built the Pubic Information Map application featured in the maps in this case, and encourages readers to go to ArcGIS.com to download the code and begin exploring social media mapping for workflows.

Using the Twitter Search API

Limitations

  • The Search API is not complete index of all Tweets, but instead an index of recent Tweets. At the moment that index includes between 6-9 days of Tweets.
  • You cannot use the Search API to find Tweets older than about a week.
  • Queries can be limited due to complexity. If this happens the Search API will respond with the error:{"error":"Sorry, your query is too complex. Please reduce complexity and try again."}
  • Search is focused in relevance and not completeness. This means that some Tweets and users may be missing from search results. If you want to match for completeness you should consider using the Streaming API instead.
  • The near operator cannot be used by the Search API. Instead you should use the geocode parameter.
  • Queries are limited to 1,000 characters in length, including any operators.
  • When performing geo-based searches with a radius, only 1,000 distinct subregions will be considered when evaluating the query.
  • In API v1.1, the Search API requires some form of authentication — either OAuth 1.0A or app-only auth
  • Recent Enhancements

    • API v1.1’s Search methods return tweets in the same format as other REST API methods.
    • Classic pagination is not offered in API v1.1. You must use since_id and max_id to naviagte through results.
    • The user IDs returned in the Search API now match the user IDs utilized in the Twitter REST & Streaming APIs. You no longer need to maintain a mapping of “search IDs” and “real IDs.”
    • In v1, use include_entities=true to have Tweet Entities included for mentions, links, media, and hashtags.
    • in_reply_to_status_id and in_reply_to_status_id_str are now included with @replies, allowing you to know the replied-to status ID, which can be looked up using GET statuses/show/:id.

    Rate Limits

    Rate Limiting on API v1.1’s search/tweets

    The GET search/tweets is part of the Twitter REST API 1.1 and is rate limited similarly to other v1.1 methods. See REST API Rate Limiting in v1.1 for information on that model. At this time, users represented by access tokens can make 180 requests/queries per 15 minutes. Using application-only auth, an application can make 450 queries/requests per 15 minutes on its own behalf without a user context.

    Rate Limiting on deprecated search.twitter.com

    The Rate Limits for the legacy Search API are not the same as for the REST API. When using the Search API you are not restricted by a certain number of API requests per hour, but instead by the complexity and frequency.

    As requests to the Search API are anonymous, the rate limit is measured against the requesting client IP.

    To prevent abuse the rate limit for Search is not published. If you are rate limited, the Search API will respond with an HTTP 420 Error. {"error":"You have been rate limited. Enhance your calm."}.

    PUTTING TWEETS ON THE MAP: LESSONS LEARNED AND THE WAY FORWARD

    On July 20, 2012 by Nate Ricklin

    Twitter Big Data + Geolocation = Massive Insight

    It’s a simple idea: Twitter + Geo.  What are people saying and where are they saying it?  These are basic questions, but getting the answers is surprisingly difficult.  In this blog post I’ll talk about some of the shortcomings with the Geo layer in Twitter’s API offerings, and what we built to get the functionality that we needed.

    At first glance using Twitter’s built-in “geo” functionality seems pretty straightforward, but dive into it and you’ll soon realize that there’s a lot to be desired.  In fact, Talking with my good buddy Charles at GNIP, catching a stream of tweets coming from a geographic area is notoriously difficult, and there’s no clear good way to do it yet.

    Twitter’s Built-in Geo-tags

    The first thing you might try when trying to put tweets on the map is looking at the “geo” field in tweets returned from the Twitter Search API.  Go ahead, try it out:  http://search.twitter.com/search.json?q=blue%20angels .  Look at the “geo” fields in the returned results, and you’ll see that exactly zero tweets have embedded geotags.  In our own internal testing, we typically see that only about a quarter to a half a percent of tweets actually have embedded geotags.  I’ve never seen it above 1%.  That’s a lot of data flying around without a home.

    Search API Location Information

    Twitter’s search API allows you specify a location (in lat/lng) and a radius that those tweets should originate within, and it goes by both actual embedded geotags in tweets as well as the “location” field that people fill out in their Twitter profiles in free-form fashion.  Problem solved, right?

    Well, not quite.  There are still a couple of problems with this:

    If you’re monitoring many keywords, the number of searches you need to perform (number of keywords you care about times the number of locations you care about) starts to really add up and you quickly run into API limits trying to track it all.

    The second problem is that Twitter’s Search API location search has been, and remains buggy and inconsistent.  Here are a few links to some of the issues that I’ve been following on this front:

    https://dev.twitter.com/discussions/3360 – General Location Bugginess

    https://dev.twitter.com/discussions/4004 – Geocode search results fall out of specified radius

    https://dev.twitter.com/issues/98 – Issue #98: Geocode search results fall out of specified radius (allegedly fixed as of 2012-05-17)

    https://dev.twitter.com/discussions/4003 – Geocode search volume lower than expected

    https://dev.twitter.com/issues/141 – Issue #141: Geocode search volume lower than expected (not resolved)

    But check this out: Perform this geo-search out in the Nevada desert:

    Streaming API Location Information

    Well maybe the Twitter Streaming API offers a solution? The main problem with the Streaming API is that you can filter by keywords OR filter by location, but not both at the same time.  Here’s the official word from the Twitter documentation:

    http://tomnod.com/2012/07/20/putting-tweets-on-the-map-lessons-learned-and-the-way-forward/

Advertisements

mapping with twitter APIs

How To Use The New Twitter API

About

@Tweereal is the map of twitter users activity in real-time. Animation on the map include only tweets containing geo-tags. There are two types of tweets on a map: with the exact coordinates and the coordinates determined with an accuracy of 1 degree (more transparent). Based on Twitter Streaming API and Google Maps Javascript API.

For live testing: be sure that your “test tweet” contains geotag (coordinates). Even with coordinates your tweet could be rate limited by Twitter API (see “Twitter API flow quality” field and try to tweet several times) or dropped by our filter as tweet with low precision.

http://tweereal.com/

http://www.codecademy.com/zh/courses/ruby-intermediate-en-rUwFe/0/1?curriculum_id=5122d5f811fbdb5456005922

 

 

In today’s tutorial we will create an interactive Google map using the geocoding service. Using the Twitter API we will retrieve the user’s location, and then display the profile picture on the map. We will also add the click action, after which we will retrieve the user’s 5 latest tweets.

THE MARKUP

At the beginning let’s write some simple HTML code.

<div id="map"></div>
<div class="twitter">
    <div class="inside"></div>
</div>
<div class="posts"></div>
<div class="get">
    <input type="hidden" value="marcinmobily" />
    <input type="hidden" value="codrops" />
    <input type="hidden" value="onextrapixel" /> 
    <input type="hidden" value="smashingmag" />          
    <input type="hidden" value="umutm" />
    <input type="hidden" value="1stwebdesigner" />   
    <input type="hidden" value="chriscoyier" />  
    <input type="hidden" value="marcofolio" />           
</div>   

Let’s examine the code line-by-line. The first “div” will display a Google map. Another “div” is the place where we store the description, name and profile picture of the Twitter users. Next, we create a “div” which will display the last 5 tweets.

At the end of the block, we store the Twitter user name, which will be used to download the location. If this is already done, we can proceed to write the JavaScript code.

THE JAVASCRIPT

At the beginning we should define the global variables.

1
var map, geocoder, marker, ey, my, mouseDown = false;

Then we create the main object that will incorporate all functions used by us. The most important function in this object is the ‘init’ function.

1
2
3
4
5
6
7
var map, geocoder, marker, ey, my, mouseDown = false;
var o = {
    init: function(){
        // in this place we will call all needed functions
    }
}
$(function(){ o.init(); }  

Let’s create a new object called “map” in the main “o” object. It is an object, where the function initiating a Google Map will be placed. The first function in this object is the “size” function, where we draw the current window size. This is what we need in order to display the map in full screen. Then we create the object entitled “data” with the parameters: “zoom”, “center” and “mapTypeId”. When the DOM is ready we call the init function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var map, geocoder, marker, ey, my, mouseDown = false;
var o = {
    init: function(){
        this.map.init();
    },
    map: {
        size: function(){
            var w = $(window).width(),
                h = $(window).height();
            return { width: w, height: h }
        },
        data: {
            zoom: 3,
            center: new google.maps.LatLng(52, 23),
            mapTypeId: google.maps.MapTypeId.ROADMAP
        },
        init: function(){
            var size = o.map.size();
            $('#map').css({ width: size.width, height: size.height });
            map = new google.maps.Map(document.getElementById('map'), o.map.data),
            geocoder = new google.maps.Geocoder();
            google.maps.event.addListener(map, 'dragstart', function(){
                $('.posts').hide();
            });
        }
    }
}
$(function(){ o.init(); }  

The next step is to create an object, where we will retrieve the Twitter user data such as name, description, location and the path to the profile picture. First, we create a function where we will retrieve the value of the ‘input’ fields and we will store it in the array. The next stage is another function where every element of previously created tables is looped. Thus, by using the Twitter API, we can extract the user’s location and using a geocoding service we can convert it to geographic coordinates. At the end, we show the user in the right place on the map and put the name, description and user picture to a blank ‘div’ just called ‘twitter’ 😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
var map, geocoder, marker, ey, my, mouseDown = false;
var o = {
    init: function(){
        this.map.init();
        this.twitter.show();
        this.twitter.click();
    },
    twitter: {
        get: function(){
            var arr = new Array;
            $('.get').find('input').each(function(i){
                var t = $(this),
                    val = t.val();
                arr[i] = val;              
            });
            return arr;
        },
        show: function(){
            var users = o.twitter.get(),
                arr = new Array;
            for (i in users){
                var user = users[i];
                $.getJSON('http://twitter.com/users/show/'+user+'.json?callback=?', function(data) {
                    var img = data.profile_image_url,
                        screen_name = data.screen_name;
                    geocoder.geocode({ address: data.location }, function(response, status){
                        if (status == google.maps.GeocoderStatus.OK) {
                            var x = response[0].geometry.location.lat(),
                                y = response[0].geometry.location.lng();
                            marker = new google.maps.Marker({
                                icon: img,
                                map: map,
                                title: screen_name,
                                position: new google.maps.LatLng(x, y)
                            });
                            arr.push('<div>');
                            arr.push('<p><a href="#" rel="'+screen_name+'"><img src="'+img+'" alt="" /></a></p>');
                            arr.push('<div>');
                            arr.push('<a href="#" rel="'+screen_name+'">'+data.name+'</a>');
                            arr.push('<p>'+data.description+'</p>');
                            arr.push('<p><a href="'+data.url+'" target="_blank">'+data.url+'</a></p>');
                            arr.push('<p>Followers: '+data.followers_count+', Following: '+data.friends_count+'</p>');
                            arr.push('</div>');
                            arr.push('</div>');
                            var html = arr.join('');
                            arr = [];
                            $('.twitter').find('.inside').append(html);
                            google.maps.event.addListener(marker, 'click', function(){
                                o.twitter.open(this.title);
                            });
                        }
                    });
                });
            }
        },
        click: function(){
            $('.twitter').find('.open').live('click', function(){
                var t = $(this), rel = t.attr('rel');
                o.twitter.open(rel);
            });
        },
        open: function(user){
            var posts = $('.posts'), arr = new Array;
            $.getJSON('http://twitter.com/status/user_timeline/'+user+'.json?count=5&callback=?', function(data) {
                $.each(data, function(i, post){
                    arr.push('<div>');
                    arr.push(post.text);
                    arr.push('</div>');
                });
                var html = arr.join('');
                posts.html(html).fadeIn();
            });
        }
    },
    map: {
        size: function(){
            var w = $(window).width(),
                h = $(window).height();
            return { width: w, height: h }
        },
        data: {
            zoom: 3,
            center: new google.maps.LatLng(52, 23),
            mapTypeId: google.maps.MapTypeId.ROADMAP
        },
        init: function(){
            var size = o.map.size();
            $('#map').css({ width: size.width, height: size.height });
            map = new google.maps.Map(document.getElementById('map'), o.map.data),
            geocoder = new google.maps.Geocoder();
            google.maps.event.addListener(map, 'dragstart', function(){
                $('.posts').hide();
            });
        }
    }
}
$(function(){ o.init(); }  

Lastly, we will add a ‘scroll’ object that has a function called ‘init’ which is responsible for scrolling the ‘.twitter’ div vertically.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
var map, geocoder, marker, ey, my, mouseDown = false;
var o = {
    init: function(){
        this.map.init();
        this.twitter.show();
        this.twitter.click();
    },
    twitter: {
        get: function(){
            var arr = new Array;
            $('.get').find('input').each(function(i){
                var t = $(this),
                    val = t.val();
                arr[i] = val;              
            });
            return arr;
        },
        show: function(){
            var users = o.twitter.get(), // retrieve all users which are stored in html
                arr = new Array;
            for (i in users){
                var user = users[i];
                $.getJSON('http://twitter.com/users/show/'+user+'.json?callback=?', function(data) {
                    var img = data.profile_image_url,
                        screen_name = data.screen_name;
                    geocoder.geocode({ address: data.location }, function(response, status){
                        if (status == google.maps.GeocoderStatus.OK) {
                            var x = response[0].geometry.location.lat(),
                                y = response[0].geometry.location.lng();
                            marker = new google.maps.Marker({
                                icon: img,
                                map: map,
                                title: screen_name,
                                position: new google.maps.LatLng(x, y)
                            });
                            arr.push('<div>');
                            arr.push('<p><a href="#" rel="'+screen_name+'"><img src="'+img+'" alt="" /></a></p>');
                            arr.push('<div>');
                            arr.push('<a href="#" rel="'+screen_name+'">'+data.name+'</a>');
                            arr.push('<p>'+data.description+'</p>');
                            arr.push('<p><a href="'+data.url+'" target="_blank">'+data.url+'</a></p>');
                            arr.push('<p>Followers: '+data.followers_count+', Following: '+data.friends_count+'</p>');
                            arr.push('</div>');
                            arr.push('</div>');
                            var html = arr.join('');
                            arr = [];
                            $('.twitter').find('.inside').append(html);
                            google.maps.event.addListener(marker, 'click', function(){
                                o.twitter.open(this.title);
                            });
                        }
                    });
                });
            }
        },
        click: function(){
            $('.twitter').find('.open').live('click', function(){
                var t = $(this), rel = t.attr('rel');
                o.twitter.open(rel);
            });
        },
        open: function(user){
            var posts = $('.posts'), arr = new Array;
            $.getJSON('http://twitter.com/status/user_timeline/'+user+'.json?count=5&callback=?', function(data) {
                $.each(data, function(i, post){
                    arr.push('<div>');
                    arr.push(post.text);
                    arr.push('</div>');
                });
                var html = arr.join('');
                posts.html(html).fadeIn();
            });
        }
    },
    map: {
        size: function(){
            var w = $(window).width(),
                h = $(window).height();
            return { width: w, height: h }
        },
        data: {
            zoom: 3,
            center: new google.maps.LatLng(52, 23),
            mapTypeId: google.maps.MapTypeId.ROADMAP
        },
        init: function(){
            var size = o.map.size();
            $('#map').css({ width: size.width, height: size.height });
            map = new google.maps.Map(document.getElementById('map'), o.map.data),
            geocoder = new google.maps.Geocoder();
            google.maps.event.addListener(map, 'dragstart', function(){
                $('.posts').hide();
            });
        }
    },
    scroll: {
        mouse: function(e){
            var y = e.pageY;
            return y;
        },
        check: function(y){
            var all = $('.twitter').height(),
                inside = $('.twitter').find('.inside').height();
            if (y < (all - inside)) {
                y = all - inside;
            } else if (y > 0) {
                y = 0;
            }
            return y;
        },
        update: function(e){
            var y = o.scroll.mouse(e),
                movey = y-my,
                top = ey+movey;
                check = o.scroll.check(top);
            $('.twitter').find('.inside').css({ top: check+'px' });
        },
        init: function(){
            $('.twitter').find('.inside').bind({
                mousedown: function(e){
                    e.preventDefault();
                    mouseDown = true;
                    var mouse = o.scroll.mouse(e);
                        my = mouse;
                    var element = $(this).position();
                        ey = element.top;
                    o.scroll.update(e);
                },
                mousemove: function(e){
                    if (mouseDown)
                        o.scroll.update(e);
                    return false;
                },
                mouseup: function(){
                    if (mouseDown)
                        mouseDown = false;
                    return false;
                },
                mouseleave: function(){
                    if (mouseDown)
                        mouseDown = false;
                    return false;
                }
            });
        }
    }
}
$(function(){ o.init(); }  

THE CSS

First, we will embed our reset.css that will reset all the basic styles, and we’ll define some main properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@import url("reset.css");
html, body {
    margin:0;
    padding:0;
}
body {
    font-family:Arial, Helvetica, sans-serif;
    font-size:12px;
    color:#333;
    line-height:18px;
}
a {
    text-decoration:none;
    color:#fff;
}

Next, we will define the styles for the map, Twitter posts and descriptions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
.twitter {
    position:fixed;
    left:0;
    bottom:0;
    background:#000;
    background:rgba(0, 0, 0, .7);
    width:100%;
    height:180px;
    color:#fff;
    overflow:hidden;
}
.twitter .inside {
    position:absolute;
    top:0;
    left:0;
    cursor:n-resize;
}
.twitter .item {
    float:left;
    width:280px;
    padding:20px;
}
.twitter .item .img {
    float:left;
    width:48px;
}
.twitter .img img {
    -moz-box-shadow:0 0 5px #000;
    -webkit-box-shadow:0 0 5px #000;
    box-shadow:0 0 5px #000;
}
.twitter .item .entry {
    float:right;
    width:215px;
    height:140px;
    color:#eee;
    font-size:11px;
    position:relative;
}
.twitter .item .count {
    position:absolute;
    left:0;
    bottom:-10px;
    font-size:10px;
    text-transform:uppercase;
}
.twitter .item .title {
    font-size:13px;
    font-weight:bold;
    color:#fff;
}
.twitter .item .url a {
    text-decoration:underline;
}
.twitter .item p {
    margin-bottom:5px;
}
.posts {
    display:none;
    position:absolute;
    left:50%;
    margin-left:-310px;
    width:580px;
    bottom:180px;
    background:#fff;
    color:#fff;
    background:#000;
    background:rgba(0, 0, 0, .7);
    padding:20px;
}
.posts .post {
    float:left;
    clear:both;
    width:100%;
    margin-bottom:20px;
    font-size:12px;
    font-weight:bold;
}

CONCLUSION

In today’s tutorial you learned how to use a geocoding service and how to use the Twitter API. An important fact to remember is that Google has imposed some limits to its geocoding service. Maximum number of requests in one day from a single IP address is 2500. Also, Twitter will rate limit the IP address from which the request was made.

http://tympanus.net/Tutorials/InteractiveGoogleMapTwitter/

 

Apigee: Making API’s easier

Apigee API Providers

https://apigee.com/console/others

After doing research on authentication for API’s, I came across Apigee. This is a tremendous tool for developers. With many API Providers, including Twitter and Foursquare this API management platform provides the opportunity for mashups among several social media API’s. I signed up for the service and tested authentication through twitter. I did a query on trends in Chicago using the WOIED. The authentication was successful and my results show Apigee snapshot of trends in Chicago. The results returned included trends like Mayweather, Bears, and Vikings. This is only a simple example, but was used to test authentication.

content-type:
application/json;charset=utf-8
x-frame-options:
SAMEORIGIN
x-rate-limit-remaining:
14
last-modified:
Sun, 15 Sep 2013 21:14:53 GMT
status:
200 OK
date:
Sun, 15 Sep 2013 21:14:53 GMT
x-transaction:
5d9ed74c8d02f3eb
pragma:
no-cache
cache-control:
no-cache, no-store, must-revalidate, pre-check=0, post-check=0
x-rate-limit-limit:
15
expires:
Tue, 31 Mar 1981 05:00:00 GMT
set-cookie:
lang=en
set-cookie:
guest_id=v1%3A137927969384488540; Domain=.twitter.com; Path=/; Expires=Tue, 15-Sep-2015 21:14:53 UTC
content-length:
1526
x-rate-limit-reset:
1379280593
server:
tfe
strict-transport-security:
max-age=631138519
x-access-level:
read-write-directmessages
[
  {
    "trends": [
      {
        "name": "#teamlevi",
        "url": "http://twitter.com/search?q=%23teamlevi",
        "promoted_content": null,
        "query": "%23teamlevi",
        "events": null
      },
      {
        "name": "#MINvsCHI",
        "url": "http://twitter.com/search?q=%23MINvsCHI",
        "promoted_content": null,
        "query": "%23MINvsCHI",
        "events": null
      },
      {
        "name": "#Bears",
        "url": "http://twitter.com/search?q=%23Bears",
        "promoted_content": null,
        "query": "%23Bears",
        "events": null
      },
      {
        "name": "#Vikings",
        "url": "http://twitter.com/search?q=%23Vikings",
        "promoted_content": null,
        "query": "%23Vikings",
        "events": null
      },
      {
        "name": "EJ Manuel",
        "url": "http://twitter.com/search?q=%22EJ+Manuel%22",
        "promoted_content": null,
        "query": "%22EJ+Manuel%22",
        "events": null
      },
      {
        "name": "#Mayweather",
        "url": "http://twitter.com/search?q=%23Mayweather",
        "promoted_content": null,
        "query": "%23Mayweather",
        "events": null
      },
      {
        "name": "Christian Ponder",
        "url": "http://twitter.com/search?q=%22Christian+Ponder%22",
        "promoted_content": null,
        "query": "%22Christian+Ponder%22",
        "events": null
      },
      {
        "name": "Devin Hester",
        "url": "http://twitter.com/search?q=%22Devin+Hester%22",
        "promoted_content": null,
        "query": "%22Devin+Hester%22",
        "events": null
      },
      {
        "name": "Jay Cutler",
        "url": "http://twitter.com/search?q=%22Jay+Cutler%22",
        "promoted_content": null,
        "query": "%22Jay+Cutler%22",
        "events": null
      },
      {
        "name": "Jordy Nelson",
        "url": "http://twitter.com/search?q=%22Jordy+Nelson%22",
        "promoted_content": null,
        "query": "%22Jordy+Nelson%22",
        "events": null
      }
    ],
    "as_of": "2013-09-15T21:14:53Z",
    "created_at": "2013-09-15T21:04:18Z",
    "locations": [
      {
        "name": "Chicago",
        "woeid": 2379574
      }
    ]
  }
]

What combinations of API’s would provide an interesting data tool for the city of Chicago?

Apigee Dashboard

Apigee can also be used for development of mobile apps through their dashboard feature.

Sources:

http://techcrunch.com/2009/08/25/apigees-ambition-is-to-be-the-google-analytics-for-apis/