Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

right way to trigger the page to re-evaluate (from Javascript)

(First: I apologize if there is an obvious answer, I'm new to hacking Twine)

Hi. As background, I'm planning on using Twine for a two-week high-school class I'm helping teach and my kids school. I'm still a bit of a twine novice, so I'm attempting to learn twine, learn the style (I'm planning on SugarCube 2.6), collect resources for the kids to learn this, and create some samples. (I have lots of technical experience, just not with these tools)

What I want to do is do some outdoor, location-based stories by leveraging the location api's in mobile Safari on an iPad. My thought about the right way to do this is to create a new SugarCube macro (like some of the examples such as the Shake ones) that accesses the location, and create a few variants (e.g., tell me if the location is within X meters of this long, lat). These are things I know how to do (dealing with location in javascript, etc).

BUT, given this, I was wondering what the best way might be to deal with updating the page when the player moves around. I was thinking of polling the location in Javascript, using a timer or requestAnimationFrame, etc. If the result of a location macro changes, it would seem the best thing to do would be to "re-evaluate" the whole page so that if an author creates a passage with a lot of dependencies on the location, things will work as expected. If I was just changing CSS or the DOM, I realize I can just make the change. I don't understand enough of the internals of twinejs to know if I need to do something else to cause twine to re-evaluate everything or if there's an obvious macro or function to call.

Thanks!

Comments

  • Knowing the internals of twinejs will not help you in this situation as it is only handles the editing of passages and the combining of those passages with a Story Format. Each Story Format is a mini web-application which uses the passage data from the Twine editor as a data source.

    I believe the best person to talk to would be @TheMadExile, the developer of the SugarCube story format.

    You are not the first person to ask about using the Loaction API, the two related threads are geolocation as trigger and relative geotriggers.
  • greyelf wrote: »
    Knowing the internals of twinejs will not help you in this situation as it is only handles the editing of passages and the combining of those passages with a Story Format. Each Story Format is a mini web-application which uses the passage data from the Twine editor as a data source.

    I believe the best person to talk to would be @TheMadExile, the developer of the SugarCube story format.

    You are not the first person to ask about using the Loaction API, the two related threads are geolocation as trigger and relative geotriggers.

    Thanks @greyelf. I'll look at those posts, and see if @TheMadExile replies.
  • BUT, given this, I was wondering what the best way might be to deal with updating the page when the player moves around. I was thinking of polling the location in Javascript, using a timer or requestAnimationFrame, etc. If the result of a location macro changes, it would seem the best thing to do would be to "re-evaluate" the whole page so that if an author creates a passage with a lot of dependencies on the location, things will work as expected. If I was just changing CSS or the DOM, I realize I can just make the change. I don't understand enough of the internals of twinejs to know if I need to do something else to cause twine to re-evaluate everything or if there's an obvious macro or function to call.
    As a somewhat naive solution. If you keep all of the markup needing updating with the main passage display area, then you could simply call Engine.show() to redisplay the currently displayed passage. That will refresh the entire passage display each time you trigger it, however, which may not be desirable.

    As an alternative. You could have a macro which would wrap your bits of location markup and update them when an event was triggered. As to the event, you'd trigger it each time new location data becomes available. That would allow you to only update the parts of the page which required it.

    For example. The following code sets the $location story variable and comes with three macros:
    <<glstart>> - Starts a geolocation watch.
    <<glstop>> - Stops the geolocation watch.
    <<glupdate>>…<</glupdate>> - Renders its contents within a wrapper element. Whenever a geolocation update occurs, it rerenders its contents and updates the wrapper.
    Code
    /*! Geolocation macro set for SugarCube 2.x */
    (function () {
    	'use strict';
    
    	// SugarCube check: if the SugarCube version isn't correct, bail out now
    	if (
    		   typeof version === 'undefined'
    		|| typeof version.title === 'undefined' || version.title !== 'SugarCube'
    		|| typeof version.major === 'undefined' || version.major < 2
    		|| typeof version.minor === 'undefined' || version.minor < 5
    	) {
    		throw new Error('Geolocation macro set requires SugarCube 2.5.0 or greater, aborting load');
    	}
    
    
    	/***************************************************************************
    	 * MACRO SETUP
    	 **************************************************************************/
    	/* The geolocation API appears to be available, setup the macros. */
    	if ('geolocation' in navigator && typeof navigator.geolocation.watchPosition === 'function') {
    		var
    			// Geolocation position options (user configurable).
    			glOptions = {
    				// Whether we want to receive the most accurate results possible.  Enabling
    				// this may result in slower response times or increased power consumption.
    				enableHighAccuracy : false, // default: false
    
    				// Cached results may not be older than the specified value (in milliseconds).
    				maximumAge : 0, // default: 0
    
    				// The maximum length of time (in milliseconds) that the device is allowed to
    				// take in order to respond with position data.
    				timeout : Infinity // default: Infinity
    			},
    
    			// ID of the running `watchPosition()` method, if any.
    			glWatchId = null;
    
    		/*
    			<<glstart>>
    		*/
    		Macro.add('glstart', {
    			handler : function () {
    				// A watch is already running, so return.
    				if (glWatchId !== null) {
    					return;
    				}
    
    				// If it does not already exist, we give `$location` an initial value, so
    				// trying to access it immediately does not cause issues if the first
    				// success callback takes a while—since the geolocation API is asynchronous.
    				if (!State.variables.hasOwnProperty('location')) {
    					State.variables.location = {
    						accuracy         : 0,
    						altitude         : null,
    						altitudeAccuracy : null,
    						heading          : null,
    						latitude         : 0,
    						longitude        : 0,
    						speed            : null
    					};
    				}
    
    				// Success callback.
    				function onSuccess(position) {
    					var
    						// These are reference types, so caching them is OK.
    						svl = State.variables.location,
    						glc = position.coords;
    
    					// Assign the geolocation coordinate properties to the `$location` object.
    					svl.accuracy         = glc.accuracy;
    					svl.altitude         = glc.altitude;
    					svl.altitudeAccuracy = glc.altitudeAccuracy;
    					svl.heading          = glc.heading;
    					svl.latitude         = glc.latitude;
    					svl.longitude        = glc.longitude;
    					svl.speed            = glc.speed;
    
    					// Trigger a global `tw:geolocationupdate` event.
    					jQuery.event.trigger('tw:geolocationupdate');
    				}
    
    				// Error callback.
    				function onError(error) {
    					/* currently a no-op; code that handles errors */
    				}
    
    				// Register a watch.
    				glWatchId = navigator.geolocation.watchPosition(onSuccess, onError, glOptions);
    			}
    		});
    
    		/*
    			<<glstop>>
    		*/
    		Macro.add('glstop', {
    			handler : function () {
    				if (glWatchId !== null) {
    					navigator.geolocation.clearWatch(glWatchId);
    					glWatchId = null;
    				}
    			}
    		});
    
    		/*
    			<<glupdate>>
    		*/
    		Macro.add('glupdate', {
    			tags    : null,
    			handler : function () {
    				// Custom debug view setup.
    				if (Config.debug) {
    					this.debugView.modes({ block : true });
    				}
    
    				var
    					contents = this.payload[0].contents,
    					$wrapper = jQuery(document.createElement('span'));
    
    				$wrapper
    					.addClass('macro-' + this.name)
    					.wiki(contents)
    					.appendTo(this.output);
    
    				jQuery(document).on('tw:geolocationupdate', function () {
    					var frag = document.createDocumentFragment();
    					new Wikifier(frag, contents);
    					$wrapper.empty().append(frag);
    				});
    			}
    		});
    	}
    
    	/* The geolocation API appears to be missing or disabled, setup no-op macros. */
    	else {
    		Macro.add(['glstart', 'glstop'], {
    			handler : function () { /* empty */ }
    		});
    		Macro.add('glupdate', {
    			tags    : null,
    			handler : function () { /* empty */ }
    		});
    	}
    }());
    

    Usage (simple example)
    Put the following in your StoryInit special passage:
    <<glstart>>
    
    Then any code which depends on the geolocation data would be wrapped in the <<glupdate>> macro. For example: (just to show all of the $location properties)
    Your current location coordinates are:
    <<glupdate>>\
    | !Accuracy:|$location.accuracy |
    | !Altitude:|$location.altitude |
    | !Altitude Accuracy:|$location.altitudeAccuracy |
    | !Heading:|$location.heading |
    | !Latitude:|$location.latitude |
    | !Longitude:|$location.longitude |
    | !Speed:|$location.speed |
    <</glupdate>>
    
  • Thanks @TheMadExile, that looks like a good starting point, and I appreciate you (and @greyelf) taking the time. I'll start here and see where I get. You are likely right that re-evaluating the whole page is not exactly what I want, since there may be more complex things happening on a typical page that you don't want re-evaluated constantly.
  • Forgot to mention. Near the top of the module you'll find the glOptions object, which allows you some control over how the location data is collected (accuracy, caching, and rate). It's this bit:
    // Geolocation position options (user configurable).
    glOptions = {
    	// Whether we want to receive the most accurate results possible.  Enabling
    	// this may result in slower response times or increased power consumption.
    	enableHighAccuracy : false, // default: false
    
    	// Cached results may not be older than the specified value (in milliseconds).
    	maximumAge : 0, // default: 0
    
    	// The maximum length of time (in milliseconds) that the device is allowed to
    	// take in order to respond with position data.
    	timeout : Infinity // default: Infinity
    },
    
    I think it's commented fairly well, however, in case you wanted another take on it, it's a standard position options object.
  • Here's an update @TheMadExile.

    I'm attaching the current draft of the code I'm using. I started with yours but added a few things:
    - reporting in meters as well as LLA. I've used different geospatial conversation in the past, but since these stories are local, I think UTM (https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system) is a reasonable one since it's easy to convert to/from. I based the conversion on https://github.com/urbanetic/utm-converter
    - added a function to let you define some points by name so we don't need to keep redoing the computations
    - added a function to compute the distance to a point from wherever you are (since this is a common case)

    It's a start. I have some other things I want to add, but I thought I'd share back where your help led me.
Sign In or Register to comment.