From: Leo Alekseyev Date: Wed, 26 Mar 2014 09:42:13 +0000 (-0700) Subject: Initial commit X-Git-Url: http://www.dnquark.com/git/?a=commitdiff_plain;h=refs%2Fheads%2Fmaster;p=quadtree.git%2F.git Initial commit --- f213a6b56e6230646563255d589cf85fdfb79bb4 diff --git a/geostore.py b/geostore.py new file mode 100644 index 0000000..eebe731 --- /dev/null +++ b/geostore.py @@ -0,0 +1,94 @@ +from treemap2d import Cell +from treemap2d import Entity +import csv +from datetime import datetime +import json + +X0, X1 = 0, 256 +TESTDATA = "./testdata.csv" + + + +class Geostore(object): + def __init__(self): + self.root = Cell((X0, X0), (X1, X1)) + # self.load_test_data() + self.cache = ResultsCache() + def load_test_data(self): + with open(TESTDATA, "rb") as csvfile: + reader = csv.reader(csvfile) + for row in reader: + self.root.add(Entity(*[float(x.strip()) for x in row])) + def add(self, x, y): + self.root.add(Entity(x, y)) + def reset(self, ul=None, lr=None): + ul = ul or (X0, X0) + lr = lr or (X1, X1) + self.root = Cell(ul, lr) + def all_cells(self): + cells = self.root.all_cells() + res = [] + for c in cells: + print c + cellrec = {} + for attr in ["x0", "x1", "y0", "y1", "xp", "yp", "level", "num_entities", "size"]: + cellrec[attr] = c.__getattribute__(attr) + res.append(cellrec) + return json.dumps(res) + def get_results(self, x, y, R, count=-1, offset=0): + output = {"request_x": x, "request_y": y, "request_R": R, "count": count, "offset": offset} + if (x, y, R) in self.cache: + res = self.cache[(x, y, R)] + output["cache_hit"] = True + else: + output["cache_hit"] = False + res = self.root.find(x, y, R, metadata=True) + self.cache[(x, y, R)] = res + for key in ["num_entities_estimate", "num_cells", "num_entities", "entities"]: + output[key] = res[key] + if count == -1: + output["entities"] = res["entities"] + else: + output["entities"] = res["entities"][offset:(offset+count)] + return json.dumps(output) + def _entity_gen(self, entities): + for e in entities: + yield e + def get_entity_iter(self): + return self._entity_gen(self.root.all_entities()) + def get_entity_iter1(self): + entities = self.root.all_entities() + def gen(): + for e in entities: yield e + return gen + +class ResultsCache(object): + """Simple LRU cache for storing results sets + + Instantiated with kwargs: + max_size - max number of items before expulsion + expunge_size - number of least-recently accessed items + to purge when exceeding max_size. + + Class wraps a dictionary implementing a minimal interface.""" + def __init__(self, max_size=20, expunge_size=10): + self.d = {} + self.CACHE_MAX_SIZE = max_size + self.CACHE_EXPUNGE_SIZE = expunge_size + if expunge_size >= max_size or max_size <= 2: + raise ValueError("Bad cache size parameters") + def __setitem__(self, key, value): + self.d[key] = [value, datetime.now()] + if len(self.d) > self.CACHE_MAX_SIZE: + for ts, key in sorted([(v[1], k) for k,v in self.d.items()])[:self.CACHE_EXPUNGE_SIZE]: + del self.d[key] + def __getitem__(self, key): + self.d[key][1] = datetime.now() + return self.d[key][0] + def __contains__(self, key): + return key in self.d + def __len__(self): + return len(self.d) + def __delitem__(self, key): + del self.d[key] + diff --git a/hello.py b/hello.py new file mode 100644 index 0000000..1f7bab4 --- /dev/null +++ b/hello.py @@ -0,0 +1,120 @@ +from flask import Flask +from flask import request +from flask import jsonify +from flask import render_template +from flask import make_response +from geostore import Geostore +app = Flask(__name__) + +store = Geostore() + +@app.route("/") +def root(): + return render_template('index.html') + +@app.route("/clickdrag2") +def clickdrag2(): + return render_template('clickdrag2.html') + +@app.route("/clickdrag3") +def clickdrag3(): + return render_template('clickdrag3.html') + +@app.route('/add', methods=['POST']) +def add(): + if not ('x' in request.form and 'y' in request.form): + raise InvalidPoint("No x ad y parameter specified in request") + x = float(request.form.get('x')) + y = float(request.form.get('y')) + store.add(x, y) + resp = make_response('{"add_point": "ok"}') + resp.headers['Content-Type'] = "application/json" + return resp + +@app.route('/reset', methods=['POST']) +def reset(): + if all(x in request.form for x in ['x0', 'y0', 'x1', 'y1']): + ul = ( float(request.form.get('x0')), float(request.form.get('y0')) ) + lr = ( float(request.form.get('x1')), float(request.form.get('y1')) ) + store.reset(ul, lr) + else: + store.reset() + resp = make_response('{"reset_store": "ok"}') + resp.headers['Content-Type'] = "application/json" + return resp + +@app.route("/all_cells") +def all_cells(): + return store.all_cells() + +@app.route("/search") +def search(): + x, y, R = parse_args() + count = request.args.get('count', -1, type=int) + offset = request.args.get('offset', 0, type=int) + return store.get_results(x, y, R, count, offset) + + +def parse_args(): + if "long" in request.args: + x = request.args.get('long', 0, type=float) + elif "x" in request.args: + x = request.args.get('x', 0, type=float) + else: + raise InvalidSearch("long or x parameter missing") + if "lat" in request.args: + y = request.args.get('lat', 0, type=float) + elif "y" in request.args: + y = request.args.get('y', 0, type=float) + else: + raise InvalidSearch("lat or y parameter missing") + if "R" in request.args: + R = request.args.get('R', 0, type=float) + elif "r" in request.args: + R = request.args.get('r', 0, type=float) + else: + raise InvalidSearch("R parameter missing") + return (x, y, R) + + +class InvalidSearch(Exception): + status_code = 400 + def __init__(self, message, status_code=None, payload=None): + Exception.__init__(self) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + def to_dict(self): + rv = dict(self.payload or ()) + rv['message'] = self.message + return rv + +class InvalidPoint(Exception): + status_code = 400 + def __init__(self, message, status_code=None, payload=None): + Exception.__init__(self) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + def to_dict(self): + rv = dict(self.payload or ()) + rv['message'] = self.message + return rv + +@app.errorhandler(InvalidSearch) +def handle_invalid_search(error): + response = jsonify(error.to_dict()) + response.status_code = error.status_code + return response + +@app.errorhandler(InvalidPoint) +def handle_invalid_point(error): + response = jsonify(error.to_dict()) + response.status_code = error.status_code + return response + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/static/application.css b/static/application.css new file mode 100644 index 0000000..e296755 --- /dev/null +++ b/static/application.css @@ -0,0 +1,37 @@ +svg { + vertical-align: middle; + background: #F0F0E0; + box-shadow: inset 0 0 3px 0px #CECECE; +} + +svg circle { + stroke-width: 2px; + stroke: #79A32B; + fill: transparent; + cursor: pointer; +} + +svg circle:active { + stroke: #45D3C7; +} + +svg circle.selected { + stroke: #3333FF; +} + + +.selection { + stroke: gray; + stroke-width: 1px; + stroke-dasharray: 4px; + stroke-opacity: 0.5; + fill: transparent; +} + +.cell { + stroke: gray; + stroke-width: 1px; + stroke-dasharray: 4px; + stroke-opacity: 0.5; + fill: transparent; +} \ No newline at end of file diff --git a/static/application.js b/static/application.js new file mode 100644 index 0000000..ace841b --- /dev/null +++ b/static/application.js @@ -0,0 +1,130 @@ +var points = []; +var width = 300, height = 300; +var svg; +var origin; +var dragging = false; +var selectionShown = false; + + +function distance(p, q) { + return Math.sqrt((p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1])); +} + +function renderMatchedEntities(data) { + console.log(data.entities); + svg.selectAll("circle.selected").data(data.entities).enter() + .append("svg:circle") + .attr("class", "selected") + .attr("stroke", "#3333FF") + .attr("r", 5) + .attr("cx", function(d) { console.log(d[1][0]); return d[1][0]; }) + .attr("cy", function(d) { console.log(d[1][1]); return d[1][1]; }); +} + + +function getSearchData(x, y, r) { + $.getJSON($SCRIPT_ROOT + '/search', {x: x, y: y, R: r}) + .done(renderMatchedEntities); +} + +function renderCells(data) { + svg.selectAll("rect.cell").remove(); + svg.selectAll("rect.cell").data(data).enter() + .append("svg:rect") + .attr("class", "cell") + .attr("x", function(d) { return d.x0; }) + .attr("y", function(d) { return d.y0; }) + .attr("width", function(d) { return d.size; }) + .attr("height", function(d) { return d.size; }); +} + + +function getAllCells() { + $.getJSON($SCRIPT_ROOT + '/all_cells') + .done(renderCells); +} + + +function clickHandler() { + if (d3.event.defaultPrevented) { + console.log("click suppressed"); + return; // click suppressed + } + if (dragging) { + console.log("click prevented by my flag"); + dragging = false; + return; + } + svg.selectAll("circle.selected").remove(); + if (selectionShown) { + selectionShown = false; + return; + } + var p = d3.mouse(this); + points.push(p); + $.post($SCRIPT_ROOT + '/add', {x: p[0], y: p[1]}); + refreshSVG(svg); + getAllCells(); + + console.log(d3.mouse(this)); +} + +var drag = d3.behavior.drag() + .on("dragstart", function() { + svg.selectAll("circle.selected").remove(); + d3.event.sourceEvent.stopPropagation(); // silence other listeners + var p = d3.mouse(this); + origin = p; + svg.selectAll('.selection').remove(); + svg.append("circle") + .attr({"class": "selection", + "cx": p[0], + "cy": p[1]}); + console.log("dragStart"); + }) + .on("drag", function () { + var sel = svg.selectAll('.selection'); + var p = d3.mouse(this); + var d = distance(p, origin); + dragging = true; + sel.attr("r", d); + // console.log("drag " + origin + " to " + p + ": dist " + d); + }) + .on("dragend", function () { + setTimeout(function() { dragging = false; }, 100); + var p = d3.mouse(this); + var d = distance(p, origin); + if (d > 2) { + getSearchData(origin[0], origin[1], d); + selectionShown = true; + } + console.log("dragEnd; d=" + d); }); + +function placeSVG() { + svg = d3.select("#svg-container").append("svg"); + // .append("rect") + svg.attr("class", "rect-main") + .attr("width", width) + .attr("height", height) + .call(drag) + .on("click", clickHandler) + ; +} + + +function refreshSVG() { + console.log(points); + svg.selectAll("circle.point").data(points).enter() + .append("svg:circle") + .attr("class", "point") + .attr("r", 4) + .attr("cx", function(d) { return d[0]; }) + .attr("cy", function(d) { return d[1]; }); +} + +$(document).ready(function() { + + placeSVG(); + $.post($SCRIPT_ROOT + '/reset', {x0: 0, x1: width, y0: 0, y1: height}); + +}); diff --git a/static/backbone-min.js b/static/backbone-min.js new file mode 100644 index 0000000..3b2593d --- /dev/null +++ b/static/backbone-min.js @@ -0,0 +1,2 @@ +(function(){var t=this;var e=t.Backbone;var i=[];var r=i.push;var s=i.slice;var n=i.splice;var a;if(typeof exports!=="undefined"){a=exports}else{a=t.Backbone={}}a.VERSION="1.1.0";var h=t._;if(!h&&typeof require!=="undefined")h=require("underscore");a.$=t.jQuery||t.Zepto||t.ender||t.$;a.noConflict=function(){t.Backbone=e;return this};a.emulateHTTP=false;a.emulateJSON=false;var o=a.Events={on:function(t,e,i){if(!l(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,i){if(!l(this,"once",t,[e,i])||!e)return this;var r=this;var s=h.once(function(){r.off(t,s);e.apply(this,arguments)});s._callback=e;return this.on(t,s,i)},off:function(t,e,i){var r,s,n,a,o,u,c,f;if(!this._events||!l(this,"off",t,[e,i]))return this;if(!t&&!e&&!i){this._events={};return this}a=t?[t]:h.keys(this._events);for(o=0,u=a.length;o").attr(t);this.setElement(e,false)}else{this.setElement(h.result(this,"el"),false)}}});a.sync=function(t,e,i){var r=T[t];h.defaults(i||(i={}),{emulateHTTP:a.emulateHTTP,emulateJSON:a.emulateJSON});var s={type:r,dataType:"json"};if(!i.url){s.url=h.result(e,"url")||U()}if(i.data==null&&e&&(t==="create"||t==="update"||t==="patch")){s.contentType="application/json";s.data=JSON.stringify(i.attrs||e.toJSON(i))}if(i.emulateJSON){s.contentType="application/x-www-form-urlencoded";s.data=s.data?{model:s.data}:{}}if(i.emulateHTTP&&(r==="PUT"||r==="DELETE"||r==="PATCH")){s.type="POST";if(i.emulateJSON)s.data._method=r;var n=i.beforeSend;i.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",r);if(n)return n.apply(this,arguments)}}if(s.type!=="GET"&&!i.emulateJSON){s.processData=false}if(s.type==="PATCH"&&E){s.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var o=i.xhr=a.ajax(h.extend(s,i));e.trigger("request",e,o,i);return o};var E=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};a.ajax=function(){return a.$.ajax.apply(a.$,arguments)};var k=a.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\((.*?)\)/g;var $=/(\(\?)?:\w+/g;var H=/\*\w+/g;var A=/[\-{}\[\]+?.,\\\^$|#\s]/g;h.extend(k.prototype,o,{initialize:function(){},route:function(t,e,i){if(!h.isRegExp(t))t=this._routeToRegExp(t);if(h.isFunction(e)){i=e;e=""}if(!i)i=this[e];var r=this;a.history.route(t,function(s){var n=r._extractParameters(t,s);i&&i.apply(r,n);r.trigger.apply(r,["route:"+e].concat(n));r.trigger("route",e,n);a.history.trigger("route",r,e,n)});return this},navigate:function(t,e){a.history.navigate(t,e);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=h.result(this,"routes");var t,e=h.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(A,"\\$&").replace(S,"(?:$1)?").replace($,function(t,e){return e?t:"([^/]+)"}).replace(H,"(.*?)");return new RegExp("^"+t+"$")},_extractParameters:function(t,e){var i=t.exec(e).slice(1);return h.map(i,function(t){return t?decodeURIComponent(t):null})}});var I=a.History=function(){this.handlers=[];h.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var N=/^[#\/]|\s+$/g;var O=/^\/+|\/+$/g;var P=/msie [\w.]+/;var C=/\/$/;var j=/[?#].*$/;I.started=false;h.extend(I.prototype,o,{interval:50,getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=this.location.pathname;var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(N,"")},start:function(t){if(I.started)throw new Error("Backbone.history has already been started");I.started=true;this.options=h.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var e=this.getFragment();var i=document.documentMode;var r=P.exec(navigator.userAgent.toLowerCase())&&(!i||i<=7);this.root=("/"+this.root+"/").replace(O,"/");if(r&&this._wantsHashChange){this.iframe=a.$('