thingsinjars

  • 29 Oct 2012

    Some App.net recipes

    This is a collection of code snippets for various common tasks you might need to accomplish with the App.net API. Most of these are focused on creating or reading geo-tagged posts. They require a developer account on app.net and at least one of an App ID, App Code, App Access Token or User Access Token. The calls here are implemented using jQuery but that's just to make it easier to copy-paste into the console to test them out (so long as you fill in the blanks).

    An important thing to bear in mind is the possibility for confusion between a 'stream' and 'streams'. By default, a 'stream' is a discrete chunk of the 20 latest posts served at a number of endpoints. This is the open, public, global stream:

    https://alpha-api.app.net/stream/0/posts/stream/global
    

    On the other hand, 'streams' are long-poll connections that serve up any matching posts as soon as they are created. The connection stays open while there is something there to receive the response. Streams are available under:

    https://alpha-api.app.net/stream/0/streams
    

    Totally not confusing. Not at all.


    Creating a user access token

    Required for any user-specific data retrieval. The only tricky thing you'll need to think about here is the scope you require.

    scope=stream email write_post follow messages export
    

    should cover most requirements.

    Requires

    • client_id

    Visit this URL:

    https://alpha.app.net/oauth/authenticate
        ?client_id=[your client ID]
        &response_type=token
        &redirect_uri=http://localhost/
        &scope=stream email write_post follow messages export
    

    Using a user access token to create a post (with annotations)

    Requires

    • User Access Token
    • text to post

    The text is essential if you don't mark a post as 'machine_only'. The annotations here are optional. Annotations don't appear in the global stream unless the requesting client asks for them.

    $.ajax({
      contentType: 'application/json',
      data: JSON.stringify({
        "annotations": [{
          "type": "net.app.core.geolocation",
          "value": {
            "latitude": 52.5,
            "longitude": 13.3,
            "altitude": 0,
            "horizontal_accuracy": 100,
            "vertical_accuracy": 100
          }
        }],
        "text": "Don't mind me, just checking something out."
      }),
      dataType: 'json',
      success: function(data) {
        console.log("Text+annotation message posted");
      },
      error: function() {
        console.log("Text+annotation message failed");
      },
      processData: false,
      type: 'POST',
      url: 'https://alpha-api.app.net/stream/0/posts?access_token={USER_ACCESS_TOKEN}'
    });
    

    Using a user access token to post a machine_only post (with annotations)

    Requires

    • User Access Token

    In this example, we're creating a post that won't show up in user's timelines and adding the 'well-known annotation' for geolocation.

    $.ajax({
      contentType: 'application/json',
      data: JSON.stringify({
        "annotations": [{
          "type": "net.app.core.geolocation",
          "value": {
            "latitude": 52.5,
            "longitude": 13.3,
            "altitude": 0,
            "horizontal_accuracy": 100,
            "vertical_accuracy": 100
          }
        }],
        machine_only: true
      }),
      dataType: 'json',
      success: function(data) {
        console.log("Non-text message posted");
      },
      error: function() {
        console.log("Non-text message failed");
      },
      processData: false,
      type: 'POST',
      url: 'https://alpha-api.app.net/stream/0/posts?access_token={USER_ACCESS_TOKEN}'
    });
    

    Retrieve the global stream, including geo-annotated posts if there are any

    Requires

    • User Access Token

    This is a very basic call to retrieve the global stream but it also instructs the endpoint to return us all annotations and include machine-only posts.

    var data = {
      "include_machine": 1,
      "include_annotations": 1,
      "access_token": "{USER_ACCESS_TOKEN}"
    };
    
    $.ajax({
        contentType: 'application/json',
        dataType: 'json',
        success: function(data) {
          console.log(data);
        },
        error: function(error, data) {
          console.log(error, data);
        },
        type: 'GET',
        url: 'https://alpha-api.app.net/stream/0/posts/stream/global',
        data: data
    });
    

    Creating an App Access Token

    This is necessary for many of the streams operations. It is not used for individual user actions, only for application-wide actions.

    • App.net API wiki on App Access Tokens

    Requires

    • client_id
    • client_secret

    client_credentials is one of the four types of grant_type specified in the OAuth 2.0 specification. I had difficulty getting this to work when using a data object:

    var data = {
        "client_id": "{CLIENT_ID}",
        "client_secret":"{CLIENT_SECRET}",
        "grant_type": "client_credentials"
    };
    

    The client_credentials kept throwing an error. Instead, sending this as a string worked fine:

    $.ajax({
      contentType: 'application/json',
      data: 'client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&grant_type=client_credentials',
      dataType: 'json',
      success: function(data) {
        console.log(data);
      },
      error: function(error, data) {
        console.log(error, data);
      },
      processData: false,
      type: 'POST',
      url: 'https://alpha.app.net/oauth/access_token'
    });
    

    One other thing to note is that this bit should be done server-side. This will throw a bunch of "…not allowed by Access-Control-Allow-Origin…" errors if you do it via jQuery.

    Returns

    {
        "access_token": "{APP_ACCESS_TOKEN}"
        }
    

    Creating a streams format

    Now you have your app access token, you can use it to tell the service what kind of data you want back. The streams offered in the API have two quite powerful aspects. Firstly, filters allow you to run many kinds of queries on the data before it is streamed to you so you don't need to recieve and process it all. Secondly, the decoupling of filters from streams means you can specify the data structure and requirements you want once then just access that custom endpoint to get the data you want back any time.

    Requires

    • App access token

    This first example just creates an unfiltered stream endpoint

    $.ajax({
      contentType: 'application/json',
      data: JSON.stringify({"object_types": ["post"], "type": "long_poll", "id": "1"}),
      dataType: 'json',
      success: function(data) {
        console.log(data);
      },
      error: function(error, responseText, response) {
        console.log(error, responseText, response);
      },
      processData: false,
      type: 'POST',
      url: 'https://alpha-api.app.net/stream/0/streams?access_token={APP_ACCESS_TOKEN}'
    });
    

    Returns

    {
        "data": {
            "endpoint": "https://stream-channel.app.net/channel/1/{LONG_RANDOM_ENDPOINT_URL}",
            "id": "77",
            "object_types": [
                "post"
            ],
            "type": "long_poll"
        },
        "meta": {
            "code": 200
        }
    }
    

    Using Filters to create a stream of geotagged posts

    We'll specify some requirements for our filter now so that it only returns back a subset of posts. The rules we're specfying here are:

    At least one item in the "/data/annotations/*/type" field
    must "match"
    the value "net.app.core.geolocation"
    

    Requires

    • User access token

    The field is specified in 'JSON Pointer' format. Within the response, there is a 'data' object and a 'meta' object. The data contains an 'annotations' object which contains an array of annotations, each of which has a type. This is represented as /data/annotations/*/type.

    $.ajax({
      contentType: 'application/json',
      data: JSON.stringify({"match_policy": "include_any", "clauses": [{"object_type": "post", "operator": "matches", "value": "net.app.core.geolocation", "field": "/data/annotations/*/type"}], "name": "Geotagged posts"}),
      dataType: 'json',
      success: function(data) {
        console.log(data);
      },
      error: function(error, responseText, response) {
        console.log(error, responseText, response);
      },
      processData: false,
      type: 'POST',
      url: 'https://alpha-api.app.net/stream/0/filters?access_token={USER_ACCESS_TOKEN}'
    });
    

    Returns

    The filter rules you just specified, the id of the filter (remember that for later) and the details of the application used to make the request.

    {
    "clauses": [
        {
            "field": "/data/annotations/*/type",
            "object_type": "post",
            "operator": "matches",
            "value": "net.app.core.geolocation"
        }
    ],
    "id": "527",
    "match_policy": "include_any",
    "name": "Geotagged posts",
    "owner": {
        "avatar_image": {
            "height": 200,
            "url": "https://d2rfichhc2fb9n.cloudfront.net/image/4/Pr63PjEwJ1fr5Q4KeL3392BMgSnIAYlHxv8OkWwzx75V8quNfpaFp4VPpKnDRxdXtYYPtIutrDVdU9NbJn7hKApQL84T5sfB1D9bWTgtizMWInignv0WyPPfM2DpqSThQgvkB68vbPzjZ8VeKM02M2GySZ4",
            "width": 200
        },
        "canonical_url": "https://alpha.app.net/thingsinjars",
        "counts": {
            "followers": 30,
            "following": 65,
            "posts": 96,
            "stars": 0
        },
        "cover_image": {
            "height": 230,
            "url": "https://d2rfichhc2fb9n.cloudfront.net/image/4/UWZ6k9xD8_8LzEVUi_Uz6C-Vn-I8uPGEBtKb9jSVoFNijTwyEm1mJYpWq6JvnA6Jd4gzW76vFnbSWvM3jadhc1QxUl9qS4NTKiv3gJmr1zY_UpFWvX3qhOIyKrBPZckf2MrinqWay3H0h9rfqY0Gp9-liEg",
            "width": 960
        },
        "created_at": "2012-08-12T17:23:44Z",
        "description": {
            "entities": {
                "hashtags": [],
                "links": [],
                "mentions": []
            },
            "html": "<span itemscope="https://app.net/schemas/Post">Nokia Maps Technologies Evangelist; CreativeJS team member; the tech side of museum140; builder of The Elementals; misuser of semi-colons;rn</span>",
            "text": "Nokia Maps Technologies Evangelist; CreativeJS team member; the tech side of museum140; builder of The Elementals; misuser of semi-colons;rn"
        },
        "id": "3191",
        "locale": "en_GB",
        "name": "Simon Madine",
        "timezone": "Europe/Berlin",
        "type": "human",
        "username": "thingsinjars"
    }
    }
    

    Listening to the geotagged post stream

    This wil return a link to a long-lasting connection to the app.net stream that will only return posts with the geolocation annotation.

    Requires

    • filter_id from the previous call

    Note: the filter_id was returned as id in the previous response.

    $.ajax({
      contentType: 'application/json',
      data: JSON.stringify({"object_types": ["post"], "type": "long_poll", "filter_id": "527"}),
      dataType: 'json',
      success: function(data) {
        console.log(data);
      },
      error: function(error, responseText, response) {
        console.log(error, responseText, response);
      },
      processData: false,
      type: 'POST',
      url: 'https://alpha-api.app.net/stream/0/streams?access_token={APP_ACCESS_TOKEN}'
    });
    

    Returns

    The same kind of response as the 'Creating a streams format' example except the data coming down on the stream is filtered.

    https://stream-channel.app.net/channel/1/{LONG_RANDOM_ENDPOINT_URL}
    

    Open that URL up in your browser (seeing as we're testing) and, in a different tab, create a geo-tagged machine-only post (see above). Your post will appear almost instantly after you've submitted it.

    Geek, Development, Javascript, Guides

  • 8 Oct 2012

    Building a Proximity Search

    This is the detailed post to go with yesterday's quick discussion about proximity search. All the code is available on GitHub.

    This assumes a bit of NodeJS knowledge, a working copy of homebrew or something similar.

    Install

    • MongoDB - brew install mongodb
    • NodeJS
    • NPM (included in NodeJS installer these days)

    These are included in the package.json but it can't hurt to mention them here:

    • npm install twitter (node twitter streaming API library)
    • npm install mongodb (native mongodb driver for node)
    • npm install express (for convenience with API later)

    Start mongod in the background. We don't quite need it yet but it needs done at some point, may as well do it now.

    Create a Twitter App

    Fill out the form Then press the button to get the single-user access token and key. I love that Twitter does this now, rather than having to create a full authentication flow for single-user applications.

    ingest.js

    (open the ingest.js file and read along with this bit)

    Using the basic native MongoDB driver, everything must be done in the database.open callback. This might lead to a bit of Nested Callback Fury but if it bothers you or becomes a bit too furious for your particular implementation, there are a couple of alternative Node-MongoDB modules that abstract this out a bit.

    // Open the proximity database
    db.open(function() {
        // Open the post collection
        db.collection('posts', function(err, collection) {
            // Start listening to the global stream
            twit.stream('statuses/sample', function(stream) {
                // For each post
                stream.on('data', function(data) {
                    if ( !! data.geo) {
                        collection.insert(data);
                    }
                });
            });
        });
    });
    

    Index the data

    The hard work has all been done for us: Geospatial Indexing in MongoDB. That's a good thing.

    Ensure the system has a Geospatial index on the tweets.

    db.posts.ensureIndex({"geo.coordinates" : "2d"})
    

    Standard Geospatial search query:

    db.posts.find({"geo.coordinates": {$near: [50, 13]}}).pretty()
    (find the closest points to (50,13) and return them sorted by distance)
    

    By this point, we've got a database full of geo-searchable posts and a way to do a proximity search on them. To be fair, it's more down to mongodb than anything we've done.

    Next, we extend the search on those posts to allow filtering by query


    db.posts.find({"geo.coordinates": {$near: [50, 13]}, text: /.*searchterm.*/}).pretty()
    

    API

    Super simple API, we only have two main query types:

    • /proximity?latitude=55&longitude=13
    • /proximity?latitude=55&longitude=13&q=searchterm

    Each of these can take an optional 'callback' parameter to enable jsonp. We're using express so the callback parameter and content type for returning JSON are both handled automatically.

    api.js

    (open the api.js file and read along with this bit)

    This next chunk of code contains everything so don't panic.

    db.open(function() {
      db.collection('posts', function(err, collection) {
        app.get('/proximity', function(req, res) {
          var latitude, longitude, q;
          latitude = parseFloat(req.query["latitude"]);
          longitude = parseFloat(req.query["longitude"]);
          q = req.query["q"];
    
          if (/^(-?d+(.d+)?)$/.test(latitude) && /^(-?d+(.d+)?)$/.test(longitude)) {
            if (typeof q === 'undefined') {
              collection.find({
                "geo.coordinates": {
                  $near: [latitude, longitude]
                }
              }, function(err, cursor) {
                cursor.toArray(function(err, items) {
                  writeResponse(items, res);
                });
              });
            } else {
              var regexQuery = new RegExp(".*" + q + ".*");
              collection.find({
                "geo.coordinates": {
                  $near: [latitude, longitude]
                },
                'text': regexQuery
              }, function(err, cursor) {
                cursor.toArray(function(err, items) {
                  writeResponse(items, res);
                });
              });
            }
          } else {
            res.send('malformed lat/lng');
          }
    
        });
      });
    });
    

    If you've already implemented the ingest.js bit, the majority of this api.js will be fairly obvious. The biggest change is that instead of loading the data stream then acting upon each individual post that comes in, we're acting on URL requests.

    app.get('/proximity', function(req, res) {
    

    For every request on this path, we try and parse the query string to pull out a latitude, longitude and optional query parameter.

    if (/^(-?d+(.d+)?)$/.test(latitude) && /^(-?d+(.d+)?)$/.test(longitude)) {
    

    If we do have valid coordinates, pass through to Mongo to do that actual search:

    collection.find({
      "geo.coordinates": {
        $near: [latitude, longitude]
      }
    }, function(err, cursor) {
      cursor.toArray(function(err, items) {
        writeResponse(items, res);
      });
    });
    

    To add a text search into this, we just need to add one more parameter to the collection.find call:

    var regexQuery = new RegExp(".*" + q + ".*");
    collection.find({
      "geo.coordinates": {
        $near: [latitude, longitude]
      },
      'text': regexQuery
    }
    

    This makes it so simple, making it it kind of feels like cheating. Somebody else did all the hard work first.

    App.net Proximity

    This works quite well on the App.net Global Timeline but it'll really become useful once the streaming API is switched on.

    Of course, the code is all there. If you want to have a go yourself, feel free.

    Development, Geek, Guides

  • 29 Sep 2012

    CoverMap - Nokia Maps on Facebook

    I'm almost managing to keep to my intended schedule of one map-based web experiment per week. Unfortunately, I've mostly been working on internal Nokia Maps projects over the weekends recently so I've not had much to post here.

    I can share my latest toy, though: CoverMap.me

    Using just the public APIs over a couple of hours last Sunday afternoon, I made this to allow you to set a Nokia Map as your Facebook Timeline cover. The whole process is really straightforward so I thought I'd go over the main parts.

    The exact aim of CoverMap.me is to allow the user to position a map exactly, choose from any of the available map styles and set the image as their cover image.

    Make a Facebook App

    Go to developers.facebook.com/apps/ and click 'Create New App', fill in the basic details – name of the app, URL it will be hosted on, etc – and you're done.

    Facebook login

    I've used the Facebook JS SDK extensively over the summer for various projects but I wanted to try out the PHP one for this. Super, super simple. Really. Include the library (available here), set your appId and secret and request the $login_url.

    $facebook->getLoginUrl(array('redirect_uri' => "http://example.com/index.php"));
    

    That will give you a link which will take care of logging the user in and giving you basic access permissions and details about them.

    Nokia Maps JS API

    When I'm hacking together something quick and simple with the Nokia Maps API, I usually use the properly awsm jQuery plugin jOVI written by the equally awsm Max. This makes 90% of the things you would want to do with a map extremely easy and if you're doing stuff advanced enough to want the extra 10%, you're probably not the type who'd want to use a jQuery plugin, anyway. Either way, you need to register on the Nokia developer site to get your Nokia app_id and secret.

    To create a map using jOVI, simply include the plugin in your page then run .jOVI on the object you want to contain the map along with starting parameters:

    $(window).on('load', function() {
      $('#mapContainer').jOVI({
        center: [38.895111, -77.036667], // Washington D.C.
        zoom: 12,           // zoom level
        behavior: true,     // map interaction
        zoomBar: true,      // zoom bar
        scaleBar: false,    // scale bar at the bottom
        overview: false,    // minimap (bottom-right)
        typeSelector: false,// normal, satellite, terrain
        positioning: true   // geolocation
      }, "APP_ID", "APP_SECRET");
    });
    

    This gives us a complete embedded map.

    As I mentioned above, part of the idea for CoverMap.me was to allow the to choose from any of the available map styles. This was an interesting oddity because the public JS API gives you the choice of 'Normal', 'Satellite', 'Satellite Plain' (a.k.a. no text), 'Smart' (a.k.a. grey), 'Smart Public Transport', 'Terrain' and 'Traffic' while the RESTful Maps API (the API that provides static, non-interactive map images) supports all of these plus options to choose each of them with big or small text plus a 'Night Time' mode. Because of this, I decided to go for a two-step approach where users were shown the JS-powered map to let them choose their location then they went through to the RESTful static map to allow them to choose from the larger array of static tiles.

    RESTful Maps

    The RESTful Maps API is relatively new but does provide a nice, quick map solution when you don't need any interactivity. Just set an img src with the query parameters you need and get back an image.

    (this should be all on one line)
    http://m.nok.it/
        ?app_id=APP_ID
        &token=APP_TOKEN
        &nord       // Don't redirect to maps.nokia.com
        &w=640      // Width
        &h=480      // Height
        &nodot      // Don't put a green dot in the centre
        &c=38.895111, -77.036667 // Where to centre
        &z=12       // Zoom level
        &t=0        // Tile Style
    

    That URL produces this image:

    Map of Washington D.C.

    Upload to Facebook

    Given the above, we've now got an image showing a map positioned exactly where the user wants it in the tile style the user likes. We just need to make the Facebook API call to set it as Timeline Cover Image and we're done.

    You'd think.

    Facebook doesn't provide an API endpoint to update a user's profile image or timeline cover. It's probably a privacy thing or a security thing or something. Either way, it doesn't exist. Never fear! There's a solution!

    With the default permissions given by a Facebook login/OAUTH token exchange/etc... (that thing we did earlier), we are allowed to upload a photo to an album.

    The easiest way to do this is to download the map tile using cURL then repost it to Facebook. The clever way to do it would be to pipe the incoming input stream directly back out to Facebook without writing to the local file system but it would be slightly more hassle to set that up and wouldn't really make much of a difference to how it works.

    // Download from RESTful Maps
    $tileUrl = "http://m.nok.it/?app_id=APP_ID&token=APP_TOKEN&nord&w=640&h=480&nodot&c=38.895111,%20-77.036667&z=12&t=0";
    $ch = curl_init( $tileUrl );
    $fp = fopen( $filename, 'wb' );
    curl_setopt( $ch, CURLOPT_FILE, $fp );
    curl_setopt( $ch, CURLOPT_HEADER, 0 );
    curl_exec( $ch ); 
    curl_close( $ch );
    fclose( $fp );
    
    //Upload to Facebook
    $full_image_path = realpath($filename);
    $args = array('message' => 'Uploaded by CoverMap.me');
    $args['image'] = '@' . $full_image_path;
    $data = $facebook->api("/{$album_id}/photos", 'post', $args);
    

    The closest thing we can do then is to construct a Facebook link which suggests the user should set the uploaded image as their Timeline Cover:

    // $data['id'] is the image's Facebook ID 
    $fb_image_link = "http://www.facebook.com/" . $username . "?preview_cover=" . $data['id'];
    

    Done

    There we go. Minimal development required to create a web app with very little demand on the user that gives them a Nokia Map on their Facebook profile. Not too bad for a Sunday afternoon.

    Go try it out and let me know what you think.

    The code is now available on GitHub

    Geek, Guides, Toys, Development

  • 23 Jul 2012

    Web Audio

    I'm getting a bit obsessed with the Web Audio API just now. That's not a bad thing, of course, I just wish the browsers would all catch up.

    Ages ago, I mentioned the SoundScape maps mashup Max and I made at the 5apps hackathon. Finally, here's the video of the two of us presenting it at a Nokia Berlin Tech Talk.

    • Web Audio intro and SoundScape

    Creative tutorials

    This goes along with the series of Web Audio API introductory tutorials I'm writing over at CreativeJS.

    Geek, Guides, Javascript

  • older posts

Categories

Toys, Guides, Opinion, Geek, Non-geek, Development, Design, CSS, JS, Open-source Ideas, Cartoons, Photos

Shop

Colourful clothes for colourful kids

I'm currently reading

Projects

  • Awsm Street – Kid's clothing
  • Stickture
  • Explanating
  • Open Source Snacks
  • My life in sans-serif
  • My life in monospace
Simon Madine (thingsinjars)

@thingsinjars.com

Hi, I’m Simon Madine and I make music, write books and code.

I’m the Engineering Lead for komment.

© 2025 Simon Madine