diff --git a/package.json b/package.json index 6163d56..748e7e3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "author": "Mathew Reiss, ishotjr, pebbledev #pokemon", + "author": "Mathew Reiss, Rob Spiess, ishotjr, liammcmains", "dependencies": {}, "keywords": [], "name": "pok-mon-go-radar", @@ -9,9 +9,31 @@ ], "displayName": "Pok\u00e9mon GO Radar", "enableMultiJS": true, + "messageKeys": { + "DisplayMessage": 6, + "PokemonExpirationTime": 1, + "PokemonId": 0, + "PokemonLatitude": 2, + "PokemonLongitude": 3, + "RequestType": 7, + "UserLatitude": 4, + "UserLongitude": 5 + }, "projectType": "native", "resources": { "media": [ + { + "file": "images/pogo.png", + "name": "POGO", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/pokeball.png", + "name": "IMAGE_pokeball", + "targetPlatforms": null, + "type": "bitmap" + }, { "file": "images/unknown.png", "name": "UNKNOWN", @@ -32,11 +54,6 @@ "name": "TRAINER", "type": "bitmap" }, - { - "file": "images/poke9.png", - "name": "IMAGE_poke9", - "type": "bitmap" - }, { "file": "images/poke99.png", "name": "IMAGE_poke99", @@ -87,6 +104,11 @@ "name": "IMAGE_poke90", "type": "bitmap" }, + { + "file": "images/poke9.png", + "name": "IMAGE_poke9", + "type": "bitmap" + }, { "file": "images/poke89.png", "name": "IMAGE_poke89", @@ -417,11 +439,6 @@ "name": "IMAGE_poke3", "type": "bitmap" }, - { - "file": "images/poke2.png", - "name": "IMAGE_poke2", - "type": "bitmap" - }, { "file": "images/poke29.png", "name": "IMAGE_poke29", @@ -472,6 +489,11 @@ "name": "IMAGE_poke20", "type": "bitmap" }, + { + "file": "images/poke2.png", + "name": "IMAGE_poke2", + "type": "bitmap" + }, { "file": "images/poke1.png", "name": "IMAGE_poke1", @@ -512,6 +534,11 @@ "name": "IMAGE_poke15", "type": "bitmap" }, + { + "file": "images/poke14.png", + "name": "IMAGE_poke14", + "type": "bitmap" + }, { "file": "images/poke149.png", "name": "IMAGE_poke149", @@ -562,16 +589,6 @@ "name": "IMAGE_poke140", "type": "bitmap" }, - { - "file": "images/poke14.png", - "name": "IMAGE_poke14", - "type": "bitmap" - }, - { - "file": "images/poke13.png", - "name": "IMAGE_poke13", - "type": "bitmap" - }, { "file": "images/poke139.png", "name": "IMAGE_poke139", @@ -622,6 +639,11 @@ "name": "IMAGE_poke130", "type": "bitmap" }, + { + "file": "images/poke13.png", + "name": "IMAGE_poke13", + "type": "bitmap" + }, { "file": "images/poke129.png", "name": "IMAGE_poke129", @@ -677,6 +699,11 @@ "name": "IMAGE_poke12", "type": "bitmap" }, + { + "file": "images/poke11.png", + "name": "IMAGE_poke11", + "type": "bitmap" + }, { "file": "images/poke119.png", "name": "IMAGE_poke119", @@ -727,11 +754,6 @@ "name": "IMAGE_poke110", "type": "bitmap" }, - { - "file": "images/poke11.png", - "name": "IMAGE_poke11", - "type": "bitmap" - }, { "file": "images/poke109.png", "name": "IMAGE_poke109", @@ -799,8 +821,8 @@ "type": "bitmap" }, { - "file": "images/pogo.png", - "name": "POGO", + "file": "images/alertBackground.png", + "name": "ALERT_BACKGROUND", "type": "bitmap" }, { @@ -809,11 +831,6 @@ "targetPlatforms": null, "trackingAdjust": -1, "type": "font" - }, - { - "file": "images/alertBackground.png", - "name": "ALERT_BACKGROUND", - "type": "bitmap" } ] }, @@ -826,47 +843,7 @@ "uuid": "6532f36c-e853-432c-8784-2e9f5c042cfc", "watchapp": { "watchface": false - }, - "messageKeys": { - "Pokemon1Id": 0, - "Pokemon1ExpirationTime": 1, - "Pokemon1Distance": 2, - "Pokemon1Bearing": 3, - "Pokemon2Id": 4, - "Pokemon2ExpirationTime": 5, - "Pokemon2Distance": 6, - "Pokemon2Bearing": 7, - "Pokemon3Id": 8, - "Pokemon3ExpirationTime": 9, - "Pokemon3Distance": 10, - "Pokemon3Bearing": 11, - "Pokemon4Id": 12, - "Pokemon4ExpirationTime": 13, - "Pokemon4Distance": 14, - "Pokemon4Bearing": 15, - "Pokemon5Id": 16, - "Pokemon5ExpirationTime": 17, - "Pokemon5Distance": 18, - "Pokemon5Bearing": 19, - "Pokemon6Id": 20, - "Pokemon6ExpirationTime": 21, - "Pokemon6Distance": 22, - "Pokemon6Bearing": 23, - "Pokemon7Id": 24, - "Pokemon7ExpirationTime": 25, - "Pokemon7Distance": 26, - "Pokemon7Bearing": 27, - "Pokemon8Id": 28, - "Pokemon8ExpirationTime": 29, - "Pokemon8Distance": 30, - "Pokemon8Bearing": 31, - "Pokemon9Id": 32, - "Pokemon9ExpirationTime": 33, - "Pokemon9Distance": 34, - "Pokemon9Bearing": 35, - "RequestType": 36, - "DisplayMessage" : 37 } }, - "version": "0.6.0" + "version": "0.7.0" } diff --git a/resources/images/pokeball.png b/resources/images/pokeball.png new file mode 100644 index 0000000..5b13e62 Binary files /dev/null and b/resources/images/pokeball.png differ diff --git a/resources/images/pokeball~color.png b/resources/images/pokeball~color.png new file mode 100644 index 0000000..e47e1db Binary files /dev/null and b/resources/images/pokeball~color.png differ diff --git a/src/js/app.js b/src/js/app.js index 6c96b24..490ff43 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,356 +1,489 @@ //"use strict"; +var MessageQueue = require("./MessageQueue"); +const MAX_GPS_ANGLE = 8388608; +const TRIG_MAX_ANGLE = 65536; -var myLatitude, myLongitude; -var hasBeenNotified = false; -//var pkmnLatitude, pkmnLongitude; +// How often to check website for updated pokemon (in milliseconds) +const UPDATE_FREQUENCY = 60000; -//var distance, bearing; -var gpsErrorReported = false; -var MessageQueue = require("./MessageQueue"); +// Mock data for testing: +var use_mock_data = false; +var date = new Date(); +var start_time = (date.getTime()) / 1000; // Used for mock_data (/100 cause We need seconds, not milliseconds!) -var firstTimeUpdatingLocation = true; -// XMLHttpRequest helper -var xhrRequest = function (url, type, callback) { - var xhr = new XMLHttpRequest(); - xhr.onload = function () { - callback(this.responseText); - }; - xhr.open(type, url); - xhr.send(); +var myLatitude = 0; +var myLongitude = 0; +var gpsErrorReported = false; + + +// ------------------------------------------------------------------------------------------------------------------------ // +// Helper Functions +// ------------------------------------------------------------------------------------------------------------------------ // +var XHR_DOWNLOAD_TIMEOUT = 20000; + +// Note: Add to log multiple things, e.g.: success + error to log both successes and errors +var XHR_LOG_NONE = 0, // No Logging + XHR_LOG_ERRORS = 1, // Log errors + XHR_LOG_SUCCESS = 2, // Log successes + XHR_LOG_MESSAGES = 4, // Log messages + XHR_LOG_VERBOSE = 255; // Log everything + + +var xhrRequest = function (url, responseType, get_or_post, params, header, xhr_log_level, success, error) { + if(xhr_log_level & XHR_LOG_MESSAGES) console.log('[XHR] Requesting URL: "' + url + '"'); + + var request = new XMLHttpRequest(); + + request.xhrTimeout = setTimeout(function() { + if(xhr_log_level & XHR_LOG_ERRORS) console.log("[XHR] Timeout Getting URL: " + url); + request.onload = null; // Stopping a "fail then success" scenario + error("[XHR] Timeout Getting URL: " + url); + }, XHR_DOWNLOAD_TIMEOUT); + + request.onload = function() { + // got response, no more need for a timeout, so clear it + clearTimeout(request.xhrTimeout); // jshint ignore:line + + if (this.readyState == 4 && this.status == 200) { + if(!responseType || responseType==="" || responseType.toLowerCase()==="text") { + if(xhr_log_level & XHR_LOG_SUCCESS) console.log("[XHR] Success: " + this.responseText); + success(this.responseText); + } else { + if(xhr_log_level & XHR_LOG_SUCCESS) console.log("[XHR] Success: [" + responseType + " data]"); + success(this.response); + } + } else { + if(xhr_log_level & XHR_LOG_ERRORS) console.log("[XHR] Error: " + this.responseText); + error(this.responseText); + } + }; + + request.onerror = function() { + if(xhr_log_level & XHR_LOG_ERRORS) console.log("[XHR] Error: Unknown failure"); + error("[XHR] Error: Unknown failure"); + }; + + var paramsString = ""; + if (params !== null) { + for (var i = 0; i < params.length; i++) { + paramsString += params[i]; + if (i < params.length - 1) { + paramsString += "&"; + } + } + } + + if (get_or_post.toUpperCase() == 'GET' && paramsString !== "") { + url += "?" + paramsString; + } + + request.open(get_or_post.toUpperCase(), url, true); + + if (responseType) + request.responseType = responseType; + + if (header !== null) { + if(xhr_log_level & XHR_LOG_MESSAGES) console.log("[XHR] Header Found: "+ header[0] + " : "+ header[1]); + request.setRequestHeader(header[0], header[1]); + } + + if (get_or_post.toUpperCase() == 'POST') { + request.send(paramsString); + } else { + request.send(); + } }; -function getPokemon() { //(latitude, longitude) { - //uncomment this to test toasts hehe "test toast" - //MessageQueue.sendAppMessage({"DisplayMessage": "Test Toast"}); - // quick lame hack: myLatitude, myLongitude may not yet be set - just skip for now... - if (isNaN(myLatitude) || isNaN(myLongitude)) { - console.log("GPS coords not set - skip API call..."); - console.log("myLatitude: " + myLatitude); - console.log("myLongitude: " + myLongitude); +// ---------------------------------------------------------------------------------------------------------------------------------- +// PokeVision Functions +// ------------------------------ +var already_requested_pokemon = false; +//function get_pokemon(latitude, longitude) { +function get_pokemon() { + // quick lame hack: myLatitude, myLongitude may not yet be set - just skip for now... + if (isNaN(myLatitude) || isNaN(myLongitude) || (myLongitude === 0 && myLatitude === 0)) { + console.log("GPS coords not set - skipping pokevision request."); + return; + } + + // For constant GPS calls, only check for new pokemon the first time + if (already_requested_pokemon) return; + already_requested_pokemon = true; + + // Ok, we're gonna request. But first, let's set up a timer to update every minute + setTimeout(function() { + already_requested_pokemon = false; + get_pokemon(); + }, UPDATE_FREQUENCY); + + + // live PokeVision data + var scanUrl = 'https://pokevision.com/map/scan/' + myLatitude + '/' + myLongitude; + var dataUrl = 'https://pokevision.com/map/data/' + myLatitude + '/' + myLongitude; - } else { + if(use_mock_data) { + // static (stable!) example of PokeVision data + scanUrl = 'https://mathewreiss.github.io/PoGO/data.json'; + dataUrl = 'https://mathewreiss.github.io/PoGO/data.json'; + } - console.log("myLatitude: " + myLatitude); - console.log("myLongitude: " + myLongitude); + // TODO: is this OK? + xhrRequest(scanUrl, "text", "GET", null, null, XHR_LOG_ERRORS, scan_url_success, download_error); + //xhrRequest(scanUrl, 'GET', scan_url_success); - // live PokeVision data, hard-coded to Ann Arbor for now - var scanUrl = 'https://pokevision.com/map/scan/' + myLatitude + '/' + myLongitude; - var dataUrl = 'https://pokevision.com/map/data/' + myLatitude + '/' + myLongitude; + function scan_url_success(scanResponseText) { + if (scanResponseText.indexOf("maintenance") > -1) { + console.log("Servers are down for maintenance"); + // TODO: something better vs. continual pop-ups! + MessageQueue.sendAppMessage({"DisplayMessage": "Servers are down for maintenance"}); + } - // static (stable!) example of PokeVision data - //var scanUrl = 'https://mathewreiss.github.io/PoGO/data.json'; - //var dataUrl = 'https://mathewreiss.github.io/PoGO/data.json'; + //console.log(scanResponseText); // JSON.stringify() not necessary! - // TODO: is this OK? - xhrRequest(scanUrl, 'GET', function(scanResponseText) { - if (scanResponseText.indexOf("maintenance") > -1) { - console.log("Down for maintenance"); + // TODO: check scanResponseText success (although...does throttling error matter + // since we can still view pokes from last scan...? - // TODO: something better vs. continual pop-ups! - MessageQueue.sendAppMessage({"DisplayMessage": "Servers are down for maintenance"}); + xhrRequest(dataUrl, "text", "GET", null, null, XHR_LOG_ERRORS, data_url_success, download_error); + } - } +} - var scanJson = JSON.parse(scanResponseText); - console.log(scanResponseText); // JSON.stringify() not necessary! - // TODO: check scanResponseText success (although...does throttling error matter - // since we can still view pokes from last scan...? - - xhrRequest(dataUrl, 'GET', function(dataResponseText) { - var json = JSON.parse(dataResponseText); - console.log(dataResponseText); // JSON.stringify() not necessary! - - // TODO: status check! - console.log('status is "' + json.status + '"'); - - // TODO: much better error checking??? - if (json.pokemon.length > 0) { - - var allNearbyPokemon = []; - - var i; - for (i = 0; i < json.pokemon.length; i++) { - - // TODO: should still actually verify vs. using blindly! - console.log('pokemon[' + i + '].pokemonId is "' + json.pokemon[i].pokemonId + '"'); - // PokeVision is string for some reason - var pokemonId = Number(json.pokemon[i].pokemonId); - console.log('pokemonId is "' + pokemonId + '"'); - - var pokemonExpirationTime = json.pokemon[i].expiration_time; - console.log('pokemonExpirationTime is "' + pokemonExpirationTime + '"'); - - var pokemonLatitude = json.pokemon[i].latitude; - console.log('pokemonLatitude is "' + pokemonLatitude + '"'); - var pokemonLongitude = json.pokemon[i].longitude; - console.log('pokemonLongitude is "' + pokemonLongitude + '"'); - - var pokemonDistance = getDistance(myLatitude, myLongitude, pokemonLatitude, pokemonLongitude); - var pokemonBearing = getBearing(myLatitude, myLongitude, pokemonLatitude, pokemonLongitude); - - var pokemonUID = json.pokemon[i].uid; - - // fails on iOS! - // per @katharine: - // > PebbleKit JS Android is not to spec. - //allNearbyPokemon.push({i, pokemonId, pokemonExpirationTime, pokemonDistance}); - - var pokemonData = { - "i": i, - "pokemonId": pokemonId, - "pokemonExpirationTime": pokemonExpirationTime, - "pokemonLatitude": pokemonLatitude, - "pokemonLongitude": pokemonLongitude, - "pokemonDistance": pokemonDistance, - "pokemonBearing": pokemonBearing, - "pokemonUID": pokemonUID - }; - allNearbyPokemon.push(pokemonData); - - } - - console.log("allNearbyPokemon: " + JSON.stringify(allNearbyPokemon)); - - // sort by distance - allNearbyPokemon.sort(function(a, b) { - return a.pokemonDistance - b.pokemonDistance; - }); - - //get rid of duplicates that have the same UID - for(var i=0; i 0) { + for (var i = 0; i < count; i++) { + // TODO: should still actually verify vs. using blindly! + console.log('pokemon[' + i + '].pokemonId is "' + json.pokemon[i].pokemonId + '"'); + // PokeVision is string for some reason + var pokemonId = Number(json.pokemon[i].pokemonId); + var pokemonExpirationTime = json.pokemon[i].expiration_time; + var pokemonLatitude = Number(json.pokemon[i].latitude); + var pokemonLatitudeInteger = Math.round((pokemonLatitude / 360) * MAX_GPS_ANGLE); + var pokemonLongitude = Number(json.pokemon[i].longitude); + var pokemonLongitudeInteger = Math.round((pokemonLongitude / 360) * MAX_GPS_ANGLE); + var pokemonDistance = Math.round(getDistance(myLatitude, myLongitude, pokemonLatitude, pokemonLongitude)); + + if(use_mock_data) pokemonExpirationTime = (pokemonExpirationTime - 1469238033) + start_time; + + console.log('pokemonId is "' + pokemonId + '"'); + console.log('pokemonExpirationTime is "' + pokemonExpirationTime + '"'); + console.log('pokemonLatitude is "' + pokemonLatitude + '" = "' + pokemonLatitudeInteger + '"'); + console.log('pokemonLongitude is "' + pokemonLongitude + '" = "' + pokemonLongitudeInteger + '"'); + console.log("pokemonDistance = " + pokemonDistance); + + var pokemonUID = json.pokemon[i].uid; + + // fails on iOS! + // per @katharine: + // > PebbleKit JS Android is not to spec. + //allNearbyPokemon.push({i, pokemonId, pokemonExpirationTime, pokemonDistance}); + + var pokemonData = { + "i": i, + "pokemonId": pokemonId, + "pokemonExpirationTime": pokemonExpirationTime, + "pokemonLatitude": pokemonLatitude, + "pokemonLongitude": pokemonLongitude, + "pokemonLatitudeInteger": pokemonLatitudeInteger, + "pokemonLongitudeInteger": pokemonLongitudeInteger, + "pokemonDistance": pokemonDistance, + "pokemonUID": pokemonUID + }; + allNearbyPokemon.push(pokemonData); + } -// based on @mathew's process_distance() -function getDistance(myLatitude, myLongitude, pkmnLatitude, pkmnLongitude) { - var distance; + console.log("allNearbyPokemon: " + JSON.stringify(allNearbyPokemon)); + + // sort by distance + allNearbyPokemon.sort(function(a, b) { + return a.pokemonDistance - b.pokemonDistance; + }); + + //get rid of duplicates that have the same UID + for(var n=0; n 0 && allNearbyPokemon.length > 0) { + for(n = 0; n 0) { + var date = new Date(); + var now = (date.getTime()) / 1000; // We need seconds, not milliseconds! + //if(use_mock_data) now = 1469237613; // Mock time, as mock data is old. + for(n = 0; n #include "pokedex.h" // TODO: replace crude hack with https://github.com/smallstoneapps/data-processor ;) -#define KEY_POKEMON1ID 0 -#define KEY_POKEMON1EXPIRATIONTIME 1 -#define KEY_POKEMON1DISTANCE 2 -#define KEY_POKEMON1BEARING 3 -// . . . -//#define KEY_POKEMON9ID 32 -//#define KEY_POKEMON9EXPIRATIONTIME 33 -//#define KEY_POKEMON9DISTANCE 34 -//#define KEY_POKEMON9BEARING 35 -#define KEY_REQUESTTYPE 36 -#define KEY_DISPLAYMESSAGE 37 - -Window *splash, *list, *compass; -MenuLayer *menu; -StatusBarLayer *status_bar; -Layer *overlay; +// ------------------------------------------------------------------------ // +// Global Variables, Structs and Constants +// ------------------------------------------------------------------------ // + +#define KEY_POKEMON_ID 0 +#define KEY_POKEMON_EXPIRATION_TIME 1 +#define KEY_POKEMON_LATITUDE 2 +#define KEY_POKEMON_LONGITUDE 3 + +#define KEY_USER_LATITUDE 4 +#define KEY_USER_LONGITUDE 5 + +#define KEY_DISPLAY_MESSAGE 6 + +#define KEY_REQUEST_TYPE 7 + + +// Measured in seconds: +#define TIME_TO_CLEAR_AFTER_CAUGHT 2 +#define TIME_TO_CLEAR_AFTER_EXPIRED 10 + + +// Predefined messages +#define MSG_LOADING 0 +#define MSG_NO_POKEMON_FOUND 1 +#define MSG_FINDING_POKEMON 2 + +// Compass window (not in this version) +//Window *compass_window; + + +// Main Window and Layers +Window *list_window; +MenuLayer *menu_layer; +StatusBarLayer *status_bar_layer; +Layer *overlay_layer; +char list_text_buffer[100] = ""; +GFont custom_font; + + +// loading splash screen +Window *splash_window; GBitmap *splash_bitmap; BitmapLayer *bitmap_layer; TextLayer *loading_layer; -Layer *alert; -TextLayer *alertText; +bool loading = true; + //alert layer -BitmapLayer *alertBackgroundLayer; -GBitmap *alertBackground; +Layer *alert_layer; +TextLayer *alert_text_layer; +BitmapLayer *alert_background_bitmap_layer; +GBitmap *alert_background_bitmap; +char alert_text_buffer[100] = ""; -//GBitmap *nearby[9]; -GBitmap *top, *bottom; -//int distances[9]; -//int angles[9]; -//int pokedex[9]; +// Compass and Distance +bool compass_exists = true; // If a compass exists (i.e. not Diorite) +int32_t compass_heading = 0; -int NUM_POKEMON = 0; +typedef enum { + BEARING_MODE_INVISIBLE = 0, // No bearing arrow + BEARING_MODE_STATIC_ARROW = 1, // Arrow points toward direction, doesn't rotate with compass + BEARING_MODE_ROTATING_ARROW = 2, // Normal: Arrows rotate with compass (if compass exists, else STATIC_ARROW) + BEARING_MODE_CARDINAL = 3 // Shows cardnal direction (N, S, SW, NNW) +} bearing_mode_enum; +bearing_mode_enum bearing_mode = BEARING_MODE_ROTATING_ARROW; -#define VALOR 0 -#define MYSTIC 1 -#define INSTINCT 2 -int TEAM = 1; -bool loading = true; +typedef enum { + DISTANCE_MODE_INVISBLE = 0, + DISTANCE_MODE_METERS = 1, + DISTANCE_MODE_FEET = 2, + DISTANCE_MODE_FOOTPRINTS = 3, // Footprints +} distance_mode_enum; +distance_mode_enum distance_mode = DISTANCE_MODE_METERS; -GFont custom_font; -typedef struct { - int angle; - int dex; - GBitmap *sprite; - char name[64]; - char distance[8]; - char listBuffer[128]; - GPath *compass; -} Pokemon; +// Team Coloration +#define TEAM_NONE 0 +#define TEAM_VALOR 1 +#define TEAM_MYSTIC 2 +#define TEAM_INSTINCT 3 +int32_t team = TEAM_MYSTIC; +GBitmap *ui_top_bitmap, *ui_bottom_bitmap; -Pokemon nearby[9]; -static const GPathInfo MINI_COMPASS_INFO = { - .num_points = 4, - .points = (GPoint []) { {0,-9}, {-6,9}, {0,3}, {6,9} } -}; +// User location +typedef struct { + int32_t lat; + int32_t lon; +} user_struct; +user_struct user; + -void temp_draw(Layer *layer, GContext *ctx){ - graphics_context_set_fill_color(ctx, GColorCobaltBlue); - graphics_fill_rect(ctx, GRect(0,0,PBL_IF_RECT_ELSE(144,180),16), 0, GCornerNone); - #ifdef PBL_RECT - graphics_context_set_stroke_color(ctx, GColorWhite); - for(int i = 0; i < 144; i++){ - if(i%2 == 0) graphics_draw_pixel(ctx, GPoint(i, 15)); - } - #endif - graphics_context_set_compositing_mode(ctx, GCompOpSet); - graphics_draw_bitmap_in_rect(ctx, top, GRect(PBL_IF_RECT_ELSE(0,18),16,144,11)); - graphics_draw_bitmap_in_rect(ctx, bottom, GRect(PBL_IF_RECT_ELSE(0,18),PBL_IF_RECT_ELSE(168,180)-11,144,11)); +// ------------------------------------------------------------------------ // +// Pokemon Structs and Functions +// ------------------------------------------------------------------------ // +// Maximum number of pokemon to allow on our list +// Currently max of 255 due to using uint8_t, but that should be plenty +#define MAX_POKEMON 100 - graphics_context_set_text_color(ctx, GColorWhite); - graphics_draw_text(ctx, "12:34", fonts_get_system_font(FONT_KEY_GOTHIC_14), GRect(0,PBL_IF_RECT_ELSE(-2,2),PBL_IF_RECT_ELSE(144,180),16), GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL); +typedef struct { + int32_t lat; + int32_t lon; + time_t expires; // time_t is int32_t + int32_t distance; // only saving to increase sort speed + int32_t dex; // only needs to be uint8_t +} pokemon_struct; + +pokemon_struct pokemon[MAX_POKEMON]; +int32_t NUM_POKEMON = 1; + +// TODO: Implement pointer list instead of sorting big struct array +//uint8_t pokemon_list[MAX_POKEMON]; + +static void remove_pokemon(int32_t index) { + if (index >= NUM_POKEMON) { + APP_LOG(APP_LOG_LEVEL_ERROR, "ERROR! Index to remove too large: %d", (int)index); + return; + } + if (NUM_POKEMON == 0) { + APP_LOG(APP_LOG_LEVEL_ERROR, "ERROR! No pokemon left to remove"); + return; + } + + for(uint8_t i=index; i0 && pokemon[j-1].distance < temp.distance) { + pokemon[j] = pokemon[j-1]; + --j; + } + pokemon[j] = temp; + } +} - AnimationHandlers handlers = { - .stopped = (AnimationStoppedHandler) stopped - }; - animation_set_handlers((Animation*) anim, handlers, NULL); - animation_schedule((Animation*) anim); +void flag_pokemon_as_caught(int index) { + if(pokemon[index].lat == 0 && pokemon[index].lon == 0) // if already flagged + return; + pokemon[index].lat = 0; + pokemon[index].lon = 0; + time_t now = time(NULL); + pokemon[index].expires = now - TIME_TO_CLEAR_AFTER_EXPIRED + TIME_TO_CLEAR_AFTER_CAUGHT; } -void draw_pokemon(GContext *ctx, const Layer *cell_layer, MenuIndex *index, void *data){ - if(index->row == 0){ - graphics_context_set_fill_color(ctx, GColorWhite); - graphics_fill_rect(ctx, layer_get_bounds(cell_layer), 0, GCornerNone); - return; - } - if(menu_cell_layer_is_highlighted(cell_layer)){ - graphics_context_set_fill_color(ctx, PBL_IF_COLOR_ELSE(GColorLightGray,GColorWhite)); - graphics_fill_rect(ctx, layer_get_bounds(cell_layer), 0, GCornerNone); - #ifndef PBL_COLOR - graphics_context_set_stroke_color(ctx, GColorBlack); - graphics_context_set_stroke_width(ctx, 2); - graphics_draw_line(ctx, GPoint(2,4), GPoint(2,56)); - #endif - } +// ------------------------------------------------------------------------ // +// GPS Functions +// ------------------------------------------------------------------------ // +// GPS Constants +// I convert all floating point GPS coordinates to 32bit integers - graphics_context_set_compositing_mode(ctx, GCompOpSet); - graphics_draw_bitmap_in_rect(ctx, nearby[index->row-1].sprite, GRect(2+30-(gbitmap_get_bounds(nearby[index->row-1].sprite).size.w/2),30-(gbitmap_get_bounds(nearby[index->row-1].sprite).size.h/2), gbitmap_get_bounds(nearby[index->row-1].sprite).size.w, gbitmap_get_bounds(nearby[index->row-1].sprite).size.h)); +#define EARTH_RADIUS_IN_METERS 6378137 +#define EARTH_CIRCUMFERENCE_IN_METERS 40075016 +#define MAX_GPS_ANGLE 8388608 // Equivalent to 360 degrees - graphics_context_set_text_color(ctx, GColorBlack); - graphics_draw_text(ctx, nearby[index->row-1].listBuffer, custom_font, GRect(64, 14, 180, 30), GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); +// 64bit GPS constants +#define EARTH_RADIUS_IN_METERS_64BIT 6378137LL +#define EARTH_CIRCUMFERENCE_IN_METERS_64BIT 40075016LL +#define MAX_GPS_ANGLE_64BIT 8388608LL +#define TRIG_MAX_RATIO_64BIT 65536LL // I know it's 65535, but this is close enough - graphics_context_set_fill_color(ctx, GColorBlack); - gpath_draw_filled(ctx, nearby[index->row-1].compass); -} -static uint16_t get_num_pokemon(MenuLayer *menu_layer, uint16_t section_index, void *data){ - return NUM_POKEMON + 1; +static int32_t get_bearing(int32_t y, int32_t x) { // Keeping (y, x) notation cause atan2 uses it. /shrug + return TRIG_MAX_ANGLE - (atan2_lookup(y, x) - (TRIG_MAX_ANGLE / 4)); } -static int16_t pokemon_cell_height(MenuLayer *menu_layer, MenuIndex *cell_index, void *data){ - if(cell_index->row == 0) return 20; - return 60; -} +#define root_depth 10 // How many iterations square root function performs +int32_t sqrt32(int32_t a) {int32_t b=a; for(int8_t i=0; itm_sec % 15 == 0) { +// Returns horizontal distance (in meters) between two longitudinal points at a specific latitude +static int32_t gps_lon_distance(int32_t lat, int32_t lon1, int32_t lon2) { + //return cos_lookup((map_lat/128)) * EARTH_CIRCUMFERENCE_IN_METERS * (pos_lon - map_lon) / (MAX_GPS_ANGLE * TRIG_MAX_RATIO); + int32_t lon_distance = ( + ( + (int64_t) cos_lookup(lat/128) * + EARTH_CIRCUMFERENCE_IN_METERS_64BIT * + (int64_t) (lon2 - lon1) + ) / ( + MAX_GPS_ANGLE_64BIT * + TRIG_MAX_RATIO_64BIT + ) + ); + return lon_distance; +} - APP_LOG(APP_LOG_LEVEL_INFO, "tick_handler() 0/15/30/45"); - // Begin dictionary - DictionaryIterator *iter; - app_message_outbox_begin(&iter); + +// ------------------------------------------------------------------------ // +// Layer Drawing Functions +// ------------------------------------------------------------------------ // +void draw_overlay_layer(Layer *layer, GContext *ctx){ + GRect bounds = layer_get_bounds(layer); + + graphics_context_set_fill_color(ctx, GColorCobaltBlue); + graphics_fill_rect(ctx, GRect(0, 0, bounds.size.w,16), 0, GCornerNone); + + #ifdef PBL_RECT + graphics_context_set_stroke_color(ctx, GColorWhite); + for (int32_t i = 0; i < bounds.size.w; i++) { + if (i%2 == 0) + graphics_draw_pixel(ctx, GPoint(i, 15)); + } + #endif - // RequestType is currently meaningless, but w/b used for e.g. "full" API call vs. distance refresh - // Add key-value pairs - dict_write_cstring(iter, KEY_REQUESTTYPE, ""); + graphics_context_set_compositing_mode(ctx, GCompOpSet); + graphics_draw_bitmap_in_rect(ctx, ui_top_bitmap, GRect(PBL_IF_RECT_ELSE(0, 18), 16, 144, 11)); + graphics_draw_bitmap_in_rect(ctx, ui_bottom_bitmap, GRect(PBL_IF_RECT_ELSE(0, 18), bounds.size.h - 11, 144, 11)); - // Send the message! - app_message_outbox_send(); + // Draw Clock + graphics_context_set_text_color(ctx, GColorWhite); + graphics_draw_text(ctx, " ", fonts_get_system_font(FONT_KEY_GOTHIC_14), GRect(0, PBL_IF_RECT_ELSE(-2,2), bounds.size.w, 16), GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL); +} +void remove_splash_screen() { + if (loading) { + window_stack_push(list_window, true); + layer_add_child(overlay_layer, alert_layer); // Move alert layer to main window + vibes_short_pulse(); + loading = false; } } -void displayToast(char *string) { - GRect from_frame = GRect(PBL_IF_ROUND_ELSE(40, 22), 180, 100, 100); - GRect to_frame = GRect(PBL_IF_ROUND_ELSE(40, 22), 34, 100, 100); +// ------------------------------------------------------------------------ // +// Menu Functions +// ------------------------------------------------------------------------ // +static const GPathInfo MINI_COMPASS_INFO = { + .num_points = 4, + .points = (GPoint []) { {0,-9}, {-6,9}, {0,3}, {6,9} } +}; - APP_LOG(APP_LOG_LEVEL_DEBUG, "Received Status: %s", string); - text_layer_set_text(alertText, string); +int16_t menu_get_header_height(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context) { + return 20; // Height of both header at top, and footer at bottom (footer = 2nd header) +} - vibes_double_pulse(); - animate_layer(alert, &from_frame, &to_frame, 1000, 0); - animate_layer(alert, &to_frame, &from_frame, 1000, 3000); +uint16_t menu_get_num_sections(struct MenuLayer *menu_layer, void *callback_context) { + return 2; } -static void bluetooth_callback(bool connected) { - if(connected) { - displayToast("Bluetooth reconnected"); - } else { - displayToast("Bluetooth disconnected"); - } +static uint16_t get_num_pokemon(MenuLayer *menu_layer, uint16_t section_index, void *data){ + if (section_index == 0) + return NUM_POKEMON; + // Second section is just a header with 0 rows under it (so return 0), therefore: it's just a footer + return 0; } -static void inbox_received_callback(DictionaryIterator *iterator, void *context) { - - APP_LOG(APP_LOG_LEVEL_INFO, "Message received!"); +static int16_t pokemon_cell_height(MenuLayer *menu_layer, MenuIndex *cell_index, void *data){ + return 60; +} - time_t now = time(NULL); - time_t expiration = now; - long int expiration_delta; - int distance = 0; - int bearing = 0; - - - Tuple *tuple; - tuple = dict_find(iterator, KEY_DISPLAYMESSAGE); - if(tuple) { - if(tuple->value->cstring) { - displayToast(tuple->value->cstring); - } - } else { - // TODO: use https://github.com/smallstoneapps/data-processor instead of this quick crude hack! - for(int i = 0; i < 9; i++){ - - // Read tuples - Tuple *pokemon_id_tuple = dict_find(iterator, KEY_POKEMON1ID + (i * 4)); - if(pokemon_id_tuple) { - nearby[i].dex = pokemon_id_tuple->value->int32; - } - - // break on first 0 ID - indicates end of data - if (nearby[i].dex == 0) { - break; - } - - Tuple *pokemon_expiration_tuple = dict_find(iterator, KEY_POKEMON1EXPIRATIONTIME + (i * 4)); - if(pokemon_expiration_tuple) { - expiration = pokemon_expiration_tuple->value->int32; - } - - expiration_delta = expiration - now; - - Tuple *pokemon_distance_tuple = dict_find(iterator, KEY_POKEMON1DISTANCE + (i * 4)); - if(pokemon_distance_tuple) { - distance = pokemon_distance_tuple->value->int32; - } - - Tuple *pokemon_bearing_tuple = dict_find(iterator, KEY_POKEMON1BEARING + (i * 4)); - if(pokemon_bearing_tuple) { - bearing = pokemon_bearing_tuple->value->int32; - } - APP_LOG(APP_LOG_LEVEL_DEBUG, "bearing: %d", bearing); - - // TODO: add check for overall validity first (inc. e.g. already expired and not worth showing) - - // TODO: refactor ASAP! - NUM_POKEMON = i + 1; - - // need to destroy old bitmap first - if(nearby[i].sprite != NULL) { - gbitmap_destroy(nearby[i].sprite); - } - - // TODO: recycle instead, since it will realistically be just a few pokemon based on data so far - // TODO: and do a better job of clean-up in general...? - - nearby[i].sprite = gbitmap_create_with_resource(poke_images[nearby[i].dex]); - //strncpy(nearby[0].listBuffer, poke_names[nearby[0].dex], sizeof(nearby[0].listBuffer)); - snprintf(nearby[i].listBuffer, sizeof(nearby[i].listBuffer), "%s\n%d:%02d\n%d m", poke_names[nearby[i].dex], - (int) expiration_delta / 60, (int) expiration_delta % 60, distance); - - nearby[i].angle = bearing * TRIG_MAX_ANGLE / 360; - APP_LOG(APP_LOG_LEVEL_DEBUG, "nearby[i].angle: %d", nearby[i].angle); - - // draw compass - nearby[i].compass = gpath_create(&MINI_COMPASS_INFO); - gpath_move_to(nearby[i].compass, GPoint(124, 40)); - //nearby[i].angle = TRIG_MAX_ANGLE/(12*i); - gpath_rotate_to(nearby[i].compass, nearby[i].angle); - - } - - menu_layer_reload_data(menu); - - if(loading){ - if(NUM_POKEMON >= 1) menu_layer_set_selected_index(menu, (MenuIndex){0,1}, MenuRowAlignNone, false); - window_stack_push(list, true); - vibes_short_pulse(); - loading = false; - } +void menu_draw_header(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *callback_context) { + graphics_context_set_fill_color(ctx, GColorWhite); + graphics_fill_rect(ctx, layer_get_bounds(cell_layer), 0, GCornerNone); +} + +void draw_pokemon(GContext *ctx, const Layer *cell_layer, MenuIndex *index, void *data){ + + if(menu_cell_layer_is_highlighted(cell_layer)){ + graphics_context_set_fill_color(ctx, GColorLightGray); + graphics_fill_rect(ctx, layer_get_bounds(cell_layer), 0, GCornerNone); + #ifndef PBL_COLOR + graphics_context_set_stroke_color(ctx, GColorBlack); + graphics_context_set_stroke_width(ctx, 2); + graphics_draw_line(ctx, GPoint(2, 4), GPoint(2, 56)); + #endif + } + + + GBitmap *sprite = NULL; + int pokemon_index = index->row; + int32_t bearing = 0; + bool bearing_valid = false; + + // --------------------- + // Prepare Icon, Compass and Text + // --------------------- + graphics_context_set_text_color(ctx, GColorBlack); + // NUM_POKEMON=1 and DEX=0 means "0 Pokemon, Display Message instead" + if(NUM_POKEMON == 1 && pokemon[0].dex == 0) { + // When in "Display Message" mode, EXPIRES denotes which message + if(pokemon[0].expires == MSG_LOADING) + strncpy(list_text_buffer, "Loading...", sizeof(list_text_buffer)); + else if(pokemon[0].expires == MSG_NO_POKEMON_FOUND) + strncpy(list_text_buffer, "No\nPokemon\nFound", sizeof(list_text_buffer)); + else if(pokemon[0].expires == MSG_FINDING_POKEMON) + strncpy(list_text_buffer, "Finding\nPokemon...", sizeof(list_text_buffer)); + else + strncpy(list_text_buffer, "Error", sizeof(list_text_buffer)); + + sprite = gbitmap_create_with_resource(poke_images[0]); // load sprite 0 (pokeball image) + + } else if(pokemon[pokemon_index].dex < 151 && pokemon[pokemon_index].dex>=0) { //if dex is between 0 and 151: + sprite = gbitmap_create_with_resource(poke_images[pokemon[pokemon_index].dex]); + + time_t now = time(NULL); + + int32_t expiration_delta = pokemon[pokemon_index].expires - now; + if(pokemon[pokemon_index].lat == 0 && pokemon[pokemon_index].lon == 0) { + strncpy(list_text_buffer, "Caught!", sizeof(list_text_buffer)); + } else if (expiration_delta < 0) { + strncpy(list_text_buffer, "Expired!", sizeof(list_text_buffer)); + } else { + int32_t y = gps_lat_distance(user.lat, pokemon[pokemon_index].lat); + int32_t x = gps_lon_distance(user.lat, user.lon, pokemon[pokemon_index].lon); + pokemon[pokemon_index].distance = gps_distance(y, x); + //bearing = get_bearing(y, x); + bearing = TRIG_MAX_ANGLE - (atan2_lookup(y, x) - (TRIG_MAX_ANGLE / 4)); + bearing_valid = true; + + snprintf(list_text_buffer, sizeof(list_text_buffer), + "%s\n%d:%02d\n%d m", + poke_names[pokemon[pokemon_index].dex], + (int)expiration_delta / 60, + (int)expiration_delta % 60, + (int)pokemon[pokemon_index].distance + ); + + } + } else { //else dex is not between 0 and 151: + sprite = gbitmap_create_with_resource(poke_images[0]); // load sprite 0 (pokeball image) + strncpy(list_text_buffer, "Unknown", sizeof(list_text_buffer)); } + + + // --------------------- // + // Draw Bearing GPath + // --------------------- // + if(bearing_valid) + switch (bearing_mode) { + case BEARING_MODE_ROTATING_ARROW: + bearing = bearing + compass_heading; // Move arrow with compass + // no break;, falls thru to display arrow + + case BEARING_MODE_STATIC_ARROW: { // I dunno why I gotta have braces here, cloudpebble complains otherwise + GPath *compass_gpath = gpath_create(&MINI_COMPASS_INFO); + gpath_move_to(compass_gpath, GPoint(124, 40)); + gpath_rotate_to(compass_gpath, bearing); + graphics_context_set_fill_color(ctx, GColorBlack); + gpath_draw_filled(ctx, compass_gpath); + gpath_destroy(compass_gpath); + break; + } + + case BEARING_MODE_CARDINAL: + // TODO: North, south, south south west, etc + break; + + default: + // invisible or other + break; + } + + // --------------------- // + // Draw Pokemon Sprite + // --------------------- // + if (sprite) { + GRect bounds = gbitmap_get_bounds(sprite); + graphics_context_set_compositing_mode(ctx, GCompOpSet); + graphics_draw_bitmap_in_rect(ctx, sprite, GRect(2+30-(bounds.size.w/2), 30-(bounds.size.h/2), bounds.size.w, bounds.size.h)); + gbitmap_destroy(sprite); + } + + // --------------------- // + // Draw Text Next to Sprite + // --------------------- // + graphics_draw_text(ctx, list_text_buffer, custom_font, GRect(64, 14, 180, 30), GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); } -static void inbox_dropped_callback(AppMessageResult reason, void *context) { - APP_LOG(APP_LOG_LEVEL_ERROR, "Message dropped!"); +// ------------------------------------------------------------------------ // +// Button Functions +// ------------------------------------------------------------------------ // +// SELECT button was clicked +void sl_click_handler(ClickRecognizerRef ref, void *context) { + MenuIndex current = menu_layer_get_selected_index(menu_layer); + //remove_pokemon(current.row); + if(NUM_POKEMON != 1 || pokemon[0].dex != 0) // if not in "message" mode + flag_pokemon_as_caught(current.row); } -static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) { - APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox send failed!"); +// DOWN button was clicked +void dn_click_handler(ClickRecognizerRef ref, void *context) { + MenuIndex current = menu_layer_get_selected_index(menu_layer); + if(current.row + 1 >= NUM_POKEMON) + return; + else + menu_layer_set_selected_next(menu_layer, false, MenuRowAlignCenter, true); } -static void outbox_sent_callback(DictionaryIterator *iterator, void *context) { - APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success!"); +// UP button was clicked +void up_click_handler(ClickRecognizerRef ref, void *context) { + MenuIndex current = menu_layer_get_selected_index(menu_layer); + if(current.row == 0) return; + else + menu_layer_set_selected_next(menu_layer, true, MenuRowAlignCenter, true); } +// BACK button was clicked +void bk_click_handler (ClickRecognizerRef recognizer, void *context) { + window_stack_pop_all(true); // Quit +} -void compass_handler(CompassHeadingData heading){ - for(int i = 0; i < NUM_POKEMON; i++){ - gpath_rotate_to(nearby[i].compass, heading.true_heading + nearby[i].angle); - } - layer_mark_dirty(window_get_root_layer(list)); +void config(void *context) { + window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 200, dn_click_handler); + window_single_repeating_click_subscribe(BUTTON_ID_UP, 200, up_click_handler); + window_single_click_subscribe(BUTTON_ID_SELECT, sl_click_handler); + window_single_click_subscribe(BUTTON_ID_BACK, bk_click_handler); } -void init(){ - - splash = window_create(); - window_set_background_color(splash, GColorBlack); - splash_bitmap = gbitmap_create_with_resource(RESOURCE_ID_POGO); - GRect window_bounds = layer_get_bounds(window_get_root_layer(splash)); - GRect bitmap_bounds = gbitmap_get_bounds(splash_bitmap); - bitmap_layer = bitmap_layer_create(GRect( (window_bounds.size.w - bitmap_bounds.size.w)/2, (window_bounds.size.h - bitmap_bounds.size.h)/2, bitmap_bounds.size.w, bitmap_bounds.size.h) ); - bitmap_layer_set_bitmap(bitmap_layer, splash_bitmap); - layer_add_child(window_get_root_layer(splash), bitmap_layer_get_layer(bitmap_layer)); - loading_layer = text_layer_create(GRect(0,window_bounds.size.w/2 + bitmap_bounds.size.w/2, PBL_IF_RECT_ELSE(144,180), 28)); - text_layer_set_text(loading_layer, "Loading..."); - text_layer_set_background_color(loading_layer, GColorBlack); - text_layer_set_text_color(loading_layer, GColorWhite); - text_layer_set_text_alignment(loading_layer, GTextAlignmentCenter); - layer_add_child(window_get_root_layer(splash), text_layer_get_layer(loading_layer)); - window_stack_push(splash, true); - // Register callbacks - app_message_register_inbox_received(inbox_received_callback); - app_message_register_inbox_dropped(inbox_dropped_callback); - app_message_register_outbox_failed(outbox_failed_callback); - app_message_register_outbox_sent(outbox_sent_callback); +// ------------------------------------------------------------------------ // +// Toast Functions +// ------------------------------------------------------------------------ // +void stopped(Animation *anim, bool finished, void *context){ + property_animation_destroy((PropertyAnimation*) anim); +} - // Open AppMessage - // TODO: sizes? - app_message_open(2048,2048); - tick_timer_service_subscribe(SECOND_UNIT, tick_handler); +void animate_layer(Layer *layer, GRect *start, GRect *finish, int duration, int delay){ + PropertyAnimation *anim = property_animation_create_layer_frame(layer, start, finish); - connection_service_subscribe((ConnectionHandlers) { - .pebble_app_connection_handler = bluetooth_callback - }); + animation_set_duration((Animation*) anim, duration); + animation_set_delay((Animation*) anim, delay); + AnimationHandlers handlers = { + .stopped = (AnimationStoppedHandler) stopped + }; + animation_set_handlers((Animation*) anim, handlers, NULL); - custom_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PKMN_10)); + animation_schedule((Animation*) anim); +} - top = gbitmap_create_with_resource(RESOURCE_ID_UI_TOP); - bottom = gbitmap_create_with_resource(RESOURCE_ID_UI_BOTTOM); - GColor *pal_top = gbitmap_get_palette(top); - GColor *pal_bottom = gbitmap_get_palette(bottom); +void displayToast(char *string) { + static GRect from_frame, to_frame; + from_frame = GRect(PBL_IF_ROUND_ELSE(40, 22), 180, 100, 100); + to_frame = GRect(PBL_IF_ROUND_ELSE(40, 22), 34, 100, 100); - #ifdef PBL_COLOR - switch(TEAM){ - case MYSTIC: - pal_top[0] = GColorCobaltBlue; - pal_bottom[2] = GColorCobaltBlue; - break; + APP_LOG(APP_LOG_LEVEL_DEBUG, "Received Toast: \"%s\"", string); + strncpy(alert_text_buffer, string, sizeof(alert_text_buffer)); + text_layer_set_text(alert_text_layer, alert_text_buffer); - case INSTINCT: - pal_top[0] = GColorOrange; - pal_bottom[2] = GColorOrange; - break; - } - #endif + vibes_double_pulse(); - list = window_create(); - window_set_click_config_provider(list, config); - window_set_background_color(list, GColorWhite); + animate_layer(alert_layer, &from_frame, &to_frame, 1000, 0); + animate_layer(alert_layer, &to_frame, &from_frame, 1000, 3000); +} - menu = menu_layer_create(GRect(0, 16, PBL_IF_RECT_ELSE(144,180), PBL_IF_RECT_ELSE(168,180) - 16)); - menu_layer_set_callbacks(menu, NULL, (MenuLayerCallbacks){ - .draw_row = draw_pokemon, - .get_num_rows = get_num_pokemon, - .get_cell_height = pokemon_cell_height, - }); - //menu_layer_set_click_config_onto_window(menu, list); - layer_add_child(window_get_root_layer(list), menu_layer_get_layer(menu)); - overlay = layer_create(layer_get_bounds(window_get_root_layer(list))); - layer_set_update_proc(overlay, temp_draw); +// ------------------------------------------------------------------------ // +// AppMessage Communication Functions +// ------------------------------------------------------------------------ // +static void inbox_received_callback(DictionaryIterator *iterator, void *context) { + // TODO: use https://github.com/smallstoneapps/data-processor instead of this quick crude hack! + // APP_LOG(APP_LOG_LEVEL_INFO, "App Message received!"); + + Tuple *display_message_tuple, + *pokemon_id_tuple, + *pokemon_expiration_tuple, + *pokemon_latitude_tuple, + *pokemon_longitude_tuple, + *user_latitude_tuple, + *user_longitude_tuple; + + + // If app message is: a DISPLAY MESSAGE + if ((display_message_tuple = dict_find(iterator, KEY_DISPLAY_MESSAGE))) { + if(display_message_tuple->value->cstring) { + displayToast(display_message_tuple->value->cstring); + } + } - status_bar = status_bar_layer_create(); - int16_t width = layer_get_bounds(overlay).size.w; - GRect frame = GRect(0, -1, width, STATUS_BAR_LAYER_HEIGHT); // -1 so the overlay still shows up beneath it - layer_set_frame(status_bar_layer_get_layer(status_bar), frame); - status_bar_layer_set_colors(status_bar, GColorCobaltBlue, GColorWhite); - layer_add_child(overlay, status_bar_layer_get_layer(status_bar)); + + // If app message is: USER GPS LOCATION + if ((user_latitude_tuple = dict_find(iterator, KEY_USER_LATITUDE)) && + (user_longitude_tuple = dict_find(iterator, KEY_USER_LONGITUDE))) { + user.lat = user_latitude_tuple->value->int32; + user.lon = user_longitude_tuple->value->int32; + //APP_LOG(APP_LOG_LEVEL_DEBUG, "New User GPS: (%d, %d)", (int)user.lat, (int)user.lon); + if(NUM_POKEMON == 1 && pokemon[0].dex == 0) { // If currently in "No pokemon" / "message" mode + //if (pokemon[0].expires == MSG_LOADING) pokemon[0].expires = MSG_FINDING_POKEMON; // set message (using expires) to + } else { + sort_pokemon_by_distance(); // new user location means distances changed! + } + } + + + // If app message is: A POKEMON + if ((pokemon_id_tuple = dict_find(iterator, KEY_POKEMON_ID))) { + int32_t pokemon_id = pokemon_id_tuple->value->int32; + time_t now = time(NULL); + // Initialize variables with default values in case tuples don't exist + int32_t pokemon_longitude = 0; + int32_t pokemon_latitude = 0; + int32_t pokemon_distance = -1; + int32_t pokemon_expiration = now; + + if ((pokemon_expiration_tuple = dict_find(iterator, KEY_POKEMON_EXPIRATION_TIME))) + pokemon_expiration = pokemon_expiration_tuple->value->int32; + + if ((pokemon_latitude_tuple = dict_find(iterator, KEY_POKEMON_LATITUDE))) + pokemon_latitude = pokemon_latitude_tuple->value->int32; + + if ((pokemon_longitude_tuple = dict_find(iterator, KEY_POKEMON_LONGITUDE))) + pokemon_longitude = pokemon_longitude_tuple->value->int32; + + int32_t y = gps_lat_distance(user.lat, pokemon_latitude); + int32_t x = gps_lon_distance(user.lat, user.lon, pokemon_longitude); + pokemon_distance = gps_distance(y, x); + + APP_LOG(APP_LOG_LEVEL_DEBUG, + "PEB Pokemon: ID %d (%d, %d), Dist:%d (%d, %d)", + (int)pokemon_id, + (int)pokemon_latitude, + (int)pokemon_longitude, + (int)pokemon_distance, + (int)x, + (int)y + ); + + if(NUM_POKEMON == 1 && pokemon[0].dex == 0) { // If currently in "No pokemon" / "message" mode + if (pokemon_id == 0) { // Received a "no pokemon found" message + pokemon[0].expires = MSG_NO_POKEMON_FOUND; // using expires to denote a message when dex=0 + if(loading) remove_splash_screen(); // No pokemon found! Remove splash screen to display message + return; + } else { + NUM_POKEMON = 0; // ID doesn't = 0, so get rid of "Loading..." or "No pokemon" message. We found pokemon! + } + } + + if (pokemon_id == 0) { + APP_LOG(APP_LOG_LEVEL_INFO, "No additional pokemon found."); + return; + } + + if (pokemon_id <= 0 || pokemon_id > 151) { // = 0 shouldn't happen, already caught above + APP_LOG(APP_LOG_LEVEL_ERROR, "ERROR! Invalid Pokemon ID = %d", (int)pokemon_id); + return; + } + + if (pokemon_latitude==0 && pokemon_longitude==0) { + APP_LOG(APP_LOG_LEVEL_ERROR, "ERROR! Bad Pokemon Location"); + return; + } + + if (pokemon_expiration < now) { + APP_LOG(APP_LOG_LEVEL_ERROR, "ERROR! Pokemon already expired!"); + return; + } + + if (NUM_POKEMON == MAX_POKEMON) // If we've maxed out our list, then don't add it + return; + + if (pokemon_distance < 0) // I'm not even sure how this is possible... + return; + + + // Passed all the error traps above! Add the new pokemon to the list! + pokemon[NUM_POKEMON].dex = pokemon_id; + pokemon[NUM_POKEMON].lat = pokemon_latitude; + pokemon[NUM_POKEMON].lon = pokemon_longitude; + pokemon[NUM_POKEMON].expires = pokemon_expiration; + pokemon[NUM_POKEMON].distance = pokemon_distance; + NUM_POKEMON++; + + if(loading) remove_splash_screen(); // Pokemon found! Remove splash screen + } + menu_layer_reload_data(menu_layer); +} - layer_add_child(window_get_root_layer(list), overlay); -/* - //DUMMY DATA - NUM_POKEMON = 5; +static void inbox_dropped_callback(AppMessageResult reason, void *context) { + APP_LOG(APP_LOG_LEVEL_ERROR, "Message dropped!"); +} - nearby[0].sprite = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_poke25); - strncpy(nearby[0].listBuffer, "Pikachu\n\n12 m", sizeof(nearby[0].listBuffer)); +static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) { + APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox send failed!"); +} - nearby[1].sprite = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_poke83); - strncpy(nearby[1].listBuffer, "Farfetchd\n\n53 m", sizeof(nearby[1].listBuffer)); +static void outbox_sent_callback(DictionaryIterator *iterator, void *context) { + APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success!"); +} - nearby[2].sprite = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_poke128); - strncpy(nearby[2].listBuffer, "Tauros\n\n121 m", sizeof(nearby[2].listBuffer)); +static void register_callbacks(){ + // Register callbacks + app_message_register_inbox_received(inbox_received_callback); + app_message_register_inbox_dropped(inbox_dropped_callback); + app_message_register_outbox_failed(outbox_failed_callback); + app_message_register_outbox_sent(outbox_sent_callback); - nearby[3].sprite = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_poke35); - strncpy(nearby[3].listBuffer, "Clefairy\n\n135 m", sizeof(nearby[3].listBuffer)); + // Open AppMessage + // TODO: sizes? + app_message_open(2048,2048); +} - nearby[4].sprite = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_poke138); - strncpy(nearby[4].listBuffer, "Omanyte\n\n163 m", sizeof(nearby[4].listBuffer)); -*/ - // crude loading message - /* - NUM_POKEMON = 1; - - nearby[0].sprite = gbitmap_create_with_resource(poke_images[0]); - strncpy(nearby[0].listBuffer, "Loading...", sizeof(nearby[0].listBuffer)); - */ - compass_service_subscribe(compass_handler); - - //menu_layer_reload_data(menu); - //menu_layer_set_selected_index(menu, (MenuIndex){0,1}, MenuRowAlignNone, false); - - //window_stack_push(list, true); - alert = layer_create(GRect(PBL_IF_ROUND_ELSE(40, 34), 200, 100, 100)); - alertText = text_layer_create(GRect(0, 0, 100, 100)); - text_layer_set_text(alertText, ""); - text_layer_set_text_alignment(alertText, GTextAlignmentCenter); - layer_add_child(alert, text_layer_get_layer(alertText)); +// ------------------------------------------------------------------------ // +// Callback Handler Functions +// ------------------------------------------------------------------------ // +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + // Iterate through all pokemon, remove old dead, update cell position + // Refresh countdown and compass once per second + cleanup_pokemon_list(); + layer_mark_dirty(window_get_root_layer(list_window)); +} - alertBackgroundLayer = bitmap_layer_create(GRect(0, 0, 100, 100)); - bitmap_layer_set_compositing_mode(alertBackgroundLayer, GCompOpSet); - alertBackground = gbitmap_create_with_resource(RESOURCE_ID_ALERT_BACKGROUND); - bitmap_layer_set_bitmap(alertBackgroundLayer, alertBackground); - layer_add_child(alert, bitmap_layer_get_layer(alertBackgroundLayer)); - layer_add_child(overlay, alert); +static void bluetooth_callback(bool connected) { + if(connected) { + displayToast("Bluetooth reconnected"); + } else { + displayToast("Bluetooth disconnected"); + } } -void deinit(){ - window_destroy(list); - window_destroy(compass); - menu_layer_destroy(menu); - layer_destroy(overlay); - fonts_unload_custom_font(custom_font); +void compass_handler(CompassHeadingData heading){ + compass_heading = heading.true_heading; + // Commenting out below to save battery. Tick timer already updates once per second. + //layer_mark_dirty(window_get_root_layer(list_window)); // Refresh every time compass updates + //layer_mark_dirty(window_get_root_layer(compass_window)); // TODO: Compass Window? } -int main(){ - init(); - app_event_loop(); - deinit(); + + +// ------------------------------------------------------------------------ // +// Main Functions +// ------------------------------------------------------------------------ // +void init(){ + // ----------------- // + // Init variables + // ----------------- // + team = TEAM_MYSTIC; + user.lat = 0; user.lon = 0; + + // crude loading message + // NUM_POKEMON=1 and DEX=0 means "message". EXPIRES=0 means use message "Loading..." + NUM_POKEMON = 1; + pokemon[0].dex = 0; + pokemon[0].expires = MSG_LOADING; // currently obsolete with splash screen covering loading message + + + // ----------------- // + // Setup Handlers + // ----------------- // + register_callbacks(); + + tick_timer_service_subscribe(SECOND_UNIT, tick_handler); + + connection_service_subscribe((ConnectionHandlers) {.pebble_app_connection_handler = bluetooth_callback}); + + compass_service_subscribe(compass_handler); + + + // ----------------- // + // Header/Footer and Font + // ----------------- // + custom_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PKMN_10)); + + ui_top_bitmap = gbitmap_create_with_resource(RESOURCE_ID_UI_TOP); + ui_bottom_bitmap = gbitmap_create_with_resource(RESOURCE_ID_UI_BOTTOM); + + #ifdef PBL_COLOR + GColor *pal_top = gbitmap_get_palette(ui_top_bitmap); + GColor *pal_bottom = gbitmap_get_palette(ui_bottom_bitmap); + + switch (team) { + case TEAM_NONE: + pal_top[0] = GColorDarkGray; + pal_bottom[2] = GColorDarkGray; + break; + + case TEAM_MYSTIC: + pal_top[0] = GColorCobaltBlue; + pal_bottom[2] = GColorCobaltBlue; + break; + + case TEAM_INSTINCT: + pal_top[0] = GColorOrange; + pal_bottom[2] = GColorOrange; + break; + } + #endif + + + + + // ---------------------------------- // + // Setup splash window + // ---------------------------------- // + splash_window = window_create(); + window_set_background_color(splash_window, GColorBlack); + window_stack_push(splash_window, true); + + // ----------------- // + // Splash Bitmap Layer + // ----------------- // + splash_bitmap = gbitmap_create_with_resource(RESOURCE_ID_POGO); + GRect window_bounds = layer_get_bounds(window_get_root_layer(splash_window)); + GRect bitmap_bounds = gbitmap_get_bounds(splash_bitmap); + bitmap_layer = bitmap_layer_create( GRect((window_bounds.size.w - bitmap_bounds.size.w)/2, (window_bounds.size.h - bitmap_bounds.size.h)/2, bitmap_bounds.size.w, bitmap_bounds.size.h) ); + bitmap_layer_set_bitmap(bitmap_layer, splash_bitmap); + layer_add_child(window_get_root_layer(splash_window), bitmap_layer_get_layer(bitmap_layer)); + + // ----------------- // + // Splash Text Layer + // ----------------- // + loading_layer = text_layer_create(GRect(0, window_bounds.size.w/2 + bitmap_bounds.size.w/2, window_bounds.size.w, 28)); + text_layer_set_text(loading_layer, "Loading..."); + text_layer_set_background_color(loading_layer, GColorBlack); + text_layer_set_text_color(loading_layer, GColorWhite); + text_layer_set_text_alignment(loading_layer, GTextAlignmentCenter); + layer_add_child(window_get_root_layer(splash_window), text_layer_get_layer(loading_layer)); + + + + // ---------------------------------- // + // Setup Main Window + // ---------------------------------- // + list_window = window_create(); + window_set_click_config_provider(list_window, config); + window_set_background_color(list_window, GColorWhite); + //window_stack_push(list_window, true); // Commented out as we now load splash screen first + + + // ----------------- // + // Setup menu layer + // ----------------- // + menu_layer = menu_layer_create(GRect(0, 16, PBL_IF_RECT_ELSE(144,180), PBL_IF_RECT_ELSE(168,180) - 16)); + menu_layer_set_callbacks(menu_layer, NULL, (MenuLayerCallbacks){ + .draw_row = draw_pokemon, + .get_num_rows = get_num_pokemon, + .get_cell_height = pokemon_cell_height, + .get_header_height = menu_get_header_height, + .draw_header = menu_draw_header, + .get_num_sections = menu_get_num_sections + }); + //menu_layer_set_click_config_onto_window(menu_layer, list_window); + layer_add_child(window_get_root_layer(list_window), menu_layer_get_layer(menu_layer)); + menu_layer_reload_data(menu_layer); + menu_layer_set_selected_index(menu_layer, (MenuIndex){0,0}, MenuRowAlignNone, false); + + + // ----------------- // + // Setup overlay layer + // ----------------- // + overlay_layer = layer_create(layer_get_bounds(window_get_root_layer(list_window))); + layer_set_update_proc(overlay_layer, draw_overlay_layer); + layer_add_child(window_get_root_layer(list_window), overlay_layer); + + + // ----------------- // + // Setup status bar + // ----------------- // + status_bar_layer = status_bar_layer_create(); + int16_t width = layer_get_bounds(overlay_layer).size.w; + GRect frame = GRect(0, -1, width, STATUS_BAR_LAYER_HEIGHT); // -1 so the overlay still shows up beneath it + layer_set_frame(status_bar_layer_get_layer(status_bar_layer), frame); + status_bar_layer_set_colors(status_bar_layer, GColorCobaltBlue, GColorWhite); + layer_add_child(overlay_layer, status_bar_layer_get_layer(status_bar_layer)); + + + // ----------------- // + // Setup alert layer + // ----------------- // + alert_layer = layer_create(GRect(PBL_IF_ROUND_ELSE(40, 34), 200, 100, 100)); + alert_text_layer = text_layer_create(GRect(0, 0, 100, 100)); + text_layer_set_text(alert_text_layer, alert_text_buffer); + text_layer_set_text_alignment(alert_text_layer, GTextAlignmentCenter); + layer_add_child(alert_layer, text_layer_get_layer(alert_text_layer)); + layer_add_child(window_get_root_layer(splash_window), alert_layer); + + // ----------------- // + // Setup alert background layer + // ----------------- // + alert_background_bitmap_layer = bitmap_layer_create(GRect(0, 0, 100, 100)); + bitmap_layer_set_compositing_mode(alert_background_bitmap_layer, GCompOpSet); + alert_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_ALERT_BACKGROUND); + bitmap_layer_set_bitmap(alert_background_bitmap_layer, alert_background_bitmap); + layer_add_child(alert_layer, bitmap_layer_get_layer(alert_background_bitmap_layer)); +} + + +void deinit() { + window_destroy(splash_window); + + + window_destroy(list_window); + //window_destroy(compass_window); + menu_layer_destroy(menu_layer); + layer_destroy(overlay_layer); + status_bar_layer_destroy(status_bar_layer); + fonts_unload_custom_font(custom_font); } + + +int main() { + init(); + app_event_loop(); + deinit(); +} \ No newline at end of file