├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── bin ├── .gitignore └── www ├── data ├── .gitignore └── iso_grouped.json ├── package.json ├── public ├── .gitignore ├── data │ ├── cb_2014_us_county_20m.json │ ├── cb_2014_us_county_20m.svg │ └── worker-gender.csv ├── fonts │ ├── AdelleSansWeb │ │ ├── AdelleSansBasic_Bold.eot │ │ ├── AdelleSansBasic_Bold.ttf │ │ ├── AdelleSansBasic_Bold.woff │ │ ├── AdelleSansBasic_Light.eot │ │ ├── AdelleSansBasic_Light.svg │ │ ├── AdelleSansBasic_Light.ttf │ │ ├── AdelleSansBasic_Light.woff │ │ ├── AdelleSansBasic_Regular.eot │ │ ├── AdelleSansBasic_Regular.svg │ │ ├── AdelleSansBasic_Regular.ttf │ │ ├── AdelleSansBasic_Regular.woff │ │ └── AdelleSans_Light.otf │ ├── Calibre-Medium.otf │ ├── PTSerif │ │ ├── PTF55F-webfont.eot │ │ ├── PTF55F-webfont.svg │ │ ├── PTF55F-webfont.ttf │ │ ├── PTF55F-webfont.woff │ │ ├── PTF55F.ttf │ │ ├── PTF56F.ttf │ │ └── Paratype PT Sans Free Font License.txt │ ├── PT_Serif-Web-Bold.ttf │ ├── qz-icons.eot │ ├── qz-icons.json │ ├── qz-icons.svg │ ├── qz-icons.ttf │ └── qz-icons.woff ├── images │ ├── ajax-loader.gif │ ├── qz.svg │ ├── topo-gray.jpg │ ├── water-topo.ai │ ├── water-topo.jpg │ ├── white_21600x10800.jpg │ ├── white_7400x3700.jpg │ ├── world.200401.3x7400x3700.raw │ └── world.topo.bathy.200408.3x5400x2700.jpg ├── javascripts │ ├── FileSaver.min.js │ ├── d3.geo.projection.min.js │ ├── d3.v3.min.js │ ├── examples.js │ ├── index.js │ ├── jquery.min.js │ ├── queue.min.js │ ├── select2.min.js │ └── topojson.min.js └── stylesheets │ ├── fonts.css │ ├── index.css │ └── select2.min.css ├── queries.js ├── routes ├── .gitignore └── index.js ├── settings.js ├── shapefile-imports └── .gitignore ├── util ├── .gitignore ├── getScaleTranslate.js ├── pgToFc.js └── zip.js └── views ├── .gitignore ├── error.jade ├── error_message.jade ├── examples.jade ├── import.jade ├── index.jade ├── layout.jade └── nav.jade /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | shapefile-imports/starter-pack 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Keith Collins, Quartz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapquery by Quartz 2 | Mapquery 0.1.0 is a proof-of-concept prototype release. All of the work on the project leading to this initial release was funded by the [Knight Foundation's Prototype Fund](http://www.knightfoundation.org/funding-initiatives/knight-prototype-fund/). 3 | 4 | Mapquery is a map data storage and retrieval API built on [Express](https://github.com/expressjs/express) and [PostGIS](http://postgis.net/). When you import a shapefile to Mapquery, you can export what you want from it quickly--along with sizing and positioning information to fit any viewport. And once your map data lives in Mapquery, you’ll never have to go looking for that shapefile again. 5 | 6 | ![Mapquery Screenshots](http://data.qz.com/2016/static-images/mapquery-github/screens.png) 7 | 8 | ### Table of contents 9 | 10 | - [Installation](https://github.com/Quartz/mapquery#installation) 11 | - [Running Mapquery](https://github.com/Quartz/mapquery#running-mapquery) 12 | - [How to import data](https://github.com/Quartz/mapquery#how-to-import-data) 13 | - [Mapquery search](https://github.com/Quartz/mapquery#mapquery-search) 14 | - [d3 example](https://github.com/Quartz/mapquery#d3-example) 15 | - [To-do list (and how you can help)](https://github.com/Quartz/mapquery#to-do-list-and-how-you-can-help) 16 | - [API endpoint reference](https://github.com/Quartz/mapquery#api-endpoint-reference) 17 | - [/api/feature-collection](https://github.com/Quartz/mapquery#apifeature-collection) 18 | - [/api/geometry-collection](https://github.com/Quartz/mapquery#apigeometry-collection) 19 | - [/api/table-data](https://github.com/Quartz/mapquery#apitable-data) 20 | - [/api/units-by-table](https://github.com/Quartz/mapquery#apiunits-by-table) 21 | 22 | ## Installation 23 | 24 | 1. Install node modules `$ npm install` 25 | 2. Install Postgres `$ brew install postgresql` 26 | 3. Install PostGIS `$ brew install postgis` 27 | 4. Start Postgres server `$ pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start` 28 | 5. Create a new local Postgres database called `mapquery`. You can run `createdb mapquery` on the command line, or download [pgadmin](http://www.pgadmin.org/download/) and use the GUI. Set your local username as the owner. 29 | 6. Run this on the command line to enable PostGIS: 30 | ``` 31 | psql -q mapquery -c " 32 | -- Enable PostGIS (includes raster) 33 | CREATE EXTENSION postgis; 34 | -- Enable Topology 35 | CREATE EXTENSION postgis_topology; 36 | -- fuzzy matching needed for Tiger 37 | CREATE EXTENSION fuzzystrmatch; 38 | -- Enable US Tiger Geocoder 39 | CREATE EXTENSION postgis_tiger_geocoder; 40 | " 41 | ``` 42 | Or if you prefer to use pgAdmin: Click on the SQL button at the top of pgAdmin. That'll open a SQL query window. Pate in the above code, excluding the top line, and click the green Run button. 43 | 44 | 7. Download the Mapquery starter pack database dump from [here](https://s3.amazonaws.com/qz-files/mapquery.dump) 45 | 8. Restore the dump file to your database `$ pg_restore --verbose --clean --no-acl --no-owner -h localhost -U YOUR_LOCAL_USERNAME -d mapquery /PATH/TO/mapquery.dump` 46 | 9. In `settings.js`, update the database connection settings to match your own: 47 | ```js 48 | module.exports = { 49 | 'd': 'mapquery', // database name 50 | 'u': 'username', //username 51 | 'p': '', //password 52 | 'h': 'localhost', //host 53 | 'port': '5432' // port 54 | }; 55 | ``` 56 | 10. Start the app with `npm start` 57 | 11. [localhost:3000](http://localhost:3000/) will load the view from `views/index.jade` 58 | 59 | Note: When you make changes to your database that you want to sync on other computers, dump it with this command: 60 | 61 | `pg_dump -Fc --no-acl --no-owner -h localhost -U YOUR_LOCAL_USERNAME mapquery > mapquery.dump` 62 | 63 | Then restore with `pg_restore`, as shown above. 64 | 65 | ## Running Mapquery 66 | 67 | To run Mapquery: 68 | 69 | - Start Postgres server `$ pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start` 70 | - Start the app with `npm start` 71 | - Go to [localhost:3000](http://localhost:3000/) in your web browser to use the Mapquery interface. 72 | 73 | We are not yet running Mapquery in production. We'll be updating this readme with information as we begin to implement a production version of the app. If you do your own production implementation, please let us know how it goes! 74 | 75 | ## How to import data 76 | 77 | Importing is currently handled through the Mapquery interface. The routes `/import/import-map` and `/import/save-map-data` import a shapefile to the database and save metadata to `mqmeta`, respectively, but they do not send a json response. Instead, they render information to `views/import.jade`. Our current plan is to implement `POST` endpoints for importing shapefile data if we find that we need that functionality. 78 | 79 | 1. With Mapquery running, go to the import page at [localhost:3000/import](http://localhost:3000/import) in your web browser. 80 | 2. Click "Select file to import" and choose a .zip file, which must include .shp and .dbf files. 81 | 3. Once you've selected the file, click "Upload" and you'll see this form, but blank: 82 | 83 | ![Mapquery Import Screenshot 1](http://data.qz.com/2016/static-images/mapquery-github/import-1.png) 84 | 85 | 4. All of this information will be inserted into the `mqmeta` table. Fill out the options: 86 | - **Map category:** You can adjust these options in `views/import.jade`. The categories are a bit arbitrary, but come in handy in the front-end search interface, for organizing your tables. 87 | - **Name of table:** This field should auto-fill with the name of the file you've selected to import. This will be the actual Postgres table name, so don't use spaces or weird characters here. 88 | - **Readable name:** Something more readable than the table name. 89 | - **Description:** Include any relevant info about the data. 90 | - **Map resolution:** The options here can be adjusted in `views/import.jade`. These are currently only used to display on the front-end search, but could be incorporated into filter searching/URL parameters once databases get larger. 91 | - **Data source/URL:** Where did you get this data? 92 | 5. Continue to the "Shapefile fields" section: 93 | ![Mapquery Import Screenshot 1](http://data.qz.com/2016/static-images/mapquery-github/import-2.png) 94 | 6. In this section, we map fields from the data you're importing to the `mqmeta` table. In the dropdowns, you'll see a preiview of _one of the rows_ of data. 95 | - **Name:** Most shapefiles should have a `name` field that provides a text representation of a given unit. In a shapefile containing countries, for example, the `name` field would be the name of each country. If the data you're importing doesn't happen to have a name field, try to find a similar field that indicates what each unit is. 96 | - **ISO Alpha 3:** If you're importing a table that includes the ISO code of each unit, it's useful to know what field the codes are in. 97 | - **Other unique identifier:** This might be the ISO code if you're importing countries, or a FIPS code if you're importing US counties. 98 | - **Group by:** If you're importing countries from a Natural Earth shapefile, for example, you may select the `continent` and `subregion` columns as your group-by fields. These are primarily for the front-end search function; it allows you to select a continent or subregion to make a map with. 99 | 7. Click "Save" 100 | 101 | ## Mapquery search 102 | 103 | Much of Mapquery is built around its front-end search feature. We hope its functionality is intuitive and self-explanatory, but there's a bit of philosophy behind how it works. First, we didn't want to hide the database tables from the end-user. When you want to make a map, the first thing you have to consider is what shape data you want to draw from. Once you've selected the table you want, the second dropdown populates based on that table's metadata, providing you in some cases with hundreds of options of units to pull out. 104 | 105 | We've discussed determining the best projection for the end-user based on the region their selected units fall into, but ultimately decided that _we_ would always want the option to pick our own projection, so assumed that you would, too. 106 | 107 | ## d3 example 108 | 109 | This example calls the Mapquery API as if it's running locally. Alternatively, you can download Mapquery's output from the front-end interface and load the static file. 110 | 111 | ```js 112 | var width = 940; 113 | var height = 500; 114 | var svg = d3.select("body").append("svg:svg") 115 | .attr("width", width) 116 | .attr("height", height); 117 | d3.json("http://localhost:3000/api/feature-collection?table=ne_50m_admin_0_countries&proj=kavrayskiy7&datatype=topojson&width="+width+"&height="+height,function(error,result){ 118 | 119 | var data = result.data; 120 | 121 | // when you call api/feature-collection, 122 | // Mapquery determines the appropriate scale and position 123 | var projection = d3.geo[data.projection]() 124 | .scale(data.scale) 125 | .translate(data.translate); 126 | 127 | var path = d3.geo.path() 128 | .projection(projection); 129 | 130 | // for topojson 131 | var units = data.map.objects.features; 132 | // for geojson, use data.map.features; 133 | 134 | svg.selectAll(".units") 135 | .data(units) 136 | .enter().append("path") 137 | .attr("class","units") 138 | .attr("id",function(d) { return d.properties.name }) 139 | .attr("d", path); 140 | }); 141 | ``` 142 | 143 | ## To-do list (and how you can help) 144 | 145 | ### Simplification 146 | 147 | There are [several](https://bost.ocks.org/mike/simplify/) [ways](https://www.jasondavies.com/simplify/) we could incorporate a simplification option into Mapquery. This will likely be the next feature we add, but invite motivated contributors to beat us to the punch. The simplification would ideally occur on the back-end, and be added as an npm module to the `utils` directory. 148 | 149 | ### Dynamic detail search 150 | 151 | On the Mapquery Search page, there are several disabled checkboxes for including details like airports or roads with a given search. One way this could work would be a bounding box search. We've written a bit of code [here](https://github.com/Quartz/mapquery/blob/master/queries.js#L62) which constructs a SQL query to search a given detail table for units. The problem here is that the query will return units for any country that falls within that bounding box, even if that country isn't included in the search. 152 | 153 | Another method would be to use the array of ISOs collected [here](https://github.com/Quartz/mapquery/blob/master/queries.js#L54) to search detail tables which include ISOs. But not every searchable table includes ISOs (like US counties, for example), nor does every detail table. 154 | 155 | It seems that the best approach may be to customize functionality around each detail. To get names of cities from Natural Earth's Populated Places table, for example, you could search by ISO, but you may also want to filter by the population field to limit the results to larger cities. 156 | 157 | Any ideas? 158 | 159 | ### Locator maps 160 | 161 | The user would query for an address or city, but what would Mapquery return? It seems we'll have to solve the dynamic detail search problem first. 162 | 163 | ### Centroids 164 | 165 | Adding the option to return centroids could be useful for building cartograms. 166 | 167 | ### Option to drop imported shapefiles 168 | 169 | By default, Mapquery saves imported zip files to the `shapefile-imports` folder. We should add an option on the import page to not keep the imported file, as we're currently not doing anything with them. 170 | 171 | ## API endpoint reference 172 | 173 | Route definitions can be found in `routes/index.js`; handlers are in `/queries.js`. 174 | 175 | ### /api/feature-collection 176 | 177 | Returns a Geojson or Topojson representation of a `FeatureCollection`, along with metadata about the map. This endpoint is currently the most fully-featured, because the `FeatureCollection` format allows for simple dynamic sizing and positioning. 178 | 179 | | Parameter | Required | Description | 180 | | -------------------- |:------------------------:|:-------------| 181 | | `table` | Yes | Must be the valid name of a table in your Postgres database 182 | | `field_value` | No (Default: All units) | Table field and value separated by a `:`. Must be a valid field name and value from the selected table. Example: "continent:Europe". 183 | | `width` | Yes | The width of the viewport of the resulting map 184 | | `height` | Yes | The height of the viewport of the resulting map 185 | | `proj` | Yes | Must be the valid name of any projection supported by [d3.geo](https://github.com/mbostock/d3/wiki/Geo-Projections) or d3's [extended projections plugin](https://github.com/d3/d3-geo-projection/). Example: "albersUsa". 186 | | `datatype` | No (Default: "topojson") | "topojson" or "geojson" 187 | 188 | **Example result** 189 | 190 | Request: `/api/feature-collection?table=ne_50m_admin_0_countries&proj=kavrayskiy7&width=940&height=500` 191 | 192 | Result: 193 | 194 | ``` 195 | { 196 | "status":"success", 197 | "message":"Retrieved FeatureCollection with projection data", 198 | "data":{ 199 | "table_metadata": 200 | { 201 | "table_id":20, 202 | "table_last_updated":"2016-04-01T16:36:11.983Z", 203 | "table_name":"ne_50m_admin_0_countries", 204 | "table_name_readable":"Countries", 205 | "table_description":null, 206 | "table_category":"Countries", 207 | "table_resolution":"1:50m", 208 | "table_source":"Natural Earth", 209 | "table_source_url":"http://www.naturalearthdata.com/downloads/50m-cultural-vectors/", 210 | "fld_iso_alpha_3":"iso_a3", 211 | "fld_name":"name", 212 | "fld_groupby1":"continent", 213 | "fld_groupby2":"subregion", 214 | "fld_groupby3":null, 215 | "fld_groupby4":null, 216 | "fld_groupby5":null, 217 | "fld_identifier":"iso_a3" 218 | }, 219 | "bounds":[[-2.68763343988672,-1.4590884304298841],[2.68763343988672,1.5707775819587302]], 220 | "scale":148.52141915187846, 221 | "translate":[470,241.7058843555333], 222 | "projection":"kavrayskiy7", 223 | "map":{ 224 | "type":"Topology", 225 | "objects":{ 226 | "type":"FeatureCollection", 227 | "features": [ARRAY OF FEATURES...] 228 | } 229 | } 230 | } 231 | } 232 | ``` 233 | 234 | ### /api/geometry-collection 235 | 236 | Returns a Geojson or Topojson representation of the specified geometry. This endpoint is currently fairly limited, as it does not provide any dynamic sizing or positioning, and we recommend using `api/feature-collection` instead. 237 | 238 | | Parameter | Required | Description | 239 | | -------------------- |:------------------------:|:-------------| 240 | | `table` | Yes | Must be the valid name of a table in your Postgres database 241 | | `field_value` | No (Default: All units) | Table field and value separated by a `:`. Must be a valid field name and value from the selected table. Example: "continent:Europe". 242 | | `datatype` | No (Default: "topojson") | "topojson" or "geojson" 243 | 244 | **Example result** 245 | 246 | Request: `/api/geometry-collection?table=ne_50m_admin_0_countries&field_value=continent:Europe&proj=mercator&datatype=topojson` 247 | 248 | Result: 249 | 250 | ``` 251 | { 252 | "status":"success", 253 | "message":"Retrieved geometry", 254 | "data":{ 255 | "type":"Topology", 256 | "objects":[ 257 | { 258 | "name":"Andorra", 259 | "gid":7, 260 | "geometry":"{ 261 | "type":"MultiPolygon", 262 | "coordinates":[ARRAY OF COORDINATES...] 263 | } 264 | }, 265 | { 266 | MORE OBJECTS... 267 | } 268 | ] 269 | } 270 | } 271 | ``` 272 | 273 | ### /api/table-data 274 | 275 | Returns all of the data from the `mqmeta` table, which is metadata about all of the maps you've imported into Mapquery. 276 | 277 | _No parameters are required (or accepted)._ 278 | 279 | **Example result** 280 | 281 | Request: `/api/table-data` 282 | 283 | Result: 284 | 285 | ``` 286 | { 287 | "data":[ 288 | { 289 | "table_id":10, 290 | "table_last_updated":"2016-02-16T22:11:10.929Z", 291 | "table_name":"ne_10m_admin_0_countries", 292 | "table_name_readable":"Countries", 293 | "table_description":"There are 247 countries in the world. Greenland as separate from Denmark. Most users will want this file instead of sovereign states.", 294 | "table_category":"Countries", 295 | "table_resolution":"1:10m", 296 | "table_source":"Natural Earth", 297 | "table_source_url":"http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-admin-0-countries/", 298 | "fld_iso_alpha_3":"iso_a3", 299 | "fld_name":"name", 300 | "fld_groupby1":"continent", 301 | "fld_groupby2":"subregion", 302 | "fld_groupby3":null, 303 | "fld_groupby4":null, 304 | "fld_groupby5":null, 305 | "fld_identifier":"iso_a3" 306 | }, 307 | {ETC...} 308 | ] 309 | } 310 | ``` 311 | 312 | ### /api/units-by-table 313 | 314 | When you import a shapefile to Mapquery through its front-end interface, you're asked which fields in the map data you'd like to group by. Those field names are recorded in the `mqmeta` table (see above). For example, if you're importing countries from a Natural Earth shapefile, you may select the `continent` and `subregion` columns as your group-by fields. You can then see all of the unique values in those fields by calling this endpoint, as well as all of the values in the table's `name` field. 315 | 316 | | Parameter | Required | Description | 317 | | -------------------- |:------------------------:|:-------------| 318 | | `table` | Yes | Must be the valid name of a table in your Postgres database 319 | 320 | **Example result** 321 | 322 | Request: `/api/units-by-table?table=ne_50m_admin_0_countries` 323 | 324 | Result: 325 | 326 | ``` 327 | { 328 | "continent":["North America","Asia","Europe","Africa","South America","Oceania","Antarctica","Seven seas (open ocean)"], 329 | "subregion":["Caribbean","Southern Asia","Southern Europe","Western Asia","Middle Africa","Northern Europe","South America","Polynesia","Antarctica","Australia and New Zealand","Seven seas (open ocean)","Western Europe","Eastern Africa","Western Africa","Eastern Europe","Central America","Northern America","Southern Africa","South-Eastern Asia","Eastern Asia","Central Asia","Northern Africa","Melanesia","Micronesia"], 330 | "name":[ALL COUNTRIES...] 331 | } 332 | ``` 333 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var routes = require('./routes/index'); 9 | 10 | var app = express(); 11 | 12 | // view engine setup 13 | app.set('views', path.join(__dirname, 'views')); 14 | app.set('view engine', 'jade'); 15 | 16 | // uncomment after placing your favicon in /public 17 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(cookieParser()); 22 | app.use(express.static(path.join(__dirname, 'public'))); 23 | 24 | app.use('/', routes); 25 | 26 | // catch 404 and forward to error handler 27 | app.use(function(req, res, next) { 28 | var err = new Error('Not Found'); 29 | err.status = 404; 30 | next(err); 31 | }); 32 | 33 | // error handlers 34 | 35 | // development error handler 36 | // will print stacktrace 37 | if (app.get('env') === 'development') { 38 | app.use(function(err, req, res, next) { 39 | res.status( err.code || 500 ) 40 | .json({ 41 | status: 'error', 42 | message: err.message, 43 | error: err 44 | }); 45 | }); 46 | } 47 | 48 | // production error handler 49 | // no stacktraces leaked to user 50 | app.use(function(err, req, res, next) { 51 | res.status(err.status || 500) 52 | .json({ 53 | status: 'error', 54 | message: err.message, 55 | error: {} 56 | }); 57 | }); 58 | 59 | // TODO: For errors on interface pages, render error to page 60 | // res.render('error', { 61 | // message: err.message, 62 | // error: err 63 | // }); 64 | 65 | module.exports = app; 66 | -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('mapquery:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapquery", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Keith Collins - Quartz", 6 | "url": "https://github.com/Quartz/" 7 | }, 8 | "description": "Map data retrieval API", 9 | "homepage": "https://github.com/Quartz/mapquery/", 10 | "license": "MIT", 11 | "scripts": { 12 | "start": "node ./bin/www" 13 | }, 14 | "dependencies": { 15 | "bluebird": "^3.3.4", 16 | "body-parser": "~1.13.2", 17 | "cookie-parser": "~1.3.5", 18 | "debug": "~2.2.0", 19 | "express": "~4.13.1", 20 | "jade": "~1.11.0", 21 | "morgan": "~1.6.1", 22 | "pg-promise": "^5.3.4", 23 | "serve-favicon": "~2.3.0", 24 | "multer": "*", 25 | "rimraf": "*", 26 | "adm-zip": "*", 27 | "d3": "~3.5.16", 28 | "d3-geo-projection": "~0.2.16", 29 | "topojson": "*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /public/data/worker-gender.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/data/worker-gender.csv -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Bold.eot -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Bold.ttf -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Bold.woff -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Light.eot -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Light.ttf -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Light.woff -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Regular.eot -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Regular.ttf -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSansBasic_Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSansBasic_Regular.woff -------------------------------------------------------------------------------- /public/fonts/AdelleSansWeb/AdelleSans_Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/AdelleSansWeb/AdelleSans_Light.otf -------------------------------------------------------------------------------- /public/fonts/Calibre-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/Calibre-Medium.otf -------------------------------------------------------------------------------- /public/fonts/PTSerif/PTF55F-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/PTSerif/PTF55F-webfont.eot -------------------------------------------------------------------------------- /public/fonts/PTSerif/PTF55F-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Copyright 2010 ParaType Ltd All rights reserved 7 | Designer : AKorolkova OUmpeleva VYefimov 8 | Foundry : ParaType Ltd 9 | Foundry URL : httpwwwparatypecom 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /public/fonts/PTSerif/PTF55F-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/PTSerif/PTF55F-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/PTSerif/PTF55F-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/PTSerif/PTF55F-webfont.woff -------------------------------------------------------------------------------- /public/fonts/PTSerif/PTF55F.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/PTSerif/PTF55F.ttf -------------------------------------------------------------------------------- /public/fonts/PTSerif/PTF56F.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/PTSerif/PTF56F.ttf -------------------------------------------------------------------------------- /public/fonts/PTSerif/Paratype PT Sans Free Font License.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2009 ParaType Ltd. 2 | with Reserved Names "PT Sans" and "ParaType". 3 | 4 | FONT LICENSE 5 | 6 | PERMISSION & CONDITIONS 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of the font software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the font software, subject to the following conditions: 8 | 9 | 1) Neither the font software nor any of its individual components, in original or modified versions, may be sold by itself. 10 | 11 | 2) Original or modified versions of the font software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 12 | 13 | 3) No modified version of the font software may use the Reserved Name(s) or combinations of Reserved Names with other words unless explicit written permission is granted by the ParaType. This restriction only applies to the primary font name as presented to the users. 14 | 15 | 4) The name of ParaType or the author(s) of the font software shall not be used to promote, endorse or advertise any modified version, except to acknowledge the contribution(s) of ParaType and the author(s) or with explicit written permission of ParaType. 16 | 17 | 5) The font software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. 18 | 19 | TERMINATION & TERRITORY 20 | This license has no limits on time and territory, but it becomes null and void if any of the above conditions are not met. 21 | 22 | DISCLAIMER 23 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL PARATYPE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 24 | 25 | ParaType Ltd 26 | http://www.paratype.ru -------------------------------------------------------------------------------- /public/fonts/PT_Serif-Web-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/PT_Serif-Web-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/qz-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/qz-icons.eot -------------------------------------------------------------------------------- /public/fonts/qz-icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "IcoMoonType": "selection", 3 | "icons": [ 4 | { 5 | "icon": { 6 | "paths": [ 7 | "M833.408 1024l2.688-2.816 0.256 0.256 181.12-180.992-1.152-1.152 649.088-655.744-181.12-180.992-648.96 655.744-655.872-655.744-180.992 181.12 660.736 660.864z" 8 | ], 9 | "attrs": [ 10 | {} 11 | ], 12 | "width": 1664, 13 | "grid": 0, 14 | "tags": [ 15 | "expand-arrow" 16 | ] 17 | }, 18 | "attrs": [ 19 | {} 20 | ], 21 | "properties": { 22 | "order": 30, 23 | "id": 31, 24 | "prevSize": 32, 25 | "code": 58887, 26 | "name": "expand-arrow", 27 | "ligatures": "" 28 | }, 29 | "setIdx": 0, 30 | "iconIdx": 0 31 | }, 32 | { 33 | "icon": { 34 | "paths": [ 35 | "M960 0h-896c-35.328 0-64 28.672-64 64v896c0 35.392 28.672 64 64 64h896c35.392 0 64-28.608 64-64v-896c0-35.328-28.608-64-64-64zM113.792 865.6v-432.576h94.080c-8.576 27.904-13.184 57.344-13.184 87.872 0 170.368 142.528 308.416 318.272 308.416 175.808 0 318.272-138.048 318.272-308.416 0-30.528-4.608-60.032-13.12-87.872h90.112v432.576c0 22.4-18.304 40.704-40.704 40.704h-713.024c-22.4 0-40.704-18.304-40.704-40.704zM699.904 270.208v-110.336c0-25.344 20.736-46.080 46.080-46.080h115.776c25.344 0 46.080 20.736 46.080 46.080v110.336c0 25.344-20.672 46.080-46.080 46.080h-115.776c-25.344 0-46.080-20.672-46.080-46.080zM718.656 509.76c0 110.144-92.032 199.296-205.632 199.296-113.536 0-205.632-89.216-205.632-199.296 0-110.016 92.096-199.232 205.632-199.232s205.632 89.216 205.632 199.232z" 36 | ], 37 | "attrs": [ 38 | {} 39 | ], 40 | "grid": 0, 41 | "tags": [ 42 | "instagram" 43 | ] 44 | }, 45 | "attrs": [ 46 | {} 47 | ], 48 | "properties": { 49 | "order": 29, 50 | "id": 30, 51 | "prevSize": 32, 52 | "code": 58898, 53 | "name": "instagram", 54 | "ligatures": "" 55 | }, 56 | "setIdx": 0, 57 | "iconIdx": 1 58 | }, 59 | { 60 | "icon": { 61 | "paths": [ 62 | "M368.185 848.213c-53.134 53.020-139.15 53.020-192.341-0.114-53.020-53.248-52.964-138.98-0.057-192.228l192.398-192.341c17.579-17.408 38.684-28.501 60.871-34.475 45.397-12.117 95.516-1.024 131.356 34.588 13.369 13.54 15.189 17.351 21.561 30.777 5.12 10.638 111.957-84.651 106.553-92.046-10.923-15.076-15.986-19.115-31.915-35.044-35.1-34.987-77.369-58.197-122.027-70.087-69.803-18.603-145.067-8.988-208.668 28.672-19.115 11.321-37.376 24.974-53.931 41.472l-192.341 192.284c-106.212 106.325-106.268 278.528-0.057 384.683 106.212 106.212 278.414 106.212 384.683 0l47.388-47.787c-31.915-31.915-54.556-55.125-95.687-96.199l-47.787 47.844z", 63 | "M944.469 79.701c-106.325-106.155-278.528-106.268-384.683 0l-63.772 63.716c28.217 26.169 73.444 75.036 96.199 96.199l63.659-63.716c53.077-53.191 139.036-52.964 192.284 0 53.077 53.305 53.020 139.036-0.057 192.171l-109.739 121.458-82.489 70.827c-17.465 17.579-38.684 28.672-60.985 34.645-45.34 12.231-99.328-2.674-134.94-38.343-25.486 23.438-47.9 46.876-96.142 96.199 34.987 34.987 81.067 62.009 125.724 73.899 69.746 18.546 145.067 8.988 208.612-28.786 19.228-11.321 37.433-24.974 53.931-41.472l192.228-192.284c106.382-106.212 106.155-278.414 0.171-384.512z" 64 | ], 65 | "attrs": [ 66 | {}, 67 | {} 68 | ], 69 | "grid": 0, 70 | "tags": [ 71 | "link" 72 | ] 73 | }, 74 | "attrs": [ 75 | {}, 76 | {} 77 | ], 78 | "properties": { 79 | "order": 28, 80 | "id": 29, 81 | "prevSize": 32, 82 | "code": 58907, 83 | "name": "link", 84 | "ligatures": "" 85 | }, 86 | "setIdx": 0, 87 | "iconIdx": 2 88 | }, 89 | { 90 | "icon": { 91 | "paths": [ 92 | "M403.2 195.123v466.739h102.4v-466.688l130.714 130.662 72.448-72.397-253.44-253.389-0.973 0.922-0.922-0.973-253.389 253.389 72.397 72.448z", 93 | "M817.152 307.2v614.4h-716.8v-614.4h-102.4v716.8h921.6v-716.8z" 94 | ], 95 | "attrs": [ 96 | {}, 97 | {} 98 | ], 99 | "width": 922, 100 | "grid": 0, 101 | "tags": [ 102 | "share" 103 | ] 104 | }, 105 | "attrs": [ 106 | {}, 107 | {} 108 | ], 109 | "properties": { 110 | "order": 27, 111 | "id": 28, 112 | "prevSize": 32, 113 | "code": 58908, 114 | "name": "share", 115 | "ligatures": "" 116 | }, 117 | "setIdx": 0, 118 | "iconIdx": 3 119 | }, 120 | { 121 | "icon": { 122 | "paths": [ 123 | "M460.713 268.793l-105.749 105.597 197.286 197.286h-401.074v-571.677h-151.177v721.116h552.251l-196.833 197.135 105.673 105.749 375.373-375.751v-3.855z" 124 | ], 125 | "attrs": [ 126 | {} 127 | ], 128 | "width": 841, 129 | "grid": 0, 130 | "tags": [ 131 | "reply-arrow" 132 | ] 133 | }, 134 | "attrs": [ 135 | {} 136 | ], 137 | "properties": { 138 | "order": 24, 139 | "id": 27, 140 | "prevSize": 32, 141 | "code": 58904, 142 | "name": "reply-arrow", 143 | "ligatures": "" 144 | }, 145 | "setIdx": 0, 146 | "iconIdx": 4 147 | }, 148 | { 149 | "icon": { 150 | "paths": [ 151 | "M790.221 974.029c-8.704 0-17.408-2.15-25.293-6.349l-255.693-134.349-255.59 134.349c-7.987 4.198-16.794 6.349-25.395 6.349-11.264 0-22.528-3.584-32.051-10.547-16.794-12.083-25.19-32.768-21.709-53.248l48.845-284.57-206.848-201.626c-14.848-14.541-20.275-36.25-13.824-56.013 6.349-19.763 23.45-34.099 44.032-37.171l285.696-41.472 127.898-258.97c9.216-18.534 28.16-30.413 48.947-30.413s39.731 11.878 48.947 30.413l127.795 258.97 285.696 41.472c20.582 3.072 37.683 17.408 44.237 37.171 6.349 19.763 1.024 41.472-13.926 56.013l-206.848 201.626 48.845 284.57c3.584 20.48-4.915 41.165-21.709 53.35-9.421 6.861-20.787 10.445-32.051 10.445z" 152 | ], 153 | "attrs": [ 154 | {} 155 | ], 156 | "grid": 0, 157 | "tags": [ 158 | "star" 159 | ] 160 | }, 161 | "attrs": [ 162 | {} 163 | ], 164 | "properties": { 165 | "order": 25, 166 | "id": 26, 167 | "prevSize": 32, 168 | "code": 58905, 169 | "name": "star", 170 | "ligatures": "" 171 | }, 172 | "setIdx": 0, 173 | "iconIdx": 5 174 | }, 175 | { 176 | "icon": { 177 | "paths": [ 178 | "M1728 0h-1664c-35.328 0-64 28.672-64 64v896c0 35.392 28.672 64 64 64h1664c35.392 0 64-28.608 64-64v-896c0-35.328-28.608-64-64-64zM559.040 686.72h-104.896l-40.96-185.472h-1.28l-40.96 185.472h-104.896l-76.736-342.784h112.576l16 87.616c5.76 32 9.6 51.84 15.36 101.12h1.28c6.4-49.92 10.88-69.12 17.92-101.12l19.2-87.616h95.936l18.56 85.12c7.040 32.576 12.16 54.976 18.56 103.616h1.28c5.76-48.64 9.6-70.4 15.36-101.696l16-86.976h98.496l-76.8 342.72zM1035.84 686.72h-104.96l-40.896-185.472h-1.28l-40.96 185.472h-104.896l-76.8-342.848h112.576l16 87.616c5.76 32 9.6 51.84 15.36 101.12h1.28c6.4-49.92 10.88-69.12 17.92-101.12l19.2-87.616h96l18.496 85.12c7.040 32.576 12.16 54.976 18.56 103.616h1.344c5.76-48.64 9.536-70.4 15.36-101.696l15.936-86.976h98.496l-76.736 342.784zM1512.64 686.72h-104.96l-40.96-185.472h-1.28l-41.024 185.472h-104.896l-76.736-342.848h112.64l15.936 87.616c5.824 32 9.6 51.84 15.36 101.12h1.344c6.4-49.92 10.88-69.12 17.856-101.12l19.2-87.616h95.936l18.56 85.12c7.040 32.576 12.16 54.976 18.56 103.616h1.28c5.76-48.64 9.6-70.4 15.36-101.696l16-86.976h98.496l-76.672 342.784z" 179 | ], 180 | "attrs": [ 181 | {} 182 | ], 183 | "width": 1792, 184 | "grid": 0, 185 | "tags": [ 186 | "www" 187 | ] 188 | }, 189 | "attrs": [ 190 | {} 191 | ], 192 | "properties": { 193 | "order": 26, 194 | "id": 25, 195 | "prevSize": 32, 196 | "code": 58906, 197 | "name": "www", 198 | "ligatures": "" 199 | }, 200 | "setIdx": 0, 201 | "iconIdx": 6 202 | }, 203 | { 204 | "icon": { 205 | "paths": [ 206 | "M509.879 1023.342l102.327-102.181-337.189-337.189h895.269v-144.603h-895.195l336.75-337.115-102.254-102.254-509.586 509.879v3.657z" 207 | ], 208 | "attrs": [ 209 | {} 210 | ], 211 | "width": 1170, 212 | "grid": 0, 213 | "tags": [ 214 | "left-arrow" 215 | ] 216 | }, 217 | "attrs": [ 218 | {} 219 | ], 220 | "properties": { 221 | "order": 23, 222 | "id": 24, 223 | "prevSize": 32, 224 | "code": 58903, 225 | "name": "left-arrow", 226 | "ligatures": "" 227 | }, 228 | "setIdx": 0, 229 | "iconIdx": 7 230 | }, 231 | { 232 | "icon": { 233 | "paths": [ 234 | "M1024 0h-983.040c-22.651 0-40.96 18.35-40.96 40.96v734.74c0 22.651 18.309 40.96 40.96 40.96h123.453l1.27 207.34 289.26-207.34h569.057c22.651 0 40.96-18.309 40.96-40.96v-734.74c0-22.61-18.309-40.96-40.96-40.96zM655.36 450.15h-81.92v81.92h-81.92v-81.92h-81.92v-81.92h81.92v-81.92h81.92v81.92h81.92v81.92z" 235 | ], 236 | "attrs": [ 237 | {} 238 | ], 239 | "width": 1065, 240 | "grid": 0, 241 | "tags": [ 242 | "annotations-rollover" 243 | ] 244 | }, 245 | "attrs": [ 246 | {} 247 | ], 248 | "properties": { 249 | "order": 20, 250 | "id": 23, 251 | "prevSize": 32, 252 | "code": 58880, 253 | "name": "annotations-rollover", 254 | "ligatures": "" 255 | }, 256 | "setIdx": 0, 257 | "iconIdx": 8 258 | }, 259 | { 260 | "icon": { 261 | "paths": [ 262 | "M1024 40.96v734.74h-582.205l-235.643 168.96-0.819-128.205-0.246-40.714h-164.127v-734.781h983.040zM1024 0h-983.040c-22.651 0-40.96 18.35-40.96 40.96v734.74c0 22.651 18.309 40.96 40.96 40.96h123.453l1.27 207.34 289.26-207.34h569.057c22.651 0 40.96-18.309 40.96-40.96v-734.74c0-22.61-18.309-40.96-40.96-40.96v0z", 263 | "M655.36 368.23h-81.92v-81.92h-81.92v81.92h-81.92v81.92h81.92v81.92h81.92v-81.92h81.92z" 264 | ], 265 | "attrs": [ 266 | { 267 | "fill": "#E0E0E0" 268 | }, 269 | { 270 | "fill": "#168DD9" 271 | } 272 | ], 273 | "width": 1065, 274 | "grid": 0, 275 | "tags": [ 276 | "annotations" 277 | ] 278 | }, 279 | "attrs": [ 280 | { 281 | "fill": "#E0E0E0" 282 | }, 283 | { 284 | "fill": "#168DD9" 285 | } 286 | ], 287 | "properties": { 288 | "order": 2, 289 | "id": 22, 290 | "prevSize": 32, 291 | "code": 58881, 292 | "name": "annotations", 293 | "ligatures": "" 294 | }, 295 | "setIdx": 0, 296 | "iconIdx": 9 297 | }, 298 | { 299 | "icon": { 300 | "paths": [ 301 | "M1408 255.808c0-35.328-28.608-64-64-64-1.152 0-1.984 0.576-3.136 0.64h-380.864v-128.896c0 0 0 0 0 0 0-7.104-1.856-13.696-4.032-20.096-0.512-1.536-0.32-3.328-0.96-4.8-9.728-23.040-32.512-39.168-59.008-39.168h-384c-35.328 0-64 28.672-64 64 0 0 0 0 0 0v128.896h-384c-35.328 0-64 28.672-64 64v0 704c0 35.392 28.672 64 64 64h1280c35.392 0 64-28.608 64-64 0-0.128-0.064-0.256-0.064-0.384s0.064-0.064 0.064-0.192v-704zM358.528 386.24c0 35.328-28.672 64-64 64h-69.376c-35.328 0-64-28.672-64-64v-16c0-35.328 28.672-64 64-64h69.312c35.328 0 64 28.672 64 64v16zM714.496 863.808c-139.968 0-253.312-113.408-253.312-253.312s113.408-253.376 253.312-253.376 253.376 113.472 253.376 253.376c0 139.904-113.408 253.312-253.376 253.312z" 302 | ], 303 | "attrs": [ 304 | {} 305 | ], 306 | "width": 1408, 307 | "grid": 0, 308 | "tags": [ 309 | "camera" 310 | ] 311 | }, 312 | "attrs": [ 313 | {} 314 | ], 315 | "properties": { 316 | "order": 22, 317 | "id": 21, 318 | "prevSize": 32, 319 | "code": 58882, 320 | "name": "camera", 321 | "ligatures": "" 322 | }, 323 | "setIdx": 0, 324 | "iconIdx": 10 325 | }, 326 | { 327 | "icon": { 328 | "paths": [ 329 | "M1024 90.496l-90.496-90.496-421.504 421.504-421.568-421.568-90.496 90.496 421.568 421.568-421.504 421.504 90.496 90.496 421.504-421.504 421.44 421.44 90.496-90.496-421.44-421.44z" 330 | ], 331 | "attrs": [ 332 | {} 333 | ], 334 | "grid": 0, 335 | "tags": [ 336 | "close" 337 | ] 338 | }, 339 | "attrs": [ 340 | {} 341 | ], 342 | "properties": { 343 | "order": 3, 344 | "id": 20, 345 | "prevSize": 32, 346 | "code": 58883, 347 | "name": "close", 348 | "ligatures": "" 349 | }, 350 | "setIdx": 0, 351 | "iconIdx": 11 352 | }, 353 | { 354 | "icon": { 355 | "paths": [ 356 | "M1170.286 0h-1170.286v1024h1316.571v-1024h-146.286zM1051.941 146.286l-393.582 393.728-393.728-393.728h787.31zM146.286 877.714v-642.926l510.683 510.683 1.317-1.317 1.39 1.463 510.61-510.757v642.853h-1024z" 357 | ], 358 | "attrs": [ 359 | {} 360 | ], 361 | "width": 1317, 362 | "grid": 0, 363 | "tags": [ 364 | "daily-brief" 365 | ] 366 | }, 367 | "attrs": [ 368 | {} 369 | ], 370 | "properties": { 371 | "order": 4, 372 | "id": 19, 373 | "prevSize": 32, 374 | "code": 58884, 375 | "name": "daily-brief", 376 | "ligatures": "" 377 | }, 378 | "setIdx": 0, 379 | "iconIdx": 12 380 | }, 381 | { 382 | "icon": { 383 | "paths": [ 384 | "M895.68 570.816l-90.496-90.56-293.184 293.248v-773.504h-128v773.44l-293.184-293.184-90.496 90.496 446.080 446.080 1.536-1.6 1.664 1.664z" 385 | ], 386 | "attrs": [ 387 | {} 388 | ], 389 | "width": 896, 390 | "grid": 0, 391 | "tags": [ 392 | "down-arrow" 393 | ] 394 | }, 395 | "attrs": [ 396 | {} 397 | ], 398 | "properties": { 399 | "order": 19, 400 | "id": 17, 401 | "prevSize": 32, 402 | "code": 58885, 403 | "name": "down-arrow", 404 | "ligatures": "" 405 | }, 406 | "setIdx": 0, 407 | "iconIdx": 14 408 | }, 409 | { 410 | "icon": { 411 | "paths": [ 412 | "M960 0h-896c-35.392 0-64 28.672-64 64v61.12l512 512 512-512v-61.12c0-35.328-28.672-64-64-64z", 413 | "M1024 774.4v-513.472l-256.768 256.704z", 414 | "M545.92 738.944c-4.672 4.672-10.112 8.192-15.936 10.56-17.344 6.976-37.952 3.52-51.968-10.496v0-0.064l-153.408-153.472-324.608 324.672v49.856c0 35.392 28.608 64 64 64h896c35.328 0 64-28.608 64-64v-49.856l-324.608-324.608-153.472 153.408z", 415 | "M0 260.928v513.472l256.768-256.768z" 416 | ], 417 | "attrs": [ 418 | {}, 419 | {}, 420 | {}, 421 | {} 422 | ], 423 | "grid": 0, 424 | "tags": [ 425 | "email" 426 | ] 427 | }, 428 | "attrs": [ 429 | {}, 430 | {}, 431 | {}, 432 | {} 433 | ], 434 | "properties": { 435 | "order": 1, 436 | "id": 16, 437 | "prevSize": 32, 438 | "code": 58886, 439 | "name": "email", 440 | "ligatures": "" 441 | }, 442 | "setIdx": 0, 443 | "iconIdx": 15 444 | }, 445 | { 446 | "icon": { 447 | "paths": [ 448 | "M967.36 0.128h-910.784c-31.168 0-56.448 25.28-56.448 56.512v910.72c0 31.232 25.344 56.512 56.448 56.512h490.304v-396.48h-133.376v-154.432h133.376v-113.984c0-132.16 80.704-204.224 198.72-204.224 56.512 0 105.152 4.224 119.232 6.144v138.176h-81.856c-64.128 0-76.48 30.464-76.48 75.264v98.624h153.024l-19.968 154.432h-132.992v396.48h260.864c31.104 0 56.512-25.28 56.512-56.512v-910.72c-0.064-31.232-25.408-56.512-56.576-56.512z" 449 | ], 450 | "attrs": [ 451 | {} 452 | ], 453 | "grid": 0, 454 | "tags": [ 455 | "facebook" 456 | ] 457 | }, 458 | "attrs": [ 459 | {} 460 | ], 461 | "properties": { 462 | "order": 17, 463 | "id": 14, 464 | "prevSize": 32, 465 | "code": 58888, 466 | "name": "facebook", 467 | "ligatures": "" 468 | }, 469 | "setIdx": 0, 470 | "iconIdx": 17 471 | }, 472 | { 473 | "icon": { 474 | "paths": [ 475 | "M336.192 614.656c-82.048-0.96-155.584 49.92-155.584 111.168 0 62.4 59.264 114.304 141.376 114.304 115.264 0 155.52-48.832 155.52-111.168 0-7.488-0.896-14.848-2.624-22.016-9.024-35.328-40.96-52.8-85.504-83.84-16.192-5.184-33.984-8.256-53.184-8.448z", 476 | "M309.12 214.272c-55.104-1.6-92.032 53.76-82.56 126.080 9.536 72.32 61.888 132.288 116.928 133.952s92.032-55.616 82.496-128c-9.472-72.32-61.888-130.432-116.864-132.032z", 477 | "M960 0h-896c-35.328 0-64 28.672-64 64v896c0 35.392 28.672 64 64 64h896c35.392 0 64-28.608 64-64v-896c0-35.328-28.608-64-64-64zM502.848 348.096c0 45.184-25.024 78.016-60.352 105.6-34.496 27.008-41.088 38.336-41.088 61.184 0 19.52 36.928 52.8 56.32 66.496 56.512 39.936 74.752 76.992 74.752 138.944 0 77.184-74.752 153.984-210.112 153.984-118.72 0-218.88-48.32-218.88-125.568 0-78.4 91.392-153.984 210.112-153.984 12.864 0 24.768-0.32 36.992-0.32-16.256-15.808-29.056-35.2-29.056-59.072 0-14.144 4.544-27.84 10.816-39.936-6.4 0.448-12.992 0.576-19.712 0.576-97.536 0-162.88-69.376-162.88-155.264 0-84.096 90.176-156.736 185.984-156.736 53.504 0 213.824 0 213.824 0l-47.808 46.656h-67.776c44.864 0 68.864 63.68 68.864 117.44zM920.512 370.752h-140.032v140.032h-46.656v-140.032h-140.096v-46.72h140.032v-140.032h46.656v140.032h140.032v46.72z" 478 | ], 479 | "attrs": [ 480 | {}, 481 | {}, 482 | {} 483 | ], 484 | "grid": 0, 485 | "tags": [ 486 | "google-plus" 487 | ] 488 | }, 489 | "attrs": [ 490 | {}, 491 | {}, 492 | {} 493 | ], 494 | "properties": { 495 | "order": 16, 496 | "id": 13, 497 | "prevSize": 32, 498 | "code": 58889, 499 | "name": "google-plus", 500 | "ligatures": "" 501 | }, 502 | "setIdx": 0, 503 | "iconIdx": 18 504 | }, 505 | { 506 | "icon": { 507 | "paths": [ 508 | "M-0 877.714h1316.571v146.286h-1316.571v-146.286z", 509 | "M-0 438.857h1316.571v146.286h-1316.571v-146.286z", 510 | "M2328.43 512.512l-361.984-361.984-103.57 103.424 260.023 259.803-260.023 259.95 103.424 103.497 362.057-362.057-1.317-1.317z", 511 | "M-0-0h1316.571v146.286h-1316.571v-146.286z" 512 | ], 513 | "attrs": [ 514 | {}, 515 | {}, 516 | { 517 | "opacity": 0.7 518 | }, 519 | {} 520 | ], 521 | "width": 2328, 522 | "grid": 0, 523 | "tags": [ 524 | "hamburrow" 525 | ] 526 | }, 527 | "attrs": [ 528 | {}, 529 | {}, 530 | { 531 | "opacity": 0.7 532 | }, 533 | {} 534 | ], 535 | "properties": { 536 | "order": 5, 537 | "id": 12, 538 | "prevSize": 32, 539 | "code": 58890, 540 | "name": "hamburrow", 541 | "ligatures": "" 542 | }, 543 | "setIdx": 0, 544 | "iconIdx": 19 545 | }, 546 | { 547 | "icon": { 548 | "paths": [ 549 | "M949.888-1.664h-873.472c-41.728 0-75.648 33.088-75.648 73.856v877.184c0 40.896 33.92 73.984 75.648 73.984h873.472c41.792 0 75.904-33.152 75.904-73.984v-877.184c-0.064-40.768-34.112-73.856-75.904-73.856zM274.688 871.872h-152.064v-489.216h152.128v489.216zM198.656 315.776c-48.768 0-88.128-39.488-88.128-88.192 0-48.576 39.36-88.064 88.128-88.064 48.576 0 88.064 39.488 88.064 88.064 0 48.704-39.488 88.192-88.064 88.192zM844.032 871.872h-151.808v-237.888c0-56.832-1.152-129.728-79.040-129.728-79.104 0-91.136 61.76-91.136 125.632v241.984h-152v-489.216h145.856v66.816h2.048c20.352-38.528 69.952-79.040 143.872-79.040 153.792 0 182.272 101.312 182.272 233.088v268.352z" 550 | ], 551 | "attrs": [ 552 | {} 553 | ], 554 | "grid": 0, 555 | "tags": [ 556 | "linkedin" 557 | ] 558 | }, 559 | "attrs": [ 560 | {} 561 | ], 562 | "properties": { 563 | "order": 6, 564 | "id": 11, 565 | "prevSize": 32, 566 | "code": 58891, 567 | "name": "linkedin", 568 | "ligatures": "" 569 | }, 570 | "setIdx": 0, 571 | "iconIdx": 20 572 | }, 573 | { 574 | "icon": { 575 | "paths": [ 576 | "M512 0c-282.795 0-512 229.205-512 512s229.205 512 512 512 512-229.205 512-512-229.205-512-512-512zM456.761 731.25h-113.778v-440.434h113.778v440.434zM697.060 731.25h-113.778v-440.434h113.778v440.434z" 577 | ], 578 | "attrs": [ 579 | {} 580 | ], 581 | "grid": 0, 582 | "tags": [ 583 | "pause" 584 | ] 585 | }, 586 | "attrs": [ 587 | {} 588 | ], 589 | "properties": { 590 | "order": 7, 591 | "id": 10, 592 | "prevSize": 32, 593 | "code": 58892, 594 | "name": "pause", 595 | "ligatures": "" 596 | }, 597 | "setIdx": 0, 598 | "iconIdx": 21 599 | }, 600 | { 601 | "icon": { 602 | "paths": [ 603 | "M960 0h-896c-35.392 0-64 28.608-64 64v896c0 35.328 28.672 64 64 64h237.76c-4.48-39.936-9.28-111.68 5.504-173.888 10.24-43.648 68.992-292.416 68.992-292.416s-17.152-34.304-17.152-84.864c0-79.552 46.080-138.944 103.488-138.944 48.768 0 72.384 36.608 72.384 80.576 0 49.152-31.232 122.432-47.36 190.464-13.504 56.96 28.48 103.36 84.608 103.36 101.76 0 170.176-130.624 170.176-285.312 0-117.568-79.168-205.696-223.296-205.696-162.752 0-264.192 121.408-264.192 257.024 0 46.72 13.824 79.744 35.456 105.344 9.856 11.712 11.328 16.448 7.68 29.824-2.624 9.984-8.448 33.728-10.88 43.136-3.648 13.632-14.592 18.432-26.88 13.44-75.008-30.72-109.952-112.768-109.952-205.184 0-152.384 128.576-335.232 383.552-335.232 204.928 0 339.776 148.288 339.776 307.456 0 210.496-116.992 367.744-289.472 367.744-57.984 0-112.512-31.296-131.2-66.88 0 0-31.040 123.648-37.696 147.584-14.976 54.592-48.768 108.8-72.32 142.464h617.024c35.328 0 64-28.672 64-64v-896c0-35.392-28.672-64-64-64z" 604 | ], 605 | "attrs": [ 606 | {} 607 | ], 608 | "grid": 0, 609 | "tags": [ 610 | "pinterest" 611 | ] 612 | }, 613 | "attrs": [ 614 | {} 615 | ], 616 | "properties": { 617 | "order": 8, 618 | "id": 9, 619 | "prevSize": 32, 620 | "code": 58893, 621 | "name": "pinterest", 622 | "ligatures": "" 623 | }, 624 | "setIdx": 0, 625 | "iconIdx": 22 626 | }, 627 | { 628 | "icon": { 629 | "paths": [ 630 | "M512 0c-282.795 0-512 229.205-512 512s229.205 512 512 512 512-229.205 512-512-229.205-512-512-512zM394.069 743.253v-462.507l344.974 231.253-344.974 231.253z" 631 | ], 632 | "attrs": [ 633 | {} 634 | ], 635 | "grid": 0, 636 | "tags": [ 637 | "play" 638 | ] 639 | }, 640 | "attrs": [ 641 | {} 642 | ], 643 | "properties": { 644 | "order": 9, 645 | "id": 8, 646 | "prevSize": 32, 647 | "code": 58894, 648 | "name": "play", 649 | "ligatures": "" 650 | }, 651 | "setIdx": 0, 652 | "iconIdx": 23 653 | }, 654 | { 655 | "icon": { 656 | "paths": [ 657 | "M899.541 792.932c60.79-82.507 96.82-184.427 96.82-294.765 0-274.556-223.638-498.167-498.167-498.167-274.776 0-498.194 223.61-498.194 498.167 0 274.776 223.418 498.194 498.194 498.194 107.98 0 208.118-34.494 289.829-93.283l124.843 124.871 110.914-110.749-124.24-124.267zM786.872 680.263l-184.070-184.070-110.749 110.914 183.111 183.083c-51.742 31.396-112.312 49.411-176.941 49.411-188.348 0-341.434-153.086-341.434-341.406 0-188.129 153.086-341.379 341.434-341.379 188.074 0 341.352 153.223 341.352 341.379 0 66.795-19.386 129.313-52.701 182.069z", 658 | "M5492.555 24.678v158.378h-296.273v792.054h-160.434v-792.054h-296.218v-158.378z", 659 | "M3015.816 24.678h-192.626l-366.276 950.432h168.167l68.605-185.167h451.745l68.55 185.167h168.167l-366.331-950.432zM2754.778 641.82l164.849-429.013 164.63 429.013h-329.479z", 660 | "M6578.689 183.056l-531.263 631.565h531.263v160.489h-740.532v-168.88l533.182-623.174h-496.192v-156.788h703.542z", 661 | "M2146.301 24.678v601.266c0 203.84-165.644 369.622-369.43 369.622-203.84 0-369.622-165.809-369.622-369.622v-601.266h160.297v601.266c-3.537 117.385 91.912 212.861 209.352 212.861 117.166 0 212.615-95.449 210.311-212.861v-601.266h159.091z", 662 | "M4218.321 653.199c108.995-54.292 183.632-166.193 183.632-295.176 0-174.227-143.626-329.835-322.514-333.345h-351.963v950.405h160.517v-286.21h145.353l330.795 335.127 111.49-110.174-257.309-260.627zM4067.785 532.058h-179.793v-348.042h179.793c97.835 0 177.435 78.010 177.435 174.035 0 96.052-79.6 174.007-177.435 174.007z" 663 | ], 664 | "attrs": [ 665 | {}, 666 | {}, 667 | {}, 668 | {}, 669 | {}, 670 | {} 671 | ], 672 | "width": 6579, 673 | "grid": 0, 674 | "tags": [ 675 | "quartz" 676 | ] 677 | }, 678 | "attrs": [ 679 | {}, 680 | {}, 681 | {}, 682 | {}, 683 | {}, 684 | {} 685 | ], 686 | "properties": { 687 | "order": 10, 688 | "id": 7, 689 | "prevSize": 32, 690 | "code": 58895, 691 | "name": "quartz", 692 | "ligatures": "" 693 | }, 694 | "setIdx": 0, 695 | "iconIdx": 24 696 | }, 697 | { 698 | "icon": { 699 | "paths": [ 700 | "M1023.699 938.852l-282.782-267.727c55.581-70.575 89.002-159.396 89.002-256.166 0-229.128-185.771-414.899-414.959-414.899s-414.959 185.711-414.959 414.839c0 229.128 185.771 414.959 414.959 414.959 88.941 0 171.078-28.242 238.642-75.874l284.95 269.955 85.148-85.087zM414.959 703.582c-159.155 0-288.623-129.528-288.623-288.623 0-159.215 129.468-288.623 288.623-288.623s288.623 129.408 288.623 288.623c-0 159.095-129.528 288.623-288.623 288.623z" 701 | ], 702 | "attrs": [ 703 | {} 704 | ], 705 | "grid": 0, 706 | "tags": [ 707 | "search" 708 | ] 709 | }, 710 | "attrs": [ 711 | {} 712 | ], 713 | "properties": { 714 | "order": 12, 715 | "id": 6, 716 | "prevSize": 32, 717 | "code": 58896, 718 | "name": "search", 719 | "ligatures": "" 720 | }, 721 | "setIdx": 0, 722 | "iconIdx": 25 723 | }, 724 | { 725 | "icon": { 726 | "paths": [ 727 | "M1024 608v-192h-173.568c-7.488-26.56-17.92-51.904-31.104-75.52l122.624-122.624-135.744-135.808-122.688 122.624c-23.616-13.184-48.96-23.616-75.52-31.104v-173.568h-192v173.632c-26.56 7.488-51.904 17.92-75.52 31.104l-122.624-122.624-135.744 135.744 122.688 122.688c-13.248 23.552-23.616 48.896-31.104 75.456h-173.696v192h173.696c7.488 26.56 17.92 51.904 31.104 75.52l-122.688 122.624 135.744 135.744 122.688-122.624c23.552 13.184 48.896 23.616 75.456 31.104v173.632h192v-173.632c26.56-7.488 51.968-17.856 75.584-31.104l122.688 122.688 135.744-135.744-122.688-122.688c13.184-23.616 23.616-48.96 31.104-75.52h173.568zM512 704c-106.048 0-192-85.952-192-192 0-105.984 85.952-192 192-192s192 85.952 192 192c0 106.048-85.952 192-192 192z" 728 | ], 729 | "attrs": [ 730 | {} 731 | ], 732 | "grid": 0, 733 | "tags": [ 734 | "settings" 735 | ] 736 | }, 737 | "attrs": [ 738 | {} 739 | ], 740 | "properties": { 741 | "order": 13, 742 | "id": 5, 743 | "prevSize": 32, 744 | "code": 58897, 745 | "name": "settings", 746 | "ligatures": "" 747 | }, 748 | "setIdx": 0, 749 | "iconIdx": 26 750 | }, 751 | { 752 | "icon": { 753 | "paths": [ 754 | "M1181.538 0h-1024c-86.961 0-157.538 70.577-157.538 157.538v708.923c0 87.040 70.577 157.538 157.538 157.538h1024c86.961 0 157.538-70.498 157.538-157.538v-708.923c0-86.961-70.577-157.538-157.538-157.538zM1045.11 567.69l-263.798 263.719c-30.878 30.799-80.581 30.956-111.38 0-30.799-30.799-30.799-80.502 0-111.38l130.442-130.442h-452.293c-43.559 0-78.769-35.21-78.769-78.769 0-43.481 35.21-78.769 78.769-78.769h449.851l-128-128.079c-30.799-30.799-30.878-80.581 0-111.38 30.799-30.799 80.502-30.799 111.38 0l263.798 263.719c15.281 15.36 23.079 35.525 23.079 55.69s-7.798 40.33-23.079 55.69z" 755 | ], 756 | "attrs": [ 757 | {} 758 | ], 759 | "width": 1339, 760 | "grid": 0, 761 | "tags": [ 762 | "sponsored" 763 | ] 764 | }, 765 | "attrs": [ 766 | {} 767 | ], 768 | "properties": { 769 | "order": 14, 770 | "id": 3, 771 | "prevSize": 32, 772 | "code": 58899, 773 | "name": "sponsored", 774 | "ligatures": "" 775 | }, 776 | "setIdx": 0, 777 | "iconIdx": 27 778 | }, 779 | { 780 | "icon": { 781 | "paths": [ 782 | "M1023.488 195.008c-37.632 16.64-78.016 27.968-120.448 33.024 43.264-25.984 76.544-67.008 92.224-116.032-40.576 24.064-85.568 41.472-133.184 50.88-38.336-40.704-92.8-66.24-153.088-66.24-115.84 0-209.728 93.888-209.728 209.728 0 16.384 1.792 32.448 5.376 47.744-174.208-8.704-328.704-92.16-432.256-219.072-17.92 30.976-28.352 67.008-28.352 105.472 0 72.704 37.184 136.896 93.44 174.528-34.496-1.088-66.816-10.56-95.040-26.24-0.064 0.832-0.064 1.792-0.064 2.624 0 101.568 72.32 186.368 168.256 205.696-17.6 4.736-36.16 7.296-55.296 7.296-13.504 0-26.688-1.344-39.424-3.84 26.688 83.392 104.128 143.936 195.904 145.664-71.744 56.256-162.24 89.728-260.48 89.728-16.896 0-33.536-1.024-49.984-2.944 92.864 59.584 203.072 94.208 321.408 94.208 385.728 0 596.672-319.488 596.672-596.608 0-9.152-0.192-18.176-0.576-27.136 41.024-29.504 76.608-66.432 104.64-108.48z" 783 | ], 784 | "attrs": [ 785 | {} 786 | ], 787 | "grid": 0, 788 | "tags": [ 789 | "twitter" 790 | ] 791 | }, 792 | "attrs": [ 793 | {} 794 | ], 795 | "properties": { 796 | "order": 15, 797 | "id": 2, 798 | "prevSize": 32, 799 | "code": 58900, 800 | "name": "twitter", 801 | "ligatures": "" 802 | }, 803 | "setIdx": 0, 804 | "iconIdx": 28 805 | }, 806 | { 807 | "icon": { 808 | "paths": [ 809 | "M517.295 0c-275.516 0-498.845 223.33-498.845 498.845 0 94.247 26.143 182.4 71.568 257.532l-90.018 267.623 276.148-88.419c71.468 39.564 153.627 62.043 241.114 62.043 275.449 0 498.812-223.33 498.812-498.812s-223.33-498.812-498.779-498.812zM761.972 671.854c-10.191 28.541-60.112 56.082-82.525 58.114-22.446 2.065-22.446 18.35-146.733-30.572-124.287-48.989-202.782-176.439-208.876-184.598s-49.921-66.273-49.921-126.451 31.605-89.718 42.794-101.974c11.223-12.255 24.478-15.286 32.637-15.286 8.126 0 16.285 1.032 23.412 1.032 7.16 0 17.351-4.096 27.508 20.381s34.635 84.656 37.699 90.751 5.095 13.255 1.032 21.414c-4.096 8.193-6.128 13.255-12.222 20.415-6.128 7.127-12.855 15.952-18.35 21.414-6.128 6.094-12.489 12.722-5.362 24.944 7.127 12.255 31.671 52.252 68.038 84.689 46.691 41.662 86.088 54.584 98.31 60.645 12.222 6.128 19.349 5.095 26.509-3.031 7.127-8.159 30.539-35.701 38.698-47.923s16.285-10.224 27.508-6.161c11.223 4.096 71.335 33.669 83.557 39.764 12.222 6.161 20.381 9.192 23.445 14.32 3.064 5.062 3.064 29.54-7.16 58.114z" 810 | ], 811 | "attrs": [ 812 | {} 813 | ], 814 | "width": 1016, 815 | "grid": 0, 816 | "tags": [ 817 | "whats-app" 818 | ] 819 | }, 820 | "attrs": [ 821 | {} 822 | ], 823 | "properties": { 824 | "order": 21, 825 | "id": 1, 826 | "prevSize": 32, 827 | "code": 58901, 828 | "name": "whats-app", 829 | "ligatures": "" 830 | }, 831 | "setIdx": 0, 832 | "iconIdx": 29 833 | }, 834 | { 835 | "icon": { 836 | "paths": [ 837 | "M460.014 250.74h-102.385v102.385h-102.385v102.385h102.385v102.385h102.385v-102.385h102.385v-102.385h-102.385z", 838 | "M725.702 649.887c52.114-68.086 83.392-152.963 83.392-245.365 0-223.403-181.067-404.522-404.573-404.522-223.403 0-404.522 181.118-404.522 404.522s181.118 404.573 404.522 404.573c94.143 0 180.504-32.456 249.204-86.361l301.881 301.216 67.983-77.608-297.888-296.455zM404.522 706.761c-166.631 0-302.137-135.608-302.137-302.239 0-166.58 135.506-302.137 302.137-302.137s302.137 135.557 302.137 302.137c0.051 166.631-135.506 302.239-302.137 302.239z" 839 | ], 840 | "attrs": [ 841 | {}, 842 | {} 843 | ], 844 | "grid": 0, 845 | "tags": [ 846 | "zoom" 847 | ] 848 | }, 849 | "attrs": [ 850 | {}, 851 | {} 852 | ], 853 | "properties": { 854 | "order": 11, 855 | "id": 0, 856 | "prevSize": 32, 857 | "code": 58902, 858 | "name": "zoom", 859 | "ligatures": "" 860 | }, 861 | "setIdx": 0, 862 | "iconIdx": 30 863 | } 864 | ], 865 | "height": 1024, 866 | "metadata": { 867 | "name": "qz-icons" 868 | }, 869 | "preferences": { 870 | "fontPref": { 871 | "prefix": "icon-", 872 | "metadata": { 873 | "fontFamily": "qz-icons", 874 | "majorVersion": 1, 875 | "minorVersion": 0 876 | }, 877 | "showGlyphs": true, 878 | "metrics": { 879 | "emSize": 512, 880 | "baseline": 6.25, 881 | "whitespace": 50 882 | }, 883 | "resetPoint": 58880, 884 | "showQuickUse": true, 885 | "quickUsageToken": false, 886 | "showMetrics": true, 887 | "showMetadata": false 888 | }, 889 | "imagePref": { 890 | "color": 0, 891 | "height": 32, 892 | "columns": 16, 893 | "margin": 16, 894 | "png": false, 895 | "sprites": true 896 | }, 897 | "historySize": 100, 898 | "showCodes": true, 899 | "gridSize": 16, 900 | "showLiga": false, 901 | "showGrid": true, 902 | "showGlyphs": true, 903 | "showQuickUse": true, 904 | "search": "" 905 | } 906 | } -------------------------------------------------------------------------------- /public/fonts/qz-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/fonts/qz-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/qz-icons.ttf -------------------------------------------------------------------------------- /public/fonts/qz-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/fonts/qz-icons.woff -------------------------------------------------------------------------------- /public/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/ajax-loader.gif -------------------------------------------------------------------------------- /public/images/qz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 13 | 14 | 15 | 17 | 18 | 19 | 21 | 22 | 23 | 25 | 26 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/images/topo-gray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/topo-gray.jpg -------------------------------------------------------------------------------- /public/images/water-topo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/water-topo.ai -------------------------------------------------------------------------------- /public/images/water-topo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/water-topo.jpg -------------------------------------------------------------------------------- /public/images/white_21600x10800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/white_21600x10800.jpg -------------------------------------------------------------------------------- /public/images/white_7400x3700.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/white_7400x3700.jpg -------------------------------------------------------------------------------- /public/images/world.200401.3x7400x3700.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/world.200401.3x7400x3700.raw -------------------------------------------------------------------------------- /public/images/world.topo.bathy.200408.3x5400x2700.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/mapquery/a1bde1e397180b6657abfb44a2c15224a5415966/public/images/world.topo.bathy.200408.3x5400x2700.jpg -------------------------------------------------------------------------------- /public/javascripts/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 2 | var saveAs=saveAs||function(e){"use strict";if("undefined"==typeof navigator||!/MSIE [1-9]\./.test(navigator.userAgent)){var t=e.document,n=function(){return e.URL||e.webkitURL||e},o=t.createElementNS("http://www.w3.org/1999/xhtml","a"),r="download"in o,i=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},a=/Version\/[\d\.]+.*Safari/.test(navigator.userAgent),c=e.webkitRequestFileSystem,f=e.requestFileSystem||c||e.mozRequestFileSystem,u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},d="application/octet-stream",s=0,l=4e4,v=function(e){var t=function(){"string"==typeof e?n().revokeObjectURL(e):e.remove()};setTimeout(t,l)},p=function(e,t,n){t=[].concat(t);for(var o=t.length;o--;){var r=e["on"+t[o]];if("function"==typeof r)try{r.call(e,n||e)}catch(i){u(i)}}},w=function(e){return/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)?new Blob(["\ufeff",e],{type:e.type}):e},y=function(t,u,l){l||(t=w(t));var y,m,S,h=this,R=t.type,O=!1,g=function(){p(h,"writestart progress write writeend".split(" "))},b=function(){if(m&&a&&"undefined"!=typeof FileReader){var o=new FileReader;return o.onloadend=function(){var e=o.result;m.location.href="data:attachment/file"+e.slice(e.search(/[,;]/)),h.readyState=h.DONE,g()},o.readAsDataURL(t),void(h.readyState=h.INIT)}if((O||!y)&&(y=n().createObjectURL(t)),m)m.location.href=y;else{var r=e.open(y,"_blank");void 0===r&&a&&(e.location.href=y)}h.readyState=h.DONE,g(),v(y)},E=function(e){return function(){return h.readyState!==h.DONE?e.apply(this,arguments):void 0}},N={create:!0,exclusive:!1};return h.readyState=h.INIT,u||(u="download"),r?(y=n().createObjectURL(t),void setTimeout(function(){o.href=y,o.download=u,i(o),g(),v(y),h.readyState=h.DONE})):(e.chrome&&R&&R!==d&&(S=t.slice||t.webkitSlice,t=S.call(t,0,t.size,d),O=!0),c&&"download"!==u&&(u+=".download"),(R===d||c)&&(m=e),f?(s+=t.size,void f(e.TEMPORARY,s,E(function(e){e.root.getDirectory("saved",N,E(function(e){var n=function(){e.getFile(u,N,E(function(e){e.createWriter(E(function(n){n.onwriteend=function(t){m.location.href=e.toURL(),h.readyState=h.DONE,p(h,"writeend",t),v(e)},n.onerror=function(){var e=n.error;e.code!==e.ABORT_ERR&&b()},"writestart progress write abort".split(" ").forEach(function(e){n["on"+e]=h["on"+e]}),n.write(t),h.abort=function(){n.abort(),h.readyState=h.DONE},h.readyState=h.WRITING}),b)}),b)};e.getFile(u,{create:!1},E(function(e){e.remove(),n()}),E(function(e){e.code===e.NOT_FOUND_ERR?n():b()}))}),b)}),b)):void b())},m=y.prototype,S=function(e,t,n){return new y(e,t,n)};return"undefined"!=typeof navigator&&navigator.msSaveOrOpenBlob?function(e,t,n){return n||(e=w(e)),navigator.msSaveOrOpenBlob(e,t||"download")}:(m.abort=function(){var e=this;e.readyState=e.DONE,p(e,"abort")},m.readyState=m.INIT=0,m.WRITING=1,m.DONE=2,m.error=m.onwritestart=m.onprogress=m.onwrite=m.onabort=m.onerror=m.onwriteend=null,S)}}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||this.content);"undefined"!=typeof module&&module.exports?module.exports.saveAs=saveAs:"undefined"!=typeof define&&null!==define&&null!==define.amd&&define([],function(){return saveAs}); -------------------------------------------------------------------------------- /public/javascripts/examples.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Three ways you can use Mapquery data 3 | * 4 | * The Mapquery search page allows you to output data in several ways. 5 | * You can save an SVG, save raw map data, or use the API call Mapquery 6 | * generates based on your search parameters. Here's how you might use 7 | * each of those output options together with real-world data to make a graphic. 8 | * 9 | * To provide a full demo of each method, each section below is independent 10 | * from the others, so you'll notice a lot of repetition in each. 11 | * 12 | * Static files used: 13 | * data/cb_2014_us_county_20m.svg is topojson data exported from mapquery 14 | * data/cb_2014_us_county_20m.json is topojson data exported from mapquery 15 | * data/worker-gender.csv is data from the us census on worker gender by county 16 | */ 17 | 18 | // establish some globals 19 | var color = d3.scale.threshold() 20 | .domain([40,42,44,46,48,50]) 21 | .range(['#a3d3f0','#8cbad8','#74a2bf','#5d8ba9','#467492','#2f5e7c','#154866']), 22 | width = 940, 23 | height = 500; 24 | 25 | // 26 | // Example 1: Bind data to static SVG 27 | // 28 | 29 | function staticSvg() { 30 | // add static svg to #map-1 div 31 | d3.xml("data/cb_2014_us_county_20m.svg", "image/svg+xml", function(xml){ 32 | var mapContainer = d3.select("#map-1"); 33 | mapContainer.node().appendChild(xml.documentElement); 34 | var svg = d3.select("svg").attr("id","map-svg"); 35 | 36 | // load mappable data 37 | d3.csv("data/worker-gender.csv",function(error,data){ 38 | 39 | // format some fields in the census data 40 | data.forEach(function(d){ 41 | d.fips = d3.format("05d")(d.fips); 42 | d.femalePct = (+d.female_total/+d.pop_total)*100; 43 | }); 44 | 45 | // create a selector by the fips field in the census data 46 | // fips is a five-digit unique ID for each county 47 | var dataByFips = d3.nest() 48 | .key(function(d){ return d.fips }) 49 | .map(data); 50 | 51 | // loop through SVG's path elements 52 | var countyPaths = svg.selectAll("path") 53 | .each(function(d){ 54 | 55 | // get the id from the path element, 56 | // which mapquery sets with a given dataset's 57 | // unique identifier. in this case, fips code. 58 | // note that mapquery prepends a letter to the id, 59 | // which you should remove to get the code 60 | var id = d3.select(this).attr("id").substr(1); 61 | 62 | // then look for matches between the fips code from the id 63 | // and the fips code in the data we've loaded 64 | // bind that datum when a match is found 65 | var datum = (typeof dataByFips[id] !== "undefined") ? dataByFips[id][0] : {}; 66 | d3.select(this).datum(datum); 67 | 68 | }) 69 | .style("fill",function(d){ 70 | // then use the bound datum to set the fill of each path 71 | if (dataByFips[d.fips]) return color(+dataByFips[d.fips][0].femalePct); 72 | }); 73 | }); 74 | }); 75 | } 76 | staticSvg(); 77 | 78 | // 79 | // Example 2: Load data from static topojson file 80 | // 81 | 82 | // we're using queue to load our two static data files 83 | queue() 84 | .defer(d3.json, "data/cb_2014_us_county_20m.json") 85 | .defer(d3.csv, "data/worker-gender.csv") 86 | .await(loadStaticData); 87 | 88 | function loadStaticData(err, mapData, workerData) { 89 | 90 | // append an svg 91 | var svg = d3.select("#map-2").append("svg:svg") 92 | .attr("id","map-2-svg") 93 | .attr("width", width) 94 | .attr("height", height); 95 | 96 | // format some fields in the census data 97 | workerData.forEach(function(d){ 98 | d.fips = d3.format("05d")(d.fips); 99 | d.femalePct = (+d.female_total/+d.pop_total)*100; 100 | }); 101 | 102 | // create a selector by the fips field in the census data 103 | // fips is a five-digit unique ID for each county 104 | var dataByFips = d3.nest() 105 | .key(function(d){ return d.fips }) 106 | .map(workerData); 107 | 108 | // get the name of the field in our map data 109 | // that is denoted as a unique identifier 110 | // in the metadata table. 111 | // in this case, we know it's fips, but this 112 | // is how we can get the field dynamically 113 | var uniqueFld = (mapData.table_metadata.fld_identifier) ? mapData.table_metadata.fld_identifier : "name"; 114 | 115 | // setup the projection using the handy 116 | // metadata mapquery provided 117 | var projection = d3.geo[mapData.projection]() 118 | .scale(mapData.scale) 119 | .translate(mapData.translate); 120 | 121 | var path = d3.geo.path() 122 | .projection(projection); 123 | 124 | // explicitly select the toposon map units. 125 | // for geojson it would be in data.map.features 126 | var units = mapData.map.objects.features; 127 | 128 | // append each county path 129 | svg.selectAll(".units-2") 130 | .data(units) 131 | .enter().append("path") 132 | .attr("class","units-2") 133 | .attr("id",function(d) { return "u"+d.properties[uniqueFld]; }) 134 | .attr("d", path) 135 | .style("fill",function(d){ 136 | // get the fips code, the unique id, from this path's properties 137 | var fips = d3.format("05d")(d.properties[uniqueFld]); 138 | // select the census data by the fips code 139 | // when there's a match, set a color based on 140 | // the percentage of female workers in that county 141 | if (dataByFips[fips]) return color(+dataByFips[fips][0].femalePct); 142 | }); 143 | 144 | } 145 | 146 | // 147 | // Example 3: Load data directly from the API 148 | // 149 | 150 | // we're using queue to load our two static data files. 151 | // cb_2014_us_county_20m.json is topojson data exported from mapquery 152 | // worker-gender.csv is data from the us census on worker gender by county 153 | queue() 154 | .defer(d3.json, "/api/feature-collection?table=cb_2014_us_county_20m&field_value=&proj=albersUsa&width=940&height=500&datatype=topojson") 155 | .defer(d3.csv, "data/worker-gender.csv") 156 | .await(loadApiData); 157 | 158 | function loadApiData(err, result, workerData) { 159 | 160 | // append an svg 161 | var svg = d3.select("#map-3").append("svg:svg") 162 | .attr("id","map-3-svg") 163 | .attr("width", width) 164 | .attr("height", height); 165 | 166 | // format some fields in the census data 167 | workerData.forEach(function(d){ 168 | d.fips = d3.format("05d")(d.fips); 169 | d.femalePct = (+d.female_total/+d.pop_total)*100; 170 | }); 171 | 172 | // create a selector by the fips field in the census data 173 | // fips is a five-digit unique ID for each county 174 | var dataByFips = d3.nest() 175 | .key(function(d){ return d.fips }) 176 | .map(workerData); 177 | 178 | // grap the map data from the API result 179 | var mapData = result.data; 180 | 181 | // get the name of the field in our map data 182 | // that is denoted as a unique identifier 183 | // in the metadata table. 184 | // in this case, we know it's fips, but this 185 | // is how we can get the field dynamically 186 | var uniqueFld = (mapData.table_metadata.fld_identifier) ? mapData.table_metadata.fld_identifier : "name"; 187 | 188 | // setup the projection using the handy 189 | // metadata mapquery provided 190 | var projection = d3.geo[mapData.projection]() 191 | .scale(mapData.scale) 192 | .translate(mapData.translate); 193 | 194 | var path = d3.geo.path() 195 | .projection(projection); 196 | 197 | // explicitly select the topojson map units. 198 | // for geojson it would be in data.map.features 199 | var units = mapData.map.objects.features; 200 | 201 | // append each county path 202 | svg.selectAll(".units-3") 203 | .data(units) 204 | .enter().append("path") 205 | .attr("class","units-3") 206 | .attr("id",function(d) { return "u"+d.properties[uniqueFld]; }) 207 | .attr("d", path) 208 | .style("fill",function(d){ 209 | // get the fips code, the unique id, from this path's properties 210 | var fips = d3.format("05d")(d.properties[uniqueFld]); 211 | // select the census data by the fips code 212 | // when there's a match, set a color based on 213 | // the percentage of female workers in that county 214 | if (dataByFips[fips]) return color(+dataByFips[fips][0].femalePct); 215 | }); 216 | 217 | } 218 | 219 | // 220 | // Below we just call a nifty/weird little function 221 | // that dynamically creates and appends a key 222 | // to represent an arbitrary scale. 223 | // 224 | // This is not part of the demo, but feel free 225 | // to try it out and let us know if you make improvements to it! 226 | // 227 | 228 | instaScale(color,600,"scale-1"); 229 | instaScale(color,600,"scale-2"); 230 | instaScale(color,600,"scale-3"); 231 | 232 | function instaScale(colorScale,w,div) { 233 | w = (w < 300) ? 300 : w; 234 | 235 | var svgScale = d3.select("#"+div).append("svg") 236 | .attr("width", w) 237 | .attr("height", 100); 238 | 239 | var scaleDomain = [+colorScale.domain()[0]-2,+colorScale.domain()[colorScale.domain().length-1]+2]; 240 | 241 | var x = d3.scale.linear() 242 | .domain(scaleDomain) 243 | .range([10, w-10]); 244 | 245 | svgScale.selectAll("."+div+"-rects") 246 | .data(colorScale.range().map(function(d, i) { 247 | return { 248 | x0: i ? x(colorScale.domain()[i - 1]) : x.range()[0], 249 | x1: i < colorScale.domain().length ? x(colorScale.domain()[i]) : x.range()[1], 250 | z: d 251 | }; 252 | })) 253 | .enter().append("rect") 254 | .attr("id",function(d,i){ return div+"-"+i; }) 255 | .attr("class",div+"-rects") 256 | .attr("width", function(d) { return d.x1 - d.x0-5; }) 257 | .attr("height", 17.5) 258 | .attr("x", function(d) { return d.x0; }) 259 | .attr("y",25) 260 | .style("fill", function(d) { return d.z; }); 261 | 262 | svgScale.append("text") 263 | .attr("x",10) 264 | .attr("y",15) 265 | .text("Share of female workers") 266 | .style("font-family","'AdelleSans-Bold', Helvetica, Arial, sans-serif"); 267 | 268 | svgScale.selectAll("."+div+"-labels") 269 | .data(colorScale.range().map(function(d, i) { 270 | return { 271 | x0: i ? x(colorScale.domain()[i - 1]) : x.range()[0], 272 | }; 273 | })) 274 | .enter().append("text") 275 | .attr("class",div+"-labels") 276 | .attr("x", function(d,i) { if (i == 0) { return d.x0-5; } else { return d.x0-10; } }) 277 | .attr("y",60) 278 | .text(function(d,i){ 279 | var l = Math.abs(colorScale.domain()[i-1]); 280 | if (i == 0) { 281 | return "38% or less"; 282 | } else if (i == colorScale.domain().length) { 283 | return l+"% or more"; 284 | } else { 285 | return l; 286 | } 287 | }) 288 | .style("font-family","'AdelleSans-Regular', 'Helvetica Neue', Arial, Helvetica, sans-serif") 289 | .style("font-size","14px"); 290 | } -------------------------------------------------------------------------------- /public/javascripts/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // this is the js for the mapquery search page. 3 | // to see examples for using mapquery, 4 | // check out public/javascripts/examples.js instead 5 | // 6 | 7 | var projections = [ 8 | "kavrayskiy7", 9 | "mercator", 10 | "azimuthalEqualArea", 11 | "azimuthalEquidistant", 12 | "albersUsa", 13 | "conicEqualArea", 14 | "conicConformal", 15 | "conicEquidistant", 16 | "equirectangular", 17 | "gnomonic", 18 | "orthographic", 19 | "stereographic", 20 | "transverseMercator" 21 | ]; 22 | 23 | var width = 940; 24 | var height = 500; 25 | 26 | var svg; 27 | 28 | // append select boxes 29 | var tableSelectEl = d3.select("#select1").append("select") 30 | .attr("id","table_select") 31 | .attr("name","table_select") 32 | .attr("class","searchable"); 33 | var tableUnitsEl = d3.select("#select2").append("select") 34 | .attr("id","table_units_select") 35 | .attr("name","table_units_select") 36 | .attr("class","searchable"); 37 | var projSelectEl = d3.select("#select3").append("select") 38 | .attr("id","projection_select") 39 | .attr("name","projection_select") 40 | .attr("class","searchable") 41 | .selectAll("option") 42 | .data(projections) 43 | .enter().append("option") 44 | .attr("value",function(d){ return d }) 45 | .html(function(d){ return d }); 46 | 47 | var detailsDiv = d3.select("#details"), 48 | detailLabels, 49 | detailChecks, 50 | detailSpan; 51 | 52 | var unitVal; 53 | 54 | var url; 55 | 56 | function loadData(table,field_value,proj,datatype) { 57 | var proj = proj || "kavrayskiy7"; 58 | unitVal = field_value.split(":")[1]; 59 | url = "/api/feature-collection?table="+table+"&field_value="+field_value+"&proj="+proj+"&width="+width+"&height="+height+"&datatype="+datatype; 60 | queue() 61 | .defer(d3.json, "/api/table-data") 62 | .defer(d3.json, url) 63 | .await(setData); 64 | } 65 | 66 | function setData(err, tableData, mapData) { 67 | 68 | var table = (mapData.data) ? mapData.data.table_metadata.table_name : "ne_50m_admin_0_countries"; 69 | 70 | var byCategory = d3.nest() 71 | .key(function(d){ return d.table_category }) 72 | .map(tableData.data); 73 | var keys = Object.keys(byCategory); 74 | 75 | // only load table select and detail checks first time 76 | if (tableSelectEl.selectAll(".table-og")[0].length == 0) { 77 | tableSelectEl.selectAll(".table-og") 78 | .data(keys) 79 | .enter().append("optgroup") 80 | .attr("label",function(key){ return key; }) 81 | .attr("class","table-og") 82 | .each(function(key){ 83 | d3.select(this).selectAll(".table-o") 84 | .data(byCategory[key]) 85 | .enter().append("option") 86 | .attr("class","table-o") 87 | .attr("value",function(d){ return d.table_name }) 88 | .attr("selected",function(d){ return (d.table_name == table) ? "selected" : null }) 89 | .html(function(d){ return d.table_name_readable+" ("+d.table_resolution+")" }) 90 | }); 91 | } 92 | 93 | // check boxes for detail select, currently unused 94 | detailLabels = detailsDiv.selectAll(".detail-label") 95 | .data(byCategory["Detail"]) 96 | .enter().append("label") 97 | .attr("class","detail-label"); 98 | 99 | detailChecks = detailLabels.append("input") 100 | .attr("type","checkbox") 101 | .attr("id",function(d){ return d.table_name }) 102 | .attr("value",1) 103 | .attr("class","detail-check") 104 | .attr("disabled","disabled"); 105 | 106 | detailSpan = detailLabels.append("span") 107 | .html(function(d){ return d.table_name_readable }); 108 | 109 | $(".searchable").select2(); 110 | 111 | var data = mapData.data; 112 | 113 | var uniqueFld = (data.table_metadata.fld_identifier) ? data.table_metadata.fld_identifier : "name"; 114 | 115 | var proj = (data.projection) ? data.projection : "kavrayskiy7"; 116 | 117 | var projection = d3.geo[proj]() 118 | .scale(data.scale) 119 | .translate(data.translate); 120 | 121 | var path = d3.geo.path() 122 | .projection(projection); 123 | 124 | var graticule = d3.geo.graticule(); 125 | 126 | var units = data.map.features; 127 | if ($("input:radio[name ='datatype']:checked").val() == "topojson") { 128 | units = data.map.objects.features; 129 | } 130 | 131 | svg = d3.select(".map-preview").html("").append("svg:svg") 132 | .attr("id","map-svg") 133 | .attr("width", width) 134 | .attr("height", height); 135 | 136 | if ($("#graticule").is(":checked")) { 137 | svg.append("path") 138 | .datum(graticule) 139 | .attr("class","graticule") 140 | .style("fill","none") 141 | .style("stroke","#ddd") 142 | .attr("d", path); 143 | } 144 | 145 | svg.selectAll(".units") 146 | .data(units) 147 | .enter().append("path") 148 | .attr("class","units") 149 | .attr("id",function(d) { return "u"+d.properties[uniqueFld]; }) 150 | .attr("d", path); 151 | 152 | 153 | d3.select("#urlstring").html(url) 154 | 155 | $("#table_select").on("change",function(){ 156 | var table = $(this).val(); 157 | populateTableUnits(table); 158 | }); 159 | 160 | d3.select("#get-map").on("click",function(){ 161 | var table = $("#table_select").val(); 162 | var field_value = $("#table_units_select").val(); 163 | var proj = $("#projection_select").val(); 164 | var datatype = $("input:radio[name ='datatype']:checked").val(); 165 | // collect checked details, not currently in use 166 | var detailArray = []; 167 | $(".detail-check").each(function(){ 168 | if($(this).is(":checked")) detailArray.push($(this).attr("id")) 169 | }); 170 | loadData(table,field_value,proj,datatype); 171 | }); 172 | 173 | d3.select("#download-svg").on("click",function() { 174 | var svgtext = document.getElementById("map").innerHTML; 175 | var regex = /p;){var u=a++,e=c[u],o=t.call(e,1);o.push(l(u)),++p,e[0].apply(null,o)}}function l(n){return function(u,t){--p,null==s&&(null!=u?(s=u,a=d=0/0,o()):(c[n]=t,--d?i||e():o()))}}function o(){null!=s?m(s):f?m(s,c):m.apply(null,[s].concat(c))}var r,i,f,c=[],a=0,p=0,d=0,s=null,m=u;return n||(n=1/0),r={defer:function(){return s||(c.push(arguments),++d,e()),r},await:function(n){return m=n,f=!1,d||o(),r},awaitAll:function(n){return m=n,f=!0,d||o(),r}}}function u(){}var t=[].slice;n.version="1.0.7","function"==typeof define&&define.amd?define(function(){return n}):"object"==typeof module&&module.exports?module.exports=n:this.queue=n}(); -------------------------------------------------------------------------------- /public/javascripts/topojson.min.js: -------------------------------------------------------------------------------- 1 | !function(){var m={version:"1.6.19",mesh:function(t){return q(t,l.apply(this,arguments))},meshArcs:l,merge:function(t){return q(t,o.apply(this,arguments))},mergeArcs:o,feature:s,neighbors:k,presimplify:d};function a(A,t){var u={},v={},w={},x=[],B=-1;t.forEach(function(F,C){var E=A.arcs[F<0?~F:F],D;if(E.length<3&&!E[1][0]&&!E[1][1]){D=t[++B],t[B]=F,t[C]=D}});t.forEach(function(F){var I=y(F),J=I[0],D=I[1],H,G;if(H=w[J]){delete w[H.end];H.push(F);H.end=D;if(G=v[D]){delete v[G.start];var C=G===H?H:H.concat(G);v[C.start=H.start]=w[C.end=G.end]=C}else{v[H.start]=w[H.end]=H}}else{if(H=v[D]){delete v[H.start];H.unshift(F);H.start=J;if(G=w[J]){delete w[G.end];var E=G===H?H:G.concat(H);v[E.start=G.start]=w[E.end=H.end]=E}else{v[H.start]=w[H.end]=H}}else{H=[F];v[H.start=J]=w[H.end=D]=H}}});function y(D){var C=A.arcs[D<0?~D:D],F=C[0],E;if(A.transform){E=[0,0],C.forEach(function(G){E[0]+=G[0],E[1]+=G[1]})}else{E=C[C.length-1]}return D<0?[E,F]:[F,E]}function z(D,F){for(var C in D){var E=D[C];delete F[E.start];delete E.start;delete E.end;E.forEach(function(G){u[G<0?~G:G]=1});x.push(E)}}z(w,v);z(v,w);t.forEach(function(C){if(!u[C<0?~C:C]){x.push([C])}});return x}function l(E,w,u){var t=[];if(arguments.length>1){var z=[],B;function v(H){var G=H<0?~H:H;(z[G]||(z[G]=[])).push({i:H,g:B})}function F(G){G.forEach(v)}function C(G){G.forEach(F)}function A(G){if(G.type==="GeometryCollection"){G.geometries.forEach(A)}else{if(G.type in D){B=G,D[G.type](G.arcs)}}}var D={LineString:F,MultiLineString:C,Polygon:C,MultiPolygon:function(G){G.forEach(C)}};A(w);z.forEach(arguments.length<3?function(G){t.push(G[0].i)}:function(G){if(u(G[0].g,G[G.length-1].g)){t.push(G[0].i)}})}else{for(var y=0,x=E.arcs.length;y0}t.forEach(function(C){if(!C._){var A=[],B=[C];C._=1;v.push(A);while(C=B.pop()){A.push(C);C.forEach(function(D){D.forEach(function(E){w[E<0?~E:E].forEach(function(F){if(!F._){F._=1;B.push(F)}})})})}}});t.forEach(function(A){delete A._});return{type:"MultiPolygon",arcs:v.map(function(A){var D=[];A.forEach(function(F){F.forEach(function(G){G.forEach(function(H){if(w[H<0?~H:H].length<2){D.push(H)}})})});D=a(z,D);if((n=D.length)>1){var E=x(A[0][0]);for(var C=0,B;C>>1;if(u[v]0){y=x[v],w(x[y._=0]=y,0)}return z};u.remove=function(A){var z=A._,y;if(x[z]!==A){return}if(z!==--v){y=x[v],(r(y,A)<0?t:w)(x[y._=z]=y,z)}return z};function t(z,A){while(A>0){var y=((A+1)>>1)-1,B=x[y];if(r(z,B)>=0){break}x[B._=A]=B;x[z._=A=y]=z}}function w(A,B){while(true){var C=(B+1)<<1,y=C-1,z=B,D=x[z];if(y.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} -------------------------------------------------------------------------------- /queries.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var execSync = require('child_process').execSync; 3 | var rimraf = require('rimraf'); 4 | var topojson = require('topojson'); 5 | var dbconn = require('./settings'); 6 | 7 | var promise = require('bluebird'); 8 | var options = {promiseLib: promise}; 9 | var pgp = require('pg-promise')(options); 10 | 11 | var getScaleTranslate = require('./util/getScaleTranslate'); 12 | var zip = require('./util/zip'); 13 | var pgToFc = require('./util/pgToFc'); 14 | 15 | var connectionString = "postgres://"+dbconn.u+":"+dbconn.p+"@"+dbconn.h+"/"+dbconn.d;; 16 | var db = pgp(connectionString); 17 | 18 | /** 19 | * Retrieve a FeatureCollection with sizing, positioning and projection data 20 | * 21 | * @param {Object} req 22 | * @param {Object} res 23 | * @param {Object} next 24 | * @returns {Object} - Metadata and map data in the specified format. See README.MD for response specification. 25 | */ 26 | function getFeatureCollection(req, res, next) { 27 | var table = req.query.table; 28 | var field_value = null; 29 | if (req.query.field_value) { 30 | if (req.query.field_value !== "*") { 31 | field_value = req.query.field_value.split(":"); 32 | } 33 | } 34 | var width = req.query.width; 35 | var height = req.query.height; 36 | var proj = req.query.proj; 37 | var datatype = req.query.datatype; 38 | var obj = {}; 39 | db.one("select * from mqmeta where (table_name = '"+table+"')") 40 | .then(function(data){ 41 | obj.table_metadata = data; 42 | var sql = "select *, ST_AsGeoJSON(geom, 6) as jsongeom from "+table; 43 | sql += (field_value) ? " where("+field_value[0]+" = '"+field_value[1]+"')" : ""; 44 | return db.any(sql); 45 | }) 46 | .then(function (data) { 47 | var features = pgToFc(data,obj.table_metadata.fld_identifier); 48 | var mapScaleTranslate = getScaleTranslate(features,width,height,proj); 49 | obj.bounds = mapScaleTranslate.bounds; 50 | obj.geoBounds = mapScaleTranslate.geoBounds; 51 | obj.scale = mapScaleTranslate.s; 52 | obj.translate = mapScaleTranslate.t; 53 | obj.projection = proj; 54 | obj.map = (datatype == "geojson") ? features : topojson.topology(features); 55 | obj.isos = []; 56 | if (obj.table_metadata.fld_iso_alpha_3) { 57 | features.features.forEach(function(d){ 58 | if (d.properties[obj.table_metadata.fld_iso_alpha_3]) 59 | obj.isos.push(d.properties[obj.table_metadata.fld_iso_alpha_3]) 60 | }); 61 | } 62 | 63 | // for bounding box search, not currently in use 64 | var envelope = "@ ST_MakeEnvelope("; 65 | envelope += mapScaleTranslate.geoBounds[0][0]+","; 66 | envelope += mapScaleTranslate.geoBounds[0][1]+","; 67 | envelope += mapScaleTranslate.geoBounds[1][0]+","; 68 | envelope += mapScaleTranslate.geoBounds[1][1]+",4269)"; 69 | var sql = "select * from ne_10m_railroads where geom "+envelope; 70 | 71 | res.status(200).json({ 72 | status: 'success', 73 | message: 'Retrieved FeatureCollection with projection data', 74 | data: obj 75 | }); 76 | }) 77 | .catch(function (err) { 78 | return next(err); 79 | }); 80 | } 81 | 82 | /** 83 | * Retrieve a raw geometry collection 84 | * 85 | * @param {Object} req 86 | * @param {Object} res 87 | * @param {Object} next 88 | * @returns {Object} - Map data in the specified format. See README.MD for response specification. 89 | */ 90 | function getGeometry(req, res, next) { 91 | var table = req.query.table; 92 | var field_value = null; 93 | var datatype = req.query.datatype; 94 | if (req.query.field_value) { 95 | if (req.query.field_value !== "*") { 96 | field_value = req.query.field_value.split(":"); 97 | } 98 | } 99 | var sql = "select *, ST_AsGeoJSON(geom, 6) as jsongeom from "+table; 100 | sql += (field_value) ? " where("+field_value[0]+" = '"+field_value[1]+"')" : ""; 101 | db.any(sql) 102 | .then(function (data) { 103 | var obj = {}; 104 | obj.type = "Topology"; 105 | obj.objects = []; 106 | var map = (datatype == "geojson") ? data : topojson.topology(data); 107 | map.objects.forEach(function(d){ 108 | obj.objects.push({ 109 | name: d.name, 110 | gid: d.gid, 111 | geometry: d.jsongeom 112 | }); 113 | }); 114 | res.status(200) 115 | .json({ 116 | status: 'success', 117 | message: 'Retrieved geometry', 118 | data: obj 119 | }); 120 | }) 121 | .catch(function (err) { 122 | return next(err); 123 | }); 124 | } 125 | 126 | /** 127 | * Retrieve mqmeta table data 128 | * 129 | * @param {Object} req 130 | * @param {Object} res 131 | * @param {Object} next 132 | * @returns {Object} - All mqmeta data. See README.MD for response specification. 133 | */ 134 | function getAllTableData(req, res, next) { 135 | db.any('select * from mqmeta') 136 | .then(function (data) { 137 | res.status(200).json({ 138 | data: data 139 | }); 140 | }) 141 | .catch(function (err) { 142 | return next(err); 143 | }); 144 | } 145 | 146 | /** 147 | * Retrieve unique values from a given table's name field, as well as any groupby fields specified in mqmeta 148 | * 149 | * @param {Object} req 150 | * @param {Object} res 151 | * @param {Object} next 152 | * @returns {Object} - Unique values in arrays. See README.MD for response specification. 153 | */ 154 | function getTableUnits(req, res, next) { 155 | var table = req.query.table; 156 | var fld_name; 157 | var fld_iso_alpha_3; 158 | var fld_groupby = []; 159 | var units = {}; 160 | db.one("select * from mqmeta where (table_name = '"+table+"')") 161 | .then(function (data) { 162 | fld_name = data.fld_name; 163 | for (var i=1; i<=5; i++) { 164 | if (data["fld_groupby"+i]) { 165 | fld_groupby.push(data["fld_groupby"+i]); 166 | units[data["fld_groupby"+i]] = []; 167 | } 168 | } 169 | units[fld_name] = []; 170 | return db.any("select * from "+table); 171 | }) 172 | .then(function (data) { 173 | data.forEach(function(d){ 174 | if (d[fld_name]) 175 | if (units[fld_name].indexOf(d[fld_name]) === -1) 176 | units[fld_name].push(d[fld_name]); 177 | fld_groupby.forEach(function(fld){ 178 | if (units[fld].indexOf(d[fld]) === -1) 179 | units[fld].push(d[fld]); 180 | }); 181 | }); 182 | res.status(200).json(units); 183 | }) 184 | .catch(function (err) { 185 | return next(err); 186 | }); 187 | } 188 | 189 | /** 190 | * Import shapefile to Postgres database, send information about imported data to client 191 | * 192 | * @param {Object} req 193 | * @param {Object} res 194 | * @param {Object} next 195 | * @returns {Object} - Information to render views/import.jade 196 | */ 197 | function importMapData(req,res,next) { 198 | var file = req.file; 199 | db.none("drop table if exists temp") 200 | .then(function(){ 201 | var path_to_shp = zip.extractZip(file) 202 | var cmd = 'shp2pgsql -W "latin1" -c -D -s 4269 -I '+path_to_shp+' public.temp | psql -h '+dbconn.h+' -p '+dbconn.port+' -d '+dbconn.d+' -U '+dbconn.u; 203 | // exec(cmd, function(err, stdout, stderr) {}); 204 | execSync(cmd); 205 | return db.any('select * from temp'); 206 | }) 207 | .then(function (data) { 208 | if (data) { 209 | // delete folder 210 | rimraf.sync("shapefile-imports/"+file.originalname.substr(0,file.originalname.length-4)); 211 | // rename zip file 212 | fs.rename(file.path,"shapefile-imports/"+file.originalname,function(err){}); 213 | } 214 | var objKeys = Object.keys(data[0]).sort(); 215 | var objVals = []; 216 | objKeys.forEach(function(key){ 217 | var str = new String(data[0][key]).substr(0,20); 218 | objVals.push(str); 219 | }); 220 | var result = {}; 221 | result.data = data[0]; 222 | result.keys = objKeys; 223 | result.vals = objVals; 224 | result.table_name = file.originalname.substr(0,file.originalname.length-4); 225 | result.flds = [ 226 | {fld:"fld_name",name:"Name"}, 227 | {fld:"fld_iso_alpha_3",name:"ISO Alpha 3"}, 228 | {fld:"fld_identifier",name:"Other unique identifier"}, 229 | {fld:"fld_groupby1",name:"Group by 1"}, 230 | {fld:"fld_groupby2",name:"Group by 2"}, 231 | {fld:"fld_groupby3",name:"Group by 3"}, 232 | {fld:"fld_groupby4",name:"Group by 4"}, 233 | {fld:"fld_groupby5",name:"Group by 5"} 234 | ]; 235 | res.render('import', { 236 | title: "Mapquery Import", 237 | message: 'Imported table '+result.table_name+' as `temp`', 238 | data: result 239 | }); 240 | }) 241 | .catch(function (err) { 242 | return next(err); 243 | }); 244 | } 245 | 246 | /** 247 | * Save metadata from client form to mqmeta 248 | * 249 | * @param {Object} req 250 | * @param {Object} res 251 | * @param {Object} next 252 | * @returns {Object} - Information to render views/import.jade 253 | */ 254 | function saveMapData(req, res, next) { 255 | var table_name = req.body.table_name; 256 | db.none("alter table temp rename to "+table_name) 257 | .then(function () { 258 | return db.none("alter index if exists temp_geom_idx rename to "+table_name+"_geom_idx") 259 | }) 260 | .then(function () { 261 | return db.none('insert into mqmeta(table_name,table_name_readable,table_description,table_category,table_resolution,table_source,table_source_url,fld_iso_alpha_3,fld_name,fld_groupby1,fld_groupby2,fld_groupby3,fld_groupby4,fld_groupby5,fld_identifier)' + 262 | 'values(${table_name},${table_name_readable},${table_description},${table_category},${table_resolution},${table_source},${table_source_url},${fld_iso_alpha_3},${fld_name},${fld_groupby1},${fld_groupby2},${fld_groupby3},${fld_groupby4},${fld_groupby5},${fld_identifier})', 263 | req.body) 264 | }) 265 | .then(function () { 266 | res.render('import', { 267 | title: "Mapquery Import", 268 | message: 'Created new table '+table_name+' and inserted table metadata to mqmeta', 269 | data: null 270 | }); 271 | 272 | }) 273 | .catch(function (err) { 274 | return next(err); 275 | }); 276 | } 277 | 278 | module.exports = { 279 | getFeatureCollection: getFeatureCollection, 280 | getGeometry: getGeometry, 281 | getTableUnits: getTableUnits, 282 | getAllTableData: getAllTableData, 283 | importMapData: importMapData, 284 | saveMapData: saveMapData 285 | }; -------------------------------------------------------------------------------- /routes/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var multer = require('multer'); 4 | var upload = multer({dest:'./shapefile-imports/'}); 5 | 6 | var db = require('../queries'); 7 | 8 | router.get("/", function(req, res) { 9 | res.render('index', { 10 | title: "Mapquery Search" 11 | }); 12 | }); 13 | 14 | router.get("/import", function(req, res) { 15 | res.render('import', { 16 | title: "Mapquery Import", 17 | data: null 18 | }); 19 | }); 20 | 21 | router.get("/examples", function(req, res) { 22 | res.render('examples',{ 23 | title: "Mapquery Examples" 24 | }) 25 | }); 26 | 27 | router.get('/api/feature-collection', db.getFeatureCollection); 28 | router.get('/api/geometry-collection', db.getGeometry); 29 | router.get('/api/table-data', db.getAllTableData); 30 | router.get('/api/units-by-table', db.getTableUnits); 31 | 32 | router.post("/import/import-map", upload.single("file-upload"), db.importMapData); 33 | router.post('/import/save-map-data', db.saveMapData); 34 | 35 | module.exports = router; -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'd': 'mapquery', 3 | 'u': '', 4 | 'p': '', 5 | 'h': 'localhost', 6 | 'port': '5432' 7 | }; -------------------------------------------------------------------------------- /shapefile-imports/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /util/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /util/getScaleTranslate.js: -------------------------------------------------------------------------------- 1 | var d3 = require('d3'); 2 | require('d3-geo-projection')(d3); 3 | 4 | function getScaleTranslate(data,width,height,proj) { 5 | 6 | var projection = d3.geo[proj]() 7 | .scale(1) 8 | .translate([0,0]); 9 | 10 | var path = d3.geo.path() 11 | .projection(projection); 12 | 13 | var geoBounds = d3.geo.bounds(data); 14 | var bounds = path.bounds(data), 15 | dx = bounds[1][0] - bounds[0][0], 16 | dy = bounds[1][1] - bounds[0][1], 17 | x = (bounds[0][0] + bounds[1][0]) / 2, 18 | y = (bounds[0][1] + bounds[1][1]) / 2, 19 | s = .9 / Math.max(dx / width, dy / height), 20 | t = [width / 2 - s * x, height / 2 - s * y]; 21 | 22 | var obj = {geoBounds: geoBounds, bounds: bounds, s: s, t:t}; 23 | 24 | return obj; 25 | } 26 | 27 | module.exports = getScaleTranslate; -------------------------------------------------------------------------------- /util/pgToFc.js: -------------------------------------------------------------------------------- 1 | function pgToFc(queryResult,fld_identifier) { 2 | var props = ["gid","name"], 3 | i = 0, 4 | length = queryResult.length, 5 | prop = null, 6 | geojson = { 7 | "type": "FeatureCollection", 8 | "features": [] 9 | }; 10 | 11 | if (props.indexOf(fld_identifier) === -1) { 12 | props.push(fld_identifier); 13 | } 14 | 15 | for(i = 0; i < length; i++) { 16 | var feature = { 17 | "type": "Feature", 18 | "properties": {}, 19 | "geometry": JSON.parse(queryResult[i].jsongeom) 20 | }; 21 | props.forEach(function(prop){ 22 | if (typeof queryResult[i][prop] !== "undefined") 23 | feature.properties[prop] = queryResult[i][prop]; 24 | }); 25 | geojson.features.push(feature); 26 | } 27 | return geojson; 28 | } 29 | 30 | module.exports = pgToFc; -------------------------------------------------------------------------------- /util/zip.js: -------------------------------------------------------------------------------- 1 | var AdmZip = require('adm-zip'); 2 | 3 | module.exports = { 4 | getExtension: function(filename) { 5 | return filename.split(".")[filename.split(".").length-1]; 6 | }, 7 | trimExtension: function(filename) { 8 | return filename.substr(0,filename.length-4); 9 | }, 10 | getFolderPath: function(file) { 11 | return file.destination+file.originalname.substr(0,file.originalname.length-4)+'/'; 12 | }, 13 | extractZip: function(file) { 14 | if (this.getExtension(file.originalname) !== "zip") 15 | console.log('Sorry, right now you can only upload .zip archives to Mapquery. Please zip your shapefile data and try again.',null); 16 | 17 | var s = this; 18 | var shp = null; 19 | var dbf = null; 20 | var filepath = this.getFolderPath(file); 21 | var zip = new AdmZip(file.destination+file.filename); 22 | 23 | zip.getEntries().forEach(function(zipEntry) { 24 | if (s.getExtension(zipEntry.entryName) == "shp") shp = zipEntry.entryName; 25 | if (s.getExtension(zipEntry.entryName) == "dbf") dbf = zipEntry.entryName; 26 | }); 27 | 28 | if (!shp || !dbf) 29 | console.log('The zip archive you\'re trying to upload is missing a .shp or .dbf or both.',null); 30 | 31 | zip.extractAllTo(filepath,true); 32 | return filepath+shp; 33 | } 34 | } -------------------------------------------------------------------------------- /views/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} -------------------------------------------------------------------------------- /views/error_message.jade: -------------------------------------------------------------------------------- 1 | div.error-message #{error} -------------------------------------------------------------------------------- /views/examples.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | div.examples-container 5 | 6 | h1 Three ways you can use Mapquery data 7 | 8 | p The Mapquery search page allows you to output data in several ways. You can save an SVG, save raw map data, or use the API call Mapquery generates based on your search parameters. Here's how you might use each of those output options together with real-world data to make a graphic. See /public/javascripts/examples.js for details. 9 | 10 | h2 Example 1: Bind data to a static SVG 11 | p In this example, we've exported a static SVG from the Mapquery search page, loaded the SVG file into an HTML document, then bound data on worker gender to each county path. 12 | div.map-preview#map-1 13 | div.example-scale#scale-1 14 | 15 | h2 Example 2: Load data from a static topojson file 16 | p In this example, we've saved a static topojson file from Mapquery Search to a json file in the public/data directory. 17 | div.map-preview#map-2 18 | div.example-scale#scale-2 19 | 20 | h2 Example 3: Load data directly from the API 21 | p In this example, we've gotten an API url with query parameters from the Mapquery search page, and are calling data directly from the API. 22 | div.map-preview#map-3 23 | div.example-scale#scale-3 -------------------------------------------------------------------------------- /views/import.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1 Import 5 | p Start by selecting shape data to import. We currently only support imports of data compressed in a .zip file, which must include .shp and .dbf files. The import may take up to a minute. 6 | form(action="/import/import-map", method="post", enctype="multipart/form-data") 7 | .control-row 8 | label.control-button(for="file-upload") Select file to import 9 | input.control-hidden(type="file", id="file-upload", name="file-upload") 10 | input.control-button(type="submit", value="Upload") 11 | 12 | if data 13 | form(action="/import/save-map-data", method="post") 14 | .control-row 15 | h2 Table metadata 16 | p Provide some information about the data you're importing. 17 | .meta-row 18 | .field Map category 19 | .field.select-wrap 20 | select(id="table_category",name="table_category") 21 | option(value="Countries") Countries 22 | option(value="States or provinces") States or Provinces 23 | option(value="Interstate units") Interstate units (e.g. counties) 24 | option(value="Detail") Detail (e.g. roads, water) 25 | option(value="Other") Other (e.g. disputed areas) 26 | .meta-row 27 | .field Name of table 28 | .field 29 | input(type="text",id="table_name",name="table_name",value=data.table_name) 30 | .meta-row 31 | .field Readable name 32 | .field 33 | input(type="text",id="table_name_readable",name="table_name_readable") 34 | .meta-row 35 | .field Description 36 | .field 37 | textarea(id="table_description",name="table_description") 38 | .meta-row 39 | .field Map resolution 40 | .field.select-wrap 41 | select(id="table_resolution",name="table_resolution") 42 | option(value="1:10m") 1:500,000 43 | option(value="1:10m") 1:10 million 44 | option(value="1:20m") 1:20 million 45 | option(value="1:50m") 1:50 million 46 | option(value="1:110m") 1:110 million 47 | 48 | .meta-row 49 | .field Data source 50 | .field 51 | input(type="text",id="table_source",name="table_source") 52 | .meta-row 53 | .field Data source URL 54 | .field 55 | input(type="text",id="table_source_url",name="table_source_url") 56 | input(type="hidden",id="table_shapefile_path",name="table_shapefile_path" value=staticFile) 57 | 58 | .control-row 59 | h2 Shapefile fields 60 | p Use the dropdowns below to map this shapefile's fields to our metadata table. The format of the options in the dropdowns is field \ sample value. 61 | 62 | each fld in data.flds 63 | .meta-row 64 | .field #{fld.name} 65 | .field.select-wrap 66 | select(id=fld.fld,name=fld.fld) 67 | option(value="") Select field 68 | each key, index in data.keys 69 | option(value=key) 70 | | #{key} \ #{data.vals[index]} 71 | .meta-row 72 | .field 73 | input.control-button(type="submit",value="Save") -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | div.map-preview#map 5 | div.search-container 6 | h1 Search Mapquery 7 | 8 | p Start by selecting the table you want to make a map with. 9 | #select1 10 | p Then select units from within that table 11 | #select2 12 | p Choose a projection that best displays your map 13 | #select3 14 | 15 | p Select details to include. (Roads, etc. disabled in prototype release.) 16 | #details 17 | 18 | 19 | br 20 | 21 | p Select data output type 22 | #options 23 | input(type="radio", name="datatype", value="geojson",checked="checked") 24 | span.radio-label Geojson 25 | input(type="radio", name="datatype", value="topojson") 26 | span.radio-label Topojson 27 | 28 | #controls 29 | center 30 | p 31 | .control-button#get-map Get Map 32 | 33 | h1 Output 34 | p Call this URL to load this map. Note that the width and height can be adjusted. 35 | .preformat#urlstring 36 | p Alternatively, this map can be downloaded 37 | #download-buttons 38 | .control-button#download-data Download Data 39 | .control-button#download-svg Download SVG 40 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel="stylesheet", href="/stylesheets/select2.min.css") 6 | link(rel='stylesheet', href='/stylesheets/fonts.css') 7 | link(rel='stylesheet', href='/stylesheets/index.css') 8 | body 9 | include nav 10 | div.mq-container 11 | if error 12 | include error_message 13 | block content 14 | script(src="/javascripts/d3.v3.min.js") 15 | script(src="/javascripts/topojson.min.js") 16 | script(src="/javascripts/d3.geo.projection.min.js") 17 | script(src="/javascripts/queue.min.js") 18 | script(src="/javascripts/jquery.min.js") 19 | script(src="/javascripts/select2.min.js") 20 | script(src="/javascripts/FileSaver.min.js") 21 | if (title == "Mapquery Search") 22 | script(src="/javascripts/index.js") 23 | if (title == "Mapquery Examples") 24 | script(src="/javascripts/examples.js") -------------------------------------------------------------------------------- /views/nav.jade: -------------------------------------------------------------------------------- 1 | header(id="header",class="dark-theme") 2 | div.left 3 | a(href="/" id="button-search",class="header-button") search 4 | a(href="/import" id="button-import",class="header-button") import 5 | a(href="/examples" id="button-examples",class="header-button") examples --------------------------------------------------------------------------------