├── .couchappignore ├── .couchapprc ├── .gitignore ├── CNAME ├── README.md ├── external ├── icanhaz │ └── ICanHaz.js ├── leaflet-markercluster │ ├── MarkerCluster.Default.css │ ├── MarkerCluster.css │ ├── leaflet.markercluster-src.js │ └── leaflet.markercluster.js └── leaflet │ └── Bing.js ├── images ├── favicon.ico ├── node_circle.svg └── wifi_icon60.png ├── index.html ├── map.html ├── rewrites.json ├── script ├── owm_app.js └── owm_widget.js └── style ├── owm_app.css └── owm_widget.css /.couchappignore: -------------------------------------------------------------------------------- 1 | [ 2 | // filenames matching these regexps will not be pushed to the database 3 | // uncomment to activate; separate entries with "," 4 | ".*~$", 5 | ".*\\.swp$", 6 | ".*\\.bak$", 7 | ".git" 8 | ] 9 | -------------------------------------------------------------------------------- /.couchapprc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | openwifimap.net 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openwifimap-html5 2 | 3 | OpenWiFiMap is a database and map for free network WiFi routers (freifunk and others, too!). 4 | 5 | This is a HTML5 map app with data from a [OpenWiFiMap database](http://github.com/freifunk/openwifimap-api/). 6 | 7 | ## HTML5 app installations 8 | 9 | Visit the main installation at [openwifimap.net](http://openwifimap.net/) or the stripped down version at [openwifimap.net/map.html](http://openwifimap.net/map.html) (e.g. for inclusion in other sites via iframe). 10 | 11 | Of course, you can easily set up your own map! Here is a (possibly incomplete) list of installations: 12 | * [openwifimap.net](http://openwifimap.net/) 13 | * [map.pberg.freifunk.net](http://map.pberg.freifunk.net/) 14 | * [map.weimarnetz.de](http://map.weimarnetz.de/) 15 | 16 | If your installation is missing, feel free to add it or tell us by opening an issue. 17 | 18 | ## Submitting and querying data 19 | 20 | If you want to submit nodes to the database or if you want to query the database, please take a look at the [OpenWiFiMap API documentation](https://github.com/freifunk/openwifimap-api/blob/master/API.md). 21 | 22 | ## License 23 | 24 | openwifimap-html5 is licensed under the [MIT license](http://opensource.org/licenses/MIT). 25 | -------------------------------------------------------------------------------- /external/icanhaz/ICanHaz.js: -------------------------------------------------------------------------------- 1 | /*! 2 | ICanHaz.js version 0.10 -- by @HenrikJoreteg 3 | More info at: http://icanhazjs.com 4 | */ 5 | (function () { 6 | /* 7 | mustache.js — Logic-less templates in JavaScript 8 | 9 | See http://mustache.github.com/ for more info. 10 | */ 11 | 12 | var Mustache = function () { 13 | var _toString = Object.prototype.toString; 14 | 15 | Array.isArray = Array.isArray || function (obj) { 16 | return _toString.call(obj) == "[object Array]"; 17 | } 18 | 19 | var _trim = String.prototype.trim, trim; 20 | 21 | if (_trim) { 22 | trim = function (text) { 23 | return text == null ? "" : _trim.call(text); 24 | } 25 | } else { 26 | var trimLeft, trimRight; 27 | 28 | // IE doesn't match non-breaking spaces with \s. 29 | if ((/\S/).test("\xA0")) { 30 | trimLeft = /^[\s\xA0]+/; 31 | trimRight = /[\s\xA0]+$/; 32 | } else { 33 | trimLeft = /^\s+/; 34 | trimRight = /\s+$/; 35 | } 36 | 37 | trim = function (text) { 38 | return text == null ? "" : 39 | text.toString().replace(trimLeft, "").replace(trimRight, ""); 40 | } 41 | } 42 | 43 | var escapeMap = { 44 | "&": "&", 45 | "<": "<", 46 | ">": ">", 47 | '"': '"', 48 | "'": ''' 49 | }; 50 | 51 | function escapeHTML(string) { 52 | return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) { 53 | return escapeMap[s] || s; 54 | }); 55 | } 56 | 57 | var regexCache = {}; 58 | var Renderer = function () {}; 59 | 60 | Renderer.prototype = { 61 | otag: "{{", 62 | ctag: "}}", 63 | pragmas: {}, 64 | buffer: [], 65 | pragmas_implemented: { 66 | "IMPLICIT-ITERATOR": true 67 | }, 68 | context: {}, 69 | 70 | render: function (template, context, partials, in_recursion) { 71 | // reset buffer & set context 72 | if (!in_recursion) { 73 | this.context = context; 74 | this.buffer = []; // TODO: make this non-lazy 75 | } 76 | 77 | // fail fast 78 | if (!this.includes("", template)) { 79 | if (in_recursion) { 80 | return template; 81 | } else { 82 | this.send(template); 83 | return; 84 | } 85 | } 86 | 87 | // get the pragmas together 88 | template = this.render_pragmas(template); 89 | 90 | // render the template 91 | var html = this.render_section(template, context, partials); 92 | 93 | // render_section did not find any sections, we still need to render the tags 94 | if (html === false) { 95 | html = this.render_tags(template, context, partials, in_recursion); 96 | } 97 | 98 | if (in_recursion) { 99 | return html; 100 | } else { 101 | this.sendLines(html); 102 | } 103 | }, 104 | 105 | /* 106 | Sends parsed lines 107 | */ 108 | send: function (line) { 109 | if (line !== "") { 110 | this.buffer.push(line); 111 | } 112 | }, 113 | 114 | sendLines: function (text) { 115 | if (text) { 116 | var lines = text.split("\n"); 117 | for (var i = 0; i < lines.length; i++) { 118 | this.send(lines[i]); 119 | } 120 | } 121 | }, 122 | 123 | /* 124 | Looks for %PRAGMAS 125 | */ 126 | render_pragmas: function (template) { 127 | // no pragmas 128 | if (!this.includes("%", template)) { 129 | return template; 130 | } 131 | 132 | var that = this; 133 | var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) { 134 | return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); 135 | }); 136 | 137 | return template.replace(regex, function (match, pragma, options) { 138 | if (!that.pragmas_implemented[pragma]) { 139 | throw({message: 140 | "This implementation of mustache doesn't understand the '" + 141 | pragma + "' pragma"}); 142 | } 143 | that.pragmas[pragma] = {}; 144 | if (options) { 145 | var opts = options.split("="); 146 | that.pragmas[pragma][opts[0]] = opts[1]; 147 | } 148 | return ""; 149 | // ignore unknown pragmas silently 150 | }); 151 | }, 152 | 153 | /* 154 | Tries to find a partial in the curent scope and render it 155 | */ 156 | render_partial: function (name, context, partials) { 157 | name = trim(name); 158 | if (!partials || partials[name] === undefined) { 159 | throw({message: "unknown_partial '" + name + "'"}); 160 | } 161 | if (!context || typeof context[name] != "object") { 162 | return this.render(partials[name], context, partials, true); 163 | } 164 | return this.render(partials[name], context[name], partials, true); 165 | }, 166 | 167 | /* 168 | Renders inverted (^) and normal (#) sections 169 | */ 170 | render_section: function (template, context, partials) { 171 | if (!this.includes("#", template) && !this.includes("^", template)) { 172 | // did not render anything, there were no sections 173 | return false; 174 | } 175 | 176 | var that = this; 177 | 178 | var regex = this.getCachedRegex("render_section", function (otag, ctag) { 179 | // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder 180 | return new RegExp( 181 | "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) 182 | 183 | otag + // {{ 184 | "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) 185 | ctag + // }} 186 | 187 | "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped 188 | 189 | otag + // {{ 190 | "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). 191 | ctag + // }} 192 | 193 | "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. 194 | 195 | "g"); 196 | }); 197 | 198 | 199 | // for each {{#foo}}{{/foo}} section do... 200 | return template.replace(regex, function (match, before, type, name, content, after) { 201 | // before contains only tags, no sections 202 | var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", 203 | 204 | // after may contain both sections and tags, so use full rendering function 205 | renderedAfter = after ? that.render(after, context, partials, true) : "", 206 | 207 | // will be computed below 208 | renderedContent, 209 | 210 | value = that.find(name, context); 211 | 212 | if (type === "^") { // inverted section 213 | if (!value || Array.isArray(value) && value.length === 0) { 214 | // false or empty list, render it 215 | renderedContent = that.render(content, context, partials, true); 216 | } else { 217 | renderedContent = ""; 218 | } 219 | } else if (type === "#") { // normal section 220 | if (Array.isArray(value)) { // Enumerable, Let's loop! 221 | renderedContent = that.map(value, function (row) { 222 | return that.render(content, that.create_context(row), partials, true); 223 | }).join(""); 224 | } else if (that.is_object(value)) { // Object, Use it as subcontext! 225 | renderedContent = that.render(content, that.create_context(value), 226 | partials, true); 227 | } else if (typeof value == "function") { 228 | // higher order section 229 | renderedContent = value.call(context, content, function (text) { 230 | return that.render(text, context, partials, true); 231 | }); 232 | } else if (value) { // boolean section 233 | renderedContent = that.render(content, context, partials, true); 234 | } else { 235 | renderedContent = ""; 236 | } 237 | } 238 | 239 | return renderedBefore + renderedContent + renderedAfter; 240 | }); 241 | }, 242 | 243 | /* 244 | Replace {{foo}} and friends with values from our view 245 | */ 246 | render_tags: function (template, context, partials, in_recursion) { 247 | // tit for tat 248 | var that = this; 249 | 250 | var new_regex = function () { 251 | return that.getCachedRegex("render_tags", function (otag, ctag) { 252 | return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g"); 253 | }); 254 | }; 255 | 256 | var regex = new_regex(); 257 | var tag_replace_callback = function (match, operator, name) { 258 | switch(operator) { 259 | case "!": // ignore comments 260 | return ""; 261 | case "=": // set new delimiters, rebuild the replace regexp 262 | that.set_delimiters(name); 263 | regex = new_regex(); 264 | return ""; 265 | case ">": // render partial 266 | return that.render_partial(name, context, partials); 267 | case "{": // the triple mustache is unescaped 268 | case "&": // & operator is an alternative unescape method 269 | return that.find(name, context); 270 | default: // escape the value 271 | return escapeHTML(that.find(name, context)); 272 | } 273 | }; 274 | var lines = template.split("\n"); 275 | for(var i = 0; i < lines.length; i++) { 276 | lines[i] = lines[i].replace(regex, tag_replace_callback, this); 277 | if (!in_recursion) { 278 | this.send(lines[i]); 279 | } 280 | } 281 | 282 | if (in_recursion) { 283 | return lines.join("\n"); 284 | } 285 | }, 286 | 287 | set_delimiters: function (delimiters) { 288 | var dels = delimiters.split(" "); 289 | this.otag = this.escape_regex(dels[0]); 290 | this.ctag = this.escape_regex(dels[1]); 291 | }, 292 | 293 | escape_regex: function (text) { 294 | // thank you Simon Willison 295 | if (!arguments.callee.sRE) { 296 | var specials = [ 297 | '/', '.', '*', '+', '?', '|', 298 | '(', ')', '[', ']', '{', '}', '\\' 299 | ]; 300 | arguments.callee.sRE = new RegExp( 301 | '(\\' + specials.join('|\\') + ')', 'g' 302 | ); 303 | } 304 | return text.replace(arguments.callee.sRE, '\\$1'); 305 | }, 306 | 307 | /* 308 | find `name` in current `context`. That is find me a value 309 | from the view object 310 | */ 311 | find: function (name, context) { 312 | name = trim(name); 313 | 314 | // Checks whether a value is thruthy or false or 0 315 | function is_kinda_truthy(bool) { 316 | return bool === false || bool === 0 || bool; 317 | } 318 | 319 | var value; 320 | 321 | // check for dot notation eg. foo.bar 322 | if (name.match(/([a-z_]+)\./ig)) { 323 | var childValue = this.walk_context(name, context); 324 | if (is_kinda_truthy(childValue)) { 325 | value = childValue; 326 | } 327 | } else { 328 | if (is_kinda_truthy(context[name])) { 329 | value = context[name]; 330 | } else if (is_kinda_truthy(this.context[name])) { 331 | value = this.context[name]; 332 | } 333 | } 334 | 335 | if (typeof value == "function") { 336 | return value.apply(context); 337 | } 338 | if (value !== undefined) { 339 | return value; 340 | } 341 | // silently ignore unkown variables 342 | return ""; 343 | }, 344 | 345 | walk_context: function (name, context) { 346 | var path = name.split('.'); 347 | // if the var doesn't exist in current context, check the top level context 348 | var value_context = (context[path[0]] != undefined) ? context : this.context; 349 | var value = value_context[path.shift()]; 350 | while (value != undefined && path.length > 0) { 351 | value_context = value; 352 | value = value[path.shift()]; 353 | } 354 | // if the value is a function, call it, binding the correct context 355 | if (typeof value == "function") { 356 | return value.apply(value_context); 357 | } 358 | return value; 359 | }, 360 | 361 | // Utility methods 362 | 363 | /* includes tag */ 364 | includes: function (needle, haystack) { 365 | return haystack.indexOf(this.otag + needle) != -1; 366 | }, 367 | 368 | // by @langalex, support for arrays of strings 369 | create_context: function (_context) { 370 | if (this.is_object(_context)) { 371 | return _context; 372 | } else { 373 | var iterator = "."; 374 | if (this.pragmas["IMPLICIT-ITERATOR"]) { 375 | iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; 376 | } 377 | var ctx = {}; 378 | ctx[iterator] = _context; 379 | return ctx; 380 | } 381 | }, 382 | 383 | is_object: function (a) { 384 | return a && typeof a == "object"; 385 | }, 386 | 387 | /* 388 | Why, why, why? Because IE. Cry, cry cry. 389 | */ 390 | map: function (array, fn) { 391 | if (typeof array.map == "function") { 392 | return array.map(fn); 393 | } else { 394 | var r = []; 395 | var l = array.length; 396 | for(var i = 0; i < l; i++) { 397 | r.push(fn(array[i])); 398 | } 399 | return r; 400 | } 401 | }, 402 | 403 | getCachedRegex: function (name, generator) { 404 | var byOtag = regexCache[this.otag]; 405 | if (!byOtag) { 406 | byOtag = regexCache[this.otag] = {}; 407 | } 408 | 409 | var byCtag = byOtag[this.ctag]; 410 | if (!byCtag) { 411 | byCtag = byOtag[this.ctag] = {}; 412 | } 413 | 414 | var regex = byCtag[name]; 415 | if (!regex) { 416 | regex = byCtag[name] = generator(this.otag, this.ctag); 417 | } 418 | 419 | return regex; 420 | } 421 | }; 422 | 423 | return({ 424 | name: "mustache.js", 425 | version: "0.4.0", 426 | 427 | /* 428 | Turns a template and view into HTML 429 | */ 430 | to_html: function (template, view, partials, send_fun) { 431 | var renderer = new Renderer(); 432 | if (send_fun) { 433 | renderer.send = send_fun; 434 | } 435 | renderer.render(template, view || {}, partials); 436 | if (!send_fun) { 437 | return renderer.buffer.join("\n"); 438 | } 439 | } 440 | }); 441 | }(); 442 | /*! 443 | ICanHaz.js -- by @HenrikJoreteg 444 | */ 445 | /*global */ 446 | (function () { 447 | function trim(stuff) { 448 | if (''.trim) return stuff.trim(); 449 | else return stuff.replace(/^\s+/, '').replace(/\s+$/, ''); 450 | } 451 | var ich = { 452 | VERSION: "0.10", 453 | templates: {}, 454 | 455 | // grab jquery or zepto if it's there 456 | $: (typeof window !== 'undefined') ? window.jQuery || window.Zepto || null : null, 457 | 458 | // public function for adding templates 459 | // can take a name and template string arguments 460 | // or can take an object with name/template pairs 461 | // We're enforcing uniqueness to avoid accidental template overwrites. 462 | // If you want a different template, it should have a different name. 463 | addTemplate: function (name, templateString) { 464 | if (typeof name === 'object') { 465 | for (var template in name) { 466 | this.addTemplate(template, name[template]); 467 | } 468 | return; 469 | } 470 | if (ich[name]) { 471 | console.error("Invalid name: " + name + "."); 472 | } else if (ich.templates[name]) { 473 | console.error("Template \"" + name + " \" exists"); 474 | } else { 475 | ich.templates[name] = templateString; 476 | ich[name] = function (data, raw) { 477 | data = data || {}; 478 | var result = Mustache.to_html(ich.templates[name], data, ich.templates); 479 | return (ich.$ && !raw) ? ich.$(result) : result; 480 | }; 481 | } 482 | }, 483 | 484 | // clears all retrieval functions and empties cache 485 | clearAll: function () { 486 | for (var key in ich.templates) { 487 | delete ich[key]; 488 | } 489 | ich.templates = {}; 490 | }, 491 | 492 | // clears/grabs 493 | refresh: function () { 494 | ich.clearAll(); 495 | ich.grabTemplates(); 496 | }, 497 | 498 | // grabs templates from the DOM and caches them. 499 | // Loop through and add templates. 500 | // Whitespace at beginning and end of all templates inside 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 59 | 66 | 96 | 109 | 289 | 290 | 291 | 292 | 293 |
294 | 295 |
296 |
297 |

OpenWiFiMap

298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 | About 306 |
307 |
308 |

About

309 |
310 |
311 |

OpenWiFiMap is 312 |

    313 |
  • free software licensed under the MIT License.
  • 314 |
  • available at github. Please submit code, bug reports, feature requests or other ideas for improvements there!
  • 315 |
  • meant to support free networks (free as in free speech, not free beer) like freifunk.
  • 316 |
  • a HTML5 app that should run fine on mobile devices.
  • 317 |
  • is backed by CouchDB+GeoCouch and it can be easily run as a CouchApp on your own CouchDB server.
  • 318 |
  • written by: 319 |
    320 | 321 |
    322 | with contributions from Andreas Pittrich and Daniel Pfisterer.
  • 323 |
324 | The WiFi icon was created by Canopus49 and is licensed under CC-BY-SA. 325 |

326 |

327 | Donate with flattr: 328 | 329 | 332 |

333 | Close 334 |
335 |
336 |
337 |
338 | 339 |
340 |
341 |
342 | 343 |
344 |
345 |

OpenWiFiMap – List

346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 | 354 |
355 |
356 |
357 |
358 |
359 | 360 |
361 |
362 |

OpenWiFiMap – Details

363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 | 371 |
372 |
373 |
374 |
375 |
376 | 377 | 378 | 379 | 380 | -------------------------------------------------------------------------------- /map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenWiFiMap 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 38 |
39 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /rewrites.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "", 4 | "to": "index.html" 5 | }, 6 | { 7 | "from": "map.html", 8 | "to": "map.html" 9 | }, 10 | { 11 | "from": "images/*", 12 | "to": "images/*" 13 | }, 14 | { 15 | "from": "style/*", 16 | "to": "style/*" 17 | }, 18 | { 19 | "from": "external/*", 20 | "to": "external/*" 21 | }, 22 | { 23 | "from": "_view/*", 24 | "to": "_view/*" 25 | }, 26 | { 27 | "from": "_spatial/*", 28 | "to": "_spatial/*" 29 | }, 30 | { 31 | "from": "script/*", 32 | "to": "script/*" 33 | }, 34 | { 35 | "from": "images/*", 36 | "to": "images/*" 37 | }, 38 | { 39 | "from": "openwifimap", 40 | "to": "../../" 41 | }, 42 | { 43 | "from": "openwifimap/*", 44 | "to": "../../*" 45 | }, 46 | { 47 | "from": "/api/*", 48 | "to": "../../_design/owm-api/_rewrite/*" 49 | }, 50 | { 51 | "from": "/api/", 52 | "to": "../../_design/owm-api/_rewrite/" 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /script/owm_app.js: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////// 2 | // jquery mobile page + parameter handling 3 | 4 | // this function is free software, Copyright (c) 2011, Kin Blas 5 | // see https://github.com/jblas/jquery-mobile-plugins/blob/master/page-params/jqm.page.params.js 6 | function queryStringToObject( qstr ) 7 | { 8 | var result = {}, 9 | nvPairs = ( ( qstr || "" ).replace( /^\?/, "" ).split( /&/ ) ), 10 | i, pair, n, v; 11 | 12 | for ( i = 0; i < nvPairs.length; i++ ) { 13 | var pstr = nvPairs[ i ]; 14 | if ( pstr ) { 15 | pair = pstr.split( /=/ ); 16 | n = pair[ 0 ]; 17 | v = pair[ 1 ]; 18 | if ( result[ n ] === undefined ) { 19 | result[ n ] = v; 20 | } else { 21 | if ( typeof result[ n ] !== "object" ) { 22 | result[ n ] = [ result[ n ] ]; 23 | } 24 | result[ n ].push( v ); 25 | } 26 | } 27 | } 28 | 29 | return result; 30 | } 31 | 32 | ////////////////////////////////////////////////////////// 33 | // general functions 34 | function getPopupHTML(nodedata) { 35 | return ich.mappopupmust(nodedata, true); 36 | } 37 | 38 | // obj may be an object or a list 39 | function scanListsObj(obj) { 40 | if (obj instanceof Array) { 41 | for (var i=0; i0 45 | } 46 | if (obj instanceof Object) { 47 | for (var key in obj) { 48 | var val = obj[key]; 49 | if (scanListsObj(val)) { 50 | obj[String(key)+"?"] = true; 51 | } 52 | } 53 | return false; 54 | } 55 | } 56 | 57 | function latlng2lnglat(latlng) { 58 | return [latlng[1], latlng[0]]; 59 | } 60 | 61 | function validateNode(nodedata) { 62 | // ignore this node if mtime older than 7 days 63 | var date = new Date(); 64 | date.setHours(date.getHours() - 24*7); 65 | var nodedate = new Date(nodedata['mtime']); 66 | return nodedate > date; 67 | } 68 | 69 | (function() { 70 | var couchurl = 'api/'; 71 | 72 | ////////////////////////////////////////////////////////// 73 | // map page 74 | $(document).on('pageshow', '#map', function() { 75 | var mappage = $('#map'); 76 | var bbox=null; 77 | var pd = $.mobile.pageData; 78 | if (pd && pd.bbox && typeof pd.bbox == "string") { 79 | bbox = getBboxFromString(pd.bbox); 80 | } 81 | 82 | function onBboxChange(bboxstr) { 83 | var currentPageID = $.mobile.activePage == null ? 'map' : $.mobile.activePage.attr('id'); 84 | if (currentPageID=='map') { 85 | params.sethash( '#map?bbox=' + bboxstr ); 86 | $("a#listlink").attr('href', '#list?bbox=' + bboxstr); 87 | } 88 | } 89 | 90 | var owmwidget = mappage.data('owmwidget'); 91 | if (!owmwidget) { 92 | owmwidget = new OWMWidget( 93 | { 94 | divId: 'mapdiv', 95 | onBboxChange: function(bbox) { 96 | onBboxChange(bbox.toString()); 97 | }, 98 | getPopupHTML: function(nodedata) { 99 | return ich.mappopupmust(nodedata, true); 100 | } 101 | }, 102 | { 103 | trackResize: false 104 | }, 105 | { 106 | couchUrl: couchurl, coarseThreshold: 1000, 107 | nodeFilter: validateNode 108 | } 109 | ); 110 | mappage.data('owmwidget', owmwidget); 111 | 112 | function onResize() { 113 | var currentPageID = $.mobile.activePage == null ? 'map' : $.mobile.activePage.attr('id'); 114 | if (currentPageID=='map') { 115 | // This hack is needed as long as 100% height in css is f**ked up. 116 | // The height is not set to 100% at this point but so something else, 117 | // so zooming is screwed up without the hack. 118 | var cur_height = $('#mapdiv').height(); 119 | var new_height = 120 | $(window).height() 121 | - $('#map div[data-role="header"]').outerHeight() 122 | - $('#map div[data-role="footer"]').outerHeight(); 123 | $('#mapdiv').height( new_height ); 124 | owmwidget.map.invalidateSize(); 125 | } 126 | } 127 | $(window).on("pageshow resize", onResize); 128 | onResize(); 129 | 130 | if (!bbox) { 131 | owmwidget.map.fitWorld(); 132 | owmwidget.map.locate({setView: true, maxZoom: 15, timeout: 20000}) 133 | .on('locationerror', function(e) { console.log('location NOT found', e) }) 134 | .on('locationfound', function(e) { console.log('location found', e) }); 135 | } 136 | } 137 | 138 | if (bbox) { 139 | owmwidget.fitBbox(bbox); 140 | } 141 | 142 | $.getJSON("https://api.github.com/repos/freifunk/openwifimap-html5/contributors", 143 | function (data) { 144 | var html = ich.about_contrib_must({contributors:data}, true); 145 | $("#about_contrib").empty().append(html); 146 | }); 147 | }); 148 | 149 | 150 | ////////////////////////////////////////////////////////// 151 | // list page 152 | function listUpdate(data) { 153 | var html; 154 | if (!data) { 155 | html = "

No area selected! Go to the map to find nodes.

"; 156 | } else if (!data.nodes.length) { 157 | html = "

No nodes in selected area! Go to the map to find nodes.

"; 158 | } else { 159 | html = ich.listmust(data); 160 | } 161 | $("#listdiv").empty().append(html); 162 | var listul = $("#listul"); 163 | if (listul.hasClass('ui-listview')) { 164 | listul.listview('refresh'); 165 | } else { 166 | listul.listview(); 167 | } 168 | } 169 | 170 | $(document).on('pagebeforeshow', '#list', function (){ 171 | var bbox = null; 172 | var pd = $.mobile.pageData; 173 | if (pd && pd.bbox && typeof pd.bbox == "string") { 174 | bbox = getBboxFromString(pd.bbox); 175 | } 176 | if (bbox) { 177 | $("a#maplink").attr("href", "#map?bbox=" + bbox.toString()); 178 | var bboxlnglat = [latlng2lnglat(bbox[0]), latlng2lnglat(bbox[1])].toString(); 179 | $.getJSON(couchurl + 'view_nodes_spatial', 180 | { 181 | "bbox": bboxlnglat, 182 | limit: 500 // note that due to the mtime check the actual 183 | // number of displayed nodes may be lower 184 | }, function(data) { 185 | var nodes = []; 186 | for (var i=0; i' ); 264 | } 265 | 266 | if (!detailpagemap) { 267 | detailpagemap = new OWMWidget( 268 | { 269 | divId: 'detailmap', 270 | onBboxChange: function(bboxstr) {}, 271 | getPopupHTML: function(nodedata) { 272 | return ich.mappopupmust(nodedata, true); 273 | } 274 | }, 275 | { 276 | trackResize: false 277 | }, 278 | { 279 | couchUrl: couchurl, coarseThreshold: 1000, 280 | nodeFilter: validateNode 281 | } 282 | ); 283 | detailpage.data('map', detailpagemap); 284 | } else { 285 | $("#detailmapcontainer").empty().append(mapdiv); 286 | } 287 | detailmapResize(); 288 | detailpagemap.map.setView([data.latitude,data.longitude], 16); 289 | $("#detailmapcenter").click( function () { 290 | detailpagemap.map.setView([data.latitude,data.longitude], 16); 291 | }); 292 | }); 293 | } else { 294 | $("#detaildiv").empty().append("

No node selected! Go to the map to find nodes.

"); 295 | } 296 | }); 297 | })(); 298 | -------------------------------------------------------------------------------- /script/owm_widget.js: -------------------------------------------------------------------------------- 1 | var OWMWidget = function (options, mapoptions, couchmapoptions) { 2 | var widget = {}; 3 | var options = L.extend({ 4 | divId: 'map', 5 | getPopupHTML: function (nodedata) {return ''+nodedata.hostname+'';}, 6 | onBboxChange: function () {} 7 | }, options); 8 | var mapoptions = L.extend({ 9 | worldCopyJump: true 10 | }, mapoptions); 11 | function getColor(val, bad, good) { 12 | var colors = ['#D50000', '#D5A200', '#CCD500', '#00D500']; 13 | var index = Math.floor( colors.length * (val - bad)/(good - bad) ); 14 | index = Math.max(0, Math.min(colors.length-1, index)); 15 | return colors[index]; 16 | } 17 | var couchmapoptions = L.extend({ 18 | nodesUrl: 'view_nodes', 19 | nodesUrlSpatial: 'view_nodes_spatial', 20 | nodesUrlCoarse: 'view_nodes_coarse', 21 | nodeAdd: function(nodedata, layer) { 22 | var mtime = new Date(nodedata.mtime); 23 | var time = new Date(); 24 | var timediff = time-mtime; 25 | if (timediff<0) { 26 | timediff=0; 27 | } 28 | timediff /= 1000*60*60*24; 29 | var color = getColor(timediff, 4, 0); 30 | 31 | /* unfortunately, this does not work because markercluster 32 | * seems to need a marker (and not a circleMarker) 33 | return L.circleMarker(nodedata.latlng, 34 | { 35 | title: nodedata.hostname, 36 | radius: 15, 37 | color: color 38 | }) 39 | .bindPopup(options.getPopupHTML(nodedata)).addTo(layer); 40 | */ 41 | return L.marker(nodedata.latlng, 42 | { 43 | title: nodedata.hostname, 44 | icon: L.icon( {iconUrl: 'images/node_circle.svg', iconSize: [30,30], iconAnchor: [15,15]}) 45 | }) 46 | .bindPopup(options.getPopupHTML(nodedata)).addTo(layer); 47 | }, 48 | nodeFilter: function(nodedata) { 49 | // ignore this node if mtime older than 7 days 50 | var date = new Date(); 51 | date.setHours(date.getHours() - 24*7); 52 | var nodedate = new Date(nodedata['mtime']); 53 | return nodedate > date; 54 | }, 55 | linkAdd: function(node1, node2, layer) { 56 | // ignore this link if distance > 50km 57 | var latlng1 = new L.LatLng(node1.data.latlng[0], node1.data.latlng[1]), 58 | distance = Math.round(latlng1.distanceTo(node2.data.latlng)); 59 | if (distance > 5e4) { 60 | return; 61 | } 62 | 63 | var quality1 = -1; 64 | var quality2 = -1; 65 | // find qualities and pick best (does this make sense?) 66 | if (node1.data.links) { 67 | for (var i=0, link; link=node1.data.links[i++];) { 68 | if (link.id == node2.data.id && link.quality) { 69 | quality1 = link.quality; 70 | } 71 | } 72 | } 73 | if (node2.data.links) { 74 | for (var i=0, link; link=node2.data.links[i++];) { 75 | if (link.id == node1.data.id && link.quality) { 76 | quality2 = link.quality; 77 | } 78 | } 79 | } 80 | // check out of bounds 81 | function validate_quality(q) { 82 | return Math.min(1, Math.max(0, q)); 83 | } 84 | var quality = validate_quality( Math.max(quality1, quality2) ); 85 | 86 | var color = getColor(quality, 0, 1) 87 | var opacity = 0.25 + 0.5*quality; 88 | 89 | var html = '

' 90 | + node1.data.hostname 91 | + ' ↔ ' 92 | + node2.data.hostname 93 | + '

' 94 | + 'Distance: ' 95 | + distance 96 | + 'm'; 97 | if (quality1 > -1) { 98 | quality1 = validate_quality(quality1); 99 | html += '
Quality (as reported by ' 100 | + node1.data.hostname+'): ' 101 | + quality1; 102 | } 103 | if (quality2 > -1) { 104 | quality2 = validate_quality(quality2); 105 | html += '
Quality (as reported by ' 106 | + node2.data.hostname+'): ' 107 | + quality2; 108 | } 109 | 110 | return L.polyline([node1.data.latlng, node2.data.latlng], 111 | {color: color, opacity: opacity}) 112 | .bindPopup(html).addTo(layer); 113 | } 114 | }, couchmapoptions); 115 | 116 | widget.map = L.map(options['divId'], mapoptions); 117 | 118 | var tile_lyrk = L.tileLayer('https://tiles.lyrk.org/ls/{z}/{x}/{y}?apikey={key}', { 119 | key: 'ce4ebc2a30064ca19ec3ccc898486c17', 120 | styleId: 997, 121 | attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Lyrk' 122 | }).addTo( widget.map ); 123 | // https://raw.github.com/shramov/leaflet-plugins/master/layer/tile/Bing.js 124 | var tile_bing = new L.BingLayer("ArewtcSllazYp52r7tojb64N94l-OrYWuS1GjUGeTavPmJP_jde3PIdpuYm24VpR"); 125 | 126 | var couchmap = new L.CouchMap(couchmapoptions); 127 | var couchlayers = couchmap.getLayers(); 128 | 129 | widget.map.addLayer(couchlayers['nodes']).addLayer(couchlayers['links']); 130 | widget.control_layers = L.control.layers( 131 | { 132 | "Lyrk OSM": tile_lyrk, 133 | "Bing satellite": tile_bing 134 | }, 135 | { 136 | "Nodes": couchlayers['nodes'], 137 | "Links": couchlayers['links'] 138 | } 139 | ).addTo(widget.map); 140 | 141 | widget.control_scale = L.control.scale({imperial: false, maxWidth: 150, position: 'bottomright'}) 142 | .addTo(widget.map); 143 | widget.map.attributionControl.addAttribution('Nodes+Links © OpenWiFiMap contributors under ODbL'); 144 | 145 | widget.map.on('moveend', function() { 146 | var b = widget.map.getBounds(), 147 | sw = b.getSouthWest(), 148 | ne = b.getNorthEast(); 149 | options.onBboxChange([sw.lat, sw.lng, ne.lat, ne.lng]) 150 | }); 151 | 152 | widget.fitBbox = function(bbox) { 153 | // nasty hack: shrink the bbox a bit, so we don't zoom out 154 | var h = bbox[1][0] - bbox[0][0]; 155 | var w = bbox[1][1] - bbox[0][1]; 156 | var eps = 0.05; 157 | bbox[0][0] += eps*h; 158 | bbox[0][1] += eps*w; 159 | bbox[1][0] -= eps*h; 160 | bbox[1][1] -= eps*w; 161 | 162 | widget.map.fitBounds(bbox); 163 | } 164 | 165 | return widget; 166 | } 167 | 168 | function getBboxFromString(str) { 169 | var arr = str.split(','); 170 | var valid = false; 171 | if ( arr.length >= 4 ) { 172 | valid = true; 173 | for ( i = 0; i < 4; i++ ) { 174 | arr[i] = parseFloat(arr[i]); 175 | valid = valid ? isFinite(arr[i]) : false; 176 | } 177 | } 178 | arr = [[arr[0],arr[1]],[arr[2],arr[3]]]; 179 | return valid ? arr : null; 180 | } 181 | 182 | -------------------------------------------------------------------------------- /style/owm_app.css: -------------------------------------------------------------------------------- 1 | html, body, #map, #mapdiv, #detailmap { 2 | padding: 0; 3 | margin: 0; 4 | overflow: hidden; 5 | } 6 | 7 | #mapdiv { 8 | height: 100%; 9 | width: 100%; 10 | } 11 | 12 | div#container { 13 | height: 100%; 14 | width: 100%; 15 | } 16 | #container > div[data-role="page"] > div[data-role="header"] { 17 | height: 52px; 18 | width: 100%; 19 | } 20 | #container > div[data-role="page"] > div[data-role="content"] { 21 | position: absolute; 22 | width: 100%; 23 | top: 54px; 24 | bottom: 61px; 25 | padding: 0; 26 | overflow: auto; 27 | } 28 | #container > div[data-role="page"] > div[data-role="footer"] { 29 | height: 59px; 30 | width: 100%; 31 | position: absolute; 32 | bottom: 0; 33 | } 34 | 35 | #listul { 36 | margin: 0; 37 | } 38 | 39 | table { 40 | width: 100%; 41 | border-spacing: 0; 42 | } 43 | table.fifty td { 44 | vertical-align: top; 45 | padding: 6px; 46 | width: 50%; 47 | } 48 | table.fifty tr:nth-child(even) { 49 | background-color: #e0e0e0; 50 | } 51 | 52 | #avatar { 53 | vertical-align: middle; 54 | max-width: 60px; 55 | max-height: 60px; 56 | border-radius: 5px; 57 | } 58 | 59 | #avaframe { 60 | display: inline-block; 61 | width: 60px; 62 | height: 60px; 63 | line-height: 60px; 64 | padding: 5px; 65 | border: 1px solid #aaaaaa; 66 | border-radius: 5px; 67 | margin-right: 5px; 68 | background-color: #eeeeee; 69 | vertical-align: middle; 70 | text-align: center; 71 | } 72 | 73 | .titlecont h2 { 74 | display: inline-block; 75 | } 76 | 77 | #about_contrib { 78 | display: inline; 79 | } 80 | 81 | .gh-contrib { 82 | display: inline-block; 83 | margin-right: 5px; 84 | } 85 | 86 | .gh-contrib img { 87 | vertical-align: middle; 88 | } 89 | 90 | .gh-avatar { 91 | height: 20px; 92 | width: 20px; 93 | margin-right: 5px; 94 | margin-left: 5px; 95 | } 96 | -------------------------------------------------------------------------------- /style/owm_widget.css: -------------------------------------------------------------------------------- 1 | .antenna_marker { 2 | /*pointer-events: none;*/ 3 | } 4 | 5 | .leaflet-popup-content span { 6 | color: white; 7 | } 8 | /* 9 | .leaflet-marker-pane { 10 | z-index: 6; 11 | } 12 | 13 | .leaflet-overlay-pane { 14 | z-index: 7; 15 | }*/ 16 | 17 | .marker-cluster-small, 18 | .marker-cluster-medium, 19 | .marker-cluster-large, 20 | { 21 | background-color: rgba(133, 232, 5, 0.6); 22 | } 23 | 24 | .marker-cluster-small div, 25 | .marker-cluster-medium div, 26 | .marker-cluster-large div, 27 | { 28 | background-color: rgba(133, 232, 5, 0.8); 29 | } 30 | 31 | --------------------------------------------------------------------------------