Geofencing alternative proposal

In the spirit of taking until the last minute, here is something that
has a little more detail than the other proposals.  I've had *some*
feedback from Mozilla folks, but I wouldn't say that this is a Mozilla
proposal by any stretch. Consider it merely as input to the process.

---

I think that Google's proposal includes most of the right elements,
but I don't think that it covers enough of the details.  On my little
deep dive, I found quite a few.  I'm basing this proposal off the
Google one, but I'm proposing structural changes, as well as several
superficial changes.

I'm guided here by the discussion here:
https://github.com/slightlyoff/ServiceWorker/issues/445, which is only
recent.

## Initialization and Access

The core of the feature seems about right, but some restructuring
might be needed:

  interface GeolocationFenceControl {
    Promise<GeolocationFence> addFence(GeolocationFenceDescription fence);
    Promise<sequence<GeolocationFence>> getFences();
  };

Then, we make this available from window.geolocation:

  [Exposed=(Window, Worker)]
  // I should have added this annotation throughout, but I'm lazy
  partial interface Geolocation {
    readonly attribute GeolocationFenceControl geofence;
  };

And also make this available from the service worker global scope.

  partial interface ServiceWorkerGlobalScope {
    readonly attribute Geolocation geolocation;
  };

A consequence of this choice is that sites with persistent permissions
for location will be able to request location information
spontaneously from a SW.  I think that's a feature, but it's something
we should discuss.  There are implications: watchPosition might need
to be specifically prohibited from the SW context, and we might want
to consider the use of a promise-based API too.  But I'll leave those
for later discussion, I think.

The control object is also made available from the window scope in
order to register the intent to use geolocation within a Service
Worker. Specifically, to register what regions are of interest to that
service worker, if any.

  partial interface ServiceWorkerRegistration {
    readonly attribute GeolocationWorkerRegistration geolocation;
  };

If the outer window hasn't registered for geolocation in the service
worker, then the object is there, but it can be rendered
non-functional.  That is, attempts to register fences are rejected
immediately.  Note also that persistent user consent will be needed to
access geolocation in this context.

  interface GeolocationWorkerRegistration {
    Promise<GeolocationFenceControl> register();
    // I originally had a variadic argument here as a shortcut for
    // registration.register().addFence(f);
    // ...since the return values aren't especially interesting.
    // But that's pure sugar and unnecessary.
  };

[ISSUE] The GeolocationFence objects that are returned from this
context aren't going to be functional - events can't surface in the
window, they will only appear in the worker.  I don't know if it is
worth creating parallel interfaces for this purpose that don't produce
events, or whether we can just neuter those objects (i.e., make the
events not fire).  There are benefits to either approach.

## GeoFence Object Definition

  interface GeolocationFence : EventTarget {
    // cancel() kills the fence (i.e., no further events)
    Promise<void> cancel();
    attribute EventHandler onenter;
    attribute EventHandler onleave;
    attribute EventHandler onerror;
  };

As you can see, this is where you get your events.  I've had at least
one person suggest that events on the global (or globally accessible
singleton) is perfectly fine, but I think that this turns out cleaner.
Much if these issues come down to taste, so I'm happy to go with
whatever people like best on the whole.  Me, I prefer to localize
things like this.

The GeolocationFence has a dictionary analogue for
construction/initialization purposes.

  dictionary GeolocationFenceDescription {
   GeolocationFenceType type;
  };

  enum GeolocationFenceType { "circle", "polygon" };

Regarding GeolocationFenceType, we could probably get away with the
API inferring type, but that leads to errors.  The resulting IDL is
clunky, but the usage should be straightforward:

  var r = global.geolocation.geofence
    .addRegion({type: "circle", center: { latitude: 43... }, radius: 100});

Alternatively it could be made an extra argument, I'm not sure what is
best here:

    .addRegion("circle", { center: { latitude: 43... }, radius: 100});

[ISSUE]  I note that other APIs use DOMString as the input type when
enumerations are used to permit extension.  I'm not sure if that's a
good idea here.

## Region Types

I think that we need both circular and polygonal fences.  Imagine here
that there is a dictionary form for each of these.  All of these have
no constructor, instead relying on the addRegion return value for
construction.

  interface GeolocationCircularFence : GeolocationFence {
    readonly attribute GeolocationPoint center;
    readonly attribute double radius;  // metres, always
  };

  interface GeolocationPolygonFence : GeolocationFence {
    readonly attribute sequence<GeolocationPoint> exterior;
  };

Imagine dictionary analogues inheriting from
GeolocationFenceDescription with the same attributes.

Then there is a point type (not needed as a separate type if we don't
do polygonal fences).

  interface GeolocationPoint {
    readonly attribute double latitude;
    readonly attribute double longitude;
  };

This intentionally excludes altitude and the motion-related fields
from Position.

Regarding the MIN_RADIUS and MAX_RADIUS proposed by Google, I don't
see the need to expose that information.  There are certainly reasons
why certain sizes of region represent implementation challenges, but
we should instead surface errors.  There are certainly cases where
regions will be effectively useless due to being too small, but I
don't think that we need to (or indeed can) provide such specific
feedback to apps.  On the other hand, a large region can always be
addressed, even if it is not able to use efficient hardware support.
We already need to implement something in the absence of hardware
support for this, so I don't see the size being a special blocker.

## Presenting Results

The current API uses callbacks, which this would not.  So we need a
way to expose the information we have in events.  That requires a
definition.

  [Constructor(DOMString type, GeolocationEventInit init)]
  interface GeolocationEvent : Event {
    readonly attribute Position position;
  };
  // GeolocationEventInit is a dictionary with the same

Here, the event exposes the current position.  I've marked this as
mandatory on the assumption that knowing location is a precondition on
being able to provide this event, but Stephen Li's use case around
unknown locations would change that.

## Error Handling

The other proposals don't really deal with this in detail, but we
would need include an error handling in a couple of cases.

When the platform is unable to create a new geofencing region I think
we can just have the relevant promise rejected.  That's easy.

We also need to deal with transient errors (can't determine current
location and there is a risk that it is near a region boundary).

I can't think of many particularly compelling terminal errors; it
seems like once a region has been added, we should try to maintain it.
We've been discussing having service workers restored at startup,
which entails a risk that certain regions can't be re-registered.

I think that we want an onerror event for a given region that pops for
both transient (location unknown) and permanent (couldn't maintain
region registration) errors.  That might suggest two handlers, but
I'll defer to others with more experience on this point.  Here's one
option

  [Constructor(DOMString type, GeolocationErrorEventInit init)]
  interface GeolocationErrorEvent : Event {
    readonly attribute unsigned short errorCode;
      // uses PositionError.code
  };
  // GeolocationErrorEventInit is a dictionary with the same

And this relies on PositionError changes:

  partial interface PositionError {
    const unsigned short GEOFENCE_UNREGISTERED = 4;
  };

This code indicates that the corresponding fence is no more.

Received on Friday, 12 September 2014 20:31:40 UTC