+
);
}}
);
@@ -290,7 +336,8 @@ var MRConfig = (function () {
// the default map options
mapOptions: {
center: new L.LatLng(40, -90),
- zoom: 4
+ zoom: 4,
+ keyboard: false
},
// default tile URL
@@ -306,6 +353,8 @@ var MRConfig = (function () {
var MRManager = (function () {
var map;
+ var editArea;
+ var MAX_EDIT_RADIUS = 512000 // 512 km
var challenges = [];
var challenge = {};
var task = {};
@@ -315,7 +364,14 @@ var MRManager = (function () {
'lat': parseFloat(Q.lat)
} : {};
var difficulty = parseInt(Q.difficulty);
- var taskLayer;
+ var taskLayer = new L.geoJson(null, {
+ onEachFeature: function (feature, layer) {
+ if (feature.properties && feature.properties.text) {
+ layer.bindPopup(feature.properties.text);
+ return layer.openPopup();
+ }
+ }
+ });
// create a notifier
notify = MRNotifier;
@@ -420,16 +476,6 @@ var MRManager = (function () {
attribution: MRConfig.tileAttrib
});
- // and the GeoJSON layer
- taskLayer = new L.geoJson(null, {
- onEachFeature: function (feature, layer) {
- if (feature.properties && feature.properties.text) {
- layer.bindPopup(feature.properties.text);
- return layer.openPopup();
- }
- }
- });
-
// Add both the tile layer and the task layer to the map
map.addLayer(tileLayer);
map.addLayer(taskLayer);
@@ -516,6 +562,7 @@ var MRManager = (function () {
$('#challenge_title').text(challenge.title);
$('#challenge_blurb').text(challenge.blurb);
// and move on to get the stats
+
getChallengeStats();
if (presentDialog) presentChallengeDialog();
},
@@ -524,16 +571,16 @@ var MRManager = (function () {
var getChallengeStats = function () {
- // now get the challenge stats
- // var endpoint = '/api/stats/challenge/' + challenge.slug;
- // $.getJSON(endpoint, function (data) {
- // for (key in data) {
- // console.log('raw value: ' + data[key]);
- // var value = parseInt(data[key]) > 10 ? 'about ' + (~~((parseInt(data[key]) + 5) / 10) * 10) : 'only a few';
- // console.log('value for ' + key + ': ' + value);
- // $('#challenge_' + key).html(value).fadeIn();
- // };
- // });
+ //now get the challenge stats
+ var endpoint = '/api/stats/challenge/' + challenge.slug;
+ $.getJSON(endpoint, function (data) {
+ for (key in data) {
+ console.log('raw value: ' + data[key]);
+ var value = parseInt(data[key]) > 10 ? 'about ' + (~~((parseInt(data[key]) + 5) / 10) * 10) : 'only a few';
+ console.log('value for ' + key + ': ' + value);
+ $('#challenge_' + key).html(value).fadeIn();
+ };
+ });
};
var updateTask = function (action) {
@@ -785,9 +832,98 @@ var MRManager = (function () {
nextTask();
};
- var userPreferences = function () {
- //FIXME implement
- };
+ var userPickEditLocation = function () {
+ notify.play('Click on the map to let MapRoulette know where you want to edit. ' +
+ 'You can use the +/- on your keyboard to increase the radius of your area.
' +
+ 'When you are satisfied with your selection, Click this notification to confirm your selection. ' +
+ 'To unset your editing area, or cancel, click this dialog without making a selection.', {
+ killer: true,
+ timeout: false,
+ callback: {
+ afterClose: MRManager.confirmPickingLocation
+ },
+ });
+ // remove geoJSON layer
+ map.removeLayer(taskLayer);
+ // set zoom level
+ if (map.getZoom() > 10) map.setZoom(10, true);
+ // add area on click
+ map.on('click', MRManager.isPickingLocation);
+ // add handlers for increasing radius
+ $(document).bind('keypress.plusminus', function (e) {
+ if (map.hasLayer(editArea)) {
+ if (editArea.getRadius() < MAX_EDIT_RADIUS && (e.which == 43 || e.which == 61)) { // plus
+ editArea.setRadius(editArea.getRadius() + (100 * 18 - Math.max(9, map.getZoom()))); // zoom dependent increase
+ } else if (editArea.getRadius() > 0 && (e.which == 45 || e.which == 95)) { // minus
+ editArea.setRadius(editArea.getRadius() - (100 * 18 - Math.max(9, map.getZoom()))); // zoom dependent decrease
+ }
+ }
+ });
+ }
+
+ var isPickingLocation = function (e) {
+ var zoomDependentEditRadius = 100 * Math.pow(2, 18 - Math.max(6, map.getZoom()));
+ if (map.hasLayer(editArea)) {
+ zoomDependentEditRadius = editArea.getRadius();
+ map.removeLayer(editArea);
+ };
+ editArea = new L.Circle(e.latlng, zoomDependentEditRadius)
+ editArea.addTo(map);
+ }
+
+ var confirmPickingLocation = function() {
+ var data = {};
+ if (!editArea) {
+ data ={
+ "lon" : null,
+ "lat" : null,
+ "radius" : null
+ };
+ $('#msg_editarea').hide();
+ notify.play('You cleared your designated editing area.', {killer: true});
+ } else {
+ data = {
+ "lon" : editArea.getLatLng().lng,
+ "lat" : editArea.getLatLng().lat,
+ "radius" : editArea.getRadius()
+ };
+ $('#msg_editarea').show();
+ notify.play('You have set your preferred editing location.', {killer: true})
+ console.log(editArea.toGeoJSON());
+ };
+ storeServerSettings(data);
+ if(map.hasLayer(editArea)) map.removeLayer(editArea);
+ editArea = null;
+ map.addLayer(taskLayer);
+ map.off('click', MRHelpers.isPickingLocation);
+ $(document).unbind('keypress.plusminus', false);
+ getChallengeStats();
+ setTimeout(MRManager.presentChallengeSelectionDialog(), 4000);
+ }
+
+ var getServerSettings = function(keys) {
+ // This gets the server stored session settings for the given array of keys from /api/me
+ // untested and not used yet anywhere
+ $.getJSON('/api/me', function(data) {
+ var out = {};
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i] in data && data[keys[i]] != null) {
+ out[keys[i]] = data[keys[i]];
+ }
+ };
+ return out;
+ });
+ }
+
+ var storeServerSettings = function(data) {
+ // This stores a dict of settings on the server
+ $.ajax({
+ url: "/api/me",
+ type: "PUT",
+ contentType: "application/json",
+ data: JSON.stringify(data)
+ });
+ }
var registerHotkeys = function () {
$(document).bind('keypress', 'q', function () {
@@ -929,14 +1065,18 @@ var MRManager = (function () {
openTaskInId: openTaskInId,
openTaskInJosm: openTaskInJosm,
geolocateUser: geolocateUser,
- userPreferences: userPreferences,
userPickChallenge: userPickChallenge,
+ userPickEditLocation: userPickEditLocation,
+ isPickingLocation: isPickingLocation,
+ confirmPickingLocation: confirmPickingLocation,
readyToEdit: readyToEdit,
presentChallengeSelectionDialog: presentChallengeSelectionDialog,
presentChallengeHelp: presentChallengeHelp,
registerHotkeys: registerHotkeys,
displayUserStats: displayUserStats,
displayAllChallengesStats: displayAllChallengesStats,
+ getServerSettings: getServerSettings,
+ storeServerSettings: storeServerSettings
};
}
());
diff --git a/manage.py b/manage.py
index e250d79..49f9041 100644
--- a/manage.py
+++ b/manage.py
@@ -103,9 +103,12 @@ def create_testdata(challenges=10, tasks=100):
osmids = [random.randrange(1000000, 1000000000) for _ in range(2)]
# add the first point and the linestring to the task's geometries
task.geometries.append(TaskGeometry(osmids[0], p1))
- task.geometries.append(TaskGeometry(osmids[1], l1))
- # and add the first point as the task's location
- task.location = p1
+ # set a linestring for every other challenge
+ if not j % 2:
+ task.geometries.append(TaskGeometry(osmids[1], l1))
+ # because we are not using the API, we need to call set_location
+ # explicitly to set the task's location
+ task.set_location()
# generate random string for the instruction
task.instruction = task_instruction_text
# add the task to the session
@@ -130,7 +133,7 @@ def clean_stale_tasks():
for task in db.session.query(Task).filter(
Task.status.in_(['assigned', 'editing'])).join(
Task.actions).group_by(
- Task.id).having(max(Action.timestamp) < stale_threshold).all():
+ Task.id).having(max(Action.timestamp) < stale_threshold).all():
task.append_action(Action("available"))
db.session.add(task)
print "setting task %s to available" % (task.identifier)
@@ -139,5 +142,20 @@ def clean_stale_tasks():
print 'done. %i tasks made available' % counter
+@manager.command
+def populate_task_location():
+ """This command populates the new location field for each task"""
+ from maproulette.models import db, Task, Challenge
+ for challenge in db.session.query(Challenge):
+ counter = 0
+ for task in db.session.query(Task).filter_by(
+ challenge_slug=challenge.slug):
+ task.set_location()
+ counter += 1
+ db.session.commit()
+ print 'done. Location for %i tasks in challenge %s set' %\
+ (counter, challenge.title)
+
+
if __name__ == "__main__":
manager.run()
diff --git a/maproulette/api/__init__.py b/maproulette/api/__init__.py
index 9231c75..6d24cfc 100644
--- a/maproulette/api/__init__.py
+++ b/maproulette/api/__init__.py
@@ -2,19 +2,19 @@
from flask.ext.restful import reqparse, fields, marshal, \
marshal_with, Api, Resource
from flask.ext.restful.fields import Raw
-from flask import session, make_response, request, abort, url_for
+from flask import session, request, abort, url_for
from maproulette.helpers import get_random_task,\
get_challenge_or_404, get_task_or_404,\
require_signedin, osmerror, challenge_exists,\
- parse_task_json
-from maproulette.models import User, Challenge, Task, TaskGeometry, Action, db
+ parse_task_json, refine_with_user_area, user_area_is_defined,\
+ send_email
+from maproulette.models import User, Challenge, Task, Action, db
from sqlalchemy.sql import func
from geoalchemy2.functions import ST_Buffer
-from shapely.geometry.base import BaseGeometry
+from geoalchemy2.shape import to_shape
import geojson
import json
import markdown
-import requests
class ProtectedResource(Resource):
@@ -28,13 +28,7 @@ class PointField(Raw):
"""An encoded point"""
def format(self, geometry):
- # if we get a linestring, take the first point,
- # else, just get the point.
- if hasattr(geometry, 'coords'):
- point = geometry.coords[0]
- return '%f|%f' % point
- else:
- return '0|0' # this should not happen
+ return '%f|%f' % to_shape(geometry).coords[0]
class MarkdownField(Raw):
@@ -63,7 +57,10 @@ def format(self, text):
me_fields = {
'username': fields.String(attribute='display_name'),
- 'osm_id': fields.String()
+ 'osm_id': fields.String,
+ 'lat': fields.Float,
+ 'lon': fields.Float,
+ 'radius': fields.Integer
}
action_fields = {
@@ -73,49 +70,20 @@ def format(self, text):
'user': fields.String(attribute='user_id')
}
-
api = Api(app)
# override the default JSON representation to support the geo objects
-@api.representation('application/json')
-def output_json(data, code, headers=None):
- """Automatic JSON / GeoJSON output"""
- # return empty result if data contains nothing
- if not data:
- resp = make_response(geojson.dumps({}), code)
- # if this is a Shapely object, dump it as geojson
- elif isinstance(data, BaseGeometry):
- resp = make_response(geojson.dumps(data), code)
- # if this is a list of task geometries, we need to unpack it
- elif not isinstance(data, dict) and isinstance(data[0], TaskGeometry):
- # unpack the geometries FIXME can this be done in the model?
- geometries = [geojson.Feature(
- geometry=g.geometry,
- properties={
- 'selected': True,
- 'osmid': g.osmid}) for g in data]
- resp = make_response(
- geojson.dumps(geojson.FeatureCollection(geometries)),
- code)
- # otherwise perform default json representation
- else:
- resp = make_response(json.dumps(data), code)
- # finish and return the response object
- resp.headers.extend(headers or {})
- return resp
-
-
class ApiPing(Resource):
"""a simple ping endpoint"""
def get(self):
- return "I am alive"
+ return ["I am alive"]
-class ApiMe(ProtectedResource):
+class ApiStatsMe(ProtectedResource):
def get(self):
me = {}
@@ -171,7 +139,7 @@ class ApiChallengeList(ProtectedResource):
"""Challenge list endpoint"""
@marshal_with(challenge_summary)
- def get(self):
+ def get(self, **kwargs):
"""returns a list of challenges.
Optional URL parameters are:
difficulty: the desired difficulty to filter on
@@ -180,14 +148,19 @@ def get(self):
challenges whose bounding polygons contain this point)
example: /api/challenges?lon=-100.22&lat=40.45&difficulty=2
"""
+
# initialize the parser
parser = reqparse.RequestParser()
parser.add_argument('difficulty', type=int, choices=["1", "2", "3"],
- help='difficulty cannot be parsed')
+ help='difficulty is not 1, 2, 3')
parser.add_argument('lon', type=float,
- help="lon cannot be parsed")
+ help="lon is not a float")
parser.add_argument('lat', type=float,
- help="lat cannot be parsed")
+ help="lat is not a float")
+ parser.add_argument('radius', type=int,
+ help="radius is not an int")
+ parser.add_argument('include_inactive', type=bool, default=False,
+ help="include_inactive it not bool")
args = parser.parse_args()
difficulty = None
@@ -196,15 +169,11 @@ def get(self):
# Try to get difficulty from argument, or users preference
difficulty = args['difficulty']
- # for local challenges, first look at lon / lat passed in
- if args.lon is not None and args.lat is not None:
- contains = 'POINT(%s %s)' % (args.lon, args.lat)
- # if there is none, look at the user's home location from OSM
- # elif 'home_location' in session:
- # contains = 'POINT(%s %s)' % tuple(session.get('home_location'))
-
# get the list of challenges meeting the criteria
- query = db.session.query(Challenge).filter_by(active=True)
+ query = db.session.query(Challenge)
+
+ if not args.include_inactive:
+ query = query.filter_by(active=True)
if difficulty is not None:
query = query.filter_by(difficulty=difficulty)
@@ -234,6 +203,18 @@ def get(self):
"""Return information about the logged in user"""
return marshal(session, me_fields)
+ def put(self):
+ """User setting information about themselves"""
+ try:
+ payload = json.loads(request.data)
+ except Exception:
+ abort(400)
+ [session.pop(k, None) for k, v in payload.iteritems() if v is None]
+ for k, v in payload.iteritems():
+ if v is not None:
+ session[k] = v
+ return {}
+
class ApiChallengePolygon(ProtectedResource):
@@ -252,9 +233,20 @@ class ApiStatsChallenge(ProtectedResource):
def get(self, slug):
"""Return statistics for the challenge identified by 'slug'"""
+ # get the challenge
challenge = get_challenge_or_404(slug, True)
- total = len(challenge.tasks)
- unfixed = challenge.approx_tasks_available
+
+ # query the number of tasks
+ query = db.session.query(Task).filter_by(challenge_slug=challenge.slug)
+ # refine with the user defined editing area
+ query = refine_with_user_area(query)
+ # emit count
+ total = query.count()
+
+ # get the approximate number of available tasks
+ unfixed = query.filter(Task.status.in_(
+ ['available', 'created', 'skipped'])).count()
+
return {'total': total, 'unfixed': unfixed}
@@ -369,23 +361,22 @@ def get(self, slug):
# If no tasks are found with this method, then this challenge
# is complete
if task is None:
- # Send a mail to the challenge admin
- requests.post(
- "https://api.mailgun.net/v2/maproulette.org/messages",
- auth=("api", app.config["MAILGUN_API_KEY"]),
- data={"from": "MapRoulette ",
- "to": ["maproulette@maproulette.org"],
- "subject":
- "Challenge {} is complete".format(challenge.slug),
- "text":
- "{challenge} has no remaining tasks"
- " on server {server}".format(
- challenge=challenge.title,
- server=url_for('index', _external=True))})
- # Deactivate the challenge
- challenge.active = False
- db.session.add(challenge)
- db.session.commit()
+ if not user_area_is_defined():
+ # send email and deactivate challenge only when
+ # there are no more tasks for the entire challenge,
+ # not if the user has defined an area to work on.
+ subject = "Challenge {} is complete".format(challenge.slug)
+ body = "{challenge} has no remaining tasks"
+ " on server {server}".format(
+ challenge=challenge.title,
+ server=url_for('index', _external=True))
+ send_email("maproulette@maproulette.org", subject, body)
+
+ # Deactivate the challenge
+ challenge.active = False
+ db.session.add(challenge)
+ db.session.commit()
+
# Is this the right error?
return osmerror("ChallengeComplete",
"Challenge {} is complete".format(challenge.title))
@@ -448,7 +439,13 @@ def get(self, slug, identifier):
"""Returns the geometries for the task identified by
'identifier' from the challenge identified by 'slug'"""
task = get_task_or_404(slug, identifier)
- return task.geometries
+ geometries = [geojson.Feature(
+ geometry=g.geometry,
+ properties={
+ 'selected': True,
+ 'osmid': g.osmid}) for g in task.geometries]
+ return geojson.FeatureCollection(geometries)
+
# Add all resources to the RESTful API
api.add_resource(ApiPing,
@@ -469,7 +466,7 @@ def get(self, slug, identifier):
api.add_resource(ApiStatsUser,
'/api/stats/users')
# stats about the signed in user
-api.add_resource(ApiMe,
+api.add_resource(ApiStatsMe,
'/api/stats/me')
# task endpoints
api.add_resource(ApiChallengeTask,
@@ -520,12 +517,14 @@ def put(self, slug):
payload.get('difficulty'))
db.session.add(c)
db.session.commit()
+ return {}
def delete(self, slug):
"""delete a challenge"""
challenge = get_challenge_or_404(slug)
db.session.delete(challenge)
db.session.commit()
+ return {}, 204
class AdminApiTaskStatuses(Resource):
@@ -549,7 +548,7 @@ def put(self, slug, identifier):
# Parse the posted data
parse_task_json(json.loads(request.data), slug, identifier)
- return {}
+ return {}, 201
def delete(self, slug, identifier):
"""Delete a task"""
@@ -558,6 +557,7 @@ def delete(self, slug, identifier):
task.append_action(Action('deleted'))
db.session.add(task)
db.session.commit()
+ return {}, 204
class AdminApiUpdateTasks(Resource):
diff --git a/maproulette/helpers.py b/maproulette/helpers.py
index 86de321..72b6759 100644
--- a/maproulette/helpers.py
+++ b/maproulette/helpers.py
@@ -5,8 +5,13 @@
from functools import wraps
import json
from maproulette import app
-from shapely.geometry import MultiPoint, asShape
+from shapely.geometry import MultiPoint, asShape, Point
from random import random
+from sqlalchemy.sql.expression import cast
+from geoalchemy2.functions import ST_DWithin
+from geoalchemy2.shape import from_shape
+from geoalchemy2.types import Geography
+import requests
def signed_in():
@@ -122,6 +127,7 @@ def get_random_task(challenge):
'skipped',
'created']),
Task.random >= rn).order_by(Task.random)
+ q = refine_with_user_area(q)
if q.first() is None:
# we may not have gotten one if there is no task with
# Task.random <= the random value. chance of this gets
@@ -133,7 +139,7 @@ def get_random_task(challenge):
'skipped',
'created']),
Task.random < rn).order_by(Task.random)
-
+ q = refine_with_user_area(q)
return q.first() or None
@@ -183,6 +189,32 @@ def get_envelope(geoms):
return MultiPoint(geoms).envelope
+def user_area_is_defined():
+ return 'lon' and 'lat' and 'radius' in session
+
+
+def refine_with_user_area(query):
+ """Takes a query and refines it with a spatial constraint
+ based on user setting"""
+ if 'lon' and 'lat' and 'radius' in session:
+ return query.filter(ST_DWithin(
+ cast(Task.location, Geography),
+ cast(from_shape(Point(session["lon"], session["lat"])), Geography),
+ session["radius"]))
+ else:
+ return query
+
+
+def send_email(to, subject, text):
+ requests.post(
+ "https://api.mailgun.net/v2/maproulette.org/messages",
+ auth=("api", app.config["MAILGUN_API_KEY"]),
+ data={"from": "MapRoulette ",
+ "to": list(to),
+ "subject": subject,
+ "text": text})
+
+
class GeoPoint(object):
"""A geo-point class for use as a validation in the req parser"""
diff --git a/maproulette/models.py b/maproulette/models.py
index 1d08302..01c56c4 100644
--- a/maproulette/models.py
+++ b/maproulette/models.py
@@ -1,6 +1,6 @@
# """This file contains the SQLAlchemy ORM models"""
-from sqlalchemy import create_engine, and_, or_
+from sqlalchemy import create_engine
from sqlalchemy.orm import synonym
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
from sqlalchemy.ext.declarative import declarative_base
@@ -10,8 +10,7 @@
import random
from datetime import datetime
from maproulette import app
-from flask import session
-from shapely.geometry import Polygon
+from shapely.geometry import Polygon, Point, MultiPoint
import pytz
# set up the ORM engine and database object
@@ -183,17 +182,6 @@ def polygon(self, shape):
polygon = synonym('geom', descriptor=polygon)
- @property
- def approx_tasks_available(self):
- """Return the approximate number of tasks
- available for this challenge."""
-
- return len(
- [t for t in self.tasks if t.status in [
- 'created',
- 'skipped',
- 'available']])
-
@hybrid_property
def islocal(self):
"""Returns the localness of a challenge (is it small)"""
@@ -229,6 +217,8 @@ class Task(db.Model):
nullable=False)
manifest = db.Column(
db.String) # not used for now
+ location = db.Column(
+ Geometry)
geometries = db.relationship(
"TaskGeometry",
cascade='all,delete-orphan',
@@ -272,81 +262,6 @@ def has_status(cls, statuses):
statuses = [statuses]
return cls.status.in_(statuses)
- @hybrid_property
- def is_available(self):
- return self.has_status([
- 'available',
- 'created',
- 'skipped']) or (self.has_status([
- 'assigned',
- 'editing']) and datetime.utcnow() -
- app.config['TASK_EXPIRATION_THRESHOLD'] >
- self.actions[-1].timestamp)
-
- # with statuses as (select distinct on (task_id) timestamp,
- # status, task_id from actions order by task_id, id desc) select id,
- # challenge_slug from tasks join statuses c on (id = task_id)
- # where c.status in ('available','skipped','created') or (c.status in
- # ('editing','assigned') and now() - c.timestamp > '1 hour');
-
- @is_available.expression
- def is_available(cls):
- # the common table expression
- current_actions = db.session.query(Action).distinct(
- Action.task_id).order_by(Action.task_id).order_by(
- Action.id.desc()).cte(
- name="current_actions")
- # before this time, a challenge is available even if it's
- # 'assigned' or 'editing'
- available_time = datetime.utcnow() -\
- app.config['TASK_EXPIRATION_THRESHOLD']
- res = cls.id.in_(
- db.session.query(Task.id).join(current_actions).filter(
- or_(
- current_actions.c.status.in_([
- 'available',
- 'skipped',
- 'created']),
- and_(
- current_actions.c.status.in_([
- 'editing',
- 'assigned']),
- available_time >
- current_actions.c.timestamp))
- ))
- return res
-
- @property
- def location(self):
- """Returns the location for this task as a Shapely geometry.
- This is meant to give the client a quick hint about where the
- task is located without having to transfer and decode the entire
- task geometry. In reality what we do is transmit the first
- geometry we find for the task. This is then parsed into a single
- representative lon/lat in the API by getting the first coordinate
- of the geometry retrieved here. See also the PointField class in
- the API code."""
-
- if not hasattr(self, 'geometries') or len(self.geometries) == 0:
- return 'POINT(0 0)'
- else:
- g = self.geometries[0].geom
- return to_shape(g)
-
- @location.setter
- def location(self, shape):
- """Set the location for this task from a Shapely object"""
-
- self.geom = from_shape(shape)
-
- def append_action(self, action):
- self.actions.append(action)
- # duplicate the action status string in the tasks table to save lookups
- self.status = action.status
- db.session.commit()
- # if action.status == 'fixed':
- # self.validate_fixed()
-
def update(self, new_values, geometries, commit=True):
"""This updates a task based on a dict with new values"""
for k, v in new_values.iteritems():
@@ -362,64 +277,40 @@ def update(self, new_values, geometries, commit=True):
for geometry in geometries:
self.geometries = geometries
+
+ # set the location for this task, as a representative point of the
+ # combined geometries.
+ self.set_location()
+
db.session.merge(self)
if commit:
db.session.commit()
return True
- def validate_fixed(self):
- from maproulette.oauth import get_latest_changeset
- from maproulette.helpers import get_envelope
- import iso8601
-
- intersecting = False
- timeframe = False
-
- # get the latest changeset
- latest_changeset = get_latest_changeset(session.get('osm_id'))
-
- # check if the changeset bounding box covers the task geometries
- sw = (float(latest_changeset.get('min_lon')),
- float(latest_changeset.get('min_lat')))
- ne = (float(latest_changeset.get('max_lon')),
- float(latest_changeset.get('max_lat')))
- envelope = get_envelope([ne, sw])
- app.logger.debug(envelope)
- for geom in [to_shape(taskgeom.geom)
- for taskgeom in self.geometries]:
- if geom.intersects(envelope):
- intersecting = True
- break
-
- app.logger.debug('intersecting: %s ' % (intersecting,))
-
- # check if the timestamp is between assigned and fixed
- assigned_action = Action.query.filter_by(
- task_id=self.id).filter_by(
- user_id=session.get('osm_id')).filter_by(
- status='assigned').first()
-
- # get assigned time in UTC
- assigned_timestamp = assigned_action.timestamp
- assigned_timestamp = assigned_timestamp.replace(tzinfo=pytz.utc)
-
- # get the timestamp when the changeset was closed in UTC
- changeset_closed_timestamp = iso8601.parse_date(
- latest_changeset.get('closed_at')).replace(tzinfo=pytz.utc)
-
- app.logger.debug(assigned_timestamp)
- app.logger.debug(changeset_closed_timestamp)
- app.logger.debug(datetime.now(pytz.utc))
-
- timeframe = assigned_timestamp <\
- changeset_closed_timestamp <\
- datetime.now(pytz.utc) + app.config['MAX_CHANGESET_OFFSET']
-
- if intersecting and timeframe:
- app.logger.debug('validated')
- self.append_action(Action('validated', session.get('osm_id')))
- else:
- app.logger.debug('could not validate')
+ @property
+ def is_within(self, lon, lat, radius):
+ return self.location.intersects(Point(lon, lat).buffer(radius))
+
+ def append_action(self, action):
+ self.actions.append(action)
+ # duplicate the action status string in the tasks table to save lookups
+ self.status = action.status
+ db.session.commit()
+
+ def set_location(self):
+ """Set the location of a task as a cheaply calculated
+ representative point of the combined geometries."""
+ # set the location field, which is a representative point
+ # for the task's geometries
+ # first, get all individual coordinates for the geometries
+ coordinates = []
+ for geometry in self.geometries:
+ coordinates.extend(list(to_shape(geometry.geom).coords))
+ # then, set the location to a representative point
+ # (cheaper than centroid)
+ if len(coordinates) > 0:
+ self.location = from_shape(MultiPoint(
+ coordinates).representative_point(), srid=4326)
class TaskGeometry(db.Model):
diff --git a/maproulette/static/css/style.css b/maproulette/static/css/style.css
index f5a95ed..b6db916 100755
--- a/maproulette/static/css/style.css
+++ b/maproulette/static/css/style.css
@@ -152,7 +152,7 @@ div#challenge_blurb {
}
/* the little stats section */
-div#stats {
+div.smallmsg {
font-size: x-small;
color: rgb(100,100,100);
}
@@ -208,3 +208,23 @@ td.hidden {
table.stats {
padding: 4px;
}
+
+/* react transitions */
+
+.dialog-enter {
+ opacity: 0.01;
+ transition: opacity .5s ease-in;
+}
+
+.dialog-enter.dialog-enter-active {
+ opacity: 1;
+}
+
+.dialog-leave {
+ opacity: 1;
+ transition: opacity .5s ease-in;
+}
+
+.dialog-leave.example-leave-active {
+ opacity: 0.01;
+}
diff --git a/maproulette/static/index.html b/maproulette/static/index.html
new file mode 100644
index 0000000..f762525
--- /dev/null
+++ b/maproulette/static/index.html
@@ -0,0 +1 @@
+MapRoulette Maintenance>
MapRoulette is undergoing a bit of maintenance. Please check back soon!
\ No newline at end of file
diff --git a/maproulette/templates/index.html b/maproulette/templates/index.html
index 6f743d9..2a41b80 100644
--- a/maproulette/templates/index.html
+++ b/maproulette/templates/index.html
@@ -21,7 +21,10 @@
Challenge statistics
-
This challenge has N/A tasks, and we still need to fix N/A of them.
+
This challenge has N/A tasks, and we still need to fix N/A of them.
+
+ You are working within the area you selected. The numbers above are for that area, not the entire challenge.
+
THIS IS NOT AN ERROR q
I'M NOT SURE / SKIP w
diff --git a/migrations/versions/3115f24a7604_.py b/migrations/versions/3115f24a7604_.py
new file mode 100644
index 0000000..39169df
--- /dev/null
+++ b/migrations/versions/3115f24a7604_.py
@@ -0,0 +1,26 @@
+"""adding location column to tasks table
+
+Revision ID: 3115f24a7604
+Revises: 38fe795129a0
+Create Date: 2014-05-04 14:02:19.755081
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '3115f24a7604'
+down_revision = '38fe795129a0'
+
+from alembic import op
+
+
+def upgrade():
+ # there is no way to tell alembic to add a geometry column the right way?
+ op.execute(
+ "SELECT AddGeometryColumn('tasks', 'location', 4326, 'POINT', 2)")
+ # there is no way to tell alembic that we want a GiST type index?
+ op.execute("CREATE INDEX idx_tasks_location ON tasks USING GIST(location)")
+
+
+def downgrade():
+ op.drop_index('idx_tasks_location', 'tasks')
+ op.drop_column('tasks', 'location')
diff --git a/requirements.txt b/requirements.txt
index 1b833e3..cd5412b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
Flask>=0.10.1
-Flask-RESTful>=0.2.0
+# Flask-RESTful>=0.2.0
+-e git://github.com/twilio/flask-restful.git#egg=flask-restful
Flask-KVSession>=0.4
Flask-OAuthlib>=0.4.2
Flask-SQLAlchemy>=1.0