diff --git a/site/assets/styles/atelier-forest.light.css b/assets/atelier-forest.light.css similarity index 100% rename from site/assets/styles/atelier-forest.light.css rename to assets/atelier-forest.light.css diff --git a/site/assets/styles/highlight.css b/assets/highlight.css similarity index 100% rename from site/assets/styles/highlight.css rename to assets/highlight.css diff --git a/js/highlight.js b/assets/highlight.js similarity index 100% rename from js/highlight.js rename to assets/highlight.js diff --git a/site/assets/sss.css b/assets/sss.css similarity index 100% rename from site/assets/sss.css rename to assets/sss.css diff --git a/site/assets/style.css b/assets/style.css similarity index 74% rename from site/assets/style.css rename to assets/style.css index ee7f7ce..98fcfc1 100644 --- a/site/assets/style.css +++ b/assets/style.css @@ -22,15 +22,24 @@ footer a {border: none;} .half {max-width: 50%; display: inline-block; vertical-align: top;} .half:nth-child(odd) {padding-left: 20px;} +img[alt=sheetseeimg] { + width: 400px; + margin: 0 auto; +} + +.index p:first-of-type { + text-align: center; +} /* older css */ -body {font-family: 'Source Sans Pro'; font-size: 18px; background: #fff; color: #535353; border: 10px #47CCFC solid; margin: 0px; padding: 20px 20px 100px 20px; overflow: auto; } +body {font-family: 'Source Sans Pro'; background: #fff; color: #535353; border: 10px #47CCFC solid; margin: 0px; padding: 20px 20px 100px 20px; overflow: auto; } h1, h2, h3, h4, h5, h6 {font-family: Source Sans Pro, Arial, sans-serif;} h2 {margin-top: 40px;} img {width: 100%;} hr {border: 2px solid #CCF4FF;} +p {font-size: 18px;} p a, a {color: #535353; text-decoration: none; padding-bottom: 0px; border-bottom: 2px #CCF4FF solid;} a:hover {color: #47CCFC;} a:active {color: #47CCFC;} @@ -44,8 +53,20 @@ body.index table {border: 4px solid #CCF4FF; padding: 18px;} pre {word-wrap: break-word; padding: 10px; background: #F8F8F8;} pre code {padding: 0;} -code {font-family: "Source Code Pro", monospace; font-size: 14px; line-height: 1; background: #F8F8F8; color: #636363; font-weight: 400; padding: 2px 6px; border-radius: 2px;} -.hljs { background: #F8F8F8; color: #494949; line-height: 1.4em; } +code {font-family: "Source Code Pro", monospace; line-height: 1; font-size: 14px; background: #F8F8F8; color: #636363; font-weight: 400; padding: 2px 6px; border-radius: 2px;} +h3 code {font-weight: bold; background: #CCF4FF; font-size: 19px;} +.hljs {background: #F8F8F8; color: #494949; line-height: 1.4em;} + +.new-news { + color: #47CCFC; + font-size: 30px; + font-weight: bold; + border: 8px solid #47CCFC; + /*border-radius: 3px;*/ + display: inline-block; + padding: 6px 16px; + margin-top: 40px; +} /* funsies */ ::selection {background: #44FFB4;} diff --git a/buildpage.js b/buildpage.js deleted file mode 100644 index e71d407..0000000 --- a/buildpage.js +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -var glob = require('glob') -var fs = require('fs') -var marked = require('marked') -var hbs = require('handlebars') -var mkdirp = require('mkdirp') -var path = require('path') -var cpr = require('cpr') - -cpr('./demos', './site/demos', function(err, files) { - if (err) return console.log(err) - cpr('./js', './site/js', function(err, files) { - if (err) return console.log(err) - }) -}) - -fs.readFile('readme.md', function(err, file) { - if (err) return console.log(err) - var name = "index" - var content = file.toString() - var html = changeExtensions(marked(content)) - applyTemplate(html, name) -}) - -glob("docs/*.md", function (err, files) { - if (err) return console.log(err) - var filenames = files.map(function(name) { - return path.basename(name) - }) - filenames.forEach(function (file) { - var name = file.split('.md')[0] - var filePath = "./docs/" - var content = fs.readFileSync(filePath + file).toString() - var html = changeExtensions(marked(content)) - applyTemplate(html, name) - }) -}) - -function applyTemplate(html, name) { - var content = {content: html, name: name} - if (name === "index") { - content.rootstyle = "." - content.rootdoc = "docs" - content.rootdemo = "." - } else { - content.rootstyle = ".." - content.rootdoc = "." - content.rootdemo = ".." - } - var file = "template.hbs" - var rawTemplate = fs.readFileSync(file).toString() - var template = hbs.compile(rawTemplate) - var page = template(content) - writeFile(page, name) -} - -function writeFile(page, name) { - if (name === "index") { - return fs.writeFileSync('./site/' + name + '.html' , page) - } - mkdirp('./site/docs', function (err) { - if (err) return console.error(err) - fs.writeFileSync('./site/docs/' + name + '.html' , page) - }) -} - -function changeExtensions(html, name) { - if (name === "index") { - html = html.replace('/\./\.\/', '') - } - var re = /.md$/ - var newHtml = html.replace(/\.md/g, '.html') - return newHtml -} diff --git a/demos/demo-chart.html b/demos/demo-chart.html deleted file mode 100644 index 69fcb5c..0000000 --- a/demos/demo-chart.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - Sheetsee Chart Demo - - - - - - - - - - - - -
-

Pennies by State

-

spreadsheet

-
-

View Source // View Documentation

- - -
- - - - - diff --git a/demos/demo-map.html b/demos/demo-map.html index e8df041..452079a 100644 --- a/demos/demo-map.html +++ b/demos/demo-map.html @@ -4,30 +4,24 @@ Sheetsee Maps Demo - - + + -

All Pennies Map

spreadsheet

-

+
Loading...

View Source // View Documentation

-

Using linestrings, polygons and multipolygons

-
-

View Source // View Spreadsheet

+
diff --git a/demos/demo-table.html b/demos/demo-table.html index 7ff8116..1ea8c79 100644 --- a/demos/demo-table.html +++ b/demos/demo-table.html @@ -4,10 +4,9 @@ Sheetsee Table Demo - - + @@ -18,108 +17,73 @@ .no-pag:hover {color: #acacac;} input {border: none; border-bottom: 1px solid #333;margin: 10px 0px; width: 200px; font-size: 16px; padding-bottom: 6px;} .tHeader {padding: 8px; cursor: pointer; text-align: left;} - .photo {display: inline-block; float: left; width: 100px;} - #photogrid {overflow: auto;} - -
-

All Pennies

-

spreadsheet

- -

-

California Pennies

- -
-

Pretty Pennies

-
-

View Source // View Documentation

+ +
+

All Pennies

+

spreadsheet

+ + Clear +

Loading...
+

View Source // View Documentation

- -
+ +
- + - + - - - - + function showInfo (data) { + var tableOptions = { + "data": data, + "pagination": 10, + "tableDiv": "#fullTable", + "filterDiv": "#fullTableFilter" + } + Sheetsee.makeTable(tableOptions) + Sheetsee.initiateTableFilter(tableOptions) + } + + diff --git a/docs/about.html b/docs/about.html new file mode 100644 index 0000000..1304750 --- /dev/null +++ b/docs/about.html @@ -0,0 +1,81 @@ + + + + + Sheetsee.js + + + + + + + + + + + +
+

About

+

Sheetsee.js began as a part of a Code for America 2012 Fellowship project. The original idea was to make a way for cities to easily publish and maintain budget data themselves. Originally, the JavaScript was part of a WordPress theme that could be connected to a spreadsheet, allowing new pages to be created with charts, maps and tables automatically generated from the data.

+

In early 2013, after the CfA Fellowship and through a grant from Mozilla Open News, the JavaScript bits were pulled out to make it a standalone open source library: sheetsee.js.

+

Since then there have been a few updates to the library but it was not until 2017 when Sheetsee got it's next major upgrade.

+

In Spring of 2017 Sheetsee was cleaned up and downsized. Charting support and some dependencies were removed to make maintenance and overall size better.

+

Dependencies

+

Sheetsee comes into play after you use Tabletop.js, a library that handles the messy interactions with the Google Spreadsheets API for you and returns a lovely array of your data. At the end of the day, however, Sheetsee just wants an array of JSON data, if you have some on hand already, you can use Sheetsee too :)

+

Mustache, Leaflet

+

Sheetsee uses the Mustache.js library for templates in sheetsee-tables and map popups in sheetsee-maps. The maps are generated using the Leaflet.js library.

+

Hacked on Openly

+ +

Contact & Contribute

+

Issues with this website can be opened on the sheetsee.js repository. Otherwise, if your issue falls specifically with one of the modules, you can file it on its particular repo:

+ +

Thanks, y'all

+

Thanks to Code for America for providing the platform me to build the first version of sheetsee.js for Macon, Georgia.

+

Thanks to Dan Sinker at Open News for having faith and getting things together to make this Code Sprint happen and thanks to Matt Green at WBEZ for being a willing partner.

+

Thanks to Max Ogden for emotional support, teaching me JavaScript and answering lots of questions.

+

Thanks to all the authors and contributors to Tabletop.js, Leaflet.js and Mustache.js. Thanks to Google and the Internet for existing and to all those who've written tutorials or asked or answered a question on StackOverflow.

+

Thanks to Mom and Dad for getting a computer in 1996 and the mIRC scripts I started writing that I suppose would eventually lead me here.

+ + + +
+ + + diff --git a/docs/about.md b/docs/about.md index e87c11e..95700b1 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,36 +1,33 @@ # About -Sheetsee.js began as a part of my [Code for America](http://www.codeforamerica.org) 2012 Fellowship project, [See Penny Work](http://www.seepennywork.in). The idea and original code was to enable cities to easily publish and maintain themselves their budget data. The original sheetsee.js was built into Wordpress templates so that with the See Penny Work template, you could create pages that you only had to name and they would be populated with maps, charts and tables based on the page name corelating with a project in the spreadsheet. +Sheetsee.js began as a part of a [Code for America](http://www.codeforamerica.org) 2012 Fellowship project. The original idea was to make a way for cities to easily publish and maintain budget data themselves. Originally, the JavaScript was part of a WordPress theme that could be connected to a spreadsheet, allowing new pages to be created with charts, maps and tables automatically generated from the data. -In early 2013, after the CfA Fellowship, I recieved a grant from [Mozilla Open News](http://opennews.org/) to pull out the sheetsee.js bits and make it a standalone open source library. That brought us to version 2. +In early 2013, after the CfA Fellowship and through a grant from [Mozilla Open News](http://opennews.org), the JavaScript bits were pulled out to make it a standalone open source library: sheetsee.js. -The present version makes the project modular, customizable and with more mapping and table features. +Since then there have been a few updates to the library but it was not until 2017 when Sheetsee got it's next major upgrade. -## Built on top of Tabletop.js -Sheetsee pairs with [tabletop.js](https://github.com/jsoma/tabletop) a library that handles the messy interactions with the Google Spreadsheets API for you and returns a lovely array of your data. Every instance of Sheetsee begins with running tabletop.js. Well, actually, if you have some data on hand already in JSON format, you can use Sheetsee too :) +In Spring of 2017 Sheetsee was cleaned up and downsized. Charting support and some dependencies were removed to make maintenance and overall size better. -### Sheetsee.js + Mapbox.js + d3.js -Once you've got the data, you're ready to Sheetsee. You can now decide if you want to map, chart or display your data in a table. Sheetsee's table module, **sheetsee-tables**, comes with sorting, filtering and pagination. Tables use [icanhaz.js](http://www.icanhazjs.com) for very mustache.js-like templating. +## Dependencies -**Sheetsee-maps** is built ontop of [Leaflet.js](http://leafletjs.com/) and [Mapbox.js](https://www.mapbox.com/mapbox.js/) and allows you to customize colors and popups of points, lines, polygons or multipolygons. +Sheetsee comes into play after you use [Tabletop.js](https://github.com/jsoma/tabletop), a library that handles the messy interactions with the Google Spreadsheets API for you and returns a lovely array of your data. At the end of the day, however, Sheetsee just wants an array of JSON data, if you have some on hand already, you can use Sheetsee too :) -Finally, **Sheetee-charts** comes with three basic [d3.js](http://d3js.org) charts: bar, circle and line. It is difficult to make a chart that can suit many types of data, but it is easy to choose your own d3 chart and plug it in to sheetsee. Documentation for creating a d3 module is [here](docs/custom-chart.md). +### Mustache, Leaflet + +Sheetsee uses the [Mustache.js](https://mustache.github.io) library for templates in `sheetsee-tables` and map popups in `sheetsee-maps`. The maps are generated using the [Leaflet.js](http://leafletjs.com) library. ## Hacked on Openly - Sheetsee.js is open source software with a [BSD license](docs/license.md). -- Sheetsee.js is a labor of love by [jlord](http://www.github.com/jlord) ([twitter](http://www.twitter.com/jllord)) with support from [contributors](https://github.com/jlord/sheetsee.js/graphs/contributors). +- Sheetsee.js is a labor of love by [jlord](http://www.github.com/jlord) ([Twitter](http://www.twitter.com/jllord)) ## Contact & Contribute -- File a [new issue](https://github.com/jlord/sheetsee.js/issues/new) for ideas and bug reports. -- If your issue falls specifically with one of the modules, you can file it on its particular repo: - - [sheetsee](http://www.github.com/jlord/sheetsee/issues/new) +Issues with this website can be opened on the [sheetsee.js repository](https://github.com/jlord/sheetsee/issues/new). Otherwise, if your issue falls specifically with one of the modules, you can file it on its particular repo: + - [sheetsee (CLI)](http://www.github.com/jlord/sheetsee/issues/new) - [sheetsee-core](http://www.github.com/jlord/sheetsee-core/issues/new) - [sheetsee-tables](http://www.github.com/jlord/sheetsee-tables/issues/new) - [sheetsee-maps](http://www.github.com/jlord/sheetsee-maps/issues/new) - - [sheetsee-charts](http://www.github.com/jlord/sheetsee-charts/issues/new) -- [@jllord](http://www.twitter.com/jllord) on Twitter. -## Big Time Thanks +## Thanks, y'all Thanks to [Code for America](http://www.codeforamerica.org) for providing the platform me to build the first version of sheetsee.js for Macon, Georgia. @@ -38,6 +35,6 @@ Thanks to [Dan Sinker](http://www.twitter.com/dansinker) at [Open News](http://w Thanks to [Max Ogden](http://www.twitter.com/maxogden) for emotional support, teaching me JavaScript and answering lots of questions. -Thanks to all the authors and contributors to Tabletop.js, [Mapbox.js](https://www.mapbox.com/mapbox.js/), [Leaflet.js](http://leafletjs.com/), jQuery, [ICanHas.js](http://icanhazjs.com/) and [d3.js](http://d3js.org). Thanks to Google and the Internet for existing and to all those who've written tutorials or asked or answered a question on StackOverflow. +Thanks to all the authors and contributors to [Tabletop.js](https://github.com/jsoma/tabletop), [Leaflet.js](http://leafletjs.com) and [Mustache.js](https://mustache.github.io). Thanks to Google and the Internet for existing and to all those who've written tutorials or asked or answered a question on StackOverflow. Thanks to Mom and Dad for getting a computer in 1996 and the mIRC scripts I started writing that I suppose would eventually lead me here. diff --git a/site/docs/basics.html b/docs/basics.html similarity index 63% rename from site/docs/basics.html rename to docs/basics.html index b734dc1..84728a0 100644 --- a/site/docs/basics.html +++ b/docs/basics.html @@ -7,8 +7,8 @@ - - + + @@ -16,32 +16,35 @@

Spreadsheets as Databases

-

Spreadsheets are a great lightweight databases. Google Spreadsheets in particular are easy to work with and share, making this unlike most traditional database setups. That being said, traditional databases are great for bigger, more secure jobs. If you're storing lots and lots and lots of information, or storing sensitive or complex information -- the spreadsheet is not for you. But if you're working on small to medium sized personal or community projects, try a spreadsheet!

+

Spreadsheets are a great lightweight databases. Google Spreadsheets in particular are easy to work with and share, making this unlike most traditional database setups. That being said, traditional databases are great for bigger, more secure jobs. If you're storing lots and lots and lots of information, or storing sensitive or complex information—the spreadsheet is not for you. But if you're working on small to medium sized personal or community projects, try a spreadsheet!

The Short & Sweet

-
    -
  1. Link to Sheetsee.js, tabletop.js and jQuery in your HTML head.
  2. -
  3. Create a place holder <div> in your HTML for any chart, map or table you want to have.
  4. -
  5. Create templates for tables in <script> tags.
  6. -
  7. Inside of a <script> tag initialize Tabletop.js. It waits for the document to load and then initializes tabletop and calls back a function when it has returned with the spreadsheet data.
    document.addEventListener('DOMContentLoaded', function() {
    - var gData
    - var URL = "YOURSPREADSHEETSKEYHERE"
    - Tabletop.init( { key: URL, callback: callback, simpleSheet: true } )
    +
      +
    • Link to Sheetsee.js and Tabletop.js in your HTML head.
    • +
    • Link to sheetsee's default stylesheet, sss.css.
    • +
    • Create a place holder <div>, with id, in your HTML the map or table you want.
    • +
    • Create templates for tables in <script> tags with an id matching the <div> plus _template.
    • +
    • Inside of another <script> tag initialize Tabletop.js. Once it fetches your spreadsheet data, pass it onto Sheetsee.
    • +
    +
    document.addEventListener('DOMContentLoaded', function() {
    +  var URL = 'YOURSPREADSHEETSKEYHERE'
    +  Tabletop.init({key: URL, callback: callback, simpleSheet: true})
     })
     
    -
  8. -
  9. Define the function that Tabletop.js calls when it returns with the data. This function will contain all the Sheetsee.js functions that you use for the maps, charts and tables you desire. Style it up with some CSS.
    function callback(data) {
    - // All the sheetsee things you want to do!
    +
      +
    • Define the function that Tabletop.js calls when it returns with the data. This function will contain all the Sheetsee.js methods you want to use.
    • +
    +
    function callback (data) {
    +  // All the Sheetsee things you want to do!
     }
     
    -
  10. +
    • Set it and forget. Now all you need to do is edit the spreadsheet and visitors will get the latest information every time they load the page.
    • -
+

Bare Minimum Setup

-

Ignoring some HTML things to conserve space, you get the point. This is a basic setup.

+

Ignoring some HTML things to conserve space, you get the point. This is a basic setup:

<html>
   <head>
     <meta charset="utf-8">
-    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
     <script type="text/javascript" src="js/tabletop.js"></script>
     <script type="text/javascript" src='js/sheetsee.js'></script>
     <link rel="stylesheet" type="text/css" href="css/sss.css">
@@ -54,12 +57,12 @@ 

Bare Minimum Setup

</script> <script type="text/javascript"> - document.addEventListener('DOMContentLoaded', function() { - var URL = "YOURSPREADSHEETSKEYHERE" + document.addEventListener('DOMContentLoaded', function () { + var URL = 'YOURSPREADSHEETSKEYHERE' Tabletop.init( { key: URL, callback: myData, simpleSheet: true } ) }) - function myData(data) { - All the sheetsee things you want to do! + function myData (data) { + // All the sheetsee things you want to do! } </script> </body> @@ -67,23 +70,22 @@

Bare Minimum Setup

Your Data

sheetsee

-

Your Google Spreadsheet should be set up with row one as your column headers. Row two and beyond should be your data. Each header and row becomes an oject in the final array that Tabletop.js delivers of your data.

+

Your Google Spreadsheet should be set up with row one as your column headers. Row two and beyond should be your data. Each header and row becomes an object in the final array that Tabletop.js delivers of your data.

sheetsee

There shouldn't be any breaks or horizontal organization in the spreadsheet. But, feel free to format the style of your spreadsheet as you wish; borders, fonts and colors and such do not transfer or affect your data exporting.

[{"name":"Coco","breed":"Teacup Maltese","kind":"Dog","cuddlability":"5","lat":"37.74832","long":"-122.402158","picurl":"http://distilleryimage8.s3.amazonaws.com/98580826813011e2bbe622000a9f1270_7.jpg","hexcolor":"#ECECEC","rowNumber":1}...]
 

Hexcolor

sheetsee

-

You must add a column to your spreadsheet with the heading hexcolor (case insensitive). The maps, charts and such use colors and this is the easiest way to standardize that. The color scheme is up to you, all you need to do is fill the column with hexidecimal color values. This color picker by Devin Hunt is really nice. #Funtip: Coloring the background of the cell it's hexcolor brings delight!

+

You can add a column to your spreadsheet with the heading hexcolor (case insensitive) and add colors to be used in maps. The color scheme is up to you, all you need to do is fill the column with hexidecimal color values. This color picker by Devin Hunt is really nice. #Funtip: Coloring the background of the cell it's hexcolor brings delight!

Geocoding

-

If you intend to map your data and only have addresses you'll need to geocode the addresses into lat/long coordinates. Mapbox built a plugin - that does this for you in Google Docs. You can also use websites like latlong.net to get the coordinates and paste them into rows with column headers lat and long.

+

If you intend to map your data and only have addresses you'll need to geocode the addresses into lat/long coordinates. Mapbox built a plugin that does this for you in Google Docs. You can also use websites like latlong.net to get the coordinates and paste them into rows with column headers lat and long.

Publishing Your Spreadsheet

sheetsee

-

You need to do this in order to generate a unique key for your spreadsheet, which Tabletop.js will use to get your spreadsheet data. In your Google Spreadsheet, click File > Publish to the Web. Then in the next window click Start Publishing; it will then turn into a Stop Publishing button.

+

You need to do this in order to generate a unique key for your spreadsheet, which Tabletop.js will use to get your spreadsheet data. In your Google Spreadsheet, click File > Publish to the Web. Then in the next window click Start Publishing; it will then turn into a Stop Publishing button.

sheetsee

You should have an address in a box at the bottom, your key is the portion between the = and the &. You'll retrieve this later when you hook up your site to the spreadsheet. Actually, you technically can use the whole URL, but it's so long...

CSS

-

Sheetsee.js comes with a bare minimum stylesheet, sss.css, which contains elements you'll want to style when using the feature they correspond to.

+

Sheetsee.js comes with a bare minimum stylesheet, sss.css, which contains elements you'll want to style when using the feature they correspond to. Use it if you want, or as a starting off point to create your own styles.

diff --git a/docs/basics.md b/docs/basics.md index c739e15..7e587a7 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -1,37 +1,40 @@ # Spreadsheets as Databases -Spreadsheets are a great _lightweight_ databases. Google Spreadsheets in particular are easy to work with and share, making this unlike most traditional database setups. That being said, traditional databases are great for bigger, more secure jobs. If you're storing lots and lots and lots of information, or storing sensitive or complex information -- the spreadsheet is not for you. But if you're working on small to medium sized personal or community projects, try a spreadsheet! +Spreadsheets are a great _lightweight_ databases. Google Spreadsheets in particular are easy to work with and share, making this unlike most traditional database setups. That being said, traditional databases are great for bigger, more secure jobs. If you're storing lots and lots and lots of information, or storing sensitive or complex information—the spreadsheet is not for you. But if you're working on small to medium sized personal or community projects, try a spreadsheet! ## The Short & Sweet -1. Link to Sheetsee.js, [tabletop.js](https://github.com/jsoma/tabletop/) and [jQuery](http://www.jquery.org) in your HTML head. -2. Create a place holder `
` in your HTML for any chart, map or table you want to have. -3. Create templates for tables in ` @@ -44,12 +47,12 @@ Ignoring some HTML things to conserve space, you get the point. This is a basic @@ -60,7 +63,7 @@ Ignoring some HTML things to conserve space, you get the point. This is a basic ![sheetsee](https://raw.github.com/jllord/sheetsee-cache/master/img/spreadsheettodata.png) -Your Google Spreadsheet should be set up with row one as your column headers. Row two and beyond should be your data. Each header and row becomes an object in the final array that Tabletop.js delivers of your data. +Your Google Spreadsheet should be set up with row one as your column headers. Row two and beyond should be your data. Each header and row becomes an object in the final array that [Tabletop.js](https://github.com/jsoma/tabletop) delivers of your data. ![sheetsee](https://raw.github.com/jllord/sheetsee-cache/master/img/nonos.png) @@ -72,18 +75,17 @@ There shouldn't be any breaks or horizontal organization in the spreadsheet. But ![sheetsee](https://raw.github.com/jllord/sheetsee-cache/master/img/hexcolors.png) -You must add a column to your spreadsheet with the heading _hexcolor_ (case insensitive). The maps, charts and such use colors and this is the easiest way to standardize that. The color scheme is up to you, all you need to do is fill the column with hexidecimal color values. This [color picker](http://color.hailpixel.com/) by [Devin Hunt](https://twitter.com/hailpixel) is really nice. #Funtip: Coloring the background of the cell it's hexcolor brings delight! +You can add a column to your spreadsheet with the heading _hexcolor_ (case insensitive) and add colors to be used in maps. The color scheme is up to you, all you need to do is fill the column with hexidecimal color values. This [color picker](http://color.hailpixel.com/) by [Devin Hunt](https://twitter.com/hailpixel) is really nice. #Funtip: Coloring the background of the cell it's hexcolor brings delight! ### Geocoding -If you intend to map your data and only have addresses you'll need to geocode the addresses into lat/long coordinates. Mapbox built a [plugin](http://mapbox.com/tilemill/docs/guides/google-docs/#geocoding) - that does this for you in Google Docs. You can also use websites like [latlong.net](http://www.latlong.net/) to get the coordinates and paste them into rows with column headers _lat_ and _long_. +If you intend to map your data and only have addresses you'll need to geocode the addresses into lat/long coordinates. Mapbox built a [plugin](http://mapbox.com/tilemill/docs/guides/google-docs/#geocoding) that does this for you in Google Docs. You can also use websites like [latlong.net](http://www.latlong.net) to get the coordinates and paste them into rows with column headers _lat_ and _long_. ### Publishing Your Spreadsheet ![sheetsee](https://raw.github.com/jllord/sheetsee-cache/master/img/publish.png) -You need to do this in order to generate a unique key for your spreadsheet, which Tabletop.js will use to get your spreadsheet data. In your Google Spreadsheet, click _File_ > _Publish to the Web_. Then in the next window click _Start Publishing_; it will then turn into a _Stop Publishing_ button. +You need to do this in order to generate a unique key for your spreadsheet, which [Tabletop.js](https://github.com/jsoma/tabletop) will use to get your spreadsheet data. In your Google Spreadsheet, click _File_ > _Publish to the Web_. Then in the next window click _Start Publishing_; it will then turn into a _Stop Publishing_ button. ![sheetsee](https://raw.github.com/jllord/sheetsee-cache/master/img/key.png) @@ -91,4 +93,4 @@ You should have an address in a box at the bottom, your key is the portion betwe ### CSS -Sheetsee.js comes with a bare minimum stylesheet, `sss.css`, which contains elements you'll want to style when using the feature they correspond to. +Sheetsee.js comes with a bare minimum stylesheet, `sss.css`, which contains elements you'll want to style when using the feature they correspond to. Use it if you want, or as a starting off point to create your own styles. diff --git a/site/docs/building.html b/docs/building.html similarity index 53% rename from site/docs/building.html rename to docs/building.html index 0d8fca7..13bc022 100644 --- a/site/docs/building.html +++ b/docs/building.html @@ -7,8 +7,8 @@ - - + + @@ -16,16 +16,15 @@

Right-sizing

-

You can customize your sheetsee.js build with just the parts you want to use. If you want to just use the full version, you can grab it here at github.com/jlord/sheetsee.js.

-

All bundle comes with mapbox.js and handlebars.js (since both are available on NPM). Additionally you'll need to also include tabletop.js and jQuery in your HTML head like so:

-
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/tabletop.js/1.1.0/tabletop.min.js"></script>
+

You can customize your Sheetsee build with just the parts you want to use, for example to only include the mapping module or only the tables module. If you want to just use the full version, you can grab it here at github.com/jlord/sheetsee.js.

+

All builds come with Mustache.js and Leaflet.js. Additionally, you'll need to link to Tabletop.js your HTML head like so:

+
<script src="https://cdnjs.cloudflare.com/ajax/libs/tabletop.js/1.5.1/tabletop.min.js"></script>
+<script src="js/sheetsee.js"></script>
 
-

To build your Sheetsee you'll need Node.js and NPM (the latter comes with the former in most installs) on your computer and a command line.

-

Get Node/NPM

-

Download Node.js from nodejs.org/download. For most users you can just download the Mac .pkg or Windows .msi. Follow the install instructions, both include NPM. Once they're installed, proceed:

-

Install sheetsee from NPM

-

The sheetsee (with no '.js') module is the tool for building custom Sheetsee.js builds. Install sheetsee globally and then run it within the folder of your soon-to-be sheetsee.js project.

+

To build a custom Sheetsee you'll need Node.js on your computer familiarity with the command line.

+

Download Node.js from nodejs.org/download. For most users you can just download the Mac .pkg or Windows .msi. Follow the install instructions; both include npm. Then install sheetsee.

+

Install sheetsee from npm

+

The sheetsee (with no '.js') module is the tool for building custom Sheetsee builds. Install sheetsee globally and then run it within the folder of your soon-to-be Sheetsee project.

Install globally

npm install -g sheetsee
 
@@ -36,11 +35,10 @@

Install sheetsee from NPM

  • -m or -maps for maps
  • -t or -tables for tables
  • -
  • -c or -charts for charts
  • --save to write out the file*
-

* otherwise, defaults to standardout on your console which you can | pbcopy

-

So for instance, sheetsee -m -t --save will build you a Sheetsee.js with the basic data functions, the map and tables sections built in and save it as a file named sheetsee.js. Running sheetsee -m -t | pbcopy will save the output to your clipboard.

+

* otherwise, defaults to standard out on your console which you can | pbcopy

+

So for instance, sheetsee -m --save will build you a Sheetsee with the basic data functions and the map section, leaving out the tables section. It will save it as a file named 'sheetsee.js'. Running sheetsee -m | pbcopy will save the output to your clipboard.

diff --git a/docs/building.md b/docs/building.md index 77822a6..84c15cf 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,22 +1,20 @@ # Right-sizing -You can customize your sheetsee.js build with just the parts you want to use. If you want to just use the full version, you can grab it here at [github.com/jlord/sheetsee.js](https://github.com/jlord/sheetsee.js/blob/master/js/sheetsee.js). +You can customize your Sheetsee build with just the parts you want to use, for example to only include the mapping module or only the tables module. If you want to just use the full version, you can grab it here at [github.com/jlord/sheetsee.js](https://github.com/jlord/sheetsee.js/blob/master/js/sheetsee.js). -All bundle comes with [mapbox.js]() and [handlebars.js]() (since both are available on [NPM](http://www.npmjs.org)). Additionally you'll need to also include [tabletop.js](https://github.com/jsoma/tabletop) and [jQuery](http://www.jquery.com) in your HTML head like so: +All builds come with [Mustache.js](https://mustache.github.io) and [Leaflet.js](http://leafletjs.com). Additionally, you'll need to link to [Tabletop.js](https://github.com/jsoma/tabletop) your HTML head like so: ```HTML - - + + ``` -**To build your Sheetsee you'll need [Node.js](http://www.nodejs.org) and [NPM](http://www.npmjs.org) (the latter comes with the former in most installs) on your computer and a command line.** +**To build a custom Sheetsee you'll need [Node.js](http://www.nodejs.org) on your computer familiarity with the command line.** -#### Get Node/NPM +Download Node.js from [nodejs.org/download](http://nodejs.org/download). For most users you can just download the Mac _.pkg_ or Windows _.msi_. Follow the install instructions; both include npm. Then install `sheetsee`. -Download Node.js from [nodejs.org/download](http://nodejs.org/download). For most users you can just download the Mac _.pkg_ or Windows _.msi_. Follow the install instructions, both include NPM. Once they're installed, proceed: - -## Install `sheetsee` from NPM -The `sheetsee` (with no '.js') module is the tool for building custom Sheetsee.js builds. Install `sheetsee` globally and then run it within the folder of your soon-to-be sheetsee.js project. +## Install `sheetsee` from npm +The `sheetsee` (with no '.js') module is the tool for building custom Sheetsee builds. Install `sheetsee` globally and then run it within the folder of your soon-to-be Sheetsee project. _Install globally_ @@ -34,9 +32,8 @@ Here are the options for the different modules. If you want save the generated f - `-m` or `-maps` for maps - `-t` or `-tables` for tables -- `-c` or `-charts` for charts - `--save` to write out the file* -_* otherwise, defaults to standardout on your console which you can_ `| pbcopy` +_* otherwise, defaults to standard out on your console which you can_ `| pbcopy` -So for instance, `sheetsee -m -t --save` will build you a Sheetsee.js with the basic **data** functions, the **map** and **tables** sections built in and save it as a file named **sheetsee.js**. Running `sheetsee -m -t | pbcopy` will save the output to your clipboard. +So for instance, `sheetsee -m --save` will build you a Sheetsee with the basic **data** functions and the **map** section, leaving out the tables section. It will save it as a file named '**sheetsee.js**'. Running `sheetsee -m | pbcopy` will save the output to your clipboard. diff --git a/site/docs/changelog.html b/docs/changelog.html similarity index 88% rename from site/docs/changelog.html rename to docs/changelog.html index 0186e5d..57edebf 100644 --- a/site/docs/changelog.html +++ b/docs/changelog.html @@ -7,16 +7,22 @@ - - + +
-

Sheetsee v3

-

May 10, 2014

+

March 2017 Sheetsee v4

+

sheetsee-core changes

+

Methods for charts have been removed:

+
    +
  • Sheetsee.makeColorArrayOfObject(data, colors)
  • +
  • Sheetsee.addUnitsLabels(arrayObj, oldLabel, oldUnits)
  • +
+

May 10, 2014, Sheetsee v3

Better Table Template Options

Updated sheetsee-tables to allow you to re-use a template (rather than duplicating it for each different table you wanted to create). Previously it assumed your HTML table div id matched your script template id. This means that you can pass in an extra key/value pair in your table options into Sheetsee.maketable(). The new pair it takes is: "templateID" : "yourtemplateid". Example below, full sheetsee-tables documentation here.

var tableOptions = {
@@ -59,29 +65,26 @@ 

Getting Started

Ideas

Demos

Use

Contact

-

Home Page

+

Home

diff --git a/docs/changelog.md b/docs/changelog.md index 25a8b31..44685fb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,13 @@ -### Sheetsee v3 +### March 2017 Sheetsee v4 -## May 10, 2014 +sheetsee-core changes + +Methods for charts have been removed: +- `Sheetsee.makeColorArrayOfObject(data, colors)` +- `Sheetsee.addUnitsLabels(arrayObj, oldLabel, oldUnits)` + + +## May 10, 2014, Sheetsee v3 ### Better Table Template Options Updated `sheetsee-tables` to allow you to re-use a template (rather than duplicating it for each different table you wanted to create). Previously it assumed your HTML table `div` id matched your script template `id`. This means that you can pass in an extra key/value pair in your table options into `Sheetsee.maketable()`. The new pair it takes is: `"templateID" : "yourtemplateid"`. Example below, full `sheetsee-tables` documentation [here](docs/sheetsee-tables.html). diff --git a/docs/custom-charts.md b/docs/custom-charts.md deleted file mode 100644 index 53476a6..0000000 --- a/docs/custom-charts.md +++ /dev/null @@ -1,69 +0,0 @@ -# Custom Charts - -It's easy to take a [D3.js](http://d3js.org/) chart of your own and use it with Sheetsee.js. If you make it into a module, anyone can use your chart, too! - -Sheetsee charts currently work by taking in some options, like so: - -```javascript -var pieOptions = {labels: "name", units: "units", m: [80, 80, 80, 80], w: 600, h: 400, div: "#pieChart", hiColor: "#14ECC8"} -``` - -The _labels_ represent the actual thing you're charting and _units_ are how many of those things. Margin, width and height are _m, w, h_ and the `
` to build your chart in is _div_. Finally, you can supply a highlight color if you want. - -So, your chart could take the same options, but map them into your D3 code with the correct variables. An example from [maxogden/sheetsee-d3bubble](https://github.com/maxogden/sheetsee-d3bubble): - - -_Append the d3.js code with a map of your sheetsee options_ - -```JavaScript -Sheetsee.d3BubbleChart = function(data, options) { - var tree = {name: "data", children: []} - var groups = {} - - // data needs to look like this: - // var data = { name: "wahtever", children: [ - // { name: "group1", children: [ - // { name: 'bob', size: 3}, - // { name: 'judy', size: 5} - // ]}, - // { name: "group2", children: [ - // { name: 'jim', size: 10}, - // { name: 'bill', size: 5} - // ]} - // ]} - - data.map(function(r) { - var groupName = r[options.group] - groups[groupName] = true - }) - - Object.keys(groups).map(function(groupName) { - var groupMembers = [] - data.map(function(r) { - if (r[options.group] !== groupName) return - groupMembers.push({name: r[options.name], size: r[options.size]}) - }) - tree.children.push({name: groupName, children: groupMembers}) - }) - - // the rest of the code -``` - -_In your HTML call it like so_ - -```JavaScript - -``` - -There are lots of charts to get excited about in the [D3 gallery](https://github.com/mbostock/d3/wiki/Gallery). - -_View the [entire source](https://github.com/maxogden/sheetsee-d3bubble)_ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..575b3d4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,46 @@ +![sheetseeimg](img/next-sheetsee.png) + +**Sheetsee.js** is a client-side library for connecting Google Spreadsheets to a website and visualizing the information in tables and maps. + +
+ +## Spreadsheets!? + +Google Spreadsheets can be used as simple and collaborative databases, they make getting a data driven site going much easier than traditional databases. [Read more](./docs/basics.md) about using spreadsheets for databases. + +## Modules + +Each of Sheetsee's functions are divided into modules. Use just the parts you need; see docs on [building](./docs/building.md). If you don't want to build your own, you can just use the full library which includes all modules, it's [here on GitHub](http://www.github.com/jlord/sheetsee.js). + +| Module | Contains | Docs | +| ------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------- | +| **sheetsee** | Command line module for make a custom build of Sheetsee. | [Doc](./docs/building.md) | +| **sheetsee-core** | **Included in all builds**. Has helpful working-with-your-data functions. | [Doc](./docs/sheetsee-core.md) | +| **sheetsee-tables** | Contains everything you'll need to create a table including sortable columns, pagination and search.| [Doc](./docs/sheetsee-tables.md) | +| **sheetsee-maps** | For making maps with your point, line or polygon spreadsheet data. Built with Leaflet.js. | [Doc](./docs/sheetsee-maps.md) | + +
New News!
+ +## Get your Spreadsheet as JSON or Try Sheetsee with Glitch! + +The [spreadsheet.glitch.me](https://spreadsheet.glitch.me) site will give you an endpoint to use that will return your spreadsheet to you as JSON. The [sheetsee.glitch.me](https://sheetsee.glitch.me) site provides template to get started with Sheetsee; it's already set up with a server so that your data is backed up. + +### Or Fork a Site! + +There are site templates hooked up to Sheetsee that are ready to be forked on GitHub and used by you, check out the [Fork-n-go site](http://jlord.us/forkngo). + +## Sheetsee Updates + +Sheetsee has just been re-written and there are some breaking changes. Also some nice ones, like dependencies removed. The API for maps with Sheetsee have changed, [see the docs](https://github.com/jlord/sheetsee-maps#sheetseeloadmapoptions). + +
+ +## Resources & Documentation + +More resources on using Sheetsee: + +| Getting Started | Ideas | Demos | Use | +| --- | --- | --- | --- | +| [About Sheetsee](./docs/about.md) | [Templates](./docs/templates.md) | [Table Demo](./demos/demo-table.html) | [Sheetsee-core](./docs/sheetsee-core.md) | +| [Building Sheetsee](./docs/building.md) | [Tips!](./docs/tips.md) | [Map Demo](./demos/demo-map.html) | [Sheetsee-tables](./docs/sheetsee-tables.md) | +| [Basics](./docs/basics.md) | | | [Sheetsee-maps](./docs/sheetsee-maps.md) | diff --git a/docs/sheetsee-charts.md b/docs/sheetsee-charts.md deleted file mode 100644 index 98ea251..0000000 --- a/docs/sheetsee-charts.md +++ /dev/null @@ -1,145 +0,0 @@ -# Sheetsee-charts - -_[View Demo](../demos/demo-chart.html)_ - -Sheetsee.js provides three [D3.js](http://d3js.org/) chart options to use with your spreadsheet data: a bar chart, line graph and pie chart. You can also use a custom D3 chart with Sheetsee, read about that [here](custom-charts.md). - -## Make a Chart - -Each chart requires your data be an _array of objects_, with objects containing `label` and `units` key/value pairs. - -Experiment with the charts to find the correct size your `
` will need to be to hold the chart with your data in it nicely. - -You can also make your own D3 chart in a separate .js file, link to that in your HTML head and pass your data on to it after Tabletop.js returns. Information [here](custom-charts.md) on using your own chart. - -### Bar Chart - -To create a bar chart you'll need to add a placeholder `
` in your HTML with an id. - -```HTML -
-``` - -In your CSS, give it dimensions. - -```CSS -#barChart {height: 400px; max-width: 600px; background: #F8CDCD;} -``` - -You'll also have these CSS elements to style however you'd like: - -```CSS -.labels text {text-align: right;} -.bar .labels text {fill: #333;} -.bar rect {fill: #e6e6e6;} -.axis {shape-rendering: crispEdges;} -.x.axis line {stroke: #fff; fill: none;} -.x.axis path {fill: none;} -.x.axis text {fill: #333;} -.xLabel {font-family: sans-serif; font-size: 9px;} -``` - -In a ` + + + + + + +
+

sheetsee-core

+

This module is included in every Sheetsee build. It contains methods for basic data manipulation you might want to do.

+

Working With Your Data

+

Sheetsee pairs with Tabletop.js which will fetch the data from your spreadsheet and return it as an array of objects. You'll use these methods from Sheetsee after you have that data.

+

Methods

+

Here are the functions you can use!

+

Sheetsee.getKeywordCount(data, keyword)

+
    +
  • data array of objects
  • +
  • keyword string
  • +
  • Returns number
  • +
+

Given your data and keyword to search by, this function returns the number of times it occurs throughout all of the data.

+
getGroupCount(data, 'cat')
+// returns a number
+
+

Sheetsee.getKeyword(data, keyword)

+
    +
  • data array of objects
  • +
  • keyword string
  • +
  • Returns number
  • +
+

Given your data and a keyword to search by, this function returns every row which contains a match to the keyword.

+
getKeyword(data, 'cat')
+// returns array of objects
+
+

Sheetsee.getColumnTotal(data, column)

+
    +
  • data array of objects
  • +
  • column string
  • +
  • Returns number
  • +
+

Use only with columns of numbers

+

Given your data and column header, this function sums each cell in that column and returns the value.

+
getColumnTotal(data, 'cuddlability')
+// returns number
+
+

Sheetsee.getColumnAverage(data, column)

+
    +
  • data array of objects
  • +
  • column string
  • +
  • Returns number
  • +
+

Given your data and column header, this function returns the average value of every cell in the column.

+
getColumnAverage(data, 'cuddlability')
+// returns number
+
+

Sheetsee.getMin(data, column)

+
    +
  • data array of objects
  • +
  • column string
  • +
  • Returns array
  • +
+

Given your data and column header, this function returns an array of the rows with the lowest values within the specified column.

+
getMin(data, 'cuddlability')
+// returns array
+
+

Sheetsee.getMax(data, column)

+
    +
  • data array of objects
  • +
  • column string
  • +
  • Returns array
  • +
+

Given your data and column header, this function returns an array of the rows with the highest values within the specified column.

+
getMin(data, 'cuddlability')
+// returns array of objects
+
+

Sheetsee.getMatches(data, filter, column)

+
    +
  • data array of objects
  • +
  • filter string
  • +
  • column string
  • +
  • Returns array
  • +
+

Takes data, a filter term to search by within a column and returns every row that matches,

+
getMatches(data, 'dog', 'kind')
+// returns array of objects
+// [{'name': 'coco', 'kind': 'dog'...}, {'name': 'wolfgang', 'kind': 'dog'...},{'name': 'cooc', 'kind': 'dog'...} ]
+
+

Sheetsee.getOccurance(data, column)

+
    +
  • data array of objects
  • +
  • column string
  • +
  • Returns object
  • +
+

Takes data column header and returns an object with key/value pairs of how often an item occurs in the column.

+
getOccurance(data, 'kind')
+// Returns an object
+// {'dog': 3, 'cat': 3}
+
+

Math

+

Don't Forget JavaScript Math! Create variables that are the sums, differences, multiples and so forth of others. Lots of info on that here on MDN.

+
var profit09 = Sheetsee.getColumnTotal(data, '2009')
+var profit10 = Sheetsee.getColumnTotal(data, '2010')
+var difference = profit09 - profit10
+
+ + + +
+ + + diff --git a/docs/sheetsee-core.md b/docs/sheetsee-core.md index 1d76b33..d1be477 100644 --- a/docs/sheetsee-core.md +++ b/docs/sheetsee-core.md @@ -1,135 +1,130 @@ -# Sheetsee-core +# sheetsee-core -This is the core module in sheetsee and is included in all builds. It contains the functions for building your custom file as well as the basic data manipulation functions. +This module is included in every Sheetsee build. It contains methods for basic data manipulation you might want to do. ## Working With Your Data -Tabletop.js will fetch the data from your spreadsheet and return it as an _array of objects_. Sheetsee.js has functions built in to help you filter or reorganize the data if you'd like. +Sheetsee pairs with [Tabletop.js](https://github.com/jsoma/tabletop) which will fetch the data from your spreadsheet and return it as an _array of objects_. You'll use these methods from Sheetsee after you have that data. -### Sheetsee.getGroupCount(data, groupTerm) +## Methods -This takes in your data, an _array of objects_, and searches for a _string_, **groupTerm**, in each piece of your **data** (formerly the cells of your spreadsheet). It returns the number of times it found the **groupTerm**. +Here are the functions you can use! -```JAVASCRIPT -getGroupCount(data, "cat") -// returns a number -``` +### `Sheetsee.getKeywordCount(data, keyword)` -### Sheetsee.getColumnTotal(data, column) +- `data` _array of objects_ +- `keyword` _string_ +- Returns _number_ -Given your **data**, an _array of objects_, and a _string_ **column** header, this functions sums each cell in that column(so this collumn you mention best have numbers). +Given your **data** and **keyword** to search by, this function returns the number of times it occurs throughout all of the data. -```JAVASCRIPT -getColumnTotal(data, "cuddlability") -// returns number +```javascript +getGroupCount(data, 'cat') +// returns a number ``` -### Sheetsee.getAveragefromColumn(data, column) - -A really simple function that builds on `getColumnTotal()` by returning the average number in a **column** of numbers. - -```JAVASCRIPT -getColumnAverage(data, "cuddlability") -// returns number -``` +### `Sheetsee.getKeyword(data, keyword)` -### Sheetsee.getMin(data, column) +- `data` _array of objects_ +- `keyword` _string_ +- Returns _number_ -This will return an _array_ of _object_ or _objects_ (if there is a tie) of the element with the lowest _number_ value in the **column** you specify from your **data**. +Given your **data** and a **keyword** to search by, this function returns every row which contains a match to the keyword. -```JAVASCRIPT -getMin(data, "cuddlability") -// returns array +```javascript +getKeyword(data, 'cat') +// returns array of objects ``` -### Sheetsee.getMax(data, column) +### `Sheetsee.getColumnTotal(data, column)` -This will return an _array_ of _object_ or _objects_ (if there is a tie) of the element with the highest _number_ value in the **column** you specify from your **data**. - -```JAVASCRIPT -getMin(data, "cuddlability") -// returns array -``` +- `data` _array of objects_ +- `column` _string_ +- Returns _number_ -### Don't Forget JavaScript Math +_Use only with columns of numbers_ -Create variables that are the sums, differences, multiples and so forth of others. Lots of info on that [here on MDN](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math). +Given your **data** and **column** header, this function sums each cell in that column and returns the value. -```JAVASCRIPT -var profit09 = Sheetsee.getColumnTotal(data, "2009") -var profit10 = Sheetsee.getColumnTotal(data, "2010") -var difference = profit09 - profit10 +```javascript +getColumnTotal(data, 'cuddlability') +// returns number ``` -#### What These Little Bits are Good For +### `Sheetsee.getColumnAverage(data, column)` -You don't have to just create tables of your data. You can have other portions of your page that show things like, "The difference taco consumption between last week and this week is..." These are easy to create with JavaScript math functions and knowing a little bit more about [icanhaz.js](http://icanhazjs.com/). +- `data` _array of objects_ +- `column` _string_ +- Returns _number_ -### Sheetsee.getMatches(data, filter, category) +Given your **data** and **column** header, this function returns the average value of every cell in the column. -Takes **data** as an _array of objects_, a _string_ you'd like to **filter** and a _string_ of the **category** you want it to look in (a column header from your spreadsheet). - -```JAVASCRIPT -getMatches(data, "dog", "kind") +```javascript +getColumnAverage(data, 'cuddlability') +// returns number ``` -Returns an _array of objects_ matching the category's filter. - -```JAVASCRIPT -[{"name": "coco", "kind": "dog"...}, {"name": "wolfgang", "kind": "dog"...},{"name": "cooc", "kind": "dog"...} ] -``` +### `Sheetsee.getMin(data, column)` -### Sheetsee.getOccurance(data, category) +- `data` _array of objects_ +- `column` _string_ +- Returns _array_ -Takes **data** as an _array of objects_ and a _string_ for **category** (a column header from your spreadsheet) you want tally how often an element occured. +Given your **data** and **column** header, this function returns an array of the rows with the lowest values within the specified column. -```JAVASCRIPT -getOccurance(data, "kind") +```javascript +getMin(data, 'cuddlability') +// returns array ``` -Returns an object with keys and values for each variation of the category and its occurance. - -```JAVASCRIPT -{"dog": 3, "cat": 3} -``` +### `Sheetsee.getMax(data, column)` -### Sheetsee.makeColorArrayOfObject(data, colors) +- `data` _array of objects_ +- `column` _string_ +- Returns _array_ -If you use `getOccurance()` and want to then chart that data with d3.js, you'll need to make it into an _array_ (instead of an object) and add colors back in (since the hexcolor column applies to the datapoints in your original dataset and not this new dataset). +Given your **data** and **column** header, this function returns an array of the rows with the highest values within the specified column. -This function takes in your data, as an _object_, and an _array_ of hexidecimal color strings which you define. +```javascript +getMin(data, 'cuddlability') +// returns array of objects +``` -```JAVASCRIPT -var kinds = getOccurance(data, "kind") -var kindColors = ["#ff00ff", "#DCF13C"] +### `Sheetsee.getMatches(data, filter, column)` -var kindData = makeColorArrayOfObjects(mostPopBreeds, breedColors) -``` +- `data` _array of objects_ +- `filter` _string_ +- `column` _string_ +- Returns _array_ -It will return an array of objects formatted to go directly into a d3 chart with the appropriate _units_ and _label keys_, like so: +Takes **data**, a **filter** term to search by within a **column** and returns every row that matches, -```JAVASCRIPT -[{"label": "dog", "units": 2, "hexcolor": "#ff00ff"}, {"label": "cat", "units": 3, "hexcolor": "#DCF13C"}] +```javascript +getMatches(data, 'dog', 'kind') +// returns array of objects +// [{'name': 'coco', 'kind': 'dog'...}, {'name': 'wolfgang', 'kind': 'dog'...},{'name': 'cooc', 'kind': 'dog'...} ] ``` -If you pass in an array of just one color it will repeat that color for all items. If you pass fewer colors than data elements it will repeat the sequences of colors for the remainder elements. +### `Sheetsee.getOccurance(data, column)` -### Sheetsee.addUnitsLabels(arrayObj, oldLabel, oldUnits) +- `data` _array of objects_ +- `column` _string_ +- Returns _object_ -If you're using data, the data directly from Tabletop, you'll need to format it before you use the d3 charts. You'll need to determine what part of your data you want to chart - what will be your label, what your charting, and what will be your units, how many of them are there (this should be a number). +Takes **data** **column** header and returns an object with key/value pairs of how often an item occurs in the column. ```JAVASCRIPT -var data = [{"name": "coco", "kind": "dog", "cuddablity": 5}, {"name": "unagi", "kind": "cat", "cuddlability": 0}] +getOccurance(data, 'kind') +// Returns an object +// {'dog': 3, 'cat': 3} ``` -For istance, if from our original data above we want to chart the age of each cat, we'll use: +### Math -```JAVASCRIPT -Sheetsee.addUnitsLabels(data, "name", "cuddlability") -``` - -Which will return an array, ready for the d3 charts: +Don't Forget JavaScript Math! Create variables that are the sums, differences, multiples and so forth of others. Lots of info on that [here on MDN](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math). -```JAVASCRIPT -[{"label": "coco", "kind": "dog", "units": 5}, {"label": "unagi", "kind": "cat", "units": 0}] +```javascript +var profit09 = Sheetsee.getColumnTotal(data, '2009') +var profit10 = Sheetsee.getColumnTotal(data, '2010') +var difference = profit09 - profit10 ``` diff --git a/docs/sheetsee-maps.html b/docs/sheetsee-maps.html new file mode 100644 index 0000000..a8fe451 --- /dev/null +++ b/docs/sheetsee-maps.html @@ -0,0 +1,118 @@ + + + + + Sheetsee.js + + + + + + + + + + + +
+

sheetsee-maps

+

Sheetsee uses this module to handle maps in your projects. This module uses (and includes) Leaflet.js to make maps of your points, polygons, lines or multipolygons (all coordinate based). Details on what that actually looks like here. It uses (and includes) Mustache.js templates for marker popups.

+

You'll need to include Leaflet's map CSS in your HTML's head:

+
<head>
+  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" />
+</head>
+
+

Maps: Polygons and Lines

+

Sheetsee-maps supports polygons and lines; so long as you have the correct coordinate structure in your cells. More details for coordinates of lines and polygons in geoJSON are here, but briefly:

+

Must use lowercase spreadsheet column headers: 'lat' and 'long'.

+

A linestring:

+
[-122.41722106933594, 37.7663045891584], [-122.40477561950684, 37.77695634643178]
+
+

A polygon:

+
[-122.41790771484375, 37.740381166384914], [-122.41790771484375, 37.74520008134973], [-122.40966796874999, 37.74520008134973],[-122.40966796874999, 37.740381166384914], [-122.41790771484375, 37.740381166384914]
+
+

A Multipolygon:

+
[[-122.431640625, 37.79106586542567], [-122.431640625, 37.797441398913286], [-122.42666244506835, 37.797441398913286],[-122.42666244506835, 37.79106586542567], [-122.431640625, 37.79106586542567]],
+[[-122.43352890014648, 37.78197638783258], [-122.43352890014648, 37.789031004883654], [-122.42443084716797, 37.789031004883654], [-122.42443084716797, 37.78197638783258], [-122.43352890014648, 37.78197638783258]]
+
+

To Use

+

This module is used as a part of Sheetsee.js. You can download the full version or build your own with a command line tool.

+

You'll create a little bit of HTML and then some JavaScript in your HTML to use this. You can customize marker color, popup content and enable/disable clustering in your map.

+

Methods

+

Here are the functions you can use!

+

Sheetsee.createGeoJSON(data, optionsJSON)

+
    +
  • data JSON array of data
  • +
  • optionsJSON array of strings, spreadsheet column title
  • +
+

If you'd like to just generate geoJSON from a spreadsheet you can use this method.

+

This takes in your spreadsheet data in JSON format (which you can get with Tabletop.js)and the parts of your data, optionsJSON, that you plan on including in your map's popups. These will be column headers in your spreadsheet in an array of strings.

+

If you're not going to have popups on your markers, don't worry about it then and just pass in your data (by default it will use all the row's information).

+
var optionsJSON = ['name', 'breed', 'cuddlability']
+var geoJSON = Sheetsee.createGeoJSON(data, optionsJSON)
+
+

It will return an array in the special geoJSON format that map making things love.

+

Sheetsee.loadMap(options)

+

This method will generate a map for you on the page (it also generates the geoJSON for the map).

+
    +
  • options object required
      +
    • data your spreadsheet data array required
    • +
    • mapDiv the id of the div in your HTML to contain the map required
    • +
    • geoJSONincludes array of strings of column headers to include in popups
    • +
    • template HTML/Mustache template for popups
    • +
    • cluster a true/false boolean, do you want your markers clustered
    • +
    • hexcolor pick one color for your markers
    • +
    +
  • +
+
var mapOptions = {
+  data: data, // required
+  mapDiv: 'map', //required
+  geoJSONincludes: ['Name', 'Animal', 'Rating'], // optional
+  template: '<p>{{Name}}—{{Animal}}—{{Rating}}</p>', // optional
+  cluster: true, // optional
+  hexcolor: '#e91e63' // optional
+}
+Sheetsee.loadMap(mapOptions)
+
+

Breaking Changes The latest version of Sheetsee replaces three methods ('loadMap', 'addTileLayer', 'addMarkerLayer') with one loadMap which takes in an object of map options.

+

Marker colors

+

If you create a column title hexcolor in your spreadsheet and fill each cell with hex color codes, those will be used to color your markers. If you define a color for hexcolor in the options you pass to your map it will override colors in the spreadsheet data.

+

View Demo +Visit Site

+ + + +
+ + + diff --git a/docs/sheetsee-maps.md b/docs/sheetsee-maps.md index 4e2d383..ee2bf7c 100644 --- a/docs/sheetsee-maps.md +++ b/docs/sheetsee-maps.md @@ -1,127 +1,97 @@ -# Sheetsee-maps +# sheetsee-maps -_[View Demo](../demos/demo-map.html)_ +Sheetsee uses this module to handle maps in your projects. This module uses (and includes) [Leaflet.js](http://leafletjs.com) to make maps of your **points**, **polygons**, **lines** or **multipolygons** (all coordinate based). Details on what that actually looks like [here](http://leafletjs.com/examples/geojson.html). It uses (and includes) [Mustache.js](https://mustache.github.io) templates for marker popups. -Sheetsee.js uses [Mapbox.js](http://mapbox.com/mapbox.js) and [Leaflet.js](http://leafletjs.com/) to make maps of your **points**, **polygons**, **lines** or **multipolygons** (all coordinate based). Details on what that actually looks like [here](http://leafletjs.com/examples/geojson.html). +You'll need to include Leaflet's map CSS in your HTML's head: -### Maps: Polygons and Lines +```html + + + +``` -Sheetsee-maps now supports polygons and lines. So long as you have the correct coordinate structure in your cells, Sheetsee will add them to the geoJSON it creates for your maps. More details for coordinates of lines and polygons in geoJSON are [here](http://leafletjs.com/examples/geojson.html), but briefly: +## Maps: Polygons and Lines -A linestring: +Sheetsee-maps supports polygons and lines; so long as you have the correct coordinate structure in your cells. More details for coordinates of lines and polygons in geoJSON are [here](http://leafletjs.com/examples/geojson.html), but briefly: -``` +**Must use lowercase spreadsheet column headers: 'lat' and 'long'.** + +**A linestring:** + +```text [-122.41722106933594, 37.7663045891584], [-122.40477561950684, 37.77695634643178] ``` -A polygon: +**A polygon:** -``` +```text [-122.41790771484375, 37.740381166384914], [-122.41790771484375, 37.74520008134973], [-122.40966796874999, 37.74520008134973],[-122.40966796874999, 37.740381166384914], [-122.41790771484375, 37.740381166384914] ``` -A Multipolygon: +**A Multipolygon:** -``` +```text [[-122.431640625, 37.79106586542567], [-122.431640625, 37.797441398913286], [-122.42666244506835, 37.797441398913286],[-122.42666244506835, 37.79106586542567], [-122.431640625, 37.79106586542567]], [[-122.43352890014648, 37.78197638783258], [-122.43352890014648, 37.789031004883654], [-122.42443084716797, 37.789031004883654], [-122.42443084716797, 37.78197638783258], [-122.43352890014648, 37.78197638783258]] ``` -### The Parts +## To Use -You'll create a placeholder `
` in your HTML, CSS giving it a size and fire up a map from within ` -``` +_[View Demo](http://jlord.us/sheetsee.js/demos/demo-maps.html)_ +_[Visit Site](http://jlord.us/sheetsee.js)_ diff --git a/site/docs/sheetsee-tables.html b/docs/sheetsee-tables.html similarity index 50% rename from site/docs/sheetsee-tables.html rename to docs/sheetsee-tables.html index 1c7f5fb..7ee2c6b 100644 --- a/site/docs/sheetsee-tables.html +++ b/docs/sheetsee-tables.html @@ -7,47 +7,57 @@ - - + +
-

Sheetsee-tables

-

With this module you can create tables of your data that are sortable, searchable and paginate-able.

-

You'll need a placeholder <div> in your html, a <script> mustache template and a <script> that initiates the table.

+

sheetsee-tables

+

Sheetsee,js uses this module to make tables. With this module you can create tables with your spreadsheet data that are sortable, searchable and paginate-able.

+

You'll need a placeholder <div> in your html, a <script> with a Mustache.js template and a <script> that tells Sheetsee to build the table.

Your HTML Placeholder

-

This is as simple as an empty <div> with an id.

+

This is as simple as an empty <div> with an id.

Your Template

-

Your template is the mockup of what you'd like your table to look like and what content it should show. The style is up to you! It is an HTML template inside of <script> tags. The id of the template should be the same as the HTML placeholder it corresponds to but with "_template" on the end. Unless you're using one template, for multiple divs, in which case, you'll pass in the template name in the options of makeTable()

+

Your template is the mockup of what you'd like your table to look like and what content it should show. The style is up to you! It is a mustache template inside of <script> tags.

+

The id of the template should be the same as the HTML placeholder it corresponds to but with "_template" on the end.

Sorting

If you want users to be able to click on headers and sort that column, your template must include table headers with the class tHeader.

You can then style .tHeader in your CSS to make them look how you want.

+

You must also make the inner text of your table headers have the same capitalization as in your spreadsheet. It's ok to have spaces in your table header but don't use spaces in your spreadsheet headers.

+
    +
  • Spreadsheet column name: 'PlaceName'
      +
    • OK table header: 'Place Name'
    • +
    • Not OK table header: 'PLACENAME', 'placename'
    • +
    +
  • +

Your Script

-

You'll want to set your table options and pass them into Sheetsee.makeTable(). If you want to add a search/filter, pass your options into Sheetsee.initiateTableFilter()

-

Funtions

-

Sheetsee.makeTable(tableOptions)

+

You'll want to set your table options and pass them into Sheetsee.makeTable(). If you want to add a search/filter, pass your options into Sheetsee.initiateTableFilter().

+

Methods

+

Functions for you to use! There are just two, woo!

+

Sheetsee.makeTable(tableOptions)

You pass in an object containing:

    -
  • data your data array
  • -
  • pagination how many rows displayed at one time, defaults to all
  • -
  • tableDiv the
    placeholder in your HTML
  • -
  • filterDiv the <div> containing your <input> filter if using search
  • -
  • templateID if you are reusing a template, use it's name here (if you don't include this, it will assume it matches tableDiv + _template)
  • +
  • data array your data from Tabletop.js required
  • +
  • pagination number how many rows displayed at one time, defaults to all
  • +
  • tableDiv string the <div> id placeholder in your HTML, includes the hash # required
  • +
  • filterDiv string the <div> id containing your <input> filter if using search, includes the hash # required if using filter
  • +
  • templateID string the id of your <script> tag with the template, defaults to assume it's the same as tableDiv + _template.
var tableOptions = {
-                    "data": gData,
-                    "pagination": 10,
-                    "tableDiv": "#fullTable",
-                    "filterDiv": "#fullTableFilter",
-                    "templateID": "fullTable"
-                    }
+  "data": data,
+  "pagination": 10,
+  "tableDiv": "#fullTable",
+  "filterDiv": "#fullTableFilter",
+  "templateID": "fullTable_template"
+}
 Sheetsee.makeTable(tableOptions)
 

Pagination

-

If you do not put in a number for pagination, by default it will show all of the data at once. With pagination, HTML will be added at the bottom of your table for naviagtion, which you can style in your CSS:

+

If you do not put in a number for pagination, by default it will show all of the data at once. With pagination, HTML will be added at the bottom of your table for navigation, which you can style in your CSS:

HTML

<div id='Pagination' currentPage class='table-pagination'>
   Showing page {{currentPage}} of {{totalPages}}
@@ -55,26 +65,23 @@ 

Pagination

</div>

CSS

-
#Pagination {background: #eee;}
-.pagination-next, .pagination-pre {cursor: pointer;}
-.no-pag {color: #acacac;}
+
#Pagination {}
+.pagination-next {}
+.pagination-pre {}
+.no-pag {}
 
- -

If you want to have an input to allow users to search/filter the data in the table, you'll add an input to your HTML. Give it an id and if you want, placeholder text:

+

Sheetsee.initiateTableFilter(tableOptions)

+

If you want to have an input to allow users to search/filter the data in the table, you'll add an input to your HTML. Give it an id and if you want add placeholder text. You'll also need to add a 'clear' button using the .clear CSS class.

<input id="tableFilter" type="text" placeholder="filter by.."></input>
+<a href="#" class=".clear">Clear</a>
 
-

Sheetsee.initiateTableFilter(tableOptions)

-

You will then call this function with your tableOptions to make that input live and connected to your table:

+

Then you'll pass your tableOptions object into this method:

Sheetsee.initiateTableFilter(tableOptions)
 
-

It will connect that input to your data as well as inject this HTML for a button, which you can style yourself in your CSS:

-
<span class="clear button">Clear</span>
-<span class="noMatches">no matches</span>
-

Example

HTML

-
<div id="siteTable"></div>
-<input id="siteTableFilter" type="text"></input>
+
<input id="siteTableFilter" type="text"></input><a href="#" class=".clear">Clear</a>
+<div id="siteTable"></div>
 

Template

<script id="tableTemplate" type="text/html">
@@ -88,38 +95,21 @@ 

Example

JavaScript

<script type="text/javascript">
-    document.addEventListener('DOMContentLoaded', function() {
-      var tableOptions = {
-                          "data": gData,
-                          "pagination": 10,
-                          "tableDiv": "#siteTable",
-                          "filterDiv": "#siteTableFilter",
-                          "templateID": "tableTemplate"
-                          }
-      Sheetsee.makeTable(tableOptions)
-      Sheetsee.initiateTableFilter(tableOptions)
-    })
-</script>
-
-

To create another table, simply repeat the steps above (abreviated here below).

-

HTML

-
<div id="secondTable"></div>
-<input id="secondFilter" type="text"></input>
-
-

Template

-
<script text="text/javascript" id="secondTable">
-  // Template here
-</script>
-
-

JavaScript

-
<script>
-  var secondTableOpts = {} // the options
-  Sheetsee.makeTable(secondTableOpts)
-  Sheetsee.initiateTableFilter(secondTableOpts)
+  document.addEventListener('DOMContentLoaded', function() {
+    var tableOptions = {
+      "data": data,
+      "pagination": 10,
+      "tableDiv": "#siteTable",
+      "filterDiv": "#siteTableFilter",
+      "templateID": "siteTable_template"
+    }
+    Sheetsee.makeTable(tableOptions)
+    Sheetsee.initiateTableFilter(tableOptions)
+  })
 </script>
 
-

Learn more about the things you can do with ICanHaz.js.

-

View Demo

+

View Demo +Visit Site

diff --git a/docs/sheetsee-tables.md b/docs/sheetsee-tables.md index d12a07d..0b6f702 100644 --- a/docs/sheetsee-tables.md +++ b/docs/sheetsee-tables.md @@ -1,16 +1,18 @@ -# Sheetsee-tables +# sheetsee-tables -With this module you can create tables of your data that are sortable, searchable and paginate-able. +Sheetsee,js uses this module to make tables. With this module you can create tables with your spreadsheet data that are sortable, searchable and paginate-able. -You'll need a placeholder `
` in your html, a ` ``` -To create another table, simply repeat the steps above (abreviated here below). - -_HTML_ -```HTML -
- -``` -_Template_ - -```JavaScript - -``` - -_JavaScript_ - -```JavaScript - -``` - -Learn more about the things you can do with [ICanHaz.js](http://icanhazjs.com). - -_[View Demo](/demos/demo-table.html)_ +_[View Demo](http://jlord.us/sheetsee.js/demos/demo-table.html)_ +_[Visit Site](http://jlord.us/sheetsee.js)_ diff --git a/site/docs/fork-n-go.html b/docs/templates.html similarity index 71% rename from site/docs/fork-n-go.html rename to docs/templates.html index 6eaa885..d425a83 100644 --- a/site/docs/fork-n-go.html +++ b/docs/templates.html @@ -7,26 +7,30 @@ - - + + - +
-

Fork-n-Go

+

Templates

+

Here are partially set up projects to get you going!

+

Glitch

+

+

The spreadsheet.glitch.me site will give you an endpoint to use that will return your spreadsheet to you as JSON. The sheetsee.glitch.me site provides template to get started with Sheetsee; it's already set up with a server so that your data is backed up.

+

Fork-n-Go

A Fork-n-Go project is a project on GitHub that in a few clicks, starting with a fork, gives another user a live website that they control with an easy to swap-for-your-own Google Spreadsheet database.

-

Two awesome things that make this possible: Forking, the tool on GitHub that allows you to copy a public repository onto your account, and GitHub Pages, GitHub's free web hosting service for ever repository, account and organization.

-

I've built a whole other website on the idea with lots of examples: jlord.github.io/forkngo

-
-

How

-

To have a website for a repository hosted by GitHub Pages all you need is a branch named gh-pages. GitHub will then look in that branch for web files and serve them up at the address.

-

So Sheetsee.js projects, hosted on gh-pages branches on GitHub, can easily be forked and connected to another spreadsheet giving another user a live website of their own—with data they control—really easily.

-

Example

+

Two awesome things that make this possible: Forking, the tool on GitHub that allows you to copy a public repository onto your account, and GitHub Pages, GitHub's free web hosting service for every repository, account and organization.

+

I've built a whole other website on the idea with lots of examples: jlord.github.io/forkngo

+

How

+

To have a website for a repository hosted by GitHub Pages all you need is to have webfiles uploaded and to tell GitHub the name of the branch you want it to host (in your repository's settings).

+

So Sheetsee.js projects, hosted on GitHub, can easily be forked and connected to another spreadsheet giving another user a live website of their own—with data they control—really easily.

+

Example

A Fork-n-Go example from my blog post on the topic:

-

Hack Spots Fork-n-Go

+

Hack Spots Fork-n-Go

I made this website to collect hack spots all over the world from friends and friends of friends (the spreadsheet is wide open, so you can add some, too!). It’s using sheetsee to power the table, map and other elements of the page. Its source is in this repo, with just a gh-pages branch. To create an instance of this site for yourself all you need to do:

  • Create a Google spreadsheet with the same headers (just copy and paste header row from the original). Click File > Publish to Web, then Start Publishing.
  • @@ -47,29 +51,26 @@

    Getting Started

Ideas

Demos

Use

Contact

-

Home Page

+

Home

diff --git a/docs/fork-n-go.md b/docs/templates.md similarity index 66% rename from docs/fork-n-go.md rename to docs/templates.md index 96f40eb..da0e0ea 100644 --- a/docs/fork-n-go.md +++ b/docs/templates.md @@ -1,4 +1,14 @@ -# Fork-n-Go +# Templates + +Here are partially set up projects to get you going! + +## Glitch + + + +The [spreadsheet.glitch.me](https://spreadsheet.glitch.me) site will give you an endpoint to use that will return your spreadsheet to you as JSON. The [sheetsee.glitch.me](https://sheetsee.glitch.me) site provides template to get started with Sheetsee; it's already set up with a server so that your data is backed up. + +## Fork-n-Go @@ -6,20 +16,19 @@ A Fork-n-Go project is a project on GitHub that in a few clicks, starting with a Two awesome things that make this possible: **Forking**, the tool on GitHub that allows you to copy a public repository onto your account, and [**GitHub Pages**](http://pages.github.com), GitHub's free web hosting service for every repository, account and organization. -I've built a whole other website on the idea with lots of examples: [jlord.github.io/forkngo](http://jlord.github.io/forkngo/) +I've built a whole other website on the idea with lots of examples: [jlord.github.io/forkngo](http://jlord.github.io/forkngo) ---- -## How +### How -To have a website for a repository hosted by GitHub Pages all you need is a branch named `gh-pages`. GitHub will then look in that branch for web files and serve them up at the address. +To have a website for a repository hosted by GitHub Pages all you need is to have webfiles uploaded and to tell GitHub the name of the branch you want it to host (in your repository's settings). -So Sheetsee.js projects, hosted on `gh-pages` branches on GitHub, can easily be forked and connected to another spreadsheet giving another user a live website of their own—with data they control—really easily. +So Sheetsee.js projects, hosted on GitHub, can easily be forked and connected to another spreadsheet giving another user a live website of their own—with data they control—really easily. -## Example +### Example A Fork-n-Go example from my [blog post](http://jlord.github.io/blog/fork-n-go) on the topic: -### Hack Spots Fork-n-Go +#### Hack Spots Fork-n-Go I made this website to collect hack spots all over the world from friends and friends of friends (the spreadsheet is wide open, so you can add some, too!). It’s using sheetsee to power the table, map and other elements of the page. Its source is in this repo, with just a gh-pages branch. To create an instance of this site for yourself all you need to do: diff --git a/site/docs/tips.html b/docs/tips.html similarity index 74% rename from site/docs/tips.html rename to docs/tips.html index 6627083..fcf6564 100644 --- a/site/docs/tips.html +++ b/docs/tips.html @@ -7,8 +7,8 @@ - - + + @@ -16,14 +16,14 @@

Tips

-

A few things to think about beyond charts, maps and tables.

-

ICanHaz.js

-

You can use templates for more than just tables. Use them to create lists ol, ul; array of images... You'll need a placeholder <div> in your HTML, a <script> for your template and a script to call ICanHaz from your Tabletop.js callback. For a live example, see the bottom photo grid of the sheetsee-table demo.

+

A few things to think about beyond maps and tables.

+

Mustache

+

You can use templates for more than just tables. Use them to create lists ol, ul; array of images... You'll need a placeholder <div> in your HTML, a <script> for your template. In your Tabletop.js callback use Mustache to pass your data into the template and add it to the DOM.

HTML

<div id="divID"></div>
 

Template

-
<script id="divID" type="text/html">
+
<script id="divID_template" type="text/html">
   {{#rows}}
     <div><img class="photo" src="{{some-variable}}"></div>
   {{/rows}}
@@ -31,25 +31,31 @@ 

ICanHaz.js

Script

<script type="text/html">
-  // your other Sheetsee.js, Tabletop code above
-  var html = Sheetsee.ich.divID({'rows': data})
-  $('#divID').html(html)
+  document.addEventListener('DOMContentLoaded', function() {
+    var URL = 'YOURSPREADSHEETSKEYHERE'
+    Tabletop.init({key: URL, callback: showData, simpleSheet: true})
+  })
+
+  function showData (data) {
+    var template = document.querySelector('divID_template').innerHTML
+    var html = Sheetsee.Mustache.render(template, {'rows': data})
+    document.getElementByID('divID').innerHTML = html
+  }
 </script>
 
-

non-table example output

+

non-table example

lib

Query Strings

-

If your spreadsheet contains address information, using templates (Sheetsee.js uses a form of Mustache), you can embed those elements into a query string (aka a search URL) like Google Maps URL or Yelp. If you search for a location in Google Maps, you'll notice it creates a URL for that search.

+

If your spreadsheet contains address information, using templates (Sheetsee uses Mustache.js), you can embed those elements into a query string (aka a search URL) like Google Maps URL or Yelp. If you search for a location in Google Maps, you'll notice it creates a URL for that search.

So, if you have information in your spreadsheet that would go inside a query string, make a template for inserting them into a link on your page.

The basic elements are: a spreadsheet with address info + HTML template to create the query string.

-

The Sheetsee Hack-Spots is an does such a thing. Here is the spreadsheet, with address information

+

The Sheetsee Hack-Spots is an example that does such a thing. Here is the spreadsheet, with address information:

img

In the HTML template for the table on the Hack-Spots page, the button’s links look like this:

<a class="button" href="https://maps.google.com/maps?q={{address}},{{city}},{{state}}" target="_blank">View in Google Maps</a>
 <a class="button" href="http://www.yelp.com/search?find_desc={{name}}&find_loc={{city}},{{state}}" target="_blank">Find on Yelp</a>
 
-

Here is the exact line of code on GitHub.

-

We’re inserting the address, city, and state details from the spreadsheet into the structure of a query string for Google maps and Yelp. You can figure out the query string of a service by just using it (type in an address in Google Maps) and looking at the resulting URL.

+

Above we're inserting the address, city, and state details from the spreadsheet into the structure of a query string for Google maps and Yelp. You can figure out the query string of a service by just using it (type in an address in Google Maps) and looking at the resulting URL.

With a some CSS and such, the resulting website has a table with the hack spots and a button for viewing in Google Maps or Yelp:

img

When the page builds, it creates the correct link for each row. When someone clicks on the buttons it takes them directly to the Google Map search result for that address. BAM!

@@ -80,29 +86,26 @@

Getting Started

Ideas

Demos

Use

Contact

-

Home Page

+

Home

diff --git a/docs/tips.md b/docs/tips.md index 6415ba7..3d3b392 100644 --- a/docs/tips.md +++ b/docs/tips.md @@ -1,11 +1,11 @@ # Tips -A few things to think about beyond charts, maps and tables. +A few things to think about beyond maps and tables. -## ICanHaz.js +## Mustache + +You can use templates for more than just tables. Use them to create lists `ol`, `ul`; array of images... You'll need a placeholder `
` in your HTML, a ` ``` -_non-table example output_ +_non-table example_ ![lib](http://jlord.s3.amazonaws.com/wp-content/uploads/lending-ss.png) ## Query Strings -If your spreadsheet contains address information, using templates (Sheetsee.js uses a form of Mustache), you can embed those elements into a query string (aka a search URL) like Google Maps URL or Yelp. If you search for a location in Google Maps, you'll notice it creates a URL for that search. +If your spreadsheet contains address information, using templates (Sheetsee uses Mustache.js), you can embed those elements into a query string (aka a search URL) like Google Maps URL or Yelp. If you search for a location in Google Maps, you'll notice it creates a URL for that search. So, if you have information in your spreadsheet that would go inside a query string, make a template for inserting them into a link on your page. @@ -54,9 +61,8 @@ In the HTML template for the table on the [Hack-Spots](jlord.github.io/hack-spot View in Google Maps Find on Yelp ``` -Here is the exact line of code on GitHub. -We’re inserting the address, city, and state details from the spreadsheet into the structure of a query string for Google maps and Yelp. You can figure out the query string of a service by just using it (type in an address in Google Maps) and looking at the resulting URL. +Above we're inserting the address, city, and state details from the spreadsheet into the structure of a query string for Google maps and Yelp. You can figure out the query string of a service by just using it (type in an address in Google Maps) and looking at the resulting URL. With a some CSS and such, the resulting website has a table with the hack spots and a button for viewing in Google Maps or Yelp: diff --git a/img/glitch.png b/img/glitch.png new file mode 100644 index 0000000..738f7b9 Binary files /dev/null and b/img/glitch.png differ diff --git a/img/next-sheetsee.png b/img/next-sheetsee.png new file mode 100644 index 0000000..a053bdc Binary files /dev/null and b/img/next-sheetsee.png differ diff --git a/site/index.html b/index.html similarity index 54% rename from site/index.html rename to index.html index cc5d216..1340736 100644 --- a/site/index.html +++ b/index.html @@ -7,18 +7,22 @@ - - + +
-

sheetseeimg

-

Sheetsee.js is a client-side library for connecting Google Spreadsheets to a website and visualizing the information in tables, maps and charts.

+

sheetseeimg

+

Sheetsee.js is a client-side library for connecting Google Spreadsheets to a website and visualizing the information in tables and maps.

+
+ +

Spreadsheets!?

+

Google Spreadsheets can be used as simple and collaborative databases, they make getting a data driven site going much easier than traditional databases. Read more about using spreadsheets for databases.

Modules

-

Each of sheetsee.js's features are divided into modules. Use just the parts you need; see docs on building. If you don't want to build your own, you can just use the full library which includes all modules, it's here on GitHub.

+

Each of Sheetsee's functions are divided into modules. Use just the parts you need; see docs on building. If you don't want to build your own, you can just use the full library which includes all modules, it's here on GitHub.

@@ -29,8 +33,13 @@

Modules

+ + + + + - + @@ -40,66 +49,53 @@

Modules

- + - - - - -
sheetseeCommand line module for make a custom build of Sheetsee.Doc
sheetsee-coreIncluded in any build. Gets you started and has the working-with-your-data functions.Included in all builds. Has helpful working-with-your-data functions. Doc
sheetsee-mapsFor making maps with your point, line or polygon spreadsheet data. Built on Mapbox.js.For making maps with your point, line or polygon spreadsheet data. Built with Leaflet.js. Doc
sheetsee-chartsIncludes 3 basic d3 charts: bar, line and pie. You can also use your own.Doc
-

Spreadsheets!?

-

Google Spreadsheets can be used as simple and collaborative databases, they make getting a data driven site going much easier than traditional databases. Read more about using spreadsheets for databases here.

-

In the Wild

-

What can you make with Sheetsee.js? Lots of things, here are some examples:

- -

List your sheetsee project here: file an issue or pull request.

-

Resources & Documentation

-

More resources on using Sheetsee.js:

+
New News!
+ +

Get your Spreadsheet as JSON or Try Sheetsee with Glitch!

+

The spreadsheet.glitch.me site will give you an endpoint to use that will return your spreadsheet to you as JSON. The sheetsee.glitch.me site provides template to get started with Sheetsee; it's already set up with a server so that your data is backed up.

+

Or Fork a Site!

+

There are site templates hooked up to Sheetsee that are ready to be forked on GitHub and used by you, check out the Fork-n-go site.

+

Sheetsee Updates

+

Sheetsee has just been re-written and there are some breaking changes. Also some nice ones, like dependencies removed. The API for maps with Sheetsee have changed, see the docs.

+
+ +

Resources & Documentation

+

More resources on using Sheetsee:

- + - - - + + + + - - - - - - - - +
Getting Started IdeasUse DemosUse
About Sheetsee.jsFork-n-GoSheetsee-coreAbout SheetseeTemplates Table DemoSheetsee-core
Building Sheetsee Tips!Map Demo Sheetsee-tablesTable Demo
BasicsCustom chartsSheetsee-mapsMap Demo
Sheetsee-chartsChart DemoSheetsee-maps
-

Note on New Google Spreadsheets

-

Google recently updated their Google Spreadsheets and the API. For a bit this was breaking things using the old API, including Tabletop. This has been fixed and the latest version of tabletop.js works on both old and new spreadsheets. Be sure to include at least version 1.3.4 in your project.

diff --git a/js/sheetsee.js b/js/sheetsee.js index 23f82a6..4107c21 100644 --- a/js/sheetsee.js +++ b/js/sheetsee.js @@ -1,23603 +1,13733 @@ ;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o currentMax) { + result = [] + return result.push(element) } + }) + // It will return an empty array if there are + // no numbers in the column specified + return result +} + +// Returns rows with the lowest value in column +// data: array of objects, column: string +function getMin (data, column) { + var result = [] + data.forEach(function (element) { + // We are checking that the element is a number + if (!parseInt(element[column].valueOf())) return + if (result.length === 0) return result.push(element) + + var currentMax = parseInt(result[0][column].valueOf()) + var comparing = parseInt(element[column].valueOf()) - trim = function (text) { - return text == null ? "" : - text.toString().replace(trimLeft, "").replace(trimRight, ""); + if (comparing > currentMax) return + if (comparing === currentMax) return result.push(element) + if (comparing < currentMax) { + result = [] + return result.push(element) } - } + }) + // It will return an empty array if there are + // no numbers in the column specified + return result +} - var escapeMap = { - "&": "&", - "<": "<", - ">": ">", - '"': '"', - "'": ''' - }; +// Returns array of rows with elements in the column that match the filter +// data: array of objects, filter: string, column: string +function getMatches (data, filter, column) { + var matches = [] + data.forEach(function (element) { + var projectType = element[column].toString().toLowerCase() + if (projectType === filter.toLowerCase()) matches.push(element) + }) + return matches +} - function escapeHTML(string) { - return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) { - return escapeMap[s] || s; - }); +// TODO maybe remove this all together +function mostFrequent (data, category) { + // this now maps to getOccurance + return getOccurance(data, category) +} + +// This is same as frequency just formatter better +function getOccurance (data, category) { + var occuranceCount = {} + for (var i = 0; i < data.length; i++) { + if (!occuranceCount[data[i][category]]) { + occuranceCount[data[i][category]] = 0 + } + occuranceCount[data[i][category]]++ } + return occuranceCount + // returns object, keys alphabetical +} - var regexCache = {}; - var Renderer = function () {}; +module.exports.getKeywordCount = getKeywordCount +module.exports.getKeyword = getKeyword +module.exports.getColumnTotal = getColumnTotal +module.exports.getColumnAverage = getColumnAverage +module.exports.getMax = getMax +module.exports.getMin = getMin +module.exports.getMatches = getMatches +module.exports.mostFrequent = mostFrequent // maps to getOccurance +module.exports.getOccurance = getOccurance - Renderer.prototype = { - otag: "{{", - ctag: "}}", - pragmas: {}, - buffer: [], - pragmas_implemented: { - "IMPLICIT-ITERATOR": true - }, - context: {}, +},{}],2:[function(require,module,exports){ +var Mustache = require('mustache') - render: function (template, context, partials, in_recursion) { - // reset buffer & set context - if (!in_recursion) { - this.context = context; - this.buffer = []; // TODO: make this non-lazy - } +var L = require('leaflet') +require('leaflet.markercluster') - // fail fast - if (!this.includes("", template)) { - if (in_recursion) { - return template; - } else { - this.send(template); - return; - } - } +function createGeoJSON (data, optionsJSON) { + var geoJSON = [] + data.forEach(function (lineItem) { + var hasGeo = confirmGeo(lineItem) - // get the pragmas together - template = this.render_pragmas(template); + if (hasGeo && !lineItem.lat && !lineItem.long) handleLatLong(lineItem) + if (lineItem.linestring || lineItem.multipolygon) hasGeo = true + if (!hasGeo) return - // render the template - var html = this.render_section(template, context, partials); + if (!optionsJSON) { + optionsJSON = makeupOptionObject(lineItem) + var optionObj = buildOptionObject(optionsJSON, lineItem) + } else { + optionObj = buildOptionObject(optionsJSON, lineItem) + } - // render_section did not find any sections, we still need to render the tags - if (html === false) { - html = this.render_tags(template, context, partials, in_recursion); - } + var type = determineType(lineItem) - if (in_recursion) { - return html; - } else { - this.sendLines(html); - } - }, + if (lineItem.polygon || lineItem.multipolygon || lineItem.linestring) { + var shapeFeature = shapeJSON(lineItem, type, optionObj) + geoJSON.push(shapeFeature) + } else { + var pointFeature = pointJSON(lineItem, type, optionObj) + geoJSON.push(pointFeature) + } + }) + return geoJSON +} - /* - Sends parsed lines - */ - send: function (line) { - if (line !== "") { - this.buffer.push(line); - } - }, +function buildOptionObject (optionsJSON, lineItem) { + var newObj = {} + optionsJSON.forEach(function (option) { + newObj[option] = lineItem[option] + }) + return newObj +} - sendLines: function (text) { - if (text) { - var lines = text.split("\n"); - for (var i = 0; i < lines.length; i++) { - this.send(lines[i]); - } - } - }, +function makeupOptionObject (lineItem) { + var options = [] + for (var i in lineItem) { + options.push(i) + } + return options +} - /* - Looks for %PRAGMAS - */ - render_pragmas: function (template) { - // no pragmas - if (!this.includes("%", template)) { - return template; - } +function confirmGeo (lineItem) { + var hasGeo = false + if (lineItem.lat && lineItem.long || lineItem.polygon) hasGeo = true + if (lineItem.latitude && lineItem.longitude || lineItem.polygon) hasGeo = true + if (lineItem.geolatitude && lineItem.geolongitude || lineItem.polygon) hasGeo = true + return hasGeo +} - var that = this; - var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) { - return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); - }); +function handleLatLong (lineItem) { + if (lineItem.latitude && lineItem.longitude || lineItem.polygon) { + lineItem.lat = lineItem.latitude + lineItem.long = lineItem.longitude + delete lineItem.latitude + delete lineItem.longitude + return lineItem + } + if (lineItem.geolatitude && lineItem.geolongitude || lineItem.polygon) { + lineItem.lat = lineItem.geolatitude + lineItem.long = lineItem.geolongitude + delete lineItem.geolatitude + delete lineItem.geolongitude + return lineItem + } +} - return template.replace(regex, function (match, pragma, options) { - if (!that.pragmas_implemented[pragma]) { - throw({message: - "This implementation of mustache doesn't understand the '" + - pragma + "' pragma"}); - } - that.pragmas[pragma] = {}; - if (options) { - var opts = options.split("="); - that.pragmas[pragma][opts[0]] = opts[1]; - } - return ""; - // ignore unknown pragmas silently - }); +function pointJSON (lineItem, type, optionObj) { + var pointFeature = { + type: 'Feature', + 'geometry': { + 'type': type, + 'coordinates': [+lineItem.long, +lineItem.lat] }, - - /* - Tries to find a partial in the curent scope and render it - */ - render_partial: function (name, context, partials) { - name = trim(name); - if (!partials || partials[name] === undefined) { - throw({message: "unknown_partial '" + name + "'"}); - } - if (!context || typeof context[name] != "object") { - return this.render(partials[name], context, partials, true); - } - return this.render(partials[name], context[name], partials, true); + 'properties': { + 'color': lineItem.hexcolor || '#2196f3' }, + 'opts': optionObj + } + return pointFeature +} - /* - Renders inverted (^) and normal (#) sections - */ - render_section: function (template, context, partials) { - if (!this.includes("#", template) && !this.includes("^", template)) { - // did not render anything, there were no sections - return false; - } - - var that = this; +function divIcon (color) { + var markerHtmlStyles = 'background-color: #' + color.replace('#', '') + ';' + + 'width: 2rem; height: 2rem; display: block; left: -1rem; top: -1rem; border: 1px solid #fff;' + + 'position: relative; border-radius: 3rem 3rem 0; transform: rotate(45deg);' + var icon = L.divIcon({ + className: 'div-icon', + iconAnchor: [0, 24], + labelAnchor: [-6, 0], + popupAnchor: [0, -36], + html: '' + }) + return icon +} - var regex = this.getCachedRegex("render_section", function (otag, ctag) { - // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder - return new RegExp( - "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) +function shapeJSON (lineItem, type, optionObj) { + var lowercaseType = type.toLowerCase() + var coords + if (type !== 'LineString') { + coords = JSON.parse('[[' + lineItem[lowercaseType] + ']]') + } else { coords = JSON.parse('[' + lineItem[lowercaseType] + ']') } + var shapeFeature = { + type: 'Feature', + 'geometry': { + 'type': type, + 'coordinates': coords + }, + 'properties': { + 'fillColor': lineItem.hexcolor, + 'color': lineItem.hexcolor + }, + 'opts': optionObj + } + return shapeFeature +} - otag + // {{ - "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) - ctag + // }} +function determineType (lineItem) { + var type = '' + // TODO this is not actually verifying the content just the property + if (lineItem.lat && lineItem.long) type = 'Point' + if (lineItem.polygon) type = 'Polygon' + if (lineItem.multipolygon) type = 'MultiPolygon' + if (lineItem.linestring) type = 'LineString' + return type +} - "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped +// MAPS - otag + // {{ - "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). - ctag + // }} +function loadMap (mapOptions) { + if (!mapOptions.data) return // no data, no map + var map = L.map(mapOptions.mapDiv) + var tiles = mapOptions.tiles || 'http://{s}.tile.osm.org/{z}/{x}/{y}.png' + var attribution = '© OpenStreetMap contributors' - "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. + L.tileLayer(tiles, {attribution: attribution}).addTo(map) + // Set behavior + map.touchZoom.disable() + map.doubleClickZoom.disable() + map.scrollWheelZoom.disable() + addMarkerLayer(map, mapOptions) +} - "g"); - }); +function makePopupTemplate (geoJSON) { + var allOptions = geoJSON[0].opts + var keys = [] + for (var i in allOptions) keys.push(i) + var mustacheKeys = mustachify(keys) + var template = '
    ' + var counter = mustacheKeys.length + mustacheKeys.forEach(function (key) { + counter-- + if (counter === 0) template = template.concat(key, '
') + else template = template.concat(key) + }) + return template +} - // for each {{#foo}}{{/foo}} section do... - return template.replace(regex, function (match, before, type, name, content, after) { - // before contains only tags, no sections - var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", +function mustachify (array) { + var newArray = [] + array.forEach(function (item) { + item = '
  • ' + item + ': {{' + item + '}}
  • ' + newArray.push(item) + }) + return newArray +} - // after may contain both sections and tags, so use full rendering function - renderedAfter = after ? that.render(after, context, partials, true) : "", +function addMarkerLayer (map, mapOpts) { + // setting a color in options overides colors in spreadsheet + if (mapOpts.hexcolor) var iconColor = mapOpts.hexcolor - // will be computed below - renderedContent, + mapOpts.geoJSONincludes = mapOpts.geoJSONincludes || null // um? + var geoJSON = createGeoJSON(mapOpts.data, mapOpts.geoJSONincludes) - value = that.find(name, context); + // if no popup template, create one + if (!mapOpts.template) mapOpts.template = makePopupTemplate(geoJSON) - if (type === "^") { // inverted section - if (!value || Array.isArray(value) && value.length === 0) { - // false or empty list, render it - renderedContent = that.render(content, context, partials, true); - } else { - renderedContent = ""; - } - } else if (type === "#") { // normal section - if (Array.isArray(value)) { // Enumerable, Let's loop! - renderedContent = that.map(value, function (row) { - return that.render(content, that.create_context(row), partials, true); - }).join(""); - } else if (that.is_object(value)) { // Object, Use it as subcontext! - renderedContent = that.render(content, that.create_context(value), - partials, true); - } else if (typeof value == "function") { - // higher order section - renderedContent = value.call(context, content, function (text) { - return that.render(text, context, partials, true); - }); - } else if (value) { // boolean section - renderedContent = that.render(content, context, partials, true); - } else { - renderedContent = ""; - } - } + var features = {'type': 'FeatureCollection', 'features': geoJSON} - return renderedBefore + renderedContent + renderedAfter; - }); - }, + if (mapOpts.cluster) var clusterGroup = new L.MarkerClusterGroup() + var layer = L.geoJson(features) - /* - Replace {{foo}} and friends with values from our view - */ - render_tags: function (template, context, partials, in_recursion) { - // tit for tat - var that = this; - - var new_regex = function () { - return that.getCachedRegex("render_tags", function (otag, ctag) { - return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g"); - }); - }; - - var regex = new_regex(); - var tag_replace_callback = function (match, operator, name) { - switch(operator) { - case "!": // ignore comments - return ""; - case "=": // set new delimiters, rebuild the replace regexp - that.set_delimiters(name); - regex = new_regex(); - return ""; - case ">": // render partial - return that.render_partial(name, context, partials); - case "{": // the triple mustache is unescaped - case "&": // & operator is an alternative unescape method - return that.find(name, context); - default: // escape the value - return escapeHTML(that.find(name, context)); - } - }; - var lines = template.split("\n"); - for(var i = 0; i < lines.length; i++) { - lines[i] = lines[i].replace(regex, tag_replace_callback, this); - if (!in_recursion) { - this.send(lines[i]); - } - } + layer.eachLayer(function (marker) { + var popupContent = Mustache.render(mapOpts.template, marker.feature.opts) + marker.bindPopup(popupContent, {closeButton: false}) + marker.setIcon(divIcon(iconColor || marker.feature.properties.color)) + if (mapOpts.cluster) clusterGroup.addLayer(marker) + }) - if (in_recursion) { - return lines.join("\n"); - } - }, + map.fitBounds(layer.getBounds()) - set_delimiters: function (delimiters) { - var dels = delimiters.split(" "); - this.otag = this.escape_regex(dels[0]); - this.ctag = this.escape_regex(dels[1]); - }, + if (mapOpts.cluster) { + map.addLayer(clusterGroup) + addClusterCSS(iconColor || '#2196f3') + } else layer.addTo(map) +} - escape_regex: function (text) { - // thank you Simon Willison - if (!arguments.callee.sRE) { - var specials = [ - '/', '.', '*', '+', '?', '|', - '(', ')', '[', ']', '{', '}', '\\' - ]; - arguments.callee.sRE = new RegExp( - '(\\' + specials.join('|\\') + ')', 'g' - ); - } - return text.replace(arguments.callee.sRE, '\\$1'); - }, +function addClusterCSS (color) { + if (!color.match('#')) color += '#' + var css = '.marker-cluster-small, .marker-cluster-small div, .marker-cluster-medium,' + + '.marker-cluster-medium div, .marker-cluster-large, .marker-cluster-large div' + + '{background-color:' + color + ';} .marker-cluster {background-clip: padding-box; border-radius: 20px;}' + + '.marker-cluster div {width: 30px; height: 30px; margin-left: 5px; margin-top: 5px;' + + 'text-align: center; border-radius: 15px; font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;}' + + '.marker-cluster span {line-height: 30px;}' + var style = document.createElement('style') + style.innerHTML = css + document.head.appendChild(style) +} - /* - find `name` in current `context`. That is find me a value - from the view object - */ - find: function (name, context) { - name = trim(name); +module.exports.createGeoJSON = createGeoJSON +module.exports.loadMap = loadMap + +},{"leaflet":4,"leaflet.markercluster":3,"mustache":5}],3:[function(require,module,exports){ +/* + Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. + https://github.com/Leaflet/Leaflet.markercluster + (c) 2012-2013, Dave Leaver, smartrak +*/ +!function(e,t,i){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var i,n=this._featureGroup,r=this._nonPointGroup,s=this.options.chunkedLoading,o=this.options.chunkInterval,a=this.options.chunkProgress,h=e.length,l=0,_=!0;if(this._map){var u=(new Date).getTime(),d=L.bind(function(){for(var c=(new Date).getTime();h>l;l++){if(s&&0===l%200){var p=(new Date).getTime()-c;if(p>o)break}if(i=e[l],i instanceof L.LayerGroup)_&&(e=e.slice(),_=!1),this._extractNonGroupLayers(i,e),h=e.length;else if(i.getLatLng){if(!this.hasLayer(i)&&(this._addLayer(i,this._maxZoom),t||this.fire("layeradd",{layer:i}),i.__parent&&2===i.__parent.getChildCount())){var f=i.__parent.getAllChildMarkers(),m=f[0]===i?f[1]:f[0];n.removeLayer(m)}}else r.addLayer(i),t||this.fire("layeradd",{layer:i})}a&&a(l,h,(new Date).getTime()-u),l===h?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else for(var c=this._needsClustering;h>l;l++)i=e[l],i instanceof L.LayerGroup?(_&&(e=e.slice(),_=!1),this._extractNonGroupLayers(i,e),h=e.length):i.getLatLng?this.hasLayer(i)||c.push(i):r.addLayer(i);return this},removeLayers:function(e){var t,i,n=e.length,r=this._featureGroup,s=this._nonPointGroup,o=!0;if(!this._map){for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):(this._arraySplice(this._needsClustering,i),s.removeLayer(i),this.hasLayer(i)&&this._needsRemoving.push({layer:i,latlng:i._latlng}),this.fire("layerremove",{layer:i}));return this}if(this._unspiderfy){this._unspiderfy();var a=e.slice(),h=n;for(t=0;h>t;t++)i=a[t],i instanceof L.LayerGroup?(this._extractNonGroupLayers(i,a),h=a.length):this._unspiderfyLayer(i)}for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):i.__parent?(this._removeLayer(i,!0,!0),this.fire("layerremove",{layer:i}),r.hasLayer(i)&&(r.removeLayer(i),i.clusterShow&&i.clusterShow())):(s.removeLayer(i),this.fire("layerremove",{layer:i}));return this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(e){e.off(this._childMarkerEventHandlers,this),delete e.__parent},this),this._map&&this._generateInitialClusters(),this},getBounds:function(){var e=new L.LatLngBounds;this._topClusterLevel&&e.extend(this._topClusterLevel._bounds);for(var t=this._needsClustering.length-1;t>=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var i,n,r,s=this._needsClustering.slice(),o=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(s),n=s.length-1;n>=0;n--){for(i=!0,r=o.length-1;r>=0;r--)if(o[r].layer===s[n]){i=!1;break}i&&e.call(t,s[n])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(i){L.stamp(i)===e&&(t=i)}),t},hasLayer:function(e){if(!e)return!1;var t,i=this._needsClustering;for(t=i.length-1;t>=0;t--)if(i[t]===e)return!0;for(i=this._needsRemoving,t=i.length-1;t>=0;t--)if(i[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){"function"!=typeof t&&(t=function(){});var i=function(){!e._icon&&!e.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),e._icon?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoomt;t++)n=this._needsRemoving[t],n.newlatlng=n.layer._latlng,n.layer._latlng=n.latlng;for(t=0,i=this._needsRemoving.length;i>t;t++)n=this._needsRemoving[t],this._removeLayer(n.layer,!0),n.layer._latlng=n.newlatlng;this._needsRemoving=[],this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i,!0)},onRemove:function(e){e.off("zoomend",this._zoomEnd,this),e.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.remove(),this._nonPointGroup.remove(),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(e){for(var t=e;t&&!t._icon;)t=t.__parent;return t||null},_arraySplice:function(e,t){for(var i=e.length-1;i>=0;i--)if(e[i]===t)return e.splice(i,1),!0},_removeFromGridUnclustered:function(e,t){for(var i=this._map,n=this._gridUnclustered,r=this._map.getMinZoom();t>=r&&n[t].removeObject(e,i.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,i){e._latlng=t,this.removeLayer(e),e._latlng=i,this.addLayer(e)},_childMarkerDragEnd:function(e){e.target.__dragStart&&this._moveChild(e.target,e.target.__dragStart,e.target._latlng),delete e.target.__dragStart},_removeLayer:function(e,t,i){var n=this._gridClusters,r=this._gridUnclustered,s=this._featureGroup,o=this._map,a=this._map.getMinZoom();t&&this._removeFromGridUnclustered(e,this._maxZoom);var h,l=e.__parent,_=l._markers;for(this._arraySplice(_,e);l&&(l._childCount--,l._boundsNeedUpdate=!0,!(l._zoomt?"small":100>t?"medium":"large",new L.DivIcon({html:"
    "+t+"
    ",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(t||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(e||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._map.getMinZoom(),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=this._map.getMaxZoom(),t=this._map.getMinZoom(),i=this.options.maxClusterRadius,n=i;"function"!=typeof i&&(n=function(){return i}),this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(n(r)),this._gridUnclustered[r]=new L.DistanceGrid(n(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var i,n,r=this._gridClusters,s=this._gridUnclustered,o=this._map.getMinZoom();for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=o;t--){i=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(i);if(a)return a._addChild(e),e.__parent=a,void 0;if(a=s[t].getNearObject(i)){var h=a.__parent;h&&this._removeLayer(a,!1);var l=new this._markerCluster(this,t,a,e);r[t].addObject(l,this._map.project(l._cLatLng,t)),a.__parent=l,e.__parent=l;var _=l;for(n=t-1;n>h._zoom;n--)_=new this._markerCluster(this,n,_),r[n].addObject(_,this._map.project(a.getLatLng(),n));return h._addChild(_),this._removeFromGridUnclustered(a,t),void 0}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return t!==i&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,n=e.getLayers(),r=0;for(t=t||[];r=0;i--)o=h[i],n.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(n,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(n,e,s,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._map.getMinZoom(),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var i=this,n=this._featureGroup;n.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){n.removeLayer(e),e.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,i){var n=this._getExpandedVisibleBounds(),r=this._map.getMinZoom();e._recursivelyAnimateChildrenInAndAddSelfToMap(n,r,t+1,i);var s=this;this._forceLayout(),e._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===e._childCount){var o=e._markers[0];this._ignoreMove=!0,o.setLatLng(o.getLatLng()),this._ignoreMove=!1,o.clusterShow&&o.clusterShow()}else e._recursively(n,i,r,function(e){e._recursivelyRemoveChildrenFromMap(n,r,t+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(t.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)},L.MarkerCluster=L.Marker.extend({initialize:function(e,t,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(e){e=e||[];for(var t=this._childClusters.length-1;t>=0;t--)this._childClusters[t].getAllChildMarkers(e);for(var i=this._markers.length-1;i>=0;i--)e.push(this._markers[i]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var e,t=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),r=this._zoom+1,s=i.getZoom();t.length>0&&n>r;){r++;var o=[];for(e=0;er?this._group._map.setView(this._latlng,r):s>=n?this._group._map.setView(this._latlng,s+1):this._group._map.fitBounds(this._bounds)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,i,n,r=this._markers,s=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),e=0;e=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())},function(e){var i,n,r=e._childClusters;for(i=r.length-1;i>=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,i,n){this._recursively(e,n,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),i),r._isSingleParent()&&i-1===n?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,i)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,i){this._recursively(i,this._group._map.getMinZoom()-1,t,function(n){if(t!==n._zoom)for(var r=n._markers.length-1;r>=0;r--){var s=n._markers[r];i.contains(s._latlng)&&(e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.clusterHide&&s.clusterHide()),n._group._featureGroup.addLayer(s))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var i=this._markers[t];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(e-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,i,n){var r,s;this._recursively(e,t-1,i-1,function(e){for(s=e._markers.length-1;s>=0;s--)r=e._markers[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(s=e._childClusters.length-1;s>=0;s--)r=e._childClusters[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,i,n,r){var s,o,a=this._childClusters,h=this._zoom;if(h>=t&&(n&&n(this),r&&h===i&&r(this)),t>h||i>h)for(s=a.length-1;s>=0;s--)o=a[s],e.intersects(o._bounds)&&o._recursively(e,t,i,n,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.Marker.include({clusterHide:function(){return this.options.opacityWhenUnclustered=this.options.opacity||1,this.setOpacity(0)},clusterShow:function(){var e=this.setOpacity(this.options.opacity||this.options.opacityWhenUnclustered);return delete this.options.opacityWhenUnclustered,e}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var i=this._getCoord(t.x),n=this._getCoord(t.y),r=this._grid,s=r[n]=r[n]||{},o=s[i]=s[i]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var i,n,r=this._getCoord(t.x),s=this._getCoord(t.y),o=this._grid,a=o[s]=o[s]||{},h=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],i=0,n=h.length;n>i;i++)if(h[i]===e)return h.splice(i,1),1===n&&delete a[r],!0},eachObject:function(e,t){var i,n,r,s,o,a,h,l=this._grid;for(i in l){o=l[i];for(n in o)for(a=o[n],r=0,s=a.length;s>r;r++)h=e.call(t,a[r]),h&&(r--,s--)}},getNearObject:function(e){var t,i,n,r,s,o,a,h,l=this._getCoord(e.x),_=this._getCoord(e.y),u=this._objectPoint,d=this._sqCellSize,c=null;for(t=_-1;_+1>=t;t++)if(r=this._grid[t])for(i=l-1;l+1>=i;i++)if(s=r[i])for(n=0,o=s.length;o>n;n++)a=s[n],h=this._sqDist(u[L.Util.stamp(a)],e),d>h&&(d=h,c=a);return c},_getCoord:function(e){return Math.floor(e/this._cellSize)},_sqDist:function(e,t){var i=t.x-e.x,n=t.y-e.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(e,t){var i=t[1].lat-t[0].lat,n=t[0].lng-t[1].lng;return n*(e.lat-t[0].lat)+i*(e.lng-t[0].lng)},findMostDistantPointFromBaseLine:function(e,t){var i,n,r,s=0,o=null,a=[];for(i=t.length-1;i>=0;i--)n=t[i],r=this.getDistant(n,e),r>0&&(a.push(n),r>s&&(s=r,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(e,t){var i=[],n=this.findMostDistantPointFromBaseLine(e,t);return n.maxPoint?(i=i.concat(this.buildConvexHull([e[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,e[1]],n.newPoints))):[e[0]]},getConvexHull:function(e){var t,i=!1,n=!1,r=!1,s=!1,o=null,a=null,h=null,l=null,_=null,u=null;for(t=e.length-1;t>=0;t--){var d=e[t];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.latr)&&(h=d,r=d.lng),(s===!1||d.lng=0;t--)e=i[t].getLatLng(),n.push(e);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(),i=this._group,n=i._map,r=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,r):(r.y+=10,e=this._generatePointsCircle(t.length,r)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,n,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),s=r/this._2PI,o=this._2PI/e,a=[];for(a.length=e,i=e-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(t.x+s*Math.cos(n),t.y+s*Math.sin(n))._round();return a},_generatePointsSpiral:function(e,t){var i,n=this._group.options.spiderfyDistanceMultiplier,r=n*this._spiralLengthStart,s=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=e,i=e-1;i>=0;i--)a+=s/r+5e-4*i,h[i]=new L.Point(t.x+r*Math.cos(a),t.y+r*Math.sin(a))._round(),r+=o/a;return h},_noanimationUnspiderfy:function(){var e,t,i=this._group,n=i._map,r=i._featureGroup,s=this.getAllChildMarkers();for(i._ignoreMove=!0,this.setOpacity(1),t=s.length-1;t>=0;t--)e=s[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(n.removeLayer(e._spiderLeg),delete e._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:s}),i._ignoreMove=!1,i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var i,n,r,s,o=this._group,a=o._map,h=o._featureGroup,l=this._group.options.spiderLegPolylineOptions;for(o._ignoreMove=!0,i=0;i=0;n--)h=u.layerPointToLatLng(t[n]),r=e[n],r._preSpiderfyLatlng=r._latlng,r.setLatLng(h),r.clusterShow&&r.clusterShow(),f&&(s=r._spiderLeg,o=s._path,o.style.strokeDashoffset=0,s.setStyle({opacity:g}));this.setOpacity(.3),_._ignoreMove=!1,setTimeout(function(){_._animationEnd(),_.fire("spiderfied",{cluster:l,markers:e})},200)},_animationUnspiderfy:function(e){var t,i,n,r,s,o,a=this,h=this._group,l=h._map,_=h._featureGroup,u=e?l._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):l.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(),c=L.Path.SVG;for(h._ignoreMove=!0,h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)t=d[i],t._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,o=!0,t._setPos&&(t._setPos(u),o=!1),t.clusterHide&&(t.clusterHide(),o=!1),o&&_.removeLayer(t),c&&(n=t._spiderLeg,r=n._path,s=r.getTotalLength()+.1,r.style.strokeDashoffset=s,n.setStyle({opacity:0})));h._ignoreMove=!1,setTimeout(function(){var e=0;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&e++;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&_.removeLayer(t),l.removeLayer(t._spiderLeg),delete t._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg) +}}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,i;for(t in e)for(i=e[t].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,i;for(t in e)i=e[t],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(e,t){var i=this.options.icon;return L.setOptions(i,e),this.setIcon(i),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}})}(window,document); +},{}],4:[function(require,module,exports){ +/* + Leaflet 1.0.3, a JS library for interactive maps. http://leafletjs.com + (c) 2010-2016 Vladimir Agafonkin, (c) 2010-2011 CloudMade +*/ +(function (window, document, undefined) { +var L = { + version: "1.0.3" +}; + +function expose() { + var oldL = window.L; + + L.noConflict = function () { + window.L = oldL; + return this; + }; + + window.L = L; +} + +// define Leaflet for Node module pattern loaders, including Browserify +if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = L; + +// define Leaflet as an AMD module +} else if (typeof define === 'function' && define.amd) { + define(L); +} + +// define Leaflet as a global L variable, saving the original L to restore later if needed +if (typeof window !== 'undefined') { + expose(); +} + + + +/* + * @namespace Util + * + * Various utility functions, used by Leaflet internally. + */ + +L.Util = { + + // @function extend(dest: Object, src?: Object): Object + // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut. + extend: function (dest) { + var i, j, len, src; + + for (j = 1, len = arguments.length; j < len; j++) { + src = arguments[j]; + for (i in src) { + dest[i] = src[i]; + } + } + return dest; + }, + + // @function create(proto: Object, properties?: Object): Object + // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create) + create: Object.create || (function () { + function F() {} + return function (proto) { + F.prototype = proto; + return new F(); + }; + })(), + + // @function bind(fn: Function, …): Function + // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). + // Has a `L.bind()` shortcut. + bind: function (fn, obj) { + var slice = Array.prototype.slice; + + if (fn.bind) { + return fn.bind.apply(fn, slice.call(arguments, 1)); + } + + var args = slice.call(arguments, 2); + + return function () { + return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); + }; + }, + + // @function stamp(obj: Object): Number + // Returns the unique ID of an object, assiging it one if it doesn't have it. + stamp: function (obj) { + /*eslint-disable */ + obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId; + return obj._leaflet_id; + /*eslint-enable */ + }, + + // @property lastId: Number + // Last unique ID used by [`stamp()`](#util-stamp) + lastId: 0, + + // @function throttle(fn: Function, time: Number, context: Object): Function + // Returns a function which executes function `fn` with the given scope `context` + // (so that the `this` keyword refers to `context` inside `fn`'s code). The function + // `fn` will be called no more than one time per given amount of `time`. The arguments + // received by the bound function will be any arguments passed when binding the + // function, followed by any arguments passed when invoking the bound function. + // Has an `L.bind` shortcut. + throttle: function (fn, time, context) { + var lock, args, wrapperFn, later; + + later = function () { + // reset lock and call if queued + lock = false; + if (args) { + wrapperFn.apply(context, args); + args = false; + } + }; + + wrapperFn = function () { + if (lock) { + // called too soon, queue to call later + args = arguments; + + } else { + // call and lock until later + fn.apply(context, arguments); + setTimeout(later, time); + lock = true; + } + }; + + return wrapperFn; + }, + + // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number + // Returns the number `num` modulo `range` in such a way so it lies within + // `range[0]` and `range[1]`. The returned value will be always smaller than + // `range[1]` unless `includeMax` is set to `true`. + wrapNum: function (x, range, includeMax) { + var max = range[1], + min = range[0], + d = max - min; + return x === max && includeMax ? x : ((x - min) % d + d) % d + min; + }, + + // @function falseFn(): Function + // Returns a function which always returns `false`. + falseFn: function () { return false; }, + + // @function formatNum(num: Number, digits?: Number): Number + // Returns the number `num` rounded to `digits` decimals, or to 5 decimals by default. + formatNum: function (num, digits) { + var pow = Math.pow(10, digits || 5); + return Math.round(num * pow) / pow; + }, + + // @function trim(str: String): String + // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) + trim: function (str) { + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); + }, + + // @function splitWords(str: String): String[] + // Trims and splits the string on whitespace and returns the array of parts. + splitWords: function (str) { + return L.Util.trim(str).split(/\s+/); + }, + + // @function setOptions(obj: Object, options: Object): Object + // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut. + setOptions: function (obj, options) { + if (!obj.hasOwnProperty('options')) { + obj.options = obj.options ? L.Util.create(obj.options) : {}; + } + for (var i in options) { + obj.options[i] = options[i]; + } + return obj.options; + }, + + // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String + // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}` + // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will + // be appended at the end. If `uppercase` is `true`, the parameter names will + // be uppercased (e.g. `'?A=foo&B=bar'`) + getParamString: function (obj, existingUrl, uppercase) { + var params = []; + for (var i in obj) { + params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); + } + return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); + }, + + // @function template(str: String, data: Object): String + // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` + // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string + // `('Hello foo, bar')`. You can also specify functions instead of strings for + // data values — they will be evaluated passing `data` as an argument. + template: function (str, data) { + return str.replace(L.Util.templateRe, function (str, key) { + var value = data[key]; + + if (value === undefined) { + throw new Error('No value provided for variable ' + str); + + } else if (typeof value === 'function') { + value = value(data); + } + return value; + }); + }, + + templateRe: /\{ *([\w_\-]+) *\}/g, + + // @function isArray(obj): Boolean + // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) + isArray: Array.isArray || function (obj) { + return (Object.prototype.toString.call(obj) === '[object Array]'); + }, + + // @function indexOf(array: Array, el: Object): Number + // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) + indexOf: function (array, el) { + for (var i = 0; i < array.length; i++) { + if (array[i] === el) { return i; } + } + return -1; + }, + + // @property emptyImageUrl: String + // Data URI string containing a base64-encoded empty GIF image. + // Used as a hack to free memory from unused images on WebKit-powered + // mobile devices (by setting image `src` to this string). + emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' +}; + +(function () { + // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + + function getPrefixed(name) { + return window['webkit' + name] || window['moz' + name] || window['ms' + name]; + } + + var lastTime = 0; + + // fallback for IE 7-8 + function timeoutDefer(fn) { + var time = +new Date(), + timeToCall = Math.max(0, 16 - (time - lastTime)); + + lastTime = time + timeToCall; + return window.setTimeout(fn, timeToCall); + } + + var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer, + cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || + getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; + + + // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number + // Schedules `fn` to be executed when the browser repaints. `fn` is bound to + // `context` if given. When `immediate` is set, `fn` is called immediately if + // the browser doesn't have native support for + // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame), + // otherwise it's delayed. Returns a request ID that can be used to cancel the request. + L.Util.requestAnimFrame = function (fn, context, immediate) { + if (immediate && requestFn === timeoutDefer) { + fn.call(context); + } else { + return requestFn.call(window, L.bind(fn, context)); + } + }; + + // @function cancelAnimFrame(id: Number): undefined + // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame). + L.Util.cancelAnimFrame = function (id) { + if (id) { + cancelFn.call(window, id); + } + }; +})(); + +// shortcuts for most used utility functions +L.extend = L.Util.extend; +L.bind = L.Util.bind; +L.stamp = L.Util.stamp; +L.setOptions = L.Util.setOptions; + + + + +// @class Class +// @aka L.Class + +// @section +// @uninheritable + +// Thanks to John Resig and Dean Edwards for inspiration! + +L.Class = function () {}; + +L.Class.extend = function (props) { + + // @function extend(props: Object): Function + // [Extends the current class](#class-inheritance) given the properties to be included. + // Returns a Javascript function that is a class constructor (to be called with `new`). + var NewClass = function () { + + // call the constructor + if (this.initialize) { + this.initialize.apply(this, arguments); + } + + // call all constructor hooks + this.callInitHooks(); + }; + + var parentProto = NewClass.__super__ = this.prototype; + + var proto = L.Util.create(parentProto); + proto.constructor = NewClass; + + NewClass.prototype = proto; + + // inherit parent's statics + for (var i in this) { + if (this.hasOwnProperty(i) && i !== 'prototype') { + NewClass[i] = this[i]; + } + } + + // mix static properties into the class + if (props.statics) { + L.extend(NewClass, props.statics); + delete props.statics; + } + + // mix includes into the prototype + if (props.includes) { + L.Util.extend.apply(null, [proto].concat(props.includes)); + delete props.includes; + } + + // merge options + if (proto.options) { + props.options = L.Util.extend(L.Util.create(proto.options), props.options); + } + + // mix given properties into the prototype + L.extend(proto, props); + + proto._initHooks = []; + + // add method for calling all hooks + proto.callInitHooks = function () { + + if (this._initHooksCalled) { return; } + + if (parentProto.callInitHooks) { + parentProto.callInitHooks.call(this); + } + + this._initHooksCalled = true; + + for (var i = 0, len = proto._initHooks.length; i < len; i++) { + proto._initHooks[i].call(this); + } + }; + + return NewClass; +}; + + +// @function include(properties: Object): this +// [Includes a mixin](#class-includes) into the current class. +L.Class.include = function (props) { + L.extend(this.prototype, props); + return this; +}; + +// @function mergeOptions(options: Object): this +// [Merges `options`](#class-options) into the defaults of the class. +L.Class.mergeOptions = function (options) { + L.extend(this.prototype.options, options); + return this; +}; + +// @function addInitHook(fn: Function): this +// Adds a [constructor hook](#class-constructor-hooks) to the class. +L.Class.addInitHook = function (fn) { // (Function) || (String, args...) + var args = Array.prototype.slice.call(arguments, 1); + + var init = typeof fn === 'function' ? fn : function () { + this[fn].apply(this, args); + }; + + this.prototype._initHooks = this.prototype._initHooks || []; + this.prototype._initHooks.push(init); + return this; +}; + + + +/* + * @class Evented + * @aka L.Evented + * @inherits Class + * + * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). + * + * @example + * + * ```js + * map.on('click', function(e) { + * alert(e.latlng); + * } ); + * ``` + * + * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: + * + * ```js + * function onClick(e) { ... } + * + * map.on('click', onClick); + * map.off('click', onClick); + * ``` + */ + + +L.Evented = L.Class.extend({ + + /* @method on(type: String, fn: Function, context?: Object): this + * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). + * + * @alternative + * @method on(eventMap: Object): this + * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` + */ + on: function (types, fn, context) { + + // types can be a map of types/handlers + if (typeof types === 'object') { + for (var type in types) { + // we don't process space-separated events here for performance; + // it's a hot path since Layer uses the on(obj) syntax + this._on(type, types[type], fn); + } + + } else { + // types can be a string of space-separated words + types = L.Util.splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._on(types[i], fn, context); + } + } + + return this; + }, + + /* @method off(type: String, fn?: Function, context?: Object): this + * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. + * + * @alternative + * @method off(eventMap: Object): this + * Removes a set of type/listener pairs. + * + * @alternative + * @method off: this + * Removes all listeners to all events on the object. + */ + off: function (types, fn, context) { + + if (!types) { + // clear all listeners if called without arguments + delete this._events; + + } else if (typeof types === 'object') { + for (var type in types) { + this._off(type, types[type], fn); + } + + } else { + types = L.Util.splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._off(types[i], fn, context); + } + } + + return this; + }, + + // attach listener (without syntactic sugar now) + _on: function (type, fn, context) { + this._events = this._events || {}; + + /* get/init listeners for type */ + var typeListeners = this._events[type]; + if (!typeListeners) { + typeListeners = []; + this._events[type] = typeListeners; + } + + if (context === this) { + // Less memory footprint. + context = undefined; + } + var newListener = {fn: fn, ctx: context}, + listeners = typeListeners; + + // check if fn already there + for (var i = 0, len = listeners.length; i < len; i++) { + if (listeners[i].fn === fn && listeners[i].ctx === context) { + return; + } + } + + listeners.push(newListener); + }, + + _off: function (type, fn, context) { + var listeners, + i, + len; + + if (!this._events) { return; } + + listeners = this._events[type]; + + if (!listeners) { + return; + } + + if (!fn) { + // Set all removed listeners to noop so they are not called if remove happens in fire + for (i = 0, len = listeners.length; i < len; i++) { + listeners[i].fn = L.Util.falseFn; + } + // clear all listeners for a type if function isn't specified + delete this._events[type]; + return; + } + + if (context === this) { + context = undefined; + } + + if (listeners) { + + // find fn and remove it + for (i = 0, len = listeners.length; i < len; i++) { + var l = listeners[i]; + if (l.ctx !== context) { continue; } + if (l.fn === fn) { + + // set the removed listener to noop so that's not called if remove happens in fire + l.fn = L.Util.falseFn; + + if (this._firingCount) { + /* copy array in case events are being fired */ + this._events[type] = listeners = listeners.slice(); + } + listeners.splice(i, 1); + + return; + } + } + } + }, + + // @method fire(type: String, data?: Object, propagate?: Boolean): this + // Fires an event of the specified type. You can optionally provide an data + // object — the first argument of the listener function will contain its + // properties. The event can optionally be propagated to event parents. + fire: function (type, data, propagate) { + if (!this.listens(type, propagate)) { return this; } + + var event = L.Util.extend({}, data, {type: type, target: this}); + + if (this._events) { + var listeners = this._events[type]; + + if (listeners) { + this._firingCount = (this._firingCount + 1) || 1; + for (var i = 0, len = listeners.length; i < len; i++) { + var l = listeners[i]; + l.fn.call(l.ctx || this, event); + } + + this._firingCount--; + } + } + + if (propagate) { + // propagate the event to parents (set with addEventParent) + this._propagateEvent(event); + } + + return this; + }, + + // @method listens(type: String): Boolean + // Returns `true` if a particular event type has any listeners attached to it. + listens: function (type, propagate) { + var listeners = this._events && this._events[type]; + if (listeners && listeners.length) { return true; } + + if (propagate) { + // also check parents for listeners if event propagates + for (var id in this._eventParents) { + if (this._eventParents[id].listens(type, propagate)) { return true; } + } + } + return false; + }, + + // @method once(…): this + // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed. + once: function (types, fn, context) { + + if (typeof types === 'object') { + for (var type in types) { + this.once(type, types[type], fn); + } + return this; + } + + var handler = L.bind(function () { + this + .off(types, fn, context) + .off(types, handler, context); + }, this); + + // add a listener that's executed once and removed after that + return this + .on(types, fn, context) + .on(types, handler, context); + }, + + // @method addEventParent(obj: Evented): this + // Adds an event parent - an `Evented` that will receive propagated events + addEventParent: function (obj) { + this._eventParents = this._eventParents || {}; + this._eventParents[L.stamp(obj)] = obj; + return this; + }, + + // @method removeEventParent(obj: Evented): this + // Removes an event parent, so it will stop receiving propagated events + removeEventParent: function (obj) { + if (this._eventParents) { + delete this._eventParents[L.stamp(obj)]; + } + return this; + }, + + _propagateEvent: function (e) { + for (var id in this._eventParents) { + this._eventParents[id].fire(e.type, L.extend({layer: e.target}, e), true); + } + } +}); + +var proto = L.Evented.prototype; + +// aliases; we should ditch those eventually + +// @method addEventListener(…): this +// Alias to [`on(…)`](#evented-on) +proto.addEventListener = proto.on; + +// @method removeEventListener(…): this +// Alias to [`off(…)`](#evented-off) + +// @method clearAllEventListeners(…): this +// Alias to [`off()`](#evented-off) +proto.removeEventListener = proto.clearAllEventListeners = proto.off; + +// @method addOneTimeEventListener(…): this +// Alias to [`once(…)`](#evented-once) +proto.addOneTimeEventListener = proto.once; + +// @method fireEvent(…): this +// Alias to [`fire(…)`](#evented-fire) +proto.fireEvent = proto.fire; + +// @method hasEventListeners(…): Boolean +// Alias to [`listens(…)`](#evented-listens) +proto.hasEventListeners = proto.listens; + +L.Mixin = {Events: proto}; + + + +/* + * @namespace Browser + * @aka L.Browser + * + * A namespace with static properties for browser/feature detection used by Leaflet internally. + * + * @example + * + * ```js + * if (L.Browser.ielt9) { + * alert('Upgrade your browser, dude!'); + * } + * ``` + */ + +(function () { + + var ua = navigator.userAgent.toLowerCase(), + doc = document.documentElement, + + ie = 'ActiveXObject' in window, + + webkit = ua.indexOf('webkit') !== -1, + phantomjs = ua.indexOf('phantom') !== -1, + android23 = ua.search('android [23]') !== -1, + chrome = ua.indexOf('chrome') !== -1, + gecko = ua.indexOf('gecko') !== -1 && !webkit && !window.opera && !ie, + + win = navigator.platform.indexOf('Win') === 0, + + mobile = typeof orientation !== 'undefined' || ua.indexOf('mobile') !== -1, + msPointer = !window.PointerEvent && window.MSPointerEvent, + pointer = window.PointerEvent || msPointer, + + ie3d = ie && ('transition' in doc.style), + webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23, + gecko3d = 'MozPerspective' in doc.style, + opera12 = 'OTransition' in doc.style; + + + var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window || + (window.DocumentTouch && document instanceof window.DocumentTouch)); + + L.Browser = { + + // @property ie: Boolean + // `true` for all Internet Explorer versions (not Edge). + ie: ie, + + // @property ielt9: Boolean + // `true` for Internet Explorer versions less than 9. + ielt9: ie && !document.addEventListener, + + // @property edge: Boolean + // `true` for the Edge web browser. + edge: 'msLaunchUri' in navigator && !('documentMode' in document), + + // @property webkit: Boolean + // `true` for webkit-based browsers like Chrome and Safari (including mobile versions). + webkit: webkit, + + // @property gecko: Boolean + // `true` for gecko-based browsers like Firefox. + gecko: gecko, + + // @property android: Boolean + // `true` for any browser running on an Android platform. + android: ua.indexOf('android') !== -1, + + // @property android23: Boolean + // `true` for browsers running on Android 2 or Android 3. + android23: android23, + + // @property chrome: Boolean + // `true` for the Chrome browser. + chrome: chrome, + + // @property safari: Boolean + // `true` for the Safari browser. + safari: !chrome && ua.indexOf('safari') !== -1, + + + // @property win: Boolean + // `true` when the browser is running in a Windows platform + win: win, + + + // @property ie3d: Boolean + // `true` for all Internet Explorer versions supporting CSS transforms. + ie3d: ie3d, + + // @property webkit3d: Boolean + // `true` for webkit-based browsers supporting CSS transforms. + webkit3d: webkit3d, + + // @property gecko3d: Boolean + // `true` for gecko-based browsers supporting CSS transforms. + gecko3d: gecko3d, + + // @property opera12: Boolean + // `true` for the Opera browser supporting CSS transforms (version 12 or later). + opera12: opera12, + + // @property any3d: Boolean + // `true` for all browsers supporting CSS transforms. + any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantomjs, + + + // @property mobile: Boolean + // `true` for all browsers running in a mobile device. + mobile: mobile, + + // @property mobileWebkit: Boolean + // `true` for all webkit-based browsers in a mobile device. + mobileWebkit: mobile && webkit, + + // @property mobileWebkit3d: Boolean + // `true` for all webkit-based browsers in a mobile device supporting CSS transforms. + mobileWebkit3d: mobile && webkit3d, + + // @property mobileOpera: Boolean + // `true` for the Opera browser in a mobile device. + mobileOpera: mobile && window.opera, + + // @property mobileGecko: Boolean + // `true` for gecko-based browsers running in a mobile device. + mobileGecko: mobile && gecko, + + + // @property touch: Boolean + // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events). + // This does not necessarily mean that the browser is running in a computer with + // a touchscreen, it only means that the browser is capable of understanding + // touch events. + touch: !!touch, + + // @property msPointer: Boolean + // `true` for browsers implementing the Microsoft touch events model (notably IE10). + msPointer: !!msPointer, + + // @property pointer: Boolean + // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx). + pointer: !!pointer, + + + // @property retina: Boolean + // `true` for browsers on a high-resolution "retina" screen. + retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1 + }; + +}()); + + + +/* + * @class Point + * @aka L.Point + * + * Represents a point with `x` and `y` coordinates in pixels. + * + * @example + * + * ```js + * var point = L.point(200, 300); + * ``` + * + * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: + * + * ```js + * map.panBy([200, 300]); + * map.panBy(L.point(200, 300)); + * ``` + */ + +L.Point = function (x, y, round) { + // @property x: Number; The `x` coordinate of the point + this.x = (round ? Math.round(x) : x); + // @property y: Number; The `y` coordinate of the point + this.y = (round ? Math.round(y) : y); +}; + +L.Point.prototype = { + + // @method clone(): Point + // Returns a copy of the current point. + clone: function () { + return new L.Point(this.x, this.y); + }, + + // @method add(otherPoint: Point): Point + // Returns the result of addition of the current and the given points. + add: function (point) { + // non-destructive, returns a new point + return this.clone()._add(L.point(point)); + }, + + _add: function (point) { + // destructive, used directly for performance in situations where it's safe to modify existing point + this.x += point.x; + this.y += point.y; + return this; + }, + + // @method subtract(otherPoint: Point): Point + // Returns the result of subtraction of the given point from the current. + subtract: function (point) { + return this.clone()._subtract(L.point(point)); + }, + + _subtract: function (point) { + this.x -= point.x; + this.y -= point.y; + return this; + }, + + // @method divideBy(num: Number): Point + // Returns the result of division of the current point by the given number. + divideBy: function (num) { + return this.clone()._divideBy(num); + }, + + _divideBy: function (num) { + this.x /= num; + this.y /= num; + return this; + }, + + // @method multiplyBy(num: Number): Point + // Returns the result of multiplication of the current point by the given number. + multiplyBy: function (num) { + return this.clone()._multiplyBy(num); + }, + + _multiplyBy: function (num) { + this.x *= num; + this.y *= num; + return this; + }, + + // @method scaleBy(scale: Point): Point + // Multiply each coordinate of the current point by each coordinate of + // `scale`. In linear algebra terms, multiply the point by the + // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) + // defined by `scale`. + scaleBy: function (point) { + return new L.Point(this.x * point.x, this.y * point.y); + }, + + // @method unscaleBy(scale: Point): Point + // Inverse of `scaleBy`. Divide each coordinate of the current point by + // each coordinate of `scale`. + unscaleBy: function (point) { + return new L.Point(this.x / point.x, this.y / point.y); + }, + + // @method round(): Point + // Returns a copy of the current point with rounded coordinates. + round: function () { + return this.clone()._round(); + }, + + _round: function () { + this.x = Math.round(this.x); + this.y = Math.round(this.y); + return this; + }, + + // @method floor(): Point + // Returns a copy of the current point with floored coordinates (rounded down). + floor: function () { + return this.clone()._floor(); + }, + + _floor: function () { + this.x = Math.floor(this.x); + this.y = Math.floor(this.y); + return this; + }, + + // @method ceil(): Point + // Returns a copy of the current point with ceiled coordinates (rounded up). + ceil: function () { + return this.clone()._ceil(); + }, + + _ceil: function () { + this.x = Math.ceil(this.x); + this.y = Math.ceil(this.y); + return this; + }, + + // @method distanceTo(otherPoint: Point): Number + // Returns the cartesian distance between the current and the given points. + distanceTo: function (point) { + point = L.point(point); + + var x = point.x - this.x, + y = point.y - this.y; + + return Math.sqrt(x * x + y * y); + }, + + // @method equals(otherPoint: Point): Boolean + // Returns `true` if the given point has the same coordinates. + equals: function (point) { + point = L.point(point); + + return point.x === this.x && + point.y === this.y; + }, + + // @method contains(otherPoint: Point): Boolean + // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). + contains: function (point) { + point = L.point(point); + + return Math.abs(point.x) <= Math.abs(this.x) && + Math.abs(point.y) <= Math.abs(this.y); + }, + + // @method toString(): String + // Returns a string representation of the point for debugging purposes. + toString: function () { + return 'Point(' + + L.Util.formatNum(this.x) + ', ' + + L.Util.formatNum(this.y) + ')'; + } +}; + +// @factory L.point(x: Number, y: Number, round?: Boolean) +// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. + +// @alternative +// @factory L.point(coords: Number[]) +// Expects an array of the form `[x, y]` instead. + +// @alternative +// @factory L.point(coords: Object) +// Expects a plain object of the form `{x: Number, y: Number}` instead. +L.point = function (x, y, round) { + if (x instanceof L.Point) { + return x; + } + if (L.Util.isArray(x)) { + return new L.Point(x[0], x[1]); + } + if (x === undefined || x === null) { + return x; + } + if (typeof x === 'object' && 'x' in x && 'y' in x) { + return new L.Point(x.x, x.y); + } + return new L.Point(x, y, round); +}; + + + +/* + * @class Bounds + * @aka L.Bounds + * + * Represents a rectangular area in pixel coordinates. + * + * @example + * + * ```js + * var p1 = L.point(10, 10), + * p2 = L.point(40, 60), + * bounds = L.bounds(p1, p2); + * ``` + * + * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: + * + * ```js + * otherBounds.intersects([[10, 10], [40, 60]]); + * ``` + */ + +L.Bounds = function (a, b) { + if (!a) { return; } + + var points = b ? [a, b] : a; + + for (var i = 0, len = points.length; i < len; i++) { + this.extend(points[i]); + } +}; + +L.Bounds.prototype = { + // @method extend(point: Point): this + // Extends the bounds to contain the given point. + extend: function (point) { // (Point) + point = L.point(point); + + // @property min: Point + // The top left corner of the rectangle. + // @property max: Point + // The bottom right corner of the rectangle. + if (!this.min && !this.max) { + this.min = point.clone(); + this.max = point.clone(); + } else { + this.min.x = Math.min(point.x, this.min.x); + this.max.x = Math.max(point.x, this.max.x); + this.min.y = Math.min(point.y, this.min.y); + this.max.y = Math.max(point.y, this.max.y); + } + return this; + }, + + // @method getCenter(round?: Boolean): Point + // Returns the center point of the bounds. + getCenter: function (round) { + return new L.Point( + (this.min.x + this.max.x) / 2, + (this.min.y + this.max.y) / 2, round); + }, + + // @method getBottomLeft(): Point + // Returns the bottom-left point of the bounds. + getBottomLeft: function () { + return new L.Point(this.min.x, this.max.y); + }, + + // @method getTopRight(): Point + // Returns the top-right point of the bounds. + getTopRight: function () { // -> Point + return new L.Point(this.max.x, this.min.y); + }, + + // @method getSize(): Point + // Returns the size of the given bounds + getSize: function () { + return this.max.subtract(this.min); + }, + + // @method contains(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle contains the given one. + // @alternative + // @method contains(point: Point): Boolean + // Returns `true` if the rectangle contains the given point. + contains: function (obj) { + var min, max; + + if (typeof obj[0] === 'number' || obj instanceof L.Point) { + obj = L.point(obj); + } else { + obj = L.bounds(obj); + } + + if (obj instanceof L.Bounds) { + min = obj.min; + max = obj.max; + } else { + min = max = obj; + } + + return (min.x >= this.min.x) && + (max.x <= this.max.x) && + (min.y >= this.min.y) && + (max.y <= this.max.y); + }, + + // @method intersects(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle intersects the given bounds. Two bounds + // intersect if they have at least one point in common. + intersects: function (bounds) { // (Bounds) -> Boolean + bounds = L.bounds(bounds); + + var min = this.min, + max = this.max, + min2 = bounds.min, + max2 = bounds.max, + xIntersects = (max2.x >= min.x) && (min2.x <= max.x), + yIntersects = (max2.y >= min.y) && (min2.y <= max.y); + + return xIntersects && yIntersects; + }, + + // @method overlaps(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle overlaps the given bounds. Two bounds + // overlap if their intersection is an area. + overlaps: function (bounds) { // (Bounds) -> Boolean + bounds = L.bounds(bounds); + + var min = this.min, + max = this.max, + min2 = bounds.min, + max2 = bounds.max, + xOverlaps = (max2.x > min.x) && (min2.x < max.x), + yOverlaps = (max2.y > min.y) && (min2.y < max.y); + + return xOverlaps && yOverlaps; + }, + + isValid: function () { + return !!(this.min && this.max); + } +}; + + +// @factory L.bounds(topLeft: Point, bottomRight: Point) +// Creates a Bounds object from two coordinates (usually top-left and bottom-right corners). +// @alternative +// @factory L.bounds(points: Point[]) +// Creates a Bounds object from the points it contains +L.bounds = function (a, b) { + if (!a || a instanceof L.Bounds) { + return a; + } + return new L.Bounds(a, b); +}; + + + +/* + * @class Transformation + * @aka L.Transformation + * + * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d` + * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing + * the reverse. Used by Leaflet in its projections code. + * + * @example + * + * ```js + * var transformation = new L.Transformation(2, 5, -1, 10), + * p = L.point(1, 2), + * p2 = transformation.transform(p), // L.point(7, 8) + * p3 = transformation.untransform(p2); // L.point(1, 2) + * ``` + */ + + +// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number) +// Creates a `Transformation` object with the given coefficients. +L.Transformation = function (a, b, c, d) { + this._a = a; + this._b = b; + this._c = c; + this._d = d; +}; + +L.Transformation.prototype = { + // @method transform(point: Point, scale?: Number): Point + // Returns a transformed point, optionally multiplied by the given scale. + // Only accepts actual `L.Point` instances, not arrays. + transform: function (point, scale) { // (Point, Number) -> Point + return this._transform(point.clone(), scale); + }, + + // destructive transform (faster) + _transform: function (point, scale) { + scale = scale || 1; + point.x = scale * (this._a * point.x + this._b); + point.y = scale * (this._c * point.y + this._d); + return point; + }, + + // @method untransform(point: Point, scale?: Number): Point + // Returns the reverse transformation of the given point, optionally divided + // by the given scale. Only accepts actual `L.Point` instances, not arrays. + untransform: function (point, scale) { + scale = scale || 1; + return new L.Point( + (point.x / scale - this._b) / this._a, + (point.y / scale - this._d) / this._c); + } +}; + + + +/* + * @namespace DomUtil + * + * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model) + * tree, used by Leaflet internally. + * + * Most functions expecting or returning a `HTMLElement` also work for + * SVG elements. The only difference is that classes refer to CSS classes + * in HTML and SVG classes in SVG. + */ + +L.DomUtil = { + + // @function get(id: String|HTMLElement): HTMLElement + // Returns an element given its DOM id, or returns the element itself + // if it was passed directly. + get: function (id) { + return typeof id === 'string' ? document.getElementById(id) : id; + }, + + // @function getStyle(el: HTMLElement, styleAttrib: String): String + // Returns the value for a certain style attribute on an element, + // including computed values or values set through CSS. + getStyle: function (el, style) { + + var value = el.style[style] || (el.currentStyle && el.currentStyle[style]); + + if ((!value || value === 'auto') && document.defaultView) { + var css = document.defaultView.getComputedStyle(el, null); + value = css ? css[style] : null; + } + + return value === 'auto' ? null : value; + }, + + // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement + // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element. + create: function (tagName, className, container) { + + var el = document.createElement(tagName); + el.className = className || ''; + + if (container) { + container.appendChild(el); + } + + return el; + }, + + // @function remove(el: HTMLElement) + // Removes `el` from its parent element + remove: function (el) { + var parent = el.parentNode; + if (parent) { + parent.removeChild(el); + } + }, + + // @function empty(el: HTMLElement) + // Removes all of `el`'s children elements from `el` + empty: function (el) { + while (el.firstChild) { + el.removeChild(el.firstChild); + } + }, + + // @function toFront(el: HTMLElement) + // Makes `el` the last children of its parent, so it renders in front of the other children. + toFront: function (el) { + el.parentNode.appendChild(el); + }, + + // @function toBack(el: HTMLElement) + // Makes `el` the first children of its parent, so it renders back from the other children. + toBack: function (el) { + var parent = el.parentNode; + parent.insertBefore(el, parent.firstChild); + }, + + // @function hasClass(el: HTMLElement, name: String): Boolean + // Returns `true` if the element's class attribute contains `name`. + hasClass: function (el, name) { + if (el.classList !== undefined) { + return el.classList.contains(name); + } + var className = L.DomUtil.getClass(el); + return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); + }, + + // @function addClass(el: HTMLElement, name: String) + // Adds `name` to the element's class attribute. + addClass: function (el, name) { + if (el.classList !== undefined) { + var classes = L.Util.splitWords(name); + for (var i = 0, len = classes.length; i < len; i++) { + el.classList.add(classes[i]); + } + } else if (!L.DomUtil.hasClass(el, name)) { + var className = L.DomUtil.getClass(el); + L.DomUtil.setClass(el, (className ? className + ' ' : '') + name); + } + }, + + // @function removeClass(el: HTMLElement, name: String) + // Removes `name` from the element's class attribute. + removeClass: function (el, name) { + if (el.classList !== undefined) { + el.classList.remove(name); + } else { + L.DomUtil.setClass(el, L.Util.trim((' ' + L.DomUtil.getClass(el) + ' ').replace(' ' + name + ' ', ' '))); + } + }, + + // @function setClass(el: HTMLElement, name: String) + // Sets the element's class. + setClass: function (el, name) { + if (el.className.baseVal === undefined) { + el.className = name; + } else { + // in case of SVG element + el.className.baseVal = name; + } + }, + + // @function getClass(el: HTMLElement): String + // Returns the element's class. + getClass: function (el) { + return el.className.baseVal === undefined ? el.className : el.className.baseVal; + }, + + // @function setOpacity(el: HTMLElement, opacity: Number) + // Set the opacity of an element (including old IE support). + // `opacity` must be a number from `0` to `1`. + setOpacity: function (el, value) { + + if ('opacity' in el.style) { + el.style.opacity = value; + + } else if ('filter' in el.style) { + L.DomUtil._setOpacityIE(el, value); + } + }, + + _setOpacityIE: function (el, value) { + var filter = false, + filterName = 'DXImageTransform.Microsoft.Alpha'; + + // filters collection throws an error if we try to retrieve a filter that doesn't exist + try { + filter = el.filters.item(filterName); + } catch (e) { + // don't set opacity to 1 if we haven't already set an opacity, + // it isn't needed and breaks transparent pngs. + if (value === 1) { return; } + } + + value = Math.round(value * 100); + + if (filter) { + filter.Enabled = (value !== 100); + filter.Opacity = value; + } else { + el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; + } + }, + + // @function testProp(props: String[]): String|false + // Goes through the array of style names and returns the first name + // that is a valid style name for an element. If no such name is found, + // it returns false. Useful for vendor-prefixed styles like `transform`. + testProp: function (props) { + + var style = document.documentElement.style; + + for (var i = 0; i < props.length; i++) { + if (props[i] in style) { + return props[i]; + } + } + return false; + }, + + // @function setTransform(el: HTMLElement, offset: Point, scale?: Number) + // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels + // and optionally scaled by `scale`. Does not have an effect if the + // browser doesn't support 3D CSS transforms. + setTransform: function (el, offset, scale) { + var pos = offset || new L.Point(0, 0); + + el.style[L.DomUtil.TRANSFORM] = + (L.Browser.ie3d ? + 'translate(' + pos.x + 'px,' + pos.y + 'px)' : + 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + + (scale ? ' scale(' + scale + ')' : ''); + }, + + // @function setPosition(el: HTMLElement, position: Point) + // Sets the position of `el` to coordinates specified by `position`, + // using CSS translate or top/left positioning depending on the browser + // (used by Leaflet internally to position its layers). + setPosition: function (el, point) { // (HTMLElement, Point[, Boolean]) + + /*eslint-disable */ + el._leaflet_pos = point; + /*eslint-enable */ + + if (L.Browser.any3d) { + L.DomUtil.setTransform(el, point); + } else { + el.style.left = point.x + 'px'; + el.style.top = point.y + 'px'; + } + }, + + // @function getPosition(el: HTMLElement): Point + // Returns the coordinates of an element previously positioned with setPosition. + getPosition: function (el) { + // this method is only used for elements previously positioned using setPosition, + // so it's safe to cache the position for performance + + return el._leaflet_pos || new L.Point(0, 0); + } +}; + + +(function () { + // prefix style property names + + // @property TRANSFORM: String + // Vendor-prefixed fransform style name (e.g. `'webkitTransform'` for WebKit). + L.DomUtil.TRANSFORM = L.DomUtil.testProp( + ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); + + + // webkitTransition comes first because some browser versions that drop vendor prefix don't do + // the same for the transitionend event, in particular the Android 4.1 stock browser + + // @property TRANSITION: String + // Vendor-prefixed transform style name. + var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp( + ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); + + L.DomUtil.TRANSITION_END = + transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend'; + + // @function disableTextSelection() + // Prevents the user from generating `selectstart` DOM events, usually generated + // when the user drags the mouse through a page with text. Used internally + // by Leaflet to override the behaviour of any click-and-drag interaction on + // the map. Affects drag interactions on the whole document. + + // @function enableTextSelection() + // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection). + if ('onselectstart' in document) { + L.DomUtil.disableTextSelection = function () { + L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); + }; + L.DomUtil.enableTextSelection = function () { + L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); + }; + + } else { + var userSelectProperty = L.DomUtil.testProp( + ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); + + L.DomUtil.disableTextSelection = function () { + if (userSelectProperty) { + var style = document.documentElement.style; + this._userSelect = style[userSelectProperty]; + style[userSelectProperty] = 'none'; + } + }; + L.DomUtil.enableTextSelection = function () { + if (userSelectProperty) { + document.documentElement.style[userSelectProperty] = this._userSelect; + delete this._userSelect; + } + }; + } + + // @function disableImageDrag() + // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but + // for `dragstart` DOM events, usually generated when the user drags an image. + L.DomUtil.disableImageDrag = function () { + L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); + }; + + // @function enableImageDrag() + // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection). + L.DomUtil.enableImageDrag = function () { + L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault); + }; + + // @function preventOutline(el: HTMLElement) + // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline) + // of the element `el` invisible. Used internally by Leaflet to prevent + // focusable elements from displaying an outline when the user performs a + // drag interaction on them. + L.DomUtil.preventOutline = function (element) { + while (element.tabIndex === -1) { + element = element.parentNode; + } + if (!element || !element.style) { return; } + L.DomUtil.restoreOutline(); + this._outlineElement = element; + this._outlineStyle = element.style.outline; + element.style.outline = 'none'; + L.DomEvent.on(window, 'keydown', L.DomUtil.restoreOutline, this); + }; + + // @function restoreOutline() + // Cancels the effects of a previous [`L.DomUtil.preventOutline`](). + L.DomUtil.restoreOutline = function () { + if (!this._outlineElement) { return; } + this._outlineElement.style.outline = this._outlineStyle; + delete this._outlineElement; + delete this._outlineStyle; + L.DomEvent.off(window, 'keydown', L.DomUtil.restoreOutline, this); + }; +})(); + + + +/* @class LatLng + * @aka L.LatLng + * + * Represents a geographical point with a certain latitude and longitude. + * + * @example + * + * ``` + * var latlng = L.latLng(50.5, 30.5); + * ``` + * + * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: + * + * ``` + * map.panTo([50, 30]); + * map.panTo({lon: 30, lat: 50}); + * map.panTo({lat: 50, lng: 30}); + * map.panTo(L.latLng(50, 30)); + * ``` + */ + +L.LatLng = function (lat, lng, alt) { + if (isNaN(lat) || isNaN(lng)) { + throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); + } + + // @property lat: Number + // Latitude in degrees + this.lat = +lat; + + // @property lng: Number + // Longitude in degrees + this.lng = +lng; + + // @property alt: Number + // Altitude in meters (optional) + if (alt !== undefined) { + this.alt = +alt; + } +}; + +L.LatLng.prototype = { + // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean + // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overriden by setting `maxMargin` to a small number. + equals: function (obj, maxMargin) { + if (!obj) { return false; } + + obj = L.latLng(obj); + + var margin = Math.max( + Math.abs(this.lat - obj.lat), + Math.abs(this.lng - obj.lng)); + + return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); + }, + + // @method toString(): String + // Returns a string representation of the point (for debugging purposes). + toString: function (precision) { + return 'LatLng(' + + L.Util.formatNum(this.lat, precision) + ', ' + + L.Util.formatNum(this.lng, precision) + ')'; + }, + + // @method distanceTo(otherLatLng: LatLng): Number + // Returns the distance (in meters) to the given `LatLng` calculated using the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula). + distanceTo: function (other) { + return L.CRS.Earth.distance(this, L.latLng(other)); + }, + + // @method wrap(): LatLng + // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. + wrap: function () { + return L.CRS.Earth.wrapLatLng(this); + }, + + // @method toBounds(sizeInMeters: Number): LatLngBounds + // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. + toBounds: function (sizeInMeters) { + var latAccuracy = 180 * sizeInMeters / 40075017, + lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); + + return L.latLngBounds( + [this.lat - latAccuracy, this.lng - lngAccuracy], + [this.lat + latAccuracy, this.lng + lngAccuracy]); + }, + + clone: function () { + return new L.LatLng(this.lat, this.lng, this.alt); + } +}; + + + +// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng +// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). + +// @alternative +// @factory L.latLng(coords: Array): LatLng +// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. + +// @alternative +// @factory L.latLng(coords: Object): LatLng +// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. + +L.latLng = function (a, b, c) { + if (a instanceof L.LatLng) { + return a; + } + if (L.Util.isArray(a) && typeof a[0] !== 'object') { + if (a.length === 3) { + return new L.LatLng(a[0], a[1], a[2]); + } + if (a.length === 2) { + return new L.LatLng(a[0], a[1]); + } + return null; + } + if (a === undefined || a === null) { + return a; + } + if (typeof a === 'object' && 'lat' in a) { + return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); + } + if (b === undefined) { + return null; + } + return new L.LatLng(a, b, c); +}; + + + +/* + * @class LatLngBounds + * @aka L.LatLngBounds + * + * Represents a rectangular geographical area on a map. + * + * @example + * + * ```js + * var corner1 = L.latLng(40.712, -74.227), + * corner2 = L.latLng(40.774, -74.125), + * bounds = L.latLngBounds(corner1, corner2); + * ``` + * + * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: + * + * ```js + * map.fitBounds([ + * [40.712, -74.227], + * [40.774, -74.125] + * ]); + * ``` + * + * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. + */ + +L.LatLngBounds = function (corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) + if (!corner1) { return; } + + var latlngs = corner2 ? [corner1, corner2] : corner1; + + for (var i = 0, len = latlngs.length; i < len; i++) { + this.extend(latlngs[i]); + } +}; + +L.LatLngBounds.prototype = { + + // @method extend(latlng: LatLng): this + // Extend the bounds to contain the given point + + // @alternative + // @method extend(otherBounds: LatLngBounds): this + // Extend the bounds to contain the given bounds + extend: function (obj) { + var sw = this._southWest, + ne = this._northEast, + sw2, ne2; + + if (obj instanceof L.LatLng) { + sw2 = obj; + ne2 = obj; + + } else if (obj instanceof L.LatLngBounds) { + sw2 = obj._southWest; + ne2 = obj._northEast; + + if (!sw2 || !ne2) { return this; } + + } else { + return obj ? this.extend(L.latLng(obj) || L.latLngBounds(obj)) : this; + } + + if (!sw && !ne) { + this._southWest = new L.LatLng(sw2.lat, sw2.lng); + this._northEast = new L.LatLng(ne2.lat, ne2.lng); + } else { + sw.lat = Math.min(sw2.lat, sw.lat); + sw.lng = Math.min(sw2.lng, sw.lng); + ne.lat = Math.max(ne2.lat, ne.lat); + ne.lng = Math.max(ne2.lng, ne.lng); + } + + return this; + }, + + // @method pad(bufferRatio: Number): LatLngBounds + // Returns bigger bounds created by extending the current bounds by a given percentage in each direction. + pad: function (bufferRatio) { + var sw = this._southWest, + ne = this._northEast, + heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, + widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; + + return new L.LatLngBounds( + new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), + new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); + }, + + // @method getCenter(): LatLng + // Returns the center point of the bounds. + getCenter: function () { + return new L.LatLng( + (this._southWest.lat + this._northEast.lat) / 2, + (this._southWest.lng + this._northEast.lng) / 2); + }, + + // @method getSouthWest(): LatLng + // Returns the south-west point of the bounds. + getSouthWest: function () { + return this._southWest; + }, + + // @method getNorthEast(): LatLng + // Returns the north-east point of the bounds. + getNorthEast: function () { + return this._northEast; + }, + + // @method getNorthWest(): LatLng + // Returns the north-west point of the bounds. + getNorthWest: function () { + return new L.LatLng(this.getNorth(), this.getWest()); + }, + + // @method getSouthEast(): LatLng + // Returns the south-east point of the bounds. + getSouthEast: function () { + return new L.LatLng(this.getSouth(), this.getEast()); + }, + + // @method getWest(): Number + // Returns the west longitude of the bounds + getWest: function () { + return this._southWest.lng; + }, + + // @method getSouth(): Number + // Returns the south latitude of the bounds + getSouth: function () { + return this._southWest.lat; + }, + + // @method getEast(): Number + // Returns the east longitude of the bounds + getEast: function () { + return this._northEast.lng; + }, + + // @method getNorth(): Number + // Returns the north latitude of the bounds + getNorth: function () { + return this._northEast.lat; + }, + + // @method contains(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle contains the given one. + + // @alternative + // @method contains (latlng: LatLng): Boolean + // Returns `true` if the rectangle contains the given point. + contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean + if (typeof obj[0] === 'number' || obj instanceof L.LatLng || 'lat' in obj) { + obj = L.latLng(obj); + } else { + obj = L.latLngBounds(obj); + } + + var sw = this._southWest, + ne = this._northEast, + sw2, ne2; + + if (obj instanceof L.LatLngBounds) { + sw2 = obj.getSouthWest(); + ne2 = obj.getNorthEast(); + } else { + sw2 = ne2 = obj; + } + + return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && + (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); + }, + + // @method intersects(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. + intersects: function (bounds) { + bounds = L.latLngBounds(bounds); + + var sw = this._southWest, + ne = this._northEast, + sw2 = bounds.getSouthWest(), + ne2 = bounds.getNorthEast(), + + latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), + lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); + + return latIntersects && lngIntersects; + }, + + // @method overlaps(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. + overlaps: function (bounds) { + bounds = L.latLngBounds(bounds); + + var sw = this._southWest, + ne = this._northEast, + sw2 = bounds.getSouthWest(), + ne2 = bounds.getNorthEast(), + + latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), + lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); + + return latOverlaps && lngOverlaps; + }, + + // @method toBBoxString(): String + // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. + toBBoxString: function () { + return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); + }, + + // @method equals(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. + equals: function (bounds) { + if (!bounds) { return false; } + + bounds = L.latLngBounds(bounds); + + return this._southWest.equals(bounds.getSouthWest()) && + this._northEast.equals(bounds.getNorthEast()); + }, + + // @method isValid(): Boolean + // Returns `true` if the bounds are properly initialized. + isValid: function () { + return !!(this._southWest && this._northEast); + } +}; + +// TODO International date line? + +// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng) +// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. + +// @alternative +// @factory L.latLngBounds(latlngs: LatLng[]) +// Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). +L.latLngBounds = function (a, b) { + if (a instanceof L.LatLngBounds) { + return a; + } + return new L.LatLngBounds(a, b); +}; + + + +/* + * @namespace Projection + * @section + * Leaflet comes with a set of already defined Projections out of the box: + * + * @projection L.Projection.LonLat + * + * Equirectangular, or Plate Carree projection — the most simple projection, + * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as + * latitude. Also suitable for flat worlds, e.g. game maps. Used by the + * `EPSG:3395` and `Simple` CRS. + */ + +L.Projection = {}; + +L.Projection.LonLat = { + project: function (latlng) { + return new L.Point(latlng.lng, latlng.lat); + }, + + unproject: function (point) { + return new L.LatLng(point.y, point.x); + }, + + bounds: L.bounds([-180, -90], [180, 90]) +}; + + + +/* + * @namespace Projection + * @projection L.Projection.SphericalMercator + * + * Spherical Mercator projection — the most common projection for online maps, + * used by almost all free and commercial tile providers. Assumes that Earth is + * a sphere. Used by the `EPSG:3857` CRS. + */ + +L.Projection.SphericalMercator = { + + R: 6378137, + MAX_LATITUDE: 85.0511287798, + + project: function (latlng) { + var d = Math.PI / 180, + max = this.MAX_LATITUDE, + lat = Math.max(Math.min(max, latlng.lat), -max), + sin = Math.sin(lat * d); + + return new L.Point( + this.R * latlng.lng * d, + this.R * Math.log((1 + sin) / (1 - sin)) / 2); + }, + + unproject: function (point) { + var d = 180 / Math.PI; + + return new L.LatLng( + (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, + point.x * d / this.R); + }, + + bounds: (function () { + var d = 6378137 * Math.PI; + return L.bounds([-d, -d], [d, d]); + })() +}; + + + +/* + * @class CRS + * @aka L.CRS + * Abstract class that defines coordinate reference systems for projecting + * geographical points into pixel (screen) coordinates and back (and to + * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See + * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system). + * + * Leaflet defines the most usual CRSs by default. If you want to use a + * CRS not defined by default, take a look at the + * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. + */ + +L.CRS = { + // @method latLngToPoint(latlng: LatLng, zoom: Number): Point + // Projects geographical coordinates into pixel coordinates for a given zoom. + latLngToPoint: function (latlng, zoom) { + var projectedPoint = this.projection.project(latlng), + scale = this.scale(zoom); + + return this.transformation._transform(projectedPoint, scale); + }, + + // @method pointToLatLng(point: Point, zoom: Number): LatLng + // The inverse of `latLngToPoint`. Projects pixel coordinates on a given + // zoom into geographical coordinates. + pointToLatLng: function (point, zoom) { + var scale = this.scale(zoom), + untransformedPoint = this.transformation.untransform(point, scale); + + return this.projection.unproject(untransformedPoint); + }, + + // @method project(latlng: LatLng): Point + // Projects geographical coordinates into coordinates in units accepted for + // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). + project: function (latlng) { + return this.projection.project(latlng); + }, + + // @method unproject(point: Point): LatLng + // Given a projected coordinate returns the corresponding LatLng. + // The inverse of `project`. + unproject: function (point) { + return this.projection.unproject(point); + }, + + // @method scale(zoom: Number): Number + // Returns the scale used when transforming projected coordinates into + // pixel coordinates for a particular zoom. For example, it returns + // `256 * 2^zoom` for Mercator-based CRS. + scale: function (zoom) { + return 256 * Math.pow(2, zoom); + }, + + // @method zoom(scale: Number): Number + // Inverse of `scale()`, returns the zoom level corresponding to a scale + // factor of `scale`. + zoom: function (scale) { + return Math.log(scale / 256) / Math.LN2; + }, + + // @method getProjectedBounds(zoom: Number): Bounds + // Returns the projection's bounds scaled and transformed for the provided `zoom`. + getProjectedBounds: function (zoom) { + if (this.infinite) { return null; } + + var b = this.projection.bounds, + s = this.scale(zoom), + min = this.transformation.transform(b.min, s), + max = this.transformation.transform(b.max, s); + + return L.bounds(min, max); + }, + + // @method distance(latlng1: LatLng, latlng2: LatLng): Number + // Returns the distance between two geographical coordinates. + + // @property code: String + // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) + // + // @property wrapLng: Number[] + // An array of two numbers defining whether the longitude (horizontal) coordinate + // axis wraps around a given range and how. Defaults to `[-180, 180]` in most + // geographical CRSs. If `undefined`, the longitude axis does not wrap around. + // + // @property wrapLat: Number[] + // Like `wrapLng`, but for the latitude (vertical) axis. + + // wrapLng: [min, max], + // wrapLat: [min, max], + + // @property infinite: Boolean + // If true, the coordinate space will be unbounded (infinite in both axes) + infinite: false, + + // @method wrapLatLng(latlng: LatLng): LatLng + // Returns a `LatLng` where lat and lng has been wrapped according to the + // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. + // Only accepts actual `L.LatLng` instances, not arrays. + wrapLatLng: function (latlng) { + var lng = this.wrapLng ? L.Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, + lat = this.wrapLat ? L.Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, + alt = latlng.alt; + + return L.latLng(lat, lng, alt); + }, + + // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds + // Returns a `LatLngBounds` with the same size as the given one, ensuring + // that its center is within the CRS's bounds. + // Only accepts actual `L.LatLngBounds` instances, not arrays. + wrapLatLngBounds: function (bounds) { + var center = bounds.getCenter(), + newCenter = this.wrapLatLng(center), + latShift = center.lat - newCenter.lat, + lngShift = center.lng - newCenter.lng; + + if (latShift === 0 && lngShift === 0) { + return bounds; + } + + var sw = bounds.getSouthWest(), + ne = bounds.getNorthEast(), + newSw = L.latLng({lat: sw.lat - latShift, lng: sw.lng - lngShift}), + newNe = L.latLng({lat: ne.lat - latShift, lng: ne.lng - lngShift}); + + return new L.LatLngBounds(newSw, newNe); + } +}; - // Checks whether a value is thruthy or false or 0 - function is_kinda_truthy(bool) { - return bool === false || bool === 0 || bool; - } - var value; - // check for dot notation eg. foo.bar - if (name.match(/([a-z_]+)\./ig)) { - var childValue = this.walk_context(name, context); - if (is_kinda_truthy(childValue)) { - value = childValue; - } - } else { - if (is_kinda_truthy(context[name])) { - value = context[name]; - } else if (is_kinda_truthy(this.context[name])) { - value = this.context[name]; - } - } +/* + * @namespace CRS + * @crs L.CRS.Simple + * + * A simple CRS that maps longitude and latitude into `x` and `y` directly. + * May be used for maps of flat surfaces (e.g. game maps). Note that the `y` + * axis should still be inverted (going from bottom to top). `distance()` returns + * simple euclidean distance. + */ - if (typeof value == "function") { - return value.apply(context); - } - if (value !== undefined) { - return value; - } - // silently ignore unkown variables - return ""; - }, +L.CRS.Simple = L.extend({}, L.CRS, { + projection: L.Projection.LonLat, + transformation: new L.Transformation(1, 0, -1, 0), - walk_context: function (name, context) { - var path = name.split('.'); - // if the var doesn't exist in current context, check the top level context - var value_context = (context[path[0]] != undefined) ? context : this.context; - var value = value_context[path.shift()]; - while (value != undefined && path.length > 0) { - value_context = value; - value = value[path.shift()]; - } - // if the value is a function, call it, binding the correct context - if (typeof value == "function") { - return value.apply(value_context); - } - return value; - }, + scale: function (zoom) { + return Math.pow(2, zoom); + }, - // Utility methods + zoom: function (scale) { + return Math.log(scale) / Math.LN2; + }, - /* includes tag */ - includes: function (needle, haystack) { - return haystack.indexOf(this.otag + needle) != -1; - }, + distance: function (latlng1, latlng2) { + var dx = latlng2.lng - latlng1.lng, + dy = latlng2.lat - latlng1.lat; - // by @langalex, support for arrays of strings - create_context: function (_context) { - if (this.is_object(_context)) { - return _context; - } else { - var iterator = "."; - if (this.pragmas["IMPLICIT-ITERATOR"]) { - iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; - } - var ctx = {}; - ctx[iterator] = _context; - return ctx; - } - }, + return Math.sqrt(dx * dx + dy * dy); + }, - is_object: function (a) { - return a && typeof a == "object"; - }, + infinite: true +}); - /* - Why, why, why? Because IE. Cry, cry cry. - */ - map: function (array, fn) { - if (typeof array.map == "function") { - return array.map(fn); - } else { - var r = []; - var l = array.length; - for(var i = 0; i < l; i++) { - r.push(fn(array[i])); - } - return r; - } - }, - getCachedRegex: function (name, generator) { - var byOtag = regexCache[this.otag]; - if (!byOtag) { - byOtag = regexCache[this.otag] = {}; - } - var byCtag = byOtag[this.ctag]; - if (!byCtag) { - byCtag = byOtag[this.ctag] = {}; - } +/* + * @namespace CRS + * @crs L.CRS.Earth + * + * Serves as the base for CRS that are global such that they cover the earth. + * Can only be used as the base for other CRS and cannot be used directly, + * since it does not have a `code`, `projection` or `transformation`. `distance()` returns + * meters. + */ + +L.CRS.Earth = L.extend({}, L.CRS, { + wrapLng: [-180, 180], + + // Mean Earth Radius, as recommended for use by + // the International Union of Geodesy and Geophysics, + // see http://rosettacode.org/wiki/Haversine_formula + R: 6371000, + + // distance between two geographical points using spherical law of cosines approximation + distance: function (latlng1, latlng2) { + var rad = Math.PI / 180, + lat1 = latlng1.lat * rad, + lat2 = latlng2.lat * rad, + a = Math.sin(lat1) * Math.sin(lat2) + + Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad); + + return this.R * Math.acos(Math.min(a, 1)); + } +}); - var regex = byCtag[name]; - if (!regex) { - regex = byCtag[name] = generator(this.otag, this.ctag); - } - return regex; - } - }; - return({ - name: "mustache.js", - version: "0.4.0", - - /* - Turns a template and view into HTML - */ - to_html: function (template, view, partials, send_fun) { - var renderer = new Renderer(); - if (send_fun) { - renderer.send = send_fun; - } - renderer.render(template, view || {}, partials); - if (!send_fun) { - return renderer.buffer.join("\n"); - } - } - }); -}(); -/*! - ICanHaz.js -- by @HenrikJoreteg -*/ -/*global */ -(function () { - function trim(stuff) { - if (''.trim) return stuff.trim(); - else return stuff.replace(/^\s+/, '').replace(/\s+$/, ''); - } +/* + * @namespace CRS + * @crs L.CRS.EPSG3857 + * + * The most common CRS for online maps, used by almost all free and commercial + * tile providers. Uses Spherical Mercator projection. Set in by default in + * Map's `crs` option. + */ + +L.CRS.EPSG3857 = L.extend({}, L.CRS.Earth, { + code: 'EPSG:3857', + projection: L.Projection.SphericalMercator, + + transformation: (function () { + var scale = 0.5 / (Math.PI * L.Projection.SphericalMercator.R); + return new L.Transformation(scale, 0.5, -scale, 0.5); + }()) +}); + +L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { + code: 'EPSG:900913' +}); + + + +/* + * @namespace CRS + * @crs L.CRS.EPSG4326 + * + * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. + * + * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic), + * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer` + * with this CRS, ensure that there are two 256x256 pixel tiles covering the + * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90), + * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set. + */ + +L.CRS.EPSG4326 = L.extend({}, L.CRS.Earth, { + code: 'EPSG:4326', + projection: L.Projection.LonLat, + transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5) +}); + + + +/* + * @class Map + * @aka L.Map + * @inherits Evented + * + * The central class of the API — it is used to create a map on a page and manipulate it. + * + * @example + * + * ```js + * // initialize the map on the "map" div with a given center and zoom + * var map = L.map('map', { + * center: [51.505, -0.09], + * zoom: 13 + * }); + * ``` + * + */ + +L.Map = L.Evented.extend({ + + options: { + // @section Map State Options + // @option crs: CRS = L.CRS.EPSG3857 + // The [Coordinate Reference System](#crs) to use. Don't change this if you're not + // sure what it means. + crs: L.CRS.EPSG3857, + + // @option center: LatLng = undefined + // Initial geographic center of the map + center: undefined, + + // @option zoom: Number = undefined + // Initial map zoom level + zoom: undefined, + + // @option minZoom: Number = undefined + // Minimum zoom level of the map. Overrides any `minZoom` option set on map layers. + minZoom: undefined, + + // @option maxZoom: Number = undefined + // Maximum zoom level of the map. Overrides any `maxZoom` option set on map layers. + maxZoom: undefined, + + // @option layers: Layer[] = [] + // Array of layers that will be added to the map initially + layers: [], + + // @option maxBounds: LatLngBounds = null + // When this option is set, the map restricts the view to the given + // geographical bounds, bouncing the user back if the user tries to pan + // outside the view. To set the restriction dynamically, use + // [`setMaxBounds`](#map-setmaxbounds) method. + maxBounds: undefined, + + // @option renderer: Renderer = * + // The default method for drawing vector layers on the map. `L.SVG` + // or `L.Canvas` by default depending on browser support. + renderer: undefined, + + + // @section Animation Options + // @option zoomAnimation: Boolean = true + // Whether the map zoom animation is enabled. By default it's enabled + // in all browsers that support CSS3 Transitions except Android. + zoomAnimation: true, + + // @option zoomAnimationThreshold: Number = 4 + // Won't animate zoom if the zoom difference exceeds this value. + zoomAnimationThreshold: 4, + + // @option fadeAnimation: Boolean = true + // Whether the tile fade animation is enabled. By default it's enabled + // in all browsers that support CSS3 Transitions except Android. + fadeAnimation: true, + + // @option markerZoomAnimation: Boolean = true + // Whether markers animate their zoom with the zoom animation, if disabled + // they will disappear for the length of the animation. By default it's + // enabled in all browsers that support CSS3 Transitions except Android. + markerZoomAnimation: true, + + // @option transform3DLimit: Number = 2^23 + // Defines the maximum size of a CSS translation transform. The default + // value should not be changed unless a web browser positions layers in + // the wrong place after doing a large `panBy`. + transform3DLimit: 8388608, // Precision limit of a 32-bit float + + // @section Interaction Options + // @option zoomSnap: Number = 1 + // Forces the map's zoom level to always be a multiple of this, particularly + // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom. + // By default, the zoom level snaps to the nearest integer; lower values + // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0` + // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom. + zoomSnap: 1, + + // @option zoomDelta: Number = 1 + // Controls how much the map's zoom level will change after a + // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+` + // or `-` on the keyboard, or using the [zoom controls](#control-zoom). + // Values smaller than `1` (e.g. `0.5`) allow for greater granularity. + zoomDelta: 1, + + // @option trackResize: Boolean = true + // Whether the map automatically handles browser window resize to update itself. + trackResize: true + }, + + initialize: function (id, options) { // (HTMLElement or String, Object) + options = L.setOptions(this, options); + + this._initContainer(id); + this._initLayout(); + + // hack for https://github.com/Leaflet/Leaflet/issues/1980 + this._onResize = L.bind(this._onResize, this); + + this._initEvents(); + + if (options.maxBounds) { + this.setMaxBounds(options.maxBounds); + } + + if (options.zoom !== undefined) { + this._zoom = this._limitZoom(options.zoom); + } + + if (options.center && options.zoom !== undefined) { + this.setView(L.latLng(options.center), options.zoom, {reset: true}); + } + + this._handlers = []; + this._layers = {}; + this._zoomBoundLayers = {}; + this._sizeChanged = true; + + this.callInitHooks(); + + // don't animate on browsers without hardware-accelerated transitions or old Android/Opera + this._zoomAnimated = L.DomUtil.TRANSITION && L.Browser.any3d && !L.Browser.mobileOpera && + this.options.zoomAnimation; + + // zoom transitions run with the same duration for all layers, so if one of transitionend events + // happens after starting zoom animation (propagating to the map pane), we know that it ended globally + if (this._zoomAnimated) { + this._createAnimProxy(); + L.DomEvent.on(this._proxy, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this); + } + + this._addLayers(this.options.layers); + }, + + + // @section Methods for modifying map state + + // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this + // Sets the view of the map (geographical center and zoom) with the given + // animation options. + setView: function (center, zoom, options) { + + zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); + center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds); + options = options || {}; + + this._stop(); + + if (this._loaded && !options.reset && options !== true) { + + if (options.animate !== undefined) { + options.zoom = L.extend({animate: options.animate}, options.zoom); + options.pan = L.extend({animate: options.animate, duration: options.duration}, options.pan); + } + + // try animating pan or zoom + var moved = (this._zoom !== zoom) ? + this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : + this._tryAnimatedPan(center, options.pan); + + if (moved) { + // prevent resize handler call, the view will refresh after animation anyway + clearTimeout(this._sizeTimer); + return this; + } + } + + // animation didn't start, just reset the map view + this._resetView(center, zoom); + + return this; + }, + + // @method setZoom(zoom: Number, options: Zoom/pan options): this + // Sets the zoom of the map. + setZoom: function (zoom, options) { + if (!this._loaded) { + this._zoom = zoom; + return this; + } + return this.setView(this.getCenter(), zoom, {zoom: options}); + }, + + // @method zoomIn(delta?: Number, options?: Zoom options): this + // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). + zoomIn: function (delta, options) { + delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1); + return this.setZoom(this._zoom + delta, options); + }, + + // @method zoomOut(delta?: Number, options?: Zoom options): this + // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). + zoomOut: function (delta, options) { + delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1); + return this.setZoom(this._zoom - delta, options); + }, + + // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this + // Zooms the map while keeping a specified geographical point on the map + // stationary (e.g. used internally for scroll zoom and double-click zoom). + // @alternative + // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this + // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary. + setZoomAround: function (latlng, zoom, options) { + var scale = this.getZoomScale(zoom), + viewHalf = this.getSize().divideBy(2), + containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng), + + centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), + newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); + + return this.setView(newCenter, zoom, {zoom: options}); + }, + + _getBoundsCenterZoom: function (bounds, options) { + + options = options || {}; + bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds); + + var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), + paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), + + zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); + + zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom; + + var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), + + swPoint = this.project(bounds.getSouthWest(), zoom), + nePoint = this.project(bounds.getNorthEast(), zoom), + center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); + + return { + center: center, + zoom: zoom + }; + }, + + // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this + // Sets a map view that contains the given geographical bounds with the + // maximum zoom level possible. + fitBounds: function (bounds, options) { + + bounds = L.latLngBounds(bounds); + + if (!bounds.isValid()) { + throw new Error('Bounds are not valid.'); + } + + var target = this._getBoundsCenterZoom(bounds, options); + return this.setView(target.center, target.zoom, options); + }, + + // @method fitWorld(options?: fitBounds options): this + // Sets a map view that mostly contains the whole world with the maximum + // zoom level possible. + fitWorld: function (options) { + return this.fitBounds([[-90, -180], [90, 180]], options); + }, + + // @method panTo(latlng: LatLng, options?: Pan options): this + // Pans the map to a given center. + panTo: function (center, options) { // (LatLng) + return this.setView(center, this._zoom, {pan: options}); + }, + + // @method panBy(offset: Point): this + // Pans the map by a given number of pixels (animated). + panBy: function (offset, options) { + offset = L.point(offset).round(); + options = options || {}; + + if (!offset.x && !offset.y) { + return this.fire('moveend'); + } + // If we pan too far, Chrome gets issues with tiles + // and makes them disappear or appear in the wrong place (slightly offset) #2602 + if (options.animate !== true && !this.getSize().contains(offset)) { + this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); + return this; + } + + if (!this._panAnim) { + this._panAnim = new L.PosAnimation(); + + this._panAnim.on({ + 'step': this._onPanTransitionStep, + 'end': this._onPanTransitionEnd + }, this); + } + + // don't fire movestart if animating inertia + if (!options.noMoveStart) { + this.fire('movestart'); + } + + // animate pan unless animate: false specified + if (options.animate !== false) { + L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim'); + + var newPos = this._getMapPanePos().subtract(offset).round(); + this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); + } else { + this._rawPanBy(offset); + this.fire('move').fire('moveend'); + } + + return this; + }, + + // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this + // Sets the view of the map (geographical center and zoom) performing a smooth + // pan-zoom animation. + flyTo: function (targetCenter, targetZoom, options) { + + options = options || {}; + if (options.animate === false || !L.Browser.any3d) { + return this.setView(targetCenter, targetZoom, options); + } + + this._stop(); + + var from = this.project(this.getCenter()), + to = this.project(targetCenter), + size = this.getSize(), + startZoom = this._zoom; + + targetCenter = L.latLng(targetCenter); + targetZoom = targetZoom === undefined ? startZoom : targetZoom; + + var w0 = Math.max(size.x, size.y), + w1 = w0 * this.getZoomScale(startZoom, targetZoom), + u1 = (to.distanceTo(from)) || 1, + rho = 1.42, + rho2 = rho * rho; + + function r(i) { + var s1 = i ? -1 : 1, + s2 = i ? w1 : w0, + t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1, + b1 = 2 * s2 * rho2 * u1, + b = t1 / b1, + sq = Math.sqrt(b * b + 1) - b; + + // workaround for floating point precision bug when sq = 0, log = -Infinite, + // thus triggering an infinite loop in flyTo + var log = sq < 0.000000001 ? -18 : Math.log(sq); + + return log; + } + + function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } + function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } + function tanh(n) { return sinh(n) / cosh(n); } + + var r0 = r(0); + + function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); } + function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; } + + function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); } + + var start = Date.now(), + S = (r(1) - r0) / rho, + duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8; + + function frame() { + var t = (Date.now() - start) / duration, + s = easeOut(t) * S; + + if (t <= 1) { + this._flyToFrame = L.Util.requestAnimFrame(frame, this); + + this._move( + this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), + this.getScaleZoom(w0 / w(s), startZoom), + {flyTo: true}); + + } else { + this + ._move(targetCenter, targetZoom) + ._moveEnd(true); + } + } + + this._moveStart(true); + + frame.call(this); + return this; + }, + + // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this + // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto), + // but takes a bounds parameter like [`fitBounds`](#map-fitbounds). + flyToBounds: function (bounds, options) { + var target = this._getBoundsCenterZoom(bounds, options); + return this.flyTo(target.center, target.zoom, options); + }, + + // @method setMaxBounds(bounds: Bounds): this + // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option). + setMaxBounds: function (bounds) { + bounds = L.latLngBounds(bounds); + + if (!bounds.isValid()) { + this.options.maxBounds = null; + return this.off('moveend', this._panInsideMaxBounds); + } else if (this.options.maxBounds) { + this.off('moveend', this._panInsideMaxBounds); + } + + this.options.maxBounds = bounds; + + if (this._loaded) { + this._panInsideMaxBounds(); + } + + return this.on('moveend', this._panInsideMaxBounds); + }, + + // @method setMinZoom(zoom: Number): this + // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option). + setMinZoom: function (zoom) { + this.options.minZoom = zoom; + + if (this._loaded && this.getZoom() < this.options.minZoom) { + return this.setZoom(zoom); + } + + return this; + }, + + // @method setMaxZoom(zoom: Number): this + // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option). + setMaxZoom: function (zoom) { + this.options.maxZoom = zoom; + + if (this._loaded && (this.getZoom() > this.options.maxZoom)) { + return this.setZoom(zoom); + } + + return this; + }, + + // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this + // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any. + panInsideBounds: function (bounds, options) { + this._enforcingBounds = true; + var center = this.getCenter(), + newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds)); + + if (!center.equals(newCenter)) { + this.panTo(newCenter, options); + } + + this._enforcingBounds = false; + return this; + }, + + // @method invalidateSize(options: Zoom/Pan options): this + // Checks if the map container size changed and updates the map if so — + // call it after you've changed the map size dynamically, also animating + // pan by default. If `options.pan` is `false`, panning will not occur. + // If `options.debounceMoveend` is `true`, it will delay `moveend` event so + // that it doesn't happen often even if the method is called many + // times in a row. + + // @alternative + // @method invalidateSize(animate: Boolean): this + // Checks if the map container size changed and updates the map if so — + // call it after you've changed the map size dynamically, also animating + // pan by default. + invalidateSize: function (options) { + if (!this._loaded) { return this; } + + options = L.extend({ + animate: false, + pan: true + }, options === true ? {animate: true} : options); + + var oldSize = this.getSize(); + this._sizeChanged = true; + this._lastCenter = null; + + var newSize = this.getSize(), + oldCenter = oldSize.divideBy(2).round(), + newCenter = newSize.divideBy(2).round(), + offset = oldCenter.subtract(newCenter); + + if (!offset.x && !offset.y) { return this; } + + if (options.animate && options.pan) { + this.panBy(offset); + + } else { + if (options.pan) { + this._rawPanBy(offset); + } + + this.fire('move'); + + if (options.debounceMoveend) { + clearTimeout(this._sizeTimer); + this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); + } else { + this.fire('moveend'); + } + } + + // @section Map state change events + // @event resize: ResizeEvent + // Fired when the map is resized. + return this.fire('resize', { + oldSize: oldSize, + newSize: newSize + }); + }, + + // @section Methods for modifying map state + // @method stop(): this + // Stops the currently running `panTo` or `flyTo` animation, if any. + stop: function () { + this.setZoom(this._limitZoom(this._zoom)); + if (!this.options.zoomSnap) { + this.fire('viewreset'); + } + return this._stop(); + }, + + // @section Geolocation methods + // @method locate(options?: Locate options): this + // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound) + // event with location data on success or a [`locationerror`](#map-locationerror) event on failure, + // and optionally sets the map view to the user's location with respect to + // detection accuracy (or to the world view if geolocation failed). + // Note that, if your page doesn't use HTTPS, this method will fail in + // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins)) + // See `Locate options` for more details. + locate: function (options) { + + options = this._locateOptions = L.extend({ + timeout: 10000, + watch: false + // setView: false + // maxZoom: + // maximumAge: 0 + // enableHighAccuracy: false + }, options); + + if (!('geolocation' in navigator)) { + this._handleGeolocationError({ + code: 0, + message: 'Geolocation not supported.' + }); + return this; + } + + var onResponse = L.bind(this._handleGeolocationResponse, this), + onError = L.bind(this._handleGeolocationError, this); + + if (options.watch) { + this._locationWatchId = + navigator.geolocation.watchPosition(onResponse, onError, options); + } else { + navigator.geolocation.getCurrentPosition(onResponse, onError, options); + } + return this; + }, + + // @method stopLocate(): this + // Stops watching location previously initiated by `map.locate({watch: true})` + // and aborts resetting the map view if map.locate was called with + // `{setView: true}`. + stopLocate: function () { + if (navigator.geolocation && navigator.geolocation.clearWatch) { + navigator.geolocation.clearWatch(this._locationWatchId); + } + if (this._locateOptions) { + this._locateOptions.setView = false; + } + return this; + }, + + _handleGeolocationError: function (error) { + var c = error.code, + message = error.message || + (c === 1 ? 'permission denied' : + (c === 2 ? 'position unavailable' : 'timeout')); + + if (this._locateOptions.setView && !this._loaded) { + this.fitWorld(); + } + + // @section Location events + // @event locationerror: ErrorEvent + // Fired when geolocation (using the [`locate`](#map-locate) method) failed. + this.fire('locationerror', { + code: c, + message: 'Geolocation error: ' + message + '.' + }); + }, + + _handleGeolocationResponse: function (pos) { + var lat = pos.coords.latitude, + lng = pos.coords.longitude, + latlng = new L.LatLng(lat, lng), + bounds = latlng.toBounds(pos.coords.accuracy), + options = this._locateOptions; + + if (options.setView) { + var zoom = this.getBoundsZoom(bounds); + this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); + } + + var data = { + latlng: latlng, + bounds: bounds, + timestamp: pos.timestamp + }; + + for (var i in pos.coords) { + if (typeof pos.coords[i] === 'number') { + data[i] = pos.coords[i]; + } + } + + // @event locationfound: LocationEvent + // Fired when geolocation (using the [`locate`](#map-locate) method) + // went successfully. + this.fire('locationfound', data); + }, + + // TODO handler.addTo + // TODO Appropiate docs section? + // @section Other Methods + // @method addHandler(name: String, HandlerClass: Function): this + // Adds a new `Handler` to the map, given its name and constructor function. + addHandler: function (name, HandlerClass) { + if (!HandlerClass) { return this; } + + var handler = this[name] = new HandlerClass(this); + + this._handlers.push(handler); + + if (this.options[name]) { + handler.enable(); + } + + return this; + }, + + // @method remove(): this + // Destroys the map and clears all related event listeners. + remove: function () { + + this._initEvents(true); + + if (this._containerId !== this._container._leaflet_id) { + throw new Error('Map container is being reused by another instance'); + } + + try { + // throws error in IE6-8 + delete this._container._leaflet_id; + delete this._containerId; + } catch (e) { + /*eslint-disable */ + this._container._leaflet_id = undefined; + /*eslint-enable */ + this._containerId = undefined; + } + + L.DomUtil.remove(this._mapPane); + + if (this._clearControlPos) { + this._clearControlPos(); + } + + this._clearHandlers(); + + if (this._loaded) { + // @section Map state change events + // @event unload: Event + // Fired when the map is destroyed with [remove](#map-remove) method. + this.fire('unload'); + } + + for (var i in this._layers) { + this._layers[i].remove(); + } + + return this; + }, + + // @section Other Methods + // @method createPane(name: String, container?: HTMLElement): HTMLElement + // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already, + // then returns it. The pane is created as a children of `container`, or + // as a children of the main map pane if not set. + createPane: function (name, container) { + var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), + pane = L.DomUtil.create('div', className, container || this._mapPane); + + if (name) { + this._panes[name] = pane; + } + return pane; + }, + + // @section Methods for Getting Map State + + // @method getCenter(): LatLng + // Returns the geographical center of the map view + getCenter: function () { + this._checkIfLoaded(); + + if (this._lastCenter && !this._moved()) { + return this._lastCenter; + } + return this.layerPointToLatLng(this._getCenterLayerPoint()); + }, + + // @method getZoom(): Number + // Returns the current zoom level of the map view + getZoom: function () { + return this._zoom; + }, + + // @method getBounds(): LatLngBounds + // Returns the geographical bounds visible in the current map view + getBounds: function () { + var bounds = this.getPixelBounds(), + sw = this.unproject(bounds.getBottomLeft()), + ne = this.unproject(bounds.getTopRight()); + + return new L.LatLngBounds(sw, ne); + }, + + // @method getMinZoom(): Number + // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default. + getMinZoom: function () { + return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; + }, + + // @method getMaxZoom(): Number + // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers). + getMaxZoom: function () { + return this.options.maxZoom === undefined ? + (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : + this.options.maxZoom; + }, + + // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean): Number + // Returns the maximum zoom level on which the given bounds fit to the map + // view in its entirety. If `inside` (optional) is set to `true`, the method + // instead returns the minimum zoom level on which the map view fits into + // the given bounds in its entirety. + getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number + bounds = L.latLngBounds(bounds); + padding = L.point(padding || [0, 0]); + + var zoom = this.getZoom() || 0, + min = this.getMinZoom(), + max = this.getMaxZoom(), + nw = bounds.getNorthWest(), + se = bounds.getSouthEast(), + size = this.getSize().subtract(padding), + boundsSize = L.bounds(this.project(se, zoom), this.project(nw, zoom)).getSize(), + snap = L.Browser.any3d ? this.options.zoomSnap : 1; + + var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y); + zoom = this.getScaleZoom(scale, zoom); + + if (snap) { + zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level + zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; + } + + return Math.max(min, Math.min(max, zoom)); + }, + + // @method getSize(): Point + // Returns the current size of the map container (in pixels). + getSize: function () { + if (!this._size || this._sizeChanged) { + this._size = new L.Point( + this._container.clientWidth || 0, + this._container.clientHeight || 0); + + this._sizeChanged = false; + } + return this._size.clone(); + }, + + // @method getPixelBounds(): Bounds + // Returns the bounds of the current map view in projected pixel + // coordinates (sometimes useful in layer and overlay implementations). + getPixelBounds: function (center, zoom) { + var topLeftPoint = this._getTopLeftPoint(center, zoom); + return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); + }, + + // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to + // the map pane? "left point of the map layer" can be confusing, specially + // since there can be negative offsets. + // @method getPixelOrigin(): Point + // Returns the projected pixel coordinates of the top left point of + // the map layer (useful in custom layer and overlay implementations). + getPixelOrigin: function () { + this._checkIfLoaded(); + return this._pixelOrigin; + }, + + // @method getPixelWorldBounds(zoom?: Number): Bounds + // Returns the world's bounds in pixel coordinates for zoom level `zoom`. + // If `zoom` is omitted, the map's current zoom level is used. + getPixelWorldBounds: function (zoom) { + return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom); + }, + + // @section Other Methods + + // @method getPane(pane: String|HTMLElement): HTMLElement + // Returns a [map pane](#map-pane), given its name or its HTML element (its identity). + getPane: function (pane) { + return typeof pane === 'string' ? this._panes[pane] : pane; + }, + + // @method getPanes(): Object + // Returns a plain object containing the names of all [panes](#map-pane) as keys and + // the panes as values. + getPanes: function () { + return this._panes; + }, + + // @method getContainer: HTMLElement + // Returns the HTML element that contains the map. + getContainer: function () { + return this._container; + }, + + + // @section Conversion Methods + + // @method getZoomScale(toZoom: Number, fromZoom: Number): Number + // Returns the scale factor to be applied to a map transition from zoom level + // `fromZoom` to `toZoom`. Used internally to help with zoom animations. + getZoomScale: function (toZoom, fromZoom) { + // TODO replace with universal implementation after refactoring projections + var crs = this.options.crs; + fromZoom = fromZoom === undefined ? this._zoom : fromZoom; + return crs.scale(toZoom) / crs.scale(fromZoom); + }, + + // @method getScaleZoom(scale: Number, fromZoom: Number): Number + // Returns the zoom level that the map would end up at, if it is at `fromZoom` + // level and everything is scaled by a factor of `scale`. Inverse of + // [`getZoomScale`](#map-getZoomScale). + getScaleZoom: function (scale, fromZoom) { + var crs = this.options.crs; + fromZoom = fromZoom === undefined ? this._zoom : fromZoom; + var zoom = crs.zoom(scale * crs.scale(fromZoom)); + return isNaN(zoom) ? Infinity : zoom; + }, + + // @method project(latlng: LatLng, zoom: Number): Point + // Projects a geographical coordinate `LatLng` according to the projection + // of the map's CRS, then scales it according to `zoom` and the CRS's + // `Transformation`. The result is pixel coordinate relative to + // the CRS origin. + project: function (latlng, zoom) { + zoom = zoom === undefined ? this._zoom : zoom; + return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); + }, + + // @method unproject(point: Point, zoom: Number): LatLng + // Inverse of [`project`](#map-project). + unproject: function (point, zoom) { + zoom = zoom === undefined ? this._zoom : zoom; + return this.options.crs.pointToLatLng(L.point(point), zoom); + }, + + // @method layerPointToLatLng(point: Point): LatLng + // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), + // returns the corresponding geographical coordinate (for the current zoom level). + layerPointToLatLng: function (point) { + var projectedPoint = L.point(point).add(this.getPixelOrigin()); + return this.unproject(projectedPoint); + }, + + // @method latLngToLayerPoint(latlng: LatLng): Point + // Given a geographical coordinate, returns the corresponding pixel coordinate + // relative to the [origin pixel](#map-getpixelorigin). + latLngToLayerPoint: function (latlng) { + var projectedPoint = this.project(L.latLng(latlng))._round(); + return projectedPoint._subtract(this.getPixelOrigin()); + }, + + // @method wrapLatLng(latlng: LatLng): LatLng + // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the + // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the + // CRS's bounds. + // By default this means longitude is wrapped around the dateline so its + // value is between -180 and +180 degrees. + wrapLatLng: function (latlng) { + return this.options.crs.wrapLatLng(L.latLng(latlng)); + }, + + // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds + // Returns a `LatLngBounds` with the same size as the given one, ensuring that + // its center is within the CRS's bounds. + // By default this means the center longitude is wrapped around the dateline so its + // value is between -180 and +180 degrees, and the majority of the bounds + // overlaps the CRS's bounds. + wrapLatLngBounds: function (latlng) { + return this.options.crs.wrapLatLngBounds(L.latLngBounds(latlng)); + }, + + // @method distance(latlng1: LatLng, latlng2: LatLng): Number + // Returns the distance between two geographical coordinates according to + // the map's CRS. By default this measures distance in meters. + distance: function (latlng1, latlng2) { + return this.options.crs.distance(L.latLng(latlng1), L.latLng(latlng2)); + }, + + // @method containerPointToLayerPoint(point: Point): Point + // Given a pixel coordinate relative to the map container, returns the corresponding + // pixel coordinate relative to the [origin pixel](#map-getpixelorigin). + containerPointToLayerPoint: function (point) { // (Point) + return L.point(point).subtract(this._getMapPanePos()); + }, + + // @method layerPointToContainerPoint(point: Point): Point + // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), + // returns the corresponding pixel coordinate relative to the map container. + layerPointToContainerPoint: function (point) { // (Point) + return L.point(point).add(this._getMapPanePos()); + }, + + // @method containerPointToLatLng(point: Point): LatLng + // Given a pixel coordinate relative to the map container, returns + // the corresponding geographical coordinate (for the current zoom level). + containerPointToLatLng: function (point) { + var layerPoint = this.containerPointToLayerPoint(L.point(point)); + return this.layerPointToLatLng(layerPoint); + }, + + // @method latLngToContainerPoint(latlng: LatLng): Point + // Given a geographical coordinate, returns the corresponding pixel coordinate + // relative to the map container. + latLngToContainerPoint: function (latlng) { + return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); + }, + + // @method mouseEventToContainerPoint(ev: MouseEvent): Point + // Given a MouseEvent object, returns the pixel coordinate relative to the + // map container where the event took place. + mouseEventToContainerPoint: function (e) { + return L.DomEvent.getMousePosition(e, this._container); + }, + + // @method mouseEventToLayerPoint(ev: MouseEvent): Point + // Given a MouseEvent object, returns the pixel coordinate relative to + // the [origin pixel](#map-getpixelorigin) where the event took place. + mouseEventToLayerPoint: function (e) { + return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); + }, + + // @method mouseEventToLatLng(ev: MouseEvent): LatLng + // Given a MouseEvent object, returns geographical coordinate where the + // event took place. + mouseEventToLatLng: function (e) { // (MouseEvent) + return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); + }, + + + // map initialization methods + + _initContainer: function (id) { + var container = this._container = L.DomUtil.get(id); + + if (!container) { + throw new Error('Map container not found.'); + } else if (container._leaflet_id) { + throw new Error('Map container is already initialized.'); + } + + L.DomEvent.addListener(container, 'scroll', this._onScroll, this); + this._containerId = L.Util.stamp(container); + }, + + _initLayout: function () { + var container = this._container; + + this._fadeAnimated = this.options.fadeAnimation && L.Browser.any3d; + + L.DomUtil.addClass(container, 'leaflet-container' + + (L.Browser.touch ? ' leaflet-touch' : '') + + (L.Browser.retina ? ' leaflet-retina' : '') + + (L.Browser.ielt9 ? ' leaflet-oldie' : '') + + (L.Browser.safari ? ' leaflet-safari' : '') + + (this._fadeAnimated ? ' leaflet-fade-anim' : '')); + + var position = L.DomUtil.getStyle(container, 'position'); + + if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { + container.style.position = 'relative'; + } + + this._initPanes(); + + if (this._initControlPos) { + this._initControlPos(); + } + }, + + _initPanes: function () { + var panes = this._panes = {}; + this._paneRenderers = {}; + + // @section + // + // Panes are DOM elements used to control the ordering of layers on the map. You + // can access panes with [`map.getPane`](#map-getpane) or + // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the + // [`map.createPane`](#map-createpane) method. + // + // Every map has the following default panes that differ only in zIndex. + // + // @pane mapPane: HTMLElement = 'auto' + // Pane that contains all other map panes + + this._mapPane = this.createPane('mapPane', this._container); + L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); + + // @pane tilePane: HTMLElement = 200 + // Pane for `GridLayer`s and `TileLayer`s + this.createPane('tilePane'); + // @pane overlayPane: HTMLElement = 400 + // Pane for vector overlays (`Path`s), like `Polyline`s and `Polygon`s + this.createPane('shadowPane'); + // @pane shadowPane: HTMLElement = 500 + // Pane for overlay shadows (e.g. `Marker` shadows) + this.createPane('overlayPane'); + // @pane markerPane: HTMLElement = 600 + // Pane for `Icon`s of `Marker`s + this.createPane('markerPane'); + // @pane tooltipPane: HTMLElement = 650 + // Pane for tooltip. + this.createPane('tooltipPane'); + // @pane popupPane: HTMLElement = 700 + // Pane for `Popup`s. + this.createPane('popupPane'); + + if (!this.options.markerZoomAnimation) { + L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide'); + L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide'); + } + }, + + + // private methods that modify map state + + // @section Map state change events + _resetView: function (center, zoom) { + L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); + + var loading = !this._loaded; + this._loaded = true; + zoom = this._limitZoom(zoom); + + this.fire('viewprereset'); + + var zoomChanged = this._zoom !== zoom; + this + ._moveStart(zoomChanged) + ._move(center, zoom) + ._moveEnd(zoomChanged); + + // @event viewreset: Event + // Fired when the map needs to redraw its content (this usually happens + // on map zoom or load). Very useful for creating custom overlays. + this.fire('viewreset'); + + // @event load: Event + // Fired when the map is initialized (when its center and zoom are set + // for the first time). + if (loading) { + this.fire('load'); + } + }, + + _moveStart: function (zoomChanged) { + // @event zoomstart: Event + // Fired when the map zoom is about to change (e.g. before zoom animation). + // @event movestart: Event + // Fired when the view of the map starts changing (e.g. user starts dragging the map). + if (zoomChanged) { + this.fire('zoomstart'); + } + return this.fire('movestart'); + }, + + _move: function (center, zoom, data) { + if (zoom === undefined) { + zoom = this._zoom; + } + var zoomChanged = this._zoom !== zoom; + + this._zoom = zoom; + this._lastCenter = center; + this._pixelOrigin = this._getNewPixelOrigin(center); + + // @event zoom: Event + // Fired repeatedly during any change in zoom level, including zoom + // and fly animations. + if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530 + this.fire('zoom', data); + } + + // @event move: Event + // Fired repeatedly during any movement of the map, including pan and + // fly animations. + return this.fire('move', data); + }, + + _moveEnd: function (zoomChanged) { + // @event zoomend: Event + // Fired when the map has changed, after any animations. + if (zoomChanged) { + this.fire('zoomend'); + } + + // @event moveend: Event + // Fired when the center of the map stops changing (e.g. user stopped + // dragging the map). + return this.fire('moveend'); + }, + + _stop: function () { + L.Util.cancelAnimFrame(this._flyToFrame); + if (this._panAnim) { + this._panAnim.stop(); + } + return this; + }, + + _rawPanBy: function (offset) { + L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); + }, + + _getZoomSpan: function () { + return this.getMaxZoom() - this.getMinZoom(); + }, + + _panInsideMaxBounds: function () { + if (!this._enforcingBounds) { + this.panInsideBounds(this.options.maxBounds); + } + }, + + _checkIfLoaded: function () { + if (!this._loaded) { + throw new Error('Set map center and zoom first.'); + } + }, + + // DOM event handling + + // @section Interaction events + _initEvents: function (remove) { + if (!L.DomEvent) { return; } + + this._targets = {}; + this._targets[L.stamp(this._container)] = this; + + var onOff = remove ? 'off' : 'on'; + + // @event click: MouseEvent + // Fired when the user clicks (or taps) the map. + // @event dblclick: MouseEvent + // Fired when the user double-clicks (or double-taps) the map. + // @event mousedown: MouseEvent + // Fired when the user pushes the mouse button on the map. + // @event mouseup: MouseEvent + // Fired when the user releases the mouse button on the map. + // @event mouseover: MouseEvent + // Fired when the mouse enters the map. + // @event mouseout: MouseEvent + // Fired when the mouse leaves the map. + // @event mousemove: MouseEvent + // Fired while the mouse moves over the map. + // @event contextmenu: MouseEvent + // Fired when the user pushes the right mouse button on the map, prevents + // default browser context menu from showing if there are listeners on + // this event. Also fired on mobile when the user holds a single touch + // for a second (also called long press). + // @event keypress: KeyboardEvent + // Fired when the user presses a key from the keyboard while the map is focused. + L.DomEvent[onOff](this._container, 'click dblclick mousedown mouseup ' + + 'mouseover mouseout mousemove contextmenu keypress', this._handleDOMEvent, this); + + if (this.options.trackResize) { + L.DomEvent[onOff](window, 'resize', this._onResize, this); + } + + if (L.Browser.any3d && this.options.transform3DLimit) { + this[onOff]('moveend', this._onMoveEnd); + } + }, + + _onResize: function () { + L.Util.cancelAnimFrame(this._resizeRequest); + this._resizeRequest = L.Util.requestAnimFrame( + function () { this.invalidateSize({debounceMoveend: true}); }, this); + }, + + _onScroll: function () { + this._container.scrollTop = 0; + this._container.scrollLeft = 0; + }, + + _onMoveEnd: function () { + var pos = this._getMapPanePos(); + if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have + // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/ + this._resetView(this.getCenter(), this.getZoom()); + } + }, + + _findEventTargets: function (e, type) { + var targets = [], + target, + isHover = type === 'mouseout' || type === 'mouseover', + src = e.target || e.srcElement, + dragging = false; + + while (src) { + target = this._targets[L.stamp(src)]; + if (target && (type === 'click' || type === 'preclick') && !e._simulated && this._draggableMoved(target)) { + // Prevent firing click after you just dragged an object. + dragging = true; + break; + } + if (target && target.listens(type, true)) { + if (isHover && !L.DomEvent._isExternalTarget(src, e)) { break; } + targets.push(target); + if (isHover) { break; } + } + if (src === this._container) { break; } + src = src.parentNode; + } + if (!targets.length && !dragging && !isHover && L.DomEvent._isExternalTarget(src, e)) { + targets = [this]; + } + return targets; + }, + + _handleDOMEvent: function (e) { + if (!this._loaded || L.DomEvent._skipped(e)) { return; } + + var type = e.type === 'keypress' && e.keyCode === 13 ? 'click' : e.type; + + if (type === 'mousedown') { + // prevents outline when clicking on keyboard-focusable element + L.DomUtil.preventOutline(e.target || e.srcElement); + } + + this._fireDOMEvent(e, type); + }, + + _fireDOMEvent: function (e, type, targets) { + + if (e.type === 'click') { + // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). + // @event preclick: MouseEvent + // Fired before mouse click on the map (sometimes useful when you + // want something to happen on click before any existing click + // handlers start running). + var synth = L.Util.extend({}, e); + synth.type = 'preclick'; + this._fireDOMEvent(synth, synth.type, targets); + } + + if (e._stopped) { return; } + + // Find the layer the event is propagating from and its parents. + targets = (targets || []).concat(this._findEventTargets(e, type)); + + if (!targets.length) { return; } + + var target = targets[0]; + if (type === 'contextmenu' && target.listens(type, true)) { + L.DomEvent.preventDefault(e); + } + + var data = { + originalEvent: e + }; + + if (e.type !== 'keypress') { + var isMarker = target instanceof L.Marker; + data.containerPoint = isMarker ? + this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); + data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); + data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint); + } + + for (var i = 0; i < targets.length; i++) { + targets[i].fire(type, data, true); + if (data.originalEvent._stopped || + (targets[i].options.nonBubblingEvents && L.Util.indexOf(targets[i].options.nonBubblingEvents, type) !== -1)) { return; } + } + }, + + _draggableMoved: function (obj) { + obj = obj.dragging && obj.dragging.enabled() ? obj : this; + return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()); + }, + + _clearHandlers: function () { + for (var i = 0, len = this._handlers.length; i < len; i++) { + this._handlers[i].disable(); + } + }, + + // @section Other Methods + + // @method whenReady(fn: Function, context?: Object): this + // Runs the given function `fn` when the map gets initialized with + // a view (center and zoom) and at least one layer, or immediately + // if it's already initialized, optionally passing a function context. + whenReady: function (callback, context) { + if (this._loaded) { + callback.call(context || this, {target: this}); + } else { + this.on('load', callback, context); + } + return this; + }, + + + // private methods for getting map state + + _getMapPanePos: function () { + return L.DomUtil.getPosition(this._mapPane) || new L.Point(0, 0); + }, + + _moved: function () { + var pos = this._getMapPanePos(); + return pos && !pos.equals([0, 0]); + }, + + _getTopLeftPoint: function (center, zoom) { + var pixelOrigin = center && zoom !== undefined ? + this._getNewPixelOrigin(center, zoom) : + this.getPixelOrigin(); + return pixelOrigin.subtract(this._getMapPanePos()); + }, + + _getNewPixelOrigin: function (center, zoom) { + var viewHalf = this.getSize()._divideBy(2); + return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round(); + }, + + _latLngToNewLayerPoint: function (latlng, zoom, center) { + var topLeft = this._getNewPixelOrigin(center, zoom); + return this.project(latlng, zoom)._subtract(topLeft); + }, + + _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) { + var topLeft = this._getNewPixelOrigin(center, zoom); + return L.bounds([ + this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft), + this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft), + this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft), + this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft) + ]); + }, + + // layer point of the current center + _getCenterLayerPoint: function () { + return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); + }, + + // offset of the specified place to the current center in pixels + _getCenterOffset: function (latlng) { + return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); + }, + + // adjust center for view to get inside bounds + _limitCenter: function (center, zoom, bounds) { + + if (!bounds) { return center; } + + var centerPoint = this.project(center, zoom), + viewHalf = this.getSize().divideBy(2), + viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), + offset = this._getBoundsOffset(viewBounds, bounds, zoom); + + // If offset is less than a pixel, ignore. + // This prevents unstable projections from getting into + // an infinite loop of tiny offsets. + if (offset.round().equals([0, 0])) { + return center; + } + + return this.unproject(centerPoint.add(offset), zoom); + }, + + // adjust offset for view to get inside bounds + _limitOffset: function (offset, bounds) { + if (!bounds) { return offset; } + + var viewBounds = this.getPixelBounds(), + newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); + + return offset.add(this._getBoundsOffset(newBounds, bounds)); + }, + + // returns offset needed for pxBounds to get inside maxBounds at a specified zoom + _getBoundsOffset: function (pxBounds, maxBounds, zoom) { + var projectedMaxBounds = L.bounds( + this.project(maxBounds.getNorthEast(), zoom), + this.project(maxBounds.getSouthWest(), zoom) + ), + minOffset = projectedMaxBounds.min.subtract(pxBounds.min), + maxOffset = projectedMaxBounds.max.subtract(pxBounds.max), + + dx = this._rebound(minOffset.x, -maxOffset.x), + dy = this._rebound(minOffset.y, -maxOffset.y); + + return new L.Point(dx, dy); + }, + + _rebound: function (left, right) { + return left + right > 0 ? + Math.round(left - right) / 2 : + Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); + }, + + _limitZoom: function (zoom) { + var min = this.getMinZoom(), + max = this.getMaxZoom(), + snap = L.Browser.any3d ? this.options.zoomSnap : 1; + if (snap) { + zoom = Math.round(zoom / snap) * snap; + } + return Math.max(min, Math.min(max, zoom)); + }, + + _onPanTransitionStep: function () { + this.fire('move'); + }, + + _onPanTransitionEnd: function () { + L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim'); + this.fire('moveend'); + }, + + _tryAnimatedPan: function (center, options) { + // difference between the new and current centers in pixels + var offset = this._getCenterOffset(center)._floor(); + + // don't animate too far unless animate: true specified in options + if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; } + + this.panBy(offset, options); + + return true; + }, + + _createAnimProxy: function () { + + var proxy = this._proxy = L.DomUtil.create('div', 'leaflet-proxy leaflet-zoom-animated'); + this._panes.mapPane.appendChild(proxy); + + this.on('zoomanim', function (e) { + var prop = L.DomUtil.TRANSFORM, + transform = proxy.style[prop]; + + L.DomUtil.setTransform(proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1)); + + // workaround for case when transform is the same and so transitionend event is not fired + if (transform === proxy.style[prop] && this._animatingZoom) { + this._onZoomTransitionEnd(); + } + }, this); + + this.on('load moveend', function () { + var c = this.getCenter(), + z = this.getZoom(); + L.DomUtil.setTransform(proxy, this.project(c, z), this.getZoomScale(z, 1)); + }, this); + }, + + _catchTransitionEnd: function (e) { + if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) { + this._onZoomTransitionEnd(); + } + }, + + _nothingToAnimate: function () { + return !this._container.getElementsByClassName('leaflet-zoom-animated').length; + }, + + _tryAnimatedZoom: function (center, zoom, options) { + + if (this._animatingZoom) { return true; } + + options = options || {}; + + // don't animate if disabled, not supported or zoom difference is too large + if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() || + Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; } + + // offset is the pixel coords of the zoom origin relative to the current center + var scale = this.getZoomScale(zoom), + offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale); + + // don't animate if the zoom origin isn't within one screen from the current center, unless forced + if (options.animate !== true && !this.getSize().contains(offset)) { return false; } + + L.Util.requestAnimFrame(function () { + this + ._moveStart(true) + ._animateZoom(center, zoom, true); + }, this); + + return true; + }, + + _animateZoom: function (center, zoom, startAnim, noUpdate) { + if (startAnim) { + this._animatingZoom = true; + + // remember what center/zoom to set after animation + this._animateToCenter = center; + this._animateToZoom = zoom; + + L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); + } + + // @event zoomanim: ZoomAnimEvent + // Fired on every frame of a zoom animation + this.fire('zoomanim', { + center: center, + zoom: zoom, + noUpdate: noUpdate + }); + + // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693 + setTimeout(L.bind(this._onZoomTransitionEnd, this), 250); + }, + + _onZoomTransitionEnd: function () { + if (!this._animatingZoom) { return; } + + L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); + + this._animatingZoom = false; + + this._move(this._animateToCenter, this._animateToZoom); + + // This anim frame should prevent an obscure iOS webkit tile loading race condition. + L.Util.requestAnimFrame(function () { + this._moveEnd(true); + }, this); + } +}); + +// @section + +// @factory L.map(id: String, options?: Map options) +// Instantiates a map object given the DOM ID of a `
    ` element +// and optionally an object literal with `Map options`. +// +// @alternative +// @factory L.map(el: HTMLElement, options?: Map options) +// Instantiates a map object given an instance of a `
    ` HTML element +// and optionally an object literal with `Map options`. +L.map = function (id, options) { + return new L.Map(id, options); +}; - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - var ich = { - VERSION: "0.10.2", - templates: {}, - - // grab jquery or zepto if it's there - $: (typeof window !== 'undefined') ? window.jQuery || window.Zepto || null : null, - - // public function for adding templates - // can take a name and template string arguments - // or can take an object with name/template pairs - // We're enforcing uniqueness to avoid accidental template overwrites. - // If you want a different template, it should have a different name. - addTemplate: function (name, templateString) { - if (typeof name === 'object') { - for (var template in name) { - this.addTemplate(template, name[template]); - } - return; - } - if (ich[name]) { - console.error("Invalid name: " + name + "."); - } else if (ich.templates[name]) { - console.error("Template \"" + name + " \" exists"); - } else { - ich.templates[name] = templateString; - ich[name] = function (data, raw) { - data = data || {}; - var result = Mustache.to_html(ich.templates[name], data, ich.templates); - return (ich.$ && !raw) ? ich.$(trim(result)) : result; - }; - } - }, - - // clears all retrieval functions and empties cache - clearAll: function () { - for (var key in ich.templates) { - delete ich[key]; - } - ich.templates = {}; - }, - - // clears/grabs - refresh: function () { - ich.clearAll(); - ich.grabTemplates(); - }, - - // grabs templates from the DOM and caches them. - // Loop through and add templates. - // Whitespace at beginning and end of all templates inside - + + @@ -26,29 +26,26 @@

    Ideas

    Demos

    Use

    Contact

    -

    Home Page

    +

    Home

    diff --git a/site/demos/demo-chart.html b/site/demos/demo-chart.html deleted file mode 100644 index 69fcb5c..0000000 --- a/site/demos/demo-chart.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - Sheetsee Chart Demo - - - - - - - - - - - - -
    -

    Pennies by State

    -

    spreadsheet

    -
    -

    View Source // View Documentation

    - - -
    - - - - - diff --git a/site/demos/demo-map.html b/site/demos/demo-map.html deleted file mode 100644 index e8df041..0000000 --- a/site/demos/demo-map.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - - Sheetsee Maps Demo - - - - - - - - - - - -
    -

    All Pennies Map

    -

    spreadsheet

    -

    -

    View Source // View Documentation

    -

    Using linestrings, polygons and multipolygons

    -
    -

    View Source // View Spreadsheet

    - - - - - - diff --git a/site/demos/demo-table.html b/site/demos/demo-table.html deleted file mode 100644 index 7ff8116..0000000 --- a/site/demos/demo-table.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - Sheetsee Table Demo - - - - - - - - - - -
    -

    All Pennies

    -

    spreadsheet

    - -

    -

    California Pennies

    - -
    -

    Pretty Pennies

    -
    -

    View Source // View Documentation

    - - -
    - - - - - - - - - - - diff --git a/site/docs/about.html b/site/docs/about.html deleted file mode 100644 index 232df3c..0000000 --- a/site/docs/about.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - Sheetsee.js - - - - - - - - - - - -
    -

    About

    -

    Sheetsee.js began as a part of my Code for America 2012 Fellowship project, See Penny Work. The idea and original code was to enable cities to easily publish and maintain themselves their budget data. The original sheetsee.js was built into Wordpress templates so that with the See Penny Work template, you could create pages that you only had to name and they would be populated with maps, charts and tables based on the page name corelating with a project in the spreadsheet.

    -

    In early 2013, after the CfA Fellowship, I recieved a grant from Mozilla Open News to pull out the sheetsee.js bits and make it a standalone open source library. That brought us to version 2.

    -

    The present version makes the project modular, customizable and with more maping and table features.

    -

    Built on top of Tabletop.js

    -

    Sheetsee pairs with tabletop.js a library that handles the messy interactions with the Google Spreadsheets API for you and returns a lovely array of your data. Every instance of Sheetsee begins with running tabletop.js. Well, actually, if you have some data on hand already in JSON format, you can use Sheetsee too :)

    -

    Sheetsee.js + Mapbox.js + d3.js

    -

    Once you've got the data, you're ready to Sheetsee. You can now decide if you want to map, chart or display your data in a table. Sheetsee's table module, sheetsee-tables, comes with sorting, filtering and pagination. Tables use icanhaz.js for very mustache.js-like templating.

    -

    Sheetsee-maps is built ontop of Leaflet.js and Mapbox.js and allows you to customize colors and popups of points, lines, polygons or multipolygons.

    -

    Finally, Sheetee-charts comes with three basic d3.js charts: bar, circle and line. It is difficult to make a chart that can suit many types of data, but it is easy to choose your own d3 chart and plug it in to sheetsee. Documentation for creating a d3 module is here.

    -

    Hacked on Openly

    - -

    Contact & Contribute

    - -

    Big Time Thanks

    -

    Thanks to Code for America for providing the platform me to build the first version of sheetsee.js for Macon, Georgia.

    -

    Thanks to Dan Sinker at Open News for having faith and getting things together to make this Code Sprint happen and thanks to Matt Green at WBEZ for being a willing partner.

    -

    Thanks to Max Ogden for emotional support, teaching me JavaScript and answering lots of questions.

    -

    Thanks to all the authors and contributors to Tabletop.js, Mapbox.js, Leaflet.js, jQuery, ICanHas.js and d3.js. Thanks to Google and the Internet for existing and to all those who've written tutorials or asked or answered a question on StackOverflow.

    -

    Thanks to Mom and Dad for getting a computer in 1996 and the mIRC scripts I started writing that I suppose would eventually lead me here.

    - - - -
    - - - diff --git a/site/docs/custom-charts.html b/site/docs/custom-charts.html deleted file mode 100644 index 7200c88..0000000 --- a/site/docs/custom-charts.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - Sheetsee.js - - - - - - - - - - - -
    -

    Custom Charts

    -

    It's easy to take a D3.js chart of your own and use it with Sheetsee.js. If you make it into a module, anyone can use your chart, too!

    -

    Sheetsee charts currently work by taking in some options, like so:

    -
    var pieOptions = {labels: "name", units: "units", m: [80, 80, 80, 80], w: 600, h: 400, div: "#pieChart", hiColor: "#14ECC8"}
    -
    -

    The labels represent the actual thing you're charting and units are how many of those things. Margin, width and height are m, w, h and the <div> to build your chart in is div. Finally, you can supply a highlight color if you want.

    -

    So, your chart could take the same options, but map them into your D3 code with the correct variables. An example from maxogden/sheetsee-d3bubble:

    -

    Append the d3.js code with a map of your sheetsee options

    -
    Sheetsee.d3BubbleChart = function(data, options) {
    -    var tree = {name: "data", children: []}
    -    var groups = {}
    -
    -    // data needs to look like this:
    -    // var data = { name: "wahtever", children: [
    -    //   { name: "group1", children: [
    -    //     { name: 'bob', size: 3},
    -    //     { name: 'judy', size: 5}
    -    //   ]},
    -    //   { name: "group2", children: [
    -    //     { name: 'jim', size: 10},
    -    //     { name: 'bill', size: 5}
    -    //   ]}
    -    // ]}
    -
    -    data.map(function(r) {
    -        var groupName = r[options.group]
    -        groups[groupName] = true
    -    })
    -
    -    Object.keys(groups).map(function(groupName) {
    -        var groupMembers = []
    -        data.map(function(r) {
    -            if (r[options.group] !== groupName) return
    -            groupMembers.push({name: r[options.name], size: r[options.size]})
    -        })
    -        tree.children.push({name: groupName, children: groupMembers})
    -    })
    -
    -  // the rest of the code
    -
    -

    In your HTML call it like so

    -
    <script type="text/javascript">
    -  document.addEventListener('DOMContentLoaded', function() {
    -    var URL = "0AvFUWxii39gXdFhqZzdTeU5DTWtOdENkQ1Y5bHdqT0E"
    -    Tabletop.init( { key: URL, callback: showInfo, simpleSheet: true } )
    -  })
    -
    -  function showInfo(data) {
    -    Sheetsee.d3BubbleChart(data, { name: 'name', size: 'cuddlability', group: 'kind', div: '#stuff'})
    -  }
    -</script>
    -
    -

    There are lots of charts to get excited about in the D3 gallery.

    -

    View the entire source

    - - - -
    - - - diff --git a/site/docs/sheetsee-charts.html b/site/docs/sheetsee-charts.html deleted file mode 100644 index 06b3540..0000000 --- a/site/docs/sheetsee-charts.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - Sheetsee.js - - - - - - - - - - - -
    -

    Sheetsee-charts

    -

    View Demo

    -

    Sheetsee.js provides three D3.js chart options to use with your spreadsheet data: a bar chart, line graph and pie chart. You can also use a custom D3 chart with Sheetsee, read about that here.

    -

    Make a Chart

    -

    Each chart requires your data be an array of objects, with objects containing label and units key/value pairs.

    -

    Experiment with the charts to find the correct size your <div> will need to be to hold the chart with your data in it nicely.

    -

    You can also make your own D3 chart in a separate .js file, link to that in your HTML head and pass your data on to it after Tabletop.js returns. Information here on using your own chart.

    -

    Bar Chart

    -

    To create a bar chart you'll need to add a placeholder <div> in your HTML with an id.

    -
    <div id="barChart"></div>
    -
    -

    In your CSS, give it dimensions.

    -
    #barChart {height: 400px; max-width: 600px; background: #F8CDCD;}
    -
    -

    You'll also have these CSS elements to style however you'd like:

    -
    .labels text {text-align: right;}
    -.bar .labels text {fill: #333;}
    -.bar rect {fill: #e6e6e6;}
    -.axis {shape-rendering: crispEdges;}
    -.x.axis line {stroke: #fff; fill: none;}
    -.x.axis path {fill: none;}
    -.x.axis text {fill: #333;}
    -.xLabel {font-family: sans-serif; font-size: 9px;}
    -
    -

    In a <script> tag set up your options.

    -
    var barOptions = {labels: "name", units: "cuddleability", m: [60, 60, 30, 150], w: 600, h: 400, div: "#barChart", xaxis: "no. of pennies", hiColor: "#FF317D"}
    -
    -
      -
    • labels is a string, usually a column header, it's what you call what you're charting
    • -
    • units is a string, usually a column header, it's the value you're charting
    • -
    • m is margins: top, right, bottom, left
    • -
    • w and h are width and height, this should match your CSS specs
    • -
    • div is the id for the <div> in your HTML
    • -
    • xaxis is optional text label for your x axis
    • -
    • hiColor is the highlight color of your choosing!
    • -
    -

    Then call the d3BarChart() function with your data and options.

    -
    Sheetsee.d3BarChart(data, barOptions)
    -
    -

    Line Chart

    -

    To create a line chart you'll need to add a placeholder <div> in your html with an id.

    -
    <div id="lineChart"></div>
    -
    -

    In your CSS, give it dimensions.

    -
    #lineChart {height: 400px; max-width: 600px; background: #F8CDCD;}
    -
    -

    And these chart elements to style:

    -
    .axis {shape-rendering: crispEdges;}
    -.x.axis .minor, .y.axis .minor {stroke-opacity: .5;}
    -.x.axis {stroke-opacity: 1;}
    -.y.axis line, .y.axis path {fill: none; stroke: #acacac; stroke-width: 1;}
    -.bigg {-webkit-transition: all .2s ease-in-out; -webkit-transform: scale(2);}
    -path.chartLine {stroke: #333; stroke-width: 3; fill: none;}
    -div.tooltip {position: absolute; text-align: left; padding: 4px 8px; width: auto; font-size: 10px; height: auto; background: #fff; border: 0px; pointer-events: none;}
    -circle {fill: #e6e6e6;}
    -
    -

    In a <script> tag set up your options.

    -
    var lineOptions = {labels: "name", units: "cuddleability", m: [80, 100, 120, 100], w: 600, h: 400, div: "#lineChart", yaxis: "no. of pennies", hiColor: "#14ECC8"}
    -
    -
      -
    • labels is a string, usually a column header, it's what you call what you're charting
    • -
    • units is a string, usually a column header, it's the value you're charting
    • -
    • m is your margins: top, right, bottom, left
    • -
    • w and h are width and height, this should match your CSS specs
    • -
    • div is the id for the <div> in your HTML
    • -
    • yaxis is optional text label for your y axis
    • -
    • hiColor is the highlight color of your choosing!
    • -
    -

    Then call the d3LineChart() function with your data and options.

    -
    Sheetsee.d3LineChart(data, lineOptions)
    -
    -

    Pie Chart

    -

    To create a bar chart you'll need to add a placeholder <div> in your html with an id.

    -
    <div id="pieChart"></div>
    -
    -

    In your CSS, give it dimensions.

    -
    #pieChart {height: 400px; max-width: 600px; background: #F8CDCD;}
    -
    -

    Style chart elements:

    -
    .arc path { stroke: #fff;}
    -
    -

    In a <script> tag set up your options.

    -
    var pieOptions = {labels: "name", units: "units", m: [80, 80, 80, 80], w: 600, h: 400, div: "#pieChart", hiColor: "#14ECC8"}
    -
    -
      -
    • labels is a string, usually a column header, it's what you call what you're charting
    • -
    • units is a string, usually a column header, it's the value you're charting
    • -
    • m is your margins: top, right, bottom, left
    • -
    • w and h are width and height, this should match your CSS specs
    • -
    • div is the id for the <div> in your HTML
    • -
    • hiColor is the highlight color of your choosing!
    • -
    -

    Then call the d3PieChart() function with your data and options.

    -
    Sheetsee.d3PieChart(data, pieOptions)
    -
    - - - -
    - - - diff --git a/site/docs/sheetsee-core.html b/site/docs/sheetsee-core.html deleted file mode 100644 index f85b111..0000000 --- a/site/docs/sheetsee-core.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - Sheetsee.js - - - - - - - - - - - -
    -

    Sheetsee-core

    -

    This is the core module in sheetsee and is included in all builds. It contains the functions for building your custom file as well as the basic data manipulation functions.

    -

    Working With Your Data

    -

    Tabletop.js will fetch the data from your spreadsheet and return it as an array of objects. Sheetsee.js has functions built in to help you filter or reorganize the data if you'd like.

    -

    Sheetsee.getGroupCount(data, groupTerm)

    -

    This takes in your data, an array of objects, and searches for a string, groupTerm, in each piece of your data (formerly the cells of your spreadsheet). It returns the number of times it found the groupTerm.

    -
    getGroupCount(data, "cat")
    -// returns a number
    -
    -

    Sheetsee.getColumnTotal(data, column)

    -

    Given your data, an array of objects, and a string column header, this functions sums each cell in that column(so this collumn you mention best have numbers).

    -
    getColumnTotal(data, "cuddlability")
    -// returns number
    -
    -

    Sheetsee.getAveragefromColumn(data, column)

    -

    A really simple function that builds on getColumnTotal() by returning the average number in a column of numbers.

    -
    getColumnAverage(data, "cuddlability")
    -// returns number
    -
    -

    Sheetsee.getMin(data, column)

    -

    This will return an array of object or objects (if there is a tie) of the element with the lowest number value in the column you specify from your data.

    -
    getMin(data, "cuddlability")
    -// returns array
    -
    -

    Sheetsee.getMax(data, column)

    -

    This will return an array of object or objects (if there is a tie) of the element with the highest number value in the column you specify from your data.

    -
    getMin(data, "cuddlability")
    -// returns array
    -
    -

    Don't Forget JavaScript Math

    -

    Create variables that are the sums, differences, multiples and so forth of others. Lots of info on that here on MDN.

    -
    var profit09 = Sheetsee.getColumnTotal(data, "2009")
    -var profit10 = Sheetsee.getColumnTotal(data, "2010")
    -var difference = profit09 - profit10
    -
    -

    What These Little Bits are Good For

    -

    You don't have to just create tables of your data. You can have other portions of your page that show things like, "The difference taco consumption between last week and this week is..." These are easy to create with JavaScript math functions and knowing a little bit more about icanhaz.js.

    -

    Sheetsee.getMatches(data, filter, category)

    -

    Takes data as an array of objects, a string you'd like to filter and a string of the category you want it to look in (a column header from your spreadsheet).

    -
    getMatches(data, "dog", "kind")
    -
    -

    Returns an array of objects matching the category's filter.

    -
    [{"name": "coco", "kind": "dog"...}, {"name": "wolfgang", "kind": "dog"...},{"name": "cooc", "kind": "dog"...} ]
    -
    -

    Sheetsee.getOccurance(data, category)

    -

    Takes data as an array of objects and a string for category (a column header from your spreadsheet) you want tally how often an element occured.

    -
    getOccurance(data, "kind")
    -
    -

    Returns an object with keys and values for each variation of the category and its occurance.

    -
    {"dog": 3, "cat": 3}
    -
    -

    Sheetsee.makeColorArrayOfObject(data, colors)

    -

    If you use getOccurance() and want to then chart that data with d3.js, you'll need to make it into an array (instead of an object) and add colors back in (since the hexcolor column applies to the datapoints in your original dataset and not this new dataset).

    -

    This function takes in your data, as an object, and an array of hexidecimal color strings which you define.

    -
    var kinds = getOccurance(data, "kind")
    -var kindColors = ["#ff00ff", "#DCF13C"]
    -
    -var kindData = makeColorArrayOfObjects(mostPopBreeds, breedColors)
    -
    -

    It will return an array of objects formatted to go directly into a d3 chart with the appropriate units and label keys, like so:

    -
    [{"label": "dog", "units": 2, "hexcolor": "#ff00ff"}, {"label": "cat", "units": 3, "hexcolor": "#DCF13C"}]
    -
    -

    If you pass in an array of just one color it will repeat that color for all items. If you pass fewer colors than data elements it will repeat the sequences of colors for the remainder elements.

    -

    Sheetsee.addUnitsLabels(arrayObj, oldLabel, oldUnits)

    -

    If you're using data, the data directly from Tabletop, you'll need to format it before you use the d3 charts. You'll need to determine what part of your data you want to chart - what will be your label, what your charting, and what will be your units, how many of them are there (this should be a number).

    -
    var data =  [{"name": "coco", "kind": "dog", "cuddablity": 5}, {"name": "unagi", "kind": "cat", "cuddlability": 0}]
    -
    -

    For istance, if from our original data above we want to chart the age of each cat, we'll use:

    -
    Sheetsee.addUnitsLabels(data, "name", "cuddlability")
    -
    -

    Which will return an array, ready for the d3 charts:

    -
    [{"label": "coco", "kind": "dog", "units": 5}, {"label": "unagi", "kind": "cat", "units": 0}]
    -
    - - - -
    - - - diff --git a/site/docs/sheetsee-maps.html b/site/docs/sheetsee-maps.html deleted file mode 100644 index 895eb89..0000000 --- a/site/docs/sheetsee-maps.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - Sheetsee.js - - - - - - - - - - - -
    -

    Sheetsee-maps

    -

    View Demo

    -

    Sheetsee.js uses Mapbox.js and Leaflet.js to make maps of your points, polygons, lines or multipolygons (all coordinate based). Details on what that actually looks like here.

    -

    Maps: Polygons and Lines

    -

    Sheetsee-maps now supports polygons and lines. So long as you have the correct coordinate structure in your cells, Sheetsee will add them to the geoJSON it creates for your maps. More details for coordinates of lines and polygons in geoJSON are here, but briefly:

    -

    A linestring:

    -
    [-122.41722106933594, 37.7663045891584], [-122.40477561950684, 37.77695634643178]
    -

    A polygon:

    -
    [-122.41790771484375, 37.740381166384914], [-122.41790771484375, 37.74520008134973], [-122.40966796874999, 37.74520008134973],[-122.40966796874999, 37.740381166384914], [-122.41790771484375, 37.740381166384914]
    -

    A Multipolygon:

    -
    [[-122.431640625, 37.79106586542567], [-122.431640625, 37.797441398913286], [-122.42666244506835, 37.797441398913286],[-122.42666244506835, 37.79106586542567], [-122.431640625, 37.79106586542567]],
    -[[-122.43352890014648, 37.78197638783258], [-122.43352890014648, 37.789031004883654], [-122.42443084716797, 37.789031004883654], [-122.42443084716797, 37.78197638783258], [-122.43352890014648, 37.78197638783258]]
    -
    -### The Parts
    -
    -You'll create a placeholder `<div>` in your HTML, CSS giving it a size and fire up a map from within `<script>` tags. You can also customize your popup content.
    -
    -## Your HTML Placeholder `<div>`
    -
    -Create an empty `<div>` in your HTML, with an id (name). Add CSS to give it dimensions
    -
    -```HTML
    -<div id="map"></div>
    -

    CSS

    -
    #map {width: 500px; height: 500px;}
    -
    -

    Your <script> Functions

    -

    Next you'll need to create geoJSON out of your data so that it can be mapped.

    -

    Sheetsee.createGeoJSON(data, optionsJSON)

    -

    This takes in your data and the parts of your data, optionsJSON, that you plan on including in your map's popups. These will be column headers in your spreadsheet. If you're not going to have popups on your markers, don't worry about it then and just pass in your data (by default it will use all the row's information).

    -
    var optionsJSON = ["name", "breed", "cuddlability"]
    -var geoJSON = Sheetsee.createGeoJSON(gData, optionsJSON)
    -
    -

    It will return an array in the special geoJSON format that map making things love.

    -
    [{
    -  "geometry": {"type": "Point", "coordinates": [long, lat]},
    -  "properties": {
    -    "marker-size": "small",
    -    "marker-color": lineItem.hexcolor
    -  },
    -  "opts": {},
    -}}
    -
    -

    Sheetsee.loadMap(mapDiv)

    -

    To create a simple map, with no data, you simply call .loadMap() and pass in a string of the mapDiv (with no '#') from your HTML.

    -
    var map = Sheetsee.loadMap("map")
    -
    -

    Sheetsee.addTileLayer(map, tileLayer)

    -

    To add a tile layer (aka a custom map scheme/design/background) you'll use this function which takes in your map and the source of the tileLayer. This source can be a Mapbox id, a URL to a TileJSON or your own generated TileJSON. See Mapbox's Documentation for more information.

    -
    Sheetsee.addTileLayer(map, 'jllord.n7aml2bc')
    -
    -

    You can add tiles from awesome mapmakers like Stamen or create your own in Mapbox's Tilemill or online.

    -

    Sheetsee.addMarkerLayer(geoJSON, map)

    -

    To add makers, lines or shapes to your map, use this function and pass in your geoJSON so that it can get the coordinates and your map so that it places the markers there. You can customize what the content in your marker's popup looks like with a popupTemplate, which is an ICanHaz.js template in HTML and can reference the column headers you included in your optionsJSON.

    -
    var markerLayer = Sheetsee.addMarkerLayer(geoJSON, map, popupTemplate)
    -
    -

    Example template:

    -
    var popupTemplate = "<h4>Hello {{name}}</h4>"
    -
    -

    Source from the map demo:

    -
    <script type="text/javascript">
    -  document.addEventListener('DOMContentLoaded', function() {
    -    var gData
    -    var URL = "0Ao5u1U6KYND7dGN5QngweVJUWE16bTRob0d2a3dCbnc"
    -    Tabletop.init( { key: URL, callback: showInfo, simpleSheet: true } )
    -  })
    -
    -  function showInfo(data) {
    -    gData = data
    -    var optionsJSON = ["placename", "photo-url"]
    -    var template = "<ul><li><a href='{{photo-url}}' target='_blank'>"
    -                 + "<img src='{{photo-url}}'></a></li>"
    -                 + "<li><h4>{{placename}}</h4></li></ul>"
    -    var geoJSON = Sheetsee.createGeoJSON(gData, optionsJSON)
    -    var map = Sheetsee.loadMap("map")
    -    Sheetsee.addTileLayer(map, 'jllord.n7aml2bc')
    -    var markerLayer = Sheetsee.addMarkerLayer(geoJSON, map, template)
    -  }
    -</script>
    -
    - - - -
    - - - diff --git a/site/img/fbi_spinner.gif b/site/img/fbi_spinner.gif deleted file mode 100644 index 872ede5..0000000 Binary files a/site/img/fbi_spinner.gif and /dev/null differ diff --git a/site/img/hexcolors.png b/site/img/hexcolors.png deleted file mode 100644 index 83c98d9..0000000 Binary files a/site/img/hexcolors.png and /dev/null differ diff --git a/site/img/key.png b/site/img/key.png deleted file mode 100644 index 94ef13b..0000000 Binary files a/site/img/key.png and /dev/null differ diff --git a/site/img/latlong.png b/site/img/latlong.png deleted file mode 100644 index 343e393..0000000 Binary files a/site/img/latlong.png and /dev/null differ diff --git a/site/img/nonos.png b/site/img/nonos.png deleted file mode 100644 index 15ab0a7..0000000 Binary files a/site/img/nonos.png and /dev/null differ diff --git a/site/img/publish.png b/site/img/publish.png deleted file mode 100644 index c0ff710..0000000 Binary files a/site/img/publish.png and /dev/null differ diff --git a/site/img/sheetsee-03.png b/site/img/sheetsee-03.png deleted file mode 100644 index a9a4154..0000000 Binary files a/site/img/sheetsee-03.png and /dev/null differ diff --git a/site/img/spreadsheettodata.png b/site/img/spreadsheettodata.png deleted file mode 100644 index d787d9f..0000000 Binary files a/site/img/spreadsheettodata.png and /dev/null differ diff --git a/site/img/webconsole.png b/site/img/webconsole.png deleted file mode 100644 index 610ad9b..0000000 Binary files a/site/img/webconsole.png and /dev/null differ diff --git a/site/js/highlight.js b/site/js/highlight.js deleted file mode 100644 index 5807522..0000000 --- a/site/js/highlight.js +++ /dev/null @@ -1 +0,0 @@ -var hljs=new function(){function k(v){return v.replace(/&/gm,"&").replace(//gm,">")}function t(v){return v.nodeName.toLowerCase()}function i(w,x){var v=w&&w.exec(x);return v&&v.index==0}function d(v){return Array.prototype.map.call(v.childNodes,function(w){if(w.nodeType==3){return b.useBR?w.nodeValue.replace(/\n/g,""):w.nodeValue}if(t(w)=="br"){return"\n"}return d(w)}).join("")}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^language-/,"")});return v.filter(function(x){return j(x)||x=="no-highlight"})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(t(A)=="br"){z+=1}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset"}function E(G){F+=""}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=k(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+k(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};function E(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})}if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b=D.bK.split(" ").join("|")}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?\\b("+F.b+")\\b\\.?":F.b}).concat([D.tE]).concat([D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}};D.continuation={}}x(y)}function c(S,L,J,R){function v(U,V){for(var T=0;T";U+=Z+'">';return U+X+Y}function N(){var U=k(C);if(!I.k){return U}var T="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(U);while(V){T+=U.substr(X,V.index-X);var W=E(I,V);if(W){H+=W[1];T+=w(W[0],V[0])}else{T+=V[0]}X=I.lR.lastIndex;V=I.lR.exec(U)}return T+U.substr(X)}function F(){if(I.sL&&!f[I.sL]){return k(C)}var T=I.sL?c(I.sL,C,true,I.continuation.top):g(C);if(I.r>0){H+=T.r}if(I.subLanguageMode=="continuous"){I.continuation.top=T.top}return w(T.language,T.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(V,U){var T=V.cN?w(V.cN,"",true):"";if(V.rB){D+=T;C=""}else{if(V.eB){D+=k(U)+T;C=""}else{D+=T;C=U}}I=Object.create(V,{parent:{value:I}})}function G(T,X){C+=T;if(X===undefined){D+=Q();return 0}var V=v(X,I);if(V){D+=Q();P(V,X);return V.rB?0:X.length}var W=z(I,X);if(W){var U=I;if(!(U.rE||U.eE)){C+=X}D+=Q();do{if(I.cN){D+=""}H+=I.r;I=I.parent}while(I!=W.parent);if(U.eE){D+=k(X)}C="";if(W.starts){P(W.starts,"")}return U.rE?0:X.length}if(A(X,I)){throw new Error('Illegal lexeme "'+X+'" for mode "'+(I.cN||"")+'"')}C+=X;return X.length||1}var M=j(S);if(!M){throw new Error('Unknown language: "'+S+'"')}m(M);var I=R||M;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,D,true)}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+=""}}return{r:H,value:D,language:S,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:k(L)}}else{throw O}}}function g(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:k(y)};var w=v;x.forEach(function(z){if(!j(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function h(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"
    ")}return v}function p(z){var y=d(z);var A=r(z);if(A=="no-highlight"){return}var v=A?c(A,y,true):g(y);var w=u(z);if(w.length){var x=document.createElementNS("http://www.w3.org/1999/xhtml","pre");x.innerHTML=v.value;v.value=q(w,u(x),y)}v.value=h(v.value);z.innerHTML=v.value;z.className+=" hljs "+(!A&&v.language||"");z.result={language:v.language,re:v.r};if(v.second_best){z.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function e(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function j(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=g;this.fixMarkup=h;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=e;this.getLanguage=j;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("bash",function(b){var a={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)\}/}]};var d={cN:"string",b:/"/,e:/"/,c:[b.BE,a,{cN:"variable",b:/\$\(/,e:/\)/,c:[b.BE]}]};var c={cN:"string",b:/'/,e:/'/};return{l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for break continue while in do done exit return set declare case esac export exec",literal:"true false",built_in:"printf echo read cd pwd pushd popd dirs let eval unset typeset readonly getopts source shopt caller type hash bind help sudo",operator:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"shebang",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:true,c:[b.inherit(b.TM,{b:/\w[\w\d_]*/})],r:0},b.HCM,b.NM,d,c,a]}});hljs.registerLanguage("diff",function(a){return{c:[{cN:"chunk",r:10,v:[{b:/^\@\@ +\-\d+,\d+ +\+\d+,\d+ +\@\@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"header",v:[{b:/Index: /,e:/$/},{b:/=====/,e:/=====$/},{b:/^\-\-\-/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+\+\+/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"change",b:"^\\!",e:"$"}]}});hljs.registerLanguage("javascript",function(a){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},a.ASM,a.QSM,a.CLCM,a.CBLCLM,a.CNM,{b:"("+a.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[a.CLCM,a.CBLCLM,a.REGEXP_MODE,{b:/;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[a.CLCM,a.CBLCLM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+a.IR,r:0}]}});hljs.registerLanguage("xml",function(a){var c="[A-Za-z0-9\\._:-]+";var d={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"};var b={eW:true,i:/]+/}]}]}]};return{aliases:["html"],cI:true,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[b],starts:{e:"",rE:true,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},d,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"title",b:"[^ /><]+",r:0},b]}]}});hljs.registerLanguage("markdown",function(a){return{c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].+?[\\)\\]]",rB:true,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:true,rE:true,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:true,eE:true},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:true,eE:true,}],r:10},{b:"^\\[.+\\]:",e:"$",rB:true,c:[{cN:"link_reference",b:"\\[",e:"\\]",eB:true,eE:true},{cN:"link_url",b:"\\s",e:"$"}]}]}});hljs.registerLanguage("css",function(a){var b="[a-zA-Z-][a-zA-Z0-9_-]*";var c={cN:"function",b:b+"\\(",e:"\\)",c:["self",a.NM,a.ASM,a.QSM]};return{cI:true,i:"[=/|']",c:[a.CBLCLM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:true,eE:true,r:0,c:[c,a.ASM,a.QSM,a.NM]}]},{cN:"tag",b:b,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[a.CBLCLM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[c,a.NM,a.QSM,a.ASM,a.CBLCLM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}});hljs.registerLanguage("http",function(a){return{i:"\\S",c:[{cN:"status",b:"^HTTP/[0-9\\.]+",e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{cN:"request",b:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",rB:true,e:"$",c:[{cN:"string",b:" ",e:" ",eB:true,eE:true}]},{cN:"attribute",b:"^\\w",e:": ",eE:true,i:"\\n|\\s|=",starts:{cN:"string",e:"$"}},{b:"\\n\\n",starts:{sL:"",eW:true}}]}});hljs.registerLanguage("ini",function(a){return{cI:true,i:/\S/,c:[{cN:"comment",b:";",e:"$"},{cN:"title",b:"^\\[",e:"\\]"},{cN:"setting",b:"^[a-z0-9\\[\\]_-]+[ \\t]*=[ \\t]*",e:"$",c:[{cN:"value",eW:true,k:"on off true false yes no",c:[a.QSM,a.NM],r:0}]}]}});hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}}); \ No newline at end of file diff --git a/site/js/sheetsee.js b/site/js/sheetsee.js deleted file mode 100644 index 23f82a6..0000000 --- a/site/js/sheetsee.js +++ /dev/null @@ -1,25395 +0,0 @@ -;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o": ">", - '"': '"', - "'": ''' - }; - - function escapeHTML(string) { - return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) { - return escapeMap[s] || s; - }); - } - - var regexCache = {}; - var Renderer = function () {}; - - Renderer.prototype = { - otag: "{{", - ctag: "}}", - pragmas: {}, - buffer: [], - pragmas_implemented: { - "IMPLICIT-ITERATOR": true - }, - context: {}, - - render: function (template, context, partials, in_recursion) { - // reset buffer & set context - if (!in_recursion) { - this.context = context; - this.buffer = []; // TODO: make this non-lazy - } - - // fail fast - if (!this.includes("", template)) { - if (in_recursion) { - return template; - } else { - this.send(template); - return; - } - } - - // get the pragmas together - template = this.render_pragmas(template); - - // render the template - var html = this.render_section(template, context, partials); - - // render_section did not find any sections, we still need to render the tags - if (html === false) { - html = this.render_tags(template, context, partials, in_recursion); - } - - if (in_recursion) { - return html; - } else { - this.sendLines(html); - } - }, - - /* - Sends parsed lines - */ - send: function (line) { - if (line !== "") { - this.buffer.push(line); - } - }, - - sendLines: function (text) { - if (text) { - var lines = text.split("\n"); - for (var i = 0; i < lines.length; i++) { - this.send(lines[i]); - } - } - }, - - /* - Looks for %PRAGMAS - */ - render_pragmas: function (template) { - // no pragmas - if (!this.includes("%", template)) { - return template; - } - - var that = this; - var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) { - return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); - }); - - return template.replace(regex, function (match, pragma, options) { - if (!that.pragmas_implemented[pragma]) { - throw({message: - "This implementation of mustache doesn't understand the '" + - pragma + "' pragma"}); - } - that.pragmas[pragma] = {}; - if (options) { - var opts = options.split("="); - that.pragmas[pragma][opts[0]] = opts[1]; - } - return ""; - // ignore unknown pragmas silently - }); - }, - - /* - Tries to find a partial in the curent scope and render it - */ - render_partial: function (name, context, partials) { - name = trim(name); - if (!partials || partials[name] === undefined) { - throw({message: "unknown_partial '" + name + "'"}); - } - if (!context || typeof context[name] != "object") { - return this.render(partials[name], context, partials, true); - } - return this.render(partials[name], context[name], partials, true); - }, - - /* - Renders inverted (^) and normal (#) sections - */ - render_section: function (template, context, partials) { - if (!this.includes("#", template) && !this.includes("^", template)) { - // did not render anything, there were no sections - return false; - } - - var that = this; - - var regex = this.getCachedRegex("render_section", function (otag, ctag) { - // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder - return new RegExp( - "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) - - otag + // {{ - "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) - ctag + // }} - - "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped - - otag + // {{ - "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). - ctag + // }} - - "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. - - "g"); - }); - - - // for each {{#foo}}{{/foo}} section do... - return template.replace(regex, function (match, before, type, name, content, after) { - // before contains only tags, no sections - var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", - - // after may contain both sections and tags, so use full rendering function - renderedAfter = after ? that.render(after, context, partials, true) : "", - - // will be computed below - renderedContent, - - value = that.find(name, context); - - if (type === "^") { // inverted section - if (!value || Array.isArray(value) && value.length === 0) { - // false or empty list, render it - renderedContent = that.render(content, context, partials, true); - } else { - renderedContent = ""; - } - } else if (type === "#") { // normal section - if (Array.isArray(value)) { // Enumerable, Let's loop! - renderedContent = that.map(value, function (row) { - return that.render(content, that.create_context(row), partials, true); - }).join(""); - } else if (that.is_object(value)) { // Object, Use it as subcontext! - renderedContent = that.render(content, that.create_context(value), - partials, true); - } else if (typeof value == "function") { - // higher order section - renderedContent = value.call(context, content, function (text) { - return that.render(text, context, partials, true); - }); - } else if (value) { // boolean section - renderedContent = that.render(content, context, partials, true); - } else { - renderedContent = ""; - } - } - - return renderedBefore + renderedContent + renderedAfter; - }); - }, - - /* - Replace {{foo}} and friends with values from our view - */ - render_tags: function (template, context, partials, in_recursion) { - // tit for tat - var that = this; - - var new_regex = function () { - return that.getCachedRegex("render_tags", function (otag, ctag) { - return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g"); - }); - }; - - var regex = new_regex(); - var tag_replace_callback = function (match, operator, name) { - switch(operator) { - case "!": // ignore comments - return ""; - case "=": // set new delimiters, rebuild the replace regexp - that.set_delimiters(name); - regex = new_regex(); - return ""; - case ">": // render partial - return that.render_partial(name, context, partials); - case "{": // the triple mustache is unescaped - case "&": // & operator is an alternative unescape method - return that.find(name, context); - default: // escape the value - return escapeHTML(that.find(name, context)); - } - }; - var lines = template.split("\n"); - for(var i = 0; i < lines.length; i++) { - lines[i] = lines[i].replace(regex, tag_replace_callback, this); - if (!in_recursion) { - this.send(lines[i]); - } - } - - if (in_recursion) { - return lines.join("\n"); - } - }, - - set_delimiters: function (delimiters) { - var dels = delimiters.split(" "); - this.otag = this.escape_regex(dels[0]); - this.ctag = this.escape_regex(dels[1]); - }, - - escape_regex: function (text) { - // thank you Simon Willison - if (!arguments.callee.sRE) { - var specials = [ - '/', '.', '*', '+', '?', '|', - '(', ')', '[', ']', '{', '}', '\\' - ]; - arguments.callee.sRE = new RegExp( - '(\\' + specials.join('|\\') + ')', 'g' - ); - } - return text.replace(arguments.callee.sRE, '\\$1'); - }, - - /* - find `name` in current `context`. That is find me a value - from the view object - */ - find: function (name, context) { - name = trim(name); - - // Checks whether a value is thruthy or false or 0 - function is_kinda_truthy(bool) { - return bool === false || bool === 0 || bool; - } - - var value; - - // check for dot notation eg. foo.bar - if (name.match(/([a-z_]+)\./ig)) { - var childValue = this.walk_context(name, context); - if (is_kinda_truthy(childValue)) { - value = childValue; - } - } else { - if (is_kinda_truthy(context[name])) { - value = context[name]; - } else if (is_kinda_truthy(this.context[name])) { - value = this.context[name]; - } - } - - if (typeof value == "function") { - return value.apply(context); - } - if (value !== undefined) { - return value; - } - // silently ignore unkown variables - return ""; - }, - - walk_context: function (name, context) { - var path = name.split('.'); - // if the var doesn't exist in current context, check the top level context - var value_context = (context[path[0]] != undefined) ? context : this.context; - var value = value_context[path.shift()]; - while (value != undefined && path.length > 0) { - value_context = value; - value = value[path.shift()]; - } - // if the value is a function, call it, binding the correct context - if (typeof value == "function") { - return value.apply(value_context); - } - return value; - }, - - // Utility methods - - /* includes tag */ - includes: function (needle, haystack) { - return haystack.indexOf(this.otag + needle) != -1; - }, - - // by @langalex, support for arrays of strings - create_context: function (_context) { - if (this.is_object(_context)) { - return _context; - } else { - var iterator = "."; - if (this.pragmas["IMPLICIT-ITERATOR"]) { - iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; - } - var ctx = {}; - ctx[iterator] = _context; - return ctx; - } - }, - - is_object: function (a) { - return a && typeof a == "object"; - }, - - /* - Why, why, why? Because IE. Cry, cry cry. - */ - map: function (array, fn) { - if (typeof array.map == "function") { - return array.map(fn); - } else { - var r = []; - var l = array.length; - for(var i = 0; i < l; i++) { - r.push(fn(array[i])); - } - return r; - } - }, - - getCachedRegex: function (name, generator) { - var byOtag = regexCache[this.otag]; - if (!byOtag) { - byOtag = regexCache[this.otag] = {}; - } - - var byCtag = byOtag[this.ctag]; - if (!byCtag) { - byCtag = byOtag[this.ctag] = {}; - } - - var regex = byCtag[name]; - if (!regex) { - regex = byCtag[name] = generator(this.otag, this.ctag); - } - - return regex; - } - }; - - return({ - name: "mustache.js", - version: "0.4.0", - - /* - Turns a template and view into HTML - */ - to_html: function (template, view, partials, send_fun) { - var renderer = new Renderer(); - if (send_fun) { - renderer.send = send_fun; - } - renderer.render(template, view || {}, partials); - if (!send_fun) { - return renderer.buffer.join("\n"); - } - } - }); -}(); -/*! - ICanHaz.js -- by @HenrikJoreteg -*/ -/*global */ -(function () { - function trim(stuff) { - if (''.trim) return stuff.trim(); - else return stuff.replace(/^\s+/, '').replace(/\s+$/, ''); - } - - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - var ich = { - VERSION: "0.10.2", - templates: {}, - - // grab jquery or zepto if it's there - $: (typeof window !== 'undefined') ? window.jQuery || window.Zepto || null : null, - - // public function for adding templates - // can take a name and template string arguments - // or can take an object with name/template pairs - // We're enforcing uniqueness to avoid accidental template overwrites. - // If you want a different template, it should have a different name. - addTemplate: function (name, templateString) { - if (typeof name === 'object') { - for (var template in name) { - this.addTemplate(template, name[template]); - } - return; - } - if (ich[name]) { - console.error("Invalid name: " + name + "."); - } else if (ich.templates[name]) { - console.error("Template \"" + name + " \" exists"); - } else { - ich.templates[name] = templateString; - ich[name] = function (data, raw) { - data = data || {}; - var result = Mustache.to_html(ich.templates[name], data, ich.templates); - return (ich.$ && !raw) ? ich.$(trim(result)) : result; - }; - } - }, - - // clears all retrieval functions and empties cache - clearAll: function () { - for (var key in ich.templates) { - delete ich[key]; - } - ich.templates = {}; - }, - - // clears/grabs - refresh: function () { - ich.clearAll(); - ich.grabTemplates(); - }, - - // grabs templates from the DOM and caches them. - // Loop through and add templates. - // Whitespace at beginning and end of all templates inside